luci-app-wrtbwmon: sync with upstream source

Signed-off-by: CN_SZTL <cnsztl@project-openwrt.eu.org>
This commit is contained in:
CN_SZTL 2021-02-18 20:09:40 +08:00
parent c7a2362ef7
commit 7d207fd4fe
No known key found for this signature in database
GPG Key ID: 6850B6345C862176
34 changed files with 2268 additions and 1313 deletions

View File

@ -45,7 +45,9 @@ luci-app-oled source: [NateLol/luci-app-oled](https://github.com/NateLol/luci-ap
luci-app-beardropper source: [NateLol/natelol](https://github.com/NateLol/natelol).<br/>
luci-app-vssr source: [jerrykuku/luci-app-vssr](https://github.com/jerrykuku/luci-app-vssr).<br/>
luci-theme-edge source: [garypang13/luci-theme-edge](https://github.com/garypang13/luci-theme-edge).<br/>
luci-proto-minieap source: [ysc3839/luci-proto-minieap](https://github.com/ysc3839/luci-proto-minieap).
luci-proto-minieap source: [ysc3839/luci-proto-minieap](https://github.com/ysc3839/luci-proto-minieap).<br/>
wrtbwmon source: [brvphoenix/wrtbwmon](https://github.com/brvphoenix/wrtbwmon).<br/>
luci-app-wrtbwmon source: [brvphoenix/luci-app-wrtbwmon](https://github.com/brvphoenix/luci-app-wrtbwmon).
## License
### Depend on their own License.

View File

@ -0,0 +1,22 @@
#
# Copyright (C) 2020 xxx <xxx@xxx.com>
#
# This is free software, licensed under the Apache License, Version 2.0 .
#
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-wrtbwmon
PKG_VERSION:=2.0.8
PKG_RELEASE:=1
PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=
LUCI_TITLE:=A Luci module that uses wrtbwmon to track bandwidth usage
LUCI_DEPENDS:=+@BUSYBOX_CONFIG_IP +iptables
LUCI_PKGARCH:=all
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature

View File

@ -0,0 +1,5 @@
config wrtbwmon 'general'
option enabled '1'
option path '/tmp/usage.db'

View File

@ -0,0 +1,9 @@
#!/bin/sh
[ "$ACTION" = ifup -o "$ACTION" = ifupdate ] || exit 0
[ "$ACTION" = ifupdate -a -z "$IFUPDATE_ADDRESSES" -a -z "$IFUPDATE_DATA" ] && exit 0
/etc/init.d/wrtbwmon restart
logger -t wrtbwmon "Restart for $ACTION of $INTERFACE ($DEVICE)"

View File

@ -0,0 +1,44 @@
#!/bin/sh /etc/rc.common
# Copyright (C) 2019 OpenWrt.org
START=99
USE_PROCD=1
NAME=wrtbwmon
PID_FILE=/var/run/wrtbwmon.pid
args=/usr/sbin/wrtbwmon
create_instance() {
procd_open_instance
procd_set_param command $args
procd_set_param respawn
procd_set_param user root
# procd_set_param pidfile $PID_FILE
procd_close_instance
}
service_triggers()
{
procd_add_reload_trigger "$NAME"
}
start_service() {
local db enabled
config_load $NAME
config_get db general path
[ -z "$db" ] && db="/tmp/usage.db"
append args " -46"
append args "-f $db"
append args "-p /tmp/usage.htm"
append args "-u /etc/wrtbwmon.user"
append args "-d"
config_get enabled general enabled
[ "$enabled"0 -eq 0 ] || create_instance
}
stop_service() {
procd_kill wrtbwmon
kill -CONT $(cat $PID_FILE)
}

View File

@ -0,0 +1,95 @@
'use strict';
'require form';
'require fs';
'require uci';
function renameFile(str, tag) {
var dir = dirName(str);
var n = str.lastIndexOf('/'), fn = n > -1 ? str.slice(n + 1) : str;
var n = fn.lastIndexOf('.'), bn = n > -1 ? fn.slice(0, n) : fn;
var n = fn.lastIndexOf('.'), en = n > -1 ? fn.slice(n + 1) : '';
return dir + bn + '.' + tag + (en ? '.' + en : '');
}
function dirName(str) {
var n = str.lastIndexOf('/');
return n > -1 ? str.slice(0, n + 1) : '';
}
return L.view.extend({
lastPath: null,
load: function() {
return uci.load('wrtbwmon').then(L.bind(function() {
this.lastPath = uci.get_first('wrtbwmon', 'wrtbwmon', 'path') || null;
}, this));
},
render: function() {
var m, s, o;
m = new form.Map('wrtbwmon', _('Usage - Configuration'));
s = m.section(form.NamedSection, 'general', 'wrtbwmon', _('General settings'));
s.addremove = false;
o = s.option(form.Flag, 'enabled', _('Keep running in the background'));
o.rmempty = true;
o = s.option(form.Value, 'path', _('Database path'), _('This box is used to select the Database path, which is /tmp/usage.db by default.'));
o.value('/tmp/usage.db');
o.value('/etc/usage.db');
o.default = '/tmp/usage.db';
o.rmempty = false;
return m.render();
},
changePath: function() {
return uci.changes().then(L.bind(function(res) {
if (res.wrtbwmon && this.lastPath) {
for (var i = 0; i < res.wrtbwmon.length; i++) {
if (res.wrtbwmon[i][2] == "path") {
var newPath = res.wrtbwmon[i][3];
return fs.stat(dirName(newPath)).then(L.bind(function(res) {
if (res.type == 'directory') {
Promise.all([
fs.exec('/bin/cp', ['-fp', this.lastPath, newPath]),
fs.exec('/bin/cp', ['-fp', renameFile(this.lastPath, '6'), renameFile(newPath, '6')]),
fs.exec('/bin/cp', ['-fp', renameFile(this.lastPath, '46'), renameFile(newPath, '46')])
]);
return true;
}
else {
var err = new Error('Can\'t move files to non-directory path.');
err.name = 'NotDirectoryError';
throw err;
}
}, this)).catch(function(err) {
throw err;
})
}
}
}
return false;
}, this));
},
handleSaveApply: function(ev, mode) {
return this.handleSave(ev).then(L.bind(this.changePath, this)).then(L.bind(function(data) {
L.resolveDefault(L.ui.changes.apply(mode == '0')).then(L.bind(function() {
if (data) {
Promise.all([
fs.exec('/bin/rm', ['-f', this.lastPath]),
fs.exec('/bin/rm', ['-f', renameFile(this.lastPath, '6')]),
fs.exec('/bin/rm', ['-f', renameFile(this.lastPath, '46')])
]);
}
}, this));
}, this)).catch(function(err) {
if (confirm(err + '\n\n' + _('This will revert the changes. Are you sure?'))) {
L.bind(L.ui.changes.revert, L.ui.changes)();
}
});
}
});

View File

@ -0,0 +1,43 @@
'use strict';
'require fs';
'require ui';
return L.view.extend({
load: function() {
return fs.trimmed('/etc/wrtbwmon.user').catch(function(err) {
ui.addNotification(null, E('p', {}, _('Unable to load the customized hostname file: ' + err.message)));
return '';
});
},
render: function(data) {
return E('div', {'class': 'cbi-map'}, [
E('h2', {'name': 'content'}, [ _('Usage - Custom User File') ]),
E('div', {'class': 'cbi-map-descr'}, [
_('Each line must have the following format:'),
E('em', {}, E('font', {'color': 'red'}, '00:aa:bb:cc:ee:ff,hostname'))
]),
E('div', {'class': 'cbi-section'}, [
E('textarea', {
'id': 'custom_hosts',
'style': 'width: 100%;padding: .5em;',
'rows': 20
}, data)
])
]);
},
handleSave: function(ev) {
var map = document.querySelector('#custom_hosts');
return fs.write('/etc/wrtbwmon.user', map.value.trim().replace(/\r\n/g, '\n') + '\n');
},
addFooter: function() {
return E('div', { 'class': 'cbi-page-actions' }, [
E('button', {
'class': 'cbi-button cbi-button-save',
'click': L.ui.createHandlerFn(this, 'handleSave')
}, [ _('Save') ])
]);
}
});

View File

@ -0,0 +1,651 @@
'use strict';
'require fs';
'require rpc';
'require uci';
'require ui';
'require validation';
var cachedData = [];
var luciConfig = '/etc/luci-wrtbwmon.conf';
var hostNameFile = '/etc/wrtbwmon.user';
var callLuciDHCPLeases = rpc.declare({
object: 'luci-rpc',
method: 'getDHCPLeases',
expect: { '': {} }
});
var callLuciDSLStatus = rpc.declare({
object: 'luci-rpc',
method: 'getDSLStatus',
expect: { '': {} }
});
function $(tid) {
return document.getElementById(tid);
}
function clickToResetDatabase(settings) {
if (confirm(_('This will delete the database file. Are you sure?'))) {
getPath().then(function(res) {
var db = settings.protocol == 'ipv4' ? res : renameFile(res, '6');
fs.exec('/bin/rm', [db]).then(function() {
updateData($('traffic'), $('updated'), $('updating'), settings, true);
});
})
}
}
function clickToSaveConfig(keylist, cstrs) {
var data = {};
for(var i = 0; i < keylist.length; i++) {
data[keylist[i]] = cstrs[keylist[i]].getValue();
}
ui.showModal(_('Configuration'), [
E('p', { 'class': 'spinning' }, _('Saving configuration data...'))
]);
return fs.write(luciConfig, JSON.stringify(data, undefined, '\t') + '\n')
.catch(function(err) {
ui.addNotification(null, E('p', {}, [ _('Unable to save %s: %s').format(luciConfig, err) ]));
})
.then(ui.hideModal)
.then(function() { document.location.reload(); });
}
function clickToSelectInterval(settings, ev) {
if (ev.target.value > 0) {
settings.interval = parseInt(ev.target.value);
if (!L.Request.poll.active()) L.Request.poll.start();
}
else {
L.Request.poll.stop();
setUpdateMessage($('updating'), -1);
}
}
function clickToSelectProtocol(settings, ev) {
settings.protocol = ev.target.value;
updateData($('traffic'), $('updated'), $('updating'), settings, true);
}
function clickToShowMore(settings, ev) {
var table = $('traffic');
var t = table.querySelector('.tr.table-totals');
settings.showMore = ev.target.checked
if (t && t.firstElementChild)
t.firstElementChild.textContent = _('TOTAL') + (settings.showMore ? '' : ': ' + (table.childElementCount - 2));
table.querySelectorAll('.showMore').forEach(function(e) {
e.classList.toggle('hide');
});
if (!settings.showMore && table.querySelector('.th.sorted').classList.contains('hide')) {
table.querySelector('[id="thTotal"]').click();
}
}
function createOption(args, val) {
var cstr = args[0], title = args[1], desc = args.slice(-1), widget, frame;
val = val != null ? val : args[2];
widget = args.length == 5 ? new cstr(val, args[3]) : new cstr(val, args[3], args[4]);
frame = E('div', {'class': 'cbi-value'}, [
E('label', {'class': 'cbi-value-title'}, title),
E('div', {'class': 'cbi-value-field'}, E('div', {}, widget.render()))
]);
if (desc && desc != '')
L.dom.append(frame.lastChild, E('div', { 'class': 'cbi-value-description' }, desc));
return [widget, frame];
}
function displayTable(tb, settings) {
var elm, elmID, col, sortedBy, flag, IPVer;
var thID = ['thClient', 'thMAC', 'thDownload', 'thUpload', 'thTotalDown', 'thTotalUp', 'thTotal', 'thFirstSeen', 'thLastSeen', ''];
elm = tb.querySelector('.th.sorted');
elmID = elm ? elm.id : 'thTotal';
sortedBy = elm && elm.classList.contains('ascent') ? 'asc' : 'desc';
col = thID.indexOf(elmID);
IPVer = col == 0 ? settings.protocol : null;
flag = sortedBy == 'desc' ? 1 : -1;
cachedData[0].sort(sortTable.bind(this, col, IPVer, flag));
//console.time('show');
updateTable(tb, cachedData, '<em>%s</em>'.format(_('Collecting data...')), settings);
//console.timeEnd('show');
progressbar('downstream', cachedData[1][0], settings.downstream, settings.useBits, settings.useMultiple);
progressbar('upstream', cachedData[1][1], settings.upstream, settings.useBits, settings.useMultiple);
}
function formatBandWidth(bdw, useBits) {
return bdw * Math.pow(1000, 2) / (useBits ? 1 : 8);
}
function formatSize(size, useBits, useMultiple) {
var res = String.format('%%%s.2m%s'.format(useMultiple, (useBits ? 'bit' : 'B')), useBits ? size * 8 : size);
return useMultiple == '1024' ? res.replace(/([KMGTPEZ])/, '$&i') : res;
}
function formatSpeed(speed, useBits, useMultiple) {
return formatSize(speed, useBits, useMultiple) + '/s';
}
function formatDate(d) {
var Y = d.getFullYear(), M = d.getMonth() + 1, D = d.getDate();
var hh = d.getHours(), mm = d.getMinutes(), ss = d.getSeconds();
return '%04d/%02d/%02d %02d:%02d:%02d'.format(Y, M, D, hh, mm, ss);
}
function getDSLBandwidth() {
return callLuciDSLStatus().then(function(res) {
return {
upstream : res.max_data_rate_up || null,
downstream : res.max_data_rate_down || null
};
});
}
function getPath() {
return uci.load('wrtbwmon').then(function() {
var res = uci.get_first('wrtbwmon', 'wrtbwmon', 'path') || '/tmp/usage.db';
uci.unload('wrtbwmon');
return res;
});
}
function handleConfig(ev) {
ui.showModal(_('Configuration'), [
E('p', { 'class': 'spinning' }, _('Loading configuration data...'))
]);
parseDefaultSettings(luciConfig)
.then(function(settings) {
var arglist, keylist = Object.keys(settings), res, cstrs = {}, node = [], body;
arglist = [
[ui.Select, _('Default Protocol'), 'ipv4', {'ipv4': _('ipv4'), 'ipv6': _('ipv6')}, {}, ''],
[ui.Select, _('Default Refresh Interval'), '5', {'-1': _('Disabled'), '2': _('2 seconds'), '5': _('5 seconds'), '10': _('10 seconds'), '30': _('30 seconds')}, {sort: ['-1', '2', '5', '10', '30']}, ''],
[ui.Checkbox, _('Default More Columns'), false, {value_enabled: true, value_disabled: false}, ''],
[ui.Checkbox, _('Show Zeros'), true, {value_enabled: true, value_disabled: false}, ''],
[ui.Checkbox, _('Transfer Speed in Bits'), false, {value_enabled: true, value_disabled: false}, ''],
[ui.Select, _('Multiple of Unit'), '1000', {'1000': _('SI - 1000'), '1024': _('IEC - 1024')}, {}, ''],
[ui.Checkbox, _('Use DSL Bandwidth'), false, {value_enabled: true, value_disabled: false}, ''],
[ui.Textfield, _('Upstream Bandwidth'), '100', {datatype: 'ufloat'}, 'Mbps'],
[ui.Textfield, _('Downstream Bandwidth'), '100', {datatype: 'ufloat'}, 'Mbps'],
[ui.DynamicList, _('Hide MAC Addresses'), [], '', {datatype: 'macaddr'}, '']
]; // [constructor, lable, default_value(, all_values), options, description]
for (var i = 0; i < keylist.length; i++) {
res = createOption(arglist[i], settings[keylist[i]]);
cstrs[keylist[i]] = res[0];
node.push(res[1]);
}
body = [
E('p', {}, _('Configure the default values for luci-app-wrtbwmon.')),
E('div', {}, node),
E('div', { 'class': 'right' }, [
E('div', {
'class': 'btn cbi-button-neutral',
'click': ui.hideModal
}, _('Cancel')),
' ',
E('div', {
'class': 'btn cbi-button-positive',
'click': clickToSaveConfig.bind(this, keylist, cstrs),
'disabled': (L.hasViewPermission ? !L.hasViewPermission() : null) || null
}, _('Save'))
])
];
ui.showModal(_('Configuration'), body);
})
}
function loadCss(path) {
var head = document.head || document.getElementsByTagName('head')[0];
var link = E('link', {
'rel': 'stylesheet',
'href': path,
'type': 'text/css'
});
head.appendChild(link);
}
function parseDatabase(values, hosts, showZero, hideMACs) {
var valArr = [], totals = [0, 0, 0, 0, 0], valToRows, row;
valToRows = values.replace(/(^\s*)|(\s*$)/g, '').split(/\r?\n|\r/g);
valToRows.shift();
for (var i = 0; i < valToRows.length; i++) {
row = valToRows[i].split(',');
if ((!showZero && row[7] == 0) || hideMACs.indexOf(row[0]) >= 0) continue;
for (var j = 0; j < totals.length; j++) {
totals[j] += parseInt(row[3 + j]);
}
row = Array.prototype.concat(row.slice(0, 2).reverse(), row.slice(3), row.slice(0, 1));
if (row[1] in hosts && hosts[row[1]] != '-') {
row[9] = hosts[row[1]];
}
valArr.push(row);
}
return [valArr, totals];
}
function parseDefaultSettings(file) {
var keylist = ['protocol', 'interval', 'showMore', 'showZero', 'useBits', 'useMultiple', 'useDSL', 'upstream', 'downstream', 'hideMACs'];
var valuelist = ['ipv4', '5', false, true, false, '1000', false, '100', '100', []];
return fs.read(file).then(function(json) {
var settings;
try {
settings = JSON.parse(json);
}
catch(err) {
settings = {};
}
for (var i = 0; i < keylist.length; i++) {
if(!(keylist[i] in settings))
settings[keylist[i]] = valuelist[i];
}
if (settings.useDSL) {
return getDSLBandwidth().then(function(dsl) {
settings.upstream = dsl.upstream || settings.upstream;
settings.downstream = dsl.downstream || settings.downstream;
return settings;
});
}
else {
return settings;
}
});
}
function progressbar(query, v, m, useBits, useMultiple) {
var pg = $(query),
vn = v || 0,
mn = formatBandWidth(m, useBits) || 100,
fv = formatSpeed(v, useBits, useMultiple),
pc = '%.2f'.format((100 / mn) * vn),
wt = Math.floor(pc > 100 ? 100 : pc),
bgc = (pc >= 95 ? 'red' : (pc >= 80 ? 'darkorange' : (pc >= 60 ? 'yellow' : 'lime'))),
tc = (pc >= 80 ? 'white' : '#404040');
if (pg) {
pg.firstElementChild.style.width = wt + '%';
pg.firstElementChild.style.background = bgc;
pg.style.color = tc;
pg.setAttribute('title', '%s (%f%%)'.format(fv, pc));
}
}
function registerTableEventHandlers(settings, table) {
var indicators = $('xhr_poll_status') || $('indicators').getElementsByTagName('span')[0];
indicators.addEventListener('click', function() {
$('selectInterval').value = L.Request.poll.active() ? settings.interval : -1;
});
table.querySelectorAll('.th').forEach(function(e) {
if (e) {
e.addEventListener('click', function (ev) {
setSortedColumn(ev.target);
displayTable(table, settings);
});
}
});
}
function renameFile(str, tag) {
var n = str.lastIndexOf('/'), fn = n > -1 ? str.slice(n + 1) : str, dir = n > -1 ? str.slice(0, n + 1) : '';
var n = fn.lastIndexOf('.'), bn = n > -1 ? fn.slice(0, n) : fn;
var n = fn.lastIndexOf('.'), en = n > -1 ? fn.slice(n + 1) : '';
return dir + bn + '.' + tag + (en ? '.' + en : '');
}
function resolveCustomizedHostName() {
return fs.stat(hostNameFile).then(function() {
return fs.read(hostNameFile).then(function(rawStr) {
var hostNames = [], arr = rawStr.split(/\r?\n|\r/g), row;
for (var i = 0; i < arr.length; i++) {
row = arr[i].split(',');
if (row.length == 2 && row[0])
hostNames.push({ macaddr: row[0], hostname: row[1] });
}
return hostNames;
})
}).catch(function() { return []; });
}
function resolveHostNameByMACAddr() {
return Promise.all([
resolveCustomizedHostName(),
callLuciDHCPLeases()
]).then(function(res) {
var leaseNames, macaddr, hostNames = {};
leaseNames = [
res[0],
Array.isArray(res[1].dhcp_leases) ? res[1].dhcp_leases : [],
Array.isArray(res[1].dhcp6_leases) ? res[1].dhcp6_leases : []
];
for (var i = 0; i < leaseNames.length; i++) {
for (var j = 0; j < leaseNames[i].length; j++) {
if (leaseNames[i][j].macaddr) {
macaddr = leaseNames[i][j].macaddr.toLowerCase();
if (!(macaddr in hostNames) || hostNames[macaddr] == '-') {
hostNames[macaddr] = leaseNames[i][j].hostname || '-';
}
}
}
}
return hostNames;
});
}
function setSortedColumn(sorting) {
var sorted = document.querySelector('.th.sorted') || $('thTotal');
if (sorting.isSameNode(sorted)) {
sorting.classList.toggle('ascent');
}
else {
sorting.classList.add('sorted');
sorted.classList.remove('sorted', 'ascent');
}
}
function setUpdateMessage(e, sec) {
e.innerHTML = sec < 0 ? '' : ' ' + _('Updating again in %s second(s).').format('<b>' + sec + '</b>');
}
function sortTable(col, IPVer, flag, x, y) {
var byCol = x[col] == y[col] ? 1 : col;
var a = x[byCol], b = y[byCol];
if (!IPVer || byCol != 0) {
if (!(a.match(/\D/g) || b.match(/\D/g)))
a = parseInt(a), b = parseInt(b);
}
else {
IPVer == 'ipv4'
? (a = validation.parseIPv4(a) || [0, 0, 0, 0], b = validation.parseIPv4(b) || [0, 0, 0, 0])
: (a = validation.parseIPv6(a) || [0, 0, 0, 0, 0, 0, 0, 0], b = validation.parseIPv6(b) || [0, 0, 0, 0, 0, 0, 0, 0]);
}
if(Array.isArray(a) && Array.isArray(b)) {
for(var i = 0; i < a.length; i++) {
if (a[i] != b[i]) {
return (b[i] - a[i]) * flag;
}
}
return 0;
}
return a == b ? 0 : (a < b ? 1 : -1) * flag;
}
function updateData(table, updated, updating, settings, once) {
if (!(L.Poll.tick % settings.interval) || once) {
getPath().then(function(res) {
var params = settings.protocol == 'ipv4' ? '-4' : '-6';
fs.exec('/usr/sbin/wrtbwmon', [params, '-f', res]);
return params == '-4' ? res : renameFile(res, '6');
}).then(function(data) {
Promise.all([
fs.exec('/bin/cat', [ data ]),
resolveHostNameByMACAddr()
]).then(function(res) {
//console.time('start');
cachedData = parseDatabase(res[0].stdout || '', res[1], settings.showZero, settings.hideMACs);
displayTable(table, settings);
updated.innerHTML = _('Last updated at %s.').format(formatDate(new Date(document.lastModified)));
//console.timeEnd('start');
});
});
}
updatePerSec(updating, settings.interval);
}
function updatePerSec(e, interval) {
var tick = L.Poll.tick;
var sec = tick % interval ? interval - tick % interval : 0;
setUpdateMessage(e, sec);
if(sec == 0) {
setTimeout(setUpdateMessage.bind(this, e, interval), 100);
}
}
function updateTable(tb, values, placeholder, settings) {
var fragment = document.createDocumentFragment(), nodeLen = tb.childElementCount - 2;
var formData = values[0], tbTitle = tb.firstElementChild, newNode, childTD;
// Update the table data.
for (var i = 0; i < formData.length; i++) {
if (i < nodeLen) {
newNode = tbTitle.nextElementSibling;
}
else {
if (nodeLen > 0) {
newNode = fragment.firstChild.cloneNode(true);
}
else {
newNode = document.createElement('div');
childTD = document.createElement('div');
for (var j = 0; j < tbTitle.children.length; j++) {
childTD.className = 'td' + ('178'.indexOf(j) != -1 ? ' showMore' + (settings.showMore ? '' : ' hide') : '');
childTD.setAttribute('data-title', tbTitle.children[j].textContent);
newNode.appendChild(childTD.cloneNode(true));
}
}
newNode.className = 'tr cbi-rowstyle-%d'.format(i % 2 ? 2 : 1);
}
childTD = newNode.firstElementChild;
childTD.title = formData[i].slice(-1);
for (var j = 0; j < tbTitle.childElementCount; j++, childTD = childTD.nextElementSibling) {
switch (j) {
case 2:
case 3:
childTD.textContent = formatSpeed(formData[i][j], settings.useBits, settings.useMultiple);
break;
case 4:
case 5:
case 6:
childTD.textContent = formatSize(formData[i][j], settings.useBits, settings.useMultiple);
break;
case 7:
case 8:
childTD.textContent = formatDate(new Date(formData[i][j] * 1000));
break;
default:
childTD.textContent = formData[i][j];
}
}
fragment.appendChild(newNode);
}
// Remove the table data which has been deleted from the database.
while (tb.childElementCount > 1) {
tb.removeChild(tbTitle.nextElementSibling);
}
//Append the totals or placeholder row.
if(formData.length == 0) {
newNode = document.createElement('div');
newNode.className = 'tr placeholder';
childTD = document.createElement('div');
childTD.className = 'td';
childTD.innerHTML = placeholder;
newNode.appendChild(childTD);
}
else{
newNode = fragment.firstChild.cloneNode(true);
newNode.className = 'tr table-totals';
newNode.children[0].textContent = _('TOTAL') + (settings.showMore ? '' : ': ' + formData.length);
newNode.children[1].textContent = formData.length + ' ' + _('Clients');
for (var j = 0; j < tbTitle.childElementCount; j++) {
switch(j) {
case 0:
case 1:
newNode.children[j].removeAttribute('title');
newNode.children[j].style.fontWeight = 'bold';
break;
case 2:
case 3:
newNode.children[j].textContent = formatSpeed(values[1][j - 2], settings.useBits, settings.useMultiple);
break;
case 4:
case 5:
case 6:
newNode.children[j].textContent = formatSize(values[1][j - 2], settings.useBits, settings.useMultiple);
break;
default:
newNode.children[j].textContent = '';
newNode.children[j].removeAttribute('data-title');
}
}
}
fragment.appendChild(newNode);
tb.appendChild(fragment);
}
return L.view.extend({
load: function() {
return Promise.all([
parseDefaultSettings(luciConfig),
loadCss(L.resource('view/wrtbwmon/wrtbwmon.css'))
]);
},
render: function(data) {
var settings = data[0];
var node = E('div', { 'class': 'cbi-map' }, [
E('h2', {}, _('Usage - Details')),
E('div', { 'class': 'cbi-section' }, [
E('div', { 'id': 'control_panel' }, [
E('div', {}, [
E('label', {}, _('Protocol:')),
E('select', {
'id': 'selectProtocol',
'change': clickToSelectProtocol.bind(this, settings)
}, [
E('option', { 'value': 'ipv4' }, 'ipv4'),
E('option', { 'value': 'ipv6' }, 'ipv6')
])
]),
E('div', {}, [
E('label', { 'for': 'showMore' }, _('Show More Columns:')),
E('input', {
'id': 'showMore',
'type': 'checkbox',
'click': clickToShowMore.bind(this, settings)
}),
]),
E('div', {}, [
E('button', {
'class': 'btn cbi-button cbi-button-reset important',
'id': 'resetDatabase',
'click': clickToResetDatabase.bind(this, settings)
}, _('Reset Database')),
' ',
E('button', {
'class': 'btn cbi-button cbi-button-neutral',
'click': handleConfig
}, _('Configure Options'))
])
]),
E('div', {}, [
E('div', {}, [
E('div', { 'id': 'updated' }),
E('div', { 'id': 'updating' })
]),
E('div', {}, [
E('label', { 'for': 'selectInterval' }, _('Auto update every:')),
E('select', {
'id': 'selectInterval',
'change': clickToSelectInterval.bind(this, settings)
}, [
E('option', { 'value': '-1' }, _('Disabled')),
E('option', { 'value': '2' }, _('2 seconds')),
E('option', { 'value': '5' }, _('5 seconds')),
E('option', { 'value': '10' }, _('10 seconds')),
E('option', { 'value': '30' }, _('30 seconds'))
])
])
]),
E('div', { 'id': 'progressbar_panel', 'class': 'table' }, [
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td' }, E('div', {}, _('Downstream:'))),
E('div', { 'class': 'td' }, E('div', {
'id': 'downstream',
'class': 'cbi-progressbar',
'title': '-'
}, E('div')
))
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td' }, E('div', {}, _('Upstream:'))),
E('div', { 'class': 'td' }, E('div', {
'id': 'upstream',
'class': 'cbi-progressbar',
'title': '-'
}, E('div')
))
]),
]),
E('div', { 'class': 'table', 'id': 'traffic' }, [
E('div', { 'class': 'tr table-titles' }, [
E('div', { 'class': 'th', 'id': 'thClient' }, _('Clients')),
E('div', { 'class': 'th showMore hide', 'id': 'thMAC' }, _('MAC')),
E('div', { 'class': 'th', 'id': 'thDownload' }, _('Download')),
E('div', { 'class': 'th', 'id': 'thUpload' }, _('Upload')),
E('div', { 'class': 'th', 'id': 'thTotalDown' }, _('Total Down')),
E('div', { 'class': 'th', 'id': 'thTotalUp' }, _('Total Up')),
E('div', { 'class': 'th sorted', 'id': 'thTotal' }, _('Total')),
E('div', { 'class': 'th showMore hide', 'id': 'thFirstSeen' }, _('First Seen')),
E('div', { 'class': 'th showMore hide', 'id': 'thLastSeen' }, _('Last Seen'))
]),
E('div', {'class': 'tr placeholder'}, [
E('div', { 'class': 'td' }, E('em', {}, _('Collecting data...')))
])
])
])
]);
return Promise.all([
node.querySelector('[id="traffic"]'),
node.querySelector('[id="updated"]'),
node.querySelector('[id="updating"]'),
node.querySelector('[id="selectInterval"]').value = settings.interval,
node.querySelector('[id="selectProtocol"]').value = settings.protocol,
node.querySelector('[id="showMore"]').checked = settings.showMore,
node.querySelectorAll('.showMore').forEach(function(e) { settings.showMore ? e.classList.remove('hide') : e.classList.add('hide'); })
])
.then(function(data) {
L.Poll.add(updateData.bind(this, data[0], data[1], data[2], settings, false), 1);
return data[0];
})
.then(registerTableEventHandlers.bind(this, settings))
.then(function() { return node; });
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});

View File

@ -0,0 +1,73 @@
.th.sorted::after {
content: '\25bc';
}
.th.sorted.ascent::after {
content: '\25b2';
}
.hide {
display: none !important;
}
#control_panel {
display: flex;
margin-bottom: 1rem;
padding: .5rem;
line-height: 2rem;
background-color: #fff;
}
#control_panel > :nth-child(1), #control_panel > :nth-child(2) {
display: inline-block;
flex: 1 1 25%;
}
#control_panel > :nth-child(3) {
flex: 1 1 50%;
text-align: right;
}
#control_panel > * > * {
vertical-align: middle;
}
div > label {
margin-right: .5rem;
}
div > label + select {
min-width: unset;
width: auto;
}
#control_panel + div {
display: flex;
margin-bottom: .5rem;
}
#control_panel + div > div:nth-of-type(1) {
flex: 1 1 65%;
}
#control_panel + div > div:nth-of-type(2) {
flex: 1 1 35%;
text-align: right;
}
#updated, #updating {
display: inline;
}
#thClient {
width: 17%;
}
#thMAC {
width: 10%;
}
#thDownload, #thUpload {
width: 8%;
}
#thTotalDown, #thTotalUp, #thTotal {
width: 9%;
}
#thFirstSeen, #thLastSeen {
width: 15%;
}
#progressbar_panel > .tr > .td:nth-child(1) {
width: 10%;
}
.tr.table-totals {
background: #e0e0e0 !important;
}
#traffic .tr:not(.table-totals):not(.placeholder) > .td:not(.th):first-child::before {
content: attr(title)'\a';
white-space: pre-line;
}

View File

@ -0,0 +1,260 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:175
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:585
msgid "10 seconds"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:175
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:583
msgid "2 seconds"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:175
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:586
msgid "30 seconds"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:175
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:584
msgid "5 seconds"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:577
msgid "Auto update every:"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:199
msgid "Cancel"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:497
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:612
msgid "Clients"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:122
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:623
msgid "Collecting data..."
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:46
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:165
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:208
#: luasrc/controller/wrtbwmon.lua:16
msgid "Configuration"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:568
msgid "Configure Options"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:193
msgid "Configure the default values for luci-app-wrtbwmon."
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/config.js:39
msgid "Database path"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:176
msgid "Default More Columns"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:174
msgid "Default Protocol"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:175
msgid "Default Refresh Interval"
msgstr ""
#: luasrc/controller/wrtbwmon.lua:13
msgid "Details"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:175
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:582
msgid "Disabled"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:614
msgid "Download"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:182
msgid "Downstream Bandwidth"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:592
msgid "Downstream:"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/custom.js:17
msgid "Each line must have the following format:"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:619
msgid "First Seen"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/config.js:33
msgid "General settings"
msgstr ""
#: root/usr/share/rpcd/acl.d/luci-app-wrtbwmon.json:3
msgid "Grant access to LuCI app wrtbwmon"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:183
msgid "Hide MAC Addresses"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:179
msgid "IEC - 1024"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/config.js:36
msgid "Keep running in the background"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:620
msgid "Last Seen"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:411
msgid "Last updated at %s."
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:166
msgid "Loading configuration data..."
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:613
msgid "MAC"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:179
msgid "Multiple of Unit"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:541
msgid "Protocol:"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:563
msgid "Reset Database"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:179
msgid "SI - 1000"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/custom.js:40
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:205
msgid "Save"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:47
msgid "Saving configuration data..."
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:551
msgid "Show More Columns:"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:177
msgid "Show Zeros"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:80
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:496
msgid "TOTAL"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/config.js:39
msgid ""
"This box is used to select the Database path, which is /tmp/usage.db by "
"default."
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:29
msgid "This will delete the database file. Are you sure?"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/config.js:90
msgid "This will revert the changes. Are you sure?"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:618
msgid "Total"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:616
msgid "Total Down"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:617
msgid "Total Up"
msgstr ""
#: luasrc/controller/wrtbwmon.lua:10
msgid "Traffic Status"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:178
msgid "Transfer Speed in Bits"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/custom.js:8
msgid "Unable to load the customized hostname file:"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:52
msgid "Unable to save %s: %s"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:368
msgid "Updating again in %s second(s)."
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:615
msgid "Upload"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:181
msgid "Upstream Bandwidth"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:601
msgid "Upstream:"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/config.js:31
msgid "Usage - Configuration"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/custom.js:15
msgid "Usage - Custom User File"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:537
msgid "Usage - Details"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:180
msgid "Use DSL Bandwidth"
msgstr ""
#: luasrc/controller/wrtbwmon.lua:19
msgid "User file"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:174
msgid "ipv4"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:174
msgid "ipv6"
msgstr ""

View File

@ -0,0 +1,260 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8\n"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:175
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:585
msgid "10 seconds"
msgstr "10秒"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:175
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:583
msgid "2 seconds"
msgstr "2秒"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:175
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:586
msgid "30 seconds"
msgstr "30秒"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:175
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:584
msgid "5 seconds"
msgstr "5秒"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:577
msgid "Auto update every:"
msgstr "自动刷新:"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:199
msgid "Cancel"
msgstr "取消"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:497
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:612
msgid "Clients"
msgstr "客户端"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:122
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:623
msgid "Collecting data..."
msgstr "收集数据中..."
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:46
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:165
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:208
#: luasrc/controller/wrtbwmon.lua:16
msgid "Configuration"
msgstr "配置"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:568
msgid "Configure Options"
msgstr "配置选项"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:193
msgid "Configure the default values for luci-app-wrtbwmon."
msgstr "配置luci-app-wrtbwmon的默认值。"
#: htdocs/luci-static/resources/view/wrtbwmon/config.js:39
msgid "Database path"
msgstr "数据路径"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:176
msgid "Default More Columns"
msgstr "默认显示更多列"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:174
msgid "Default Protocol"
msgstr "默认协议"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:175
msgid "Default Refresh Interval"
msgstr "默认刷新间隔"
#: luasrc/controller/wrtbwmon.lua:13
msgid "Details"
msgstr "流量信息"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:175
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:582
msgid "Disabled"
msgstr "禁用"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:614
msgid "Download"
msgstr "下载"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:182
msgid "Downstream Bandwidth"
msgstr "下行带宽"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:592
msgid "Downstream:"
msgstr "下行:"
#: htdocs/luci-static/resources/view/wrtbwmon/custom.js:17
msgid "Each line must have the following format:"
msgstr "每行需要满足以下格式:"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:619
msgid "First Seen"
msgstr "初次记录"
#: htdocs/luci-static/resources/view/wrtbwmon/config.js:33
msgid "General settings"
msgstr "通用设置"
#: root/usr/share/rpcd/acl.d/luci-app-wrtbwmon.json:3
msgid "Grant access to LuCI app wrtbwmon"
msgstr "授予访问LuCI应用程序wrtbwmon的权限"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:183
msgid "Hide MAC Addresses"
msgstr "隐藏MAC地址"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:179
msgid "IEC - 1024"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/config.js:36
msgid "Keep running in the background"
msgstr "保持后台运行"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:620
msgid "Last Seen"
msgstr "最后记录"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:411
msgid "Last updated at %s."
msgstr "最后更新于%s。"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:166
msgid "Loading configuration data..."
msgstr "载入配置数据..."
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:613
msgid "MAC"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:179
msgid "Multiple of Unit"
msgstr "单位之间倍数"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:541
msgid "Protocol:"
msgstr "协议:"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:563
msgid "Reset Database"
msgstr "重置"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:179
msgid "SI - 1000"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/custom.js:40
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:205
msgid "Save"
msgstr "保存"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:47
msgid "Saving configuration data..."
msgstr "保存配置数据..."
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:551
msgid "Show More Columns:"
msgstr "显示更多列:"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:177
msgid "Show Zeros"
msgstr "显示0流量"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:80
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:496
msgid "TOTAL"
msgstr "总共"
#: htdocs/luci-static/resources/view/wrtbwmon/config.js:39
msgid ""
"This box is used to select the Database path, which is /tmp/usage.db by "
"default."
msgstr "该选项用于选择数据存储路径,默认路径为/tmp/usage.db。"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:29
msgid "This will delete the database file. Are you sure?"
msgstr "该操作将删除数据统计文件,确定执行该操作?"
#: htdocs/luci-static/resources/view/wrtbwmon/config.js:90
msgid "This will revert the changes. Are you sure?"
msgstr "更改将会重置,确定吗?"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:618
msgid "Total"
msgstr "总计"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:616
msgid "Total Down"
msgstr "总下载"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:617
msgid "Total Up"
msgstr "总上传"
#: luasrc/controller/wrtbwmon.lua:10
msgid "Traffic Status"
msgstr "流量监控"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:178
msgid "Transfer Speed in Bits"
msgstr "以bits显示传输速度"
#: htdocs/luci-static/resources/view/wrtbwmon/custom.js:8
msgid "Unable to load the customized hostname file:"
msgstr "不能载入自定义用户名文件:"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:52
msgid "Unable to save %s: %s"
msgstr "不能保存%s: %s"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:368
msgid "Updating again in %s second(s)."
msgstr "下次更新将于%s秒之后。"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:615
msgid "Upload"
msgstr "上传"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:181
msgid "Upstream Bandwidth"
msgstr "上传带宽"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:601
msgid "Upstream:"
msgstr "上行"
#: htdocs/luci-static/resources/view/wrtbwmon/config.js:31
msgid "Usage - Configuration"
msgstr "文件设置"
#: htdocs/luci-static/resources/view/wrtbwmon/custom.js:15
msgid "Usage - Custom User File"
msgstr "用户文件配置"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:537
msgid "Usage - Details"
msgstr "流量详情"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:180
msgid "Use DSL Bandwidth"
msgstr "使用DSL带宽"
#: luasrc/controller/wrtbwmon.lua:19
msgid "User file"
msgstr "用户文件"
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:174
msgid "ipv4"
msgstr ""
#: htdocs/luci-static/resources/view/wrtbwmon/details.js:174
msgid "ipv6"
msgstr ""

View File

@ -0,0 +1,12 @@
{
"protocol": "ipv4",
"interval": "5",
"showMore": false,
"showZero": true,
"useBits": false,
"useMultiple": "1000",
"useDSL": false,
"upstream": "100",
"downstream": "100",
"hideMACs": []
}

View File

@ -0,0 +1,41 @@
{
"admin/network/usage": {
"title": "Traffic Status",
"order": 60,
"action": {
"type": "alias",
"path": "admin/network/usage/details"
},
"depends": {
"acl": [ "luci-app-wrtbwmon" ],
"uci": { "wrtbwmon": true }
}
},
"admin/network/usage/details": {
"title": "Details",
"order": 10,
"action": {
"type": "view",
"path": "wrtbwmon/details"
}
},
"admin/network/usage/config": {
"title": "Configuration",
"order": 20,
"action": {
"type": "view",
"path": "wrtbwmon/config"
}
},
"admin/network/usage/custom": {
"title": "User file",
"order": 30,
"action": {
"type": "view",
"path": "wrtbwmon/custom"
}
}
}

View File

@ -0,0 +1,25 @@
{
"luci-app-wrtbwmon": {
"description": "Grant access to LuCI app wrtbwmon",
"read": {
"file": {
"/tmp/usage.db": [ "read" ],
"/etc/wrtbwmon.user": [ "read" ],
"/usr/sbin/wrtbwmon": [ "exec" ],
"/bin/cp": [ "exec" ],
"/etc/luci-wrtbwmon.conf": [ "read" ]
},
"uci": [ "wrtbwmon", "luci-app-wrtbwmon" ]
},
"write": {
"file": {
"/bin/cat": [ "exec" ],
"/bin/rm": [ "exec" ],
"/bin/cp": [ "exec" ],
"/etc/luci-wrtbwmon.conf": [ "write" ],
"/etc/wrtbwmon.user": [ "write" ]
},
"uci": [ "wrtbwmon", "luci-app-wrtbwmon" ]
}
}
}

View File

@ -0,0 +1,213 @@
#!/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=iptKey " -t mangle -j RETURN -s " arp_ip
system(ipt_cmd " -C RRDIPT_FORWARD 2>/dev/null || " ipt_cmd " -A RRDIPT_FORWARD")
ipt_cmd=iptKey " -t mangle -j RETURN -d " arp_ip
system(ipt_cmd " -C RRDIPT_FORWARD 2>/dev/null || " ipt_cmd " -A RRDIPT_FORWARD")
}
function delRule(arp_ip, ipt_cmd) {
ipt_cmd=iptKey " -t mangle -D RRDIPT_FORWARD -j RETURN "
system(ipt_cmd "-s " arp_ip " 2>/dev/null")
system(ipt_cmd "-d " arp_ip " 2>/dev/null")
}
function total(i) {
return(bw[i "/in"] + bw[i "/out"])
}
BEGIN {
if (ipv6) {
iptNF = 8
iptKey = "ip6tables"
} else {
iptNF = 9
iptKey = "iptables"
}
}
/^#/ { # get DB filename
FS = ","
dbFile = FILENAME
next
}
# data from database; first file
ARGIND==1 { #!@todo this doesn't help if the DB file is empty.
lb=$1
if (lb !~ "^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$") next
if (!(lb in mac)) {
mac[lb] = $1
ip[lb] = $2
inter[lb] = $3
speed[lb "/in"] = 0
speed[lb "/out"]= 0
bw[lb "/in"] = $6
bw[lb "/out"] = $7
firstDate[lb] = $9
lastDate[lb] = $10
ignore[lb] = 1
} else {
if ($9 < firstDate[lb])
firstDate[lb] = $9
if ($10 > lastDate[lb]) {
ip[lb] = $2
inter[lb] = $3
lastDate[lb] = $10
}
bw[lb "/in"] += $6
bw[lb "/out"] += $7
ignore[lb] = 0
}
next
}
# not triggered on the first file
FNR==1 {
FS=" "
if(ARGIND == 2) next
}
# arp: ip hw flags hw_addr mask device
ARGIND==2 {
#!@todo regex match IPs and MACs for sanity
if (ipv6) {
statFlag= ($4 != "FAILED" && $4 != "INCOMPLETE")
macAddr = $5
hwIF = $3
} else {
statFlag= ($3 != "0x0")
macAddr = $4
hwIF = $6
}
lb=$1
if (hwIF != wanIF && statFlag && macAddr ~ "^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$") {
hosts[lb] = 1
arp_mac[lb] = macAddr
arp_ip[lb] = $1
arp_inter[lb] = hwIF
arp_bw[lb "/in"] = 0
arp_bw[lb "/out"] = 0
arp_firstDate[lb] = systime()
arp_lastDate[lb] = arp_firstDate[lb]
arp_ignore[lb] = 1
}
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.
ARGIND==3 && NF==iptNF && $1!="pkts" { # iptables input
if (ipv6) {
lfn = 5
tag = "::/0"
} else {
lfn = 6
tag = "0.0.0.0/0"
}
if ($(lfn) != "*") {
m = $(lfn)
n = m "/in"
} else if ($(++lfn) != "*") {
m = $(lfn)
n = m "/out"
} else if ($(++lfn) != tag) {
m = $(lfn)
n = m "/out"
} else { # $(++lfn) != tag
m = $(++lfn)
n = m "/in"
}
if (mode == "diff" || mode == "noUpdate") print n, $2
if (mode != "noUpdate") {
if (inInterfaces(m)) { # if label is an interface
if (!(m in arp_mac)) {
cmd = "cat /sys/class/net/" m "/address"
cmd | getline arp_mac[m]
close(cmd)
if (length(arp_mac[m]) == 0) arp_mac[m] = "00:00:00:00:00:00"
arp_ip[m] = "NA"
arp_inter[m] = m
arp_bw[m "/in"] = 0
arp_bw[m "/out"] = 0
arp_firstDate[m] = systime()
arp_lastDate[m] = arp_firstDate[m]
arp_ignore[lb] = 1
}
} else {
if (!(m in arp_mac)) hosts[m] = 0
else delete hosts[m]
}
if ($2 > 0) {
arp_bw[n] = $2
arp_lastDate[m] = systime()
arp_ignore[m] = 0
}
}
}
END {
if (mode == "noUpdate") exit
for (i in arp_ip) {
lb = arp_mac[i]
if (!arp_ignore[i] || !(lb in mac)) {
ignore[lb] = 0
if (lb in mac) {
bw[lb "/in"] += arp_bw[i "/in"]
bw[lb "/out"] += arp_bw[i "/out"]
lastDate[lb] = arp_lastDate[i]
} else {
bw[lb "/in"] = arp_bw[i "/in"]
bw[lb "/out"] = arp_bw[i "/out"]
firstDate[lb] = arp_firstDate[i]
lastDate[lb] = arp_lastDate[i]
}
mac[lb] = arp_mac[i]
ip[lb] = arp_ip[i]
inter[lb] = arp_inter[i]
if (interval != 0) {
speed[lb "/in"] = int(arp_bw[i "/in"] / interval)
speed[lb "/out"]= int(arp_bw[i "/out"] / interval)
}
}
}
close(dbFile)
for (i in mac) {
if (!ignore[i]) {
print "#mac,ip,iface,speed_in,speed_out,in,out,total,first_date,last_date" > dbFile
OFS=","
for (i in mac)
print mac[i], ip[i], inter[i], speed[i "/in"], speed[i "/out"], bw[i "/in"], bw[i "/out"], total(i), firstDate[i], lastDate[i] > dbFile
close(dbFile)
break
}
}
# for hosts without rules
for (i in hosts)
if (hosts[i]) newRule(i)
else delRule(i)
}

View File

@ -0,0 +1,460 @@
#!/bin/sh
#
# Default input parameters for wrtbwmon.
runMode=0
Monitor46=4
# Some parameters for monitor process.
for46=
updatePID=
logFile=/var/log/wrtbwmon.log
lockFile=/var/lock/wrtbwmon.lock
pidFile=/var/run/wrtbwmon.pid
tmpDir=/var/tmp/wrtbwmon
interval4=0
interval6=0
# Debug parameters for readDB.awk.
mode=
DEBUG=
# Constant parameter for wrtbwmon.
binDir=/usr/sbin
dataDir=/usr/share/wrtbwmon
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'
interfaces='eth0 tun0 br-lan' # in addition to detected WAN
# DNS server for reverse lookups provided in "DNS".
# don't perform reverse DNS lookups by default
DO_RDNS=${DNS-}
header="#mac,ip,iface,speed_in,speed_out,in,out,total,first_date,last_date"
createDbIfMissing() {
[ ! -f "$DB" ] && echo $header > "$DB"
[ ! -f "$DB6" ] && echo $header > "$DB6"
}
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
[ ! -f "$DB6" ] && echo "ERROR: $DB6 does not exist" && exit 1
[ ! -w "$DB6" ] && echo "ERROR: $DB6 is not writable" && exit 1
}
checkWAN() {
[ -z "$1" ] && echo "Warning: failed to detect WAN interface."
}
lookup() {
local MAC=$1
local IP=$2
local userDB=$3
local USERSFILE=
local USER=
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() {
local IF=
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
local lan=$(detectIF lan)
[ -n "$lan" ] && echo $lan && return
}
detectWAN() {
local 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
}
lockFunc() {
#Realize the lock function by busybox lock or flock command.
# if !(lock -n $lockFile) >/dev/null 2>&1; then
# exit 1
# fi
#The following lock method is realized by other's function.
local attempts=0
local flag=0
while [ "$flag" = 0 ]; do
local tempfile=$(mktemp $tmpDir/lock.XXXXXX)
ln $tempfile $lockFile >/dev/null 2>&1 && flag=1
rm $tempfile
if [ "$flag" = 1 ]; then
[ -n "$DEBUG" ] && echo ${updatePID} "got lock after $attempts attempts"
flag=1
else
sleep 1
attempts=$(($attempts+1))
[ -n "$DEBUG" ] && echo ${updatePID} "The $attempts attempts."
[ "$attempts" -ge 10 ] && exit
fi
done
}
unlockFunc() {
#Realize the lock function by busybox lock or flock command.
# lock -u $lockFile
# rm -f $lockFile
# [ -n "$DEBUG" ] && echo ${updatePID} "released lock"
#The following lock method is realized by other's function.
rm -f $lockFile
[ -n "$DEBUG" ] && echo ${updatePID} "released lock"
}
# chain
newChain() {
local chain=$1
local ipt=$2
# Create the RRDIPT_$chain chain (it doesn't matter if it already exists).
$ipt -t mangle -N RRDIPT_$chain 2> /dev/null
# Add the RRDIPT_$chain CHAIN to the $chain chain if not present
$ipt -t mangle -C $chain -j RRDIPT_$chain 2>/dev/null
if [ $? -ne 0 ]; then
[ -n "$DEBUG" ] && echo "DEBUG: $ipt chain misplaced, recreating it..."
$ipt -t mangle -I $chain -j RRDIPT_$chain
fi
}
# chain tun
newRuleIF() {
local chain=$1
local IF=$2
local ipt=$3
local cmd=
if [ "$chain" = "OUTPUT" ]; then
cmd="$ipt -t mangle -o $IF -j RETURN"
elif [ "$chain" = "INPUT" ]; then
cmd="$ipt -t mangle -i $IF -j RETURN"
fi
[ -n "$cmd" ] && eval $cmd " -C RRDIPT_$chain 2>/dev/null" || eval $cmd " -A RRDIPT_$chain"
}
publish() {
# sort DB
# busybox sort truncates numbers to 32 bits
grep -v '^#' $DB | awk -F, '{OFS=","; a=sprintf("%f",$6/1e6); $6=""; print a,$0}' | tr -s ',' | sort -rn | awk -F, '{OFS=",";$1=sprintf("%f",$1*1e6);print}' > $tmpDir/sorted_${updatePID}.tmp
# create HTML page
local htmPage="$tmpDir/${pb_html##*/}"
rm -f $htmPage
cp $dataDir/usage.htm1 $htmPage
while IFS=, read PEAKUSAGE_IN MAC IP IFACE SPEED_IN SPEED_OUT PEAKUSAGE_OUT TOTAL FIRSTSEEN LASTSEEN
do
echo "
new Array(\"$(lookup $MAC $IP $user_def)\",\"$MAC\",\"$IP\",$SPEED_IN,$SPEED_OUT,
$PEAKUSAGE_IN,$PEAKUSAGE_OUT,$TOTAL,\"$FIRSTSEEN\",\"$LASTSEEN\")," >> $htmPage
done < $tmpDir/sorted_${updatePID}.tmp
echo "0);" >> $htmPage
sed "s/(date)/`date`/" < $dataDir/usage.htm2 >> $htmPage
mv $htmPage "$pb_html"
}
updatePrepare() {
checkDbArg
createDbIfMissing
checkDB
[ -e $tmpDir ] || mkdir -p $tmpDir
for46="$Monitor46"
local timeNow=$(cat /proc/uptime | awk '{print $1}')
if [ -e "$logFile" ]; then
local timeLast4=$(awk -F'[: ]+' '/ipv4/{print $2}' "$logFile")
local timeLast6=$(awk -F'[: ]+' '/ipv6/{print $2}' "$logFile")
interval4=$(awk -v now=$timeNow -v last=$timeLast4 'BEGIN{print (now-last)}');
interval6=$(awk -v now=$timeNow -v last=$timeLast6 'BEGIN{print (now-last)}');
for ii in 4 6; do
[[ -n "$(echo $for46 | grep ${ii})" ]] && {
if [[ "$(eval echo \$interval${ii})" \> "0.9" ]]; then
sed -i "s/^ipv${ii}: [0-9\.]\{1,\}/ipv${ii}: $timeNow/ig" "$logFile"
else
for46=`echo "$for46" | sed "s/${ii}//g"`
fi
}
done
else
echo -e "ipv4: $timeNow\nipv6: $timeNow" >"$logFile"
fi
return 0
}
update() {
updatePID=$( sh -c 'echo $PPID' )
lockFunc
local wan=$(detectWAN)
checkWAN $wan
interfaces="$interfaces $wan"
[ "$for46" = 4 ] && IPT='iptables'
[ "$for46" = 6 ] && IPT='ip6tables'
[ "$for46" = 46 ] && IPT='iptables ip6tables'
for ii in $IPT ; do
if [ -z "$( ${ii}-save | grep RRDIPT )" ]; then
for chain in $chains; do
newChain $chain $ii
done
# track local data
for chain in INPUT OUTPUT; do
for interface in $interfaces; do
[ -n "$interface" ] && [ -e "/sys/class/net/$interface" ] && newRuleIF $chain $interface $ii
done
done
fi
# this will add rules for hosts in arp table
> $tmpDir/${ii}_${updatePID}.tmp
for chain in $chains; do
$ii -nvxL RRDIPT_$chain -t mangle -Z >> $tmpDir/${ii}_${updatePID}.tmp
done
done
[ -f $tmpDir/iptables_${updatePID}.tmp ] && (
awk -v mode="$mode" -v interfaces="$interfaces" -v wanIF="$wan" -v interval=$interval4 \
-v ipv6="0" -f $binDir/readDB.awk \
$DB \
/proc/net/arp \
$tmpDir/iptables_${updatePID}.tmp
)
[ -f $tmpDir/ip6tables_${updatePID}.tmp ] && (
echo "This file is geneated by 'ip -6 neigh'" > $tmpDir/ip6addr_${updatePID}.tmp
`ip -6 neigh >> $tmpDir/ip6addr_${updatePID}.tmp`;
awk -v mode="$mode" -v interfaces="$interfaces" -v wanIF="$wan" -v interval=$interval6 \
-v ipv6="1" -f $binDir/readDB.awk \
"$DB6" \
$tmpDir/ip6addr_${updatePID}.tmp \
$tmpDir/ip6tables_${updatePID}.tmp
)
[ "$Monitor46" = 46 ] && (
cp $DB $DB46
cat $DB6 >> $DB46
awk -f $binDir/readDB.awk "$DB46"
)
[ -n "$pb_html" ] && publish
rm -f $tmpDir/*_${updatePID}.tmp
unlockFunc
}
renamefile() {
local base=$(basename -- "$1")
local ext=$([ -z "${base/*.*/}" ] && echo ".${base##*.}" || echo '')
local base="${base%.*}"
echo "$(dirname $1)/${base}$2$ext" && return
}
ending() {
iptables-save | grep -v RRDIPT | iptables-restore
ip6tables-save | grep -v RRDIPT | ip6tables-restore
if checkPid $pidFile; then
local pid=$( cat $pidFile )
rm -rf $lockFile $logFile $pidFile $tmpDir/*
kill -9 $pid >> /dev/null 2>&1
fi
echo "exit!!"
}
checkPid() {
[ -e "$1" ] && local pid=$(cat $1) || return 1
[ -d "/proc/$pid" ] && {
[ -n "$( cat /proc/$pid/cmdline | grep wrtbwmon )" ] && return 0
}
return 1
}
sleepProcess() {
sleep 1m
kill -CONT $1 >>/dev/null 2>&1
}
loop() {
trap 'ending' INT TERM HUP QUIT
if checkPid $pidFile; then
echo "Another wrtbwmon is on running!!!"
else
local loopPID=$( sh -c 'echo $PPID' )
local SPID=
echo $loopPID > $pidFile
while true ;do
[ -n "$SPID" ] && kill -9 $SPID >>/dev/null 2>&1
sleepProcess $loopPID &
SPID=$!
updatePrepare && update
kill -STOP $loopPID >>/dev/null 2>&1
done
fi
trap INT TERM HUP QUIT
}
tips() {
echo \
"Usage: $0 [options...]
Options:
-k Exit the wrtbwmon!
-f dbfile Set the DB file path
-u usrfile Set the user_def file path
-p htmlfile Set the publish htm file path
-d Enter the foreground mode.
-D Enter the daemo mode.
-4 Listen to ipv4 only.
-6 Listen to ipv6 only.
-46 Listen to ipv4 and ipv6.
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."
}
############################################################
while [ $# != 0 ];do
case $1 in
"-k" )
/etc/init.d/wrtbwmon stop
exit 0
;;
"-f" )
shift
if [ $# -gt 0 ];then
DB=$1
DB6="$(renamefile $DB .6)"
DB46="$(renamefile $DB .46)"
else
echo "No db file path seted, exit!!"
exit 1
fi
;;
"-u")
shift
if [ $# -gt 0 ];then
user_def=$1
else
echo "No user define file path seted, exit!!"
exit 1
fi
;;
"-p")
shift
if [ $# -gt 0 ];then
pb_html=$1
else
echo "No publish html file path seted, exit!!"
exit 1
fi
;;
"-d")
runMode=1
;;
"-D")
runMode=2
;;
"-4")
Monitor46=4
;;
"-6")
Monitor46=6
;;
"-46")
Monitor46=46
;;
"&&" | "||" | ";")
break
;;
"*")
tips
;;
esac
shift
done
if [ "$runMode" = '1' ]; then
loop
elif [ "$runMode" = '2' ]; then
loop >>/dev/null 2>&1 &
else
updatePrepare && update
fi

View File

@ -0,0 +1,34 @@
<html><head><title>Traffic</title>
<script type="text/javascript">
function getSize(size) {
if (size === 0) return '0 ';
var prefix=["","k","M","G","T","P","E","Z"];
var base=1024, precision = 1;
var pos=Math.floor(Math.log(size)/Math.log(base));
if (pos > 2) precision=100;
return (Math.round(size/Math.pow(base,pos)*precision)/precision)+' '+prefix[pos];
}
function padstr(str) {
return str < 10 ? '0' + str : str;
}
function dateToString(date) {
var d = new Date((/\W/g).test(date) ? date : date * 1000);
var Y = d.getFullYear(), M = d.getMonth(), D = d.getDate();
var hh = d.getHours(), mm = d.getMinutes(), ss = d.getSeconds();
return Y + '/' + padstr(M) + '/' + padstr(D) + ' ' + padstr(hh) + ':' + padstr(mm) + ':' + padstr(ss);
}
</script></head>
<body><h1>Total Usage:</h1>
<table border="1">
<tr bgcolor=silver>
<th>User</th>
<th>Down Speed</th>
<th>Up Speed</th>
<th>Download</th>
<th>Upload</th>
<th>Total</th>
<th>First seen</th>
<th>Last seen</th>
</tr>
<script type="text/javascript">
var values = new Array(

View File

@ -0,0 +1,18 @@
var speedIn = 0;
var speedOut = 0;
var totalIn = 0;
var totalOut = 0;
for (i=0; i < values.length-1; i++) {
speedIn += values[i][3];
speedOut += values[i][4]
totalIn += values[i][5];
totalOut += values[i][6];
document.write("<tr><td><div title=\"" + values[i][1] + " (" + values[i][2] + ")" + "\">" + values[i][0] + "</div></td>");
for (j=3; j < 8; j++)
document.write("<td>" + getSize(values[i][j]) + ((j == 3) || (j ==4) ? 'B/s' : 'B') + "</td>");
document.write("<td>" + dateToString(values[i][8]) + "</td><td>" + dateToString(values[i][9]) + "</td></tr>");
}
document.write("<tr><td>TOTAL</td><td>" + getSize(speedIn) + "B/s" + "</td><td>" + getSize(speedOut) + "B/s" + "</td><td>" + getSize(totalIn) + "</td><td>" + getSize(totalOut) + "</td><td>" + getSize(totalIn + totalOut) + "</td><td></td><td></td></tr>");
</script></table>
<br /><small>This page was generated on (date)</small>
</body></html>

View File

@ -1,17 +0,0 @@
# 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:=7
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature

View File

@ -1,562 +0,0 @@
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 = '<tr';
res += ' class="tr">';
res += content;
res += '</tr>';
return res;
}
/**
* Create a `th` element with content and options
* @param content
* @param opts
* @returns {string}
*/
function createTH(content, opts) {
opts = opts || {};
var res = '<th';
if (opts.right) {
res += ' align="right"';
}
if (opts.title) {
res += ' title="' + opts.title + '"';
}
if (opts.id) {
res += ' id="' + opts.id + '"';
}
res += ' class="th">';
res += content;
res += '</th>';
return res;
}
/**
* Create a `td` element with content and options
* @param content
* @param opts
* @returns {string}
*/
function createTD(content, opts) {
opts = opts || {};
var res = '<td';
if (opts.right) {
res += ' align="right"';
}
if (opts.title) {
res += ' title="' + opts.title + '"';
}
res += ' class="td">';
res += content;
res += '</td>';
return res;
}
/**
* Returns true if obj is instance of Array
* @param obj
* @returns {boolean}
*/
function isArray(obj) {
return obj instanceof Array;
}
//----------------------
// END HELPER FUNCTIONS
//----------------------
/**
* Handle the error that happened during the call to the BE
*/
function handleError() {
// TODO handle errors
// var message = 'Something went wrong...';
}
/**
* Handle the new `values` that were received from the BE
* @param values
* @returns {string}
*/
function handleValues(values) {
if (!isArray(values)) return '';
// find data and totals
var res = parseValues(values);
var data = res[0];
var totals = res[1];
// aggregate (sub-total) by hostname (or MAC address) after the global totals are computed, before sort and display
aggregateHostTotals(data);
// store them in cache for quicker re-rendering
wrt.sortData.cache.data = data;
wrt.sortData.cache.totals = totals;
renderTableData(data, totals);
}
/**
* Renders the table body
* @param data
* @param totals
*/
function renderTableData(data, totals) {
// sort data
data.sort(sortingFunction);
// display data
document.getElementById('tableBody').innerHTML = getDisplayData(data, totals);
// set sorting arrows
var el = document.getElementById(wrt.sortData.elId);
if (el) {
el.innerHTML = el.innerHTML + (wrt.sortData.dir === 'desc' ? '&#x25BC' : '&#x25B2');
}
// register table events
registerTableEventHandlers();
}
/**
* Parses the values and returns a data array, where each element in the data array is an array with two elements,
* and a totals array, that holds aggregated values for each column.
* The first element of each row in the data array, is the HTML output of the row as a `tr` element
* and the second is the actual data:
* [ result, data ]
* @param values The `values` array
* @returns {Array}
*/
function parseValues(values) {
var data = [], totals = [0, 0, 0, 0, 0];
for (var i = 0; i < values.length; i++) {
var d = parseValueRow(values[i]);
if (d[1]) {
data.push(d);
// get totals
for (var j = 0; j < totals.length; j++) {
totals[j] += d[1][3 + j];
}
}
}
return [data, totals];
}
/**
* Parse each row in the `values` array and return an array with two elements.
* The first element is the HTML output of the row as a `tr` element and the second is the actual data
* [ result, data ]
* @param data A row from the `values` array
* @returns {[ string, [] ]}
*/
function parseValueRow(data) {
// check if data is array
if (!isArray(data)) return [''];
// find old data
var oldData;
for (var i = 0; i < oldValues.length; i++) {
var cur = oldValues[i];
// compare mac addresses and ip addresses
if (oldValues[i][1] === data[1] && oldValues[i][2] === data[2]) {
oldData = cur;
break;
}
}
// find download and upload speeds
var dlSpeed = 0, upSpeed = 0;
if (oldData) {
var now = new Date(),
seconds = (now - oldDate) / 1000;
dlSpeed = (data[3] - oldData[3]) / seconds;
upSpeed = (data[4] - oldData[4]) / seconds;
}
// create rowData
var rowData = [];
for (var j = 0; j < data.length; j++) {
rowData.push(data[j]);
if (j === 2) {
rowData.push(dlSpeed, upSpeed);
}
}
// create displayData
var displayData = [
createTD(data[0] + '<br />' + 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] + '<br/> (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('倒数 <b><font color="blue">' + start + '</font></b> 秒后刷新.');
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();
});
})();

View File

@ -1,43 +0,0 @@
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"}, form("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/config/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

View File

@ -1,18 +0,0 @@
local m = Map("wrtbwmon", translate("Details"))
local s = m:section(NamedSection, "general", "wrtbwmon", translate("General settings"))
local o = s:option(Flag, "persist", translate("Persist database"),
translate("Check this to persist the database file"))
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

View File

@ -1,23 +0,0 @@
local USER_FILE_PATH = "/etc/config/wrtbwmon.user"
local fs = require "nixio.fs"
local f = SimpleForm("wrtbwmon",
translate("Usage - Custom User File"),
translate("This file is used to match users with MAC addresses and it must have the following format: 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

View File

@ -1,46 +0,0 @@
<%+header%>
<h2><%=translate("Usage")%></h2>
<p style="position:relative;">
<button id="resetDatabase" class="cbi-button" style="position:absolute;right:0;bottom:0;"><%=translate("Reset Database")%></button>
</p>
<p>
<small><span id="updated"></span> <span id="updating"></span></small>
<span style="float:right;text-align:right;">
<label>
<small><%=translate("Auto Refresh Interval")%></small>
<select id="intervalSelect" style="width:90px;height:20px;font-size:11px;">
<option value="-1"><%=translate("Disabled")%></option>
<option value="1">1 <%=translate("Second")%></option>
<option value="2">2 <%=translate("Seconds")%></option>
<option value="3">3 <%=translate("Seconds")%></option>
<option value="4">4 <%=translate("Seconds")%></option>
<option value="5" selected="selected">5 <%=translate("Seconds")%></option>
<option value="10">10 <%=translate("Seconds")%></option>
<option value="20">20 <%=translate("Seconds")%></option>
<option value="30">30 <%=translate("Seconds")%></option>
<option value="40">40 <%=translate("Seconds")%></option>
<option value="50">50 <%=translate("Seconds")%></option>
<option value="60">60 <%=translate("Seconds")%></option>
<option value="120">2 <%=translate("Minutes")%></option>
<option value="180">3 <%=translate("Minutes")%></option>
<option value="240">4 <%=translate("Minutes")%></option>
<option value="360">5 <%=translate("Minutes")%></option>
</select>
</label>
<br/>
<label>
<small><%=translate("Per-host Totals")%></small>
<input id="perHostTotals" type="checkbox" style="vertical-align:middle;"/>
</label>
<!-- label>
<small>Show per host aggregates only</small>
<input id="showPerHostTotalsOnly" type="checkbox"/>
</label -->
</span>
</p>
<table class="table">
<tbody id="tableBody"><tr><td><%=translate("Loading...")%></td></tr></tbody>
</table>
<!--suppress HtmlUnknownTarget -->
<script src="/luci-static/wrtbwmon.js"></script>
<%+footer%>

View File

@ -1,56 +0,0 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
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"
msgstr "把统计数据写入 /etc/config 中避免重启或者升级后丢失"
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"
msgid "Reset Database"
msgstr "重置数据库"
msgid "Auto Refresh Interval"
msgstr "自动刷新间隔"
msgid "Disabled"
msgstr "禁用"
msgid "Second"
msgstr "秒"
msgid "Seconds"
msgstr "秒"
msgid "Minutes"
msgstr "分钟"
msgid "Per-host Totals"
msgstr "合并每一客户端数据"
msgid "Loading..."
msgstr "加载中..."

View File

@ -1,4 +0,0 @@
config wrtbwmon 'general'
option persist '0'

View File

@ -1,22 +0,0 @@
#!/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
}

View File

@ -1,14 +0,0 @@
#!/bin/sh
uci -q batch <<-EOF >/dev/null
delete firewall.wrtbwmon
set firewall.wrtbwmon=include
set firewall.wrtbwmon.type=script
set firewall.wrtbwmon.path='/etc/wrtbwmon.include'
set firewall.wrtbwmon.reload=1
commit firewall
EOF
/etc/init.d/wrtbwmon enable
/etc/init.d/wrtbwmon start
exit 0

View File

@ -1 +0,0 @@
/etc/init.d/wrtbwmon restart >/dev/null 2>&1

View File

@ -1,157 +0,0 @@
#!/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)
}

View File

@ -1,301 +0,0 @@
#!/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

View File

@ -1,11 +0,0 @@
{
"luci-app-wrtbwmon": {
"description": "Grant UCI access for luci-app-wrtbwmon",
"read": {
"uci": [ "wrtbwmon" ]
},
"write": {
"uci": [ "wrtbwmon" ]
}
}
}

View File

@ -1,23 +0,0 @@
<html><head><title>Traffic</title>
<script type="text/javascript">
function getSize(size) {
var prefix=new Array("","k","M","G","T","P","E","Z"); var base=1000;
var pos=0;
while (size>base) {
size/=base; pos++;
}
if (pos > 2) precision=1000; else precision = 1;
return (Math.round(size*precision)/precision)+' '+prefix[pos];}
</script></head>
<body><h1>Total Usage:</h1>
<table border="1">
<tr bgcolor=silver>
<th>User</th>
<th>Download</th>
<th>Upload</th>
<th>Total</th>
<th>First seen</th>
<th>Last seen</th>
</tr>
<script type="text/javascript">
var values = new Array(

View File

@ -1,14 +0,0 @@
var totalIn = 0;
var totalOut = 0;
for (i=0; i < values.length-1; i++) {
totalIn += values[i][3];
totalOut += values[i][4];
document.write("<tr><td><div title=\"" + values[i][1] + " (" + values[i][2] + ")" + "\">" + values[i][0] + "</div></td>");
for (j=3; j < 6; j++)
document.write("<td>" + getSize(values[i][j]) + "</td>");
document.write("<td>" + values[i][6] + "</td><td>" + values[i][7] + "</td></tr>");
}
document.write("<tr><td>TOTAL</td><td>" + getSize(totalIn) + "</td><td>" + getSize(totalOut) + "</td><td>" + getSize(totalIn + totalOut) + "</td><td></td><td></td></tr>");
</script></table>
<br /><small>This page was generated on (date)</small>
</body></html>