From e2001a2816e0dccdd57bb2bdc6907ce62581484b Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Sun, 1 Dec 2024 21:20:04 +0100 Subject: [PATCH 1/8] Added live query logging --- Framework/Backend/websocket/server.js | 38 ++++++++++++++++++++++++ InfoLogger/public/logFilter/LogFilter.js | 7 ++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/Framework/Backend/websocket/server.js b/Framework/Backend/websocket/server.js index 75be7a1c9..6fdf1e9e4 100644 --- a/Framework/Backend/websocket/server.js +++ b/Framework/Backend/websocket/server.js @@ -133,6 +133,42 @@ class WebSocket { client.on('error', (err) => this.logger.error(`Connection ${err.code}`)); } + /** + * Make criteria more readable. + * This code is a close copy of InfoLogger/public/logFilter/LogFilter.js LN 101 toObject() + * @param {object} criteria - criteria to be minified + * @returns {object} minimal filter object + */ + minifyCriteria(criteria) { + // Copy everything + const criterias = JSON.parse(JSON.stringify(criteria)); + + // Clean-up the whole structure + + for (const field in criterias) { + for (const operator in criterias[field]) { + // Remote parsed properties (generated with fromJSON) + if (operator.includes('$')) { + delete criterias[field][operator]; + } + + // Remote empty inputs + if (!criterias[field][operator]) { + delete criterias[field][operator]; + } else if (operator === 'match' || operator === 'exclude') { + // Encode potential breaking characters and escape double quotes as are used by browser by default + criterias[field][operator] = encodeURI(criterias[field][operator].replace(/["]+/g, '\\"')); + } + + // Remove empty fields + if (!Object.keys(criterias[field]).length) { + delete criterias[field]; + } + } + } + return criterias; + } + /** * Called when a new message arrives * Handles connection with a client @@ -146,6 +182,8 @@ class WebSocket { // 2. Check if its message filter (no auth required) if (parsed.getCommand() == 'filter' && parsed.getPayload()) { client.filter = new Function(`return ${parsed.getPayload()}`)(); + const criterias = this.minifyCriteria(client.filter(message, true)); + this.logger.debugMessage(JSON.stringify(criterias)); } // 3. Get reply if callback exists this.processRequest(parsed) diff --git a/InfoLogger/public/logFilter/LogFilter.js b/InfoLogger/public/logFilter/LogFilter.js index 502b10def..2b59a5e4c 100644 --- a/InfoLogger/public/logFilter/LogFilter.js +++ b/InfoLogger/public/logFilter/LogFilter.js @@ -152,12 +152,17 @@ export default class LogFilter extends Observable { * This function will be stringified then sent to server so it can filter logs * 'DATA_PLACEHOLDER' will be replaced by the stringified filters too so the function contains de data * @param {WebSocketMessage} message - message to be filtered + * @param {boolean} returnCriteriasOnly - Only return the filterlog criteria. * @returns {boolean} true if message passes criterias */ - function filterFunction(message) { + function filterFunction(message, returnCriteriasOnly = false) { const log = message.payload; const criterias = 'DATA_PLACEHOLDER'; + if (returnCriteriasOnly) { + return criterias; + } + /** * Transform timestamp of infologger into javascript Date object * @param {number} timestamp - timestamp from infologger From 0fcbd852a0ae02380ab49e5fa2ad452f0539e8fa Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Mon, 2 Dec 2024 13:41:00 +0100 Subject: [PATCH 2/8] TODO rewrite this test to use mock logger --- Framework/Backend/test/mocha-ws.js | 139 +++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/Framework/Backend/test/mocha-ws.js b/Framework/Backend/test/mocha-ws.js index 88574db9b..49dd063c9 100644 --- a/Framework/Backend/test/mocha-ws.js +++ b/Framework/Backend/test/mocha-ws.js @@ -24,6 +24,108 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; let http, ws, tokenService, token; // eslint-disable-line +const filters = { + timestamp: { + since: '13:02:30', + until: '13:02:40', + $since: '2024-12-02T12:02:30.000Z', + $until: '2024-12-02T12:02:40.000Z', + }, + hostname: { + match: 'aldaqecs01-v1', + exclude: '', + $match: 'aldaqecs01-v1', + $exclude: null, + }, + rolename: { + match: '', + exclude: '', + $match: null, + $exclude: null, + }, + pid: { + match: '50990', + exclude: '', + $match: '50990', + $exclude: null, + }, + username: { + match: 'alicedaq', + exclude: '', + $match: 'alicedaq', + $exclude: null, + }, + system: { + match: 'DAQ', + exclude: '', + $match: 'DAQ', + $exclude: null, + }, + facility: { + match: 'runControl', + exclude: '', + $match: 'runControl', + $exclude: null, + }, + detector: { + match: 'TPC', + exclude: '', + $match: 'TPC', + $exclude: null, + }, + partition: { + match: '', + exclude: '', + $match: null, + $exclude: null, + }, + run: { + match: '248023', + exclude: '', + $match: '248023', + $exclude: null, + }, + errcode: { + match: '', + exclude: '', + $match: null, + $exclude: null, + }, + errline: { + match: '', + exclude: '', + $match: null, + $exclude: null, + }, + errsource: { + match: '', + exclude: '', + $match: null, + $exclude: null, + }, + message: { + match: '', + exclude: '', + $match: null, + $exclude: null, + }, + severity: { + in: 'I F', + $in: [ + 'I', + 'F', + ], + }, + level: { + max: null, + $max: null, + }, +}; + +const minifiedFilters = '{"timestamp":{"since":"13:02:30","until":"13:02:40"},"hostname":{"match":"aldaqecs01-v1"},' + +'"pid":{"match":"50990"},"username":{"match":"alicedaq"},"system":{"match":"DAQ"},"facility":{"match":"runControl"},' + +'"detector":{"match":"TPC"},"run":{"match":"248023"},"severity":{"in":"I F"}}'; + describe('websocket', () => { before(() => { tokenService = new O2TokenService(config.jwt); @@ -130,6 +232,43 @@ describe('websocket', () => { }); }); + it('Accept filter and log criteria', (done) => { + const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); + + connection.on('open', () => { + const message = { command: 'filter', + token: token, + filter: function (returnCriteriasOnly = false) { + if (returnCriteriasOnly) { + return filters; + } + return; + }.toString() }; + connection.send(JSON.stringify(message)); + }); + + connection.on('message', (message) => { + const parsed = JSON.parse(message); + if (parsed.command == 'authed') { + return; + } + new WebSocketMessage().parse(message) + .then((parsed) => { + if (parsed.getCommand() == 'filter' && parsed.getPayload()) { + connection.filter = new Function(`return ${parsed.getPayload()}`)(); + const criterias = this.minifyCriteria(connection.filter(message, true)); + if (criterias != false) { + this.logger.debugMessage(`New live filter applied: ${JSON.stringify(criterias)}`); + assert.strictEqual(criterias, minifiedFilters); + } + } + }); + + connection.terminate(); + done(); + }); + }); + it('Request message broadcast with 200', (done) => { const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); From 67ddc3573a46171252e2615729f950e86d9915bb Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Mon, 2 Dec 2024 14:57:11 +0100 Subject: [PATCH 3/8] Simplefied test --- Framework/Backend/test/mocha-ws.js | 39 +++------------------------ Framework/Backend/websocket/server.js | 4 ++- 2 files changed, 7 insertions(+), 36 deletions(-) diff --git a/Framework/Backend/test/mocha-ws.js b/Framework/Backend/test/mocha-ws.js index 49dd063c9..a65b77ccf 100644 --- a/Framework/Backend/test/mocha-ws.js +++ b/Framework/Backend/test/mocha-ws.js @@ -232,41 +232,10 @@ describe('websocket', () => { }); }); - it('Accept filter and log criteria', (done) => { - const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); - - connection.on('open', () => { - const message = { command: 'filter', - token: token, - filter: function (returnCriteriasOnly = false) { - if (returnCriteriasOnly) { - return filters; - } - return; - }.toString() }; - connection.send(JSON.stringify(message)); - }); - - connection.on('message', (message) => { - const parsed = JSON.parse(message); - if (parsed.command == 'authed') { - return; - } - new WebSocketMessage().parse(message) - .then((parsed) => { - if (parsed.getCommand() == 'filter' && parsed.getPayload()) { - connection.filter = new Function(`return ${parsed.getPayload()}`)(); - const criterias = this.minifyCriteria(connection.filter(message, true)); - if (criterias != false) { - this.logger.debugMessage(`New live filter applied: ${JSON.stringify(criterias)}`); - assert.strictEqual(criterias, minifiedFilters); - } - } - }); - - connection.terminate(); - done(); - }); + it('minifyCriteria() works as expected', (done) => { + const criterias = ws.minifyCriteria(filters); + assert.strictEqual(JSON.stringify(criterias), minifiedFilters); + done(); }); it('Request message broadcast with 200', (done) => { diff --git a/Framework/Backend/websocket/server.js b/Framework/Backend/websocket/server.js index 6fdf1e9e4..f04f56f0b 100644 --- a/Framework/Backend/websocket/server.js +++ b/Framework/Backend/websocket/server.js @@ -183,7 +183,9 @@ class WebSocket { if (parsed.getCommand() == 'filter' && parsed.getPayload()) { client.filter = new Function(`return ${parsed.getPayload()}`)(); const criterias = this.minifyCriteria(client.filter(message, true)); - this.logger.debugMessage(JSON.stringify(criterias)); + if (criterias != false) { + this.logger.debugMessage(`New live filter applied: ${JSON.stringify(criterias)}`); + } } // 3. Get reply if callback exists this.processRequest(parsed) From aa260c592298f738a38816d4d179ce8e6ba49693 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Tue, 3 Dec 2024 09:27:43 +0100 Subject: [PATCH 4/8] WIP testing --- Framework/Backend/test/mocha-ws.js | 245 ++++++++++++++++++----------- 1 file changed, 152 insertions(+), 93 deletions(-) diff --git a/Framework/Backend/test/mocha-ws.js b/Framework/Backend/test/mocha-ws.js index a65b77ccf..611c1d522 100644 --- a/Framework/Backend/test/mocha-ws.js +++ b/Framework/Backend/test/mocha-ws.js @@ -19,6 +19,8 @@ const WebSocket = require('./../websocket/server'); const HttpServer = require('./../http/server'); const O2TokenService = require('./../services/O2TokenService.js'); const WebSocketMessage = require('./../websocket/message.js'); +const sinon = require('sinon'); +// Const WebSocketClientFE = require('Framework/Frontend/js/src/WebSocketClient.js'); process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; @@ -146,117 +148,174 @@ describe('websocket', () => { }); }); - it('Drop connection due to invalid JWT token', (done) => { - const connection = new WebSocketClient(`ws://localhost:${config.http.port}`); - connection.on('close', () => { - connection.terminate(); - done(); - }); - }); + /* + * It('Drop connection due to invalid JWT token', (done) => { + * const connection = new WebSocketClient(`ws://localhost:${config.http.port}`); + * connection.on('close', () => { + * connection.terminate(); + * done(); + * }); + * }); + */ - it('Connect send, and receive a message', (done) => { - const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); + /* + * It('Connect send, and receive a message', (done) => { + * const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); + */ - connection.on('open', () => { - const message = { command: 'test', token: token }; - connection.send(JSON.stringify(message)); - }); - connection.on('message', (message) => { - const parsed = JSON.parse(message); - if (parsed.command == 'authed') { - return; - } - assert.strictEqual(parsed.command, 'test'); - connection.terminate(); - done(); - }); - }); + /* + * Connection.on('open', () => { + * const message = { command: 'test', token: token }; + * connection.send(JSON.stringify(message)); + * }); + * connection.on('message', (message) => { + * const parsed = JSON.parse(message); + * if (parsed.command == 'authed') { + * return; + * } + * assert.strictEqual(parsed.command, 'test'); + * connection.terminate(); + * done(); + * }); + * }); + */ - it('Reject message with misformatted fields', (done) => { - const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); + /* + * It('Reject message with misformatted fields', (done) => { + * const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); + */ - connection.on('open', () => { - const message = { command: '', token: token }; - connection.send(JSON.stringify(message)); - }); - connection.on('message', (message) => { - const parsed = JSON.parse(message); - if (parsed.command == 'authed') { - return; - } - assert.strictEqual(parsed.code, 400); - connection.terminate(); - done(); - }); - }); + /* + * Connection.on('open', () => { + * const message = { command: '', token: token }; + * connection.send(JSON.stringify(message)); + * }); + * connection.on('message', (message) => { + * const parsed = JSON.parse(message); + * if (parsed.command == 'authed') { + * return; + * } + * assert.strictEqual(parsed.code, 400); + * connection.terminate(); + * done(); + * }); + * }); + */ - it('Reject message with 500', (done) => { - const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); + /* + * It('Reject message with 500', (done) => { + * const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); + */ - connection.on('open', () => { - const message = { command: 'fail', token: token }; - connection.send(JSON.stringify(message)); - }); - connection.on('message', (message) => { - const parsed = JSON.parse(message); - if (parsed.command == 'authed') { - return; - } - assert.strictEqual(parsed.code, 500); - connection.terminate(); - done(); - }); - }); + /* + * Connection.on('open', () => { + * const message = { command: 'fail', token: token }; + * connection.send(JSON.stringify(message)); + * }); + * connection.on('message', (message) => { + * const parsed = JSON.parse(message); + * if (parsed.command == 'authed') { + * return; + * } + * assert.strictEqual(parsed.code, 500); + * connection.terminate(); + * done(); + * }); + * }); + */ - it('Accept filter with 200', (done) => { - const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); + /* + * It('Accept filter with 200', (done) => { + * const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); + */ + + /* + * Connection.on('open', () => { + * const message = { command: 'filter', + * token: token, + * filter: function () { + * return false; + * }.toString() }; + * connection.send(JSON.stringify(message)); + * }); + */ + /* + * Connection.on('message', (message) => { + * const parsed = JSON.parse(message); + * if (parsed.command == 'authed') { + * return; + * } + * assert.strictEqual(parsed.code, 200); + * assert.strictEqual(parsed.command, 'filter'); + * connection.terminate(); + * done(); + * }); + * }); + */ + + it('Live filter changes are logged', (done) => { + const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); + ws.logger = { + debugMessage: sinon.spy(), + }; connection.on('open', () => { - const message = { command: 'filter', - token: token, - filter: function () { - return false; - }.toString() }; - connection.send(JSON.stringify(message)); - }); + console.log('ready, lets send a message'); + const theMessage = { + command: 'filter', + token: 'token', + payload: function (message, returnCriteriasOnly = false) { + if (returnCriteriasOnly) { + return filters; + } + return 'Dont check me pls'; + }.toString(), + }; + console.log(theMessage); - connection.on('message', (message) => { - const parsed = JSON.parse(message); - if (parsed.command == 'authed') { - return; - } - assert.strictEqual(parsed.code, 200); - assert.strictEqual(parsed.command, 'filter'); + connection.send(JSON.stringify(theMessage)); + }).on('open', () => { + console.log('Checking response'); + console.log(ws.logger.debugMessage); + assert.strictEqual(ws.logger.debugMessage.calledWith(`New live filter applied: ${minifiedFilters}`), true); connection.terminate(); done(); }); }); - it('minifyCriteria() works as expected', (done) => { - const criterias = ws.minifyCriteria(filters); - assert.strictEqual(JSON.stringify(criterias), minifiedFilters); - done(); - }); + /* + * It('minifyCriteria() works as expected', (done) => { + * const criterias = ws.minifyCriteria(filters); + * assert.strictEqual(JSON.stringify(criterias), minifiedFilters); + * done(); + * }); + */ - it('Request message broadcast with 200', (done) => { - const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); + /* + * It('Request message broadcast with 200', (done) => { + * const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); + */ - connection.on('open', () => { - const message = { command: 'broadcast', token: token }; - connection.send(JSON.stringify(message)); - }); + /* + * Connection.on('open', () => { + * const message = { command: 'broadcast', token: token }; + * connection.send(JSON.stringify(message)); + * }); + */ - connection.on('message', (message) => { - const parsed = JSON.parse(message); - if (parsed.command == 'authed') { - return; - } - assert.strictEqual(parsed.code, 200); - assert.strictEqual(parsed.command, 'broadcast'); - connection.terminate(); - done(); - }); - }); + /* + * Connection.on('message', (message) => { + * const parsed = JSON.parse(message); + * if (parsed.command == 'authed') { + * return; + * } + * assert.strictEqual(parsed.code, 200); + * assert.strictEqual(parsed.command, 'broadcast'); + * connection.terminate(); + * done(); + * }); + * }); + */ after(() => { ws.shutdown(); From 7d5a297da6a0a3a2e20e649488b45694ade452a0 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Wed, 15 Jan 2025 17:37:46 +0100 Subject: [PATCH 5/8] live query test works, peer coding with George --- Framework/Backend/test/mocha-ws.js | 285 ++++++++++++-------------- Framework/Backend/websocket/server.js | 2 +- 2 files changed, 130 insertions(+), 157 deletions(-) diff --git a/Framework/Backend/test/mocha-ws.js b/Framework/Backend/test/mocha-ws.js index 611c1d522..e6a8ed306 100644 --- a/Framework/Backend/test/mocha-ws.js +++ b/Framework/Backend/test/mocha-ws.js @@ -13,18 +13,17 @@ */ const config = require('./../config-default.json'); -const WebSocketClient = require('ws'); +const { WebSocket: WsClient } = require('ws'); const assert = require('assert'); const WebSocket = require('./../websocket/server'); const HttpServer = require('./../http/server'); const O2TokenService = require('./../services/O2TokenService.js'); -const WebSocketMessage = require('./../websocket/message.js'); const sinon = require('sinon'); -// Const WebSocketClientFE = require('Framework/Frontend/js/src/WebSocketClient.js'); +const WebSocketMessage = require('./../websocket/message.js'); process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; -let http, ws, tokenService, token; // eslint-disable-line +let http, wss, tokenService, token; // eslint-disable-line const filters = { timestamp: { @@ -134,191 +133,165 @@ describe('websocket', () => { token = tokenService.generateToken(0, 'test', 'Test', 'admin'); http = new HttpServer(config.http, config.jwt); - ws = new WebSocket(http, config.jwt, 'localhost'); - ws.bind('test', (message) => { + wss = new WebSocket(http, config.jwt, 'localhost'); + + wss.bind('test', (message) => { const res = new WebSocketMessage().setCommand(message.getCommand()); return res; }); - ws.bind('fail', () => ({ test: 'test' })); + wss.bind('fail', () => ({ test: 'test' })); - ws.bind('broadcast', (message) => { + wss.bind('broadcast', (message) => { const res = new WebSocketMessage().setCommand(message.getCommand()).setBroadcast(); return res; }); }); - /* - * It('Drop connection due to invalid JWT token', (done) => { - * const connection = new WebSocketClient(`ws://localhost:${config.http.port}`); - * connection.on('close', () => { - * connection.terminate(); - * done(); - * }); - * }); - */ + it('Drop connection due to invalid JWT token', (done) => { + const connection = new WsClient(`ws://localhost:${config.http.port}`); + connection.on('close', () => { + connection.terminate(); + done(); + }); + }); - /* - * It('Connect send, and receive a message', (done) => { - * const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); - */ + it('Connect send, and receive a message', (done) => { + const connection = new WsClient(`ws://localhost:${config.http.port}/?token=${token}`); - /* - * Connection.on('open', () => { - * const message = { command: 'test', token: token }; - * connection.send(JSON.stringify(message)); - * }); - * connection.on('message', (message) => { - * const parsed = JSON.parse(message); - * if (parsed.command == 'authed') { - * return; - * } - * assert.strictEqual(parsed.command, 'test'); - * connection.terminate(); - * done(); - * }); - * }); - */ + connection.on('open', () => { + const message = { command: 'test', token: token }; + connection.send(JSON.stringify(message)); + }); + connection.on('message', (message) => { + const parsed = JSON.parse(message); + if (parsed.command == 'authed') { + return; + } + assert.strictEqual(parsed.command, 'test'); + connection.terminate(); + done(); + }); + }); - /* - * It('Reject message with misformatted fields', (done) => { - * const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); - */ + it('Reject message with misformatted fields', (done) => { + const connection = new WsClient(`ws://localhost:${config.http.port}/?token=${token}`); - /* - * Connection.on('open', () => { - * const message = { command: '', token: token }; - * connection.send(JSON.stringify(message)); - * }); - * connection.on('message', (message) => { - * const parsed = JSON.parse(message); - * if (parsed.command == 'authed') { - * return; - * } - * assert.strictEqual(parsed.code, 400); - * connection.terminate(); - * done(); - * }); - * }); - */ + connection.on('open', () => { + const message = { command: '', token: token }; + connection.send(JSON.stringify(message)); + }); + connection.on('message', (message) => { + const parsed = JSON.parse(message); + if (parsed.command == 'authed') { + return; + } + assert.strictEqual(parsed.code, 400); + connection.terminate(); + done(); + }); + }); - /* - * It('Reject message with 500', (done) => { - * const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); - */ + it('Reject message with 500', (done) => { + const connection = new WsClient(`ws://localhost:${config.http.port}/?token=${token}`); - /* - * Connection.on('open', () => { - * const message = { command: 'fail', token: token }; - * connection.send(JSON.stringify(message)); - * }); - * connection.on('message', (message) => { - * const parsed = JSON.parse(message); - * if (parsed.command == 'authed') { - * return; - * } - * assert.strictEqual(parsed.code, 500); - * connection.terminate(); - * done(); - * }); - * }); - */ + connection.on('open', () => { + const message = { command: 'fail', token: token }; + connection.send(JSON.stringify(message)); + }); + connection.on('message', (message) => { + const parsed = JSON.parse(message); + if (parsed.command == 'authed') { + return; + } + assert.strictEqual(parsed.code, 500); + connection.terminate(); + done(); + }); + }); - /* - * It('Accept filter with 200', (done) => { - * const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); - */ + it('Accept filter with 200', (done) => { + const connection = new WsClient(`ws://localhost:${config.http.port}/?token=${token}`); - /* - * Connection.on('open', () => { - * const message = { command: 'filter', - * token: token, - * filter: function () { - * return false; - * }.toString() }; - * connection.send(JSON.stringify(message)); - * }); - */ + connection.on('open', () => { + const message = { command: 'filter', + token: token, + filter: function () { + return false; + }.toString() }; + connection.send(JSON.stringify(message)); + }); - /* - * Connection.on('message', (message) => { - * const parsed = JSON.parse(message); - * if (parsed.command == 'authed') { - * return; - * } - * assert.strictEqual(parsed.code, 200); - * assert.strictEqual(parsed.command, 'filter'); - * connection.terminate(); - * done(); - * }); - * }); - */ + connection.on('message', (message) => { + const parsed = JSON.parse(message); + if (parsed.command == 'authed') { + return; + } + assert.strictEqual(parsed.code, 200); + assert.strictEqual(parsed.command, 'filter'); + connection.terminate(); + done(); + }); + }); it('Live filter changes are logged', (done) => { - const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); - ws.logger = { + const wsClient = new WsClient(`ws://localhost:${config.http.port}/?token=${token}`); + wss.logger = { debugMessage: sinon.spy(), }; - connection.on('open', () => { - console.log('ready, lets send a message'); - const theMessage = { - command: 'filter', - token: 'token', - payload: function (message, returnCriteriasOnly = false) { - if (returnCriteriasOnly) { - return filters; - } - return 'Dont check me pls'; - }.toString(), - }; - console.log(theMessage); - connection.send(JSON.stringify(theMessage)); - }).on('open', () => { - console.log('Checking response'); - console.log(ws.logger.debugMessage); - assert.strictEqual(ws.logger.debugMessage.calledWith(`New live filter applied: ${minifiedFilters}`), true); - connection.terminate(); + // eslint-disable-next-line jsdoc/require-jsdoc + function filterFunction(message, returnCriteriasOnly = false) { + if (returnCriteriasOnly) { + return 'PLACE_HOLDER'; + } + return 'Dont check me pls'; + }; + let filterFunctionAsString = filterFunction.toString(); + filterFunctionAsString = filterFunctionAsString.replace('\'PLACE_HOLDER\'', JSON.stringify(filters)); + + const theMessage = { + command: 'filter', + token: 'token', + payload: filterFunctionAsString, + }; + wsClient.on('open', () => { + wsClient.send(JSON.stringify(theMessage)); + wsClient.ping(); + }).on('pong', () => { + assert.ok(wss.logger.debugMessage.calledWith(`New live filter applied: ${minifiedFilters}`)); done(); }); }); - /* - * It('minifyCriteria() works as expected', (done) => { - * const criterias = ws.minifyCriteria(filters); - * assert.strictEqual(JSON.stringify(criterias), minifiedFilters); - * done(); - * }); - */ + it('minifyCriteria() works as expected', (done) => { + const criterias = wss.minifyCriteria(filters); + assert.strictEqual(JSON.stringify(criterias), minifiedFilters); + done(); + }); - /* - * It('Request message broadcast with 200', (done) => { - * const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); - */ + it('Request message broadcast with 200', (done) => { + const connection = new WsClient(`ws://localhost:${config.http.port}/?token=${token}`); - /* - * Connection.on('open', () => { - * const message = { command: 'broadcast', token: token }; - * connection.send(JSON.stringify(message)); - * }); - */ + connection.on('open', () => { + const message = { command: 'broadcast', token: token }; + connection.send(JSON.stringify(message)); + }); - /* - * Connection.on('message', (message) => { - * const parsed = JSON.parse(message); - * if (parsed.command == 'authed') { - * return; - * } - * assert.strictEqual(parsed.code, 200); - * assert.strictEqual(parsed.command, 'broadcast'); - * connection.terminate(); - * done(); - * }); - * }); - */ + connection.on('message', (message) => { + const parsed = JSON.parse(message); + if (parsed.command == 'authed') { + return; + } + assert.strictEqual(parsed.code, 200); + assert.strictEqual(parsed.command, 'broadcast'); + connection.terminate(); + done(); + }); + }); after(() => { - ws.shutdown(); + wss.shutdown(); http.close(); }); }); diff --git a/Framework/Backend/websocket/server.js b/Framework/Backend/websocket/server.js index f04f56f0b..a5fe839d0 100644 --- a/Framework/Backend/websocket/server.js +++ b/Framework/Backend/websocket/server.js @@ -240,7 +240,7 @@ class WebSocket { * @param {object} client - disconnected client */ onclose(client) { - this.logger.info(`ID ${client.id} Client disconnected`); + this.logger.debugMessage(`ID ${client.id} Client disconnected`); } /** From 47cb0fd23c4fac9d557bc88b3f6d44babe02b88a Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Sun, 19 Jan 2025 11:15:40 +0100 Subject: [PATCH 6/8] Moved minifyCriteria to js module --- Framework/Backend/test/mocha-ws.js | 3 +- Framework/Backend/utils/minifyCriteria.js | 37 +++++++++++++++++++++ Framework/Backend/websocket/server.js | 39 ++--------------------- 3 files changed, 41 insertions(+), 38 deletions(-) create mode 100644 Framework/Backend/utils/minifyCriteria.js diff --git a/Framework/Backend/test/mocha-ws.js b/Framework/Backend/test/mocha-ws.js index e6a8ed306..a6fb4eeb2 100644 --- a/Framework/Backend/test/mocha-ws.js +++ b/Framework/Backend/test/mocha-ws.js @@ -20,6 +20,7 @@ const HttpServer = require('./../http/server'); const O2TokenService = require('./../services/O2TokenService.js'); const sinon = require('sinon'); const WebSocketMessage = require('./../websocket/message.js'); +const { minifyCriteria } = require('../utils/minifyCriteria.js'); process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; @@ -265,7 +266,7 @@ describe('websocket', () => { }); it('minifyCriteria() works as expected', (done) => { - const criterias = wss.minifyCriteria(filters); + const criterias = minifyCriteria(filters); assert.strictEqual(JSON.stringify(criterias), minifiedFilters); done(); }); diff --git a/Framework/Backend/utils/minifyCriteria.js b/Framework/Backend/utils/minifyCriteria.js new file mode 100644 index 000000000..05d6d56e8 --- /dev/null +++ b/Framework/Backend/utils/minifyCriteria.js @@ -0,0 +1,37 @@ +/** + * Make criteria more readable. + * This code is a close copy of InfoLogger/public/logFilter/LogFilter.js LN 101 toObject() + * @param {object} criteria - criteria to be minified + * @returns {object} minimal filter object + */ +function minifyCriteria(criteria) { + // Copy everything + const criterias = JSON.parse(JSON.stringify(criteria)); + + // Clean-up the whole structure + + for (const field in criterias) { + for (const operator in criterias[field]) { + // Remote parsed properties (generated with fromJSON) + if (operator.includes('$')) { + delete criterias[field][operator]; + } + + // Remote empty inputs + if (!criterias[field][operator]) { + delete criterias[field][operator]; + } else if (operator === 'match' || operator === 'exclude') { + // Encode potential breaking characters and escape double quotes as are used by browser by default + criterias[field][operator] = encodeURI(criterias[field][operator].replace(/["]+/g, '\\"')); + } + + // Remove empty fields + if (!Object.keys(criterias[field]).length) { + delete criterias[field]; + } + } + } + return criterias; +} + +module.exports.minifyCriteria = minifyCriteria; diff --git a/Framework/Backend/websocket/server.js b/Framework/Backend/websocket/server.js index a5fe839d0..f1c069ae6 100644 --- a/Framework/Backend/websocket/server.js +++ b/Framework/Backend/websocket/server.js @@ -16,6 +16,7 @@ const WebSocketServer = require('ws').Server; const url = require('url'); const WebSocketMessage = require('./message.js'); const { LogManager } = require('../log/LogManager'); +const { minifyCriteria } = require('../utils/minifyCriteria.js'); /** * It represents WebSocket server (RFC 6455). @@ -133,42 +134,6 @@ class WebSocket { client.on('error', (err) => this.logger.error(`Connection ${err.code}`)); } - /** - * Make criteria more readable. - * This code is a close copy of InfoLogger/public/logFilter/LogFilter.js LN 101 toObject() - * @param {object} criteria - criteria to be minified - * @returns {object} minimal filter object - */ - minifyCriteria(criteria) { - // Copy everything - const criterias = JSON.parse(JSON.stringify(criteria)); - - // Clean-up the whole structure - - for (const field in criterias) { - for (const operator in criterias[field]) { - // Remote parsed properties (generated with fromJSON) - if (operator.includes('$')) { - delete criterias[field][operator]; - } - - // Remote empty inputs - if (!criterias[field][operator]) { - delete criterias[field][operator]; - } else if (operator === 'match' || operator === 'exclude') { - // Encode potential breaking characters and escape double quotes as are used by browser by default - criterias[field][operator] = encodeURI(criterias[field][operator].replace(/["]+/g, '\\"')); - } - - // Remove empty fields - if (!Object.keys(criterias[field]).length) { - delete criterias[field]; - } - } - } - return criterias; - } - /** * Called when a new message arrives * Handles connection with a client @@ -182,7 +147,7 @@ class WebSocket { // 2. Check if its message filter (no auth required) if (parsed.getCommand() == 'filter' && parsed.getPayload()) { client.filter = new Function(`return ${parsed.getPayload()}`)(); - const criterias = this.minifyCriteria(client.filter(message, true)); + const criterias = minifyCriteria(client.filter(message, true)); if (criterias != false) { this.logger.debugMessage(`New live filter applied: ${JSON.stringify(criterias)}`); } From 39ce2fa0b022ebdbc45f0c4521688b0404f959b6 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Sun, 19 Jan 2025 13:28:08 +0100 Subject: [PATCH 7/8] minifyCriteria moved to separate test files --- Framework/Backend/test/mocha-utils.js | 125 ++++++++++++++++++++++++++ Framework/Backend/test/mocha-ws.js | 7 -- 2 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 Framework/Backend/test/mocha-utils.js diff --git a/Framework/Backend/test/mocha-utils.js b/Framework/Backend/test/mocha-utils.js new file mode 100644 index 000000000..28fe8257b --- /dev/null +++ b/Framework/Backend/test/mocha-utils.js @@ -0,0 +1,125 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ +const assert = require('assert'); +const { minifyCriteria } = require('../utils/minifyCriteria.js'); + +const filters = { + timestamp: { + since: '13:02:30', + until: '13:02:40', + $since: '2024-12-02T12:02:30.000Z', + $until: '2024-12-02T12:02:40.000Z', + }, + hostname: { + match: 'aldaqecs01-v1', + exclude: '', + $match: 'aldaqecs01-v1', + $exclude: null, + }, + rolename: { + match: '', + exclude: '', + $match: null, + $exclude: null, + }, + pid: { + match: '50990', + exclude: '', + $match: '50990', + $exclude: null, + }, + username: { + match: 'alicedaq', + exclude: '', + $match: 'alicedaq', + $exclude: null, + }, + system: { + match: 'DAQ', + exclude: '', + $match: 'DAQ', + $exclude: null, + }, + facility: { + match: 'runControl', + exclude: '', + $match: 'runControl', + $exclude: null, + }, + detector: { + match: 'TPC', + exclude: '', + $match: 'TPC', + $exclude: null, + }, + partition: { + match: '', + exclude: '', + $match: null, + $exclude: null, + }, + run: { + match: '248023', + exclude: '', + $match: '248023', + $exclude: null, + }, + errcode: { + match: '', + exclude: '', + $match: null, + $exclude: null, + }, + errline: { + match: '', + exclude: '', + $match: null, + $exclude: null, + }, + errsource: { + match: '', + exclude: '', + $match: null, + $exclude: null, + }, + message: { + match: '', + exclude: '', + $match: null, + $exclude: null, + }, + severity: { + in: 'I F', + $in: [ + 'I', + 'F', + ], + }, + level: { + max: null, + $max: null, + }, +}; + +const minifiedFilters = '{"timestamp":{"since":"13:02:30","until":"13:02:40"},"hostname":{"match":"aldaqecs01-v1"},' + +'"pid":{"match":"50990"},"username":{"match":"alicedaq"},"system":{"match":"DAQ"},"facility":{"match":"runControl"},' + +'"detector":{"match":"TPC"},"run":{"match":"248023"},"severity":{"in":"I F"}}'; + +describe('Utils - minifyCriteria()', () => { + it('minifyCriteria() works as expected', (done) => { + const criterias = minifyCriteria(filters); + assert.strictEqual(JSON.stringify(criterias), minifiedFilters); + done(); + }); +}); diff --git a/Framework/Backend/test/mocha-ws.js b/Framework/Backend/test/mocha-ws.js index a6fb4eeb2..6d43cd930 100644 --- a/Framework/Backend/test/mocha-ws.js +++ b/Framework/Backend/test/mocha-ws.js @@ -20,7 +20,6 @@ const HttpServer = require('./../http/server'); const O2TokenService = require('./../services/O2TokenService.js'); const sinon = require('sinon'); const WebSocketMessage = require('./../websocket/message.js'); -const { minifyCriteria } = require('../utils/minifyCriteria.js'); process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; @@ -265,12 +264,6 @@ describe('websocket', () => { }); }); - it('minifyCriteria() works as expected', (done) => { - const criterias = minifyCriteria(filters); - assert.strictEqual(JSON.stringify(criterias), minifiedFilters); - done(); - }); - it('Request message broadcast with 200', (done) => { const connection = new WsClient(`ws://localhost:${config.http.port}/?token=${token}`); From a7c7432055af04da515988a1b27cca20ea90e947 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Sun, 19 Jan 2025 13:33:11 +0100 Subject: [PATCH 8/8] Try catch added at call to minifyCriteria in server.js --- Framework/Backend/websocket/server.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Framework/Backend/websocket/server.js b/Framework/Backend/websocket/server.js index f1c069ae6..88ec05d72 100644 --- a/Framework/Backend/websocket/server.js +++ b/Framework/Backend/websocket/server.js @@ -147,7 +147,12 @@ class WebSocket { // 2. Check if its message filter (no auth required) if (parsed.getCommand() == 'filter' && parsed.getPayload()) { client.filter = new Function(`return ${parsed.getPayload()}`)(); - const criterias = minifyCriteria(client.filter(message, true)); + let criterias; + try { + criterias = minifyCriteria(client.filter(message, true)); + } catch { + this.logger.errorMessage('Invalid payload criteria received at onmessage()'); + } if (criterias != false) { this.logger.debugMessage(`New live filter applied: ${JSON.stringify(criterias)}`); }