const url = require('url'); const axios = require('axios'); const qs = require('querystring') const WebSocket = require('ws'); // disable certificate verification process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; const baseUrl = 'https://192.168.1.1'; const username = 'ubnt'; const password = 'Pa55w0rd'; (async () => { //console.error('Start'); const sessionCookie = await logon(baseUrl, username, password); //console.error('Logged on'); //console.error({ sessionCookie }); await refreshHostNames(sessionCookie) const { protocol, hostname } = url.parse(baseUrl) const wsproto = protocol == 'https:' ? 'wss:' : 'ws:'; const hostnameIsIp = (/\d+\.\d+\.\d+\.\d+/).test(hostname) const ws = new WebSocket(`${wsproto}//${hostname}/ws/stats`, { servername: hostnameIsIp ? '' : undefined // suppress "[DEP0123] DeprecationWarning: Setting the TLS ServerName to an IP address is not permitted by RFC 6066. This will be ignored in a future version." }); ws.on('open', function open(x) { //console.error("Opening websocket"); const initMessage = JSON.stringify({ SUBSCRIBE: [{ name: "export" }], SESSION_ID: sessionCookie }); //console.error("Sending: ", initMessage); ws.send(initMessage.length + '\n' + initMessage, function (e) { if (e) console.error('init message error', e); }); //console.error('sent init message'); }); let messageLength = 0; let messageContent = ''; ws.on('message', async (data) => { if (messageLength == 0) { //console.log('... new msg'); const newlinepos = data.indexOf('\n'); messageLength = ~~data.slice(0, newlinepos); messageContent = data.slice(newlinepos + 1); } else { //... append messageContent += data; } if (messageContent.length < messageLength) { // incomplete, wait for next part return; } await handleMessage(messageContent); try { await refreshHostNames(sessionCookie); } catch (error) { console.error('ERROR: refreshHostNames ', error); } messageLength = 0; messageContent = ""; }); ws.on('error', (code, reason) => { console.error('WS ERROR', { code, reason }); }) ws.on('close', (code, reason) => { console.error('WS CLOSE', { code, reason }); }) })(); const _hostnames = {}; let _messageCount = 0 async function handleMessage(messageContent) { // header row if (_messageCount++ == 0) { console.log('time,categoryName,appName,hostname,ip,tx_bytes,tx_rate,rx_bytes,rx_rate'); } /* { "export": { "192.168.1.66": { "Google Static Content(SSL)|Network protocols": { "tx_bytes": "2355", "tx_rate": "0", "rx_bytes": "1500", "rx_rate": "0" }, "Youtube|Media streaming services": { "tx_bytes": "85079", "tx_rate": "0", "rx_bytes": "688716", "rx_rate": "0" }, }, ... */ //console.error(messageContent); const message = JSON.parse(messageContent); if (!message["export"]) return; const time = new Date().toISOString().substring(0, 19).replace('T', ' '); const exportItems = message['export']; for (const ip in exportItems) { const exportItem = exportItems[ip]; for (const appAndCategory in exportItem) { const stat = exportItem[appAndCategory] const hostname = _hostnames[ip] || ip; const [appName, categoryName] = appAndCategory.split(/\|/); console.log(`${time},${csvEncode(categoryName)},${csvEncode(appName)},${csvEncode(hostname)},${ip},${stat.tx_bytes},${stat.tx_rate},${stat.rx_bytes},${stat.rx_rate}`); } } } function csvEncode(str) { if (str == null) return ''; if (str.indexOf(",") > -1 || str.indexOf("\"") > -1) str = '"' + str.replace(/"/g, '""') + '"'; return str; } async function logon(baseUrl, username, password) { const form = { username: username, password: password }; const response = await axios.post(baseUrl, qs.stringify(form), { maxRedirects: 0, validateStatus: () => true // accept all certs }); if (!response.headers['set-cookie']){ throw new Error('Logon failed, please check username/password') } const cookies = response.headers['set-cookie'].reduce((obj, item) => { const [name, value] = item.split(/=/); obj[name] = value.split(/;/)[0]; return obj; }, {}); //console.log({cookies}); const sessionCookie = cookies['beaker.session.id']; return sessionCookie; } async function refreshHostNames(sessionCookie) { // only run every minute if ((new Date() - (refreshHostNames.lastUpdated || 0)) / 1000 < 60) return; //console.error('Refresh host names') const response = await axios.get(baseUrl.replace(/\/$/, '') + "/api/edge/data.json?data=dhcp_leases", { headers: { "Cookie": `beaker.session.id=${sessionCookie}` } }); //console.log("refreshHostNames:", response.data.output['dhcp-server-leases'].LAN); const leases = response.data.output['dhcp-server-leases'].LAN; for (const ip in leases) { _hostnames[ip] = leases[ip]['client-hostname'].replace(/\?/g, ''); } refreshHostNames.lastUpdated = new Date(); }