let PROJECT_KEY_LENGTH = parseInt(process.env.PROJECT_KEY_LENGTH) || 20; const {logger} = require('./logger'); const extractRoomId = (peerId) => { let {projectKey, sessionId, tabId} = extractPeerId(peerId); if (projectKey && sessionId) { return `${projectKey}-${sessionId}`; } return null; } const extractTabId = (peerId) => { let {projectKey, sessionId, tabId} = extractPeerId(peerId); if (tabId) { return tabId; } return null; } const extractPeerId = (peerId) => { let splited = peerId.split("-"); if (splited.length < 2 || splited.length > 3) { logger.debug(`cannot split peerId: ${peerId}`); return {}; } if (PROJECT_KEY_LENGTH > 0 && splited[0].length !== PROJECT_KEY_LENGTH) { logger.debug(`wrong project key length for peerId: ${peerId}`); return {}; } if (splited.length === 2) { return {projectKey: splited[0], sessionId: splited[1], tabId: (Math.random() + 1).toString(36).substring(2)}; } return {projectKey: splited[0], sessionId: splited[1], tabId: splited[2]}; }; const request_logger = (identity) => { return (req, res, next) => { logger.debug(identity, new Date().toTimeString(), 'REQUEST', req.method, req.originalUrl); req.startTs = performance.now(); // track request's start timestamp res.on('finish', function () { if (this.statusCode !== 200) { logger.info(new Date().toTimeString(), 'RESPONSE', req.method, req.originalUrl, this.statusCode); } else { logger.debug(new Date().toTimeString(), 'RESPONSE', req.method, req.originalUrl, this.statusCode); } }) next(); } }; const extractProjectKeyFromRequest = function (req) { if (req.params.projectKey) { logger.debug(`[WS]where projectKey=${req.params.projectKey}`); return req.params.projectKey; } return undefined; } const extractSessionIdFromRequest = function (req) { if (req.params.sessionId) { logger.debug(`[WS]where sessionId=${req.params.sessionId}`); return req.params.sessionId; } return undefined; } const isValidSession = function (sessionInfo, filters) { const result = {matched: false, filters: {}}; for (const [filterName, body] of Object.entries(filters)) { // range by filter names (key) if (body.values === undefined || body.values === null) { return result; } let found = false; for (const [sessKey, sessValue] of Object.entries(sessionInfo)) { if (sessValue === undefined || sessValue === null) { continue; } if (typeof (sessValue) === "object") { const partRes = isValidSession(sessValue, {[filterName]: body}) if (partRes.matched) { found = true; Object.assign(result.filters, partRes.filters); break; } } else if (sessKey.toLowerCase() === filterName.toLowerCase()) { for (let v of body.values) { if (body.operator === "is" && v && String(sessValue).toLowerCase() === String(v).toLowerCase() || body.operator !== "is" && String(sessValue).toLowerCase().indexOf(String(v).toLowerCase()) >= 0) { found = true; result.filters[filterName] = v; break; } } if (found) { break; } } } if (!found) { return result; } } result.matched = true; return result; } const getValidAttributes = function (sessionInfo, query) { let matches = []; let deduplicate = []; for (const [skey, svalue] of Object.entries(sessionInfo)) { if (svalue !== undefined && svalue !== null) { if (typeof (svalue) === "object") { matches = [...matches, ...getValidAttributes(svalue, query)] } else if ((query.key === undefined || skey.toLowerCase() === query.key.toLowerCase()) && String(svalue).toLowerCase().indexOf(query.value.toLowerCase()) >= 0 && deduplicate.indexOf(skey + '_' + svalue) < 0) { matches.push({"type": skey.toUpperCase(), "value": svalue}); deduplicate.push(skey + '_' + svalue); } } } return matches; } const hasFilters = function (filters) { return filters && filters.filter && Object.keys(filters.filter).length > 0; } const hasQuery = function (filters) { return filters && filters.query && Object.keys(filters.query).length > 0; } const objectToObjectOfArrays = function (obj) { let _obj = {} if (obj) { for (let k of Object.keys(obj)) { if (obj[k] !== undefined && obj[k] !== null) { _obj[k] = obj[k]; if (!Array.isArray(_obj[k].values)) { _obj[k] = [_obj[k]]; } for (let i = 0; i < _obj[k].values.length; i++) { _obj[k].values[i] = String(_obj[k].values[i]); } } } } return _obj; } const transformFilters = function (filter) { for (let key of Object.keys(filter)) { //To support old v1.7.0 payload if (Array.isArray(filter[key]) || filter[key] === undefined || filter[key] === null) { logger.debug(`[WS]old format for key=${key}`); filter[key] = {"values": filter[key]}; } if (filter[key].operator) { logger.debug(`[WS]where operator=${filter[key].operator}`); } else { logger.debug(`[WS]where operator=DEFAULT-contains`); filter[key].operator = "contains"; } } return filter; } const extractPayloadFromRequest = async function (req, res) { let filters = { "query": {}, // for autocomplete "filter": {}, // for sessions search "sort": { "key": req.body.sort && req.body.sort.key ? req.body.sort.key : undefined, "order": req.body.sort && req.body.sort?.order.toLowerCase() === "desc" }, "pagination": { "limit": req.body.pagination && req.body.pagination.limit ? req.body.pagination.limit : undefined, "page": req.body.pagination && req.body.pagination.page ? req.body.pagination.page : undefined } }; if (req.query.q) { logger.debug(`[WS]where q=${req.query.q}`); filters.query.value = req.query.q; } if (req.query.key) { logger.debug(`[WS]where key=${req.query.key}`); filters.query.key = req.query.key; } if (req.query.userId) { logger.debug(`[WS]where userId=${req.query.userId}`); filters.filter.userID = [req.query.userId]; } filters.filter = objectToObjectOfArrays(filters.filter); filters.filter = {...filters.filter, ...(req.body.filter || {})}; filters.filter = transformFilters(filters.filter); logger.debug("payload/filters:" + JSON.stringify(filters)) return filters; } const getValue = function (obj, key) { if (obj !== undefined && obj !== null) { let val; for (let k of Object.keys(obj)) { if (typeof (obj[k]) === "object") { val = getValue(obj[k], key); } else if (k.toLowerCase() === key.toLowerCase()) { val = obj[k]; } if (val !== undefined) { return isNaN(val) ? val : Number(val); } } } return undefined; } const sortPaginate = function (list, filters, counter) { if (typeof (list) === "object" && !Array.isArray(list)) { for (const [key, value] of Object.entries(list)) { list[key] = sortPaginate(value, filters); } return list } const total = list.length; if (filters.sort.key && filters.sort.key !== "timestamp") { list.sort((a, b) => { const vA = getValue(a, filters.sort.key); const vB = getValue(b, filters.sort.key); return vA > vB ? 1 : vA < vB ? -1 : 0; }); } else { list.sort((a, b) => { const tA = getValue(a, "timestamp"); const tB = getValue(b, "timestamp"); return tB - tA }); } if (!filters.sort.order) { list.reverse(); } if (filters.pagination.page && filters.pagination.limit) { list = list.slice((filters.pagination.page - 1) * filters.pagination.limit, filters.pagination.page * filters.pagination.limit); } return {"total": total, "sessions": list, "counter": counter}; } const uniqueAutocomplete = function (list) { let _list = []; let deduplicate = []; for (let e of list) { if (deduplicate.indexOf(e.type + "_" + e.value) < 0) { _list.push(e); deduplicate.push(e.type + "_" + e.value) } } return _list; } const getAvailableRooms = async function (io) { return io.sockets.adapter.rooms; } const getCompressionConfig = function () { // WS: The theoretical overhead per socket is 19KB (11KB for compressor and 8KB for decompressor) let perMessageDeflate = false; if (process.env.COMPRESSION === "true") { logger.info(`WS compression: enabled`); perMessageDeflate = { zlibDeflateOptions: { windowBits: 10, memLevel: 1 }, zlibInflateOptions: { windowBits: 10 } } } else { logger.info(`WS compression: disabled`); } return { perMessageDeflate: perMessageDeflate, clientNoContextTakeover: true }; } module.exports = { transformFilters, extractRoomId, extractTabId, extractPeerId, request_logger, getValidAttributes, extractProjectKeyFromRequest, extractSessionIdFromRequest, isValidSession, hasFilters, hasQuery, objectToObjectOfArrays, extractPayloadFromRequest, sortPaginate, uniqueAutocomplete, getAvailableRooms, getCompressionConfig };