diff --git a/package-lock.json b/package-lock.json index 44267a0..ef92525 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "visualizer-server", - "version": "0.3.5", + "version": "0.3.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "visualizer-server", - "version": "0.3.5", + "version": "0.3.8", "license": "ISC", "dependencies": { "@reduxjs/toolkit": "^1.9.0", diff --git a/package.json b/package.json index 678e647..8200fdc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "visualizer-server", - "version": "0.3.5", + "version": "0.3.8", "description": "Visualize RTC stats data", "repository": "https://github.com/8x8Cloud/rtc-visualizer", "author": "8x8", diff --git a/src/client/app/files/formatter.js b/src/client/app/files/formatter.js index 2eb8a67..5d4044f 100644 --- a/src/client/app/files/formatter.js +++ b/src/client/app/files/formatter.js @@ -9,7 +9,7 @@ export const parseData = input => { lines.forEach(line => { if (line.length) { const data = JSON.parse(line) - const time = new Date(data.time || data[data.length - 1]) + const time = new Date(data.time || data[3]) delete data.time switch (data[0]) { diff --git a/src/client/app/raw/legacy.js b/src/client/app/raw/legacy.js index 9d01d2a..3965af5 100644 --- a/src/client/app/raw/legacy.js +++ b/src/client/app/raw/legacy.js @@ -172,11 +172,18 @@ function createSpecCandidateTable (container, allGroupedStats) { function createContainers (connid, url) { let signalingState, iceConnectionState, connectionState, candidates const container = document.createElement('details') - container.open = true + container.open = false container.style.margin = '10px' - + let innerText = ''; let summary = document.createElement('summary') - summary.innerText = 'Connection:' + connid + ' URL: ' + url + if (connid !== 'null') { + innerText = 'Connection:' + connid// + ' URL: ' + url + } else { + innerText = 'Meeting events'; + } + + summary.innerText = innerText; + container.appendChild(summary) if (connid !== 'null') { @@ -228,9 +235,34 @@ function createContainers (connid, url) { return container } +function convertTotalToRateSeries(timeSeries) { + return timeSeries.reduce( + (accumulator, currentValue) => { + + const {prevValue} = accumulator; + accumulator.prevValue = currentValue; + + if (!prevValue.length) { + return accumulator; + } + + const [timestamp = 0, totalBytesSent = 0] = currentValue; + const [prevTimestamp = 0, prevTotalBytesSent = 0] = prevValue; + + const sampleRateSeconds = (timestamp - prevTimestamp) / 1000; + const bitRate = (totalBytesSent - prevTotalBytesSent) * 8; + const bitRatePerSecond = Math.round(bitRate / sampleRateSeconds); + + accumulator.bitRate.push([timestamp, bitRatePerSecond]); + return accumulator; + }, + { bitRate: [], prevValue: []}, + ).bitRate; +} + function processGUM (data) { const container = document.createElement('details') - container.open = true + container.open = false container.style.margin = '10px' const summary = document.createElement('summary') @@ -440,23 +472,21 @@ function processConnections (connectionIds, data) { container.appendChild(chartContainer) const ignoredSeries = ['type', 'ssrc'] - const hiddenSeries = [ - 'bytesReceived', 'bytesSent', - 'headerBytesReceived', 'headerBytesSent', - 'packetsReceived', 'packetsSent', - 'qpSum', 'estimatedPlayoutTimestamp', - 'framesEncoded', 'framesDecoded', - 'lastPacketReceivedTimestamp', 'lastPacketSentTimestamp', - 'remoteTimestamp', - 'audioInputLevel', 'audioOutputLevel', - 'totalSamplesDuration', - 'totalSamplesReceived', 'jitterBufferEmittedCount', - // legacy - 'googDecodingCTN', 'googDecodingCNG', 'googDecodingNormal', - 'googDecodingPLCCNG', 'googDecodingCTSG', 'googDecodingMuted', - 'googEchoCancellationEchoDelayStdDev', - 'googCaptureStartNtpTimeMs' + const visibleSeries = [ + 'bytesReceivedInBits/S', 'bytesSentInBits/S', + 'targetBitrate', 'packetsLost', 'jitter', + 'availableOutgoingBitrate', 'roundTripTime' ] + const rateSeriesWhitelist = ['bytesSent', 'bytesReceived']; + + // Calculate bitrate per second for time series that contain cumulated values + // over time, time total bytes sent or total bytes received + Object.keys(series[reportname]) + .filter(name => rateSeriesWhitelist.includes(name)) + .map(name => { + const rateSeries = convertTotalToRateSeries(series[reportname][name]); + series[reportname][`${name}InBits/S`] = rateSeries; + }) const traces = Object.keys(series[reportname]) .filter(name => !ignoredSeries.includes(name)) @@ -467,11 +497,11 @@ function processConnections (connectionIds, data) { return { mode: 'lines+markers', name: name, - visible: hiddenSeries.includes(name) ? 'legendonly' : true, + visible: visibleSeries.includes(name) ? true: 'legendonly', x: data.map(d => new Date(d[0])), y: data.map(d => d[1]) } - }) + }) // expand the graph when opening container.ontoggle = () => container.open && Plotly.react(chartContainer, traces) @@ -503,7 +533,7 @@ export const clearStats = () => { export const showStats = data => { clearStats() - document.getElementById('userAgent').innerHTML = `User Agent: ${data.userAgent}` + // TODO Add user agent support document.getElementById('userAgent').innerHTML = `User Agent: ${data.userAgent}` processGUM(data.getUserMedia) window.setTimeout(processConnections, 0, Object.keys(data.peerConnections), data) } diff --git a/src/server/jwt-auth.mjs b/src/server/jwt-auth.mjs index 7c6d30b..49f2afa 100644 --- a/src/server/jwt-auth.mjs +++ b/src/server/jwt-auth.mjs @@ -5,24 +5,53 @@ function addPEMHeaders (headerlessPEMKey) { return `-----BEGIN CERTIFICATE-----\n${headerlessPEMKey}\n-----END CERTIFICATE-----` } +function addRSAPublicKeyPEMHeaders(headerlessPEMKey) { + const nlHeaderlessPEMKey = headerlessPEMKey.replace(/(.{64})/g, '$1\n'); + + return `-----BEGIN PUBLIC KEY-----\n${nlHeaderlessPEMKey}\n-----END PUBLIC KEY-----`; +} + const { RTCSTATS_JWT_PUBLIC_KEY } = process.env -const formattedKey = addPEMHeaders(RTCSTATS_JWT_PUBLIC_KEY) +const { RTCSTATS_JWT_EGHT_PUBLIC_KEY } = process.env + +const formattedKeyJaaS = addPEMHeaders(RTCSTATS_JWT_PUBLIC_KEY) +const formattedKeyEGHT = addRSAPublicKeyPEMHeaders(RTCSTATS_JWT_EGHT_PUBLIC_KEY) + + +function isValidJaaSToken(authorization) { + try { + const bearerToken = authorization.substring(7) + const decodedToken = jwt.verify(bearerToken, formattedKeyJaaS) + + return decodedToken; + } catch (error) { + log.error(`JAAS Bearer authorization failed: ${JSON.stringify(error)}`) + } +} + +function isValidEGHTToken(authorization) { + try { + const bearerToken = authorization.substring(7) + const decodedToken = jwt.verify(bearerToken, formattedKeyEGHT) + + return decodedToken; + } catch (error) { + log.error(`EGHT Bearer authorization failed: ${JSON.stringify(error)}`) + } +} export default (req, res, next) => { const { headers: { authorization = '' } = {} } = req if (authorization.startsWith('Bearer ')) { - try { - const bearerToken = authorization.substring(7) - const decodedToken = jwt.verify(bearerToken, formattedKey) - + const decodedToken = isValidJaaSToken(authorization) || isValidEGHTToken(authorization); + + if (decodedToken) { req.user = decodedToken - - next() - } catch (error) { - log.error(`Bearer authorization failed: ${JSON.stringify(error)}`) - res.status(401).json({ error: 'Unauthorized' }) + return next() } + + res.status(401).json({ error: 'Unauthorized' }) } else { log.warn(`Bearer authorization token not present, found: ${authorization}`)