From 0983105908692f3af5c3098a8e650d3170634e89 Mon Sep 17 00:00:00 2001 From: mStirner Date: Sat, 28 Jan 2023 17:26:40 +0100 Subject: [PATCH 01/30] demo instance added --- README.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f691935..c286d43 100644 --- a/README.md +++ b/README.md @@ -25,16 +25,12 @@ Fork this repository, apply the changes you want to make, and create a pull requ __*Note*__: If you want to contribute, please take a look on the "documentation" repository, section "[How to document the source code](https://github.com/OpenHausIO/documentation#how-to-document-the-source-code)". -## Docker quick start: -Build the docker image: -```sh -npm run build:docker-image -``` - -Start a container: -```sh -docker run --rm -it --net host --name backend --env=UUID=c04a9aa6-7261-11ed-8578-cb6ee612422e --env=VAULT_MASTER_PASSWORD=Pa$$w0rd --env=USERS_JWT_SECRET=Pa$$w0rd --env=DATABASE_HOST=172.17.0.1 --env=API_AUTH_ENABLED=false openhaus/backend -``` +## Demo +There exsits a public demo: http://demo.open-haus.io
+It is deployed with docker and runs the frontend & backend container.
+The instance is rested to its default values every 10 Minutes.
+No authentication required, full API support. + ## License Im currently not sure, under what license i publish this work.
From 23b1f6f34ac84327bdea7b98d52352658885d0bc Mon Sep 17 00:00:00 2001 From: mStirner Date: Sat, 28 Jan 2023 23:50:44 +0100 Subject: [PATCH 02/30] http api section added --- README.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c286d43..5d3640c 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,18 @@ The [docs](docs) folder is soon be removed. For the HTTP API we provide a [postman collection](postman.json). +## Demo +There exsits a public demo: http://demo.open-haus.io
+It is deployed with docker and runs the frontend & backend container.
+The instance is rested to its default values every 10 Minutes.
+No authentication required, full API support. + + +## HTTP API +We provide a [postmann collection](./postman.json) that you can import.
+It containes documentation about every URL endpoint and its meaning.
+Get postman on https://www.postman.com/ its great tool for HTTP testing & documentation. + ## Contribution If you have questions, want to contribute or just wanna have a talk, open a new issue. @@ -25,12 +37,6 @@ Fork this repository, apply the changes you want to make, and create a pull requ __*Note*__: If you want to contribute, please take a look on the "documentation" repository, section "[How to document the source code](https://github.com/OpenHausIO/documentation#how-to-document-the-source-code)". -## Demo -There exsits a public demo: http://demo.open-haus.io
-It is deployed with docker and runs the frontend & backend container.
-The instance is rested to its default values every 10 Minutes.
-No authentication required, full API support. - ## License Im currently not sure, under what license i publish this work.
From 81023062c7bfa174f9a0b18b2241fe489d618903 Mon Sep 17 00:00:00 2001 From: mStirner Date: Sun, 29 Jan 2023 17:48:22 +0100 Subject: [PATCH 03/30] fix #269, implement labels array --- system/component/class.component.js | 65 ++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/system/component/class.component.js b/system/component/class.component.js index 95f9f20..572aa4a 100644 --- a/system/component/class.component.js +++ b/system/component/class.component.js @@ -17,6 +17,10 @@ const PENDING_CHANGE_EVENTS = new Set(); * - remove * - find * + * Methods that are not hookable: + * - found + * - labels + * * @class COMPONENT * * @extends COMMON system/component/class.common.js @@ -24,6 +28,7 @@ const PENDING_CHANGE_EVENTS = new Set(); * @property {Array} items Store where instance of items are keept * @property {Object} collection MongoDB collection instance * @property {Object} schema Joi Object schema which is extend by a timestamp object: + * @property {Array} schema.labels Array that allow custom labels for identification * @property {Object} schema.timestamps Timestamps * @property {Number} schema.timestamps.created Set to `Date.now()` when a item is created/added * @property {Number} schema.timestamps.updated Set to `Date.now()` when a item is updated @@ -92,6 +97,9 @@ module.exports = class COMPONENT extends COMMON { this.schema = Joi.object({ ...schema, + //labels: Joi.array().items(Joi.string().regex(/^[a-zA-Z0-9]+=[a-zA-Z0-9]+$/)).default([]) + //labels: Joi.array().items(Joi.string().regex(/^[a-z0-9\.]+=[a-z0-9]+$/)).default([]), + labels: Joi.array().items(Joi.string().regex(/^[a-z0-9]+=[a-z0-9]+$/)).default([]), timestamps: Joi.object({ ...schema?.timestamps, created: Joi.number().allow(null).default(null), @@ -600,6 +608,8 @@ module.exports = class COMPONENT extends COMMON { * @param {Object} filter Object that matches the component schema * @param {Function} cb Callback function * @param {Function} [nothing] Function that is called when not matching item in array is found. Usefull to add then something, when its not found. + * + * @returns {Function} Cleanup. If you dont to get pdates/called again, call the "cleanup" function */ found(filter, cb, nothing) { @@ -653,7 +663,6 @@ module.exports = class COMPONENT extends COMMON { }); } - // NOTE: Why is a array from the add eventlistener returned?! let ev = (item) => { handler(filter, item); }; @@ -676,6 +685,60 @@ module.exports = class COMPONENT extends COMMON { } + /** + * @function _labels + * Checks if filter array contains matching labels + * + * @param {Array} arr Array with item labels to check with filter + * @param {Array} filter Filter array + * + * @returns {Boolean} true/false if filter matches label array + */ + _labels(arr, filter) { + return filter.every((filter) => { + if (arr.includes(filter)) { + + return true; + + } else { + + let [key, value] = filter.split("="); + + return arr.some((label) => { + + let [k, v] = label.split("="); + + if (value === "*") { + return key === k; + } + + if (key === "*") { + return value === v; + } + + return false; + + }); + + } + }); + } + + + /** + * @function labels + * Retun all items that matches the givel label filter + * + * @param {Array} filter Filter for labels array + * + * @returns {Array} With all items that have matching labels + */ + labels(filter) { + return this.items.filter(({ labels }) => { + return this._labels(labels || [], filter); + }); + } + // /* _exportItemMethod(name, prop) { From da4df38457f19d4801b03202409f189059cd57d3 Mon Sep 17 00:00:00 2001 From: mStirner Date: Sun, 5 Feb 2023 19:16:54 +0100 Subject: [PATCH 04/30] fix #288 (pull request) --- tests/system/component/test.component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/system/component/test.component.js b/tests/system/component/test.component.js index 7dd3fc5..9193426 100644 --- a/tests/system/component/test.component.js +++ b/tests/system/component/test.component.js @@ -67,7 +67,7 @@ describe("component", function () { "_defineMethod", "_mapMethod", "_ready", // common methods "items", "collection", "schema", // common properties "get", "add", "update", "remove", "find", // component methods - "found" + "found", "_labels", "labels" ]; for (let item of getItmes(instance)) { From 93b2dd80e0bd1c9459b1b51b6dcb6be02c6347fc Mon Sep 17 00:00:00 2001 From: mStirner Date: Mon, 6 Feb 2023 20:11:42 +0100 Subject: [PATCH 05/30] fix #275 --- Dockerfile | 6 ++++++ Gruntfile.js | 9 ++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 8179132..2143f07 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,6 +28,12 @@ WORKDIR /opt/OpenHaus/backend COPY --from=builder node_modules node_modules RUN apk --no-cache add openssl +ARG version=unknown +LABEL version=$version + +ARG buildDate=unknown +LABEL buildDate=$buildDate + COPY ./build/ ./ #COPY ./package.json ./ diff --git a/Gruntfile.js b/Gruntfile.js index 9c309d6..860a6a1 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -88,10 +88,17 @@ module.exports = function (grunt) { grunt.registerTask("build:docker", () => { - cp.execSync(`docker build . -t openhaus/${pkg.name}:latest --build-arg version=${pkg.version}`, { + + let buildArgs = [ + `--build-arg version=${pkg.version}`, + `--build-arg buildDate=${Date.now()}`, + ].join(" "); + + cp.execSync(`docker build . -t openhaus/${pkg.name}:latest ${buildArgs}`, { env: process.env, stdio: "inherit" }); + }); From 411ce5a5f19ab38e62a5de57d677db6450d8b29d Mon Sep 17 00:00:00 2001 From: mStirner Date: Sat, 11 Feb 2023 21:51:00 +0100 Subject: [PATCH 06/30] mdns added. Fix #236. Draft! --- components/mdns/class.mdns.js | 23 +++++ components/mdns/index.js | 90 ++++++++++++++++++++ components/mdns/message-handler.js | 76 +++++++++++++++++ index.js | 3 +- routes/index.js | 7 ++ routes/router.api.mdns.js | 104 +++++++++++++++++++++++ tests/components/index.js | 2 +- tests/components/mdns.js | 132 +++++++++++++++++++++++++++++ 8 files changed, 435 insertions(+), 2 deletions(-) create mode 100644 components/mdns/class.mdns.js create mode 100644 components/mdns/index.js create mode 100644 components/mdns/message-handler.js create mode 100644 routes/router.api.mdns.js create mode 100644 tests/components/mdns.js diff --git a/components/mdns/class.mdns.js b/components/mdns/class.mdns.js new file mode 100644 index 0000000..19efe27 --- /dev/null +++ b/components/mdns/class.mdns.js @@ -0,0 +1,23 @@ +class MDNS { + + constructor(obj) { + + Object.assign(this, obj); + this._id = String(obj._id); + + Object.defineProperty(this, "_matches", { + value: [], + writable: false, + configurable: false, + enumerable: false + }); + + } + + match(cb) { + this._matches.push(cb); + } + +} + +module.exports = MDNS; \ No newline at end of file diff --git a/components/mdns/index.js b/components/mdns/index.js new file mode 100644 index 0000000..5435dd4 --- /dev/null +++ b/components/mdns/index.js @@ -0,0 +1,90 @@ +const mongodb = require("mongodb"); +const Joi = require("joi"); + +//const logger = require("../../system/logger").create("rooms"); +//const COMMON_COMPONENT = require("../../system/component/common.js"); +const COMPONENT = require("../../system/component/class.component.js"); + +const MDNS = require("./class.mdns.js"); + +const messageHandler = require("./message-handler.js"); + +/** + * @description + * Listen for mdns message and sends query requests.
+ * This requires the "connector". + * + * The emitted message events is the parsed data received on the underlaying udp socket. + * + * @class C_MDNS + * @extends COMPONENT system/component/class.component.js + * + * @emits message Received message on udp socket; Arguments: [0]=parsed dns packet, [1]=raw udp message + * + * @link router.api.mdns.js routes/router.api.mdns.js + * @see https://en.wikipedia.org/wiki/Multicast_DNS + * @see https://www.npmjs.com/package/dns-packet + */ +class C_MDNS extends COMPONENT { + + constructor() { + + // inject logger, collection and schema object + super("mdns", { + _id: Joi.string().pattern(/^[0-9a-fA-F]{24}$/).default(() => { + return String(new mongodb.ObjectId()); + }), + name: Joi.string().required(), + type: Joi.string().valid("SRV", "PTR", "A", "AAAA").default("A"), + timestamps: { + announced: Joi.number().allow(null).default(null) + } + }, module); + + this.hooks.post("add", (data, next) => { + next(null, new MDNS(data)); + }); + + this.collection.createIndex({ + name: 1, + type: 1 + }, { + unique: true + }); + + // handle incoming messages + // triggers registerd callback for mdns items + messageHandler(this); + + } + +} + + +// create component instance +const instance = module.exports = new C_MDNS(); + + +// init component +// set items/build cache +instance.init((scope, ready) => { + scope.collection.find({}).toArray((err, data) => { + if (err) { + + // shit... + ready(err); + + } else { + + data = data.map((obj) => { + return new MDNS(obj); + }); + + scope.items.push(...data); + + // init done + ready(null); + + } + }); +}); \ No newline at end of file diff --git a/components/mdns/message-handler.js b/components/mdns/message-handler.js new file mode 100644 index 0000000..7db87a0 --- /dev/null +++ b/components/mdns/message-handler.js @@ -0,0 +1,76 @@ +module.exports = (scope) => { + scope._ready(({ logger }) => { + + // proceed mdns item instance + // deconstruct properties + let matchCallbacks = scope.items.map(({ name, type, _matches }) => { + return { + name, + type, + _matches + }; + }); + + + // listen for newly added items + scope.events.on("added", ({ name, type, _matches }) => { + matchCallbacks.push({ + name, + type, + _matches + }); + }); + + + scope.events.on("message", (packet, message) => { + + // feedback + logger.trace("Message received on udp socket, record:", packet, "message: ", message); + + if (packet.type === "response") { + packet.answers.forEach((record) => { + matchCallbacks.forEach(async ({ name, type, _matches }, i) => { + + // create regex from db data + // allow wildcards writen as * in items + name = name.replace(/\./, "\\."); + name = name.replace("*", ".*"); + let rexp = new RegExp(name); + + if (type === record.type && rexp.test(record.name)) { + + logger.verbose("Matching recourd found", record, name, type); + + let { timestamps, _id } = scope.items[i]; + timestamps.announced = Date.now(); + + // update mdns item timestamps + await scope.update(_id, { + timestamps: { + ...timestamps + } + }); + + _matches.forEach((cb) => { + cb(record); + }); + + } else { + + // Do nothing if nothing matches + //console.log(">> NON << MAchting record type"); + + } + + }); + }); + } else { + + logger.trace("Other packet type then response received", packet.type); + + } + + }); + + }); +}; \ No newline at end of file diff --git a/index.js b/index.js index 8109e2e..24febdd 100644 --- a/index.js +++ b/index.js @@ -201,7 +201,8 @@ const init_components = () => { "ssdp", "store", "users", - "vault" + "vault", + "mdns" ].sort(() => { // pseudo randomize start/init of components diff --git a/routes/index.js b/routes/index.js index 33129a5..4e10536 100644 --- a/routes/index.js +++ b/routes/index.js @@ -10,6 +10,7 @@ const C_VAULT = require("../components/vault"); const C_SSDP = require("../components/ssdp"); const C_STORE = require("../components/store"); const C_USERS = require("../components/users"); +const C_MDNS = require("../components/mdns"); // Remove due to issue #273 //const { encode } = require("../helper/sanitize"); @@ -127,6 +128,7 @@ module.exports = (server) => { const ssdpRouter = express.Router(); const storeRouter = express.Router(); const usersRouter = express.Router(); + const mdnsRouter = express.Router(); // http://127.0.0.1/api/plugins api.use("/plugins", pluginsRouter); @@ -176,6 +178,11 @@ module.exports = (server) => { require("./rest-handler.js")(C_USERS, usersRouter); //require("./router.api.users.js")(app, vaultRouter); + // http://127.0.0.1/api/mdns + api.use("/mdns", mdnsRouter); + require("./router.api.mdns.js")(app, mdnsRouter); + require("./rest-handler.js")(C_MDNS, mdnsRouter); + // NOTE: Drop this?! api.use((req, res) => { res.status(404).end(); diff --git a/routes/router.api.mdns.js b/routes/router.api.mdns.js new file mode 100644 index 0000000..9b38acf --- /dev/null +++ b/routes/router.api.mdns.js @@ -0,0 +1,104 @@ +const { decode } = require("dns-packet"); + +const C_MDNS = require("../components/mdns"); + +// external modules +const WebSocket = require("ws"); + +module.exports = (app, router) => { + + // websocket server + let wss = new WebSocket.Server({ + noServer: true + }); + + // detect broken connections + let interval = setInterval(() => { + wss.clients.forEach((ws) => { + + if (!ws.isAlive) { + ws.terminate(); + return; + } + + ws.isAlive = false; + ws.ping(); + + }); + }, Number(process.env.API_WEBSOCKET_TIMEOUT)); + + + // if the server closes + // clear the interval + wss.on("close", () => { + clearInterval(interval); + }); + + + // http route handler + router.get("/", (req, res, next) => { + + console.log("Request to /ai/mdns"); + + // check if connection is a simple get request or ws client + if ((!req.headers["upgrade"] || !req.headers["connection"])) { + //return res.status(403).end(); + next(); // let the rest-handler.js do its job + } + + // listen for websockt clients + // keep sending new log entrys to client + wss.once("connection", (ws) => { + + console.log("Clien connected to mdns"); + + ws.on("message", (msg) => { + C_MDNS.events.emit("message", decode(msg), msg); + }); + + ws.on("close", () => { + console.log("Client disconnected disolaskjdflaskjfdasdf"); + }); + + + // QUERY LOCAL DNS + /* + setInterval(() => { + + console.log("Query for HTTP Server"); + + let msg = encode({ + type: "query", + id: 1, + flags: RECURSION_DESIRED, + questions: [{ + type: "A", + //name: '_http._tcp.local' + name: "*" + }] + }); + + ws.send(msg); + + }, 30_000); + */ + + }); + + // handle request as websocket + // perform websocket handshake + wss.handleUpgrade(req, req.socket, req.headers, (ws) => { + + ws.isAlive = true; + + ws.on("pong", () => { + ws.isAlive = true; + }); + + wss.emit("connection", ws, req); + + }); + + }); + +}; \ No newline at end of file diff --git a/tests/components/index.js b/tests/components/index.js index 1578c94..759e512 100644 --- a/tests/components/index.js +++ b/tests/components/index.js @@ -7,7 +7,7 @@ describe("Components", () => { [ "devices", "endpoints", "plugins", "rooms", "ssdp", "store", "users", - "vault" + "vault", "mdns" ].forEach((name) => { describe(name, () => { diff --git a/tests/components/mdns.js b/tests/components/mdns.js new file mode 100644 index 0000000..54dcf9f --- /dev/null +++ b/tests/components/mdns.js @@ -0,0 +1,132 @@ +const assert = require("assert"); +const mongodb = require("mongodb"); + +try { + + const C_COMPONENT = require(`../../components/mdns/index.js`); + const MDNS = require("../../components/mdns/class.mdns.js"); + + const workflow = require("./test.workflow.js"); + + let _id = String(new mongodb.ObjectId()); + + + workflow(C_COMPONENT, "add", (done, { event }) => { + C_COMPONENT.add({ + _id, + type: "A", + name: "shelly*.local" + }, (err, item) => { + try { + + // check event arguments + event.args.forEach((args) => { + assert.equal(args[0] instanceof MDNS, true); + }); + + assert.ok(err === null); + assert.equal(item instanceof MDNS, true); + + done(err); + + } catch (err) { + + done(err); + + } + }); + }); + + + workflow(C_COMPONENT, "get", (done) => { + C_COMPONENT.get(_id, (err, item) => { + try { + + assert.ok(err === null); + assert.equal(item instanceof MDNS, true); + + done(err); + + } catch (err) { + + done(err); + + } + }); + }); + + + workflow(C_COMPONENT, "update", (done) => { + C_COMPONENT.update(_id, { + type: "SRV" + }, (err, item) => { + try { + + assert.ok(err === null); + assert.equal(item instanceof MDNS, true); + assert.equal(item.name, "shelly*.local"); + + done(err); + + } catch (err) { + + done(err); + + } + }); + }); + + + workflow(C_COMPONENT, "update", "Double update result / event arguments check", (done, { event }) => { + Promise.all([ + + // update call 1 + C_COMPONENT.update(_id, { + name: `shelly*_tcp.local` + }), + + // update call 2 + C_COMPONENT.update(_id, { + name: `shelly*_tcp.local`, + type: "A" + }), + + ]).then(() => { + + event.args.forEach((args) => { + assert.equal(args[0] instanceof MDNS, true); + }); + + done(); + + }).catch(done); + }); + + + workflow(C_COMPONENT, "remove", (done, { post }) => { + C_COMPONENT.remove(_id, (err, item) => { + try { + + // check post arguments item instance + post.args.forEach((args) => { + assert.equal(args[0] instanceof MDNS, true); + }); + + assert.ok(err === null); + assert.equal(item instanceof MDNS, true); + + done(err); + + } catch (err) { + + done(err); + + } + }); + }); + + +} catch (err) { + console.error(err); + process.exit(100); +} \ No newline at end of file From ae49d2ac4a2855701210ec0bc1f39ec0035540bb Mon Sep 17 00:00:00 2001 From: mStirner Date: Sun, 12 Feb 2023 18:19:43 +0100 Subject: [PATCH 07/30] mqtt draft added, fix #274 --- components/mqtt/class.mqtt.js | 54 ++++++++++ components/mqtt/exit-codes.js | 61 ++++++++++++ components/mqtt/index.js | 106 ++++++++++++++++++++ components/mqtt/message-handler.js | 152 +++++++++++++++++++++++++++++ index.js | 8 +- package-lock.json | 121 +++++++++++++++++++++-- package.json | 3 +- routes/index.js | 7 ++ routes/router.api.mqtt.js | 80 +++++++++++++++ tests/components/index.js | 2 +- tests/components/mqtt.js | 139 ++++++++++++++++++++++++++ 11 files changed, 723 insertions(+), 10 deletions(-) create mode 100644 components/mqtt/class.mqtt.js create mode 100644 components/mqtt/exit-codes.js create mode 100644 components/mqtt/index.js create mode 100644 components/mqtt/message-handler.js create mode 100644 routes/router.api.mqtt.js create mode 100644 tests/components/mqtt.js diff --git a/components/mqtt/class.mqtt.js b/components/mqtt/class.mqtt.js new file mode 100644 index 0000000..ecafe25 --- /dev/null +++ b/components/mqtt/class.mqtt.js @@ -0,0 +1,54 @@ +/** + * @description + * Represents a mqtt topic item + * + * @class MQTT + * + * @param {Object} obj Object that matches the item schema. See properties below: + * + * @property {String} _id MongoDB Object is as string + * @property {String} topic MQTT topic e.g. `air-sensor/sensor/particulate_matter_25m_concentration/state` + * @property {String} description Description for Admins/Topic + */ +class MQTT { + + constructor(obj) { + + Object.assign(this, obj); + this._id = String(obj._id); + + Object.defineProperty(this, "_subscriber", { + value: [], + writable: false, + configurable: false, + enumerable: false + }); + + Object.defineProperty(this, "_publisher", { + value: () => { }, + writable: true, + configurable: true, + enumerable: false + }); + + } + + /** + * Subscribe to this topic + * @param {Function} cb Callback + */ + subscribe(cb) { + this._subscriber.push(cb); + } + + /** + * Publish data on this topic + * @param {*} data Payload + */ + publish(data) { + this._publisher(data); + } + +} + +module.exports = MQTT; \ No newline at end of file diff --git a/components/mqtt/exit-codes.js b/components/mqtt/exit-codes.js new file mode 100644 index 0000000..4637797 --- /dev/null +++ b/components/mqtt/exit-codes.js @@ -0,0 +1,61 @@ +// https://mosquitto.org/man/mosquitto_passwd-1.html#idm102 + +module.exports = (version = 3) => { + if (version >= 5) { + + return { + 0: "Success", + 128: "Unspecified error", + 129: "Malformed packet", + 130: "Protocol error", + 131: "Implementation specific error", + 132: "Unsupported protocol version", + 133: "Client ID not valid", + 134: "Bad username or password", + 135: "Not authorized", + 136: "Server unavailable", + 137: "Server busy", + 138: "Banned", + 139: "Server shutting down", + 140: "Bad authentication method", + 141: "Keep alive timeout", + 142: "Session taken over", + 143: "Topic filter invalid", + 144: "Topic name invalid", + 145: "", // unused + 146: "", // unused + 147: "Receive maximum exceeded", + 148: "Topic alias invalid", + 149: "Packet too large", + 150: "", // unused + 151: "Quota exceeded", + 152: "Administrative action", + 153: "Payload format invalid", + 154: "Retain not supported", + 155: "QoS not supported", + 156: "Use another server", + 157: "Server moved", + 158: "Shared subscriptions not supported", + 159: "Connection rate exceeded", + 160: "Maximum connect time", + 161: "Subscription IDs not supported", + 162: "Wildcard subscriptions not supported", + }; + + } else if (version >= 3) { + + return { + 0: "Success", + 1: "Connection refused: Bad protocol version", + 2: "Connection refused: Identifier rejected", + 3: "Connection refused: Identifier rejected", + 4: "Connection refused: Bad username/password", + 5: "Connection refused: Not authorized" + }; + + } else { + + throw new Error(`Unsupported protocol version "${version}"`); + + } +}; \ No newline at end of file diff --git a/components/mqtt/index.js b/components/mqtt/index.js new file mode 100644 index 0000000..10deecd --- /dev/null +++ b/components/mqtt/index.js @@ -0,0 +1,106 @@ +const mongodb = require("mongodb"); +const Joi = require("joi"); + +//const logger = require("../../system/logger").create("rooms"); +//const COMMON_COMPONENT = require("../../system/component/common.js"); +const COMPONENT = require("../../system/component/class.component.js"); + +const MQTT = require("./class.mqtt.js"); + +const messageHandler = require("./message-handler.js"); +//const packetHandler = require("./packet-handler.js"); + +/** + * @description + * Receives MQTT messages from broker.
+ * It can publish and subscribe to topics. + * The emitted events are a mix from mqtt & custom ones. + * + * NOTE: Currenlty no authentication is possible. + * + * @class C_MQTT + * @extends COMPONENT system/component/class.component.js + * + * @emits message Received message over websocket/tcp connection; Arguments: [0] = tcp message + * @emits connect + * @emits connack + * @emits subscribe + * @emits suback + * @emits unsubscribe + * @emits unsuback + * @emits publish Something was published; Arguments: [0] = payload (buffer); [1] = parsed packet + * @emits puback + * @emits pubrec + * @emits pubrel + * @emits pubcomp + * @emits pingreq Ping request + * @emits pingresp Ping response + * @emits disconnect + * @emits auth + * @emits connected When websocket connected; Arguments: [0] = WebSocket client object "ws" + * @emits disconnected When websocket disconnected; Arguments: [0] = WebSocket client object "ws" + * + * @link router.api.mqtt.js routes/router.api.mqtt.js + * @see https://en.wikipedia.org/wiki/MQTT + * @see https://www.npmjs.com/package/mqtt-packet + */ +class C_MQTT extends COMPONENT { + + constructor() { + + // inject logger, collection and schema object + super("mqtt", { + _id: Joi.string().pattern(/^[0-9a-fA-F]{24}$/).default(() => { + return String(new mongodb.ObjectId()); + }), + topic: Joi.string().required(), + description: Joi.string().allow(null).default(null) + }, module); + + this.hooks.post("add", (data, next) => { + next(null, new MQTT(data)); + }); + + this.collection.createIndex({ + topic: 1 + }, { + unique: true + }); + + // handle incoming messages + // triggers registerd callback for mdns items + messageHandler(this); + //packetHandler(this); + + } + +} + + +// create component instance +const instance = module.exports = new C_MQTT(); + + +// init component +// set items/build cache +instance.init((scope, ready) => { + scope.collection.find({}).toArray((err, data) => { + if (err) { + + // shit... + ready(err); + + } else { + + data = data.map((obj) => { + return new MQTT(obj); + }); + + scope.items.push(...data); + + // init done + ready(null); + + } + }); +}); \ No newline at end of file diff --git a/components/mqtt/message-handler.js b/components/mqtt/message-handler.js new file mode 100644 index 0000000..06994c6 --- /dev/null +++ b/components/mqtt/message-handler.js @@ -0,0 +1,152 @@ +const crypto = require("crypto"); +const mqtt = require("mqtt-packet"); + +const VERSION = Number(process.env.MQTT_BROKER_VERSION); + +const parser = mqtt.parser({ + protocolVersion: VERSION +}); + +const exitCodes = require("./exit-codes.js")(VERSION); + +module.exports = (scope) => { + scope._ready(({ logger, events }) => { + + // ping timer + let interval = null; + + events.on("connected", (ws) => { + + logger.debug("TCP socket connected to broker"); + + events.once("disconnected", () => { + clearInterval(interval); + logger.trace("Ping interval cleared"); + }); + + // TODO make this object configurable + let data = mqtt.generate({ + cmd: "connect", + protocolId: "MQTT", // Or "MQIsdp" in MQTT 3.1 and 5.0 + protocolVersion: VERSION, // Or 3 in MQTT 3.1, or 5 in MQTT 5.0 + clean: true, // Can also be false + clientId: process.env.MQTT_CLIENT_ID, + keepalive: 10, // Seconds which can be any positive number, with 0 as the default setting + /* + will: { + topic: "mydevice/test", + payload: Buffer.from("2134f"), // Payloads are buffers + + } + */ + }); + + ws.send(data); + + + events.on("publish", (packet) => { + scope.items.forEach(({ topic, _subscriber }) => { + + if (String(packet.topic).startsWith(topic) || packet.topic === topic) { + _subscriber.forEach((cb) => { + cb(packet.payload, packet); + }); + } + + }); + }); + + + events.once("connack", (packet) => { + if (packet.returnCode === 0) { + + events.once("suback", () => { + + logger.debug("Subscribed to topic #"); + + let ping = mqtt.generate({ + cmd: "pingreq" + }); + + interval = setInterval(() => { + ws.send(ping); + }, Number(process.env.MQTT_PING_INTERVAL)); + + // monkey patch publisher function + scope.items.forEach((item) => { + item._publisher = (payload) => { + + scope.logger.verbose(`Publish on topic ${item.topic}`, payload); + + let pub = mqtt.generate({ + cmd: "publish", + messageId: crypto.randomInt(0, 65535), + qos: 0, + dup: false, + topic: item.topic, + payload: Buffer.from(`${payload}`), + retain: false + }); + + ws.send(pub); + + }; + }); + + }); + + let sub = mqtt.generate({ + cmd: "subscribe", + messageId: crypto.randomInt(0, 65535), + /* + properties: { // MQTT 5.0 properties + subscriptionIdentifier: 145, + userProperties: { + test: "shellies" + } + }, + */ + subscriptions: [{ + topic: "#", + qos: 0, + nl: false, // no Local MQTT 5.0 flag + rap: true, // Retain as Published MQTT 5.0 flag + rh: 1 // Retain Handling MQTT 5.0 + }] + }); + + ws.send(sub); + + } + }); + + }); + + + parser.on("packet", (packet) => { + + logger.verbose("Packet received", packet); + + if (packet.cmd === "connack") { + if (packet.returnCode == 0) { + + logger.debug("Connected to broker"); + + } else { + + logger.warn(`Could not connecto to broker: "${exitCodes[packet.returnCode]}"`); + + } + } + + events.emit(packet.cmd, packet); + + }); + + + events.on("message", (message) => { + parser.parse(message); + }); + + }); +}; \ No newline at end of file diff --git a/index.js b/index.js index 24febdd..c6c8618 100644 --- a/index.js +++ b/index.js @@ -56,7 +56,10 @@ process.env = Object.assign({ VAULT_SALT_BYTE_LEN: "16", USERS_BCRYPT_SALT_ROUNDS: "12", USERS_JWT_SECRET: "", - USERS_JWT_ALGORITHM: "HS384" + USERS_JWT_ALGORITHM: "HS384", + MQTT_BROKER_VERSION: "3", + MQTT_CLIENT_ID: "OpenHaus", + MQTT_PING_INTERVAL: "5000" }, env.parsed, process.env); @@ -202,7 +205,8 @@ const init_components = () => { "store", "users", "vault", - "mdns" + "mdns", + "mqtt" ].sort(() => { // pseudo randomize start/init of components diff --git a/package-lock.json b/package-lock.json index 5b1e434..49a9d46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "backend", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "backend", - "version": "1.0.0", + "version": "2.0.0", "hasInstallScript": true, "license": "ISC", "dependencies": { @@ -19,6 +19,7 @@ "joi": "^17.6.4", "jsonwebtoken": "^9.0.0", "mongodb": "^4.11.0", + "mqtt-packet": "^8.1.2", "uuid": "^9.0.0", "ws": "^8.10.0" }, @@ -1691,6 +1692,39 @@ "node": ">=8" } }, + "node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/bluebird": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", @@ -4683,6 +4717,37 @@ "node": ">=12" } }, + "node_modules/mqtt-packet": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-8.1.2.tgz", + "integrity": "sha512-vL1YTct+TAy0PqX3Jv8jM3JMzObH6vC/lyA0I5LtD4xvydOdIdmofrSp12PE3jajiIOUaW3XxmQekbyToXpsSw==", + "dependencies": { + "bl": "^5.0.0", + "debug": "^4.1.1", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/mqtt-packet/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mqtt-packet/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -5588,8 +5653,7 @@ "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "node_modules/proxy-addr": { "version": "2.0.7", @@ -8156,6 +8220,27 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "requires": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + } + } + }, "bluebird": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", @@ -10447,6 +10532,31 @@ } } }, + "mqtt-packet": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-8.1.2.tgz", + "integrity": "sha512-vL1YTct+TAy0PqX3Jv8jM3JMzObH6vC/lyA0I5LtD4xvydOdIdmofrSp12PE3jajiIOUaW3XxmQekbyToXpsSw==", + "requires": { + "bl": "^5.0.0", + "debug": "^4.1.1", + "process-nextick-args": "^2.0.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -11139,8 +11249,7 @@ "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "proxy-addr": { "version": "2.0.7", diff --git a/package.json b/package.json index d769021..d7ae471 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "joi": "^17.6.4", "jsonwebtoken": "^9.0.0", "mongodb": "^4.11.0", + "mqtt-packet": "^8.1.2", "uuid": "^9.0.0", "ws": "^8.10.0" }, @@ -52,4 +53,4 @@ "nodemon": "^2.0.19", "sinon": "^14.0.2" } -} \ No newline at end of file +} diff --git a/routes/index.js b/routes/index.js index 4e10536..2b0366b 100644 --- a/routes/index.js +++ b/routes/index.js @@ -11,6 +11,7 @@ const C_SSDP = require("../components/ssdp"); const C_STORE = require("../components/store"); const C_USERS = require("../components/users"); const C_MDNS = require("../components/mdns"); +const C_MQTT = require("../components/mqtt"); // Remove due to issue #273 //const { encode } = require("../helper/sanitize"); @@ -129,6 +130,7 @@ module.exports = (server) => { const storeRouter = express.Router(); const usersRouter = express.Router(); const mdnsRouter = express.Router(); + const mqttRouter = express.Router(); // http://127.0.0.1/api/plugins api.use("/plugins", pluginsRouter); @@ -183,6 +185,11 @@ module.exports = (server) => { require("./router.api.mdns.js")(app, mdnsRouter); require("./rest-handler.js")(C_MDNS, mdnsRouter); + // http://127.0.0.1/api/mqtt + api.use("/mqtt", mqttRouter); + require("./router.api.mqtt.js")(app, mqttRouter); + require("./rest-handler.js")(C_MQTT, mqttRouter); + // NOTE: Drop this?! api.use((req, res) => { res.status(404).end(); diff --git a/routes/router.api.mqtt.js b/routes/router.api.mqtt.js new file mode 100644 index 0000000..58f6fa9 --- /dev/null +++ b/routes/router.api.mqtt.js @@ -0,0 +1,80 @@ +const C_MQTT = require("../components/mqtt"); + +// external modules +const WebSocket = require("ws"); + +module.exports = (app, router) => { + + // websocket server + let wss = new WebSocket.Server({ + noServer: true + }); + + // detect broken connections + let interval = setInterval(() => { + wss.clients.forEach((ws) => { + + if (!ws.isAlive) { + ws.terminate(); + return; + } + + ws.isAlive = false; + ws.ping(); + + }); + }, Number(process.env.API_WEBSOCKET_TIMEOUT)); + + + // if the server closes + // clear the interval + wss.on("close", () => { + clearInterval(interval); + }); + + + // http route handler + router.get("/", (req, res, next) => { + + console.log("Request to /ai/mqtt"); + + // check if connection is a simple get request or ws client + if ((!req.headers["upgrade"] || !req.headers["connection"])) { + //return res.status(403).end(); + next(); // let the rest-handler.js do its job + } + + // listen for websockt clients + // keep sending new log entrys to client + wss.once("connection", (ws) => { + + C_MQTT.events.emit("connected", ws); + + ws.on("message", (msg) => { + C_MQTT.events.emit("message", msg); + }); + + ws.on("close", () => { + console.log("MQTT Client disconnected disolaskjdflaskjfdasdf"); + C_MQTT.events.emit("disconnected", ws); + }); + + }); + + // handle request as websocket + // perform websocket handshake + wss.handleUpgrade(req, req.socket, req.headers, (ws) => { + + ws.isAlive = true; + + ws.on("pong", () => { + ws.isAlive = true; + }); + + wss.emit("connection", ws, req); + + }); + + }); + +}; \ No newline at end of file diff --git a/tests/components/index.js b/tests/components/index.js index 759e512..14d9299 100644 --- a/tests/components/index.js +++ b/tests/components/index.js @@ -7,7 +7,7 @@ describe("Components", () => { [ "devices", "endpoints", "plugins", "rooms", "ssdp", "store", "users", - "vault", "mdns" + "vault", "mdns", "mqtt" ].forEach((name) => { describe(name, () => { diff --git a/tests/components/mqtt.js b/tests/components/mqtt.js new file mode 100644 index 0000000..84a6ef2 --- /dev/null +++ b/tests/components/mqtt.js @@ -0,0 +1,139 @@ +const assert = require("assert"); +const mongodb = require("mongodb"); + +try { + + const C_COMPONENT = require(`../../components/mqtt/index.js`); + const MQTT = require("../../components/mqtt/class.mqtt.js"); + + const workflow = require("./test.workflow.js"); + + let _id = String(new mongodb.ObjectId()); + + + workflow(C_COMPONENT, "add", (done, { event }) => { + C_COMPONENT.add({ + _id, + topic: "air-sensor/", + }, (err, item) => { + try { + + // check event arguments + event.args.forEach((args) => { + assert.equal(args[0] instanceof MQTT, true); + }); + + assert.ok(err === null); + assert.equal(item instanceof MQTT, true); + assert.ok(item._publisher); + assert.ok(item._subscriber); + assert.equal(item.subscribe instanceof Function, true); + assert.equal(item.publish instanceof Function, true); + + done(err); + + } catch (err) { + + done(err); + + } + }); + }); + + + workflow(C_COMPONENT, "get", (done) => { + C_COMPONENT.get(_id, (err, item) => { + try { + + assert.ok(err === null); + assert.equal(item instanceof MQTT, true); + + done(err); + + } catch (err) { + + done(err); + + } + }); + }); + + + workflow(C_COMPONENT, "update", (done) => { + C_COMPONENT.update(_id, { + topic: "air-sensor/sensor/particulate_matter_25m_concentration/state" + }, (err, item) => { + try { + + assert.ok(err === null); + assert.equal(item instanceof MQTT, true); + assert.equal(item.topic, "air-sensor/sensor/particulate_matter_25m_concentration/state"); + assert.ok(item._publisher); + assert.ok(item._subscriber); + assert.equal(item.subscribe instanceof Function, true); + assert.equal(item.publish instanceof Function, true); + + done(err); + + } catch (err) { + + done(err); + + } + }); + }); + + + workflow(C_COMPONENT, "update", "Double update result / event arguments check", (done, { event }) => { + Promise.all([ + + // update call 1 + C_COMPONENT.update(_id, { + topic: `air-sensor/status` + }), + + // update call 2 + C_COMPONENT.update(_id, { + topic: `air-sensor/`, + description: "Ikea VINDRIKTNING Air sensor MQTT topic" + }), + + ]).then(() => { + + event.args.forEach((args) => { + assert.equal(args[0] instanceof MQTT, true); + }); + + done(); + + }).catch(done); + }); + + + workflow(C_COMPONENT, "remove", (done, { post }) => { + C_COMPONENT.remove(_id, (err, item) => { + try { + + // check post arguments item instance + post.args.forEach((args) => { + assert.equal(args[0] instanceof MQTT, true); + }); + + assert.ok(err === null); + assert.equal(item instanceof MQTT, true); + + done(err); + + } catch (err) { + + done(err); + + } + }); + }); + + +} catch (err) { + console.error(err); + process.exit(100); +} \ No newline at end of file From 20ff3006d1c39b7f4c535933afd98c61036e5660 Mon Sep 17 00:00:00 2001 From: mStirner Date: Sun, 12 Feb 2023 18:22:20 +0100 Subject: [PATCH 08/30] Revert "mqtt draft added, fix #274" This reverts commit ae49d2ac4a2855701210ec0bc1f39ec0035540bb. --- components/mqtt/class.mqtt.js | 54 ---------- components/mqtt/exit-codes.js | 61 ------------ components/mqtt/index.js | 106 -------------------- components/mqtt/message-handler.js | 152 ----------------------------- index.js | 8 +- package-lock.json | 121 ++--------------------- package.json | 3 +- routes/index.js | 7 -- routes/router.api.mqtt.js | 80 --------------- tests/components/index.js | 2 +- tests/components/mqtt.js | 139 -------------------------- 11 files changed, 10 insertions(+), 723 deletions(-) delete mode 100644 components/mqtt/class.mqtt.js delete mode 100644 components/mqtt/exit-codes.js delete mode 100644 components/mqtt/index.js delete mode 100644 components/mqtt/message-handler.js delete mode 100644 routes/router.api.mqtt.js delete mode 100644 tests/components/mqtt.js diff --git a/components/mqtt/class.mqtt.js b/components/mqtt/class.mqtt.js deleted file mode 100644 index ecafe25..0000000 --- a/components/mqtt/class.mqtt.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * @description - * Represents a mqtt topic item - * - * @class MQTT - * - * @param {Object} obj Object that matches the item schema. See properties below: - * - * @property {String} _id MongoDB Object is as string - * @property {String} topic MQTT topic e.g. `air-sensor/sensor/particulate_matter_25m_concentration/state` - * @property {String} description Description for Admins/Topic - */ -class MQTT { - - constructor(obj) { - - Object.assign(this, obj); - this._id = String(obj._id); - - Object.defineProperty(this, "_subscriber", { - value: [], - writable: false, - configurable: false, - enumerable: false - }); - - Object.defineProperty(this, "_publisher", { - value: () => { }, - writable: true, - configurable: true, - enumerable: false - }); - - } - - /** - * Subscribe to this topic - * @param {Function} cb Callback - */ - subscribe(cb) { - this._subscriber.push(cb); - } - - /** - * Publish data on this topic - * @param {*} data Payload - */ - publish(data) { - this._publisher(data); - } - -} - -module.exports = MQTT; \ No newline at end of file diff --git a/components/mqtt/exit-codes.js b/components/mqtt/exit-codes.js deleted file mode 100644 index 4637797..0000000 --- a/components/mqtt/exit-codes.js +++ /dev/null @@ -1,61 +0,0 @@ -// https://mosquitto.org/man/mosquitto_passwd-1.html#idm102 - -module.exports = (version = 3) => { - if (version >= 5) { - - return { - 0: "Success", - 128: "Unspecified error", - 129: "Malformed packet", - 130: "Protocol error", - 131: "Implementation specific error", - 132: "Unsupported protocol version", - 133: "Client ID not valid", - 134: "Bad username or password", - 135: "Not authorized", - 136: "Server unavailable", - 137: "Server busy", - 138: "Banned", - 139: "Server shutting down", - 140: "Bad authentication method", - 141: "Keep alive timeout", - 142: "Session taken over", - 143: "Topic filter invalid", - 144: "Topic name invalid", - 145: "", // unused - 146: "", // unused - 147: "Receive maximum exceeded", - 148: "Topic alias invalid", - 149: "Packet too large", - 150: "", // unused - 151: "Quota exceeded", - 152: "Administrative action", - 153: "Payload format invalid", - 154: "Retain not supported", - 155: "QoS not supported", - 156: "Use another server", - 157: "Server moved", - 158: "Shared subscriptions not supported", - 159: "Connection rate exceeded", - 160: "Maximum connect time", - 161: "Subscription IDs not supported", - 162: "Wildcard subscriptions not supported", - }; - - } else if (version >= 3) { - - return { - 0: "Success", - 1: "Connection refused: Bad protocol version", - 2: "Connection refused: Identifier rejected", - 3: "Connection refused: Identifier rejected", - 4: "Connection refused: Bad username/password", - 5: "Connection refused: Not authorized" - }; - - } else { - - throw new Error(`Unsupported protocol version "${version}"`); - - } -}; \ No newline at end of file diff --git a/components/mqtt/index.js b/components/mqtt/index.js deleted file mode 100644 index 10deecd..0000000 --- a/components/mqtt/index.js +++ /dev/null @@ -1,106 +0,0 @@ -const mongodb = require("mongodb"); -const Joi = require("joi"); - -//const logger = require("../../system/logger").create("rooms"); -//const COMMON_COMPONENT = require("../../system/component/common.js"); -const COMPONENT = require("../../system/component/class.component.js"); - -const MQTT = require("./class.mqtt.js"); - -const messageHandler = require("./message-handler.js"); -//const packetHandler = require("./packet-handler.js"); - -/** - * @description - * Receives MQTT messages from broker.
- * It can publish and subscribe to topics. - * The emitted events are a mix from mqtt & custom ones. - * - * NOTE: Currenlty no authentication is possible. - * - * @class C_MQTT - * @extends COMPONENT system/component/class.component.js - * - * @emits message Received message over websocket/tcp connection; Arguments: [0] = tcp message - * @emits connect - * @emits connack - * @emits subscribe - * @emits suback - * @emits unsubscribe - * @emits unsuback - * @emits publish Something was published; Arguments: [0] = payload (buffer); [1] = parsed packet - * @emits puback - * @emits pubrec - * @emits pubrel - * @emits pubcomp - * @emits pingreq Ping request - * @emits pingresp Ping response - * @emits disconnect - * @emits auth - * @emits connected When websocket connected; Arguments: [0] = WebSocket client object "ws" - * @emits disconnected When websocket disconnected; Arguments: [0] = WebSocket client object "ws" - * - * @link router.api.mqtt.js routes/router.api.mqtt.js - * @see https://en.wikipedia.org/wiki/MQTT - * @see https://www.npmjs.com/package/mqtt-packet - */ -class C_MQTT extends COMPONENT { - - constructor() { - - // inject logger, collection and schema object - super("mqtt", { - _id: Joi.string().pattern(/^[0-9a-fA-F]{24}$/).default(() => { - return String(new mongodb.ObjectId()); - }), - topic: Joi.string().required(), - description: Joi.string().allow(null).default(null) - }, module); - - this.hooks.post("add", (data, next) => { - next(null, new MQTT(data)); - }); - - this.collection.createIndex({ - topic: 1 - }, { - unique: true - }); - - // handle incoming messages - // triggers registerd callback for mdns items - messageHandler(this); - //packetHandler(this); - - } - -} - - -// create component instance -const instance = module.exports = new C_MQTT(); - - -// init component -// set items/build cache -instance.init((scope, ready) => { - scope.collection.find({}).toArray((err, data) => { - if (err) { - - // shit... - ready(err); - - } else { - - data = data.map((obj) => { - return new MQTT(obj); - }); - - scope.items.push(...data); - - // init done - ready(null); - - } - }); -}); \ No newline at end of file diff --git a/components/mqtt/message-handler.js b/components/mqtt/message-handler.js deleted file mode 100644 index 06994c6..0000000 --- a/components/mqtt/message-handler.js +++ /dev/null @@ -1,152 +0,0 @@ -const crypto = require("crypto"); -const mqtt = require("mqtt-packet"); - -const VERSION = Number(process.env.MQTT_BROKER_VERSION); - -const parser = mqtt.parser({ - protocolVersion: VERSION -}); - -const exitCodes = require("./exit-codes.js")(VERSION); - -module.exports = (scope) => { - scope._ready(({ logger, events }) => { - - // ping timer - let interval = null; - - events.on("connected", (ws) => { - - logger.debug("TCP socket connected to broker"); - - events.once("disconnected", () => { - clearInterval(interval); - logger.trace("Ping interval cleared"); - }); - - // TODO make this object configurable - let data = mqtt.generate({ - cmd: "connect", - protocolId: "MQTT", // Or "MQIsdp" in MQTT 3.1 and 5.0 - protocolVersion: VERSION, // Or 3 in MQTT 3.1, or 5 in MQTT 5.0 - clean: true, // Can also be false - clientId: process.env.MQTT_CLIENT_ID, - keepalive: 10, // Seconds which can be any positive number, with 0 as the default setting - /* - will: { - topic: "mydevice/test", - payload: Buffer.from("2134f"), // Payloads are buffers - - } - */ - }); - - ws.send(data); - - - events.on("publish", (packet) => { - scope.items.forEach(({ topic, _subscriber }) => { - - if (String(packet.topic).startsWith(topic) || packet.topic === topic) { - _subscriber.forEach((cb) => { - cb(packet.payload, packet); - }); - } - - }); - }); - - - events.once("connack", (packet) => { - if (packet.returnCode === 0) { - - events.once("suback", () => { - - logger.debug("Subscribed to topic #"); - - let ping = mqtt.generate({ - cmd: "pingreq" - }); - - interval = setInterval(() => { - ws.send(ping); - }, Number(process.env.MQTT_PING_INTERVAL)); - - // monkey patch publisher function - scope.items.forEach((item) => { - item._publisher = (payload) => { - - scope.logger.verbose(`Publish on topic ${item.topic}`, payload); - - let pub = mqtt.generate({ - cmd: "publish", - messageId: crypto.randomInt(0, 65535), - qos: 0, - dup: false, - topic: item.topic, - payload: Buffer.from(`${payload}`), - retain: false - }); - - ws.send(pub); - - }; - }); - - }); - - let sub = mqtt.generate({ - cmd: "subscribe", - messageId: crypto.randomInt(0, 65535), - /* - properties: { // MQTT 5.0 properties - subscriptionIdentifier: 145, - userProperties: { - test: "shellies" - } - }, - */ - subscriptions: [{ - topic: "#", - qos: 0, - nl: false, // no Local MQTT 5.0 flag - rap: true, // Retain as Published MQTT 5.0 flag - rh: 1 // Retain Handling MQTT 5.0 - }] - }); - - ws.send(sub); - - } - }); - - }); - - - parser.on("packet", (packet) => { - - logger.verbose("Packet received", packet); - - if (packet.cmd === "connack") { - if (packet.returnCode == 0) { - - logger.debug("Connected to broker"); - - } else { - - logger.warn(`Could not connecto to broker: "${exitCodes[packet.returnCode]}"`); - - } - } - - events.emit(packet.cmd, packet); - - }); - - - events.on("message", (message) => { - parser.parse(message); - }); - - }); -}; \ No newline at end of file diff --git a/index.js b/index.js index c6c8618..24febdd 100644 --- a/index.js +++ b/index.js @@ -56,10 +56,7 @@ process.env = Object.assign({ VAULT_SALT_BYTE_LEN: "16", USERS_BCRYPT_SALT_ROUNDS: "12", USERS_JWT_SECRET: "", - USERS_JWT_ALGORITHM: "HS384", - MQTT_BROKER_VERSION: "3", - MQTT_CLIENT_ID: "OpenHaus", - MQTT_PING_INTERVAL: "5000" + USERS_JWT_ALGORITHM: "HS384" }, env.parsed, process.env); @@ -205,8 +202,7 @@ const init_components = () => { "store", "users", "vault", - "mdns", - "mqtt" + "mdns" ].sort(() => { // pseudo randomize start/init of components diff --git a/package-lock.json b/package-lock.json index 49a9d46..5b1e434 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "backend", - "version": "2.0.0", + "version": "1.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "backend", - "version": "2.0.0", + "version": "1.0.0", "hasInstallScript": true, "license": "ISC", "dependencies": { @@ -19,7 +19,6 @@ "joi": "^17.6.4", "jsonwebtoken": "^9.0.0", "mongodb": "^4.11.0", - "mqtt-packet": "^8.1.2", "uuid": "^9.0.0", "ws": "^8.10.0" }, @@ -1692,39 +1691,6 @@ "node": ">=8" } }, - "node_modules/bl": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", - "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", - "dependencies": { - "buffer": "^6.0.3", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bl/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/bluebird": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", @@ -4717,37 +4683,6 @@ "node": ">=12" } }, - "node_modules/mqtt-packet": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-8.1.2.tgz", - "integrity": "sha512-vL1YTct+TAy0PqX3Jv8jM3JMzObH6vC/lyA0I5LtD4xvydOdIdmofrSp12PE3jajiIOUaW3XxmQekbyToXpsSw==", - "dependencies": { - "bl": "^5.0.0", - "debug": "^4.1.1", - "process-nextick-args": "^2.0.1" - } - }, - "node_modules/mqtt-packet/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/mqtt-packet/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -5653,7 +5588,8 @@ "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true }, "node_modules/proxy-addr": { "version": "2.0.7", @@ -8220,27 +8156,6 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, - "bl": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", - "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", - "requires": { - "buffer": "^6.0.3", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - }, - "dependencies": { - "buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - } - } - }, "bluebird": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", @@ -10532,31 +10447,6 @@ } } }, - "mqtt-packet": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-8.1.2.tgz", - "integrity": "sha512-vL1YTct+TAy0PqX3Jv8jM3JMzObH6vC/lyA0I5LtD4xvydOdIdmofrSp12PE3jajiIOUaW3XxmQekbyToXpsSw==", - "requires": { - "bl": "^5.0.0", - "debug": "^4.1.1", - "process-nextick-args": "^2.0.1" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -11249,7 +11139,8 @@ "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true }, "proxy-addr": { "version": "2.0.7", diff --git a/package.json b/package.json index d7ae471..d769021 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "joi": "^17.6.4", "jsonwebtoken": "^9.0.0", "mongodb": "^4.11.0", - "mqtt-packet": "^8.1.2", "uuid": "^9.0.0", "ws": "^8.10.0" }, @@ -53,4 +52,4 @@ "nodemon": "^2.0.19", "sinon": "^14.0.2" } -} +} \ No newline at end of file diff --git a/routes/index.js b/routes/index.js index 2b0366b..4e10536 100644 --- a/routes/index.js +++ b/routes/index.js @@ -11,7 +11,6 @@ const C_SSDP = require("../components/ssdp"); const C_STORE = require("../components/store"); const C_USERS = require("../components/users"); const C_MDNS = require("../components/mdns"); -const C_MQTT = require("../components/mqtt"); // Remove due to issue #273 //const { encode } = require("../helper/sanitize"); @@ -130,7 +129,6 @@ module.exports = (server) => { const storeRouter = express.Router(); const usersRouter = express.Router(); const mdnsRouter = express.Router(); - const mqttRouter = express.Router(); // http://127.0.0.1/api/plugins api.use("/plugins", pluginsRouter); @@ -185,11 +183,6 @@ module.exports = (server) => { require("./router.api.mdns.js")(app, mdnsRouter); require("./rest-handler.js")(C_MDNS, mdnsRouter); - // http://127.0.0.1/api/mqtt - api.use("/mqtt", mqttRouter); - require("./router.api.mqtt.js")(app, mqttRouter); - require("./rest-handler.js")(C_MQTT, mqttRouter); - // NOTE: Drop this?! api.use((req, res) => { res.status(404).end(); diff --git a/routes/router.api.mqtt.js b/routes/router.api.mqtt.js deleted file mode 100644 index 58f6fa9..0000000 --- a/routes/router.api.mqtt.js +++ /dev/null @@ -1,80 +0,0 @@ -const C_MQTT = require("../components/mqtt"); - -// external modules -const WebSocket = require("ws"); - -module.exports = (app, router) => { - - // websocket server - let wss = new WebSocket.Server({ - noServer: true - }); - - // detect broken connections - let interval = setInterval(() => { - wss.clients.forEach((ws) => { - - if (!ws.isAlive) { - ws.terminate(); - return; - } - - ws.isAlive = false; - ws.ping(); - - }); - }, Number(process.env.API_WEBSOCKET_TIMEOUT)); - - - // if the server closes - // clear the interval - wss.on("close", () => { - clearInterval(interval); - }); - - - // http route handler - router.get("/", (req, res, next) => { - - console.log("Request to /ai/mqtt"); - - // check if connection is a simple get request or ws client - if ((!req.headers["upgrade"] || !req.headers["connection"])) { - //return res.status(403).end(); - next(); // let the rest-handler.js do its job - } - - // listen for websockt clients - // keep sending new log entrys to client - wss.once("connection", (ws) => { - - C_MQTT.events.emit("connected", ws); - - ws.on("message", (msg) => { - C_MQTT.events.emit("message", msg); - }); - - ws.on("close", () => { - console.log("MQTT Client disconnected disolaskjdflaskjfdasdf"); - C_MQTT.events.emit("disconnected", ws); - }); - - }); - - // handle request as websocket - // perform websocket handshake - wss.handleUpgrade(req, req.socket, req.headers, (ws) => { - - ws.isAlive = true; - - ws.on("pong", () => { - ws.isAlive = true; - }); - - wss.emit("connection", ws, req); - - }); - - }); - -}; \ No newline at end of file diff --git a/tests/components/index.js b/tests/components/index.js index 14d9299..759e512 100644 --- a/tests/components/index.js +++ b/tests/components/index.js @@ -7,7 +7,7 @@ describe("Components", () => { [ "devices", "endpoints", "plugins", "rooms", "ssdp", "store", "users", - "vault", "mdns", "mqtt" + "vault", "mdns" ].forEach((name) => { describe(name, () => { diff --git a/tests/components/mqtt.js b/tests/components/mqtt.js deleted file mode 100644 index 84a6ef2..0000000 --- a/tests/components/mqtt.js +++ /dev/null @@ -1,139 +0,0 @@ -const assert = require("assert"); -const mongodb = require("mongodb"); - -try { - - const C_COMPONENT = require(`../../components/mqtt/index.js`); - const MQTT = require("../../components/mqtt/class.mqtt.js"); - - const workflow = require("./test.workflow.js"); - - let _id = String(new mongodb.ObjectId()); - - - workflow(C_COMPONENT, "add", (done, { event }) => { - C_COMPONENT.add({ - _id, - topic: "air-sensor/", - }, (err, item) => { - try { - - // check event arguments - event.args.forEach((args) => { - assert.equal(args[0] instanceof MQTT, true); - }); - - assert.ok(err === null); - assert.equal(item instanceof MQTT, true); - assert.ok(item._publisher); - assert.ok(item._subscriber); - assert.equal(item.subscribe instanceof Function, true); - assert.equal(item.publish instanceof Function, true); - - done(err); - - } catch (err) { - - done(err); - - } - }); - }); - - - workflow(C_COMPONENT, "get", (done) => { - C_COMPONENT.get(_id, (err, item) => { - try { - - assert.ok(err === null); - assert.equal(item instanceof MQTT, true); - - done(err); - - } catch (err) { - - done(err); - - } - }); - }); - - - workflow(C_COMPONENT, "update", (done) => { - C_COMPONENT.update(_id, { - topic: "air-sensor/sensor/particulate_matter_25m_concentration/state" - }, (err, item) => { - try { - - assert.ok(err === null); - assert.equal(item instanceof MQTT, true); - assert.equal(item.topic, "air-sensor/sensor/particulate_matter_25m_concentration/state"); - assert.ok(item._publisher); - assert.ok(item._subscriber); - assert.equal(item.subscribe instanceof Function, true); - assert.equal(item.publish instanceof Function, true); - - done(err); - - } catch (err) { - - done(err); - - } - }); - }); - - - workflow(C_COMPONENT, "update", "Double update result / event arguments check", (done, { event }) => { - Promise.all([ - - // update call 1 - C_COMPONENT.update(_id, { - topic: `air-sensor/status` - }), - - // update call 2 - C_COMPONENT.update(_id, { - topic: `air-sensor/`, - description: "Ikea VINDRIKTNING Air sensor MQTT topic" - }), - - ]).then(() => { - - event.args.forEach((args) => { - assert.equal(args[0] instanceof MQTT, true); - }); - - done(); - - }).catch(done); - }); - - - workflow(C_COMPONENT, "remove", (done, { post }) => { - C_COMPONENT.remove(_id, (err, item) => { - try { - - // check post arguments item instance - post.args.forEach((args) => { - assert.equal(args[0] instanceof MQTT, true); - }); - - assert.ok(err === null); - assert.equal(item instanceof MQTT, true); - - done(err); - - } catch (err) { - - done(err); - - } - }); - }); - - -} catch (err) { - console.error(err); - process.exit(100); -} \ No newline at end of file From e24c6ab8d09677c1b7aa2392d5eb19936cdc99c2 Mon Sep 17 00:00:00 2001 From: mStirner Date: Sun, 12 Feb 2023 18:23:13 +0100 Subject: [PATCH 09/30] "dns-packet" added --- package-lock.json | 34 ++++++++++++++++++++++++++++++++-- package.json | 3 ++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5b1e434..f6d4af1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "backend", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "backend", - "version": "1.0.0", + "version": "2.0.0", "hasInstallScript": true, "license": "ISC", "dependencies": { @@ -14,6 +14,7 @@ "body-parser": "^1.20.1", "colors": "^1.4.0", "dateformat": "^4.6.3", + "dns-packet": "^5.4.0", "dotenv": "^16.0.3", "express": "^4.18.2", "joi": "^17.6.4", @@ -1160,6 +1161,11 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", @@ -2259,6 +2265,17 @@ "node": ">=8" } }, + "node_modules/dns-packet": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.4.0.tgz", + "integrity": "sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -7726,6 +7743,11 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" + }, "@mapbox/node-pre-gyp": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", @@ -8574,6 +8596,14 @@ "path-type": "^4.0.0" } }, + "dns-packet": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.4.0.tgz", + "integrity": "sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==", + "requires": { + "@leichtgewicht/ip-codec": "^2.0.1" + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", diff --git a/package.json b/package.json index d769021..d2a3be0 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "body-parser": "^1.20.1", "colors": "^1.4.0", "dateformat": "^4.6.3", + "dns-packet": "^5.4.0", "dotenv": "^16.0.3", "express": "^4.18.2", "joi": "^17.6.4", @@ -52,4 +53,4 @@ "nodemon": "^2.0.19", "sinon": "^14.0.2" } -} \ No newline at end of file +} From 5896a376d403e00c8824b34b5ba853872ea5ad40 Mon Sep 17 00:00:00 2001 From: mStirner Date: Sun, 12 Feb 2023 18:38:29 +0100 Subject: [PATCH 10/30] mqtt component draft added, fix #274 --- components/mqtt/class.mqtt.js | 54 ++++++++++ components/mqtt/exit-codes.js | 61 ++++++++++++ components/mqtt/index.js | 106 ++++++++++++++++++++ components/mqtt/message-handler.js | 152 +++++++++++++++++++++++++++++ index.js | 8 +- package-lock.json | 121 +++++++++++++++++++++-- package.json | 3 +- routes/index.js | 9 ++ routes/router.api.mqtt.js | 80 +++++++++++++++ tests/components/index.js | 2 +- tests/components/mqtt.js | 139 ++++++++++++++++++++++++++ 11 files changed, 725 insertions(+), 10 deletions(-) create mode 100644 components/mqtt/class.mqtt.js create mode 100644 components/mqtt/exit-codes.js create mode 100644 components/mqtt/index.js create mode 100644 components/mqtt/message-handler.js create mode 100644 routes/router.api.mqtt.js create mode 100644 tests/components/mqtt.js diff --git a/components/mqtt/class.mqtt.js b/components/mqtt/class.mqtt.js new file mode 100644 index 0000000..ecafe25 --- /dev/null +++ b/components/mqtt/class.mqtt.js @@ -0,0 +1,54 @@ +/** + * @description + * Represents a mqtt topic item + * + * @class MQTT + * + * @param {Object} obj Object that matches the item schema. See properties below: + * + * @property {String} _id MongoDB Object is as string + * @property {String} topic MQTT topic e.g. `air-sensor/sensor/particulate_matter_25m_concentration/state` + * @property {String} description Description for Admins/Topic + */ +class MQTT { + + constructor(obj) { + + Object.assign(this, obj); + this._id = String(obj._id); + + Object.defineProperty(this, "_subscriber", { + value: [], + writable: false, + configurable: false, + enumerable: false + }); + + Object.defineProperty(this, "_publisher", { + value: () => { }, + writable: true, + configurable: true, + enumerable: false + }); + + } + + /** + * Subscribe to this topic + * @param {Function} cb Callback + */ + subscribe(cb) { + this._subscriber.push(cb); + } + + /** + * Publish data on this topic + * @param {*} data Payload + */ + publish(data) { + this._publisher(data); + } + +} + +module.exports = MQTT; \ No newline at end of file diff --git a/components/mqtt/exit-codes.js b/components/mqtt/exit-codes.js new file mode 100644 index 0000000..4637797 --- /dev/null +++ b/components/mqtt/exit-codes.js @@ -0,0 +1,61 @@ +// https://mosquitto.org/man/mosquitto_passwd-1.html#idm102 + +module.exports = (version = 3) => { + if (version >= 5) { + + return { + 0: "Success", + 128: "Unspecified error", + 129: "Malformed packet", + 130: "Protocol error", + 131: "Implementation specific error", + 132: "Unsupported protocol version", + 133: "Client ID not valid", + 134: "Bad username or password", + 135: "Not authorized", + 136: "Server unavailable", + 137: "Server busy", + 138: "Banned", + 139: "Server shutting down", + 140: "Bad authentication method", + 141: "Keep alive timeout", + 142: "Session taken over", + 143: "Topic filter invalid", + 144: "Topic name invalid", + 145: "", // unused + 146: "", // unused + 147: "Receive maximum exceeded", + 148: "Topic alias invalid", + 149: "Packet too large", + 150: "", // unused + 151: "Quota exceeded", + 152: "Administrative action", + 153: "Payload format invalid", + 154: "Retain not supported", + 155: "QoS not supported", + 156: "Use another server", + 157: "Server moved", + 158: "Shared subscriptions not supported", + 159: "Connection rate exceeded", + 160: "Maximum connect time", + 161: "Subscription IDs not supported", + 162: "Wildcard subscriptions not supported", + }; + + } else if (version >= 3) { + + return { + 0: "Success", + 1: "Connection refused: Bad protocol version", + 2: "Connection refused: Identifier rejected", + 3: "Connection refused: Identifier rejected", + 4: "Connection refused: Bad username/password", + 5: "Connection refused: Not authorized" + }; + + } else { + + throw new Error(`Unsupported protocol version "${version}"`); + + } +}; \ No newline at end of file diff --git a/components/mqtt/index.js b/components/mqtt/index.js new file mode 100644 index 0000000..10deecd --- /dev/null +++ b/components/mqtt/index.js @@ -0,0 +1,106 @@ +const mongodb = require("mongodb"); +const Joi = require("joi"); + +//const logger = require("../../system/logger").create("rooms"); +//const COMMON_COMPONENT = require("../../system/component/common.js"); +const COMPONENT = require("../../system/component/class.component.js"); + +const MQTT = require("./class.mqtt.js"); + +const messageHandler = require("./message-handler.js"); +//const packetHandler = require("./packet-handler.js"); + +/** + * @description + * Receives MQTT messages from broker.
+ * It can publish and subscribe to topics. + * The emitted events are a mix from mqtt & custom ones. + * + * NOTE: Currenlty no authentication is possible. + * + * @class C_MQTT + * @extends COMPONENT system/component/class.component.js + * + * @emits message Received message over websocket/tcp connection; Arguments: [0] = tcp message + * @emits connect + * @emits connack + * @emits subscribe + * @emits suback + * @emits unsubscribe + * @emits unsuback + * @emits publish Something was published; Arguments: [0] = payload (buffer); [1] = parsed packet + * @emits puback + * @emits pubrec + * @emits pubrel + * @emits pubcomp + * @emits pingreq Ping request + * @emits pingresp Ping response + * @emits disconnect + * @emits auth + * @emits connected When websocket connected; Arguments: [0] = WebSocket client object "ws" + * @emits disconnected When websocket disconnected; Arguments: [0] = WebSocket client object "ws" + * + * @link router.api.mqtt.js routes/router.api.mqtt.js + * @see https://en.wikipedia.org/wiki/MQTT + * @see https://www.npmjs.com/package/mqtt-packet + */ +class C_MQTT extends COMPONENT { + + constructor() { + + // inject logger, collection and schema object + super("mqtt", { + _id: Joi.string().pattern(/^[0-9a-fA-F]{24}$/).default(() => { + return String(new mongodb.ObjectId()); + }), + topic: Joi.string().required(), + description: Joi.string().allow(null).default(null) + }, module); + + this.hooks.post("add", (data, next) => { + next(null, new MQTT(data)); + }); + + this.collection.createIndex({ + topic: 1 + }, { + unique: true + }); + + // handle incoming messages + // triggers registerd callback for mdns items + messageHandler(this); + //packetHandler(this); + + } + +} + + +// create component instance +const instance = module.exports = new C_MQTT(); + + +// init component +// set items/build cache +instance.init((scope, ready) => { + scope.collection.find({}).toArray((err, data) => { + if (err) { + + // shit... + ready(err); + + } else { + + data = data.map((obj) => { + return new MQTT(obj); + }); + + scope.items.push(...data); + + // init done + ready(null); + + } + }); +}); \ No newline at end of file diff --git a/components/mqtt/message-handler.js b/components/mqtt/message-handler.js new file mode 100644 index 0000000..06994c6 --- /dev/null +++ b/components/mqtt/message-handler.js @@ -0,0 +1,152 @@ +const crypto = require("crypto"); +const mqtt = require("mqtt-packet"); + +const VERSION = Number(process.env.MQTT_BROKER_VERSION); + +const parser = mqtt.parser({ + protocolVersion: VERSION +}); + +const exitCodes = require("./exit-codes.js")(VERSION); + +module.exports = (scope) => { + scope._ready(({ logger, events }) => { + + // ping timer + let interval = null; + + events.on("connected", (ws) => { + + logger.debug("TCP socket connected to broker"); + + events.once("disconnected", () => { + clearInterval(interval); + logger.trace("Ping interval cleared"); + }); + + // TODO make this object configurable + let data = mqtt.generate({ + cmd: "connect", + protocolId: "MQTT", // Or "MQIsdp" in MQTT 3.1 and 5.0 + protocolVersion: VERSION, // Or 3 in MQTT 3.1, or 5 in MQTT 5.0 + clean: true, // Can also be false + clientId: process.env.MQTT_CLIENT_ID, + keepalive: 10, // Seconds which can be any positive number, with 0 as the default setting + /* + will: { + topic: "mydevice/test", + payload: Buffer.from("2134f"), // Payloads are buffers + + } + */ + }); + + ws.send(data); + + + events.on("publish", (packet) => { + scope.items.forEach(({ topic, _subscriber }) => { + + if (String(packet.topic).startsWith(topic) || packet.topic === topic) { + _subscriber.forEach((cb) => { + cb(packet.payload, packet); + }); + } + + }); + }); + + + events.once("connack", (packet) => { + if (packet.returnCode === 0) { + + events.once("suback", () => { + + logger.debug("Subscribed to topic #"); + + let ping = mqtt.generate({ + cmd: "pingreq" + }); + + interval = setInterval(() => { + ws.send(ping); + }, Number(process.env.MQTT_PING_INTERVAL)); + + // monkey patch publisher function + scope.items.forEach((item) => { + item._publisher = (payload) => { + + scope.logger.verbose(`Publish on topic ${item.topic}`, payload); + + let pub = mqtt.generate({ + cmd: "publish", + messageId: crypto.randomInt(0, 65535), + qos: 0, + dup: false, + topic: item.topic, + payload: Buffer.from(`${payload}`), + retain: false + }); + + ws.send(pub); + + }; + }); + + }); + + let sub = mqtt.generate({ + cmd: "subscribe", + messageId: crypto.randomInt(0, 65535), + /* + properties: { // MQTT 5.0 properties + subscriptionIdentifier: 145, + userProperties: { + test: "shellies" + } + }, + */ + subscriptions: [{ + topic: "#", + qos: 0, + nl: false, // no Local MQTT 5.0 flag + rap: true, // Retain as Published MQTT 5.0 flag + rh: 1 // Retain Handling MQTT 5.0 + }] + }); + + ws.send(sub); + + } + }); + + }); + + + parser.on("packet", (packet) => { + + logger.verbose("Packet received", packet); + + if (packet.cmd === "connack") { + if (packet.returnCode == 0) { + + logger.debug("Connected to broker"); + + } else { + + logger.warn(`Could not connecto to broker: "${exitCodes[packet.returnCode]}"`); + + } + } + + events.emit(packet.cmd, packet); + + }); + + + events.on("message", (message) => { + parser.parse(message); + }); + + }); +}; \ No newline at end of file diff --git a/index.js b/index.js index 8109e2e..08f1a21 100644 --- a/index.js +++ b/index.js @@ -56,7 +56,10 @@ process.env = Object.assign({ VAULT_SALT_BYTE_LEN: "16", USERS_BCRYPT_SALT_ROUNDS: "12", USERS_JWT_SECRET: "", - USERS_JWT_ALGORITHM: "HS384" + USERS_JWT_ALGORITHM: "HS384", + MQTT_BROKER_VERSION: "3", + MQTT_CLIENT_ID: "OpenHaus", + MQTT_PING_INTERVAL: "5000" }, env.parsed, process.env); @@ -201,7 +204,8 @@ const init_components = () => { "ssdp", "store", "users", - "vault" + "vault", + "mqtt" ].sort(() => { // pseudo randomize start/init of components diff --git a/package-lock.json b/package-lock.json index 5b1e434..49a9d46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "backend", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "backend", - "version": "1.0.0", + "version": "2.0.0", "hasInstallScript": true, "license": "ISC", "dependencies": { @@ -19,6 +19,7 @@ "joi": "^17.6.4", "jsonwebtoken": "^9.0.0", "mongodb": "^4.11.0", + "mqtt-packet": "^8.1.2", "uuid": "^9.0.0", "ws": "^8.10.0" }, @@ -1691,6 +1692,39 @@ "node": ">=8" } }, + "node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/bluebird": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", @@ -4683,6 +4717,37 @@ "node": ">=12" } }, + "node_modules/mqtt-packet": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-8.1.2.tgz", + "integrity": "sha512-vL1YTct+TAy0PqX3Jv8jM3JMzObH6vC/lyA0I5LtD4xvydOdIdmofrSp12PE3jajiIOUaW3XxmQekbyToXpsSw==", + "dependencies": { + "bl": "^5.0.0", + "debug": "^4.1.1", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/mqtt-packet/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mqtt-packet/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -5588,8 +5653,7 @@ "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "node_modules/proxy-addr": { "version": "2.0.7", @@ -8156,6 +8220,27 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "requires": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + } + } + }, "bluebird": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", @@ -10447,6 +10532,31 @@ } } }, + "mqtt-packet": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-8.1.2.tgz", + "integrity": "sha512-vL1YTct+TAy0PqX3Jv8jM3JMzObH6vC/lyA0I5LtD4xvydOdIdmofrSp12PE3jajiIOUaW3XxmQekbyToXpsSw==", + "requires": { + "bl": "^5.0.0", + "debug": "^4.1.1", + "process-nextick-args": "^2.0.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -11139,8 +11249,7 @@ "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "proxy-addr": { "version": "2.0.7", diff --git a/package.json b/package.json index d769021..d7ae471 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "joi": "^17.6.4", "jsonwebtoken": "^9.0.0", "mongodb": "^4.11.0", + "mqtt-packet": "^8.1.2", "uuid": "^9.0.0", "ws": "^8.10.0" }, @@ -52,4 +53,4 @@ "nodemon": "^2.0.19", "sinon": "^14.0.2" } -} \ No newline at end of file +} diff --git a/routes/index.js b/routes/index.js index 33129a5..cf0dd4b 100644 --- a/routes/index.js +++ b/routes/index.js @@ -10,6 +10,7 @@ const C_VAULT = require("../components/vault"); const C_SSDP = require("../components/ssdp"); const C_STORE = require("../components/store"); const C_USERS = require("../components/users"); +const C_MQTT = require("../components/mqtt"); // Remove due to issue #273 //const { encode } = require("../helper/sanitize"); @@ -127,6 +128,7 @@ module.exports = (server) => { const ssdpRouter = express.Router(); const storeRouter = express.Router(); const usersRouter = express.Router(); + const mqttRouter = express.Router(); // http://127.0.0.1/api/plugins api.use("/plugins", pluginsRouter); @@ -176,6 +178,13 @@ module.exports = (server) => { require("./rest-handler.js")(C_USERS, usersRouter); //require("./router.api.users.js")(app, vaultRouter); + // http://127.0.0.1/api/ssdp + api.use("/mqtt", mqttRouter); + require("./router.api.mqtt.js")(app, mqttRouter); + require("./rest-handler.js")(C_MQTT, mqttRouter); + + + // NOTE: Drop this?! api.use((req, res) => { res.status(404).end(); diff --git a/routes/router.api.mqtt.js b/routes/router.api.mqtt.js new file mode 100644 index 0000000..58f6fa9 --- /dev/null +++ b/routes/router.api.mqtt.js @@ -0,0 +1,80 @@ +const C_MQTT = require("../components/mqtt"); + +// external modules +const WebSocket = require("ws"); + +module.exports = (app, router) => { + + // websocket server + let wss = new WebSocket.Server({ + noServer: true + }); + + // detect broken connections + let interval = setInterval(() => { + wss.clients.forEach((ws) => { + + if (!ws.isAlive) { + ws.terminate(); + return; + } + + ws.isAlive = false; + ws.ping(); + + }); + }, Number(process.env.API_WEBSOCKET_TIMEOUT)); + + + // if the server closes + // clear the interval + wss.on("close", () => { + clearInterval(interval); + }); + + + // http route handler + router.get("/", (req, res, next) => { + + console.log("Request to /ai/mqtt"); + + // check if connection is a simple get request or ws client + if ((!req.headers["upgrade"] || !req.headers["connection"])) { + //return res.status(403).end(); + next(); // let the rest-handler.js do its job + } + + // listen for websockt clients + // keep sending new log entrys to client + wss.once("connection", (ws) => { + + C_MQTT.events.emit("connected", ws); + + ws.on("message", (msg) => { + C_MQTT.events.emit("message", msg); + }); + + ws.on("close", () => { + console.log("MQTT Client disconnected disolaskjdflaskjfdasdf"); + C_MQTT.events.emit("disconnected", ws); + }); + + }); + + // handle request as websocket + // perform websocket handshake + wss.handleUpgrade(req, req.socket, req.headers, (ws) => { + + ws.isAlive = true; + + ws.on("pong", () => { + ws.isAlive = true; + }); + + wss.emit("connection", ws, req); + + }); + + }); + +}; \ No newline at end of file diff --git a/tests/components/index.js b/tests/components/index.js index 1578c94..307ad70 100644 --- a/tests/components/index.js +++ b/tests/components/index.js @@ -7,7 +7,7 @@ describe("Components", () => { [ "devices", "endpoints", "plugins", "rooms", "ssdp", "store", "users", - "vault" + "vault", "mqtt" ].forEach((name) => { describe(name, () => { diff --git a/tests/components/mqtt.js b/tests/components/mqtt.js new file mode 100644 index 0000000..84a6ef2 --- /dev/null +++ b/tests/components/mqtt.js @@ -0,0 +1,139 @@ +const assert = require("assert"); +const mongodb = require("mongodb"); + +try { + + const C_COMPONENT = require(`../../components/mqtt/index.js`); + const MQTT = require("../../components/mqtt/class.mqtt.js"); + + const workflow = require("./test.workflow.js"); + + let _id = String(new mongodb.ObjectId()); + + + workflow(C_COMPONENT, "add", (done, { event }) => { + C_COMPONENT.add({ + _id, + topic: "air-sensor/", + }, (err, item) => { + try { + + // check event arguments + event.args.forEach((args) => { + assert.equal(args[0] instanceof MQTT, true); + }); + + assert.ok(err === null); + assert.equal(item instanceof MQTT, true); + assert.ok(item._publisher); + assert.ok(item._subscriber); + assert.equal(item.subscribe instanceof Function, true); + assert.equal(item.publish instanceof Function, true); + + done(err); + + } catch (err) { + + done(err); + + } + }); + }); + + + workflow(C_COMPONENT, "get", (done) => { + C_COMPONENT.get(_id, (err, item) => { + try { + + assert.ok(err === null); + assert.equal(item instanceof MQTT, true); + + done(err); + + } catch (err) { + + done(err); + + } + }); + }); + + + workflow(C_COMPONENT, "update", (done) => { + C_COMPONENT.update(_id, { + topic: "air-sensor/sensor/particulate_matter_25m_concentration/state" + }, (err, item) => { + try { + + assert.ok(err === null); + assert.equal(item instanceof MQTT, true); + assert.equal(item.topic, "air-sensor/sensor/particulate_matter_25m_concentration/state"); + assert.ok(item._publisher); + assert.ok(item._subscriber); + assert.equal(item.subscribe instanceof Function, true); + assert.equal(item.publish instanceof Function, true); + + done(err); + + } catch (err) { + + done(err); + + } + }); + }); + + + workflow(C_COMPONENT, "update", "Double update result / event arguments check", (done, { event }) => { + Promise.all([ + + // update call 1 + C_COMPONENT.update(_id, { + topic: `air-sensor/status` + }), + + // update call 2 + C_COMPONENT.update(_id, { + topic: `air-sensor/`, + description: "Ikea VINDRIKTNING Air sensor MQTT topic" + }), + + ]).then(() => { + + event.args.forEach((args) => { + assert.equal(args[0] instanceof MQTT, true); + }); + + done(); + + }).catch(done); + }); + + + workflow(C_COMPONENT, "remove", (done, { post }) => { + C_COMPONENT.remove(_id, (err, item) => { + try { + + // check post arguments item instance + post.args.forEach((args) => { + assert.equal(args[0] instanceof MQTT, true); + }); + + assert.ok(err === null); + assert.equal(item instanceof MQTT, true); + + done(err); + + } catch (err) { + + done(err); + + } + }); + }); + + +} catch (err) { + console.error(err); + process.exit(100); +} \ No newline at end of file From 7f21d6b3e35f585fd61784b43bbcc43f0e898f1f Mon Sep 17 00:00:00 2001 From: mStirner Date: Sun, 12 Feb 2023 18:45:00 +0100 Subject: [PATCH 11/30] Added `MQTT_BROKER_VERSION` to env variables. Fix tests --- tests/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/index.js b/tests/index.js index aee3a46..616cee4 100644 --- a/tests/index.js +++ b/tests/index.js @@ -35,7 +35,8 @@ process.env = Object.assign({ VAULT_SALT_BYTE_LEN: "16", USERS_BCRYPT_SALT_ROUNDS: "12", USERS_JWT_SECRET: "Pa$$w0rd", - USERS_JWT_ALGORITHM: "HS384" + USERS_JWT_ALGORITHM: "HS384", + MQTT_BROKER_VERSION: "3" }, env.parsed, process.env); From f0f0398d4b9974e26405660d57a7e7bfd6bbb08f Mon Sep 17 00:00:00 2001 From: mStirner Date: Sun, 12 Feb 2023 19:38:00 +0100 Subject: [PATCH 12/30] MDNS component added --- postman.json | 315 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 312 insertions(+), 3 deletions(-) diff --git a/postman.json b/postman.json index 327ead9..6e777b8 100644 --- a/postman.json +++ b/postman.json @@ -328,6 +328,315 @@ } ] }, + { + "name": "MDNS", + "item": [ + { + "name": "Create mdns entry", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"status code: 200\", () => {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Check room name: input = output\", () => {", + "", + " let res = pm.response.json();", + " let req = JSON.parse(pm.request.body);", + "", + " pm.expect(res.name).to.eql(req.name);", + "", + "});", + "", + "pm.test(\"Check properties\", () => {", + "", + " let res = pm.response.json();", + "", + " let props = [", + " \"name\", \"timestamps\", \"_id\",", + " \"number\", \"floor\", \"icon\"", + " ];", + "", + " Object.keys(res).forEach((key) => {", + " pm.expect(props.includes(key)).to.be.true;", + " });", + "", + " props.forEach((item) => {", + " pm.expect(Object.prototype.hasOwnProperty.call(res, item)).to.be.true;", + " });", + "", + "})" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"shelly*.local\",\n \"type\": \"A\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/mdns", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "mdns" + ] + } + }, + "response": [] + }, + { + "name": "Get all mdns targets", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"The response has all properties\", () => {", + " let json = pm.response.json();", + " pm.expect(json).to.have.lengthOf(json.length);", + "});", + "", + "pm.test(\"Status code is 200\", () => {", + " pm.response.to.have.status(200);", + "});", + "", + "console.log(\"Fooo\")" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "x-auth-token", + "value": "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImhhbnMuaHViZXJ0QGV4YW1wbGUuY29tIiwidXVpZCI6ImM3N2E3NjJkLWM4ODYtNGQ2My1iNGM1LWU0MDJhZGNmYTdiZSIsImlhdCI6MTY1NDI2ODI4NX0.w4mkvTuJ-OXzTcmvWhwIT84oOmo2399hSEfWGbA-9SUWndMWUiHvly1A7-kSV93e", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/mdns", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "mdns" + ] + } + }, + "response": [] + }, + { + "name": "Get sinlge mdns target", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/mdns/63e7f7ba26b161df7f3af1d6", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "mdns", + "63e7f7ba26b161df7f3af1d6" + ] + } + }, + "response": [] + }, + { + "name": "Update mdns target", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const HOST = pm.collectionVariables.get(\"HOST\");", + "const PORT = pm.collectionVariables.get(\"PORT\");", + "", + "//console.log(pm.request.url.toString())", + "", + "pm.sendRequest({", + " url: `http://${HOST}:${PORT}/api/rooms/`,", + " method: 'GET',", + "}, function (err, res) {", + " if(err){", + "", + " consle.error(err);", + "", + " }else {", + "", + " let data = res.json();", + " let key = Math.floor(Math.random()*data.length);", + " let item = data[key];", + "", + " pm.variables.set(\"_id\", item._id);", + "", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"labels\": [\n \"manufacturer=shelly\"\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/mdns/63e7f7ba26b161df7f3af1d6", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "mdns", + "63e7f7ba26b161df7f3af1d6" + ] + } + }, + "response": [] + }, + { + "name": "Delete mdns target", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "console.log(\"_id varaible\", pm.variables.get(\"_id\"));" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "console.log(\"_id varaible\", pm.variables.get(\"_id\"));", + "", + "pm.test(\"status code: 200\", () => {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/mdns/63e7f7ba26b161df7f3af1d6", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "mdns", + "63e7f7ba26b161df7f3af1d6" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + }, { "name": "SSDP", "item": [ @@ -1309,7 +1618,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"name\": \"AV - Receiver\"\n}", + "raw": " {\n \"_id\": \"63a5a4c2bd5fe7cb165960d0\",\n \"name\": \"Fernseher\",\n \"device\": \"63a5a4c2bd5fe7cb165960cd\",\n \"commands\": [\n {\n \"payload\": \"KEY_0\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_0\",\n \"name\": \"KEY_0\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d1\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_1\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_1\",\n \"name\": \"KEY_1\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d2\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_2\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_2\",\n \"name\": \"KEY_2\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d3\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_3\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_3\",\n \"name\": \"KEY_3\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d4\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_4\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_4\",\n \"name\": \"KEY_4\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d5\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_5\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_5\",\n \"name\": \"KEY_5\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d6\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_6\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_6\",\n \"name\": \"KEY_6\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d7\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_7\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_7\",\n \"name\": \"KEY_7\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d8\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_8\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_8\",\n \"name\": \"KEY_8\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d9\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_9\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_9\",\n \"name\": \"KEY_9\",\n \"_id\": \"63a5a4c2bd5fe7cb165960da\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_10\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_10\",\n \"name\": \"KEY_10\",\n \"_id\": \"63a5a4c2bd5fe7cb165960db\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_11\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_11\",\n \"name\": \"KEY_11\",\n \"_id\": \"63a5a4c2bd5fe7cb165960dc\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_12\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_12\",\n \"name\": \"KEY_12\",\n \"_id\": \"63a5a4c2bd5fe7cb165960dd\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_CHDOWN\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_CHDOWN\",\n \"name\": \"KEY_CHDOWN\",\n \"_id\": \"63a5a4c2bd5fe7cb165960de\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_CHUP\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_CHUP\",\n \"name\": \"KEY_CHUP\",\n \"_id\": \"63a5a4c2bd5fe7cb165960df\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_VOLDOWN\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_VOLDOWN\",\n \"name\": \"KEY_VOLDOWN\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e0\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_VOLUP\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_VOLUP\",\n \"name\": \"KEY_VOLUP\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e1\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_MUTE\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_MUTE\",\n \"name\": \"KEY_MUTE\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e2\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_SOURCE\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_SOURCE\",\n \"name\": \"KEY_SOURCE\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e3\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_LEFT\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_LEFT\",\n \"name\": \"KEY_LEFT\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e4\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_RIGHT\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_RIGHT\",\n \"name\": \"KEY_RIGHT\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e5\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_UP\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_UP\",\n \"name\": \"KEY_UP\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e6\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_DOWN\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_DOWN\",\n \"name\": \"KEY_DOWN\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e7\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_ENTER\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_ENTER\",\n \"name\": \"KEY_ENTER\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e8\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_MENU\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_MENU\",\n \"name\": \"KEY_MENU\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e9\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_EXIT\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_EXIT\",\n \"name\": \"KEY_EXIT\",\n \"_id\": \"63a5a4c2bd5fe7cb165960ea\",\n \"identifier\": null,\n \"description\": null\n }\n ],\n \"timestamps\": {\n \"created\": 1671800002321,\n \"updated\": null\n },\n \"enabled\": true,\n \"room\": \"62a4bbf0d9256b5e8d69889c\",\n \"states\": [],\n \"identifier\": null,\n \"icon\": \"fa-solid fa-tv\"\n }", "options": { "raw": { "language": "json" @@ -1317,7 +1626,7 @@ } }, "url": { - "raw": "http://{{HOST}}:{{PORT}}/api/endpoints/6266c441e207ba2a3e3c9222", + "raw": "http://{{HOST}}:{{PORT}}/api/endpoints/63a5a4c2bd5fe7cb165960d0", "protocol": "http", "host": [ "{{HOST}}" @@ -1326,7 +1635,7 @@ "path": [ "api", "endpoints", - "6266c441e207ba2a3e3c9222" + "63a5a4c2bd5fe7cb165960d0" ] } }, From a2783e87dc50a2a0f626874a6cb0d676e7ed8e00 Mon Sep 17 00:00:00 2001 From: mStirner Date: Sun, 12 Feb 2023 19:43:34 +0100 Subject: [PATCH 13/30] mqtt component added --- postman.json | 309 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 309 insertions(+) diff --git a/postman.json b/postman.json index 6e777b8..8206f2b 100644 --- a/postman.json +++ b/postman.json @@ -637,6 +637,315 @@ } ] }, + { + "name": "MQTT", + "item": [ + { + "name": "Create mqtt topic", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"status code: 200\", () => {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Check room name: input = output\", () => {", + "", + " let res = pm.response.json();", + " let req = JSON.parse(pm.request.body);", + "", + " pm.expect(res.name).to.eql(req.name);", + "", + "});", + "", + "pm.test(\"Check properties\", () => {", + "", + " let res = pm.response.json();", + "", + " let props = [", + " \"name\", \"timestamps\", \"_id\",", + " \"number\", \"floor\", \"icon\"", + " ];", + "", + " Object.keys(res).forEach((key) => {", + " pm.expect(props.includes(key)).to.be.true;", + " });", + "", + " props.forEach((item) => {", + " pm.expect(Object.prototype.hasOwnProperty.call(res, item)).to.be.true;", + " });", + "", + "})" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"topic\": \"air-sensor/sensor/particulate_matter_25m_concentration\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/mqtt", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "mqtt" + ] + } + }, + "response": [] + }, + { + "name": "Get all mqtt topics", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"The response has all properties\", () => {", + " let json = pm.response.json();", + " pm.expect(json).to.have.lengthOf(json.length);", + "});", + "", + "pm.test(\"Status code is 200\", () => {", + " pm.response.to.have.status(200);", + "});", + "", + "console.log(\"Fooo\")" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "x-auth-token", + "value": "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImhhbnMuaHViZXJ0QGV4YW1wbGUuY29tIiwidXVpZCI6ImM3N2E3NjJkLWM4ODYtNGQ2My1iNGM1LWU0MDJhZGNmYTdiZSIsImlhdCI6MTY1NDI2ODI4NX0.w4mkvTuJ-OXzTcmvWhwIT84oOmo2399hSEfWGbA-9SUWndMWUiHvly1A7-kSV93e", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/mqtt", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "mqtt" + ] + } + }, + "response": [] + }, + { + "name": "Get sinlge mqtt topic", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/mqtt/63e8f7d2ab413a9760e9b08c", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "mqtt", + "63e8f7d2ab413a9760e9b08c" + ] + } + }, + "response": [] + }, + { + "name": "Update mqtt topic", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const HOST = pm.collectionVariables.get(\"HOST\");", + "const PORT = pm.collectionVariables.get(\"PORT\");", + "", + "//console.log(pm.request.url.toString())", + "", + "pm.sendRequest({", + " url: `http://${HOST}:${PORT}/api/rooms/`,", + " method: 'GET',", + "}, function (err, res) {", + " if(err){", + "", + " consle.error(err);", + "", + " }else {", + "", + " let data = res.json();", + " let key = Math.floor(Math.random()*data.length);", + " let item = data[key];", + "", + " pm.variables.set(\"_id\", item._id);", + "", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"description\": \"Ikea VINDRIKTNING MQTT modd\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/mqtt/63e8f7d2ab413a9760e9b08c", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "mqtt", + "63e8f7d2ab413a9760e9b08c" + ] + } + }, + "response": [] + }, + { + "name": "Delete mqtt topic", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "console.log(\"_id varaible\", pm.variables.get(\"_id\"));" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "console.log(\"_id varaible\", pm.variables.get(\"_id\"));", + "", + "pm.test(\"status code: 200\", () => {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/mqtt/63e8f7d2ab413a9760e9b08c", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "mqtt", + "63e8f7d2ab413a9760e9b08c" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + }, { "name": "SSDP", "item": [ From ef7338ece8943821918aca5f30b5e74c2e0d7c4e Mon Sep 17 00:00:00 2001 From: mStirner Date: Sun, 12 Feb 2023 20:25:29 +0100 Subject: [PATCH 14/30] fix #295 --- helper/merge.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/helper/merge.js b/helper/merge.js index f816857..bfe03dd 100644 --- a/helper/merge.js +++ b/helper/merge.js @@ -18,9 +18,16 @@ function merge(dst, src) { let sourceArray = dst[key] || []; res[key] = cur.map((valObj, index) => { + + // fix #295 + if (!(typeof valObj === "object")) { + return valObj; + } + // this is simply merging on index, but if you wanted a "smarter" merge, you could look up // the dst by a specific key with sourceArray.find(...) return merge(sourceArray[index] || {}, valObj); + }); } else if (typeof cur === "object" && cur !== null) { From 823acea0b9d1a9c099acf918b6a7a44605c8f223 Mon Sep 17 00:00:00 2001 From: mStirner Date: Sun, 12 Feb 2023 20:34:32 +0100 Subject: [PATCH 15/30] fix #296 --- routes/router.api.mdns.js | 3 ++- routes/router.api.mqtt.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/routes/router.api.mdns.js b/routes/router.api.mdns.js index 9b38acf..c71a0af 100644 --- a/routes/router.api.mdns.js +++ b/routes/router.api.mdns.js @@ -38,12 +38,13 @@ module.exports = (app, router) => { // http route handler router.get("/", (req, res, next) => { - console.log("Request to /ai/mdns"); + console.log("Request to /api/mdns"); // check if connection is a simple get request or ws client if ((!req.headers["upgrade"] || !req.headers["connection"])) { //return res.status(403).end(); next(); // let the rest-handler.js do its job + return; } // listen for websockt clients diff --git a/routes/router.api.mqtt.js b/routes/router.api.mqtt.js index 58f6fa9..20a46a6 100644 --- a/routes/router.api.mqtt.js +++ b/routes/router.api.mqtt.js @@ -42,6 +42,7 @@ module.exports = (app, router) => { if ((!req.headers["upgrade"] || !req.headers["connection"])) { //return res.status(403).end(); next(); // let the rest-handler.js do its job + return; } // listen for websockt clients From 21bacb853407487f701cbeb293dd6b0cc032dd33 Mon Sep 17 00:00:00 2001 From: mStirner Date: Sun, 12 Feb 2023 20:49:26 +0100 Subject: [PATCH 16/30] Fix #294 --- system/component/class.component.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/system/component/class.component.js b/system/component/class.component.js index 572aa4a..d52a951 100644 --- a/system/component/class.component.js +++ b/system/component/class.component.js @@ -270,10 +270,17 @@ module.exports = class COMPONENT extends COMMON { */ this._defineMethod("add", (final) => { + let duplicate = false; + final((item) => { - //this.items.push(item); - items.push(item); + + // Fix #294 + if (!duplicate) { + items.push(item); + } + return Promise.resolve(); + }); return (data) => { @@ -324,6 +331,8 @@ module.exports = class COMPONENT extends COMMON { */ }); + duplicate = !!item; + // remove id when error occurs PENDING_CHANGE_EVENTS.delete(result.value._id); From 5e6b8162a874f8399ff3db519d9597555d1c70b2 Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Mon, 13 Feb 2023 11:30:54 +0100 Subject: [PATCH 17/30] Implement Webhooks, fix #180 --- components/webhooks/class.webhook.js | 45 ++ components/webhooks/index.js | 62 ++ index.js | 3 +- postman.json | 984 ++++++++++++++++++++++++++- routes/index.js | 7 + routes/router.api.webhooks.js | 38 ++ tests/components/index.js | 2 +- tests/components/webhooks.js | 134 ++++ 8 files changed, 1268 insertions(+), 7 deletions(-) create mode 100644 components/webhooks/class.webhook.js create mode 100644 components/webhooks/index.js create mode 100644 routes/router.api.webhooks.js create mode 100644 tests/components/webhooks.js diff --git a/components/webhooks/class.webhook.js b/components/webhooks/class.webhook.js new file mode 100644 index 0000000..331c156 --- /dev/null +++ b/components/webhooks/class.webhook.js @@ -0,0 +1,45 @@ +/** + * @description + * Represents a webhook item + * + * @class Webhook + * + * @param {Object} obj Object that matches the item schema. See properties below: + * + * @property {String} _id MongoDB Object is as string + * @property {String} name Webhook name + */ +module.exports = class Webhook { + + constructor(obj) { + + Object.assign(this, obj); + this._id = String(obj._id); + + Object.defineProperty(this, "_handler", { + value: [], + configurable: false, + enumerable: false, + writable: false + }); + + Object.defineProperty(this, "_trigger", { + value: (body, query) => { + + this._handler.forEach((cb) => { + cb(body, query); + }); + + }, + enumerable: false, + configurable: false, + writable: false + }); + + } + + handle(cb) { + this._handler.push(cb); + } + +}; \ No newline at end of file diff --git a/components/webhooks/index.js b/components/webhooks/index.js new file mode 100644 index 0000000..b5f6b10 --- /dev/null +++ b/components/webhooks/index.js @@ -0,0 +1,62 @@ +const mongodb = require("mongodb"); +const Joi = require("joi"); + +//const logger = require("../../system/logger").create("rooms"); +//const COMMON_COMPONENT = require("../../system/component/common.js"); +const COMPONENT = require("../../system/component/class.component.js"); + +const Webhook = require("./class.webhook.js"); + +/** + * @description + * Implement webhook functionality + * + * @class C_WEBHOOKS + * @extends COMPONENT system/component/class.component.js + */ +class C_WEBHOOKS extends COMPONENT { + constructor() { + + // inject logger, collection and schema object + super("webhooks", { + _id: Joi.string().pattern(/^[0-9a-fA-F]{24}$/).default(() => { + return String(new mongodb.ObjectId()); + }), + name: Joi.string().required() + }, module); + + this.hooks.post("add", (data, next) => { + next(null, new Webhook(data)); + }); + + } +} + + +// create component instance +const instance = module.exports = new C_WEBHOOKS(); + + +// init component +// set items/build cache +instance.init((scope, ready) => { + scope.collection.find({}).toArray((err, data) => { + if (err) { + + // shit... + ready(err); + + } else { + + data = data.map((obj) => { + return new Webhook(obj); + }); + + scope.items.push(...data); + + // init done + ready(null); + + } + }); +}); \ No newline at end of file diff --git a/index.js b/index.js index 8109e2e..78e0bad 100644 --- a/index.js +++ b/index.js @@ -201,7 +201,8 @@ const init_components = () => { "ssdp", "store", "users", - "vault" + "vault", + "webhooks" ].sort(() => { // pseudo randomize start/init of components diff --git a/postman.json b/postman.json index 327ead9..76e6e11 100644 --- a/postman.json +++ b/postman.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "7c990139-b42a-4603-9c76-841a6cda01aa", + "_postman_id": "87196fff-8d06-44e2-a0ce-626fa3a54a1b", "name": "OpenHaus", "description": "SmartHome/IoT application", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" @@ -328,6 +328,978 @@ } ] }, + { + "name": "Webhooks", + "item": [ + { + "name": "Create new webhook", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"status code: 200\", () => {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Check room name: input = output\", () => {", + "", + " let res = pm.response.json();", + " let req = JSON.parse(pm.request.body);", + "", + " pm.expect(res.name).to.eql(req.name);", + "", + "});", + "", + "pm.test(\"Check properties\", () => {", + "", + " let res = pm.response.json();", + "", + " let props = [", + " \"name\", \"timestamps\", \"_id\",", + " \"number\", \"floor\", \"icon\"", + " ];", + "", + " Object.keys(res).forEach((key) => {", + " pm.expect(props.includes(key)).to.be.true;", + " });", + "", + " props.forEach((item) => {", + " pm.expect(Object.prototype.hasOwnProperty.call(res, item)).to.be.true;", + " });", + "", + "})" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Triggerd from Shelly i3\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/webhooks", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "webhooks" + ] + } + }, + "response": [] + }, + { + "name": "Get all webhooks", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"The response has all properties\", () => {", + " let json = pm.response.json();", + " pm.expect(json).to.have.lengthOf(json.length);", + "});", + "", + "pm.test(\"Status code is 200\", () => {", + " pm.response.to.have.status(200);", + "});", + "", + "console.log(\"Fooo\")" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "x-auth-token", + "value": "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImhhbnMuaHViZXJ0QGV4YW1wbGUuY29tIiwidXVpZCI6ImM3N2E3NjJkLWM4ODYtNGQ2My1iNGM1LWU0MDJhZGNmYTdiZSIsImlhdCI6MTY1NDI2ODI4NX0.w4mkvTuJ-OXzTcmvWhwIT84oOmo2399hSEfWGbA-9SUWndMWUiHvly1A7-kSV93e", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/webhooks", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "webhooks" + ] + } + }, + "response": [] + }, + { + "name": "Get sinlge webhook", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/webhooks/63e9fe420d172b3b39313175", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "webhooks", + "63e9fe420d172b3b39313175" + ] + } + }, + "response": [] + }, + { + "name": "Update existing webhook", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const HOST = pm.collectionVariables.get(\"HOST\");", + "const PORT = pm.collectionVariables.get(\"PORT\");", + "", + "//console.log(pm.request.url.toString())", + "", + "pm.sendRequest({", + " url: `http://${HOST}:${PORT}/api/rooms/`,", + " method: 'GET',", + "}, function (err, res) {", + " if(err){", + "", + " consle.error(err);", + "", + " }else {", + "", + " let data = res.json();", + " let key = Math.floor(Math.random()*data.length);", + " let item = data[key];", + "", + " pm.variables.set(\"_id\", item._id);", + "", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Garage\",\n \"icon\": \"fa-solid fa-warehouse\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/webhooks/63e9fe420d172b3b39313175", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "webhooks", + "63e9fe420d172b3b39313175" + ] + } + }, + "response": [] + }, + { + "name": "Delete exisiting room", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "console.log(\"_id varaible\", pm.variables.get(\"_id\"));" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "console.log(\"_id varaible\", pm.variables.get(\"_id\"));", + "", + "pm.test(\"status code: 200\", () => {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/webhooks/63e9fe420d172b3b39313175", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "webhooks", + "63e9fe420d172b3b39313175" + ] + } + }, + "response": [] + }, + { + "name": "Trigger webhook", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"and\": \"also\",\n \"json\": \"data\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/webhooks/63e9fe420d172b3b39313175/trigger?optional=query¶meter=can&be=given", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "webhooks", + "63e9fe420d172b3b39313175", + "trigger" + ], + "query": [ + { + "key": "optional", + "value": "query" + }, + { + "key": "parameter", + "value": "can" + }, + { + "key": "be", + "value": "given" + } + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + }, + { + "name": "MDNS", + "item": [ + { + "name": "Create mdns entry", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"status code: 200\", () => {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Check room name: input = output\", () => {", + "", + " let res = pm.response.json();", + " let req = JSON.parse(pm.request.body);", + "", + " pm.expect(res.name).to.eql(req.name);", + "", + "});", + "", + "pm.test(\"Check properties\", () => {", + "", + " let res = pm.response.json();", + "", + " let props = [", + " \"name\", \"timestamps\", \"_id\",", + " \"number\", \"floor\", \"icon\"", + " ];", + "", + " Object.keys(res).forEach((key) => {", + " pm.expect(props.includes(key)).to.be.true;", + " });", + "", + " props.forEach((item) => {", + " pm.expect(Object.prototype.hasOwnProperty.call(res, item)).to.be.true;", + " });", + "", + "})" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"shelly*.local\",\n \"type\": \"A\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/mdns", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "mdns" + ] + } + }, + "response": [] + }, + { + "name": "Get all mdns targets", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"The response has all properties\", () => {", + " let json = pm.response.json();", + " pm.expect(json).to.have.lengthOf(json.length);", + "});", + "", + "pm.test(\"Status code is 200\", () => {", + " pm.response.to.have.status(200);", + "});", + "", + "console.log(\"Fooo\")" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "x-auth-token", + "value": "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImhhbnMuaHViZXJ0QGV4YW1wbGUuY29tIiwidXVpZCI6ImM3N2E3NjJkLWM4ODYtNGQ2My1iNGM1LWU0MDJhZGNmYTdiZSIsImlhdCI6MTY1NDI2ODI4NX0.w4mkvTuJ-OXzTcmvWhwIT84oOmo2399hSEfWGbA-9SUWndMWUiHvly1A7-kSV93e", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/mdns", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "mdns" + ] + } + }, + "response": [] + }, + { + "name": "Get sinlge mdns target", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/mdns/63e7f7ba26b161df7f3af1d6", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "mdns", + "63e7f7ba26b161df7f3af1d6" + ] + } + }, + "response": [] + }, + { + "name": "Update mdns target", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const HOST = pm.collectionVariables.get(\"HOST\");", + "const PORT = pm.collectionVariables.get(\"PORT\");", + "", + "//console.log(pm.request.url.toString())", + "", + "pm.sendRequest({", + " url: `http://${HOST}:${PORT}/api/rooms/`,", + " method: 'GET',", + "}, function (err, res) {", + " if(err){", + "", + " consle.error(err);", + "", + " }else {", + "", + " let data = res.json();", + " let key = Math.floor(Math.random()*data.length);", + " let item = data[key];", + "", + " pm.variables.set(\"_id\", item._id);", + "", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"labels\": [\n \"manufacturer=shelly\"\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/mdns/63e7f7ba26b161df7f3af1d6", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "mdns", + "63e7f7ba26b161df7f3af1d6" + ] + } + }, + "response": [] + }, + { + "name": "Delete mdns target", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "console.log(\"_id varaible\", pm.variables.get(\"_id\"));" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "console.log(\"_id varaible\", pm.variables.get(\"_id\"));", + "", + "pm.test(\"status code: 200\", () => {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/mdns/63e7f7ba26b161df7f3af1d6", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "mdns", + "63e7f7ba26b161df7f3af1d6" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + }, + { + "name": "MQTT", + "item": [ + { + "name": "Create mqtt topic", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"status code: 200\", () => {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Check room name: input = output\", () => {", + "", + " let res = pm.response.json();", + " let req = JSON.parse(pm.request.body);", + "", + " pm.expect(res.name).to.eql(req.name);", + "", + "});", + "", + "pm.test(\"Check properties\", () => {", + "", + " let res = pm.response.json();", + "", + " let props = [", + " \"name\", \"timestamps\", \"_id\",", + " \"number\", \"floor\", \"icon\"", + " ];", + "", + " Object.keys(res).forEach((key) => {", + " pm.expect(props.includes(key)).to.be.true;", + " });", + "", + " props.forEach((item) => {", + " pm.expect(Object.prototype.hasOwnProperty.call(res, item)).to.be.true;", + " });", + "", + "})" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"topic\": \"air-sensor/sensor/particulate_matter_25m_concentration\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/mqtt", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "mqtt" + ] + } + }, + "response": [] + }, + { + "name": "Get all mqtt topics", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"The response has all properties\", () => {", + " let json = pm.response.json();", + " pm.expect(json).to.have.lengthOf(json.length);", + "});", + "", + "pm.test(\"Status code is 200\", () => {", + " pm.response.to.have.status(200);", + "});", + "", + "console.log(\"Fooo\")" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "x-auth-token", + "value": "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImhhbnMuaHViZXJ0QGV4YW1wbGUuY29tIiwidXVpZCI6ImM3N2E3NjJkLWM4ODYtNGQ2My1iNGM1LWU0MDJhZGNmYTdiZSIsImlhdCI6MTY1NDI2ODI4NX0.w4mkvTuJ-OXzTcmvWhwIT84oOmo2399hSEfWGbA-9SUWndMWUiHvly1A7-kSV93e", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/mqtt", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "mqtt" + ] + } + }, + "response": [] + }, + { + "name": "Get sinlge mqtt topic", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/mqtt/63e8f7d2ab413a9760e9b08c", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "mqtt", + "63e8f7d2ab413a9760e9b08c" + ] + } + }, + "response": [] + }, + { + "name": "Update mqtt topic", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const HOST = pm.collectionVariables.get(\"HOST\");", + "const PORT = pm.collectionVariables.get(\"PORT\");", + "", + "//console.log(pm.request.url.toString())", + "", + "pm.sendRequest({", + " url: `http://${HOST}:${PORT}/api/rooms/`,", + " method: 'GET',", + "}, function (err, res) {", + " if(err){", + "", + " consle.error(err);", + "", + " }else {", + "", + " let data = res.json();", + " let key = Math.floor(Math.random()*data.length);", + " let item = data[key];", + "", + " pm.variables.set(\"_id\", item._id);", + "", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"description\": \"Ikea VINDRIKTNING MQTT modd\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/mqtt/63e8f7d2ab413a9760e9b08c", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "mqtt", + "63e8f7d2ab413a9760e9b08c" + ] + } + }, + "response": [] + }, + { + "name": "Delete mqtt topic", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "console.log(\"_id varaible\", pm.variables.get(\"_id\"));" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "console.log(\"_id varaible\", pm.variables.get(\"_id\"));", + "", + "pm.test(\"status code: 200\", () => {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "http://{{HOST}}:{{PORT}}/api/mqtt/63e8f7d2ab413a9760e9b08c", + "protocol": "http", + "host": [ + "{{HOST}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "mqtt", + "63e8f7d2ab413a9760e9b08c" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + }, { "name": "SSDP", "item": [ @@ -1309,7 +2281,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"name\": \"AV - Receiver\"\n}", + "raw": " {\n \"_id\": \"63a5a4c2bd5fe7cb165960d0\",\n \"name\": \"Fernseher\",\n \"device\": \"63a5a4c2bd5fe7cb165960cd\",\n \"commands\": [\n {\n \"payload\": \"KEY_0\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_0\",\n \"name\": \"KEY_0\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d1\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_1\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_1\",\n \"name\": \"KEY_1\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d2\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_2\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_2\",\n \"name\": \"KEY_2\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d3\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_3\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_3\",\n \"name\": \"KEY_3\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d4\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_4\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_4\",\n \"name\": \"KEY_4\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d5\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_5\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_5\",\n \"name\": \"KEY_5\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d6\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_6\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_6\",\n \"name\": \"KEY_6\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d7\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_7\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_7\",\n \"name\": \"KEY_7\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d8\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_8\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_8\",\n \"name\": \"KEY_8\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d9\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_9\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_9\",\n \"name\": \"KEY_9\",\n \"_id\": \"63a5a4c2bd5fe7cb165960da\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_10\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_10\",\n \"name\": \"KEY_10\",\n \"_id\": \"63a5a4c2bd5fe7cb165960db\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_11\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_11\",\n \"name\": \"KEY_11\",\n \"_id\": \"63a5a4c2bd5fe7cb165960dc\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_12\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_12\",\n \"name\": \"KEY_12\",\n \"_id\": \"63a5a4c2bd5fe7cb165960dd\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_CHDOWN\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_CHDOWN\",\n \"name\": \"KEY_CHDOWN\",\n \"_id\": \"63a5a4c2bd5fe7cb165960de\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_CHUP\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_CHUP\",\n \"name\": \"KEY_CHUP\",\n \"_id\": \"63a5a4c2bd5fe7cb165960df\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_VOLDOWN\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_VOLDOWN\",\n \"name\": \"KEY_VOLDOWN\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e0\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_VOLUP\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_VOLUP\",\n \"name\": \"KEY_VOLUP\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e1\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_MUTE\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_MUTE\",\n \"name\": \"KEY_MUTE\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e2\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_SOURCE\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_SOURCE\",\n \"name\": \"KEY_SOURCE\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e3\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_LEFT\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_LEFT\",\n \"name\": \"KEY_LEFT\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e4\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_RIGHT\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_RIGHT\",\n \"name\": \"KEY_RIGHT\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e5\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_UP\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_UP\",\n \"name\": \"KEY_UP\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e6\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_DOWN\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_DOWN\",\n \"name\": \"KEY_DOWN\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e7\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_ENTER\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_ENTER\",\n \"name\": \"KEY_ENTER\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e8\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_MENU\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_MENU\",\n \"name\": \"KEY_MENU\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e9\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_EXIT\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_EXIT\",\n \"name\": \"KEY_EXIT\",\n \"_id\": \"63a5a4c2bd5fe7cb165960ea\",\n \"identifier\": null,\n \"description\": null\n }\n ],\n \"timestamps\": {\n \"created\": 1671800002321,\n \"updated\": null\n },\n \"enabled\": true,\n \"room\": \"62a4bbf0d9256b5e8d69889c\",\n \"states\": [],\n \"identifier\": null,\n \"icon\": \"fa-solid fa-tv\"\n }", "options": { "raw": { "language": "json" @@ -1317,7 +2289,7 @@ } }, "url": { - "raw": "http://{{HOST}}:{{PORT}}/api/endpoints/6266c441e207ba2a3e3c9222", + "raw": "http://{{HOST}}:{{PORT}}/api/endpoints/63a5a4c2bd5fe7cb165960d0", "protocol": "http", "host": [ "{{HOST}}" @@ -1326,7 +2298,7 @@ "path": [ "api", "endpoints", - "6266c441e207ba2a3e3c9222" + "63a5a4c2bd5fe7cb165960d0" ] } }, @@ -2001,7 +2973,9 @@ "request": { "method": "GET", "header": [], - "url": null + "url": { + "raw": "" + } }, "response": [] } diff --git a/routes/index.js b/routes/index.js index 33129a5..c300932 100644 --- a/routes/index.js +++ b/routes/index.js @@ -10,6 +10,7 @@ const C_VAULT = require("../components/vault"); const C_SSDP = require("../components/ssdp"); const C_STORE = require("../components/store"); const C_USERS = require("../components/users"); +const C_WEBHOOKS = require("../components/webhooks"); // Remove due to issue #273 //const { encode } = require("../helper/sanitize"); @@ -127,6 +128,7 @@ module.exports = (server) => { const ssdpRouter = express.Router(); const storeRouter = express.Router(); const usersRouter = express.Router(); + const webhooksRouter = express.Router(); // http://127.0.0.1/api/plugins api.use("/plugins", pluginsRouter); @@ -176,6 +178,11 @@ module.exports = (server) => { require("./rest-handler.js")(C_USERS, usersRouter); //require("./router.api.users.js")(app, vaultRouter); + // http://127.0.0.1/api/webhooks + api.use("/webhooks", webhooksRouter); + require("./router.api.webhooks.js")(app, webhooksRouter); + require("./rest-handler.js")(C_WEBHOOKS, webhooksRouter); + // NOTE: Drop this?! api.use((req, res) => { res.status(404).end(); diff --git a/routes/router.api.webhooks.js b/routes/router.api.webhooks.js new file mode 100644 index 0000000..a60171a --- /dev/null +++ b/routes/router.api.webhooks.js @@ -0,0 +1,38 @@ +const C_WEBHOOKS = require("../components/webhooks"); + + +module.exports = (app, router) => { + + router.param("_id", (req, res, next, _id) => { + C_WEBHOOKS.get(_id, (err, obj) => { + if (err) { + + res.status(400).json({ + error: err + }); + + } else { + + if (!obj) { + return res.status(404).end(); + } + + req.item = obj; + + next(); + + } + }); + }); + + router.all("/:_id/trigger", (req, res) => { + + //res.end(`Hello from webhook: ${req.method}, ${JSON.stringify(req.item)}`); + + req.item._trigger(req.body, req.query); + + res.status(202).end(); + + }); + +}; \ No newline at end of file diff --git a/tests/components/index.js b/tests/components/index.js index 1578c94..b53a504 100644 --- a/tests/components/index.js +++ b/tests/components/index.js @@ -7,7 +7,7 @@ describe("Components", () => { [ "devices", "endpoints", "plugins", "rooms", "ssdp", "store", "users", - "vault" + "vault", "webhooks" ].forEach((name) => { describe(name, () => { diff --git a/tests/components/webhooks.js b/tests/components/webhooks.js new file mode 100644 index 0000000..d9173b1 --- /dev/null +++ b/tests/components/webhooks.js @@ -0,0 +1,134 @@ +const assert = require("assert"); +const mongodb = require("mongodb"); + +try { + + const C_COMPONENT = require(`../../components/webhooks/index.js`); + const Webhook = require("../../components/webhooks/class.webhook.js"); + + const workflow = require("./test.workflow.js"); + + let _id = String(new mongodb.ObjectId()); + + + workflow(C_COMPONENT, "add", (done, { event }) => { + C_COMPONENT.add({ + _id, + name: "Webhook #1", + }, (err, item) => { + try { + + // check event arguments + event.args.forEach((args) => { + assert.equal(args[0] instanceof Webhook, true); + }); + + assert.ok(err === null); + assert.equal(item instanceof Webhook, true); + assert.equal(item._handler instanceof Array, true); + assert.equal(item._trigger instanceof Function, true); + + done(err); + + } catch (err) { + + done(err); + + } + }); + }); + + + workflow(C_COMPONENT, "get", (done) => { + C_COMPONENT.get(_id, (err, item) => { + try { + + assert.ok(err === null); + assert.equal(item instanceof Webhook, true); + + done(err); + + } catch (err) { + + done(err); + + } + }); + }); + + + workflow(C_COMPONENT, "update", (done) => { + C_COMPONENT.update(_id, { + name: "Webhook #1 - updated", + }, (err, item) => { + try { + + assert.ok(err === null); + assert.equal(item instanceof Webhook, true); + assert.equal(item._handler instanceof Array, true); + assert.equal(item._trigger instanceof Function, true); + assert.equal(item.name, "Webhook #1 - updated"); + + done(err); + + } catch (err) { + + done(err); + + } + }); + }); + + + workflow(C_COMPONENT, "update", "Double update result / event arguments check", (done, { event }) => { + Promise.all([ + + // update call 1 + C_COMPONENT.update(_id, { + name: "New name", + }), + + // update call 2 + C_COMPONENT.update(_id, { + name: "New name - updated", + }) + + ]).then(() => { + + event.args.forEach((args) => { + assert.equal(args[0] instanceof Webhook, true); + }); + + done(); + + }).catch(done); + }); + + + workflow(C_COMPONENT, "remove", (done, { post }) => { + C_COMPONENT.remove(_id, (err, item) => { + try { + + // check post arguments item instance + post.args.forEach((args) => { + assert.equal(args[0] instanceof Webhook, true); + }); + + assert.ok(err === null); + assert.equal(item instanceof Webhook, true); + + done(err); + + } catch (err) { + + done(err); + + } + }); + }); + + +} catch (err) { + console.error(err); + process.exit(100); +} \ No newline at end of file From c37a9d756e35718e99596d0299e8326433351fce Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Mon, 13 Feb 2023 11:38:39 +0100 Subject: [PATCH 18/30] Update index.js --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 2a86b28..bfbb14d 100644 --- a/index.js +++ b/index.js @@ -205,7 +205,7 @@ const init_components = () => { "store", "users", "vault", - "webhooks" + "webhooks", "mqtt", "mdns" ].sort(() => { @@ -486,4 +486,4 @@ const starter = new Promise((resolve) => { logger.info("Startup complete"); -}); \ No newline at end of file +}); From 344c9822f151ff05f253dcc48e83c40fcd04f253 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Feb 2023 23:03:04 +0000 Subject: [PATCH 19/30] Bump @sideway/formula from 3.0.0 to 3.0.1 Bumps [@sideway/formula](https://github.com/sideway/formula) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/sideway/formula/releases) - [Commits](https://github.com/sideway/formula/compare/v3.0.0...v3.0.1) --- updated-dependencies: - dependency-name: "@sideway/formula" dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index d60e268..7c675f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1256,9 +1256,9 @@ } }, "node_modules/@sideway/formula": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", - "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" }, "node_modules/@sideway/pinpoint": { "version": "2.0.0", @@ -7883,9 +7883,9 @@ } }, "@sideway/formula": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", - "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" }, "@sideway/pinpoint": { "version": "2.0.0", From 45bca35866dcc545e1442940e9617b351bcb4549 Mon Sep 17 00:00:00 2001 From: mStirner Date: Thu, 16 Feb 2023 18:11:56 +0100 Subject: [PATCH 20/30] publish command added --- Gruntfile.js | 19 +++++++++++++++++++ package.json | 7 ++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 860a6a1..6cbaf4b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -94,6 +94,11 @@ module.exports = function (grunt) { `--build-arg buildDate=${Date.now()}`, ].join(" "); + cp.execSync(`docker build . -t openhaus/${pkg.name}:${pkg.version} ${buildArgs}`, { + env: process.env, + stdio: "inherit" + }); + cp.execSync(`docker build . -t openhaus/${pkg.name}:latest ${buildArgs}`, { env: process.env, stdio: "inherit" @@ -144,4 +149,18 @@ module.exports = function (grunt) { }); + grunt.registerTask("publish", () => { + [ + `docker push openhaus/${pkg.name}:${pkg.version}`, + `docker push openhaus/${pkg.name}:latest` + ].forEach((cmd) => { + cp.execSync(cmd, { + env: process.env, + stdio: "inherit" + }); + }); + + }); + + }; \ No newline at end of file diff --git a/package.json b/package.json index dd6e67a..8834647 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "backend", "private": true, - "version": "2.0.0", + "version": "2.1.0", "description": "", "main": "index.js", "scripts": { @@ -18,7 +18,8 @@ "release": "grunt release", "db:export": "mongodump --db OpenHaus --archive=./demo-database.gz", "db:import": "mongorestore --db OpenHaus --archive=./demo-database.gz", - "postinstall": "scripts/post-install.sh" + "postinstall": "scripts/post-install.sh", + "publish": "grunt publish" }, "engines": { "node": ">=0.16.0" @@ -54,4 +55,4 @@ "nodemon": "^2.0.19", "sinon": "^14.0.2" } -} +} \ No newline at end of file From 0d3251fb0fae4f044250edc426f4544bc5148753 Mon Sep 17 00:00:00 2001 From: mStirner Date: Mon, 20 Feb 2023 17:41:29 +0100 Subject: [PATCH 21/30] Fix #303 --- components/plugins/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/plugins/index.js b/components/plugins/index.js index bbc0538..77153bf 100644 --- a/components/plugins/index.js +++ b/components/plugins/index.js @@ -47,7 +47,7 @@ class C_PLUGINS extends COMPONENT { //runlevel: Joi.number().min(0).max(2).default(0), autostart: Joi.boolean().default(true), enabled: Joi.boolean().default(true), - intents: Joi.array().items("devices", "endpoints", "plugins", "rooms", "ssdp", "store", "users", "vault").required() + intents: Joi.array().items("devices", "endpoints", "plugins", "rooms", "ssdp", "store", "users", "vault", "mqtt", "mdns", "webhooks").required() }, module); this.hooks.post("add", (data, next) => { From bd671910b29f1fa81a38f4bc6b98d38856bb11dc Mon Sep 17 00:00:00 2001 From: mStirner Date: Mon, 20 Feb 2023 18:35:29 +0100 Subject: [PATCH 22/30] Fix #305 --- components/mqtt/message-handler.js | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/components/mqtt/message-handler.js b/components/mqtt/message-handler.js index 06994c6..7800041 100644 --- a/components/mqtt/message-handler.js +++ b/components/mqtt/message-handler.js @@ -15,6 +15,19 @@ module.exports = (scope) => { // ping timer let interval = null; + events.on("publish", (packet) => { + scope.items.forEach(({ topic, _subscriber }) => { + + if (String(packet.topic).startsWith(topic) || packet.topic === topic) { + _subscriber.forEach((cb) => { + cb(packet.payload, packet); + }); + } + + }); + }); + + events.on("connected", (ws) => { logger.debug("TCP socket connected to broker"); @@ -36,27 +49,13 @@ module.exports = (scope) => { will: { topic: "mydevice/test", payload: Buffer.from("2134f"), // Payloads are buffers - + } */ }); ws.send(data); - - events.on("publish", (packet) => { - scope.items.forEach(({ topic, _subscriber }) => { - - if (String(packet.topic).startsWith(topic) || packet.topic === topic) { - _subscriber.forEach((cb) => { - cb(packet.payload, packet); - }); - } - - }); - }); - - events.once("connack", (packet) => { if (packet.returnCode === 0) { From 69e542895a9fe6f7930c6f91e9dad6c4c213a602 Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Fri, 24 Feb 2023 10:46:15 +0100 Subject: [PATCH 23/30] fix #306 --- system/component/class.component.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/system/component/class.component.js b/system/component/class.component.js index d52a951..9d4d4ad 100644 --- a/system/component/class.component.js +++ b/system/component/class.component.js @@ -572,12 +572,13 @@ module.exports = class COMPONENT extends COMMON { */ this._defineMethod("find", () => { return (query) => { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { // https://javascript.plainenglish.io/4-ways-to-compare-objects-in-javascript-97fe9b2a949c // https://stackoverflow.com/a/1068883/5781499 // https://dmitripavlutin.com/how-to-compare-objects-in-javascript/ + /* let item = this.items.find((item) => { // for (let key of query) { ?! for (let key in Object.keys(query)) { @@ -596,6 +597,28 @@ module.exports = class COMPONENT extends COMMON { } resolve([item]); + */ + + // fix #306 + let item = this.items.find((item) => { + + let loop = (target, filter) => { + return Object.keys(filter).every((key) => { + + if (target[key] instanceof Object) { + return loop(target[key], filter[key]); + } else { + return target[key] === filter[key]; + } + + }); + }; + + return loop(item, query); + + }); + + resolve([item || null]); }); }; From a6265a952eb822a8a5d3c1045f9ea6aa7033794f Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Fri, 24 Feb 2023 17:04:45 +0100 Subject: [PATCH 24/30] fix #301 --- system/logger/index.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/system/logger/index.js b/system/logger/index.js index bdb17cb..fedf82b 100644 --- a/system/logger/index.js +++ b/system/logger/index.js @@ -18,7 +18,9 @@ Object.defineProperty(logger, "create", { value: function create(name) { let file = path.resolve(process.env.LOG_PATH, `${name}.log`); - let stream = createWriteStream(file); + let stream = createWriteStream(file, { + flags: "a" + }); stream.on("error", (err) => { console.error(err); @@ -51,8 +53,12 @@ Object.defineProperty(logger, "create", { * ``` */ -const system = createWriteStream(path.resolve(process.env.LOG_PATH, "system.log")); -const combined = createWriteStream(path.resolve(process.env.LOG_PATH, "combined.log")); +const system = createWriteStream(path.resolve(process.env.LOG_PATH, "system.log"), { + flags: "a" +}); +const combined = createWriteStream(path.resolve(process.env.LOG_PATH, "combined.log"), { + flags: "a" +}); [system, combined].forEach((stream) => { stream.on("error", (err) => { @@ -106,7 +112,9 @@ Object.defineProperty(logger, "create", { }); } - let stream = createWriteStream(file); + let stream = createWriteStream(file, { + flags: "a" + }); stream.on("error", (err) => { console.error(err); From 78ebcacead37e11275a743403b3eafe6d58db77f Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Fri, 24 Feb 2023 17:22:03 +0100 Subject: [PATCH 25/30] Fix #308 --- components/mdns/message-handler.js | 25 +++++++++++++++++++++++++ routes/router.api.mdns.js | 2 ++ 2 files changed, 27 insertions(+) diff --git a/components/mdns/message-handler.js b/components/mdns/message-handler.js index 7db87a0..20d3dcc 100644 --- a/components/mdns/message-handler.js +++ b/components/mdns/message-handler.js @@ -1,3 +1,5 @@ +const { encode, RECURSION_DESIRED } = require("dns-packet"); + module.exports = (scope) => { scope._ready(({ logger }) => { @@ -22,6 +24,29 @@ module.exports = (scope) => { }); + scope.events.on("connected", (ws) => { + + let questions = scope.items.map(({ type, name }) => { + return { + type, + name + }; + }); + + let query = encode({ + type: "query", + id: 1, + flags: RECURSION_DESIRED, + questions + }); + + logger.debug("Connected, send query", query, questions); + + ws.send(query); + + }); + + scope.events.on("message", (packet, message) => { // feedback diff --git a/routes/router.api.mdns.js b/routes/router.api.mdns.js index c71a0af..a5895d3 100644 --- a/routes/router.api.mdns.js +++ b/routes/router.api.mdns.js @@ -53,6 +53,8 @@ module.exports = (app, router) => { console.log("Clien connected to mdns"); + C_MDNS.events.emit("connected", ws); + ws.on("message", (msg) => { C_MDNS.events.emit("message", decode(msg), msg); }); From c9d7cc783baaba9eca71ab3f5e126d41a76de297 Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Fri, 24 Feb 2023 17:22:23 +0100 Subject: [PATCH 26/30] See #307 --- system/component/class.component.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/system/component/class.component.js b/system/component/class.component.js index 9d4d4ad..5abc169 100644 --- a/system/component/class.component.js +++ b/system/component/class.component.js @@ -685,6 +685,32 @@ module.exports = class COMPONENT extends COMMON { }; + + /* + // potential fix for #307 + // the problem is not the method, but the wildcard in labels + let handler = (filter, item) => { + + let loop = (target, filter) => { + return Object.keys(filter).every((key) => { + + if (target[key] instanceof Object) { + return loop(target[key], filter[key]); + } else { + return target[key] === filter[key]; + } + + }); + }; + + if (loop(item, filter)) { + matched = true; + cb(item); + } + + }; + */ + this.items.forEach((item) => { handler(filter, item); }); From 1ad16dd9c392ac0eb262b8c139695ad6f482d5d9 Mon Sep 17 00:00:00 2001 From: mStirner Date: Fri, 24 Feb 2023 19:14:14 +0100 Subject: [PATCH 27/30] fix #311 --- adapter/base64.js | 41 -------------------------- adapter/json.js | 42 --------------------------- components/devices/class.interface.js | 2 +- 3 files changed, 1 insertion(+), 84 deletions(-) delete mode 100644 adapter/base64.js delete mode 100644 adapter/json.js diff --git a/adapter/base64.js b/adapter/base64.js deleted file mode 100644 index 358419a..0000000 --- a/adapter/base64.js +++ /dev/null @@ -1,41 +0,0 @@ -const { Transform } = require("stream"); -const logger = require("../system/logger").create("adapter/base64"); - -module.exports = (options) => { - - const encode = new Transform({ - transform(chunk, encoding, cb) { - - let data = chunk.toString("base64"); - - logger.verbose("[encode]: %j", data); - - this.push(data); - - cb(); - - }, - ...options - }); - - const decode = new Transform({ - transform(chunk, encoding, cb) { - - let data = Buffer.from(chunk.toString(), "base64").toString("ascii"); - - logger.verbose("[decode]: %j", data); - - this.push(data); - - cb(); - - }, - ...options - }); - - return { - encode, - decode - }; - -}; \ No newline at end of file diff --git a/adapter/json.js b/adapter/json.js deleted file mode 100644 index 7e80e09..0000000 --- a/adapter/json.js +++ /dev/null @@ -1,42 +0,0 @@ -const { Transform } = require("stream"); -const logger = require("../system/logger").create("adapter/json"); - -module.exports = (options) => { - - const encode = new Transform({ - transform(chunk, encoding, cb) { - - let data = JSON.stringify(chunk); - - logger.verbose("[encode] %d", data); - - this.push(data); - - cb(); - - }, - ...options - }); - - const decode = new Transform({ - transform(chunk, encoding, cb) { - - let data = JSON.parse(chunk); - - logger.verbose("[decode] %d", data); - - this.push(data); - - cb(); - - }, - ...options - - }); - - return { - encode, - decode - }; - -}; \ No newline at end of file diff --git a/components/devices/class.interface.js b/components/devices/class.interface.js index 985ad3b..fa1024e 100644 --- a/components/devices/class.interface.js +++ b/components/devices/class.interface.js @@ -87,7 +87,7 @@ module.exports = class Interface { is: "SERIAL", then: SERIAL }), - adapter: Joi.array().items("base64", "eiscp", "json", "raw").default(["raw"]), + adapter: Joi.array().items("eiscp", "raw").default(["raw"]), description: Joi.string().allow(null).default(null) }); From 3be64f3308bfde8f3f4521b677664d0e57bf9ea2 Mon Sep 17 00:00:00 2001 From: mStirner Date: Fri, 24 Feb 2023 19:24:45 +0100 Subject: [PATCH 28/30] fix #310 --- components/devices/class.interface.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/devices/class.interface.js b/components/devices/class.interface.js index fa1024e..aff2909 100644 --- a/components/devices/class.interface.js +++ b/components/devices/class.interface.js @@ -108,7 +108,7 @@ module.exports = class Interface { options = Object.assign({ keepAlive: true, - maxSockets: 1, + //maxSockets: 1, keepAliveMsecs: 3000, // use this as websocket ping/pong value to detect broken connections? }, options); From 6ecdb03ee88863de675081a01f6f2c81d4bca82c Mon Sep 17 00:00:00 2001 From: mStirner Date: Sat, 25 Feb 2023 18:04:59 +0100 Subject: [PATCH 29/30] fix #309 --- components/plugins/class.plugin.js | 13 ++++++++++--- index.js | 2 +- routes/router.api.plugins.js | 28 ++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/components/plugins/class.plugin.js b/components/plugins/class.plugin.js index 335413b..1b2129e 100644 --- a/components/plugins/class.plugin.js +++ b/components/plugins/class.plugin.js @@ -29,10 +29,10 @@ class Plugin { } /** - * @function boot - * Start/boot installed plugin + * @function start + * Start installed plugin */ - boot() { + start() { if (this.enabled) { let plugin = path.resolve(process.cwd(), "plugins", this.uuid); @@ -116,6 +116,13 @@ class Plugin { } } + /* + stop(){ + // TODO: Implement + // When plugins run in seperate worker process + } + */ + } module.exports = Plugin; \ No newline at end of file diff --git a/index.js b/index.js index bfbb14d..4a53e58 100644 --- a/index.js +++ b/index.js @@ -467,7 +467,7 @@ const starter = new Promise((resolve) => { logger.verbose(`Start plugin "${plugin.name}" (${plugin.uuid})`); - plugin.boot(); + plugin.start(); started += 1; diff --git a/routes/router.api.plugins.js b/routes/router.api.plugins.js index 4609b1d..2b5c653 100644 --- a/routes/router.api.plugins.js +++ b/routes/router.api.plugins.js @@ -59,4 +59,32 @@ module.exports = (app, router) => { } }); + router.post("/:_id/start", (req, res) => { + try { + + req.item.start(); + res.json(req.item); + + } catch (err) { + + res.status(500).end(err); + + } + }); + + /* + router.post("/:_id/stop", (req, res) => { + try { + + req.item.stop(); + res.json(req.item); + + } catch (err) { + + res.status(500).end(err); + + } + }); + */ + }; \ No newline at end of file From ba4e1932d0496f8a828ee35e20b34298007c6099 Mon Sep 17 00:00:00 2001 From: mStirner Date: Sat, 25 Feb 2023 18:05:05 +0100 Subject: [PATCH 30/30] added --- backend.service | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 backend.service diff --git a/backend.service b/backend.service new file mode 100644 index 0000000..6d163d2 --- /dev/null +++ b/backend.service @@ -0,0 +1,17 @@ +[Unit] +Description=OpenHaus Backend +Wants=network-online.target +After=network-online.target + +[Service] +ExecStart=/usr/bin/node /opt/OpenHaus/backend/index.js +WorkingDirectory=/opt/OpenHaus/backend +Restart=always +RestartSec=10 +Environment=NODE_ENV=production +Environment=VAULT_MASTER_PASSWORD=Pa$$w0rd +Environment=USERS_JWT_SECRET=Pa$$w0rd +Environment=UUID=00000000-0000-0000-0000-000000000000 + +[Install] +WantedBy=multi-user.target \ No newline at end of file