From 7d9fd4576b351bb7fabf012cb91cbf4e14e660ac Mon Sep 17 00:00:00 2001 From: coolsnowwolf Date: Sun, 30 Sep 2018 19:31:45 +0800 Subject: [PATCH] BIGGGGG Upate! An all new Real Time Traffic monitor luci package has been added! --- package/lean/luci-app-wrtbwmon/Makefile | 17 + .../htdocs/luci-static/wrtbwmon.js | 562 ++++++++++++++++++ .../luasrc/controller/wrtbwmon.lua | 43 ++ .../luasrc/model/cbi/wrtbwmon/config.lua | 18 + .../luasrc/model/cbi/wrtbwmon/custom.lua | 23 + .../luasrc/view/wrtbwmon.htm | 46 ++ .../luci-app-wrtbwmon/po/zh-cn/wrtbwmon.po | 30 + .../root/etc/config/wrtbwmon | 4 + .../root/etc/init.d/wrtbwmon | 22 + .../root/etc/uci-defaults/luci-wrtbwmon | 5 + .../root/usr/sbin/readDB.awk | 157 +++++ .../luci-app-wrtbwmon/root/usr/sbin/wrtbwmon | 301 ++++++++++ .../root/usr/share/wrtbwmon/usage.htm1 | 23 + .../root/usr/share/wrtbwmon/usage.htm2 | 14 + 14 files changed, 1265 insertions(+) create mode 100644 package/lean/luci-app-wrtbwmon/Makefile create mode 100644 package/lean/luci-app-wrtbwmon/htdocs/luci-static/wrtbwmon.js create mode 100644 package/lean/luci-app-wrtbwmon/luasrc/controller/wrtbwmon.lua create mode 100644 package/lean/luci-app-wrtbwmon/luasrc/model/cbi/wrtbwmon/config.lua create mode 100644 package/lean/luci-app-wrtbwmon/luasrc/model/cbi/wrtbwmon/custom.lua create mode 100644 package/lean/luci-app-wrtbwmon/luasrc/view/wrtbwmon.htm create mode 100644 package/lean/luci-app-wrtbwmon/po/zh-cn/wrtbwmon.po create mode 100644 package/lean/luci-app-wrtbwmon/root/etc/config/wrtbwmon create mode 100755 package/lean/luci-app-wrtbwmon/root/etc/init.d/wrtbwmon create mode 100755 package/lean/luci-app-wrtbwmon/root/etc/uci-defaults/luci-wrtbwmon create mode 100755 package/lean/luci-app-wrtbwmon/root/usr/sbin/readDB.awk create mode 100755 package/lean/luci-app-wrtbwmon/root/usr/sbin/wrtbwmon create mode 100644 package/lean/luci-app-wrtbwmon/root/usr/share/wrtbwmon/usage.htm1 create mode 100644 package/lean/luci-app-wrtbwmon/root/usr/share/wrtbwmon/usage.htm2 diff --git a/package/lean/luci-app-wrtbwmon/Makefile b/package/lean/luci-app-wrtbwmon/Makefile new file mode 100644 index 000000000..dbf91c76d --- /dev/null +++ b/package/lean/luci-app-wrtbwmon/Makefile @@ -0,0 +1,17 @@ +# Copyright (C) 2016 Openwrt.org +# +# This is free software, licensed under the Apache License, Version 2.0 . +# + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI support for Wrtbwmon +LUCI_DEPENDS:=+luci-app-nlbwmon +LUCI_PKGARCH:=all +PKG_VERSION:=1.0 +PKG_RELEASE:=1 + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature + diff --git a/package/lean/luci-app-wrtbwmon/htdocs/luci-static/wrtbwmon.js b/package/lean/luci-app-wrtbwmon/htdocs/luci-static/wrtbwmon.js new file mode 100644 index 000000000..36812e723 --- /dev/null +++ b/package/lean/luci-app-wrtbwmon/htdocs/luci-static/wrtbwmon.js @@ -0,0 +1,562 @@ +var wrt = { + // variables for auto-update, interval is in seconds + scheduleTimeout: undefined, + updateTimeout: undefined, + isScheduled: true, + interval: 5, + // option on whether to show per host sub-totals + perHostTotals: false, + // variables for sorting + sortData: { + column: 7, + elId: 'thTotal', + dir: 'desc', + cache: {} + } +}; + +(function () { + var oldDate, oldValues = []; + + // find base path + var re = /(.*?admin\/nlbw\/[^/]+)/; + var basePath = window.location.pathname.match(re)[1]; + + //---------------------- + // HELPER FUNCTIONS + //---------------------- + + /** + * Human readable text for size + * @param size + * @returns {string} + */ + function getSize(size) { + var prefix = [' ', 'k', 'M', 'G', 'T', 'P', 'E', 'Z']; + var precision, base = 1000, pos = 0; + while (size > base) { + size /= base; + pos++; + } + if (pos > 2) precision = 1000; else precision = 1; + return (Math.round(size * precision) / precision) + ' ' + prefix[pos] + 'B'; + } + + /** + * Human readable text for date + * @param date + * @returns {string} + */ + function dateToString(date) { + return date.toString().substring(0, 24); + } + + /** + * Gets the string representation of the date received from BE + * @param value + * @returns {*} + */ + function getDateString(value) { + var tmp = value.split('_'), + str = tmp[0].split('-').reverse().join('-') + 'T' + tmp[1]; + return dateToString(new Date(str)); + } + + /** + * Create a `tr` element with content + * @param content + * @returns {string} + */ + function createTR(content) { + var res = '' + data[2], {title: data[1]}), + createTD(getSize(dlSpeed) + '/s', {right: true}), + createTD(getSize(upSpeed) + '/s', {right: true}), + createTD(getSize(data[3]), {right: true}), + createTD(getSize(data[4]), {right: true}), + createTD(getSize(data[5]), {right: true}), + createTD(getDateString(data[6])), + createTD(getDateString(data[7])) + ]; + + // display row data + var result = ''; + for (var k = 0; k < displayData.length; k++) { + result += displayData[k]; + } + result = createTR(result); + return [result, rowData]; + } + + /** + * Creates the HTML output based on the `data` and `totals` inputs + * @param data + * @param totals + * @returns {string} HTML output + */ + function getDisplayData(data, totals) { + var result = + createTH('客户端', {id: 'thClient'}) + + createTH('下载带宽', {id: 'thDownload'}) + + createTH('上传带宽', {id: 'thUpload'}) + + createTH('总下载流量', {id: 'thTotalDown'}) + + createTH('总上传流量', {id: 'thTotalUp'}) + + createTH('流量合计', {id: 'thTotal'}) + + createTH('首次上线时间', {id: 'thFirstSeen'}) + + createTH('最后上线时间', {id: 'thLastSeen'}); + result = createTR(result); + for (var k = 0; k < data.length; k++) { + result += data[k][0]; + } + var totalsRow = createTH('总计'); + for (var m = 0; m < totals.length; m++) { + var t = totals[m]; + totalsRow += createTD(getSize(t) + (m < 2 ? '/s' : ''), {right: true}); + } + result += createTR(totalsRow); + return result; + } + + /** + * Calculates per host sub-totals and adds them in the data input + * @param data The data input + */ + function aggregateHostTotals(data) { + if (!wrt.perHostTotals) return; + + var curHost = 0, insertAt = 1; + while (curHost < data.length && insertAt < data.length) { + // grab the current hostname/mac, and walk the data looking for rows with the same host/mac + var hostName = data[curHost][1][0].toLowerCase(); + for (var k = curHost + 1; k < data.length; k++) { + if (data[k][1][0].toLowerCase() === hostName) { + // this is another row for the same host, group it with any other rows for this host + data.splice(insertAt, 0, data.splice(k, 1)[0]); + insertAt++; + } + } + + // if we found more than one row for the host, add a subtotal row + if (insertAt > curHost + 1) { + var hostTotals = [data[curHost][1][0], '', '', 0, 0, 0, 0, 0]; + for (var i = curHost; i < insertAt && i < data.length; i++) { + for (var j = 3; j < hostTotals.length; j++) { + hostTotals[j] += data[i][1][j]; + } + } + var hostTotalRow = createTH(data[curHost][1][0] + '
(host total)', {title: data[curHost][1][1]}); + for (var m = 3; m < hostTotals.length; m++) { + var t = hostTotals[m]; + hostTotalRow += createTD(getSize(t) + (m < 5 ? '/s' : ''), {right: true}); + } + hostTotalRow = createTR(hostTotalRow); + data.splice(insertAt, 0, [hostTotalRow, hostTotals]); + } + curHost = insertAt; + insertAt = curHost + 1; + } + } + + /** + * Sorting function used to sort the `data`. Uses the global sort settings + * @param x first item to compare + * @param y second item to compare + * @returns {number} 1 for desc, -1 for asc, 0 for equal + */ + function sortingFunction(x, y) { + // get data from global variable + var sortColumn = wrt.sortData.column, sortDirection = wrt.sortData.dir; + var a = x[1][sortColumn]; + var b = y[1][sortColumn]; + if (a === b) { + return 0; + } else if (sortDirection === 'desc') { + return a < b ? 1 : -1; + } else { + return a > b ? 1 : -1; + } + } + + /** + * Sets the relevant global sort variables and re-renders the table to apply the new sorting + * @param elId + * @param column + */ + function setSortColumn(elId, column) { + if (column === wrt.sortData.column) { + // same column clicked, switch direction + wrt.sortData.dir = wrt.sortData.dir === 'desc' ? 'asc' : 'desc'; + } else { + // change sort column + wrt.sortData.column = column; + // reset sort direction + wrt.sortData.dir = 'desc'; + } + wrt.sortData.elId = elId; + + // render table data from cache + renderTableData(wrt.sortData.cache.data, wrt.sortData.cache.totals); + } + + /** + * Registers the table events handlers for sorting when clicking the column headers + */ + function registerTableEventHandlers() { + // note these ordinals are into the data array, not the table output + document.getElementById('thClient').addEventListener('click', function () { + setSortColumn(this.id, 0); // hostname + }); + document.getElementById('thDownload').addEventListener('click', function () { + setSortColumn(this.id, 3); // dl speed + }); + document.getElementById('thUpload').addEventListener('click', function () { + setSortColumn(this.id, 4); // ul speed + }); + document.getElementById('thTotalDown').addEventListener('click', function () { + setSortColumn(this.id, 5); // total down + }); + document.getElementById('thTotalUp').addEventListener('click', function () { + setSortColumn(this.id, 6); // total up + }); + document.getElementById('thTotal').addEventListener('click', function () { + setSortColumn(this.id, 7); // total + }); + } + + /** + * Fetches and handles the updated `values` from the BE + * @param once If set to true, it re-schedules itself for execution based on selected interval + */ + function receiveData(once) { + var ajax = new XMLHttpRequest(); + ajax.onreadystatechange = function () { + // noinspection EqualityComparisonWithCoercionJS + if (this.readyState == 4 && this.status == 200) { + var re = /(var values = new Array[^;]*;)/, + match = ajax.responseText.match(re); + if (!match) { + handleError(); + } else { + // evaluate values + eval(match[1]); + //noinspection JSUnresolvedVariable + var v = values; + if (!v) { + handleError(); + } else { + handleValues(v); + // set old values + oldValues = v; + // set old date + oldDate = new Date(); + document.getElementById('updated').innerHTML = '数据更新时间 ' + dateToString(oldDate); + } + } + var int = wrt.interval; + if (!once && int > 0) reschedule(int); + } + }; + ajax.open('GET', basePath + '/usage_data', true); + ajax.send(); + } + + /** + * Registers DOM event listeners for user interaction + */ + function addEventListeners() { + document.getElementById('intervalSelect').addEventListener('change', function () { + var int = wrt.interval = this.value; + if (int > 0) { + // it is not scheduled, schedule it + if (!wrt.isScheduled) { + reschedule(int); + } + } else { + // stop the scheduling + stopSchedule(); + } + }); + + document.getElementById('resetDatabase').addEventListener('click', function () { + if (confirm('This will delete the database file. Are you sure?')) { + var ajax = new XMLHttpRequest(); + ajax.onreadystatechange = function () { + // noinspection EqualityComparisonWithCoercionJS + if (this.readyState == 4 && this.status == 204) { + location.reload(); + } + }; + ajax.open('GET', basePath + '/usage_reset', true); + ajax.send(); + } + }); + + document.getElementById('perHostTotals').addEventListener('change', function () { + wrt.perHostTotals = !wrt.perHostTotals; + }); + } + + //---------------------- + // AUTO-UPDATE + //---------------------- + + /** + * Stop auto-update schedule + */ + function stopSchedule() { + window.clearTimeout(wrt.scheduleTimeout); + window.clearTimeout(wrt.updateTimeout); + setUpdateMessage(''); + wrt.isScheduled = false; + } + + /** + * Start auto-update schedule + * @param seconds + */ + function reschedule(seconds) { + wrt.isScheduled = true; + seconds = seconds || 60; + updateSeconds(seconds); + wrt.scheduleTimeout = window.setTimeout(receiveData, seconds * 1000); + } + + /** + * Sets the text of the `#updating` element + * @param msg + */ + function setUpdateMessage(msg) { + document.getElementById('updating').innerHTML = msg; + } + + /** + * Updates the 'Updating in X seconds' message + * @param start + */ + function updateSeconds(start) { + setUpdateMessage('倒数 ' + start + ' 秒后刷新.'); + if (start > 0) { + wrt.updateTimeout = window.setTimeout(function () { + updateSeconds(start - 1); + }, 1000); + } + } + + //---------------------- + // END AUTO-UPDATE + //---------------------- + + /** + * Check for dependency, and if all is well, run callback + * @param cb Callback function + */ + function checkForDependency(cb) { + var ajax = new XMLHttpRequest(); + ajax.onreadystatechange = function () { + // noinspection EqualityComparisonWithCoercionJS + if (this.readyState == 4 && this.status == 200) { + // noinspection EqualityComparisonWithCoercionJS + if (ajax.responseText == "1") { + cb(); + } else { + alert("wrtbwmon is not installed!"); + } + } + }; + ajax.open('GET', basePath + '/check_dependency', true); + ajax.send(); + } + + checkForDependency(function () { + // register events + addEventListeners(); + // Main entry point + receiveData(); + }); + +})(); diff --git a/package/lean/luci-app-wrtbwmon/luasrc/controller/wrtbwmon.lua b/package/lean/luci-app-wrtbwmon/luasrc/controller/wrtbwmon.lua new file mode 100644 index 000000000..4211100b5 --- /dev/null +++ b/package/lean/luci-app-wrtbwmon/luasrc/controller/wrtbwmon.lua @@ -0,0 +1,43 @@ +module("luci.controller.wrtbwmon", package.seeall) + +function index() + entry({"admin", "nlbw", "usage"}, alias("admin", "nlbw", "usage", "details"), _("Usage"), 60) + entry({"admin", "nlbw", "usage", "details"}, template("wrtbwmon"), _("Details"), 10).leaf=true + entry({"admin", "nlbw", "usage", "config"}, cbi("wrtbwmon/config"), _("Configuration"), 20).leaf=true + entry({"admin", "nlbw", "usage", "custom"}, cbi("wrtbwmon/custom"), _("User file"), 30).leaf=true + entry({"admin", "nlbw", "usage", "check_dependency"}, call("check_dependency")).dependent=true + entry({"admin", "nlbw", "usage", "usage_data"}, call("usage_data")).dependent=true + entry({"admin", "nlbw", "usage", "usage_reset"}, call("usage_reset")).dependent=true +end + +function usage_database_path() + local cursor = luci.model.uci.cursor() + if cursor:get("wrtbwmon", "general", "persist") == "1" then + return "/etc/config/usage.db" + else + return "/tmp/usage.db" + end +end + +function check_dependency() + local ret = "0" + if require("luci.model.ipkg").installed('iptables') then + ret = "1" + end + luci.http.prepare_content("text/plain") + luci.http.write(ret) +end + +function usage_data() + local db = usage_database_path() + local publish_cmd = "wrtbwmon publish " .. db .. " /tmp/usage.htm /etc/wrtbwmon.user" + local cmd = "wrtbwmon update " .. db .. " && " .. publish_cmd .. " && cat /tmp/usage.htm" + luci.http.prepare_content("text/html") + luci.http.write(luci.sys.exec(cmd)) +end + +function usage_reset() + local db = usage_database_path() + local ret = luci.sys.call("wrtbwmon update " .. db .. " && rm " .. db) + luci.http.status(204) +end diff --git a/package/lean/luci-app-wrtbwmon/luasrc/model/cbi/wrtbwmon/config.lua b/package/lean/luci-app-wrtbwmon/luasrc/model/cbi/wrtbwmon/config.lua new file mode 100644 index 000000000..10edc41cf --- /dev/null +++ b/package/lean/luci-app-wrtbwmon/luasrc/model/cbi/wrtbwmon/config.lua @@ -0,0 +1,18 @@ +local m = Map("wrtbwmon", "详细设置") + +local s = m:section(NamedSection, "general", "wrtbwmon", "通用设置") + +local o = s:option(Flag, "persist", "写入数据库到ROM", + "把统计数据写入 /etc/config 中避免重启或者升级后丢失 (需要占用ROM空间并降低闪存寿命)") +o.rmempty = false + +function o.write(self, section, value) + if value == '1' then + luci.sys.call("mv /tmp/usage.db /etc/config/usage.db") + elseif value == '0' then + luci.sys.call("mv /etc/config/usage.db /tmp/usage.db") + end + return Flag.write(self, section ,value) +end + +return m diff --git a/package/lean/luci-app-wrtbwmon/luasrc/model/cbi/wrtbwmon/custom.lua b/package/lean/luci-app-wrtbwmon/luasrc/model/cbi/wrtbwmon/custom.lua new file mode 100644 index 000000000..cd882d4c8 --- /dev/null +++ b/package/lean/luci-app-wrtbwmon/luasrc/model/cbi/wrtbwmon/custom.lua @@ -0,0 +1,23 @@ +local USER_FILE_PATH = "/etc/wrtbwmon.user" + +local fs = require "nixio.fs" + +local f = SimpleForm("wrtbwmon", + "自定义MAC地址对应的主机名", + "每一行的格式为 00:aa:bb:cc:ee:ff,username (不支持中文主机名)") + +local o = f:field(Value, "_custom") + +o.template = "cbi/tvalue" +o.rows = 20 + +function o.cfgvalue(self, section) + return fs.readfile(USER_FILE_PATH) +end + +function o.write(self, section, value) + value = value:gsub("\r\n?", "\n") + fs.writefile(USER_FILE_PATH, value) +end + +return f diff --git a/package/lean/luci-app-wrtbwmon/luasrc/view/wrtbwmon.htm b/package/lean/luci-app-wrtbwmon/luasrc/view/wrtbwmon.htm new file mode 100644 index 000000000..c1bf43952 --- /dev/null +++ b/package/lean/luci-app-wrtbwmon/luasrc/view/wrtbwmon.htm @@ -0,0 +1,46 @@ +<%+header%> +

客户端实时流量监测

+

+ +

+

+ + + +
+ + +
+

+ + +
Loading...
+ + +<%+footer%> diff --git a/package/lean/luci-app-wrtbwmon/po/zh-cn/wrtbwmon.po b/package/lean/luci-app-wrtbwmon/po/zh-cn/wrtbwmon.po new file mode 100644 index 000000000..47e8ea29d --- /dev/null +++ b/package/lean/luci-app-wrtbwmon/po/zh-cn/wrtbwmon.po @@ -0,0 +1,30 @@ +msgid "Usage" +msgstr "实时流量监测" + +msgid "Details" +msgstr "详细信息" + +msgid "Configuration" +msgstr "配置" + +msgid "User file" +msgstr "自定义主机信息" + +msgid "Usage - Configuration" +msgstr "详细设置" + +msgid "General settings" +msgstr "通用设置" + +msgid "Persist database" +msgstr "写入数据库到硬盘" + +msgid "Check this to persist the database file under /etc/config. " +msgstr "把统计数据写入 /etc/config 中避免重启或者升级后丢失 (需要占用空间并降低ROM寿命)" + +msgid "Usage - Custom User File" +msgstr "自定义MAC地址对应的主机名" + +msgid "This file is used to match users with MAC addresses and it must have the following format: 00:aa:bb:cc:ee:ff,username" +msgstr "每一行的格式为 00:aa:bb:cc:ee:ff,username (不支持中文主机名)" + diff --git a/package/lean/luci-app-wrtbwmon/root/etc/config/wrtbwmon b/package/lean/luci-app-wrtbwmon/root/etc/config/wrtbwmon new file mode 100644 index 000000000..419270dba --- /dev/null +++ b/package/lean/luci-app-wrtbwmon/root/etc/config/wrtbwmon @@ -0,0 +1,4 @@ + +config wrtbwmon 'general' + option persist '0' + diff --git a/package/lean/luci-app-wrtbwmon/root/etc/init.d/wrtbwmon b/package/lean/luci-app-wrtbwmon/root/etc/init.d/wrtbwmon new file mode 100755 index 000000000..d57c09ae1 --- /dev/null +++ b/package/lean/luci-app-wrtbwmon/root/etc/init.d/wrtbwmon @@ -0,0 +1,22 @@ +#!/bin/sh /etc/rc.common +# +# start/stop wrtbwmon bandwidth monitor + +### BEGIN INIT INFO +# Provides: wrtbwmon +# Required-Start: $network $local_fs $remote_fs +# Required-Stop: $local_fs $remote_fs +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: iptables-based bandwidth monitor +### END INIT INFO + +START=91 + +start(){ + /usr/sbin/wrtbwmon setup /tmp/usage.db +} + +stop(){ + /usr/sbin/wrtbwmon remove +} diff --git a/package/lean/luci-app-wrtbwmon/root/etc/uci-defaults/luci-wrtbwmon b/package/lean/luci-app-wrtbwmon/root/etc/uci-defaults/luci-wrtbwmon new file mode 100755 index 000000000..c86001a6f --- /dev/null +++ b/package/lean/luci-app-wrtbwmon/root/etc/uci-defaults/luci-wrtbwmon @@ -0,0 +1,5 @@ +#!/bin/sh + +/etc/init.d/wrtbwmon enable +/etc/init.d/wrtbwmon start +exit 0 diff --git a/package/lean/luci-app-wrtbwmon/root/usr/sbin/readDB.awk b/package/lean/luci-app-wrtbwmon/root/usr/sbin/readDB.awk new file mode 100755 index 000000000..fe67e4ae8 --- /dev/null +++ b/package/lean/luci-app-wrtbwmon/root/usr/sbin/readDB.awk @@ -0,0 +1,157 @@ +#!/usr/bin/awk + +function inInterfaces(host){ + return(interfaces ~ "(^| )"host"($| )") +} + +function newRule(arp_ip, + ipt_cmd){ + # checking for existing rules shouldn't be necessary if newRule is + # always called after db is read, arp table is read, and existing + # iptables rules are read. + ipt_cmd="iptables -t mangle -j RETURN -s " arp_ip + system(ipt_cmd " -C RRDIPT_FORWARD 2>/dev/null || " ipt_cmd " -A RRDIPT_FORWARD") + ipt_cmd="iptables -t mangle -j RETURN -d " arp_ip + system(ipt_cmd " -C RRDIPT_FORWARD 2>/dev/null || " ipt_cmd " -A RRDIPT_FORWARD") +} + +function total(i){ + return(bw[i "/in"] + bw[i "/out"]) +} + +function date( cmd, d){ + cmd="date +%d-%m-%Y_%H:%M:%S" + cmd | getline d + close(cmd) + #!@todo could start a process with "while true; do date ...; done" + return(d) +} + +BEGIN { + od="" + fid=1 + debug=0 + rrd=0 +} + +/^#/ { # get DB filename + FS="," + dbFile=FILENAME + next +} + +# data from database; first file +FNR==NR { #!@todo this doesn't help if the DB file is empty. + if($2 == "NA") + #!@todo could get interface IP here + n=$1 + else + n=$2 + + hosts[n] = "" # add this host/interface to hosts + mac[n] = $1 + ip[n] = $2 + inter[n] = $3 + bw[n "/in"] = $4 + bw[n "/out"] = $5 + firstDate[n] = $7 + lastDate[n] = $8 + next +} + +# not triggered on the first file +FNR==1 { + FS=" " + fid++ #!@todo use fid for all files; may be problematic for empty files + next +} + +# arp: ip hw flags hw_addr mask device +fid==2 { + #!@todo regex match IPs and MACs for sanity + arp_ip = $1 + arp_flags = $3 + arp_mac = $4 + arp_dev = $6 + if(arp_flags != "0x0" && !(arp_ip in ip)){ + if(debug) + print "new host:", arp_ip, arp_flags > "/dev/stderr" + hosts[arp_ip] = "" + mac[arp_ip] = arp_mac + ip[arp_ip] = arp_ip + inter[arp_ip] = arp_dev + bw[arp_ip "/in"] = bw[arp_ip "/out"] = 0 + firstDate[arp_ip] = lastDate[arp_ip] = date() + } + next +} + +#!@todo could use mangle chain totals or tailing "unnact" rules to +# account for data for new hosts from their first presence on the +# network to rule creation. The "unnact" rules would have to be +# maintained at the end of the list, and new rules would be inserted +# at the top. + +# skip line +# read the chain name and deal with the data accordingly +fid==3 && $1 == "Chain"{ + rrd=$2 ~ /RRDIPT_.*/ + next +} + +fid==3 && rrd && (NF < 9 || $1=="pkts"){ next } + +fid==3 && rrd { # iptables input + if($6 != "*"){ + m=$6 + n=m "/out" + } else if($7 != "*"){ + m=$7 + n=m "/in" + } else if($8 != "0.0.0.0/0"){ + m=$8 + n=m "/out" + } else { # $9 != "0.0.0.0/0" + m=$9 + n=m "/in" + } + + # remove host from array; any hosts left in array at END get new + # iptables rules + + #!@todo this deletes a host if any rule exists; if only one + # directional rule is removed, this will not remedy the situation + delete hosts[m] + + if($2 > 0){ # counted some bytes + if(mode == "diff" || mode == "noUpdate") + print n, $2 + if(mode!="noUpdate"){ + if(inInterfaces(m)){ # if label is an interface + if(!(m in mac)){ # if label was not in db (also not in + # arp table, but interfaces won't be + # there anyway) + firstDate[m] = date() + mac[m] = inter[m] = m + ip[m] = "NA" + bw[m "/in"]=bw[m "/out"]= 0 + } + } + bw[n]+=$2 + lastDate[m] = date() + } + } +} + +END { + if(mode=="noUpdate") exit + close(dbFile) + system("rm -f " dbFile) + print "#mac,ip,iface,in,out,total,first_date,last_date" > dbFile + OFS="," + for(i in mac) + print mac[i], ip[i], inter[i], bw[i "/in"], bw[i "/out"], total(i), firstDate[i], lastDate[i] > dbFile + close(dbFile) + # for hosts without rules + for(host in hosts) if(!inInterfaces(host)) newRule(host) +} diff --git a/package/lean/luci-app-wrtbwmon/root/usr/sbin/wrtbwmon b/package/lean/luci-app-wrtbwmon/root/usr/sbin/wrtbwmon new file mode 100755 index 000000000..b2c0b9a07 --- /dev/null +++ b/package/lean/luci-app-wrtbwmon/root/usr/sbin/wrtbwmon @@ -0,0 +1,301 @@ +#!/bin/sh +# +# wrtbwmon: traffic logging tool for routers +# +# Peter Bailey (peter.eldridge.bailey+wrtbwmon AT gmail.com) +# +# Based on work by: +# Emmanuel Brucy (e.brucy AT qut.edu.au) +# Fredrik Erlandsson (erlis AT linux.nu) +# twist - http://wiki.openwrt.org/RrdTrafficWatch + +trap "rm -f /tmp/*_$$.tmp; kill $$" INT +binDir=/usr/sbin +dataDir=/usr/share/wrtbwmon +lockDir=/tmp/wrtbwmon.lock +pidFile=$lockDir/pid +networkFuncs=/lib/functions/network.sh +uci=`which uci 2>/dev/null` +nslookup=`which nslookup 2>/dev/null` +nvram=`which nvram 2>/dev/null` + +chains='INPUT OUTPUT FORWARD' +DEBUG= +interfaces='eth0 tun0' # in addition to detected WAN +DB=$2 +mode= + +# DNS server for reverse lookups provided in "DNS". +# don't perform reverse DNS lookups by default +DO_RDNS=${DNS-} + +header="#mac,ip,iface,in,out,total,first_date,last_date" + +createDbIfMissing() +{ + [ ! -f "$DB" ] && echo $header > "$DB" +} + +checkDbArg() +{ + [ -z "$DB" ] && echo "ERROR: Missing argument 2 (database file)" && exit 1 +} + +checkDB() +{ + [ ! -f "$DB" ] && echo "ERROR: $DB does not exist" && exit 1 + [ ! -w "$DB" ] && echo "ERROR: $DB is not writable" && exit 1 +} + +checkWAN() +{ + [ -z "$wan" ] && echo "Warning: failed to detect WAN interface." +} + +lookup() +{ + MAC=$1 + IP=$2 + userDB=$3 + for USERSFILE in $userDB /tmp/dhcp.leases /tmp/dnsmasq.conf /etc/dnsmasq.conf /etc/hosts; do + [ -e "$USERSFILE" ] || continue + case $USERSFILE in + /tmp/dhcp.leases ) + USER=$(grep -i "$MAC" $USERSFILE | cut -f4 -s -d' ') + ;; + /etc/hosts ) + USER=$(grep "^$IP " $USERSFILE | cut -f2 -s -d' ') + ;; + * ) + USER=$(grep -i "$MAC" "$USERSFILE" | cut -f2 -s -d,) + ;; + esac + [ "$USER" = "*" ] && USER= + [ -n "$USER" ] && break + done + if [ -n "$DO_RDNS" -a -z "$USER" -a "$IP" != "NA" -a -n "$nslookup" ]; then + USER=`$nslookup $IP $DNS | awk '!/server can/{if($4){print $4; exit}}' | sed -re 's/[.]$//'` + fi + [ -z "$USER" ] && USER=${MAC} + echo $USER +} + +detectIF() +{ + if [ -f "$networkFuncs" ]; then + IF=`. $networkFuncs; network_get_device netdev $1; echo $netdev` + [ -n "$IF" ] && echo $IF && return + fi + + if [ -n "$uci" -a -x "$uci" ]; then + IF=`$uci get network.${1}.ifname 2>/dev/null` + [ $? -eq 0 -a -n "$IF" ] && echo $IF && return + fi + + if [ -n "$nvram" -a -x "$nvram" ]; then + IF=`$nvram get ${1}_ifname 2>/dev/null` + [ $? -eq 0 -a -n "$IF" ] && echo $IF && return + fi +} + +detectLAN() +{ + [ -e /sys/class/net/br-lan ] && echo br-lan && return + lan=$(detectIF lan) + [ -n "$lan" ] && echo $lan && return +} + +detectWAN() +{ + [ -n "$WAN_IF" ] && echo $WAN_IF && return + wan=$(detectIF wan) + [ -n "$wan" ] && echo $wan && return + wan=$(ip route show 2>/dev/null | grep default | sed -re '/^default/ s/default.*dev +([^ ]+).*/\1/') + [ -n "$wan" ] && echo $wan && return + [ -f "$networkFuncs" ] && wan=$(. $networkFuncs; network_find_wan wan; echo $wan) + [ -n "$wan" ] && echo $wan && return +} + +lock() +{ + attempts=0 + while [ $attempts -lt 10 ]; do + mkdir $lockDir 2>/dev/null && break + attempts=$((attempts+1)) + pid=`cat $pidFile 2>/dev/null` + if [ -n "$pid" ]; then + if [ -d "/proc/$pid" ]; then + [ -n "$DEBUG" ] && echo "WARNING: Lockfile detected but process $(cat $pidFile) does not exist !" + rm -rf $lockDir + else + sleep 1 + fi + fi + done + mkdir $lockDir 2>/dev/null + echo $$ > $pidFile + [ -n "$DEBUG" ] && echo $$ "got lock after $attempts attempts" + trap '' INT +} + +unlock() +{ + rm -rf $lockDir + [ -n "$DEBUG" ] && echo $$ "released lock" + trap "rm -f /tmp/*_$$.tmp; kill $$" INT +} + +# chain +newChain() +{ + chain=$1 + # Create the RRDIPT_$chain chain (it doesn't matter if it already exists). + iptables -t mangle -N RRDIPT_$chain 2> /dev/null + + # Add the RRDIPT_$chain CHAIN to the $chain chain if not present + iptables -t mangle -C $chain -j RRDIPT_$chain 2>/dev/null + if [ $? -ne 0 ]; then + [ -n "$DEBUG" ] && echo "DEBUG: iptables chain misplaced, recreating it..." + iptables -t mangle -I $chain -j RRDIPT_$chain + fi +} + +# chain tun +newRuleIF() +{ + chain=$1 + IF=$2 + + #!@todo test + if [ "$chain" = "OUTPUT" ]; then + cmd="iptables -t mangle -o $IF -j RETURN" + eval $cmd " -C RRDIPT_$chain 2>/dev/null" || eval $cmd " -A RRDIPT_$chain" + elif [ "$chain" = "INPUT" ]; then + cmd="iptables -t mangle -i $IF -j RETURN" + eval $cmd " -C RRDIPT_$chain 2>/dev/null" || eval $cmd " -A RRDIPT_$chain" + fi +} + +update() +{ + #!@todo could let readDB.awk handle this; that would place header + #!info in fewer places + createDbIfMissing + + checkDB + checkWAN + + > /tmp/iptables_$$.tmp + lock + # only zero our own chains + for chain in $chains; do + iptables -nvxL RRDIPT_$chain -t mangle -Z >> /tmp/iptables_$$.tmp + done + # the iptables and readDB commands have to be separate. Otherwise, + # they will fight over iptables locks + awk -v mode="$mode" -v interfaces=\""$interfaces"\" -f $binDir/readDB.awk \ + $DB \ + /proc/net/arp \ + /tmp/iptables_$$.tmp + unlock +} + +############################################################ + +case $1 in + "dump" ) + checkDbArg + lock + tr ',' '\t' < "$DB" + unlock + ;; + + "update" ) + checkDbArg + wan=$(detectWAN) + interfaces="$interfaces $wan" + update + rm -f /tmp/*_$$.tmp + exit + ;; + + "publish" ) + checkDbArg + [ -z "$3" ] && echo "ERROR: Missing argument 3 (output html file)" && exit 1 + + # sort DB + lock + + # busybox sort truncates numbers to 32 bits + grep -v '^#' $DB | awk -F, '{OFS=","; a=sprintf("%f",$4/1e6); $4=""; print a,$0}' | tr -s ',' | sort -rn | awk -F, '{OFS=",";$1=sprintf("%f",$1*1e6);print}' > /tmp/sorted_$$.tmp + + # create HTML page + rm -f $3.tmp + cp $dataDir/usage.htm1 $3.tmp + + #!@todo fix publishing + while IFS=, read PEAKUSAGE_IN MAC IP IFACE PEAKUSAGE_OUT TOTAL FIRSTSEEN LASTSEEN + do + echo " +new Array(\"$(lookup $MAC $IP $4)\",\"$MAC\",\"$IP\", +$PEAKUSAGE_IN,$PEAKUSAGE_OUT,$TOTAL,\"$FIRSTSEEN\",\"$LASTSEEN\")," >> $3.tmp + done < /tmp/sorted_$$.tmp + echo "0);" >> $3.tmp + + sed "s/(date)/`date`/" < $dataDir/usage.htm2 >> $3.tmp + mv $3.tmp $3 + + unlock + + #Free some memory + rm -f /tmp/*_$$.tmp + ;; + + "setup" ) + checkDbArg + [ -w "$DB" ] && echo "Warning: using existing $DB" + createDbIfMissing + + for chain in $chains; do + newChain $chain + done + + #lan=$(detectLAN) + wan=$(detectWAN) + checkWAN + interfaces="$interfaces $wan" + + # track local data + for chain in INPUT OUTPUT; do + for interface in $interfaces; do + [ -n "$interface" ] && [ -e "/sys/class/net/$interface" ] && newRuleIF $chain $interface + done + done + + # this will add rules for hosts in arp table + update + + rm -f /tmp/*_$$.tmp + ;; + + "remove" ) + iptables-save | grep -v RRDIPT | iptables-restore + rm -rf "$lockDir" + ;; + + *) + echo \ +"Usage: $0 {setup|update|publish|remove} [options...] +Options: + $0 setup database_file + $0 update database_file + $0 publish database_file path_of_html_report [user_file] +Examples: + $0 setup /tmp/usage.db + $0 update /tmp/usage.db + $0 publish /tmp/usage.db /www/user/usage.htm /jffs/users.txt + $0 remove +Note: [user_file] is an optional file to match users with MAC addresses. + Its format is \"00:MA:CA:DD:RE:SS,username\", with one entry per line." + ;; +esac diff --git a/package/lean/luci-app-wrtbwmon/root/usr/share/wrtbwmon/usage.htm1 b/package/lean/luci-app-wrtbwmon/root/usr/share/wrtbwmon/usage.htm1 new file mode 100644 index 000000000..1f0c342a7 --- /dev/null +++ b/package/lean/luci-app-wrtbwmon/root/usr/share/wrtbwmon/usage.htm1 @@ -0,0 +1,23 @@ +Traffic + +

Total Usage:

+ + + + + + + + + +
UserDownloadUploadTotalFirst seenLast seen
+
This page was generated on (date) +