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 88574db9b..6d43cd930 100644 --- a/Framework/Backend/test/mocha-ws.js +++ b/Framework/Backend/test/mocha-ws.js @@ -13,16 +13,119 @@ */ 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 sinon = require('sinon'); 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: { + 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(() => { @@ -30,22 +133,23 @@ 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}`); + const connection = new WsClient(`ws://localhost:${config.http.port}`); connection.on('close', () => { connection.terminate(); done(); @@ -53,7 +157,7 @@ describe('websocket', () => { }); it('Connect send, and receive a message', (done) => { - const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); + const connection = new WsClient(`ws://localhost:${config.http.port}/?token=${token}`); connection.on('open', () => { const message = { command: 'test', token: token }; @@ -71,7 +175,7 @@ describe('websocket', () => { }); it('Reject message with misformatted fields', (done) => { - const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); + const connection = new WsClient(`ws://localhost:${config.http.port}/?token=${token}`); connection.on('open', () => { const message = { command: '', token: token }; @@ -89,7 +193,7 @@ describe('websocket', () => { }); it('Reject message with 500', (done) => { - const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); + const connection = new WsClient(`ws://localhost:${config.http.port}/?token=${token}`); connection.on('open', () => { const message = { command: 'fail', token: token }; @@ -107,7 +211,7 @@ describe('websocket', () => { }); it('Accept filter with 200', (done) => { - const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); + const connection = new WsClient(`ws://localhost:${config.http.port}/?token=${token}`); connection.on('open', () => { const message = { command: 'filter', @@ -130,8 +234,38 @@ describe('websocket', () => { }); }); + it('Live filter changes are logged', (done) => { + const wsClient = new WsClient(`ws://localhost:${config.http.port}/?token=${token}`); + wss.logger = { + debugMessage: sinon.spy(), + }; + + // 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('Request message broadcast with 200', (done) => { - const connection = new WebSocketClient(`ws://localhost:${config.http.port}/?token=${token}`); + const connection = new WsClient(`ws://localhost:${config.http.port}/?token=${token}`); connection.on('open', () => { const message = { command: 'broadcast', token: token }; @@ -151,7 +285,7 @@ describe('websocket', () => { }); after(() => { - ws.shutdown(); + wss.shutdown(); http.close(); }); }); 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 75be7a1c9..88ec05d72 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). @@ -146,6 +147,15 @@ class WebSocket { // 2. Check if its message filter (no auth required) if (parsed.getCommand() == 'filter' && parsed.getPayload()) { client.filter = new Function(`return ${parsed.getPayload()}`)(); + 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)}`); + } } // 3. Get reply if callback exists this.processRequest(parsed) @@ -200,7 +210,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`); } /** 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