/** * The main app initialisation- and glue code */ function updateClosestActiveChartData() { if(!closestActiveClient) return; // Need that to query the backend console.log('Querying for closest MAC ' + closestActiveClient.mac); maltegoRequestSSIDsXML.querySelector('AdditionalFields Field[Name="mac"]') .textContent = closestActiveClient.mac; d3.xhr(backendURL + '/transforms/fetchSSIDs.py?Key=031e0d4e8a573436b5d62054354f2ce0') .header("Content-Type", "application/xml") .post(new XMLSerializer().serializeToString(maltegoRequestSSIDsXML), function(err, data) { var ssids = parseMaltegoClosestActiveSSIDs(maltegoTransformToXmlDOM(data.responseText)); var numSSIDs = ssids.length; var hiddenSsids = ssids.slice(activeClientMaxSSIDs, ssids.length); if(numSSIDs > activeClientMaxSSIDs) { // We only display a few, so remove uninteresting ones if we have more var partitionedSsids = ssids.partition(uninterestingSsidsPartitionFilter); ssids = partitionedSsids[1].concat(partitionedSsids[1].length < activeClientMaxSSIDs ? partitionedSsids[0].slice(0, activeClientMaxSSIDs - partitionedSsids[1].length) : []); hiddenSsids = partitionedSsids[1].length < activeClientMaxSSIDs ? partitionedSsids[0].slice(activeClientMaxSSIDs - partitionedSsids[1].length) : partitionedSsids[0]; } ssids = ssids.slice(0, activeClientMaxSSIDs); if(numSSIDs > ssids.length) { // We have limited screen estate var name = (numSSIDs - ssids.length) + ' more WiFis'; var hiddenSsidNode = {name: name, id: name.hashCode()}; if(closestActiveClientSSIDInfo.children[closestActiveClientSSIDInfo.children.length - 1].children) hiddenSsidNode.children = hiddenSsids; // Use previous collapsed / expanded state else hiddenSsidNode._children = hiddenSsids; ssids.push(hiddenSsidNode); } // Update the current active client data for the chart closestActiveClientSSIDInfo.name = closestActiveClient.hostname; if(!closestActiveClientSSIDInfo.name) // Use nicely formatted MAC address closestActiveClientSSIDInfo.name = closestActiveClient.mac.replace(/^([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/, '$1:$2:$3:$4:$5:$6').toUpperCase(); closestActiveClientSSIDInfo.name = closestActiveClientSSIDInfo.name.substring(0, 17); // Shorten if hostname, mac has 17 characters. We have limited screen estate for this closestActiveClientSSIDInfo.id = closestActiveClientSSIDInfo.name.hashCode(); closestActiveClientSSIDInfo.children = ssids; activeVendorChart = updateActiveVendorChart(closestActiveClient, activeVendorChart); updateSsidChart(); }); } function doTransitionOffset(selector, attr, offset, durationMs, delay) { var el = d3.select(selector); var currPos = window.getComputedStyle(el.node(0)).getPropertyValue(attr); var endPosPx = Math.round(Number.parseFloat(currPos) + Number.parseFloat(offset)) + 'px' var trans = el.style(attr, currPos).transition().duration(durationMs).style(attr, endPosPx); if(delay) trans = trans.delay(delay); return trans; } function hideChartAnimation() { doTransitionOffset('.logo.header', 'top', offscreenOffsets.logoHeaderTop, transitionDurMs); doTransitionOffset('.claim', 'top', offscreenOffsets.claimTop, transitionDurMs); doTransitionOffset('.visitorChart', 'left', offscreenOffsets.visitorChartLeft, transitionDurMs); doTransitionOffset('.durationChart', 'left', offscreenOffsets.durationChartLeft, transitionDurMs); doTransitionOffset('.radarChart', 'left', offscreenOffsets.radarChartLeft, transitionDurMs); } function showChartAnimation(delay) { doTransitionOffset('.logo.header', 'top', -Number.parseFloat(offscreenOffsets.logoHeaderTop), transitionDurMs, delay); doTransitionOffset('.claim', 'top', -Number.parseFloat(offscreenOffsets.claimTop), transitionDurMs, delay); doTransitionOffset('.visitorChart', 'left', -Number.parseFloat(offscreenOffsets.visitorChartLeft), transitionDurMs, delay); doTransitionOffset('.durationChart', 'left', -Number.parseFloat(offscreenOffsets.durationChartLeft), transitionDurMs, delay); return doTransitionOffset('.radarChart', 'left', -Number.parseFloat(offscreenOffsets.radarChartLeft), transitionDurMs, delay); } function hideVideoAnimation() { var videoEl = d3.select('.video.car2ad'); videoEl.node(0).pause(); videoEl.style('opacity', 1) .transition().duration(transitionDurMs * 0.5) .style('opacity', 0); return function() { // callback for state transition end event videoEl.node(0).currentTime = 0.0; // Seek video to start }; } function showVideoAnimation(delay) { return d3.select('.video.car2ad').style('opacity', 0) .transition().delay(delay).duration(transitionDurMs * 0.5) .style('opacity', 1).each('end', function() { this.play(); // Start the video after the animation }); } function hideActiveAnimation() { doTransitionOffset('.logo.header.active', 'top', offscreenOffsets.logoHeaderTop, transitionDurMs); doTransitionOffset('.claim.active', 'top', offscreenOffsets.claimTop, transitionDurMs); doTransitionOffset('.ssidTreeChart', 'left', offscreenOffsets.ssidsChartLeft, transitionDurMs); doTransitionOffset('.activeVendorChart', 'left', offscreenOffsets.activeVendorChartLeft, transitionDurMs); doTransitionOffset('.numSsidChart', 'left', offscreenOffsets.numSsidsChartLeft, transitionDurMs); } function showActiveAnimation(delay) { doTransitionOffset('.logo.header.active', 'top', -Number.parseFloat(offscreenOffsets.logoHeaderTop), transitionDurMs, delay); doTransitionOffset('.claim.active', 'top', -Number.parseFloat(offscreenOffsets.claimTop), transitionDurMs, delay); doTransitionOffset('.ssidTreeChart', 'left', -Number.parseFloat(offscreenOffsets.ssidsChartLeft), transitionDurMs, delay); doTransitionOffset('.activeVendorChart', 'left', -Number.parseFloat(offscreenOffsets.activeVendorChartLeft), transitionDurMs, delay); return doTransitionOffset('.numSsidChart', 'left', -Number.parseFloat(offscreenOffsets.numSsidsChartLeft), transitionDurMs, delay); } /** Do the animations for the state transition to a new slide (chart, video, active, ...)*/ function transitionToState(newState) { if(newState === currState || transitionInProgress) return; var endTransCb = null; if(currState === states.chart) endTransCb = hideChartAnimation(); else if(currState === states.video) endTransCb = hideVideoAnimation(); else if(currState === states.active) endTransCb = hideActiveAnimation(); currState = newState; var animation = null; var delay = transitionDurMs * 0.5; if(newState === states.chart) animation = showChartAnimation(delay); else if(newState === states.video) animation = showVideoAnimation(delay); else if(newState == states.active) animation = showActiveAnimation(delay); if(endTransCb) animation = animation.transition().each('end', endTransCb); animation.transition().each('end', function() { transitionInProgress = false; }); // Need to call transition again to prevent overriding existing end event handler } /////////////////////// // Initialise the app d3.select('.logo.header').style('top', '-450px').transition() .duration(1500).style('top', '0px'); // First get the xml file for the POST request d3.xhr('maltegoRequestClients.xml').get(function(err, data) { data.responseXML.querySelector('AdditionalFields Field[Name="start_time"]') .textContent = '2016-08-28 18:14:33.717'; data.responseXML.querySelector('AdditionalFields Field[Name="end_time"]') .textContent = '2016-08-29 13:00:37.507'; // Now POST to the backend to retrieve the raw data d3.xhr(backendURL + '/transforms/fetchClients.py?Key=031e0d4e8a573436b5d62054354f2ce0') .header("Content-Type", "application/xml") .post(new XMLSerializer().serializeToString(data.responseXML), function(err, data) { data = parseMaltegoClientsData(maltegoTransformToXmlDOM(data.responseText)); visitorChart = createUpdateVisitorChart(data); }); // Now fetch all the clients for the long-term statistics data.responseXML.querySelector('AdditionalFields Field[Name="start_time"]') .textContent = '2016-01-01 00:00:00.000'; data.responseXML.querySelector('AdditionalFields Field[Name="end_time"]') .textContent = '2016-12-31 23:59:59.000'; d3.xhr(backendURL + '/transforms/fetchClients.py?Key=031e0d4e8a573436b5d62054354f2ce0') .header("Content-Type", "application/xml") .post(new XMLSerializer().serializeToString(data.responseXML), function(err, data) { data = parseMaltegoClientsData(maltegoTransformToXmlDOM(data.responseText)); updateNumSsidsChart(data, numSsidsChart); }); // Update the data with a bigger time window window.setTimeout(function() { console.log('visitors update: ' + new Date()); data.responseXML.querySelector('AdditionalFields Field[Name="end_time"]') .textContent = '2016-08-31 13:43:07.000'; d3.xhr(backendURL + '/transforms/fetchClients.py?Key=031e0d4e8a573436b5d62054354f2ce0') .header("Content-Type", "application/xml") .post(new XMLSerializer().serializeToString(data.responseXML), function(err, data) { data = parseMaltegoClientsData(maltegoTransformToXmlDOM(data.responseText)); visitorChart = createUpdateVisitorChart(data, visitorChart); }); return true; // stop timer }, 5000); }); d3.xhr('maltegoRequestClientsActive.xml').get(function(err, data) { d3.xhr(backendURL + '/transforms/fetchClientsActive.py?Key=031e0d4e8a573436b5d62054354f2ce0') .header("Content-Type", "application/xml") .post(data.responseText, function(err, data) { var data = parseMaltegoClientsActiveData(maltegoTransformToXmlDOM(data.responseText)); createUpdateRadarChart(data); updateClosestActiveChartData(); }); // TODO Update the chart continuously, see https://bost.ocks.org/mike/path/ for a good technique window.setInterval(function() { console.log('active update: ' + new Date()); // data.responseXML.querySelector('AdditionalFields Field[Name="end_time"]') // .textContent = '2016-08-31 13:43:07.000'; d3.xhr(backendURL + '/transforms/fetchClientsActive.py?Key=031e0d4e8a573436b5d62054354f2ce0') .header("Content-Type", "application/xml") .post(new XMLSerializer().serializeToString(data.responseXML), function(err, data) { var data = parseMaltegoClientsActiveData(maltegoTransformToXmlDOM(data.responseText)); createUpdateRadarChart(data); updateClosestActiveChartData(); }); }, 5000); }); d3.xhr('maltegoRequestSSIDs.xml').get(function(err, data) { maltegoRequestSSIDsXML = data.responseXML; }); // Draw the demo SSID tree, will be updated with real information as soon as it is available updateSsidChart(closestActiveClientSSIDInfo); // Go back to the chart slide when the full screen video is done d3.select('.video.car2ad').on('ended', function() { transitionToState(states.chart); }); // Key handlers for state transitions window.onkeyup = function(e) { var key = e.keyCode ? e.keyCode : e.which; console.log('key: ' + key); if(key === 86) // v transitionToState(states.video); else if(key == 67) // c transitionToState(states.chart); else if(key === 65) // a transitionToState(states.active); } // XXX dev // transitionToState(states.active); // TODO Think about how to remote-control the state transitions // TODO Use the last N hours for the backend queries // TODO Update the average duration // TODO Add chart labels