Skip to content

Instantly share code, notes, and snippets.

@duncansmart
Last active March 28, 2024 15:09
Show Gist options
  • Select an option

  • Save duncansmart/ed19dab6780e3f4db88c048c25b2b7d3 to your computer and use it in GitHub Desktop.

Select an option

Save duncansmart/ed19dab6780e3f4db88c048c25b2b7d3 to your computer and use it in GitHub Desktop.

Revisions

  1. duncansmart revised this gist Nov 26, 2020. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions readme.md
    Original file line number Diff line number Diff line change
    @@ -6,6 +6,7 @@ Usage:
    * Run `node edgemax-dump-traffic-stats.js`

    To write to a file, pipe to a file like so:

    node edgemax-dump-traffic-stats.js > edgerouter-stats.csv

    Only tested on EdgeRouter X v2.0.8-hotfix.1
  2. duncansmart revised this gist Nov 26, 2020. 1 changed file with 11 additions and 0 deletions.
    11 changes: 11 additions & 0 deletions readme.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,11 @@
    Usage:

    * Download both files: `edgemax-dump-traffic-stats.js` and `package.json` to the same directory.
    * Run `npm install` in that dir to get all the dependencies.
    * Update the URL/username/password in `edgemax-dump-traffic-stats.js` for your environment
    * Run `node edgemax-dump-traffic-stats.js`

    To write to a file, pipe to a file like so:
    node edgemax-dump-traffic-stats.js > edgerouter-stats.csv

    Only tested on EdgeRouter X v2.0.8-hotfix.1
  3. duncansmart revised this gist Nov 26, 2020. No changes.
  4. duncansmart revised this gist Nov 26, 2020. No changes.
  5. duncansmart revised this gist Jul 8, 2020. 1 changed file with 6 additions and 0 deletions.
    6 changes: 6 additions & 0 deletions package.json
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,6 @@
    {
    "dependencies": {
    "axios": "^0.19.2",
    "ws": "^7.3.0"
    }
    }
  6. duncansmart created this gist Jul 8, 2020.
    192 changes: 192 additions & 0 deletions edgemax-dump-traffic-stats.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,192 @@
    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();
    }