From 499be46588590be226667a4237eddff6c0dde538 Mon Sep 17 00:00:00 2001 From: Aleksandr Gumroian Date: Thu, 5 Sep 2024 12:50:08 +0200 Subject: [PATCH 1/2] Using socket.io for websocket handling and make reforis configurable Socket.io wrapper is used to handle websockets now, this means that websocket logic had to be redone. Also it is necessary to set `REFORIS_PREFIX` env variable during the build process. To set the path of backend url. It was previously fixed to `/reforis`. --- package-lock.json | 124 +++++++++++++++++-- package.json | 3 +- src/common/WiFiSettings/ResetWiFiSettings.js | 2 +- src/utils/forisUrls.js | 4 +- src/webSockets/WebSockets.js | 86 ++++--------- src/webSockets/hooks.js | 2 +- 6 files changed, 144 insertions(+), 77 deletions(-) diff --git a/package-lock.json b/package-lock.json index 07bd196..f59a412 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,8 @@ "moment": "^2.30.1", "qrcode.react": "^3.1.0", "react-datetime": "^3.2.0", - "react-uid": "^2.3.3" + "react-uid": "^2.3.3", + "socket.io-client": "^4.6.1" }, "devDependencies": { "@babel/cli": "^7.24.7", @@ -3583,6 +3584,12 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@testing-library/dom": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-5.6.1.tgz", @@ -6372,7 +6379,6 @@ "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -6722,6 +6728,28 @@ "node": ">= 0.8" } }, + "node_modules/engine.io-client": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", + "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.17.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", @@ -13948,8 +13976,7 @@ "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==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/multicast-dns": { "version": "7.2.5", @@ -16552,6 +16579,34 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, + "node_modules/socket.io-client": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", + "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -18360,7 +18415,6 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -18393,6 +18447,14 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -21053,6 +21115,11 @@ "@sinonjs/commons": "^3.0.0" } }, + "@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, "@testing-library/dom": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-5.6.1.tgz", @@ -23262,7 +23329,6 @@ "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dev": true, "requires": { "ms": "2.1.2" } @@ -23522,6 +23588,23 @@ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "dev": true }, + "engine.io-client": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", + "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==" + }, "enhanced-resolve": { "version": "5.17.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", @@ -28947,8 +29030,7 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "multicast-dns": { "version": "7.2.5", @@ -30877,6 +30959,26 @@ } } }, + "socket.io-client": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", + "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + } + }, + "socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + } + }, "sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -32198,7 +32300,6 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "dev": true, "requires": {} }, "xml-name-validator": { @@ -32213,6 +32314,11 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==" + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index 749ae12..2f5e447 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "moment": "^2.30.1", "qrcode.react": "^3.1.0", "react-datetime": "^3.2.0", - "react-uid": "^2.3.3" + "react-uid": "^2.3.3", + "socket.io-client": "^4.6.1" }, "peerDependencies": { "bootstrap": "^5.3.3", diff --git a/src/common/WiFiSettings/ResetWiFiSettings.js b/src/common/WiFiSettings/ResetWiFiSettings.js index 0c227a3..8bbf586 100644 --- a/src/common/WiFiSettings/ResetWiFiSettings.js +++ b/src/common/WiFiSettings/ResetWiFiSettings.js @@ -26,7 +26,7 @@ function ResetWiFiSettings({ ws, endpoint }) { useEffect(() => { const module = "wifi"; - ws.subscribe(module).bind(module, "reset", () => { + ws.bind(module, "reset", () => { // eslint-disable-next-line no-restricted-globals setTimeout(() => location.reload(), 1000); }); diff --git a/src/utils/forisUrls.js b/src/utils/forisUrls.js index 0232d2a..9f6ab73 100644 --- a/src/utils/forisUrls.js +++ b/src/utils/forisUrls.js @@ -1,11 +1,11 @@ /* - * Copyright (C) 2019-2021 CZ.NIC z.s.p.o. (http://www.nic.cz/) + * Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/) * * This is free software, licensed under the GNU General Public License v3. * See /LICENSE for more information. */ -export const REFORIS_URL_PREFIX = "/reforis"; +export const REFORIS_URL_PREFIX = process.env.REFORIS_PREFIX || ""; export const REFORIS_API_URL_PREFIX = `${REFORIS_URL_PREFIX}/api`; export const ForisURLs = { diff --git a/src/webSockets/WebSockets.js b/src/webSockets/WebSockets.js index cdf4fec..51ad889 100644 --- a/src/webSockets/WebSockets.js +++ b/src/webSockets/WebSockets.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2022 CZ.NIC z.s.p.o. (http://www.nic.cz/) + * Copyright (C) 2020-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/) * * This is free software, licensed under the GNU General Public License v3. * See /LICENSE for more information. @@ -7,47 +7,33 @@ /* eslint no-console: "off" */ -const PROTOCOL = window.location.protocol === "http:" ? "ws" : "wss"; +import { io } from "socket.io-client"; -const URL = process.env.LIGHTTPD - ? `${PROTOCOL}://${window.location.host}/${process.env.WSPATH || "foris-ws"}` - : `${PROTOCOL}://${window.location.hostname}:9081`; - -const WAITING_FOR_CONNECTION_TIMEOUT = 500; +import { REFORIS_URL_PREFIX } from "../utils/forisUrls"; class WebSockets { constructor() { - this.ws = new WebSocket(URL); - this.ws.onerror = (e) => { - console.error("WS: Error:", e); - }; - this.ws.onmessage = (e) => { - console.debug(`WS: Received Message: ${e.data}`); - const data = JSON.parse(e.data); - this.dispatch(data); - }; - this.ws.onopen = () => { - console.debug("WS: Connection open."); - }; - this.ws.onclose = () => { - console.debug("WS: Connection closed."); - }; + this.socket = io("/notifications", { + path: `${REFORIS_URL_PREFIX}/reforis-ws`, + }); + this.connection = null; + this.socket.on("disconnect", (reason) => { + this.connection = null; + console.debug(`SocketIO disconnected (${reason})`); + }); + this.socket.on("notification", (message) => { + console.debug("WS: Received Message:", message); + this.dispatch(message); + }); + this.socket.on("connect", (connection) => { + this.connection = connection; + console.debug(`SocketIO connected.`); + }); // callbacks[module][action] this.callbacks = {}; } - waitForConnection(callback) { - if (this.ws.readyState === 1) { - callback(); - } else { - const that = this; - setTimeout(() => { - that.waitForConnection(callback); - }, WAITING_FOR_CONNECTION_TIMEOUT); - } - } - bind(module, action, callback) { this.callbacks[module] = this.callbacks[module] || {}; this.callbacks[module][action] = this.callbacks[module][action] || []; @@ -55,13 +41,6 @@ class WebSockets { return this; } - subscribe(module) { - this.waitForConnection(() => { - this.send("subscribe", module); - }); - return this; - } - unbind(module, action, callback) { const callbacks = this.callbacks[module][action]; @@ -75,28 +54,12 @@ class WebSockets { } if (Object.keys(this.callbacks[module]).length === 0) { - this.unsubscribe(module); + delete this.callbacks[module]; } return this; } - unsubscribe(module) { - this.waitForConnection(() => { - this.send("unsubscribe", module); - delete this.callbacks[module]; - }); - return this; - } - - send(action, params) { - const payload = JSON.stringify({ action, params }); - this.waitForConnection(() => { - this.ws.send(payload); - }); - return this; - } - dispatch(json) { if (!json.module) return; @@ -105,20 +68,17 @@ class WebSockets { chain = this.callbacks[json.module][json.action]; } catch (error) { if (error instanceof TypeError) { - console.warn( - `Callback for this message wasn't found:${error.data}` + console.debug( + `Callbacks for this module wasn't found: ${json.module}` ); } else throw error; } if (typeof chain === "undefined") return; + console.debug("Handling WS message", json); chain.forEach((callback) => callback(json)); } - - close() { - this.ws.close(); - } } export default WebSockets; diff --git a/src/webSockets/hooks.js b/src/webSockets/hooks.js index c75e0a8..f6a7a04 100644 --- a/src/webSockets/hooks.js +++ b/src/webSockets/hooks.js @@ -33,7 +33,7 @@ function useWSForisModule( setData(message.data); } - ws.subscribe(module).bind(module, action, callback); + ws.bind(module, action, callback); return () => { ws.unbind(module, action, callback); From cc1389536e581d4ceaa0179a65ed16084a9fe6b5 Mon Sep 17 00:00:00 2001 From: Aleksandr Gumroian Date: Thu, 5 Sep 2024 13:01:34 +0200 Subject: [PATCH 2/2] Fix tests --- src/common/__tests__/RebootButton.test.js | 2 +- src/testUtils/mockGlobals.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/common/__tests__/RebootButton.test.js b/src/common/__tests__/RebootButton.test.js index 74c5b8e..8ffb440 100644 --- a/src/common/__tests__/RebootButton.test.js +++ b/src/common/__tests__/RebootButton.test.js @@ -46,7 +46,7 @@ describe("", () => { fireEvent.click(getByText(componentContainer, "Reboot")); fireEvent.click(getByText(componentContainer, "Confirm reboot")); expect(mockAxios.post).toHaveBeenCalledWith( - "/reforis/api/reboot", + "/api/reboot", undefined, expect.anything() ); diff --git a/src/testUtils/mockGlobals.js b/src/testUtils/mockGlobals.js index f7818e3..5c5f83e 100644 --- a/src/testUtils/mockGlobals.js +++ b/src/testUtils/mockGlobals.js @@ -10,3 +10,4 @@ global._ = (str) => str; global.ngettext = (str) => str; global.babel = { format: (str) => str }; global.ForisTranslations = { locale: "en" }; +global.setImmediate = (fn) => setTimeout(fn, 0); \ No newline at end of file