mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 00:05:13 +08:00
⛄ Sync 2026-04-23 20:32:35
This commit is contained in:
1 parent
17738c6f79
commit
fe51d2b304
206 files changed
+725671
No files matched your search
@@ -0,0 +1,19 @@
|
||||
#
|
||||
# Copyright (C) 2008-2014 The LuCI Team <luci@lists.subsignal.org>
|
||||
#
|
||||
# This is free software, licensed under the Apache License, Version 2.0 .
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=LuCI Status Pages
|
||||
LUCI_DEPENDS:=+luci-base +libiwinfo +rpcd-mod-iwinfo
|
||||
|
||||
PKG_RELEASE:=1
|
||||
PKG_BUILD_DEPENDS:=iwinfo
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<svg width="100%" height="100%" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<polyline id="rx" points="" style="fill:blue;fill-opacity:0.4;stroke:blue;stroke-width:1" />
|
||||
<polyline id="tx" points="" style="fill:green;fill-opacity:0.4;stroke:green;stroke-width:1" />
|
||||
|
||||
<line x1="0" y1="25%" x2="100%" y2="25%" style="stroke:black;stroke-width:0.1" />
|
||||
<text id="label_75" x="20" y="24%" style="fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000"> </text>
|
||||
|
||||
<line x1="0" y1="50%" x2="100%" y2="50%" style="stroke:black;stroke-width:0.1" />
|
||||
<text id="label_50" x="20" y="49%" style="fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000"> </text>
|
||||
|
||||
<line x1="0" y1="75%" x2="100%" y2="75%" style="stroke:black;stroke-width:0.1" />
|
||||
<text id="label_25" x="20" y="74%" style="fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000"> </text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<svg width="100%" height="100%" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<polyline id="rx" points="" style="fill:blue;fill-opacity:0.4;stroke:blue;stroke-width:1" />
|
||||
<polyline id="tx" points="" style="fill:green;fill-opacity:0.4;stroke:green;stroke-width:1" />
|
||||
|
||||
<line x1="0" y1="25%" x2="100%" y2="25%" style="stroke:black;stroke-width:0.1" />
|
||||
<text id="label_75" x="10" y="24%" style="fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000">-25 dbm</text>
|
||||
|
||||
<line x1="0" y1="50%" x2="100%" y2="50%" style="stroke:black;stroke-width:0.1" />
|
||||
<text id="label_50" x="10" y="49%" style="fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000">-50 dbm</text>
|
||||
|
||||
<line x1="0" y1="75%" x2="100%" y2="75%" style="stroke:black;stroke-width:0.1" />
|
||||
<text id="label_25" x="10" y="74%" style="fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000">-75 dbm</text>
|
||||
|
||||
<line x1="0" y1="90%" x2="100%" y2="90%" style="stroke:black;stroke-width:0.1" />
|
||||
<text id="label_10" x="10" y="89%" style="fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000">-90 dbm</text>
|
||||
<defs>
|
||||
<linearGradient id="GradientVerticalCenteredBlack">
|
||||
<stop class="stopinvis" offset="0%" />
|
||||
<stop class="stopblack" offset="50%" />
|
||||
<stop class="stopinvis" offset="100%" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<style>
|
||||
.stopinvis {
|
||||
stop-color: "transparent";
|
||||
stop-opacity: 0;
|
||||
}
|
||||
.stopblack {
|
||||
stop-color: "black";
|
||||
stop-opacity: .1;
|
||||
}
|
||||
</style>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<svg width="100%" height="100%" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<polyline id="tcp" points="" style="fill:green;fill-opacity:0.4;stroke:green;stroke-width:1" />
|
||||
<polyline id="udp" points="" style="fill:blue;fill-opacity:0.4;stroke:blue;stroke-width:1" />
|
||||
<polyline id="other" points="" style="fill:red;fill-opacity:0.4;stroke:red;stroke-width:1" />
|
||||
|
||||
<line x1="0" y1="25%" x2="100%" y2="25%" style="stroke:black;stroke-width:0.1" />
|
||||
<text id="label_75" x="20" y="24%" style="fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000"> </text>
|
||||
|
||||
<line x1="0" y1="50%" x2="100%" y2="50%" style="stroke:black;stroke-width:0.1" />
|
||||
<text id="label_50" x="20" y="49%" style="fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000"> </text>
|
||||
|
||||
<line x1="0" y1="75%" x2="100%" y2="75%" style="stroke:black;stroke-width:0.1" />
|
||||
<text id="label_25" x="20" y="74%" style="fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000"> </text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<svg width="100%" height="100%" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<polyline id="load01" points="" style="fill:#ff0000;fill-opacity:0.4;stroke:#ff0000;stroke-width:1" />
|
||||
<polyline id="load05" points="" style="fill:#ff6600;fill-opacity:0.4;stroke:#ff6600;stroke-width:1" />
|
||||
<polyline id="load15" points="" style="fill:#ffaa00;fill-opacity:0.4;stroke:#ffaa00;stroke-width:1" />
|
||||
|
||||
<line x1="0" y1="25%" x2="100%" y2="25%" style="stroke:black;stroke-width:0.1" />
|
||||
<text id="label_75" x="20" y="24%" style="fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000"> </text>
|
||||
|
||||
<line x1="0" y1="50%" x2="100%" y2="50%" style="stroke:black;stroke-width:0.1" />
|
||||
<text id="label_50" x="20" y="49%" style="fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000"> </text>
|
||||
|
||||
<line x1="0" y1="75%" x2="100%" y2="75%" style="stroke:black;stroke-width:0.1" />
|
||||
<text id="label_25" x="20" y="74%" style="fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000"> </text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<svg width="100%" height="100%" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<polyline id="rate" points="" style="fill:green;fill-opacity:0.4;stroke:green;stroke-width:1" />
|
||||
|
||||
<line x1="0" y1="25%" x2="100%" y2="25%" style="stroke:black;stroke-width:0.1" />
|
||||
<text id="label_75" x="20" y="24%" style="fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000"> </text>
|
||||
|
||||
<line x1="0" y1="50%" x2="100%" y2="50%" style="stroke:black;stroke-width:0.1" />
|
||||
<text id="label_50" x="20" y="49%" style="fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000"> </text>
|
||||
|
||||
<line x1="0" y1="75%" x2="100%" y2="75%" style="stroke:black;stroke-width:0.1" />
|
||||
<text id="label_25" x="20" y="74%" style="fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000"> </text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 973 B |
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<svg width="100%" height="100%" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<polyline id="rssi" points="" style="fill:blue;fill-opacity:0.4;stroke:blue;stroke-width:1" />
|
||||
<polyline id="noise" points="" style="fill:red;fill-opacity:0.4;stroke:red;stroke-width:1" />
|
||||
|
||||
<line x1="0" y1="25%" x2="100%" y2="25%" style="stroke:black;stroke-width:0.1" />
|
||||
<text id="label_75" x="20" y="24%" style="fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000"> </text>
|
||||
|
||||
<line x1="0" y1="50%" x2="100%" y2="50%" style="stroke:black;stroke-width:0.1" />
|
||||
<text id="label_50" x="20" y="49%" style="fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000"> </text>
|
||||
|
||||
<line x1="0" y1="75%" x2="100%" y2="75%" style="stroke:black;stroke-width:0.1" />
|
||||
<text id="label_25" x="20" y="74%" style="fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000"> </text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,320 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require dom';
|
||||
'require poll';
|
||||
'require request';
|
||||
'require ui';
|
||||
'require rpc';
|
||||
'require network';
|
||||
|
||||
const callLuciRealtimeStats = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getRealtimeStats',
|
||||
params: [ 'mode', 'device' ],
|
||||
expect: { result: [] }
|
||||
});
|
||||
|
||||
const graphPolls = [];
|
||||
const pollInterval = 3;
|
||||
|
||||
Math.log2 = Math.log2 || function(x) { return Math.log(x) * Math.LOG2E; };
|
||||
|
||||
function rate(n, br) {
|
||||
n = (n || 0).toFixed(2);
|
||||
return [ '%1024.2mbit/s'.format(n * 8), br ? E('br') : ' ', '(%1024.2mB/s)'.format(n) ]
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
return Promise.all([
|
||||
this.loadSVG(L.resource('svg/bandwidth.svg')),
|
||||
network.getDevices()
|
||||
]);
|
||||
},
|
||||
|
||||
updateGraph(ifname, svg, lines, cb) {
|
||||
const G = svg.firstElementChild;
|
||||
|
||||
const view = document.querySelector('#view');
|
||||
|
||||
const width = view.offsetWidth - 2;
|
||||
const height = 300 - 2;
|
||||
const step = 5;
|
||||
|
||||
const data_wanted = Math.floor(width / step);
|
||||
|
||||
const data_values = [];
|
||||
|
||||
for (const line of lines)
|
||||
if (line)
|
||||
data_values.push([]);
|
||||
|
||||
const info = {
|
||||
line_current: [],
|
||||
line_average: [],
|
||||
line_peak: []
|
||||
};
|
||||
|
||||
/* prefill datasets */
|
||||
for (let i = 0; i < data_values.length; i++)
|
||||
for (let j = 0; j < data_wanted; j++)
|
||||
data_values[i][j] = 0;
|
||||
|
||||
/* plot horizontal time interval lines */
|
||||
for (let i = width % (step * 60); i < width; i += step * 60) {
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
line.setAttribute('x1', i);
|
||||
line.setAttribute('y1', 0);
|
||||
line.setAttribute('x2', i);
|
||||
line.setAttribute('y2', '100%');
|
||||
line.setAttribute('style', 'stroke:black;stroke-width:0.1');
|
||||
|
||||
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
text.setAttribute('x', i + 5);
|
||||
text.setAttribute('y', 15);
|
||||
text.setAttribute('style', 'fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000');
|
||||
text.appendChild(document.createTextNode(Math.round((width - i) / step / 60) + 'm'));
|
||||
|
||||
G.appendChild(line);
|
||||
G.appendChild(text);
|
||||
}
|
||||
|
||||
info.interval = pollInterval;
|
||||
info.timeframe = data_wanted / 60;
|
||||
|
||||
graphPolls.push({
|
||||
ifname: ifname,
|
||||
svg: svg,
|
||||
lines: lines,
|
||||
cb: cb,
|
||||
info: info,
|
||||
width: width,
|
||||
height: height,
|
||||
step: step,
|
||||
values: data_values,
|
||||
timestamp: 0,
|
||||
fill: 1
|
||||
});
|
||||
},
|
||||
|
||||
pollData() {
|
||||
poll.add(L.bind(function() {
|
||||
const tasks = [];
|
||||
|
||||
for (const ctx of graphPolls) {
|
||||
tasks.push(L.resolveDefault(callLuciRealtimeStats('interface', ctx.ifname), []));
|
||||
}
|
||||
|
||||
return Promise.all(tasks).then(L.bind(function(datasets) {
|
||||
for (let gi = 0; gi < graphPolls.length; gi++) {
|
||||
let ctx = graphPolls[gi];
|
||||
const data = datasets[gi];
|
||||
const values = ctx.values;
|
||||
const lines = ctx.lines;
|
||||
const info = ctx.info;
|
||||
|
||||
let data_scale = 0;
|
||||
const data_wanted = Math.floor(ctx.width / ctx.step);
|
||||
let last_timestamp = NaN;
|
||||
|
||||
for (let i = 0, di = 0; di < lines.length; di++) {
|
||||
if (lines[di] == null)
|
||||
continue;
|
||||
|
||||
const multiply = (lines[di].multiply != null) ? lines[di].multiply : 1;
|
||||
const offset = (lines[di].offset != null) ? lines[di].offset : 0;
|
||||
|
||||
for (let j = ctx.timestamp ? 0 : 1; j < data.length; j++) {
|
||||
/* skip overlapping entries */
|
||||
if (data[j][0] <= ctx.timestamp)
|
||||
continue;
|
||||
|
||||
if (i == 0) {
|
||||
ctx.fill++;
|
||||
last_timestamp = data[j][0];
|
||||
}
|
||||
|
||||
if (lines[di].counter) {
|
||||
/* normalize difference against time interval */
|
||||
if (j > 0) {
|
||||
var time_delta = data[j][0] - data[j - 1][0];
|
||||
if (time_delta) {
|
||||
info.line_current[i] = (data[j][di + 1] * multiply - data[j - 1][di + 1] * multiply) / time_delta;
|
||||
info.line_current[i] -= Math.min(info.line_current[i], offset);
|
||||
values[i].push(info.line_current[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
info.line_current[i] = data[j][di + 1] * multiply;
|
||||
info.line_current[i] -= Math.min(info.line_current[i], offset);
|
||||
values[i].push(info.line_current[i]);
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
/* cut off outdated entries */
|
||||
ctx.fill = Math.min(ctx.fill, data_wanted);
|
||||
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const len = values[i].length;
|
||||
values[i] = values[i].slice(len - data_wanted, len);
|
||||
|
||||
/* find peaks, averages */
|
||||
info.line_peak[i] = NaN;
|
||||
info.line_average[i] = 0;
|
||||
|
||||
for (let j = 0; j < values[i].length; j++) {
|
||||
info.line_peak[i] = isNaN(info.line_peak[i]) ? values[i][j] : Math.max(info.line_peak[i], values[i][j]);
|
||||
info.line_average[i] += values[i][j];
|
||||
}
|
||||
|
||||
info.line_average[i] = info.line_average[i] / ctx.fill;
|
||||
}
|
||||
|
||||
info.peak = Math.max.apply(Math, info.line_peak);
|
||||
|
||||
/* remember current timestamp, calculate horizontal scale */
|
||||
if (!isNaN(last_timestamp))
|
||||
ctx.timestamp = last_timestamp;
|
||||
|
||||
const size = Math.floor(Math.log2(info.peak));
|
||||
const div = Math.pow(2, size - (size % 10));
|
||||
const p_o_d = info.peak / div;
|
||||
const mult = (p_o_d < 5) ? 2 : ((p_o_d < 50) ? 10 : ((p_o_d < 500) ? 100 : 1000));
|
||||
|
||||
info.peak = info.peak + (mult * div) - (info.peak % (mult * div));
|
||||
|
||||
data_scale = ctx.height / info.peak;
|
||||
|
||||
/* plot data */
|
||||
for (let i = 0, di = 0; di < lines.length; di++) {
|
||||
if (lines[di] == null)
|
||||
continue;
|
||||
|
||||
const el = ctx.svg.firstElementChild.getElementById(lines[di].line);
|
||||
let pt = `0,${ctx.height}`;
|
||||
let y = 0;
|
||||
|
||||
if (!el)
|
||||
continue;
|
||||
|
||||
for (let j = 0; j < values[i].length; j++) {
|
||||
const x = j * ctx.step;
|
||||
|
||||
y = ctx.height - Math.floor(values[i][j] * data_scale);
|
||||
//y -= Math.floor(y % (1 / data_scale));
|
||||
|
||||
y = isNaN(y) ? ctx.height : y;
|
||||
|
||||
pt += ` ${x},${y}`;
|
||||
}
|
||||
|
||||
pt += ` ${ctx.width},${y} ${ctx.width},${ctx.height}`;
|
||||
|
||||
el.setAttribute('points', pt);
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
info.label_25 = 0.25 * info.peak;
|
||||
info.label_50 = 0.50 * info.peak;
|
||||
info.label_75 = 0.75 * info.peak;
|
||||
|
||||
if (typeof(ctx.cb) == 'function')
|
||||
ctx.cb(ctx.svg, info);
|
||||
}
|
||||
}, this));
|
||||
}, this), pollInterval);
|
||||
},
|
||||
|
||||
loadSVG(src) {
|
||||
return request.get(src).then(function(response) {
|
||||
if (!response.ok)
|
||||
throw new Error(response.statusText);
|
||||
|
||||
return E('div', {
|
||||
'style': 'width:100%;height:300px;border:1px solid #000;background:#fff'
|
||||
}, E(response.text()));
|
||||
});
|
||||
},
|
||||
|
||||
render([svg, devs]) {
|
||||
|
||||
const v = E('div', { 'class': 'cbi-map', 'id': 'map' }, E('div'));
|
||||
|
||||
for (const dev of devs) {
|
||||
const ifname = dev.getName();
|
||||
const ssid = dev.wif?.getSSID?.() || null;
|
||||
|
||||
if (!ifname || !dev.isUp() || dev.wif?.isDisabled())
|
||||
continue;
|
||||
|
||||
const csvg = svg.cloneNode(true);
|
||||
|
||||
v.firstElementChild.appendChild(E('div', { 'class': 'cbi-section', 'data-tab': ifname, 'data-tab-title': ssid ? `${ifname} ${ssid}` : ifname }, [
|
||||
csvg,
|
||||
E('div', { 'class': 'right' }, E('small', { 'id': 'scale' }, '-')),
|
||||
E('br'),
|
||||
|
||||
E('table', { 'class': 'table', 'style': 'width:100%;table-layout:fixed' }, [
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td right top' }, E('strong', { 'style': 'border-bottom:2px solid blue' }, [ _('Inbound:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'rx_bw_cur' }, rate(0, true)),
|
||||
|
||||
E('td', { 'class': 'td right top' }, E('strong', {}, [ _('Average:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'rx_bw_avg' }, rate(0, true)),
|
||||
|
||||
E('td', { 'class': 'td right top' }, E('strong', {}, [ _('Peak:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'rx_bw_peak' }, rate(0, true))
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td right top' }, E('strong', { 'style': 'border-bottom:2px solid green' }, [ _('Outbound:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'tx_bw_cur' }, rate(0, true)),
|
||||
|
||||
E('td', { 'class': 'td right top' }, E('strong', {}, [ _('Average:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'tx_bw_avg' }, rate(0, true)),
|
||||
|
||||
E('td', { 'class': 'td right top' }, E('strong', {}, [ _('Peak:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'tx_bw_peak' }, rate(0, true))
|
||||
])
|
||||
]),
|
||||
E('div', {'class': 'cbi-section-create'})
|
||||
]));
|
||||
|
||||
this.updateGraph(ifname, csvg, [ { line: 'rx', counter: true }, null, { line: 'tx', counter: true } ], function(svg, info) {
|
||||
const G = svg.firstElementChild, tab = svg.parentNode;
|
||||
|
||||
G.getElementById('label_25').firstChild.data = rate(info.label_25).join('');
|
||||
G.getElementById('label_50').firstChild.data = rate(info.label_50).join('');
|
||||
G.getElementById('label_75').firstChild.data = rate(info.label_75).join('');
|
||||
|
||||
tab.querySelector('#scale').firstChild.data = _('(%d minute window, %d second interval)').format(info.timeframe, info.interval);
|
||||
|
||||
dom.content(tab.querySelector('#rx_bw_cur'), rate(info.line_current[0], true));
|
||||
dom.content(tab.querySelector('#rx_bw_avg'), rate(info.line_average[0], true));
|
||||
dom.content(tab.querySelector('#rx_bw_peak'), rate(info.line_peak[0], true));
|
||||
|
||||
dom.content(tab.querySelector('#tx_bw_cur'), rate(info.line_current[1], true));
|
||||
dom.content(tab.querySelector('#tx_bw_avg'), rate(info.line_average[1], true));
|
||||
dom.content(tab.querySelector('#tx_bw_peak'), rate(info.line_peak[1], true));
|
||||
});
|
||||
}
|
||||
|
||||
ui.tabs.initTabGroup(v.firstElementChild.childNodes);
|
||||
|
||||
this.pollData();
|
||||
|
||||
return E([], [
|
||||
E('h2', _('Bandwidth')),
|
||||
E('div', {'class': 'cbi-map-descr'}, _('This page displays the bandwidth used for all available physical interfaces.')),
|
||||
v
|
||||
]);
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,907 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require poll';
|
||||
'require request';
|
||||
'require network';
|
||||
'require fs';
|
||||
'require ui';
|
||||
'require rpc';
|
||||
'require tools.prng as random';
|
||||
|
||||
function parseIwDevMap(stdout) {
|
||||
const byPhy = {};
|
||||
let currentPhy = null;
|
||||
|
||||
for (const rawLine of String(stdout || '').split(/\n/)) {
|
||||
const phyMatch = rawLine.match(/^\s*phy#(\d+)\s*$/);
|
||||
const ifMatch = rawLine.match(/^\s*Interface\s+(\S+)\s*$/);
|
||||
|
||||
if (phyMatch) {
|
||||
currentPhy = `phy${phyMatch[1]}`;
|
||||
byPhy[currentPhy] ??= [];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentPhy && ifMatch)
|
||||
byPhy[currentPhy].push(ifMatch[1]);
|
||||
}
|
||||
|
||||
return Object.entries(byPhy).reduce((map, entry) => {
|
||||
const phy = entry[0];
|
||||
const ifaces = entry[1];
|
||||
const preferred = ifaces.find((ifname) => !/^wifi\d+$/.test(ifname)) || ifaces[0];
|
||||
const temp = `tmpsta${phy.replace(/^phy/, '')}`;
|
||||
|
||||
ifaces.forEach((ifname) => {
|
||||
map[ifname] = {
|
||||
phy: phy,
|
||||
preferred: preferred,
|
||||
temp: temp
|
||||
};
|
||||
});
|
||||
|
||||
return map;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function decodeIwSSID(raw) {
|
||||
const value = String(raw || '');
|
||||
const bytes = [];
|
||||
let hasEscapes = false;
|
||||
let hasWideChars = false;
|
||||
|
||||
if (value === '')
|
||||
return null;
|
||||
|
||||
for (let i = 0; i < value.length;) {
|
||||
if (value[i] == '\\' && value[i + 1] == 'x' && i + 3 < value.length) {
|
||||
const hex = value.substr(i + 2, 2);
|
||||
const byte = parseInt(hex, 16);
|
||||
|
||||
if (!isNaN(byte)) {
|
||||
bytes.push(byte);
|
||||
i += 4;
|
||||
hasEscapes = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const code = value.charCodeAt(i++);
|
||||
|
||||
if (code > 0xff) {
|
||||
hasWideChars = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code !== 0)
|
||||
bytes.push(code & 0xff);
|
||||
}
|
||||
|
||||
if (hasWideChars)
|
||||
return value.replace(/\u0000+/g, '').trim() || null;
|
||||
|
||||
if (!bytes.length)
|
||||
return null;
|
||||
|
||||
try {
|
||||
if ((hasEscapes || bytes.some((byte) => byte >= 0x80)) && typeof(TextDecoder) == 'function')
|
||||
return new TextDecoder('utf-8', { fatal: false }).decode(new Uint8Array(bytes)).trim() || null;
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
|
||||
return String.fromCharCode.apply(null, bytes).replace(/\u0000+/g, '').trim() || null;
|
||||
}
|
||||
|
||||
function frequencyToChannel(freq) {
|
||||
freq = +freq;
|
||||
|
||||
if (isNaN(freq) || freq <= 0)
|
||||
return null;
|
||||
if (freq == 2484)
|
||||
return 14;
|
||||
if (freq >= 2412 && freq <= 2472)
|
||||
return Math.round((freq - 2407) / 5);
|
||||
if (freq >= 5000 && freq <= 5895)
|
||||
return Math.round((freq - 5000) / 5);
|
||||
if (freq >= 5955 && freq <= 7115)
|
||||
return Math.round((freq - 5950) / 5);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function deriveBand(res) {
|
||||
const freq = +res.mhz;
|
||||
const channel = +res.channel;
|
||||
|
||||
if (!isNaN(freq) && freq >= 5955)
|
||||
return 6;
|
||||
if (!isNaN(freq) && freq >= 5000)
|
||||
return 5;
|
||||
if (!isNaN(freq) && freq >= 2400)
|
||||
return 2;
|
||||
if (!isNaN(channel) && channel >= 36)
|
||||
return 5;
|
||||
if (!isNaN(channel) && channel >= 1 && channel <= 14)
|
||||
return 2;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeScanResult(res) {
|
||||
const out = Object.assign({}, res);
|
||||
|
||||
out.ssid = decodeIwSSID(out.ssid);
|
||||
out.channel = +out.channel || frequencyToChannel(out.mhz);
|
||||
out.mhz = +out.mhz || null;
|
||||
out.band = deriveBand(out);
|
||||
out.mode = out.mode || 'Master';
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseChannelWidth(widthText) {
|
||||
const value = String(widthText || '');
|
||||
|
||||
if (/80\+80/i.test(value))
|
||||
return 8080;
|
||||
if (/160/i.test(value))
|
||||
return 160;
|
||||
if (/80/i.test(value))
|
||||
return 80;
|
||||
if (/20 or 40/i.test(value))
|
||||
return 40;
|
||||
|
||||
return 20;
|
||||
}
|
||||
|
||||
function parseIwScan(stdout) {
|
||||
const results = [];
|
||||
const lines = String(stdout || '').split(/\n/);
|
||||
let current = null;
|
||||
let section = null;
|
||||
|
||||
function finishCurrent() {
|
||||
if (!current?.bssid || current.signal == null)
|
||||
return;
|
||||
|
||||
results.push(normalizeScanResult(current));
|
||||
}
|
||||
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.trim();
|
||||
const bssMatch = line.match(/^BSS\s+([0-9a-f:]{17})\b/i);
|
||||
|
||||
if (bssMatch) {
|
||||
finishCurrent();
|
||||
current = {
|
||||
bssid: bssMatch[1].toUpperCase(),
|
||||
mode: 'Master'
|
||||
};
|
||||
section = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!current || line === '')
|
||||
continue;
|
||||
|
||||
const freqMatch = line.match(/^freq:\s*(\d+)/i);
|
||||
if (freqMatch) {
|
||||
current.mhz = +freqMatch[1];
|
||||
if (current.channel == null)
|
||||
current.channel = frequencyToChannel(current.mhz);
|
||||
continue;
|
||||
}
|
||||
|
||||
const signalMatch = line.match(/^signal:\s*(-?\d+(?:\.\d+)?)\s*dBm/i);
|
||||
if (signalMatch) {
|
||||
current.signal = Math.round(parseFloat(signalMatch[1]));
|
||||
continue;
|
||||
}
|
||||
|
||||
const ssidMatch = line.match(/^SSID:\s*(.*)$/);
|
||||
if (ssidMatch) {
|
||||
current.ssid = decodeIwSSID(ssidMatch[1]);
|
||||
continue;
|
||||
}
|
||||
|
||||
const capabilityMatch = line.match(/^capability:\s*([A-Z-]+)/i);
|
||||
if (capabilityMatch) {
|
||||
if (/IBSS/i.test(capabilityMatch[1]))
|
||||
current.mode = 'Ad-Hoc';
|
||||
else if (/ESS/i.test(capabilityMatch[1]))
|
||||
current.mode = 'Master';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^HT operation:/i.test(line)) {
|
||||
current.ht_operation ??= {};
|
||||
section = 'ht';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^VHT operation:/i.test(line)) {
|
||||
current.vht_operation ??= {};
|
||||
section = 'vht';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^HE operation:/i.test(line)) {
|
||||
current.he_operation ??= {};
|
||||
section = 'he';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^EHT operation:/i.test(line)) {
|
||||
current.eht_operation ??= {};
|
||||
section = 'eht';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (section == 'ht') {
|
||||
const primaryMatch = line.match(/primary channel:\s*(\d+)/i);
|
||||
const offsetMatch = line.match(/secondary channel offset:\s*(below|above|no secondary)/i);
|
||||
const widthMatch = line.match(/STA channel width:\s*(20 MHz|any)/i);
|
||||
|
||||
if (primaryMatch) {
|
||||
current.channel = +primaryMatch[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (offsetMatch) {
|
||||
current.ht_operation.secondary_channel_offset = offsetMatch[1] == 'no secondary' ? 'none' : offsetMatch[1];
|
||||
if (offsetMatch[1] == 'above' || offsetMatch[1] == 'below')
|
||||
current.ht_operation.channel_width = 2040;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (widthMatch && widthMatch[1] == '20 MHz')
|
||||
current.ht_operation.channel_width = 20;
|
||||
}
|
||||
else if (section == 'vht') {
|
||||
const widthMatch = line.match(/channel width:\s*([0-9]+)\s*\(([^)]+)\)/i);
|
||||
const center1Match = line.match(/center freq segment 1:\s*(\d+)/i);
|
||||
const center2Match = line.match(/center freq segment 2:\s*(\d+)/i);
|
||||
|
||||
if (widthMatch) {
|
||||
current.vht_operation.channel_width = parseChannelWidth(widthMatch[2]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (center1Match) {
|
||||
current.vht_operation.center_freq_1 = +center1Match[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (center2Match) {
|
||||
current.vht_operation.center_freq_2 = +center2Match[1];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (section == 'he') {
|
||||
const widthMatch = line.match(/channel width:\s*(20|40|80|160|320)\s*MHz/i);
|
||||
const center1Match = line.match(/center freq segment 1:\s*(\d+)/i);
|
||||
const center2Match = line.match(/center freq segment 2:\s*(\d+)/i);
|
||||
|
||||
if (widthMatch) {
|
||||
current.he_operation.channel_width = +widthMatch[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (center1Match) {
|
||||
current.he_operation.center_freq_1 = +center1Match[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (center2Match) {
|
||||
current.he_operation.center_freq_2 = +center2Match[1];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (section == 'eht') {
|
||||
const widthMatch = line.match(/channel width:\s*(320)\s*MHz/i);
|
||||
const center2Match = line.match(/center freq segment 2:\s*(\d+)/i);
|
||||
|
||||
if (widthMatch) {
|
||||
current.eht_operation.channel_width = +widthMatch[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (center2Match)
|
||||
current.eht_operation.center_freq_2 = +center2Match[1];
|
||||
}
|
||||
}
|
||||
|
||||
finishCurrent();
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function getSignalPercent(res) {
|
||||
const qv = +res.quality;
|
||||
const qm = +res.quality_max;
|
||||
|
||||
if (qv > 0 && qm > 0)
|
||||
return Math.floor((100 / qm) * qv);
|
||||
if (res.signal != null)
|
||||
return Math.max(0, Math.min(100, Math.round(((+res.signal) + 110) / 70 * 100)));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
callFrequencyList: rpc.declare({
|
||||
object: 'iwinfo',
|
||||
method: 'freqlist',
|
||||
params: [ 'device' ],
|
||||
expect: { results: [] }
|
||||
}),
|
||||
|
||||
callNetworkDevices: rpc.declare({
|
||||
object: 'luci-rpc',
|
||||
method: 'getNetworkDevices',
|
||||
expect: { devices: [] }
|
||||
}),
|
||||
|
||||
cachedNetworkDevices: null,
|
||||
cachedNetworkDevicesPromise: null,
|
||||
|
||||
scanViaIw(scanIfname) {
|
||||
const attempts = [
|
||||
[ 'dev', scanIfname, 'scan' ],
|
||||
[ 'dev', scanIfname, 'scan', 'ap-force' ],
|
||||
[ 'dev', scanIfname, 'scan', 'dump' ]
|
||||
];
|
||||
let index = 0;
|
||||
|
||||
const tryNext = () => {
|
||||
if (index >= attempts.length)
|
||||
return [];
|
||||
|
||||
return L.resolveDefault(fs.exec('/usr/sbin/iw', attempts[index++]), null).then((res) => {
|
||||
const parsed = parseIwScan(res?.stdout);
|
||||
|
||||
if (parsed.length)
|
||||
return parsed;
|
||||
|
||||
return tryNext();
|
||||
}).catch(() => tryNext());
|
||||
};
|
||||
|
||||
return tryNext();
|
||||
},
|
||||
|
||||
cleanupTemporaryScanIface(tempIfname) {
|
||||
return L.resolveDefault(fs.exec('/usr/sbin/ip', [ 'link', 'set', tempIfname, 'down' ]), null)
|
||||
.then(() => L.resolveDefault(fs.exec('/usr/sbin/iw', [ 'dev', tempIfname, 'del' ]), null));
|
||||
},
|
||||
|
||||
scanViaTemporaryIface(phy, tempIfname) {
|
||||
return this.cleanupTemporaryScanIface(tempIfname)
|
||||
.then(() => fs.exec('/usr/sbin/iw', [ 'phy', phy, 'interface', 'add', tempIfname, 'type', 'station' ]))
|
||||
.then(() => fs.exec('/usr/sbin/ip', [ 'link', 'set', tempIfname, 'up' ]))
|
||||
.then(() => this.scanViaIw(tempIfname))
|
||||
.finally(L.bind(this.cleanupTemporaryScanIface, this, tempIfname));
|
||||
},
|
||||
|
||||
loadNetworkDevices(forceReload) {
|
||||
if (forceReload) {
|
||||
this.cachedNetworkDevices = null;
|
||||
this.cachedNetworkDevicesPromise = null;
|
||||
}
|
||||
|
||||
if (this.cachedNetworkDevices)
|
||||
return Promise.resolve(this.cachedNetworkDevices);
|
||||
|
||||
if (this.cachedNetworkDevicesPromise)
|
||||
return this.cachedNetworkDevicesPromise;
|
||||
|
||||
this.cachedNetworkDevicesPromise = this.callNetworkDevices().then((devices) => {
|
||||
this.cachedNetworkDevices = devices || [];
|
||||
this.cachedNetworkDevicesPromise = null;
|
||||
|
||||
return this.cachedNetworkDevices;
|
||||
}).catch((err) => {
|
||||
this.cachedNetworkDevicesPromise = null;
|
||||
throw err;
|
||||
});
|
||||
|
||||
return this.cachedNetworkDevicesPromise;
|
||||
},
|
||||
|
||||
resolveScanDevice(radioDev, forceReload) {
|
||||
return this.loadNetworkDevices(forceReload).then((devices) => {
|
||||
const apDevice = devices.find((dev) => dev.wireless &&
|
||||
dev.wireless.radio == radioDev.getName() &&
|
||||
dev.type !== 'wifi' &&
|
||||
dev.type !== 'radio');
|
||||
|
||||
return apDevice?.device || radioDev.getName();
|
||||
});
|
||||
},
|
||||
|
||||
getScanResultsForRadio(radioDev) {
|
||||
return this.resolveScanDevice(radioDev).then((scanIfname) => {
|
||||
const iwDev = this.iwDevMap?.[scanIfname] || this.iwDevMap?.[radioDev.getName()];
|
||||
const scanTasks = [];
|
||||
|
||||
if (iwDev?.phy)
|
||||
scanTasks.push(() => this.scanViaTemporaryIface(iwDev.phy, iwDev.temp));
|
||||
|
||||
scanTasks.push(() => this.scanViaIw(scanIfname));
|
||||
|
||||
const tryNext = (index) => {
|
||||
if (index >= scanTasks.length)
|
||||
return [];
|
||||
|
||||
return Promise.resolve(scanTasks[index]()).then((results) => {
|
||||
if (Array.isArray(results) && results.length)
|
||||
return results;
|
||||
|
||||
return tryNext(index + 1);
|
||||
}).catch(() => tryNext(index + 1));
|
||||
};
|
||||
|
||||
return tryNext(0);
|
||||
});
|
||||
},
|
||||
|
||||
render_signal_badge(signalPercent, signalValue) {
|
||||
let icon, title, value;
|
||||
|
||||
if (signalPercent < 0)
|
||||
icon = L.resource('icons/signal-none.svg');
|
||||
else if (signalPercent == 0)
|
||||
icon = L.resource('icons/signal-000-000.svg');
|
||||
else if (signalPercent < 25)
|
||||
icon = L.resource('icons/signal-000-025.svg');
|
||||
else if (signalPercent < 50)
|
||||
icon = L.resource('icons/signal-025-050.svg');
|
||||
else if (signalPercent < 75)
|
||||
icon = L.resource('icons/signal-050-075.svg');
|
||||
else
|
||||
icon = L.resource('icons/signal-075-100.svg');
|
||||
|
||||
value = '%d\xa0%s'.format(signalValue, _('dBm'));
|
||||
title = '%s: %d %s'.format(_('Signal'), signalValue, _('dBm'));
|
||||
|
||||
return E('div', {
|
||||
'class': 'ifacebadge',
|
||||
'title': title,
|
||||
'data-signal': signalValue
|
||||
}, [
|
||||
E('img', { 'src': icon }),
|
||||
value
|
||||
]);
|
||||
},
|
||||
|
||||
add_wifi_to_graph(chan_analysis, res, scanCache, channels, channel_width) {
|
||||
const offset_tbl = chan_analysis.offset_tbl;
|
||||
const height = chan_analysis.graph.offsetHeight - 2;
|
||||
const step = chan_analysis.col_width;
|
||||
const height_diff = (height-(height-(res.signal*-4)));
|
||||
|
||||
if (scanCache[res.bssid].color == null)
|
||||
scanCache[res.bssid].color = random.derive_color(res.bssid);
|
||||
|
||||
if (scanCache[res.bssid].graph == null || scanCache[res.bssid].graph === undefined) {
|
||||
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
|
||||
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
const color = scanCache[res.bssid].color;
|
||||
|
||||
line.setAttribute('style', 'fill:'+color+'4f'+';stroke:'+color+';stroke-width:0.5');
|
||||
text.setAttribute('style', 'fill:'+color+';font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000');
|
||||
text.appendChild(document.createTextNode(res.ssid || res.bssid));
|
||||
|
||||
group.appendChild(line);
|
||||
group.appendChild(text);
|
||||
|
||||
chan_analysis.graph.firstElementChild.appendChild(group);
|
||||
scanCache[res.bssid].graph = { group : group, line : line, text : text };
|
||||
}
|
||||
|
||||
channels.forEach(function(channel) {
|
||||
if (channel_width > 2) {
|
||||
if (!("main" in scanCache[res.bssid].graph)) {
|
||||
const main = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
|
||||
main.setAttribute('style', 'fill:url(#GradientVerticalCenteredBlack)');
|
||||
scanCache[res.bssid].graph.group.appendChild(main);
|
||||
chan_analysis.graph.firstElementChild.lastElementChild.appendChild(main);
|
||||
scanCache[res.bssid].graph["main"] = main;
|
||||
}
|
||||
const main_offset = offset_tbl[res.channel];
|
||||
const points = [
|
||||
(main_offset-(step*(2 )))+','+height,
|
||||
(main_offset-(step*(2-1)))+','+height_diff,
|
||||
(main_offset+(step*(2-1)))+','+height_diff,
|
||||
(main_offset+(step*(2 )))+','+height
|
||||
];
|
||||
scanCache[res.bssid].graph.main.setAttribute('points', points);
|
||||
}
|
||||
|
||||
const chan_offset = offset_tbl[channel];
|
||||
const points = [
|
||||
(chan_offset-(step*(channel_width )))+','+height,
|
||||
(chan_offset-(step*(channel_width-1)))+','+height_diff,
|
||||
(chan_offset+(step*(channel_width-1)))+','+height_diff,
|
||||
(chan_offset+(step*(channel_width )))+','+height
|
||||
];
|
||||
|
||||
scanCache[res.bssid].graph.text.setAttribute('x', offset_tbl[res.channel]-step);
|
||||
scanCache[res.bssid].graph.text.setAttribute('y', height_diff - 2);
|
||||
scanCache[res.bssid].graph.line.setAttribute('points', points);
|
||||
scanCache[res.bssid].graph.group.style.zIndex = res.signal*-1;
|
||||
scanCache[res.bssid].graph.group.style.opacity = res.stale ? '0.5' : null;
|
||||
})
|
||||
},
|
||||
|
||||
create_channel_graph(chan_analysis, freq_tbl, band) {
|
||||
const columns = (band != 2) ? freq_tbl.length * 4 : freq_tbl.length + 3;
|
||||
const chan_graph = chan_analysis.graph;
|
||||
const G = chan_graph.firstElementChild;
|
||||
const step = (chan_graph.offsetWidth - 2) / columns;
|
||||
let curr_offset = step;
|
||||
|
||||
function createGraphHLine(graph, pos, width, dash) {
|
||||
const elem = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
elem.setAttribute('x1', pos);
|
||||
elem.setAttribute('y1', 0);
|
||||
elem.setAttribute('x2', pos);
|
||||
elem.setAttribute('y2', '100%');
|
||||
elem.setAttribute('style', 'stroke:black;stroke-width:'+width+';stroke-dasharray:'+dash);
|
||||
graph.appendChild(elem);
|
||||
}
|
||||
|
||||
function createGraphText(graph, pos, text) {
|
||||
const elem = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
elem.setAttribute('y', 15);
|
||||
elem.setAttribute('style', 'fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000');
|
||||
elem.setAttribute('x', pos + 5);
|
||||
elem.appendChild(document.createTextNode(text));
|
||||
graph.appendChild(elem);
|
||||
}
|
||||
|
||||
chan_analysis.col_width = step;
|
||||
|
||||
createGraphHLine(G,curr_offset, 0.1, 1);
|
||||
for (let i=0; i< freq_tbl.length;i++) {
|
||||
const channel = freq_tbl[i]
|
||||
chan_analysis.offset_tbl[channel] = curr_offset+step;
|
||||
|
||||
if (band != 2) {
|
||||
createGraphHLine(G,curr_offset+step, 0.1, 3);
|
||||
if (channel < 100)
|
||||
createGraphText(G,curr_offset-(step/2), channel);
|
||||
else
|
||||
createGraphText(G,curr_offset-step, channel);
|
||||
} else {
|
||||
createGraphHLine(G,curr_offset+step, 0.1, 0);
|
||||
createGraphText(G,curr_offset+step, channel);
|
||||
}
|
||||
curr_offset += step;
|
||||
|
||||
if ((band != 2) && freq_tbl[i+1]) {
|
||||
const next_channel = freq_tbl[i+1];
|
||||
/* Check if we are transitioning to another 5/6Ghz band range */
|
||||
if ((next_channel - channel) == 4) {
|
||||
for (let j=1; j < 4; j++) {
|
||||
chan_analysis.offset_tbl[channel+j] = curr_offset+step;
|
||||
if (j == 2)
|
||||
createGraphHLine(G,curr_offset+step, 0.1, 0);
|
||||
else
|
||||
createGraphHLine(G,curr_offset+step, 0.1, 1);
|
||||
curr_offset += step;
|
||||
}
|
||||
} else {
|
||||
chan_analysis.offset_tbl[channel+1] = curr_offset+step;
|
||||
createGraphHLine(G,curr_offset+step, 0.1, 1);
|
||||
curr_offset += step;
|
||||
|
||||
chan_analysis.offset_tbl[next_channel-2] = curr_offset+step;
|
||||
createGraphHLine(G,curr_offset+step, 0.5, 0);
|
||||
curr_offset += step;
|
||||
|
||||
chan_analysis.offset_tbl[next_channel-1] = curr_offset+step;
|
||||
createGraphHLine(G,curr_offset+step, 0.1, 1);
|
||||
curr_offset += step;
|
||||
}
|
||||
}
|
||||
}
|
||||
createGraphHLine(G,curr_offset+step, 0.1, 1);
|
||||
|
||||
chan_analysis.tab.addEventListener('cbi-tab-active', L.bind(function(ev) {
|
||||
this.active_tab = ev.detail.tab;
|
||||
if (!this.radios[this.active_tab].loadedOnce)
|
||||
this.handleScanRefresh();
|
||||
}, this));
|
||||
},
|
||||
|
||||
handleScanRefresh() {
|
||||
if (!this.active_tab)
|
||||
return;
|
||||
|
||||
const radio = this.radios[this.active_tab];
|
||||
let q;
|
||||
|
||||
return this.getScanResultsForRadio(radio.dev).then(L.bind(function(results) {
|
||||
const table = radio.table;
|
||||
const chan_analysis = radio.graph;
|
||||
const scanCache = radio.scanCache;
|
||||
const band = radio.band;
|
||||
|
||||
const rows = [];
|
||||
|
||||
for (let res of results) {
|
||||
if (scanCache[res.bssid] == null)
|
||||
scanCache[res.bssid] = {};
|
||||
|
||||
scanCache[res.bssid].data = res;
|
||||
scanCache[res.bssid].data.stale = false;
|
||||
}
|
||||
|
||||
for (let k in scanCache)
|
||||
if (scanCache[k].data.stale)
|
||||
results.push(scanCache[k].data);
|
||||
|
||||
results.sort(function(a, b) {
|
||||
const channelDiff = (+a.channel || 0) - (+b.channel || 0);
|
||||
const ssidA = a.ssid || '';
|
||||
const ssidB = b.ssid || '';
|
||||
const bssidA = a.bssid || '';
|
||||
const bssidB = b.bssid || '';
|
||||
|
||||
if (channelDiff)
|
||||
return channelDiff;
|
||||
if (ssidA < ssidB)
|
||||
return -1;
|
||||
if (ssidA > ssidB)
|
||||
return 1;
|
||||
if (bssidA < bssidB)
|
||||
return -1;
|
||||
if (bssidA > bssidB)
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
for (let res of results) {
|
||||
q = getSignalPercent(res);
|
||||
const s = res.stale ? 'opacity:0.5' : '';
|
||||
const center_channels = [res.channel];
|
||||
let chan_width = 2;
|
||||
|
||||
/* Skip WiFi not supported by the current band */
|
||||
if (band != res.band)
|
||||
continue;
|
||||
if (chan_analysis.offset_tbl[res.channel] == null)
|
||||
continue;
|
||||
|
||||
res.channel_width = "20 MHz";
|
||||
if (res.ht_operation != null) {
|
||||
/* Detect 40 MHz operation by looking for the presence of
|
||||
* a secondary channel. */
|
||||
if (res.ht_operation.secondary_channel_offset == "below") {
|
||||
res.channel_width = "40 MHz";
|
||||
chan_width = 4; /* 40 MHz Channel Used */
|
||||
center_channels[0] -= 2;
|
||||
} else if (res.ht_operation.secondary_channel_offset == "above") {
|
||||
res.channel_width = "40 MHz";
|
||||
chan_width = 4; /* 40 MHz Channel Used */
|
||||
center_channels[0] += 2;
|
||||
} else {
|
||||
/* Fallback to 20 MHz due to discovery of other APs on the
|
||||
* same channel (802.11n coexistence mechanism). */
|
||||
if (res.ht_operation.channel_width == 2040)
|
||||
res.channel_width = "20 MHz (40 MHz Intolerant)";
|
||||
}
|
||||
}
|
||||
|
||||
/* if channel_width <= 40, refer to HT (above) for actual channel width,
|
||||
* as vht_operation.channel_width == 40 really only means that the used
|
||||
* bandwidth is <= 40 and could be 20 Mhz as well */
|
||||
if (res.vht_operation?.channel_width > 40) {
|
||||
center_channels[0] = res.vht_operation.center_freq_1;
|
||||
if (res.vht_operation.channel_width == 80) {
|
||||
chan_width = 8;
|
||||
res.channel_width = "80 MHz";
|
||||
|
||||
/* If needed, adjust based on the 802.11ac Wave 2 interop workaround. */
|
||||
if (res.vht_operation.center_freq_2) {
|
||||
const diff = Math.abs(res.vht_operation.center_freq_2 -
|
||||
res.vht_operation.center_freq_1);
|
||||
|
||||
if (diff == 8) {
|
||||
chan_width = 16;
|
||||
res.channel_width = "160 MHz";
|
||||
center_channels.push(res.vht_operation.center_freq_2);
|
||||
} else if (diff > 8) {
|
||||
chan_width = 8;
|
||||
res.channel_width = "80+80 MHz";
|
||||
center_channels.push(res.vht_operation.center_freq_2);
|
||||
}
|
||||
}
|
||||
} else if (res.vht_operation.channel_width == 8080) {
|
||||
res.channel_width = "80+80 MHz";
|
||||
chan_width = 8;
|
||||
center_channels.push(res.vht_operation.center_freq_2);
|
||||
} else if (res.vht_operation.channel_width == 160) {
|
||||
res.channel_width = "160 MHz";
|
||||
chan_width = 16;
|
||||
}
|
||||
}
|
||||
|
||||
if (res.he_operation?.channel_width > 20) {
|
||||
center_channels[0] = res.he_operation.center_freq_1;
|
||||
chan_width = res.he_operation.channel_width / 10;
|
||||
switch (res.he_operation.channel_width) {
|
||||
case 40:
|
||||
res.channel_width = "40 MHz";
|
||||
break;
|
||||
case 80:
|
||||
res.channel_width = "80 MHz";
|
||||
break;
|
||||
case 160:
|
||||
res.channel_width = "160 MHz";
|
||||
center_channels.push(res.he_operation.center_freq_2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (res.eht_operation?.channel_width == 320) {
|
||||
chan_width = 32;
|
||||
res.channel_width = "320 MHz";
|
||||
center_channels.push(res.eht_operation.center_freq_2);
|
||||
}
|
||||
|
||||
this.add_wifi_to_graph(chan_analysis, res, scanCache, center_channels, chan_width);
|
||||
|
||||
rows.push([
|
||||
E('span', { 'style': s }, this.render_signal_badge(q, res.signal)),
|
||||
E('span', { 'style': s }, [
|
||||
E('span', { 'style': 'color:'+scanCache[res.bssid].color }, '⬤ '),
|
||||
(res.ssid != null) ? '%h'.format(res.ssid) : E('em', _('hidden'))
|
||||
]),
|
||||
E('span', { 'style': s }, '%d'.format(res.channel)),
|
||||
E('span', { 'style': s }, '%h'.format(res.channel_width)),
|
||||
E('span', { 'style': s }, '%h'.format(res.mode)),
|
||||
E('span', { 'style': s }, '%h'.format(res.bssid))
|
||||
]);
|
||||
|
||||
scanCache[res.bssid].data.stale = true;
|
||||
}
|
||||
|
||||
cbi_update_table(table, rows);
|
||||
|
||||
if (!radio.loadedOnce) {
|
||||
radio.loadedOnce = true;
|
||||
poll.stop();
|
||||
}
|
||||
}, this))
|
||||
},
|
||||
|
||||
radios: {},
|
||||
|
||||
loadSVG(src) {
|
||||
return request.get(src).then(function(response) {
|
||||
if (!response.ok)
|
||||
throw new Error(response.statusText);
|
||||
|
||||
return E('div', {
|
||||
'id': 'channel_graph',
|
||||
'style': 'width:100%;height:400px;border:1px solid #000;background:#fff'
|
||||
}, E(response.text()));
|
||||
});
|
||||
},
|
||||
|
||||
load() {
|
||||
return Promise.all([
|
||||
this.loadSVG(L.resource('svg/channel_analysis.svg')),
|
||||
L.resolveDefault(fs.exec('/usr/sbin/iw', [ 'dev' ]), null),
|
||||
network.getWifiDevices().then(L.bind(function(data) {
|
||||
const tasks = [], ret = [];
|
||||
|
||||
for (let d of data) {
|
||||
ret[d.getName()] = { dev : d };
|
||||
|
||||
tasks.push(this.callFrequencyList(d.getName())
|
||||
.then(L.bind(function(radio, data) {
|
||||
ret[radio.getName()].freq = data;
|
||||
}, this, d)));
|
||||
}
|
||||
|
||||
return Promise.all(tasks).then(function() { return ret; })
|
||||
}, this))
|
||||
]).then((data) => {
|
||||
this.iwDevMap = parseIwDevMap(data[1]?.stdout);
|
||||
|
||||
return [ data[0], data[2] ];
|
||||
});
|
||||
},
|
||||
|
||||
render([svg, wifiDevs]) {
|
||||
const h2 = E('div', {'class' : 'cbi-title-section'}, [
|
||||
E('h2', {'class': 'cbi-title-field'}, [ _('Channel Analysis') ]),
|
||||
E('div', {'class': 'cbi-title-buttons' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-edit',
|
||||
'click': ui.createHandlerFn(this, 'handleScanRefresh')
|
||||
}, [ _('Refresh Channels') ])])
|
||||
]);
|
||||
|
||||
const tabs = E('div', {}, E('div'));
|
||||
|
||||
for (let ifname in wifiDevs) {
|
||||
const bands = {
|
||||
[2] : { title: '2.4GHz', channels: [] },
|
||||
[5] : { title: '5GHz', channels: [] },
|
||||
[6] : { title: '6GHz', channels: [] },
|
||||
};
|
||||
|
||||
/* Split FrequencyList in Bands */
|
||||
wifiDevs[ifname].freq.forEach(function(freq) {
|
||||
const band = deriveBand(freq);
|
||||
|
||||
if (bands[band])
|
||||
bands[band].channels.push(freq.channel);
|
||||
});
|
||||
|
||||
for (let band in bands) {
|
||||
if (bands[band].channels.length == 0)
|
||||
continue;
|
||||
|
||||
const csvg = svg.cloneNode(true);
|
||||
const table = E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th col-2 middle center' }, _('Signal')),
|
||||
E('th', { 'class': 'th col-4 middle left' }, _('SSID')),
|
||||
E('th', { 'class': 'th col-2 middle center hide-xs' }, _('Channel')),
|
||||
E('th', { 'class': 'th col-3 middle left' }, _('Channel Width')),
|
||||
E('th', { 'class': 'th col-2 middle left hide-xs' }, _('Mode')),
|
||||
E('th', { 'class': 'th col-3 middle left hide-xs' }, _('BSSID'))
|
||||
])
|
||||
]),
|
||||
tab = E('div', { 'data-tab': ifname+band, 'data-tab-title': ifname+' ('+bands[band].title+')' },
|
||||
[E('br'),csvg,E('br'),table,E('br')]),
|
||||
graph_data = {
|
||||
graph: csvg,
|
||||
offset_tbl: {},
|
||||
col_width: 0,
|
||||
tab: tab,
|
||||
};
|
||||
|
||||
this.radios[ifname+band] = {
|
||||
dev: wifiDevs[ifname].dev,
|
||||
band: band,
|
||||
graph: graph_data,
|
||||
table: table,
|
||||
scanCache: {},
|
||||
loadedOnce: false,
|
||||
};
|
||||
|
||||
cbi_update_table(table, [], E('em', { class: 'spinning' }, _('Starting wireless scan...')));
|
||||
|
||||
tabs.firstElementChild.appendChild(tab)
|
||||
|
||||
requestAnimationFrame(L.bind(this.create_channel_graph, this, graph_data, bands[band].channels, band));
|
||||
}
|
||||
}
|
||||
|
||||
ui.tabs.initTabGroup(tabs.firstElementChild.childNodes);
|
||||
|
||||
const activePane = Array.from(tabs.firstElementChild.childNodes).find((pane) => pane.getAttribute('data-tab-active') == 'true');
|
||||
this.active_tab = activePane?.getAttribute('data-tab') || null;
|
||||
|
||||
this.pollFn = L.bind(this.handleScanRefresh, this);
|
||||
poll.add(this.pollFn);
|
||||
|
||||
if (this.active_tab)
|
||||
poll.start();
|
||||
|
||||
return E('div', {}, [h2, tabs]);
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,556 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require poll';
|
||||
'require request';
|
||||
'require rpc';
|
||||
'require fs';
|
||||
|
||||
const callLuciRealtimeStats = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getRealtimeStats',
|
||||
params: [ 'mode', 'device' ],
|
||||
expect: { result: [] }
|
||||
});
|
||||
|
||||
const callLuciConntrackList = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getConntrackList',
|
||||
expect: { result: [] }
|
||||
});
|
||||
|
||||
const callNetworkRrdnsLookup = rpc.declare({
|
||||
object: 'network.rrdns',
|
||||
method: 'lookup',
|
||||
params: [ 'addrs', 'timeout', 'limit' ],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callLuciRpcGetHostHints = rpc.declare({
|
||||
object: 'luci-rpc',
|
||||
method: 'getHostHints',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callLuciRpcGetNetworkDevices = rpc.declare({
|
||||
object: 'luci-rpc',
|
||||
method: 'getNetworkDevices',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callLuciRpcGetDHCPLeases = rpc.declare({
|
||||
object: 'luci-rpc',
|
||||
method: 'getDHCPLeases',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const graphPolls = [];
|
||||
const pollInterval = 3;
|
||||
const dns_cache = Object.create(null);
|
||||
const service_cache = Object.create(null);
|
||||
const ethers_cache = Object.create(null);
|
||||
let ethers_cache_is_loaded = false;
|
||||
let enableLookups = false;
|
||||
let filterText = '';
|
||||
|
||||
const recheck_lookup_queue = {};
|
||||
|
||||
Math.log2 = Math.log2 || function(x) { return Math.log(x) * Math.LOG2E; };
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
return Promise.all([
|
||||
this.loadSVG(L.resource('svg/connections.svg'))
|
||||
]);
|
||||
},
|
||||
|
||||
updateGraph(svg, lines, cb) {
|
||||
const G = svg.firstElementChild;
|
||||
|
||||
const view = document.querySelector('#view');
|
||||
|
||||
const width = view.offsetWidth - 2;
|
||||
const height = 300 - 2;
|
||||
const step = 5;
|
||||
|
||||
const data_wanted = Math.floor(width / step);
|
||||
|
||||
const data_values = [];
|
||||
|
||||
for (let line of lines)
|
||||
if (line != null)
|
||||
data_values.push([]);
|
||||
|
||||
const info = {
|
||||
line_current: [],
|
||||
line_average: [],
|
||||
line_peak: []
|
||||
};
|
||||
|
||||
/* prefill datasets */
|
||||
for (let dv of data_values)
|
||||
for (let j = 0; j < data_wanted; j++)
|
||||
dv[j] = 0;
|
||||
|
||||
/* plot horizontal time interval lines */
|
||||
for (let i = width % (step * 60); i < width; i += step * 60) {
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
line.setAttribute('x1', i);
|
||||
line.setAttribute('y1', 0);
|
||||
line.setAttribute('x2', i);
|
||||
line.setAttribute('y2', '100%');
|
||||
line.setAttribute('style', 'stroke:black;stroke-width:0.1');
|
||||
|
||||
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
text.setAttribute('x', i + 5);
|
||||
text.setAttribute('y', 15);
|
||||
text.setAttribute('style', 'fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000');
|
||||
text.appendChild(document.createTextNode(Math.round((width - i) / step / 60) + 'm'));
|
||||
|
||||
G.appendChild(line);
|
||||
G.appendChild(text);
|
||||
}
|
||||
|
||||
info.interval = pollInterval;
|
||||
info.timeframe = data_wanted / 60;
|
||||
|
||||
graphPolls.push({
|
||||
svg: svg,
|
||||
lines: lines,
|
||||
cb: cb,
|
||||
info: info,
|
||||
width: width,
|
||||
height: height,
|
||||
step: step,
|
||||
values: data_values,
|
||||
timestamp: 0,
|
||||
fill: 1
|
||||
});
|
||||
},
|
||||
|
||||
async updateConntrack(conn) {
|
||||
async function fetchServices() {
|
||||
if (!enableLookups) return;
|
||||
if (Object.keys(service_cache).length > 0) return;
|
||||
|
||||
try {
|
||||
const rawData = await fs.read('/etc/services');
|
||||
const lines = rawData.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^([\w-]+)\s+(\d+)\/(\w+)/);
|
||||
if (match) {
|
||||
const service = match[1].toUpperCase();
|
||||
const port = match[2];
|
||||
const protocol = match[3].toUpperCase();
|
||||
if (!(port in service_cache)) service_cache[port] = {};
|
||||
service_cache[port][protocol] = service;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching services:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function joinAddressWithPortOrServiceName(address, port, protocol) {
|
||||
if (!port) return address;
|
||||
if (enableLookups) {
|
||||
const service = service_cache[port]?.[protocol];
|
||||
if (service) return `${address}:${service}`;
|
||||
}
|
||||
return `${address}:${port}`;
|
||||
}
|
||||
|
||||
await fetchServices();
|
||||
|
||||
const lookup_queue = new Set();
|
||||
const rows = [];
|
||||
|
||||
conn.sort((a, b) => b.bytes - a.bytes);
|
||||
|
||||
for (const c of conn) {
|
||||
if ((c.src === '127.0.0.1' && c.dst === '127.0.0.1') ||
|
||||
(c.src === '::1' && c.dst === '::1'))
|
||||
continue;
|
||||
|
||||
if (enableLookups) {
|
||||
if (!(c.src in dns_cache)) lookup_queue.add(c.src);
|
||||
if (!(c.dst in dns_cache)) lookup_queue.add(c.dst);
|
||||
}
|
||||
|
||||
const src = enableLookups && (c.src in dns_cache) ? dns_cache[c.src] : (c.layer3 === 'ipv6' ? `[${c.src}]` : c.src);
|
||||
const dst = enableLookups && (c.dst in dns_cache) ? dns_cache[c.dst] : (c.layer3 === 'ipv6' ? `[${c.dst}]` : c.dst);
|
||||
const network = c.layer3.toUpperCase();
|
||||
const protocol = c.layer4.toUpperCase();
|
||||
const source = '%h'.format(joinAddressWithPortOrServiceName(src, c.sport, protocol));
|
||||
const destination = '%h'.format(joinAddressWithPortOrServiceName(dst, c.dport, protocol));
|
||||
const transfer = [c.bytes, '%1024.2mB (%d %s)'.format(c.bytes, c.packets, _('Pkts.'))];
|
||||
|
||||
if (filterText) {
|
||||
const filterTextExpressions = filterText.split(' ');
|
||||
if (filterTextExpressions.some(el => el.toUpperCase() !== network && el.toUpperCase() !== protocol
|
||||
&& !c.src.includes(el) && !source.includes(el)
|
||||
&& !c.dst.includes(el) && !destination.includes(el))) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
rows.push([network, protocol, source, destination, transfer]);
|
||||
}
|
||||
|
||||
cbi_update_table('#connections', rows, E('em', _('No information available')));
|
||||
|
||||
if (!enableLookups || lookup_queue.size === 0) return;
|
||||
|
||||
const reduced_lookup_queue = lookup_queue.size > 100
|
||||
? new Set([...lookup_queue].slice(0, 100))
|
||||
: new Set(lookup_queue);
|
||||
|
||||
async function softFailure(fn) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (err) {
|
||||
console.debug('Lookup failed:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function updateDnsCache(addr, name) {
|
||||
if (!addr || !name) return;
|
||||
if (!(addr in dns_cache)) dns_cache[addr] = name;
|
||||
lookup_queue.delete(addr);
|
||||
reduced_lookup_queue.delete(addr);
|
||||
}
|
||||
|
||||
// 1. DHCP Leases
|
||||
const leases = await softFailure(() => callLuciRpcGetDHCPLeases());
|
||||
if (leases && Object.keys(leases).length !== 0) {
|
||||
for (const lease of [...(leases.dhcp_leases || []), ...(leases.dhcp6_leases || [])]) {
|
||||
const addr = lease.ipaddr || lease.ip6addr;
|
||||
updateDnsCache(addr, lease.hostname);
|
||||
}
|
||||
}
|
||||
|
||||
if (reduced_lookup_queue.size > 0) {
|
||||
// 2. Reverse DNS Lookup
|
||||
const dnsReplies = await softFailure(() => callNetworkRrdnsLookup([...reduced_lookup_queue], 5000, 1000));
|
||||
if (dnsReplies && Object.keys(dnsReplies).length !== 0) {
|
||||
for (const addr of Object.keys(dnsReplies)) updateDnsCache(addr, dnsReplies[addr]);
|
||||
}
|
||||
}
|
||||
|
||||
if (reduced_lookup_queue.size > 0) {
|
||||
// 3. Resolve names via hints
|
||||
const hints = ethers_cache_is_loaded ? ethers_cache : await softFailure(() => callLuciRpcGetHostHints());
|
||||
|
||||
if (hints && Object.keys(hints).length !== 0) {
|
||||
for (const [ether, obj] of Object.entries(hints)) {
|
||||
if (!ether || !obj?.name) continue;
|
||||
if (!(ether in ethers_cache) && Object.keys(obj).length !== 0) ethers_cache[ether] = obj;
|
||||
for (const addr of [...(obj.ipaddrs || []), ...(obj.ip6addrs || [])])
|
||||
updateDnsCache(addr, obj.name);
|
||||
}
|
||||
if (Object.keys(ethers_cache).length) ethers_cache_is_loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (reduced_lookup_queue.size > 0) {
|
||||
// 4. Network devices
|
||||
const devices = await softFailure(() => callLuciRpcGetNetworkDevices());
|
||||
if (devices) {
|
||||
for (const device of Object.values(devices)) {
|
||||
if (!device.mac) continue;
|
||||
const name = ethers_cache[device.mac]?.name;
|
||||
if (!name) continue;
|
||||
for (const item of [...(device.ipaddrs || []), ...(device.ip6addrs || [])])
|
||||
updateDnsCache(item.address, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final cleanup for unresolved addresses
|
||||
for (const address of reduced_lookup_queue)
|
||||
if ((recheck_lookup_queue[address] || 0) > 2)
|
||||
dns_cache[address] = address.includes(':') ? `[${address}]` : address;
|
||||
else
|
||||
recheck_lookup_queue[address] = (recheck_lookup_queue[address] || 0) + 1;
|
||||
|
||||
const btn = document.querySelector('.btn.toggle-lookups');
|
||||
if (btn) {
|
||||
btn.firstChild.data = enableLookups ? _('Disable DNS lookups') : _('Enable DNS lookups');
|
||||
btn.classList.remove('spinning');
|
||||
btn.disabled = false;
|
||||
}
|
||||
},
|
||||
|
||||
async pollData() {
|
||||
poll.add(L.bind(async function() {
|
||||
const tasks = [
|
||||
L.resolveDefault(callLuciConntrackList(), [])
|
||||
];
|
||||
|
||||
graphPolls.forEach(() => {
|
||||
tasks.push(L.resolveDefault(callLuciRealtimeStats('conntrack'), []));
|
||||
});
|
||||
|
||||
const datasets = await Promise.all(tasks);
|
||||
await this.updateConntrack(datasets[0]);
|
||||
for (let gi = 0; gi < graphPolls.length; gi++) {
|
||||
const ctx = graphPolls[gi];
|
||||
const data = datasets[gi + 1];
|
||||
const values = ctx.values;
|
||||
const lines = ctx.lines;
|
||||
const info = ctx.info;
|
||||
|
||||
let data_scale = 0;
|
||||
let data_wanted = Math.floor(ctx.width / ctx.step);
|
||||
let last_timestamp = NaN;
|
||||
|
||||
for (let i = 0, di = 0; di < lines.length; di++) {
|
||||
if (lines[di] == null)
|
||||
continue;
|
||||
|
||||
const multiply = (lines[di].multiply != null) ? lines[di].multiply : 1;
|
||||
const offset = (lines[di].offset != null) ? lines[di].offset : 0;
|
||||
|
||||
for (let j = ctx.timestamp ? 0 : 1; j < data.length; j++) {
|
||||
/* skip overlapping entries */
|
||||
if (data[j][0] <= ctx.timestamp)
|
||||
continue;
|
||||
|
||||
if (i == 0) {
|
||||
ctx.fill++;
|
||||
last_timestamp = data[j][0];
|
||||
}
|
||||
|
||||
info.line_current[i] = data[j][di + 1] * multiply;
|
||||
info.line_current[i] -= Math.min(info.line_current[i], offset);
|
||||
values[i].push(info.line_current[i]);
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
/* cut off outdated entries */
|
||||
ctx.fill = Math.min(ctx.fill, data_wanted);
|
||||
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const len = values[i].length;
|
||||
values[i] = values[i].slice(len - data_wanted, len);
|
||||
|
||||
/* find peaks, averages */
|
||||
info.line_peak[i] = NaN;
|
||||
info.line_average[i] = 0;
|
||||
|
||||
for (let j = 0; j < values[i].length; j++) {
|
||||
info.line_peak[i] = isNaN(info.line_peak[i]) ? values[i][j] : Math.max(info.line_peak[i], values[i][j]);
|
||||
info.line_average[i] += values[i][j];
|
||||
}
|
||||
|
||||
info.line_average[i] = info.line_average[i] / ctx.fill;
|
||||
}
|
||||
|
||||
info.peak = Math.max.apply(Math, info.line_peak);
|
||||
|
||||
/* remember current timestamp, calculate horizontal scale */
|
||||
if (!isNaN(last_timestamp))
|
||||
ctx.timestamp = last_timestamp;
|
||||
|
||||
const size = Math.floor(Math.log2(info.peak));
|
||||
const div = Math.pow(2, size - (size % 10));
|
||||
let mult = info.peak / div;
|
||||
mult = (mult < 5) ? 2 : ((mult < 50) ? 10 : ((mult < 500) ? 100 : 1000));
|
||||
|
||||
info.peak = info.peak + (mult * div) - (info.peak % (mult * div));
|
||||
|
||||
data_scale = ctx.height / info.peak;
|
||||
|
||||
/* plot data */
|
||||
for (let i = 0, di = 0; di < lines.length; di++) {
|
||||
if (lines[di] == null)
|
||||
continue;
|
||||
|
||||
const el = ctx.svg.firstElementChild.getElementById(lines[di].line);
|
||||
let pt = '0,' + ctx.height;
|
||||
let y = 0;
|
||||
|
||||
if (!el)
|
||||
continue;
|
||||
|
||||
for (let j = 0; j < values[i].length; j++) {
|
||||
let x = j * ctx.step;
|
||||
|
||||
y = ctx.height - Math.floor(values[i][j] * data_scale);
|
||||
//y -= Math.floor(y % (1 / data_scale));
|
||||
|
||||
y = isNaN(y) ? ctx.height : y;
|
||||
|
||||
pt += ' ' + x + ',' + y;
|
||||
}
|
||||
|
||||
pt += ' ' + ctx.width + ',' + y + ' ' + ctx.width + ',' + ctx.height;
|
||||
|
||||
el.setAttribute('points', pt);
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
info.label_25 = 0.25 * info.peak;
|
||||
info.label_50 = 0.50 * info.peak;
|
||||
info.label_75 = 0.75 * info.peak;
|
||||
|
||||
if (typeof(ctx.cb) == 'function')
|
||||
ctx.cb(ctx.svg, info);
|
||||
}
|
||||
}, this), pollInterval);
|
||||
},
|
||||
|
||||
loadSVG(src) {
|
||||
return request.get(src).then(function(response) {
|
||||
if (!response.ok)
|
||||
throw new Error(response.statusText);
|
||||
|
||||
return E('div', {
|
||||
'style': 'width:100%;height:300px;border:1px solid #000;background:#fff'
|
||||
}, E(response.text()));
|
||||
});
|
||||
},
|
||||
|
||||
render([svg]) {
|
||||
|
||||
const v = E('div', { 'class': 'cbi-map', 'id': 'map' }, [
|
||||
E('h2', _('Connections')),
|
||||
E('div', {'class': 'cbi-map-descr'}, _('This page displays the active connections via this device.')),
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
svg,
|
||||
E('div', { 'class': 'right' }, E('small', { 'id': 'scale' }, '-')),
|
||||
E('br'),
|
||||
|
||||
E('table', { 'class': 'table', 'style': 'width:100%;table-layout:fixed' }, [
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td right top' }, E('strong', { 'style': 'border-bottom:2px solid blue' }, [ _('UDP:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'lb_udp_cur' }, [ '0' ]),
|
||||
|
||||
E('td', { 'class': 'td right top' }, E('strong', {}, [ _('Average:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'lb_udp_avg' }, [ '0' ]),
|
||||
|
||||
E('td', { 'class': 'td right top' }, E('strong', {}, [ _('Peak:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'lb_udp_peak' }, [ '0' ])
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td right top' }, E('strong', { 'style': 'border-bottom:2px solid green' }, [ _('TCP:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'lb_tcp_cur' }, [ '0' ]),
|
||||
|
||||
E('td', { 'class': 'td right top' }, E('strong', {}, [ _('Average:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'lb_tcp_avg' }, [ '0' ]),
|
||||
|
||||
E('td', { 'class': 'td right top' }, E('strong', {}, [ _('Peak:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'lb_tcp_peak' }, [ '0' ])
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td right top' }, E('strong', { 'style': 'border-bottom:2px solid red' }, [ _('Other:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'lb_otr_cur' }, [ '0' ]),
|
||||
|
||||
E('td', { 'class': 'td right top' }, E('strong', {}, [ _('Average:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'lb_otr_avg' }, [ '0' ]),
|
||||
|
||||
E('td', { 'class': 'td right top' }, E('strong', {}, [ _('Peak:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'lb_otr_peak' }, [ '0' ])
|
||||
])
|
||||
]),
|
||||
|
||||
E('br'),
|
||||
|
||||
E('div', {
|
||||
id: 'settings-row',
|
||||
style: 'display: flex',
|
||||
}, [
|
||||
E('span', {
|
||||
class: 'filter',
|
||||
style: 'flex: auto',
|
||||
}, [
|
||||
E('input', {
|
||||
id: 'filter-connections',
|
||||
type: 'text',
|
||||
placeholder: '192.168.1.15 / UDP 1.15 / :443',
|
||||
class: 'cbi-input-text',
|
||||
style: 'width: 100%',
|
||||
keyup: function(ev) {
|
||||
filterText = this.value;
|
||||
}
|
||||
})
|
||||
]),
|
||||
E('span', {
|
||||
class: 'right',
|
||||
style: 'flex: auto',
|
||||
}, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-apply toggle-lookups',
|
||||
'click': function(ev) {
|
||||
if (!enableLookups) {
|
||||
ev.currentTarget.classList.add('spinning');
|
||||
ev.currentTarget.disabled = true;
|
||||
enableLookups = true;
|
||||
}
|
||||
else {
|
||||
ev.currentTarget.firstChild.data = _('Enable DNS lookups');
|
||||
enableLookups = false;
|
||||
}
|
||||
|
||||
this.blur();
|
||||
}
|
||||
}, [ enableLookups ? _('Disable DNS lookups') : _('Enable DNS lookups') ])
|
||||
]),
|
||||
]),
|
||||
|
||||
E('br'),
|
||||
|
||||
E('div', { 'class': 'cbi-section-node' }, [
|
||||
E('table', { 'class': 'table', 'id': 'connections' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th col-2 hide-xs' }, [ _('Network') ]),
|
||||
E('th', { 'class': 'th col-2' }, [ _('Protocol') ]),
|
||||
E('th', { 'class': 'th col-7' }, [ _('Source') ]),
|
||||
E('th', { 'class': 'th col-7' }, [ _('Destination') ]),
|
||||
E('th', { 'class': 'th col-4' }, [ _('Transfer') ])
|
||||
]),
|
||||
E('tr', { 'class': 'tr placeholder' }, [
|
||||
E('td', { 'class': 'td' }, [
|
||||
E('em', {}, [ _('Collecting data...') ])
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
this.updateGraph(svg, [ { line: 'udp' }, { line: 'tcp' }, { line: 'other' } ], function(svg, info) {
|
||||
const G = svg.firstElementChild, tab = svg.parentNode;
|
||||
|
||||
G.getElementById('label_25').firstChild.data = '%d'.format(info.label_25);
|
||||
G.getElementById('label_50').firstChild.data = '%d'.format(info.label_50);
|
||||
G.getElementById('label_75').firstChild.data = '%d'.format(info.label_75);
|
||||
|
||||
tab.querySelector('#scale').firstChild.data = _('(%d minute window, %d second interval)').format(info.timeframe, info.interval);
|
||||
|
||||
tab.querySelector('#lb_udp_cur').firstChild.data = '%d'.format(info.line_current[0]);
|
||||
tab.querySelector('#lb_udp_avg').firstChild.data = '%d'.format(info.line_average[0]);
|
||||
tab.querySelector('#lb_udp_peak').firstChild.data = '%d'.format(info.line_peak[0]);
|
||||
|
||||
tab.querySelector('#lb_tcp_cur').firstChild.data = '%d'.format(info.line_current[1]);
|
||||
tab.querySelector('#lb_tcp_avg').firstChild.data = '%d'.format(info.line_average[1]);
|
||||
tab.querySelector('#lb_tcp_peak').firstChild.data = '%d'.format(info.line_peak[1]);
|
||||
|
||||
tab.querySelector('#lb_otr_cur').firstChild.data = '%d'.format(info.line_current[2]);
|
||||
tab.querySelector('#lb_otr_avg').firstChild.data = '%d'.format(info.line_average[2]);
|
||||
tab.querySelector('#lb_otr_peak').firstChild.data = '%d'.format(info.line_peak[2]);
|
||||
});
|
||||
|
||||
this.pollData();
|
||||
|
||||
return v;
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,296 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require fs';
|
||||
'require poll';
|
||||
'require ui';
|
||||
|
||||
return view.extend({
|
||||
logFilterFrom: '0',
|
||||
logFilterTo: '',
|
||||
invertLogRangeFilter: false,
|
||||
|
||||
minSeverity: '',
|
||||
invertMinSeverity: false,
|
||||
|
||||
sortLogsDescending: false,
|
||||
|
||||
logTextFilter: '',
|
||||
invertLogTextSearch: false,
|
||||
|
||||
severity: [
|
||||
['', 'KERN_DEFAULT', _('Default')],
|
||||
// ['0', 'KERN_EMERG'], // unusable kernels tend to halt ergo no running system
|
||||
['1', 'KERN_ALERT', _('Alert')],
|
||||
['2', 'KERN_CRIT', _('Critical')],
|
||||
['3', 'KERN_ERR', _('Error')],
|
||||
['4', 'KERN_WARNING', _('Warning')],
|
||||
['5', 'KERN_NOTICE', _('Notice')],
|
||||
['6', 'KERN_INFO', _('Info')],
|
||||
['7', 'KERN_DEBUG', _('Debug')],
|
||||
// ['c', 'KERN_CONT'], // for follow-on printed lines lacking newline
|
||||
/*
|
||||
As of 24.10 there appear to be kernel log lines printed with severity 14-15
|
||||
which seems like a bug in ubox. So we must structure the filter in an
|
||||
'at least' fashion to include those.
|
||||
*/
|
||||
],
|
||||
|
||||
async retrieveLog() {
|
||||
return fs.exec_direct('/bin/dmesg', [ '-r' ]).then(logdata => {
|
||||
let loglines = [];
|
||||
let lastSeverity = null;
|
||||
|
||||
logdata.trim().split(/\n/).forEach(line => {
|
||||
const priorityMatch = line.match(/^<(\w+)>/);
|
||||
if (!priorityMatch) return;
|
||||
|
||||
const tag = priorityMatch[1];
|
||||
const isCont = tag === 'c';
|
||||
const cleanLine = line.replace(/^<\w+>/, '');
|
||||
const timeMatch = cleanLine.match(/^\[\s*(\d+(?:\.\d+)?)\]/);
|
||||
const time = timeMatch ? parseFloat(timeMatch[1]) : null;
|
||||
|
||||
if (!isCont) {
|
||||
lastSeverity = parseInt(tag, 10); // update severity
|
||||
}
|
||||
|
||||
loglines.push({
|
||||
severity: isCont ? lastSeverity : parseInt(tag, 10),
|
||||
isCont,
|
||||
time,
|
||||
text: cleanLine
|
||||
});
|
||||
});
|
||||
|
||||
// Filter by time
|
||||
const hasStart = this.logFilterFrom;
|
||||
const hasEnd = this.logFilterTo;
|
||||
|
||||
if (hasStart || hasEnd) {
|
||||
loglines = loglines.filter(({ time }) => {
|
||||
if (time == null) return false;
|
||||
|
||||
let inRange = true;
|
||||
if (hasStart && hasEnd)
|
||||
inRange = time >= this.logFilterFrom && time <= this.logFilterTo;
|
||||
else if (hasStart)
|
||||
inRange = time >= this.logFilterFrom;
|
||||
else if (hasEnd)
|
||||
inRange = time <= this.logFilterTo;
|
||||
|
||||
return this.invertLogRangeFilter ? !inRange : inRange;
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by severity
|
||||
loglines = loglines.filter(entry => {
|
||||
if (!entry.isCont) {
|
||||
if (!this.invertMinSeverity)
|
||||
return (entry.severity >= this.minSeverity);
|
||||
else
|
||||
return (entry.severity < this.minSeverity);
|
||||
}
|
||||
});
|
||||
|
||||
// Filter by text
|
||||
if (this.logTextFilter) {
|
||||
loglines = loglines.filter(({ text }) => {
|
||||
const match = text.includes(this.logTextFilter);
|
||||
return this.invertLogTextSearch ? !match : match;
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by time
|
||||
if (this.sortLogsDescending) loglines.reverse();
|
||||
|
||||
return {
|
||||
value: loglines.map(l => l.text).join('\n'),
|
||||
rows: loglines.length + 1
|
||||
};
|
||||
}).catch(function(err) {
|
||||
ui.addNotification(null, E('p', {}, _('Unable to load log data: ' + err.message)));
|
||||
return '';
|
||||
});
|
||||
},
|
||||
|
||||
async pollLog() {
|
||||
const element = document.getElementById('syslog');
|
||||
if (element) {
|
||||
const log = await this.retrieveLog();
|
||||
element.value = log.value;
|
||||
element.rows = log.rows;
|
||||
}
|
||||
},
|
||||
|
||||
async load() {
|
||||
poll.add(this.pollLog.bind(this));
|
||||
return await this.retrieveLog();
|
||||
},
|
||||
|
||||
render(loglines) {
|
||||
const scrollDownButton = E('button', {
|
||||
'id': 'scrollDownButton',
|
||||
'class': 'cbi-button cbi-button-neutral',
|
||||
}, _('Scroll to tail', 'scroll to bottom (the tail) of the log file')
|
||||
);
|
||||
scrollDownButton.addEventListener('click', () => {
|
||||
scrollUpButton.scrollIntoView();
|
||||
scrollDownButton.blur();
|
||||
});
|
||||
|
||||
const scrollUpButton = E('button', {
|
||||
'id' : 'scrollUpButton',
|
||||
'class': 'cbi-button cbi-button-neutral',
|
||||
}, _('Scroll to head', 'scroll to top (the head) of the log file')
|
||||
);
|
||||
scrollUpButton.addEventListener('click', () => {
|
||||
scrollDownButton.scrollIntoView();
|
||||
scrollUpButton.blur();
|
||||
});
|
||||
|
||||
const self = this;
|
||||
|
||||
|
||||
// Create range invert checkbox
|
||||
const rangeTimeInvert = E('input', {
|
||||
'id': 'invertLogRangeTime',
|
||||
'type': 'checkbox',
|
||||
'class': 'cbi-input-checkbox',
|
||||
});
|
||||
|
||||
// Create from time filter
|
||||
const fromTimeFilter = E('input', {
|
||||
'id': 'logFromTime',
|
||||
'class': 'cbi-input-text',
|
||||
'style': 'margin-bottom:10px',
|
||||
'type': 'number',
|
||||
'min': '0',
|
||||
'step': '0.1',
|
||||
'placeholder': '0.000000',
|
||||
});
|
||||
|
||||
// Create to time filter
|
||||
const toTimeFilter = E('input', {
|
||||
'id': 'logToTime',
|
||||
'class': 'cbi-input-text',
|
||||
'style': 'margin-bottom:10px',
|
||||
'type': 'number',
|
||||
'min': '0',
|
||||
'step': '0.1',
|
||||
'placeholder': '0.000000',
|
||||
});
|
||||
|
||||
|
||||
// Create range invert checkbox
|
||||
const severityInvert = E('input', {
|
||||
'id': 'invertSeverity',
|
||||
'type': 'checkbox',
|
||||
'class': 'cbi-input-checkbox',
|
||||
});
|
||||
|
||||
// Create severity select-dropdown from severity map
|
||||
const severitySelect = E('select', {
|
||||
'id': 'logSeveritySelect',
|
||||
'class': 'cbi-input-select',
|
||||
},
|
||||
this.severity.map(([val, , label]) =>
|
||||
E('option', { value: val }, label)
|
||||
));
|
||||
|
||||
// Create range invert checkbox
|
||||
const descendingSort = E('input', {
|
||||
'id': 'invertAscendingSort',
|
||||
'type': 'checkbox',
|
||||
'class': 'cbi-input-checkbox',
|
||||
});
|
||||
|
||||
// Create raw text search invert checkbox
|
||||
const filterTextInvert = E('input', {
|
||||
'id': 'invertLogTextSearch',
|
||||
'type': 'checkbox',
|
||||
'class': 'cbi-input-checkbox',
|
||||
});
|
||||
|
||||
// Create raw text search text input
|
||||
const filterTextInput = E('input', {
|
||||
'id': 'logTextFilter',
|
||||
'class': 'cbi-input-text',
|
||||
});
|
||||
|
||||
function handleLogFilterChange() {
|
||||
// time
|
||||
self.invertLogRangeFilter = rangeTimeInvert.checked;
|
||||
self.logFilterFrom = fromTimeFilter.value;
|
||||
self.logFilterTo = toTimeFilter.value;
|
||||
|
||||
// severity
|
||||
self.minSeverity = severitySelect.value;
|
||||
self.invertMinSeverity = severityInvert.checked;
|
||||
|
||||
// sort
|
||||
self.sortLogsDescending = descendingSort.checked;
|
||||
|
||||
// text
|
||||
self.logTextFilter = filterTextInput.value;
|
||||
self.invertLogTextSearch = filterTextInvert.checked;
|
||||
self.pollLog();
|
||||
}
|
||||
|
||||
// time
|
||||
rangeTimeInvert.addEventListener('change', handleLogFilterChange);
|
||||
fromTimeFilter.addEventListener('input', handleLogFilterChange);
|
||||
toTimeFilter.addEventListener('input', handleLogFilterChange);
|
||||
// severity
|
||||
severitySelect.addEventListener('change', handleLogFilterChange);
|
||||
severityInvert.addEventListener('change', handleLogFilterChange);
|
||||
// sort
|
||||
descendingSort.addEventListener('change', handleLogFilterChange);
|
||||
// text
|
||||
filterTextInput.addEventListener('input', handleLogFilterChange);
|
||||
filterTextInvert.addEventListener('change', handleLogFilterChange);
|
||||
|
||||
return E([], [
|
||||
E('h2', {}, [ _('Kernel Log') ]),
|
||||
E('div', { 'id': 'content_syslog' }, [
|
||||
E('div', { 'style': 'margin-bottom:10px' }, [
|
||||
E('label', { 'for': 'invertLogFacilitySearch', 'style': 'margin-right:5px' }, _('Not')),
|
||||
rangeTimeInvert,
|
||||
E('label', { 'for': 'logFacilitySelect', 'style': 'margin: 0 5px' }, _('between:')),
|
||||
fromTimeFilter,
|
||||
E('label', { 'for': 'logSeveritySelect', 'style': 'margin: 0 5px' }, _('and:')),
|
||||
toTimeFilter,
|
||||
]),
|
||||
E('div', { 'style': 'margin-bottom:10px' }, [
|
||||
E('label', { 'for': 'invertLogSeveritySearch', 'style': 'margin-right:5px' }, _('Not')),
|
||||
severityInvert,
|
||||
'\xa0',
|
||||
severitySelect,
|
||||
E('label', { 'for': 'logSeveritySelect', 'style': 'margin: 0 5px' }, _('and above')),
|
||||
]),
|
||||
E('div', { 'style': 'margin-bottom:10px' }, [
|
||||
E('label', { 'for': 'invertAscendingSort', 'style': 'margin-right:5px' }, _('Reverse sort')),
|
||||
descendingSort,
|
||||
]),
|
||||
E('div', { 'style': 'margin-bottom:10px' }, [
|
||||
E('label', { 'for': 'invertLogTextSearch', 'style': 'margin-right:5px' }, _('Not')),
|
||||
filterTextInvert,
|
||||
E('label', { 'for': 'logTextFilter', 'style': 'margin: 0 5px' }, _('including:')),
|
||||
filterTextInput,
|
||||
]),
|
||||
E('div', {'style': 'padding-bottom: 20px'}, [scrollDownButton]),
|
||||
E('textarea', {
|
||||
'id': 'syslog',
|
||||
'style': 'font-size:12px',
|
||||
'readonly': 'readonly',
|
||||
'wrap': 'off',
|
||||
'rows': loglines.rows
|
||||
}, [ loglines.value ]),
|
||||
E('div', {'style': 'padding-bottom: 20px'}, [scrollUpButton])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require rpc';
|
||||
|
||||
var callGetUnixtime = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getUnixtime',
|
||||
expect: { result: 0 }
|
||||
});
|
||||
|
||||
var callLuciVersion = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getVersion'
|
||||
});
|
||||
|
||||
var callSystemBoard = rpc.declare({
|
||||
object: 'system',
|
||||
method: 'board'
|
||||
});
|
||||
|
||||
var callSystemInfo = rpc.declare({
|
||||
object: 'system',
|
||||
method: 'info'
|
||||
});
|
||||
|
||||
var callCPUBench = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getCPUBench'
|
||||
});
|
||||
|
||||
var callCPUInfo = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getCPUInfo'
|
||||
});
|
||||
|
||||
var callTempInfo = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getTempInfo'
|
||||
});
|
||||
|
||||
return baseclass.extend({
|
||||
title: _('System'),
|
||||
disableCache: true,
|
||||
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
L.resolveDefault(callSystemBoard(), {}),
|
||||
L.resolveDefault(callSystemInfo(), {}),
|
||||
L.resolveDefault(callCPUBench(), {}),
|
||||
L.resolveDefault(callCPUInfo(), {}),
|
||||
L.resolveDefault(callTempInfo(), {}),
|
||||
L.resolveDefault(callLuciVersion(), { revision: _('unknown version'), branch: 'LuCI' }),
|
||||
L.resolveDefault(callGetUnixtime(), 0)
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var boardinfo = data[0],
|
||||
systeminfo = data[1],
|
||||
cpubench = data[2],
|
||||
cpuinfo = data[3],
|
||||
tempinfo = data[4],
|
||||
luciversion = data[5],
|
||||
unixtime = data[6];
|
||||
|
||||
luciversion = luciversion.branch;
|
||||
|
||||
var datestr = null;
|
||||
|
||||
if (unixtime) {
|
||||
var date = new Date(unixtime * 1000);
|
||||
|
||||
datestr = new Intl.DateTimeFormat(undefined, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'long'
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
var fields = [
|
||||
_('Hostname'), boardinfo.hostname,
|
||||
_('Architecture'), (cpuinfo.cpuinfo || boardinfo.system) + ' ' + cpubench.cpubench,
|
||||
_('Target Platform'), (L.isObject(boardinfo.release) ? boardinfo.release.target : ''),
|
||||
_('Firmware Version'), (L.isObject(boardinfo.release)
|
||||
? '%s%s / '.format(
|
||||
boardinfo.release.description || '',
|
||||
boardinfo.release.revision ? boardinfo.release.revision : ''
|
||||
)
|
||||
: '') + (luciversion || ''),
|
||||
_('Kernel Version'), boardinfo.kernel,
|
||||
_('Local Time'), datestr,
|
||||
_('Uptime'), systeminfo.uptime ? '%t'.format(systeminfo.uptime) : null,
|
||||
_('Load Average'), Array.isArray(systeminfo.load) ? '%.2f, %.2f, %.2f'.format(
|
||||
systeminfo.load[0] / 65535.0,
|
||||
systeminfo.load[1] / 65535.0,
|
||||
systeminfo.load[2] / 65535.0
|
||||
) : null
|
||||
];
|
||||
|
||||
if (tempinfo.tempinfo) {
|
||||
fields.splice(6, 0, _('Temperature'));
|
||||
fields.splice(7, 0, tempinfo.tempinfo);
|
||||
}
|
||||
if (boardinfo.model != "Default string Default string") {
|
||||
fields.splice(2, 0, _('Model'));
|
||||
fields.splice(3, 0, boardinfo.model);
|
||||
}
|
||||
|
||||
var table = E('table', { 'class': 'table' });
|
||||
|
||||
for (var i = 0; i < fields.length; i += 2) {
|
||||
table.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'width': '33%' }, [ fields[i] ]),
|
||||
E('td', { 'class': 'td left' }, [ (fields[i + 1] != null) ? fields[i + 1] : '?' ])
|
||||
]));
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,777 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require fs';
|
||||
'require ui';
|
||||
'require uci';
|
||||
'require rpc';
|
||||
'require network';
|
||||
'require firewall';
|
||||
|
||||
const callGetBuiltinEthernetPorts = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getBuiltinEthernetPorts',
|
||||
expect: { result: [] }
|
||||
});
|
||||
|
||||
const callSwconfigPortState = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getSwconfigPortState',
|
||||
params: [ 'switch' ],
|
||||
expect: { result: [] }
|
||||
});
|
||||
|
||||
const callNetworkDeviceStatus = rpc.declare({
|
||||
object: 'network.device',
|
||||
method: 'status',
|
||||
params: [ 'name' ],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
function isString(v)
|
||||
{
|
||||
return typeof(v) === 'string' && v !== '';
|
||||
}
|
||||
|
||||
function parseBoardPortList(value)
|
||||
{
|
||||
if (Array.isArray(value))
|
||||
return value.filter(isString);
|
||||
|
||||
if (isString(value))
|
||||
return value.trim().split(/[\s,]+/).filter(isString);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function addKnownPort(knownPorts, seenPorts, role, device, extra)
|
||||
{
|
||||
if (!isString(device) || seenPorts[device])
|
||||
return;
|
||||
|
||||
seenPorts[device] = true;
|
||||
|
||||
const port = Object.assign({
|
||||
role,
|
||||
device,
|
||||
label: device
|
||||
}, extra || {});
|
||||
|
||||
if (!port.netdev && !port.swstate)
|
||||
port.netdev = network.instantiateDevice(device);
|
||||
|
||||
knownPorts.push(port);
|
||||
}
|
||||
|
||||
function parseSwitchPortToken(token)
|
||||
{
|
||||
if (!isString(token))
|
||||
return null;
|
||||
|
||||
const match = token.match(/^(\d+)([a-z*]+)?$/i);
|
||||
|
||||
return match ? { port: +match[1], flags: (match[2] || '').toLowerCase() } : null;
|
||||
}
|
||||
|
||||
function appendSwitchMemberPorts(sw, ports, cpuPorts, memberPorts)
|
||||
{
|
||||
for (const value of ports) {
|
||||
const token = parseSwitchPortToken(value);
|
||||
|
||||
if (!token)
|
||||
continue;
|
||||
|
||||
for (const port of sw.ports) {
|
||||
if (port.num != token.port)
|
||||
continue;
|
||||
|
||||
if (isString(port.device))
|
||||
cpuPorts[port.device] = true;
|
||||
else
|
||||
memberPorts.push('%s:%d'.format(sw['.name'] || sw.name || '', token.port));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildLegacySwitchMappings(mapping, board)
|
||||
{
|
||||
const switchVlans = uci.sections('network', 'switch_vlan');
|
||||
|
||||
if (!L.isObject(board) || !L.isObject(board.switch))
|
||||
return;
|
||||
|
||||
for (const section of switchVlans) {
|
||||
const swname = section.device;
|
||||
const sw = board.switch[swname];
|
||||
const vid = section.vid || section.vlan;
|
||||
|
||||
if (!L.isObject(sw) || !/^[0-9]{1,4}$/.test(vid) || +vid > 4095)
|
||||
continue;
|
||||
|
||||
const ports = L.toArray(section.ports);
|
||||
const cpuPorts = {};
|
||||
let memberPorts = [];
|
||||
|
||||
sw['.name'] = swname;
|
||||
appendSwitchMemberPorts(sw, ports, cpuPorts, memberPorts);
|
||||
|
||||
memberPorts = memberPorts.filter((port, index) => memberPorts.indexOf(port) === index);
|
||||
|
||||
for (const cpuDev in cpuPorts)
|
||||
mapping['%s.%s'.format(cpuDev, vid)] = memberPorts;
|
||||
}
|
||||
|
||||
for (const swname in board.switch) {
|
||||
const sw = board.switch[swname];
|
||||
|
||||
if (!L.isObject(sw) || !Array.isArray(sw.roles) || !Array.isArray(sw.ports))
|
||||
continue;
|
||||
|
||||
sw['.name'] = swname;
|
||||
|
||||
for (const role of sw.roles) {
|
||||
if (!L.isObject(role) || !isString(role.device))
|
||||
continue;
|
||||
|
||||
const cpuPorts = {};
|
||||
let memberPorts = [];
|
||||
|
||||
appendSwitchMemberPorts(sw, parseBoardPortList(role.ports), cpuPorts, memberPorts);
|
||||
|
||||
if (!cpuPorts[role.device])
|
||||
cpuPorts[role.device] = true;
|
||||
|
||||
memberPorts = memberPorts.filter((port, index) => memberPorts.indexOf(port) === index);
|
||||
|
||||
if (!mapping[role.device])
|
||||
mapping[role.device] = [];
|
||||
|
||||
for (const port of memberPorts)
|
||||
if (mapping[role.device].indexOf(port) === -1)
|
||||
mapping[role.device].push(port);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getSwitchPortLabel(board, switchName, portNum)
|
||||
{
|
||||
const sw = L.isObject(board) && L.isObject(board.switch) ? board.switch[switchName] : null;
|
||||
|
||||
if (!L.isObject(sw) || !Array.isArray(sw.ports))
|
||||
return '%s:%d'.format(switchName, portNum);
|
||||
|
||||
for (let i = 0; i < sw.ports.length; i++) {
|
||||
if (sw.ports[i].num != portNum)
|
||||
continue;
|
||||
|
||||
if (isString(sw.ports[i].label))
|
||||
return sw.ports[i].label;
|
||||
|
||||
if (isString(sw.ports[i].role)) {
|
||||
let index = 1;
|
||||
|
||||
for (let j = 0; j < i; j++)
|
||||
if (sw.ports[j].role == sw.ports[i].role)
|
||||
index++;
|
||||
|
||||
return sw.ports[i].role + index;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return '%s:%d'.format(switchName, portNum);
|
||||
}
|
||||
|
||||
function addResolvedPort(knownPorts, seenPorts, role, device, mapping, board, swstate)
|
||||
{
|
||||
const resolved = resolveVLANPorts(device, mapping);
|
||||
|
||||
for (const value of resolved) {
|
||||
const match = value.match(/^([^:]+):(\d+)$/);
|
||||
|
||||
if (match) {
|
||||
const switchName = match[1];
|
||||
const portNum = +match[2];
|
||||
const switchPorts = swstate[switchName] || {};
|
||||
|
||||
addKnownPort(knownPorts, seenPorts, role, value, {
|
||||
label: getSwitchPortLabel(board, switchName, portNum),
|
||||
swstate: switchPorts[portNum] || {
|
||||
port: portNum,
|
||||
link: false,
|
||||
speed: 0,
|
||||
duplex: false,
|
||||
rx_bytes: 0,
|
||||
tx_bytes: 0,
|
||||
rx_packets: 0,
|
||||
tx_packets: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
addKnownPort(knownPorts, seenPorts, role, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addBoardNetworkPorts(knownPorts, seenPorts, role, entry, mapping, board, swstate)
|
||||
{
|
||||
if (!L.isObject(entry))
|
||||
return;
|
||||
|
||||
const values = parseBoardPortList(entry.ports).concat(parseBoardPortList(entry.ifname));
|
||||
|
||||
if (isString(entry.device))
|
||||
values.push(entry.device);
|
||||
|
||||
for (const value of values)
|
||||
addResolvedPort(knownPorts, seenPorts, role, value, mapping, board, swstate);
|
||||
}
|
||||
|
||||
function resolveVLANChain(ifname, bridges, mapping)
|
||||
{
|
||||
while (!mapping[ifname]) {
|
||||
const m = ifname.match(/^(.+)\.([^.]+)$/);
|
||||
|
||||
if (!m)
|
||||
break;
|
||||
|
||||
if (bridges[m[1]]) {
|
||||
if (bridges[m[1]].vlan_filtering)
|
||||
mapping[ifname] = bridges[m[1]].vlans[m[2]];
|
||||
else
|
||||
mapping[ifname] = bridges[m[1]].ports;
|
||||
}
|
||||
else if (/^[0-9]{1,4}$/.test(m[2]) && m[2] <= 4095) {
|
||||
mapping[ifname] = [ m[1] ];
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
|
||||
ifname = m[1];
|
||||
}
|
||||
}
|
||||
|
||||
function buildVLANMappings(mapping, board)
|
||||
{
|
||||
const bridge_vlans = uci.sections('network', 'bridge-vlan');
|
||||
const vlan_devices = uci.sections('network', 'device');
|
||||
const interfaces = uci.sections('network', 'interface');
|
||||
const bridges = {};
|
||||
|
||||
/* find bridge VLANs */
|
||||
for (let i = 0, s; (s = bridge_vlans[i]) != null; i++) {
|
||||
if (!isString(s.device) || !/^[0-9]{1,4}$/.test(s.vlan) || +s.vlan > 4095)
|
||||
continue;
|
||||
|
||||
const aliases = L.toArray(s.alias);
|
||||
const ports = L.toArray(s.ports);
|
||||
const br = bridges[s.device] = (bridges[s.device] || { ports: [], vlans: {}, vlan_filtering: true });
|
||||
|
||||
br.vlans[s.vlan] = [];
|
||||
|
||||
for (let p of ports) {
|
||||
const port = p.replace(/:[ut*]+$/, '');
|
||||
|
||||
if (br.ports.indexOf(port) === -1)
|
||||
br.ports.push(port);
|
||||
|
||||
br.vlans[s.vlan].push(port);
|
||||
}
|
||||
|
||||
for (let a of aliases)
|
||||
if (a != s.vlan)
|
||||
br.vlans[a] = br.vlans[s.vlan];
|
||||
}
|
||||
|
||||
/* find bridges, VLAN devices */
|
||||
for (let i = 0, s; (s = vlan_devices[i]) != null; i++) {
|
||||
if (s.type == 'bridge') {
|
||||
if (!isString(s.name))
|
||||
continue;
|
||||
|
||||
const ports = L.toArray(s.ports);
|
||||
const br = bridges[s.name] || (bridges[s.name] = { ports: [], vlans: {}, vlan_filtering: false });
|
||||
|
||||
if (s.vlan_filtering == '0')
|
||||
br.vlan_filtering = false;
|
||||
else if (s.vlan_filtering == '1')
|
||||
br.vlan_filtering = true;
|
||||
|
||||
for (let p of ports)
|
||||
if (br.ports.indexOf(p) === -1)
|
||||
br.ports.push(p);
|
||||
|
||||
mapping[s.name] = br.ports;
|
||||
}
|
||||
else if (s.type == '8021q' || s.type == '8021ad') {
|
||||
if (!isString(s.name) || !isString(s.vid) || !isString(s.ifname))
|
||||
continue;
|
||||
|
||||
/* parent device is a bridge */
|
||||
if (bridges[s.ifname]) {
|
||||
/* parent bridge is VLAN enabled, device refers to VLAN ports */
|
||||
if (bridges[s.ifname].vlan_filtering)
|
||||
mapping[s.name] = bridges[s.ifname].vlans[s.vid];
|
||||
|
||||
/* parent bridge is not VLAN enabled, device refers to all bridge ports */
|
||||
else
|
||||
mapping[s.name] = bridges[s.ifname].ports;
|
||||
}
|
||||
|
||||
/* parent is a simple netdev */
|
||||
else {
|
||||
mapping[s.name] = [ s.ifname ];
|
||||
}
|
||||
|
||||
resolveVLANChain(s.ifname, bridges, mapping);
|
||||
}
|
||||
}
|
||||
|
||||
/* resolve VLAN tagged interfaces in bridge ports */
|
||||
for (let brname in bridges) {
|
||||
for (let bp of bridges[brname].ports)
|
||||
resolveVLANChain(bp, bridges, mapping);
|
||||
|
||||
for (let vid in bridges[brname].vlans)
|
||||
for (let v of bridges[brname].vlans[vid])
|
||||
resolveVLANChain(v, bridges, mapping);
|
||||
}
|
||||
|
||||
buildLegacySwitchMappings(mapping, board);
|
||||
|
||||
/* find implicit VLAN devices */
|
||||
for (let i = 0, s; (s = interfaces[i]) != null; i++) {
|
||||
if (!isString(s.device))
|
||||
continue;
|
||||
|
||||
resolveVLANChain(s.device, bridges, mapping);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveVLANPorts(ifname, mapping, seen)
|
||||
{
|
||||
const ports = [];
|
||||
|
||||
if (!seen)
|
||||
seen = {};
|
||||
|
||||
if (mapping[ifname]) {
|
||||
for (let m of mapping[ifname]) {
|
||||
if (!seen[m]) {
|
||||
seen[m] = true;
|
||||
ports.push.apply(ports, resolveVLANPorts(m, mapping, seen));
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
ports.push(ifname);
|
||||
}
|
||||
|
||||
return ports.sort(L.naturalCompare);
|
||||
}
|
||||
|
||||
function buildInterfaceMapping(zones, networks, board) {
|
||||
const vlanmap = {};
|
||||
const portmap = {};
|
||||
const netmap = {};
|
||||
|
||||
buildVLANMappings(vlanmap, board);
|
||||
|
||||
for (let net of networks) {
|
||||
const l3dev = net.getDevice();
|
||||
|
||||
if (!l3dev)
|
||||
continue;
|
||||
|
||||
const ports = resolveVLANPorts(l3dev.getName(), vlanmap);
|
||||
|
||||
for (let p of ports) {
|
||||
portmap[p] = portmap[p] || { networks: [], zones: [] };
|
||||
portmap[p].networks.push(net);
|
||||
}
|
||||
|
||||
netmap[net.getName()] = net;
|
||||
}
|
||||
|
||||
for (let z of zones) {
|
||||
const networknames = z.getNetworks();
|
||||
|
||||
for (let nn of networknames) {
|
||||
if (!netmap[nn])
|
||||
continue;
|
||||
|
||||
const l3dev = netmap[nn].getDevice();
|
||||
|
||||
if (!l3dev)
|
||||
continue;
|
||||
|
||||
const ports = resolveVLANPorts(l3dev.getName(), vlanmap);
|
||||
|
||||
for (let p of ports) {
|
||||
portmap[p] = portmap[p] || { networks: [], zones: [] };
|
||||
|
||||
if (portmap[p].zones.indexOf(z) === -1)
|
||||
portmap[p].zones.push(z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return portmap;
|
||||
}
|
||||
|
||||
function formatSpeed(carrier, speed, duplex) {
|
||||
if ((speed > 0) && duplex) {
|
||||
const d = (duplex == 'half') ? '\u202f(H)' : '';
|
||||
const e = E('span', { 'title': _('Speed: %d Mbit/s, Duplex: %s').format(speed, duplex) });
|
||||
|
||||
switch (true) {
|
||||
case (speed < 1000):
|
||||
e.innerText = '%d\u202fM%s'.format(speed, d);
|
||||
break;
|
||||
case (speed == 1000):
|
||||
e.innerText = '1\u202fGbE' + d;
|
||||
break;
|
||||
case (speed >= 1e6 && speed < 1e9):
|
||||
e.innerText = '%f\u202fTbE'.format(speed / 1e6);
|
||||
break;
|
||||
case (speed >= 1e9):
|
||||
e.innerText = '%f\u202fPbE'.format(speed / 1e9);
|
||||
break;
|
||||
default: e.innerText = '%f\u202fGbE'.format(speed / 1000);
|
||||
}
|
||||
|
||||
return e;
|
||||
}
|
||||
|
||||
return carrier ? _('Connected') : _('no link');
|
||||
}
|
||||
|
||||
function getPSEStatus(pse) {
|
||||
if (!pse)
|
||||
return null;
|
||||
|
||||
const status = pse['c33-power-status'] || pse['podl-power-status'],
|
||||
power = pse['c33-actual-power'];
|
||||
|
||||
return {
|
||||
status: status,
|
||||
power: power,
|
||||
isDelivering: status === 'delivering' && power > 0
|
||||
};
|
||||
}
|
||||
|
||||
function formatPSEPower(pse) {
|
||||
if (!pse)
|
||||
return null;
|
||||
|
||||
const status = pse['c33-power-status'] || pse['podl-power-status'],
|
||||
power = pse['c33-actual-power'];
|
||||
|
||||
if (status === 'delivering' && power) {
|
||||
const watts = (power / 1000).toFixed(1);
|
||||
/* Format: "⚡ 15.4 W" - lightning bolt + narrow space + watts + narrow space + W */
|
||||
return E('span', { 'style': 'color:#000' },
|
||||
[ '\u26a1\ufe0e\u202f%s\u202fW'.format(watts) ]);
|
||||
}
|
||||
else if (status === 'searching') {
|
||||
return E('span', { 'style': 'color:#000' },
|
||||
[ '\u26a1\ufe0e\u202f' + _('searching') ]);
|
||||
}
|
||||
else if (status === 'fault' || status === 'otherfault' || status === 'error') {
|
||||
return E('span', { 'style': 'color:#d9534f' },
|
||||
[ '\u26a1\ufe0e\u202f' + _('fault') ]);
|
||||
}
|
||||
else if (status === 'disabled') {
|
||||
return E('span', { 'style': 'color:#888' },
|
||||
[ '\u26a1\ufe0e\u202f' + _('off') ]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatStats(port, pse) {
|
||||
const stats = port.netdev
|
||||
? (port.netdev._devstate('stats') || {})
|
||||
: {
|
||||
rx_bytes: port.swstate ? port.swstate.rx_bytes : null,
|
||||
rx_packets: port.swstate ? port.swstate.rx_packets : null,
|
||||
multicast: null,
|
||||
rx_errors: null,
|
||||
rx_dropped: null,
|
||||
tx_bytes: port.swstate ? port.swstate.tx_bytes : null,
|
||||
tx_packets: port.swstate ? port.swstate.tx_packets : null,
|
||||
tx_errors: null,
|
||||
tx_dropped: null,
|
||||
collisions: null
|
||||
};
|
||||
const items = [
|
||||
_('Received bytes'), '%1024mB'.format(stats.rx_bytes),
|
||||
_('Received packets'), '%1000mPkts.'.format(stats.rx_packets),
|
||||
_('Received multicast'), '%1000mPkts.'.format(stats.multicast),
|
||||
_('Receive errors'), '%1000mPkts.'.format(stats.rx_errors),
|
||||
_('Receive dropped'), '%1000mPkts.'.format(stats.rx_dropped),
|
||||
|
||||
_('Transmitted bytes'), '%1024mB'.format(stats.tx_bytes),
|
||||
_('Transmitted packets'), '%1000mPkts.'.format(stats.tx_packets),
|
||||
_('Transmit errors'), '%1000mPkts.'.format(stats.tx_errors),
|
||||
_('Transmit dropped'), '%1000mPkts.'.format(stats.tx_dropped),
|
||||
|
||||
_('Collisions seen'), stats.collisions
|
||||
];
|
||||
|
||||
if (pse) {
|
||||
const status = pse['c33-power-status'] || pse['podl-power-status'],
|
||||
power = pse['c33-actual-power'],
|
||||
powerClass = pse['c33-power-class'],
|
||||
powerLimit = pse['c33-available-power-limit'];
|
||||
|
||||
items.push(_('PoE status'), status || _('unknown'));
|
||||
|
||||
if (power)
|
||||
items.push(_('PoE power'), '%.1f W'.format(power / 1000));
|
||||
|
||||
if (powerClass)
|
||||
items.push(_('PoE class'), powerClass);
|
||||
|
||||
if (powerLimit)
|
||||
items.push(_('PoE limit'), '%.1f W'.format(powerLimit / 1000));
|
||||
}
|
||||
|
||||
return ui.itemlist(E('span'), items);
|
||||
}
|
||||
|
||||
function getPortSpeed(port)
|
||||
{
|
||||
return port.netdev ? port.netdev.getSpeed() : (port.swstate ? port.swstate.speed : null);
|
||||
}
|
||||
|
||||
function getPortDuplex(port)
|
||||
{
|
||||
if (port.netdev)
|
||||
return port.netdev.getDuplex();
|
||||
|
||||
return (port.swstate && port.swstate.link) ? (port.swstate.duplex ? 'full' : 'half') : null;
|
||||
}
|
||||
|
||||
function getPortCarrier(port)
|
||||
{
|
||||
return port.netdev ? port.netdev.getCarrier() : !!(port.swstate && port.swstate.link);
|
||||
}
|
||||
|
||||
function getPortTXBytes(port)
|
||||
{
|
||||
return port.netdev ? port.netdev.getTXBytes() : (port.swstate ? port.swstate.tx_bytes : null);
|
||||
}
|
||||
|
||||
function getPortRXBytes(port)
|
||||
{
|
||||
return port.netdev ? port.netdev.getRXBytes() : (port.swstate ? port.swstate.rx_bytes : null);
|
||||
}
|
||||
|
||||
function renderNetworkBadge(network, zonename) {
|
||||
const l3dev = network.getDevice();
|
||||
const span = E('span', { 'class': 'ifacebadge', 'style': 'margin:.125em 0' }, [
|
||||
E('span', {
|
||||
'class': 'zonebadge',
|
||||
'title': zonename ? _('Part of zone %q').format(zonename) : _('No zone assigned'),
|
||||
'style': firewall.getZoneColorStyle(zonename)
|
||||
}, '\u202f'),
|
||||
'\u202f', network.getName(), ': '
|
||||
]);
|
||||
|
||||
if (l3dev)
|
||||
span.appendChild(E('img', {
|
||||
'title': l3dev.getI18n(),
|
||||
'src': L.resource('icons/%s%s.svg'.format(l3dev.getType(), l3dev.isUp() ? '' : '_disabled'))
|
||||
}));
|
||||
else
|
||||
span.appendChild(E('em', _('(no interfaces attached)')));
|
||||
|
||||
return span;
|
||||
}
|
||||
|
||||
function renderNetworksTooltip(pmap) {
|
||||
const res = [ null ];
|
||||
const zmap = {};
|
||||
|
||||
const zones = (pmap && Array.isArray(pmap.zones)) ? pmap.zones : [];
|
||||
const networks = (pmap && Array.isArray(pmap.networks)) ? pmap.networks : [];
|
||||
|
||||
for (let pmz of zones) {
|
||||
const networknames = pmz.getNetworks();
|
||||
|
||||
for (let nn of networknames)
|
||||
zmap[nn] = pmz.getName();
|
||||
}
|
||||
|
||||
for (let pmn of networks)
|
||||
res.push(E('br'), renderNetworkBadge(pmn, zmap[pmn.getName()]));
|
||||
|
||||
if (res.length > 1)
|
||||
res[0] = N_((res.length - 1) / 2, 'Part of network:', 'Part of networks:');
|
||||
else
|
||||
res[0] = _('Port is not part of any network');
|
||||
|
||||
return E([], res);
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
title: _('Port status'),
|
||||
deferFirstLoad: true,
|
||||
disableCache: true,
|
||||
|
||||
load() {
|
||||
return Promise.all([
|
||||
L.resolveDefault(callGetBuiltinEthernetPorts(), []),
|
||||
L.resolveDefault(fs.read('/etc/board.json'), '{}'),
|
||||
firewall.getZones(),
|
||||
network.getNetworks(),
|
||||
uci.load('network')
|
||||
]).then((data) => {
|
||||
const builtinPorts = data[0] || [];
|
||||
const board = JSON.parse(data[1] || '{}');
|
||||
const allPorts = new Set();
|
||||
const swstate = {};
|
||||
const tasks = [];
|
||||
|
||||
builtinPorts.forEach((port) => {
|
||||
if (port.device)
|
||||
allPorts.add(port.device);
|
||||
});
|
||||
|
||||
if (allPorts.size === 0 && board.network) {
|
||||
['lan', 'wan'].forEach((role) => {
|
||||
if (board.network[role]) {
|
||||
if (Array.isArray(board.network[role].ports))
|
||||
board.network[role].ports.forEach((p) => allPorts.add(p));
|
||||
else if (board.network[role].device)
|
||||
allPorts.add(board.network[role].device);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (L.isObject(board) && L.isObject(board.switch)) {
|
||||
for (const switchName in board.switch) {
|
||||
tasks.push(L.resolveDefault(callSwconfigPortState(switchName), []).then((ports) => {
|
||||
swstate[switchName] = ports.reduce((map, port) => {
|
||||
map[port.port] = port;
|
||||
return map;
|
||||
}, {});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
const psePromises = Array.from(allPorts).map((devname) => {
|
||||
return L.resolveDefault(callNetworkDeviceStatus(devname), {}).then((status) => {
|
||||
return { name: devname, pse: status.pse || null };
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(tasks).then(() => Promise.all(psePromises)).then((pseResults) => {
|
||||
const pseMap = {};
|
||||
|
||||
pseResults.forEach((result) => {
|
||||
if (result.pse)
|
||||
pseMap[result.name] = result.pse;
|
||||
});
|
||||
|
||||
data.push(swstate);
|
||||
data.push(pseMap);
|
||||
|
||||
return data;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
render(data) {
|
||||
const board = JSON.parse(data[1]),
|
||||
swstate = data[5] || {},
|
||||
port_map = buildInterfaceMapping(data[2], data[3], board),
|
||||
pseMap = data[6] || {};
|
||||
let known_ports = [];
|
||||
const seenPorts = {};
|
||||
const vlanMap = {};
|
||||
|
||||
buildVLANMappings(vlanMap, board);
|
||||
|
||||
if (Array.isArray(data[0]) && data[0].length > 0) {
|
||||
known_ports = data[0].reduce((ports, port) => {
|
||||
addResolvedPort(ports, seenPorts, port.role, port.device, vlanMap, board, swstate);
|
||||
return ports;
|
||||
}, []);
|
||||
}
|
||||
|
||||
if (L.isObject(board) && L.isObject(board.network)) {
|
||||
for (let k = 'lan'; k != null; k = (k == 'lan') ? 'wan' : null) {
|
||||
if (!L.isObject(board.network[k]))
|
||||
continue;
|
||||
|
||||
addBoardNetworkPorts(known_ports, seenPorts, k, board.network[k], vlanMap, board, swstate);
|
||||
}
|
||||
}
|
||||
|
||||
if (!known_ports.length)
|
||||
return null;
|
||||
|
||||
known_ports.sort(function(a, b) {
|
||||
return L.naturalCompare(a.label || a.device, b.label || b.device);
|
||||
});
|
||||
|
||||
return E('div', { 'style': 'display:grid;grid-template-columns:repeat(auto-fit, minmax(100px, 1fr));margin-bottom:1em;align-items:center;justify-items:center;text-align:center' }, known_ports.map(function(port) {
|
||||
const speed = getPortSpeed(port);
|
||||
const duplex = getPortDuplex(port);
|
||||
const carrier = getPortCarrier(port);
|
||||
const pmap = port_map[port.device];
|
||||
const pzones = (pmap && pmap.zones.length) ? pmap.zones.sort((a, b) => L.naturalCompare(a.getName(), b.getName())) : [ null ];
|
||||
const pse = pseMap[port.device];
|
||||
const pseInfo = getPSEStatus(pse);
|
||||
const psePower = formatPSEPower(pse);
|
||||
|
||||
/* Select port icon based on carrier and PSE status */
|
||||
let portIcon;
|
||||
if (pseInfo && pseInfo.isDelivering) {
|
||||
portIcon = carrier ? 'pse_up' : 'pse_down';
|
||||
} else {
|
||||
portIcon = carrier ? 'up' : 'down';
|
||||
}
|
||||
|
||||
const statsContent = [
|
||||
'\u25b2\u202f%1024.1mB'.format(getPortTXBytes(port)),
|
||||
E('br'),
|
||||
'\u25bc\u202f%1024.1mB'.format(getPortRXBytes(port))
|
||||
];
|
||||
|
||||
if (psePower) {
|
||||
statsContent.push(E('br'));
|
||||
statsContent.push(psePower);
|
||||
}
|
||||
|
||||
statsContent.push(E('span', { 'class': 'cbi-tooltip' }, formatStats(port, pse)));
|
||||
|
||||
return E('div', { 'class': 'ifacebox', 'style': 'margin:.25em;width:100px' }, [
|
||||
E('div', { 'class': 'ifacebox-head', 'style': 'font-weight:bold' }, [ port.label || port.device ]),
|
||||
E('div', { 'class': 'ifacebox-body' }, [
|
||||
E('img', { 'src': L.resource('icons/port_%s.svg').format(portIcon) }),
|
||||
E('br'),
|
||||
formatSpeed(carrier, speed, duplex)
|
||||
]),
|
||||
E('div', { 'class': 'ifacebox-head cbi-tooltip-container', 'style': 'display:flex' }, [
|
||||
E([], pzones.map(function(zone) {
|
||||
return E('div', {
|
||||
'class': 'zonebadge',
|
||||
'style': 'cursor:help;flex:1;height:3px;opacity:' + (carrier ? 1 : 0.25) + ';' + firewall.getZoneColorStyle(zone)
|
||||
});
|
||||
})),
|
||||
E('span', { 'class': 'cbi-tooltip left' }, [ renderNetworksTooltip(pmap) ])
|
||||
]),
|
||||
E('div', { 'class': 'ifacebox-body' }, [
|
||||
E('div', { 'class': 'cbi-tooltip-container', 'style': 'text-align:left;font-size:80%' }, statsContent)
|
||||
])
|
||||
]);
|
||||
}));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require rpc';
|
||||
|
||||
var callSystemInfo = rpc.declare({
|
||||
object: 'system',
|
||||
method: 'info'
|
||||
});
|
||||
|
||||
function progressbar(value, max, byte) {
|
||||
var vn = parseInt(value) || 0,
|
||||
mn = parseInt(max) || 100,
|
||||
fv = byte ? String.format('%1024.2mB', value) : value,
|
||||
fm = byte ? String.format('%1024.2mB', max) : max,
|
||||
pc = Math.floor((100 / mn) * vn);
|
||||
|
||||
return E('div', {
|
||||
'class': 'cbi-progressbar',
|
||||
'title': '%s / %s (%d%%)'.format(fv, fm, pc)
|
||||
}, E('div', { 'style': 'width:%.2f%%'.format(pc) }));
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
title: _('Memory'),
|
||||
|
||||
load: function() {
|
||||
return L.resolveDefault(callSystemInfo(), {});
|
||||
},
|
||||
|
||||
render: function(systeminfo) {
|
||||
var mem = L.isObject(systeminfo.memory) ? systeminfo.memory : {},
|
||||
swap = L.isObject(systeminfo.swap) ? systeminfo.swap : {};
|
||||
|
||||
var fields = [
|
||||
_('Used'), (mem.total && mem.available) ? (mem.total - mem.free - mem.buffered - mem.cached) : null, mem.total,
|
||||
];
|
||||
|
||||
if (mem.buffered)
|
||||
fields.push(_('Buffered'), mem.buffered, mem.total);
|
||||
|
||||
if (mem.cached)
|
||||
fields.push(_('Cached'), mem.cached, mem.total);
|
||||
|
||||
if (swap.total > 0)
|
||||
fields.push(_('Swap used'), swap.total - swap.free, swap.total);
|
||||
|
||||
var table = E('table', { 'class': 'table' });
|
||||
|
||||
for (var i = 0; i < fields.length; i += 3) {
|
||||
table.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'width': '33%' }, [ fields[i] ]),
|
||||
E('td', { 'class': 'td left' }, [
|
||||
(fields[i + 1] != null) ? progressbar(fields[i + 1], fields[i + 2], true) : '?'
|
||||
])
|
||||
]));
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require rpc';
|
||||
|
||||
var callMountPoints = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getMountPoints',
|
||||
expect: { result: [] }
|
||||
});
|
||||
|
||||
function progressbar(value, max, byte) {
|
||||
var vn = parseInt(value) || 0,
|
||||
mn = parseInt(max) || 100,
|
||||
fv = byte ? String.format('%1024.2mB', value) : value,
|
||||
fm = byte ? String.format('%1024.2mB', max) : max,
|
||||
pc = Math.floor((100 / mn) * vn);
|
||||
|
||||
return E('div', {
|
||||
'class': 'cbi-progressbar',
|
||||
'title': '%s / %s (%d%%)'.format(fv, fm, pc)
|
||||
}, E('div', { 'style': 'width:%.2f%%'.format(pc) }));
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
title: _('Storage'),
|
||||
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
L.resolveDefault(callMountPoints(), {}),
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var mounts = data[0],
|
||||
overlay = null;
|
||||
|
||||
for (var i = 0; i < mounts.length; i++) {
|
||||
if (mounts[i].mount == '/overlay') {
|
||||
overlay = mounts[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!overlay) {
|
||||
for (var j = 0; j < mounts.length; j++) {
|
||||
if (mounts[j].mount == '/') {
|
||||
overlay = mounts[j];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var table = E('table', { 'class': 'table' });
|
||||
|
||||
if (overlay) {
|
||||
table.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'width': '33%' }, [ overlay.mount ]),
|
||||
E('td', { 'class': 'td left' }, [
|
||||
progressbar(overlay.size - overlay.free, overlay.size, true)
|
||||
])
|
||||
]));
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require fs';
|
||||
'require network';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
|
||||
|
||||
/* returns per odhcp6c active interface JSON like:
|
||||
{"result":{"eth1":{"dhcp_solicit":3,"dhcp_advertise":3,"dhcp_request":3,...}}} */
|
||||
const callOdhcp6cStats = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getOdhcp6cStats',
|
||||
expect: { '': {} },
|
||||
});
|
||||
|
||||
var callOnlineUsers = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getOnlineUsers'
|
||||
});
|
||||
|
||||
function progressbar(value, max, byte) {
|
||||
const vn = parseInt(value) || 0;
|
||||
const mn = parseInt(max) || 100;
|
||||
const fv = byte ? String.format('%1024.2mB', value) : value;
|
||||
const fm = byte ? String.format('%1024.2mB', max) : max;
|
||||
const pc = Math.floor((100 / mn) * vn);
|
||||
|
||||
return E('div', {
|
||||
'class': 'cbi-progressbar',
|
||||
'title': '%s / %s (%d%%)'.format(fv, fm, pc)
|
||||
}, E('div', { 'style': 'width:%.2f%%'.format(pc) }));
|
||||
}
|
||||
|
||||
function renderbox(ifc, ipv6, dhcpv6_stats) {
|
||||
const dev = ifc.getL3Device();
|
||||
const active = (dev && ifc.getProtocol() != 'none');
|
||||
const addrs = (ipv6 ? ifc.getIP6Addrs() : ifc.getIPAddrs()) || [];
|
||||
const dnssrv = (ipv6 ? ifc.getDNS6Addrs() : ifc.getDNSAddrs()) || [];
|
||||
const expires = ifc.getExpiry();
|
||||
const uptime = ifc.getUptime();
|
||||
|
||||
function addEntries(label, array) {
|
||||
return Array.isArray(array) ? array.flatMap((item) => [label, item]) : [label, null];
|
||||
}
|
||||
|
||||
function addDhcpv6Stats() {
|
||||
if (ipv6 && ifc.getProtocol() === 'dhcpv6' && dhcpv6_stats && dhcpv6_stats[dev.device]) {
|
||||
const arr = [];
|
||||
for (const [pkt_type, count] of Object.entries(dhcpv6_stats[dev.device]))
|
||||
arr.push(pkt_type.replace('dhcp_', _('DHCPv6') + ' '), `${count} ${_('pkts', 'packets, abbreviated')}`);
|
||||
return [_('DHCPv6 Statistics'), E('span', { 'class': 'cbi-tooltip-container'}, [
|
||||
'📊',
|
||||
E('span', { 'class': 'cbi-tooltip' }, ui.itemlist(E('span'), arr))
|
||||
])];
|
||||
}
|
||||
return ['', null];
|
||||
}
|
||||
|
||||
return E('div', { class: 'ifacebox' }, [
|
||||
E('div', { class: 'ifacebox-head center ' + (active ? 'active' : '') },
|
||||
E('strong', ipv6 ? _('IPv6 Upstream') : _('IPv4 Upstream'))),
|
||||
E('div', { class: 'ifacebox-body left' }, [
|
||||
L.itemlist(E('span'), [
|
||||
_('Protocol'), ifc.getI18n() || E('em', _('Not connected')),
|
||||
...addEntries(_('Prefix Delegated'), ipv6 ? ifc.getIP6Prefixes?.() : null),
|
||||
...addEntries(_('Address'), addrs),
|
||||
_('Gateway'), ipv6 ? (ifc.getGateway6Addr() || '::') : (ifc.getGatewayAddr() || '0.0.0.0'),
|
||||
...addEntries(_('DNS'), dnssrv),
|
||||
_('Expires'), (expires != null && expires > -1) ? '%t'.format(expires) : null,
|
||||
_('Connected'), (uptime > 0) ? '%t'.format(uptime) : null,
|
||||
...addDhcpv6Stats(),
|
||||
]),
|
||||
E('div', {}, renderBadge(
|
||||
L.resource('icons/%s.svg').format(dev ? dev.getType() : 'ethernet_disabled'), null,
|
||||
_('Device'), dev ? dev.getI18n() : '-',
|
||||
_('MAC address'), dev.getMAC())
|
||||
)
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
title: _('Network'),
|
||||
|
||||
load() {
|
||||
return Promise.all([
|
||||
fs.trimmed('/proc/sys/net/netfilter/nf_conntrack_count'),
|
||||
fs.trimmed('/proc/sys/net/netfilter/nf_conntrack_max'),
|
||||
network.getWANNetworks(),
|
||||
network.getWAN6Networks(),
|
||||
callOdhcp6cStats(),
|
||||
L.resolveDefault(callOnlineUsers(), {})
|
||||
]);
|
||||
},
|
||||
|
||||
render([ct_count, ct_max, wan_nets, wan6_nets, dhcpv6_stats, onlineusers]) {
|
||||
|
||||
const fields = [
|
||||
{ label: _('Active Connections'), value: ct_max ? ct_count : null },
|
||||
{ label: _('Online Users'), value: onlineusers ? onlineusers.onlineusers : null }
|
||||
];
|
||||
|
||||
const ctstatus = E('table', { 'class': 'table' });
|
||||
|
||||
for (const { label, value } of fields) {
|
||||
if (label == _('Online Users')) {
|
||||
ctstatus.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'width': '33%' }, [ label ]),
|
||||
E('td', { 'class': 'td left' }, [
|
||||
(value != null) ? value : '?'
|
||||
])
|
||||
]));
|
||||
} else {
|
||||
ctstatus.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'width': '33%' }, [ label ]),
|
||||
E('td', { 'class': 'td left' }, [
|
||||
(value != null) ? progressbar(value, ct_max) : '?'
|
||||
])
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
const netstatus = E('div', { 'class': 'network-status-table' });
|
||||
|
||||
for (const wan_net of wan_nets)
|
||||
netstatus.appendChild(renderbox(wan_net, false));
|
||||
|
||||
for (const wan6_net of wan6_nets)
|
||||
netstatus.appendChild(renderbox(wan6_net, true, dhcpv6_stats?.result));
|
||||
|
||||
return E([
|
||||
netstatus,
|
||||
ctstatus
|
||||
]);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,243 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require network';
|
||||
'require validation';
|
||||
|
||||
const callLuciDHCPLeases = rpc.declare({
|
||||
object: 'luci-rpc',
|
||||
method: 'getDHCPLeases',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callUfpList = rpc.declare({
|
||||
object: 'fingerprint',
|
||||
method: 'fingerprint',
|
||||
});
|
||||
|
||||
return baseclass.extend({
|
||||
title: _('DHCP Leases'),
|
||||
deferFirstLoad: true,
|
||||
disableCache: true,
|
||||
|
||||
isMACStatic: {},
|
||||
isDUIDStatic: {},
|
||||
isDUIDIAIDStatic: {},
|
||||
|
||||
load() {
|
||||
return Promise.all([
|
||||
callLuciDHCPLeases(),
|
||||
network.getHostHints(),
|
||||
L.hasSystemFeature('ufpd') ? callUfpList() : null,
|
||||
L.resolveDefault(uci.load('dhcp'))
|
||||
]);
|
||||
},
|
||||
|
||||
render([dhcp_leases, host_hints, ufp_list]) {
|
||||
if (L.hasSystemFeature('dnsmasq') || L.hasSystemFeature('odhcpd'))
|
||||
return this.renderLeases(dhcp_leases, host_hints, ufp_list);
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
handleCreateStaticLease(lease, ev) {
|
||||
ev.currentTarget.classList.add('spinning');
|
||||
ev.currentTarget.disabled = true;
|
||||
ev.currentTarget.blur();
|
||||
|
||||
const cfg = uci.add('dhcp', 'host');
|
||||
uci.set('dhcp', cfg, 'name', lease.hostname);
|
||||
uci.set('dhcp', cfg, 'ip', lease.ipaddr);
|
||||
uci.set('dhcp', cfg, 'mac', [lease.macaddr.toLowerCase()]);
|
||||
|
||||
return uci.save()
|
||||
.then(L.bind(L.ui.changes.init, L.ui.changes))
|
||||
.then(L.bind(L.ui.changes.displayChanges, L.ui.changes));
|
||||
},
|
||||
|
||||
handleCreateStaticLease6(lease, ev) {
|
||||
ev.currentTarget.classList.add('spinning');
|
||||
ev.currentTarget.disabled = true;
|
||||
ev.currentTarget.blur();
|
||||
|
||||
const cfg = uci.add('dhcp', 'host');
|
||||
const ip6addr = lease.ip6addrs?.[0]?.replace(/\/128$/, '');
|
||||
const ip6arr = ip6addr ? validation.parseIPv6(ip6addr) : null;
|
||||
|
||||
// Combine DUID and IAID if both available
|
||||
// (note that we know that lease.duid is set here)
|
||||
let duid_iaid = lease.duid.toLowerCase();
|
||||
if (lease.iaid)
|
||||
duid_iaid += `%${lease.iaid}`.toLowerCase();
|
||||
|
||||
uci.set('dhcp', cfg, 'name', lease.hostname);
|
||||
uci.set('dhcp', cfg, 'duid', [duid_iaid]);
|
||||
if (lease.macaddr)
|
||||
uci.set('dhcp', cfg, 'mac', [lease.macaddr.toLowerCase()]);
|
||||
if (ip6arr)
|
||||
uci.set('dhcp', cfg, 'hostid', (ip6arr[6] * 0xFFFF + ip6arr[7]).toString(16));
|
||||
|
||||
return uci.save()
|
||||
.then(L.bind(L.ui.changes.init, L.ui.changes))
|
||||
.then(L.bind(L.ui.changes.displayChanges, L.ui.changes));
|
||||
},
|
||||
|
||||
renderLeases(dhcp_leases, host_hints, macaddr) {
|
||||
const leases = Array.isArray(dhcp_leases.dhcp_leases) ? dhcp_leases.dhcp_leases : [];
|
||||
const leases6 = Array.isArray(dhcp_leases.dhcp6_leases) ? dhcp_leases.dhcp6_leases : [];
|
||||
if (leases.length == 0 && leases6.length == 0)
|
||||
return E('em', _('No active leases found'));
|
||||
const machints = host_hints.getMACHints(false);
|
||||
const isReadonlyView = !L.hasViewPermission();
|
||||
|
||||
for (const host of uci.sections('dhcp', 'host')) {
|
||||
|
||||
for (const mac of L.toArray(host.mac).map(m => m.toLowerCase()))
|
||||
this.isMACStatic[mac] = true;
|
||||
|
||||
for (const duid_iaid of L.toArray(host.duid).map(m => m.toLowerCase())) {
|
||||
const parts = duid_iaid.split('%').length;
|
||||
|
||||
if (parts == 1)
|
||||
this.isDUIDStatic[duid_iaid] = true;
|
||||
else if (parts == 2)
|
||||
this.isDUIDIAIDStatic[duid_iaid] = true;
|
||||
}
|
||||
};
|
||||
|
||||
const table = E('table', { 'id': 'status_leases', 'class': 'table leases' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
L.hasSystemFeature('odhcpd', 'dhcpv4') ? E('th', { 'class': 'th' }, _('Interface')) : E([]),
|
||||
E('th', { 'class': 'th' }, _('Hostname')),
|
||||
E('th', { 'class': 'th' }, _('IPv4 address')),
|
||||
E('th', { 'class': 'th' }, _('MAC address')),
|
||||
E('th', { 'class': 'th' }, _('DUID')),
|
||||
E('th', { 'class': 'th' }, _('IAID')),
|
||||
E('th', { 'class': 'th' }, _('Remaining time')),
|
||||
isReadonlyView ? E([]) : E('th', { 'class': 'th cbi-section-actions' }, _('Static Lease'))
|
||||
])
|
||||
]);
|
||||
|
||||
cbi_update_table(table, leases.map(L.bind(function(lease) {
|
||||
let exp;
|
||||
let vendor;
|
||||
|
||||
if (lease.expires === false)
|
||||
exp = E('em', _('unlimited'));
|
||||
else if (lease.expires <= 0)
|
||||
exp = E('em', _('expired'));
|
||||
else
|
||||
exp = '%t'.format(lease.expires);
|
||||
|
||||
const hint = lease.macaddr ? machints.filter(function(h) { return h[0] == lease.macaddr })[0] : null;
|
||||
let host = null;
|
||||
|
||||
if (hint && lease.hostname && lease.hostname != hint[1])
|
||||
host = '%s (%s)'.format(lease.hostname, hint[1]);
|
||||
else if (lease.hostname)
|
||||
host = lease.hostname;
|
||||
|
||||
if (macaddr)
|
||||
vendor = macaddr[lease.macaddr.toLowerCase()]?.vendor ?? null;
|
||||
|
||||
const columns = [
|
||||
host || '-',
|
||||
lease.ipaddr,
|
||||
vendor ? lease.macaddr + ` (${vendor})` : lease.macaddr,
|
||||
lease.duid || '-',
|
||||
lease.iaid || '-',
|
||||
exp,
|
||||
];
|
||||
|
||||
if (L.hasSystemFeature('odhcpd', 'dhcpv4'))
|
||||
columns.unshift(lease.interface || '-');
|
||||
|
||||
if (!isReadonlyView && lease.macaddr != null) {
|
||||
columns.push(E('button', {
|
||||
'class': 'cbi-button cbi-button-apply',
|
||||
'click': L.bind(this.handleCreateStaticLease, this, lease),
|
||||
'data-tooltip': _('Reserve a specific IP address for this device'),
|
||||
'disabled': this.isMACStatic[lease.macaddr.toLowerCase()]
|
||||
}, [ _('Reserve IP') ]));
|
||||
}
|
||||
|
||||
return columns;
|
||||
}, this)), E('em', _('No active leases found')));
|
||||
|
||||
const table6 = E('table', { 'id': 'status_leases6', 'class': 'table leases6' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
L.hasSystemFeature('odhcpd', 'dhcpv6') ? E('th', { 'class': 'th' }, _('Interface')) : E([]),
|
||||
E('th', { 'class': 'th' }, _('Hostname')),
|
||||
E('th', { 'class': 'th' }, _('IPv6 addresses')),
|
||||
E('th', { 'class': 'th' }, _('DUID')),
|
||||
E('th', { 'class': 'th' }, _('IAID')),
|
||||
E('th', { 'class': 'th' }, _('Remaining time')),
|
||||
isReadonlyView ? E([]) : E('th', { 'class': 'th cbi-section-actions' }, _('Static Lease'))
|
||||
])
|
||||
]);
|
||||
|
||||
cbi_update_table(table6, leases6.map(L.bind(function(lease) {
|
||||
let exp;
|
||||
|
||||
if (lease.expires === false)
|
||||
exp = E('em', _('unlimited'));
|
||||
else if (lease.expires <= 0)
|
||||
exp = E('em', _('expired'));
|
||||
else
|
||||
exp = '%t'.format(lease.expires);
|
||||
|
||||
const hint = lease.macaddr ? machints.filter(function(h) { return h[0] == lease.macaddr })[0] : null;
|
||||
let host = null;
|
||||
|
||||
if (hint && lease.hostname && lease.hostname != hint[1] && lease.ip6addr != hint[1])
|
||||
host = '%s (%s)'.format(lease.hostname, hint[1]);
|
||||
else if (lease.hostname)
|
||||
host = lease.hostname;
|
||||
else if (hint)
|
||||
host = hint[1];
|
||||
|
||||
const duid = lease.duid?.toLowerCase();
|
||||
const iaid = lease.iaid?.toLowerCase();
|
||||
|
||||
// Note: "disabled: false" doesn't work
|
||||
let disabled = null;
|
||||
if (!duid)
|
||||
disabled = true;
|
||||
else if (duid && this.isDUIDStatic[duid])
|
||||
disabled = true;
|
||||
else if (duid && iaid && this.isDUIDIAIDStatic[`${duid}%${iaid}`])
|
||||
disabled = true;
|
||||
|
||||
const columns = [
|
||||
host || '-',
|
||||
lease.ip6addrs ? lease.ip6addrs.join('<br />') : lease.ip6addr,
|
||||
duid || '-',
|
||||
iaid || '-',
|
||||
exp
|
||||
];
|
||||
|
||||
if (L.hasSystemFeature('odhcpd', 'dhcpv6'))
|
||||
columns.unshift(lease.interface || '-');
|
||||
|
||||
if (!isReadonlyView && lease.duid) {
|
||||
columns.push(E('button', {
|
||||
'class': 'cbi-button cbi-button-apply',
|
||||
'click': L.bind(this.handleCreateStaticLease6, this, lease),
|
||||
'data-tooltip': _('Reserve a specific IP address for this device'),
|
||||
'disabled': disabled
|
||||
}, [ _('Reserve IP') ]));
|
||||
}
|
||||
|
||||
return columns;
|
||||
}, this)), E('em', _('No active leases found')));
|
||||
|
||||
return E([
|
||||
E('h3', _('Active DHCPv4 Leases')),
|
||||
table,
|
||||
E('h3', _('Active DHCPv6 Leases')),
|
||||
table6
|
||||
]);
|
||||
},
|
||||
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,353 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require dom';
|
||||
'require poll';
|
||||
'require network';
|
||||
'require ui';
|
||||
|
||||
return view.extend({
|
||||
overviewCacheTtl: 30000,
|
||||
|
||||
wrapDeferredInclude: function(spec) {
|
||||
return {
|
||||
title: spec.title,
|
||||
deferFirstLoad: true,
|
||||
_moduleName: spec.name,
|
||||
_include: null,
|
||||
_loading: null,
|
||||
|
||||
resolveModule: function() {
|
||||
if (this._include != null)
|
||||
return Promise.resolve(this._include);
|
||||
|
||||
if (this._loading != null)
|
||||
return this._loading;
|
||||
|
||||
this._loading = L.resolveDefault(L.require(this._moduleName), null).then(L.bind(function(include) {
|
||||
this._include = include;
|
||||
this._loading = null;
|
||||
|
||||
if (include != null && include.title != null)
|
||||
this.title = include.title;
|
||||
|
||||
return include;
|
||||
}, this)).catch(L.bind(function(err) {
|
||||
this._loading = null;
|
||||
return Promise.reject(err);
|
||||
}, this));
|
||||
|
||||
return this._loading;
|
||||
},
|
||||
|
||||
load: function() {
|
||||
return this.resolveModule().then(function(include) {
|
||||
return (include != null && typeof(include.load) == 'function') ? include.load() : null;
|
||||
});
|
||||
},
|
||||
|
||||
render: function(result) {
|
||||
return (this._include != null && typeof(this._include.render) == 'function')
|
||||
? this._include.render(result)
|
||||
: null;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
getIncludeCacheKey: function(include) {
|
||||
return 'luci.status.include.%s'.format(include.id || '');
|
||||
},
|
||||
|
||||
restoreIncludeCache: function(include, container) {
|
||||
var cached;
|
||||
var now = Date.now();
|
||||
|
||||
if (include.disableCache || !window.sessionStorage || !include.id)
|
||||
return false;
|
||||
|
||||
try {
|
||||
cached = JSON.parse(window.sessionStorage.getItem(this.getIncludeCacheKey(include)));
|
||||
}
|
||||
catch (err) {
|
||||
cached = null;
|
||||
}
|
||||
|
||||
if (!cached || typeof(cached.html) != 'string' || typeof(cached.ts) != 'number')
|
||||
return false;
|
||||
|
||||
if ((now - cached.ts) > this.overviewCacheTtl) {
|
||||
window.sessionStorage.removeItem(this.getIncludeCacheKey(include));
|
||||
return false;
|
||||
}
|
||||
|
||||
container.innerHTML = cached.html;
|
||||
container.setAttribute('data-cached', '1');
|
||||
return true;
|
||||
},
|
||||
|
||||
cacheIncludeContent: function(include, container) {
|
||||
if (include.disableCache || !window.sessionStorage || !include.id)
|
||||
return;
|
||||
|
||||
try {
|
||||
window.sessionStorage.setItem(this.getIncludeCacheKey(include), JSON.stringify({
|
||||
ts: Date.now(),
|
||||
html: container.innerHTML
|
||||
}));
|
||||
}
|
||||
catch (err) {
|
||||
}
|
||||
},
|
||||
|
||||
clearIncludeCache: function(include) {
|
||||
if (include.disableCache || !window.sessionStorage || !include.id)
|
||||
return;
|
||||
|
||||
window.sessionStorage.removeItem(this.getIncludeCacheKey(include));
|
||||
},
|
||||
|
||||
renderIncludePlaceholder: function(container) {
|
||||
dom.content(container,
|
||||
E('p', {}, E('em', { 'class': 'spinning' },
|
||||
[ _('Collecting data...') ])
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
applyIncludeResult: function(include, container, result, first_load) {
|
||||
var content = null;
|
||||
|
||||
if (include.hide && !first_load)
|
||||
return;
|
||||
|
||||
if (typeof(include.render) == 'function')
|
||||
content = include.render(result);
|
||||
else if (include.content != null)
|
||||
content = include.content;
|
||||
|
||||
if (typeof(include.oneshot) == 'function') {
|
||||
include.oneshot(result);
|
||||
include.oneshot = null;
|
||||
}
|
||||
|
||||
if (content != null) {
|
||||
container.parentNode.style.display = '';
|
||||
container.parentNode.classList.add('fade-in');
|
||||
|
||||
if (!include.hide) {
|
||||
dom.content(container, content);
|
||||
container.removeAttribute('data-cached');
|
||||
this.cacheIncludeContent(include, container);
|
||||
}
|
||||
}
|
||||
else if (first_load) {
|
||||
container.parentNode.style.display = 'none';
|
||||
this.clearIncludeCache(include);
|
||||
}
|
||||
},
|
||||
|
||||
loadInclude: function(include, container, first_load) {
|
||||
if (include.hide && !first_load)
|
||||
return Promise.resolve(null);
|
||||
|
||||
if (!include.hide && !container.hasAttribute('data-cached'))
|
||||
this.renderIncludePlaceholder(container);
|
||||
|
||||
if (typeof(include.load) == 'function') {
|
||||
return include.load().then(L.bind(function(result) {
|
||||
this.applyIncludeResult(include, container, result, first_load);
|
||||
return result;
|
||||
}, this)).catch(function() {
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
this.applyIncludeResult(include, container, null, first_load);
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
|
||||
handleToggleSection: function(include, container, ev) {
|
||||
var btn = ev.currentTarget;
|
||||
|
||||
include.hide = !include.hide;
|
||||
|
||||
btn.setAttribute('data-style', include.hide ? 'active' : 'inactive');
|
||||
btn.setAttribute('class', include.hide ? 'label notice' : 'label');
|
||||
btn.firstChild.data = include.hide ? _('Show') : _('Hide');
|
||||
btn.blur();
|
||||
|
||||
container.style.display = include.hide ? 'none' : 'block';
|
||||
|
||||
if (include.hide) {
|
||||
localStorage.setItem(include.id, 'hide');
|
||||
} else {
|
||||
if (!this.restoreIncludeCache(include, container))
|
||||
this.renderIncludePlaceholder(container);
|
||||
localStorage.removeItem(include.id);
|
||||
this.loadInclude(include, container, false);
|
||||
}
|
||||
},
|
||||
|
||||
invokeIncludesLoad: function(includes, first_load) {
|
||||
var tasks = [];
|
||||
|
||||
for (var i = 0; i < includes.length; i++) {
|
||||
if (includes[i].hide && !first_load) {
|
||||
tasks.push(Promise.resolve(null));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof(includes[i].load) == 'function') {
|
||||
tasks.push(Promise.resolve(includes[i].load()).catch(function() {
|
||||
return null;
|
||||
}));
|
||||
}
|
||||
else {
|
||||
tasks.push(Promise.resolve(null));
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(tasks);
|
||||
},
|
||||
|
||||
poll_status: function(includes, containers, first_load) {
|
||||
var fetch = first_load ? Promise.resolve(null) : network.flushCache();
|
||||
|
||||
return fetch.then(L.bind(
|
||||
this.invokeIncludesLoad, this, includes, first_load
|
||||
)).then(L.bind(function(results) {
|
||||
for (var i = 0; i < includes.length; i++) {
|
||||
this.applyIncludeResult(includes[i], containers[i], results ? results[i] : null, first_load);
|
||||
}
|
||||
|
||||
var ssi = document.querySelector('div.includes');
|
||||
if (ssi) {
|
||||
ssi.style.display = '';
|
||||
ssi.classList.add('fade-in');
|
||||
}
|
||||
}, this));
|
||||
},
|
||||
|
||||
loadIncludeGroup: function(includes, containers, deferred, first_load) {
|
||||
var tasks = [];
|
||||
|
||||
for (var i = 0; i < includes.length; i++) {
|
||||
if (!!includes[i].deferFirstLoad != !!deferred)
|
||||
continue;
|
||||
|
||||
tasks.push(this.loadInclude(includes[i], containers[i], first_load));
|
||||
}
|
||||
|
||||
return Promise.all(tasks);
|
||||
},
|
||||
|
||||
prime_status: function(includes, containers) {
|
||||
var ssi = document.querySelector('div.includes');
|
||||
var deferredLoader = L.bind(function() {
|
||||
this.loadIncludeGroup(includes, containers, true, true);
|
||||
}, this);
|
||||
|
||||
for (var i = 0; i < includes.length; i++) {
|
||||
if (includes[i].hide)
|
||||
continue;
|
||||
|
||||
containers[i].parentNode.style.display = '';
|
||||
|
||||
if (!this.restoreIncludeCache(includes[i], containers[i]))
|
||||
this.renderIncludePlaceholder(containers[i]);
|
||||
}
|
||||
|
||||
if (ssi)
|
||||
ssi.style.display = '';
|
||||
|
||||
this.loadIncludeGroup(includes, containers, false, true);
|
||||
|
||||
if (window.requestAnimationFrame)
|
||||
window.requestAnimationFrame(function() { window.setTimeout(deferredLoader, 0); });
|
||||
else
|
||||
window.setTimeout(deferredLoader, 0);
|
||||
},
|
||||
|
||||
startOverviewRefresh: function(includes, containers) {
|
||||
/* Defer startup until after the view DOM has been inserted to keep
|
||||
* first paint and poll initialization from racing each other. */
|
||||
window.setTimeout(L.bind(function() {
|
||||
this.prime_status(includes, containers);
|
||||
poll.add(L.bind(this.poll_status, this, includes, containers, false));
|
||||
poll.start();
|
||||
}, this), 0);
|
||||
},
|
||||
|
||||
load: function() {
|
||||
var includeModules = [
|
||||
{ name: 'view.status.include.10_system' },
|
||||
{ name: 'view.status.include.19_cpu' },
|
||||
{ name: 'view.status.include.15_ports', title: _('Port status'), deferFirstLoad: true },
|
||||
{ name: 'view.status.include.20_memory' },
|
||||
{ name: 'view.status.include.25_storage' },
|
||||
{ name: 'view.status.include.30_network' },
|
||||
{ name: 'view.status.include.40_dhcp', title: _('DHCP Leases'), deferFirstLoad: true },
|
||||
{ name: 'view.status.include.50_dsl' },
|
||||
{ name: 'view.status.include.60_wifi' }
|
||||
];
|
||||
|
||||
return Promise.all(includeModules.map(L.bind(function(spec) {
|
||||
if (spec.deferFirstLoad)
|
||||
return this.wrapDeferredInclude(spec);
|
||||
|
||||
return L.resolveDefault(L.require(spec.name), null);
|
||||
}, this))).then(function(includes) {
|
||||
return includes.filter(function(include) {
|
||||
return include != null;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
render: function(includes) {
|
||||
var rv = E([]), containers = [];
|
||||
|
||||
for (var i = 0; i < includes.length; i++) {
|
||||
var title = null;
|
||||
|
||||
if (includes[i].title != null)
|
||||
title = includes[i].title;
|
||||
else
|
||||
title = String(includes[i]).replace(/^\[ViewStatusInclude\d+_(.+)Class\]$/,
|
||||
function(m, n) { return n.replace(/(^|_)(.)/g,
|
||||
function(m, s, c) { return (s ? ' ' : '') + c.toUpperCase() })
|
||||
});
|
||||
|
||||
includes[i].id = title;
|
||||
includes[i].hide = localStorage.getItem(includes[i].id) == 'hide';
|
||||
|
||||
var container = E('div');
|
||||
|
||||
rv.appendChild(E('div', { 'class': 'cbi-section', 'style': 'display: none' }, [
|
||||
E('div', { 'class': 'cbi-title' },[
|
||||
E('h3', { 'style': 'display: flex; justify-content: space-between' }, [
|
||||
title || '-',
|
||||
E('span', {
|
||||
'class': includes[i].hide ? 'label notice' : 'label',
|
||||
'style': 'display: flex; align-items: center; justify-content: center; min-width: 4em',
|
||||
'data-style': includes[i].hide ? 'active' : 'inactive',
|
||||
'data-indicator': 'poll-status',
|
||||
'data-clickable': 'true',
|
||||
'click': ui.createHandlerFn(this, 'handleToggleSection',
|
||||
includes[i], container)
|
||||
}, [ _(includes[i].hide ? 'Show' : 'Hide') ])
|
||||
]),
|
||||
]),
|
||||
container
|
||||
]));
|
||||
|
||||
containers.push(container);
|
||||
}
|
||||
|
||||
this.startOverviewRefresh(includes, containers);
|
||||
|
||||
return rv;
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,374 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require poll';
|
||||
'require fs';
|
||||
'require ui';
|
||||
|
||||
const table_names = [ 'Filter', 'NAT', 'Mangle', 'Raw' ];
|
||||
const raw_style = 'font-family:monospace;font-size:smaller;text-align:right';
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
return L.resolveDefault(fs.stat('/usr/sbin/ip6tables'));
|
||||
},
|
||||
|
||||
createTableSection(is_ipv6, table) {
|
||||
const idiv = document.querySelector('div[data-tab="%s"]'.format(is_ipv6 ? 'ip6tables' : 'iptables'));
|
||||
let tdiv = idiv.querySelector('[data-table="%s-%s"]'.format(is_ipv6 ? 'ipv6' : 'ipv4', table));
|
||||
const title = '%s: %s'.format(_('Table'), table);
|
||||
|
||||
if (!tdiv) {
|
||||
tdiv = E('div', { 'data-table': '%s-%s'.format(is_ipv6 ? 'ipv6' : 'ipv4', table) }, [
|
||||
E('h3', {}, title),
|
||||
E('div')
|
||||
]);
|
||||
|
||||
if (idiv.firstElementChild.nodeName.toLowerCase() === 'p')
|
||||
idiv.removeChild(idiv.firstElementChild);
|
||||
|
||||
let added = false, thisIdx = table_names.indexOf(table);
|
||||
|
||||
idiv.querySelectorAll('[data-table]').forEach(function(child) {
|
||||
const childIdx = table_names.indexOf(child.getAttribute('data-table').split(/-/)[1]);
|
||||
|
||||
if (added === false && childIdx > thisIdx) {
|
||||
idiv.insertBefore(tdiv, child);
|
||||
added = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (added === false)
|
||||
idiv.appendChild(tdiv);
|
||||
}
|
||||
|
||||
return tdiv.lastElementChild;
|
||||
},
|
||||
|
||||
createChainSection(is_ipv6, table, chain, policy, packets, bytes, references) {
|
||||
const tdiv = this.createTableSection(is_ipv6, table);
|
||||
let cdiv = tdiv.querySelector('[data-chain="%s"]'.format(chain));
|
||||
let title;
|
||||
|
||||
if (policy)
|
||||
title = '%s <em>%s</em> <span>(%s: <em>%s</em>, %d %s, %.2mB %s)</span>'
|
||||
.format(_('Chain'), chain, _('Policy'), policy, packets, _('Packets'), bytes, _('Traffic'));
|
||||
else
|
||||
title = '%s <em>%s</em> <span class="references">(%d %s)</span>'
|
||||
.format(_('Chain'), chain, references, _('References'));
|
||||
|
||||
if (!cdiv) {
|
||||
cdiv = E('div', { 'data-chain': chain }, [
|
||||
E('h4', { 'id': 'rule_%s-%s_%s'.format(is_ipv6 ? 'ipv6' : 'ipv4', table.toLowerCase(), chain) }, title),
|
||||
E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, _('Pkts.')),
|
||||
E('th', { 'class': 'th' }, _('Traffic')),
|
||||
E('th', { 'class': 'th' }, _('Target')),
|
||||
E('th', { 'class': 'th' }, _('Prot.')),
|
||||
E('th', { 'class': 'th' }, _('In')),
|
||||
E('th', { 'class': 'th' }, _('Out')),
|
||||
E('th', { 'class': 'th' }, _('Source')),
|
||||
E('th', { 'class': 'th' }, _('Destination')),
|
||||
E('th', { 'class': 'th' }, _('Options')),
|
||||
E('th', { 'class': 'th' }, _('Comment'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
tdiv.appendChild(cdiv);
|
||||
}
|
||||
else {
|
||||
cdiv.firstElementChild.innerHTML = title;
|
||||
}
|
||||
|
||||
return cdiv.lastElementChild;
|
||||
},
|
||||
|
||||
updateChainSection(chaintable, rows) {
|
||||
if (!chaintable)
|
||||
return;
|
||||
|
||||
cbi_update_table(chaintable, rows, _('No rules in this chain.'));
|
||||
|
||||
if (rows.length === 0 && document.querySelector('[data-hide-empty="true"]'))
|
||||
chaintable.parentNode.style.display = 'none';
|
||||
else
|
||||
chaintable.parentNode.style.display = '';
|
||||
|
||||
chaintable.parentNode.setAttribute('data-empty', rows.length === 0);
|
||||
},
|
||||
|
||||
parseIptablesDump(is_ipv6, table, s) {
|
||||
let current_chain = null;
|
||||
let current_rules = [];
|
||||
const seen_chains = {};
|
||||
const chain_refs = {};
|
||||
const re = /([^\n]*)\n/g;
|
||||
let m, m2;
|
||||
const raw = document.querySelector('[data-raw-counters="true"]');
|
||||
|
||||
while ((m = re.exec(s)) != null) {
|
||||
if (m[1].match(/^Chain (.+) \(policy (\w+) (\d+) packets, (\d+) bytes\)$/)) {
|
||||
const chain = RegExp.$1;
|
||||
const policy = RegExp.$2;
|
||||
const packets = +RegExp.$3;
|
||||
const bytes = +RegExp.$4;
|
||||
|
||||
this.updateChainSection(current_chain, current_rules);
|
||||
|
||||
seen_chains[chain] = true;
|
||||
current_chain = this.createChainSection(is_ipv6, table, chain, policy, packets, bytes);
|
||||
current_rules = [];
|
||||
}
|
||||
else if (m[1].match(/^Chain (.+) \((\d+) references\)$/)) {
|
||||
const chain = RegExp.$1;
|
||||
const references = +RegExp.$2;
|
||||
|
||||
this.updateChainSection(current_chain, current_rules);
|
||||
|
||||
seen_chains[chain] = true;
|
||||
current_chain = this.createChainSection(is_ipv6, table, chain, null, null, null, references);
|
||||
current_rules = [];
|
||||
}
|
||||
else if (m[1].match(/^num /)) {
|
||||
continue;
|
||||
}
|
||||
else if ((m2 = m[1].match(/^(\d+) +(\d+) +(\d+) +(.*?) +(\S+) +(\S*) +(\S+) +(\S+) +(!?[a-f0-9:.]+(?:\/[a-f0-9:.]+)?) +(!?[a-f0-9:.]+(?:\/[a-f0-9:.]+)?) +(.+)$/)) !== null) {
|
||||
const num = +m2[1];
|
||||
const pkts = +m2[2];
|
||||
const bytes = +m2[3];
|
||||
const target = m2[4];
|
||||
const proto = m2[5];
|
||||
const indev = m2[7];
|
||||
const outdev = m2[8];
|
||||
const srcnet = m2[9];
|
||||
const dstnet = m2[10];
|
||||
let options = m2[11] || '-';
|
||||
let comment = '-';
|
||||
|
||||
options = options.trim().replace(/(?:^| )\/\* (.+) \*\//,
|
||||
function(m1, m2) {
|
||||
comment = m2.replace(/^!fw3(: |$)/, '').trim() || '-';
|
||||
return '';
|
||||
}) || '-';
|
||||
|
||||
current_rules.push([
|
||||
E('div', {
|
||||
'class': 'nowrap',
|
||||
'style': raw ? raw_style : null,
|
||||
'data-format': '%.2m',
|
||||
'data-value': pkts
|
||||
}, (raw ? '%d' : '%.2m').format(pkts)),
|
||||
E('div', {
|
||||
'class': 'nowrap',
|
||||
'style': raw ? raw_style : null,
|
||||
'data-format': '%.2mB',
|
||||
'data-value': bytes
|
||||
}, (raw ? '%d' : '%.2mB').format(bytes)),
|
||||
target ? '<span class="target">%s</span>'.format(target) : '-',
|
||||
proto,
|
||||
(indev !== '*') ? '<span class="ifacebadge nowrap">%s</span>'.format(indev) : '*',
|
||||
(outdev !== '*') ? '<span class="ifacebadge nowrap">%s</span>'.format(outdev) : '*',
|
||||
srcnet,
|
||||
dstnet,
|
||||
options,
|
||||
[ comment, '%h'.format(comment) ]
|
||||
]);
|
||||
|
||||
if (target) {
|
||||
chain_refs[target] = chain_refs[target] || [];
|
||||
chain_refs[target].push([ current_chain, num ]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.updateChainSection(current_chain, current_rules);
|
||||
|
||||
document.querySelectorAll('[data-table="%s-%s"] [data-chain]'.format(is_ipv6 ? 'ipv6' : 'ipv4', table)).forEach(L.bind(function(cdiv) {
|
||||
if (!seen_chains[cdiv.getAttribute('data-chain')]) {
|
||||
cdiv.parentNode.removeChild(cdiv);
|
||||
return;
|
||||
}
|
||||
|
||||
cdiv.querySelectorAll('.target').forEach(L.bind(function(tspan) {
|
||||
if (seen_chains[tspan.textContent]) {
|
||||
tspan.classList.add('jump');
|
||||
tspan.addEventListener('click', this.handleJumpTarget);
|
||||
}
|
||||
}, this));
|
||||
|
||||
cdiv.querySelectorAll('.references').forEach(L.bind(function(rspan) {
|
||||
const refs = chain_refs[cdiv.getAttribute('data-chain')];
|
||||
if (refs && refs.length) {
|
||||
rspan.classList.add('cbi-tooltip-container');
|
||||
rspan.appendChild(E('small', { 'class': 'cbi-tooltip ifacebadge', 'style': 'top:1em; left:auto' }, [ E('ul') ]));
|
||||
|
||||
refs.forEach(L.bind(function(ref) {
|
||||
const chain = ref[0].parentNode.getAttribute('data-chain');
|
||||
const num = ref[1];
|
||||
|
||||
rspan.lastElementChild.lastElementChild.appendChild(E('li', {}, [
|
||||
_('Chain'), ' ',
|
||||
E('span', {
|
||||
'class': 'jump',
|
||||
'data-num': num,
|
||||
'click': this.handleJumpTarget
|
||||
}, chain),
|
||||
', %s #%d'.format(_('Rule'), num)
|
||||
]));
|
||||
}, this));
|
||||
}
|
||||
}, this));
|
||||
}, this));
|
||||
},
|
||||
|
||||
pollFirewallLists(has_ip6tables) {
|
||||
const cmds = [ '/usr/sbin/iptables' ];
|
||||
|
||||
if (has_ip6tables)
|
||||
cmds.push('/usr/sbin/ip6tables');
|
||||
|
||||
poll.add(L.bind(function() {
|
||||
const tasks = [];
|
||||
|
||||
for (let i = 0; i < cmds.length; i++) {
|
||||
for (let tn of table_names) {
|
||||
tasks.push(L.resolveDefault(
|
||||
fs.exec_direct(cmds[i], [ '--line-numbers', '-w', '-nvxL', '-t', tn.toLowerCase() ])
|
||||
.then(this.parseIptablesDump.bind(this, i > 0, tn))));
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(tasks);
|
||||
}, this));
|
||||
},
|
||||
|
||||
handleJumpTarget(ev) {
|
||||
const link = ev.target;
|
||||
const table = findParent(link, '[data-table]').getAttribute('data-table');
|
||||
const chain = link.textContent;
|
||||
const num = +link.getAttribute('data-num');
|
||||
const elem = document.getElementById('rule_%s_%s'.format(table.toLowerCase(), chain));
|
||||
|
||||
if (elem) {
|
||||
if (elem.scrollIntoView) {
|
||||
elem.scrollIntoView();
|
||||
} else {
|
||||
(document.documentElement || document.body.parentNode || document.body).scrollTop = elem.offsetTop - 40;
|
||||
}
|
||||
elem.classList.remove('flash');
|
||||
void elem.offsetWidth;
|
||||
elem.classList.add('flash');
|
||||
|
||||
if (num) {
|
||||
const rule = elem.nextElementSibling.childNodes[num];
|
||||
if (rule) {
|
||||
rule.classList.remove('flash');
|
||||
void rule.offsetWidth;
|
||||
rule.classList.add('flash');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
handleRawCounters(ev) {
|
||||
const btn = ev.currentTarget;
|
||||
const raw = (btn.getAttribute('data-raw-counters') === 'false');
|
||||
|
||||
btn.setAttribute('data-raw-counters', raw);
|
||||
btn.firstChild.data = raw ? _('Human-readable counters') : _('Show raw counters');
|
||||
btn.blur();
|
||||
|
||||
document.querySelectorAll('[data-value]')
|
||||
.forEach(function(div) {
|
||||
const fmt = raw ? '%d' : div.getAttribute('data-format');
|
||||
|
||||
div.style = raw ? raw_style : '';
|
||||
div.innerText = fmt.format(div.getAttribute('data-value'));
|
||||
});
|
||||
},
|
||||
|
||||
handleHideEmpty(ev) {
|
||||
const btn = ev.currentTarget;
|
||||
const hide = (btn.getAttribute('data-hide-empty') === 'false');
|
||||
|
||||
btn.setAttribute('data-hide-empty', hide);
|
||||
btn.firstChild.data = hide ? _('Show empty chains') : _('Hide empty chains');
|
||||
btn.blur();
|
||||
|
||||
document.querySelectorAll('[data-chain][data-empty="true"]')
|
||||
.forEach(function(chaintable) {
|
||||
chaintable.style.display = hide ? 'none' : '';
|
||||
});
|
||||
},
|
||||
|
||||
handleCounterReset(has_ip6tables, ev) {
|
||||
return Promise.all([
|
||||
fs.exec('/usr/sbin/iptables', [ '-Z' ])
|
||||
.catch(function(err) { ui.addNotification(null, E('p', {}, _('Unable to reset iptables counters: %s').format(err.message))) }),
|
||||
has_ip6tables ? fs.exec('/usr/sbin/ip6tables', [ '-Z' ])
|
||||
.catch(function(err) { ui.addNotification(null, E('p', {}, _('Unable to reset ip6tables counters: %s').format(err.message))) }) : null
|
||||
]);
|
||||
},
|
||||
|
||||
handleRestart(ev) {
|
||||
return fs.exec_direct('/etc/init.d/firewall', [ 'restart' ])
|
||||
.catch(function(err) { ui.addNotification(null, E('p', {}, _('Unable to restart firewall: %s').format(err.message))) });
|
||||
},
|
||||
|
||||
render(has_ip6tables) {
|
||||
const view = E([], [
|
||||
E('style', { 'type': 'text/css' }, [
|
||||
'.cbi-tooltip-container, span.jump { border-bottom:1px dotted #00f;cursor:pointer }',
|
||||
'ul { list-style:none }',
|
||||
'.references { position:relative }',
|
||||
'.references .cbi-tooltip { left:0!important;top:1.5em!important }',
|
||||
'h4>span { font-size:90% }'
|
||||
]),
|
||||
|
||||
E('h2', {}, [ _('Firewall Status') ]),
|
||||
E('div', { 'class': 'right', 'style': 'margin-bottom:-1.5em' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button',
|
||||
'data-hide-empty': false,
|
||||
'click': ui.createHandlerFn(this, 'handleHideEmpty')
|
||||
}, [ _('Hide empty chains') ]),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'cbi-button',
|
||||
'data-raw-counters': false,
|
||||
'click': ui.createHandlerFn(this, 'handleRawCounters')
|
||||
}, [ _('Show raw counters') ]),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'cbi-button',
|
||||
'click': ui.createHandlerFn(this, 'handleCounterReset', has_ip6tables)
|
||||
}, [ _('Reset Counters') ]),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'cbi-button',
|
||||
'click': ui.createHandlerFn(this, 'handleRestart')
|
||||
}, [ _('Restart Firewall') ])
|
||||
]),
|
||||
E('div', {}, [
|
||||
E('div', { 'data-tab': 'iptables', 'data-tab-title': has_ip6tables ? _('IPv4 Firewall') : null, 'data-tab-active': has_ip6tables ? null : true }, [
|
||||
E('p', {}, E('em', { 'class': 'spinning' }, [ _('Collecting data...') ]))
|
||||
]),
|
||||
has_ip6tables ? E('div', { 'data-tab': 'ip6tables', 'data-tab-title': _('IPv6 Firewall') }, [
|
||||
E('p', {}, E('em', { 'class': 'spinning' }, [ _('Collecting data...') ]))
|
||||
]) : E([])
|
||||
])
|
||||
]);
|
||||
|
||||
if (has_ip6tables)
|
||||
ui.tabs.initTabGroup(view.lastElementChild.childNodes);
|
||||
|
||||
this.pollFirewallLists(has_ip6tables);
|
||||
|
||||
return view;
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,295 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require poll';
|
||||
'require request';
|
||||
'require rpc';
|
||||
|
||||
const callLuciRealtimeStats = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getRealtimeStats',
|
||||
params: [ 'mode', 'device' ],
|
||||
expect: { result: [] }
|
||||
});
|
||||
|
||||
const graphPolls = [];
|
||||
const pollInterval = 3;
|
||||
|
||||
Math.log2 = Math.log2 || function(x) { return Math.log(x) * Math.LOG2E; };
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
return Promise.all([
|
||||
this.loadSVG(L.resource('svg/load.svg'))
|
||||
]);
|
||||
},
|
||||
|
||||
updateGraph(svg, lines, cb) {
|
||||
const G = svg.firstElementChild;
|
||||
|
||||
const view = document.querySelector('#view');
|
||||
|
||||
const width = view.offsetWidth - 2;
|
||||
const height = 300 - 2;
|
||||
const step = 5;
|
||||
|
||||
const data_wanted = Math.floor(width / step);
|
||||
|
||||
const data_values = [];
|
||||
|
||||
for (let line of lines)
|
||||
if (line != null)
|
||||
data_values.push([]);
|
||||
|
||||
const info = {
|
||||
line_current: [],
|
||||
line_average: [],
|
||||
line_peak: []
|
||||
};
|
||||
|
||||
/* prefill datasets */
|
||||
for (let dv of data_values)
|
||||
for (let j = 0; j < data_wanted; j++)
|
||||
dv[j] = 0;
|
||||
|
||||
/* plot horizontal time interval lines */
|
||||
for (let i = width % (step * 60); i < width; i += step * 60) {
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
line.setAttribute('x1', i);
|
||||
line.setAttribute('y1', 0);
|
||||
line.setAttribute('x2', i);
|
||||
line.setAttribute('y2', '100%');
|
||||
line.setAttribute('style', 'stroke:black;stroke-width:0.1');
|
||||
|
||||
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
text.setAttribute('x', i + 5);
|
||||
text.setAttribute('y', 15);
|
||||
text.setAttribute('style', 'fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000');
|
||||
text.appendChild(document.createTextNode(Math.round((width - i) / step / 60) + 'm'));
|
||||
|
||||
G.appendChild(line);
|
||||
G.appendChild(text);
|
||||
}
|
||||
|
||||
info.interval = pollInterval;
|
||||
info.timeframe = data_wanted / 60;
|
||||
|
||||
graphPolls.push({
|
||||
svg: svg,
|
||||
lines: lines,
|
||||
cb: cb,
|
||||
info: info,
|
||||
width: width,
|
||||
height: height,
|
||||
step: step,
|
||||
values: data_values,
|
||||
timestamp: 0,
|
||||
fill: 1
|
||||
});
|
||||
},
|
||||
|
||||
pollData() {
|
||||
poll.add(L.bind(function() {
|
||||
const tasks = [];
|
||||
|
||||
graphPolls.forEach(() => {
|
||||
tasks.push(L.resolveDefault(callLuciRealtimeStats('load'), []));
|
||||
});
|
||||
|
||||
return Promise.all(tasks).then(L.bind(function(datasets) {
|
||||
for (let gi = 0; gi < graphPolls.length; gi++) {
|
||||
const ctx = graphPolls[gi];
|
||||
const data = datasets[gi];
|
||||
const values = ctx.values;
|
||||
const lines = ctx.lines;
|
||||
const info = ctx.info;
|
||||
|
||||
let data_scale = 0;
|
||||
let data_wanted = Math.floor(ctx.width / ctx.step);
|
||||
let last_timestamp = NaN;
|
||||
|
||||
for (let i = 0, di = 0; di < lines.length; di++) {
|
||||
if (lines[di] == null)
|
||||
continue;
|
||||
|
||||
const multiply = (lines[di].multiply != null) ? lines[di].multiply : 1;
|
||||
const offset = (lines[di].offset != null) ? lines[di].offset : 0;
|
||||
|
||||
for (let j = ctx.timestamp ? 0 : 1; j < data.length; j++) {
|
||||
/* skip overlapping entries */
|
||||
if (data[j][0] <= ctx.timestamp)
|
||||
continue;
|
||||
|
||||
if (i == 0) {
|
||||
ctx.fill++;
|
||||
last_timestamp = data[j][0];
|
||||
}
|
||||
|
||||
info.line_current[i] = data[j][di + 1] * multiply;
|
||||
info.line_current[i] -= Math.min(info.line_current[i], offset);
|
||||
values[i].push(info.line_current[i]);
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
/* cut off outdated entries */
|
||||
ctx.fill = Math.min(ctx.fill, data_wanted);
|
||||
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const len = values[i].length;
|
||||
values[i] = values[i].slice(len - data_wanted, len);
|
||||
|
||||
/* find peaks, averages */
|
||||
info.line_peak[i] = NaN;
|
||||
info.line_average[i] = 0;
|
||||
|
||||
for (let j = 0; j < values[i].length; j++) {
|
||||
info.line_peak[i] = isNaN(info.line_peak[i]) ? values[i][j] : Math.max(info.line_peak[i], values[i][j]);
|
||||
info.line_average[i] += values[i][j];
|
||||
}
|
||||
|
||||
info.line_average[i] = info.line_average[i] / ctx.fill;
|
||||
}
|
||||
|
||||
info.peak = Math.max.apply(Math, info.line_peak);
|
||||
|
||||
/* remember current timestamp, calculate horizontal scale */
|
||||
if (!isNaN(last_timestamp))
|
||||
ctx.timestamp = last_timestamp;
|
||||
|
||||
const size = Math.floor(Math.log2(info.peak));
|
||||
const div = Math.pow(2, size - (size % 10));
|
||||
let mult = info.peak / div;
|
||||
mult = (mult < 5) ? 2 : ((mult < 50) ? 10 : ((mult < 500) ? 100 : 1000));
|
||||
|
||||
info.peak = info.peak + (mult * div) - (info.peak % (mult * div));
|
||||
|
||||
data_scale = ctx.height / info.peak;
|
||||
|
||||
/* plot data */
|
||||
for (let i = 0, di = 0; di < lines.length; di++) {
|
||||
if (lines[di] == null)
|
||||
continue;
|
||||
|
||||
const el = ctx.svg.firstElementChild.getElementById(lines[di].line);
|
||||
let pt = '0,' + ctx.height;
|
||||
let y = 0;
|
||||
|
||||
if (!el)
|
||||
continue;
|
||||
|
||||
for (let j = 0; j < values[i].length; j++) {
|
||||
let x = j * ctx.step;
|
||||
|
||||
y = ctx.height - Math.floor(values[i][j] * data_scale);
|
||||
//y -= Math.floor(y % (1 / data_scale));
|
||||
|
||||
y = isNaN(y) ? ctx.height : y;
|
||||
|
||||
pt += ' ' + x + ',' + y;
|
||||
}
|
||||
|
||||
pt += ' ' + ctx.width + ',' + y + ' ' + ctx.width + ',' + ctx.height;
|
||||
|
||||
el.setAttribute('points', pt);
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
info.label_25 = 0.25 * info.peak;
|
||||
info.label_50 = 0.50 * info.peak;
|
||||
info.label_75 = 0.75 * info.peak;
|
||||
|
||||
if (typeof(ctx.cb) == 'function')
|
||||
ctx.cb(ctx.svg, info);
|
||||
}
|
||||
}, this));
|
||||
}, this), pollInterval);
|
||||
},
|
||||
|
||||
loadSVG(src) {
|
||||
return request.get(src).then(function(response) {
|
||||
if (!response.ok)
|
||||
throw new Error(response.statusText);
|
||||
|
||||
return E('div', {
|
||||
'style': 'width:100%;height:300px;border:1px solid #000;background:#fff'
|
||||
}, E(response.text()));
|
||||
});
|
||||
},
|
||||
|
||||
render([svg]) {
|
||||
const v = E('div', { 'class': 'cbi-map', 'id': 'map' }, [
|
||||
E('h2', _('System load')),
|
||||
E('div', {'class': 'cbi-map-descr'}, _('Load Average is a metric that is used by Linux to keep track of system resources.')),
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
svg,
|
||||
E('div', { 'class': 'right' }, E('small', { 'id': 'scale' }, '-')),
|
||||
E('br'),
|
||||
|
||||
E('table', { 'class': 'table', 'style': 'width:100%;table-layout:fixed' }, [
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td right top' }, E('strong', { 'style': 'border-bottom:2px solid #f00' }, [ _('1 Minute Load:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'lb_load01_cur' }, [ '0.00' ]),
|
||||
|
||||
E('td', { 'class': 'td right top' }, E('strong', {}, [ _('Average:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'lb_load01_avg' }, [ '0.00' ]),
|
||||
|
||||
E('td', { 'class': 'td right top' }, E('strong', {}, [ _('Peak:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'lb_load01_peak' }, [ '0.00' ])
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td right top' }, E('strong', { 'style': 'border-bottom:2px solid #f60' }, [ _('5 Minute Load:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'lb_load05_cur' }, [ '0.00' ]),
|
||||
|
||||
E('td', { 'class': 'td right top' }, E('strong', {}, [ _('Average:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'lb_load05_avg' }, [ '0.00' ]),
|
||||
|
||||
E('td', { 'class': 'td right top' }, E('strong', {}, [ _('Peak:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'lb_load05_peak' }, [ '0.00' ])
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td right top' }, E('strong', { 'style': 'border-bottom:2px solid #fa0' }, [ _('15 Minute Load:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'lb_load15_cur' }, [ '0.00' ]),
|
||||
|
||||
E('td', { 'class': 'td right top' }, E('strong', {}, [ _('Average:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'lb_load15_avg' }, [ '0.00' ]),
|
||||
|
||||
E('td', { 'class': 'td right top' }, E('strong', {}, [ _('Peak:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'lb_load15_peak' }, [ '0.00' ])
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
this.updateGraph(svg, [ { line: 'load01' }, { line: 'load05' }, { line: 'load15' } ], function(svg, info) {
|
||||
const G = svg.firstElementChild, tab = svg.parentNode;
|
||||
|
||||
G.getElementById('label_25').firstChild.data = '%.2f'.format(info.label_25 / 100);
|
||||
G.getElementById('label_50').firstChild.data = '%.2f'.format(info.label_50 / 100);
|
||||
G.getElementById('label_75').firstChild.data = '%.2f'.format(info.label_75 / 100);
|
||||
|
||||
tab.querySelector('#scale').firstChild.data = _('(%d minute window, %d second interval)').format(info.timeframe, info.interval);
|
||||
|
||||
tab.querySelector('#lb_load01_cur').firstChild.data = '%.2f'.format(info.line_current[0] / 100);
|
||||
tab.querySelector('#lb_load01_avg').firstChild.data = '%.2f'.format(info.line_average[0] / 100);
|
||||
tab.querySelector('#lb_load01_peak').firstChild.data = '%.2f'.format(info.line_peak[0] / 100);
|
||||
|
||||
tab.querySelector('#lb_load05_cur').firstChild.data = '%.2f'.format(info.line_current[1] / 100);
|
||||
tab.querySelector('#lb_load05_avg').firstChild.data = '%.2f'.format(info.line_average[1] / 100);
|
||||
tab.querySelector('#lb_load05_peak').firstChild.data = '%.2f'.format(info.line_peak[1] / 100);
|
||||
|
||||
tab.querySelector('#lb_load15_cur').firstChild.data = '%.2f'.format(info.line_current[2] / 100);
|
||||
tab.querySelector('#lb_load15_avg').firstChild.data = '%.2f'.format(info.line_average[2] / 100);
|
||||
tab.querySelector('#lb_load15_peak').firstChild.data = '%.2f'.format(info.line_peak[2] / 100);
|
||||
});
|
||||
|
||||
this.pollData();
|
||||
|
||||
return v;
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,813 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require poll';
|
||||
'require fs';
|
||||
'require ui';
|
||||
'require dom';
|
||||
|
||||
const expr_translations = {
|
||||
'meta.iifname': _('Ingress device name', 'nft meta iifname'),
|
||||
'meta.oifname': _('Egress device name', 'nft meta oifname'),
|
||||
'meta.iif': _('Ingress device id', 'nft meta iif'),
|
||||
'meta.oif': _('Egress device id', 'nft meta oif'),
|
||||
|
||||
'meta.l4proto': _('IP protocol', 'nft meta l4proto'),
|
||||
'meta.l4proto.tcp': 'TCP',
|
||||
'meta.l4proto.udp': 'UDP',
|
||||
'meta.l4proto.icmp': 'ICMP',
|
||||
'meta.l4proto.icmpv6': 'ICMPv6',
|
||||
'meta.l4proto.ipv6-icmp': 'ICMPv6',
|
||||
|
||||
'meta.nfproto': _('Address family', 'nft meta nfproto'),
|
||||
'meta.nfproto.ipv4': 'IPv4',
|
||||
'meta.nfproto.ipv6': 'IPv6',
|
||||
|
||||
'meta.mark': _('Packet mark', 'nft meta mark'),
|
||||
|
||||
'meta.time': _('Packet receive time', 'nft meta time'),
|
||||
'meta.hour': _('Current time', 'nft meta hour'),
|
||||
'meta.day': _('Current weekday', 'nft meta day'),
|
||||
|
||||
'ct.state': _('Conntrack state', 'nft ct state'),
|
||||
|
||||
'ct.status': _('Conntrack status', 'nft ct status'),
|
||||
'ct.status.dnat': 'DNAT',
|
||||
|
||||
'ip.dscp': 'DSCP',
|
||||
'ip6.dscp': 'DSCP',
|
||||
|
||||
'ether.saddr': _('Source MAC address', 'nft ether saddr'),
|
||||
'ether.daddr': _('Destination MAC address', 'nft ether daddr'),
|
||||
|
||||
'ip.protocol': _('IP protocol', 'nft ip protocol'),
|
||||
'ip.protocol.tcp': 'TCP',
|
||||
'ip.protocol.udp': 'UDP',
|
||||
'ip.protocol.icmp': 'ICMP',
|
||||
'ip.protocol.icmpv6': 'ICMPv6',
|
||||
'ip.protocol.ipv6-icmp': 'ICMPv6',
|
||||
|
||||
'ip.saddr': _('Source IP', 'nft ip saddr'),
|
||||
'ip.daddr': _('Destination IP', 'nft ip daddr'),
|
||||
'ip.sport': _('Source port', 'nft ip sport'),
|
||||
'ip.dport': _('Destination port', 'nft ip dport'),
|
||||
'ip6.saddr': _('Source IPv6', 'nft ip6 saddr'),
|
||||
'ip6.daddr': _('Destination IPv6', 'nft ip6 daddr'),
|
||||
'icmp.code': _('ICMP code', 'nft icmp code'),
|
||||
'icmp.type': _('ICMP type', 'nft icmp type'),
|
||||
'icmpv6.code': _('ICMPv6 code', 'nft icmpv6 code'),
|
||||
'icmpv6.type': _('ICMPv6 type', 'nft icmpv6 type'),
|
||||
'tcp.sport': _('TCP source port', 'nft tcp sport'),
|
||||
'tcp.dport': _('TCP destination port', 'nft tcp dport'),
|
||||
'udp.sport': _('UDP source port', 'nft udp sport'),
|
||||
'udp.dport': _('UDP destination port', 'nft udp dport'),
|
||||
'tcp.flags': _('TCP flags', 'nft tcp flags'),
|
||||
'th.sport': _('Transport header source port', 'nft th sport'),
|
||||
'th.dport': _('Transport header destination port', 'nft th dport'),
|
||||
|
||||
'natflag.random': _('Randomize source port mapping', 'nft nat flag random'),
|
||||
'natflag.fully-random': _('Full port randomization', 'nft nat flag fully-random'),
|
||||
'natflag.persistent': _('Use same source and destination for each connection', 'nft nat flag persistent'),
|
||||
|
||||
'rt.mtu': _('Effective route MTU', 'nft rt mtu'),
|
||||
|
||||
'tcpoption.maxseg.size': _('TCP MSS', 'nft tcp option maxseg size'),
|
||||
|
||||
'unit.packets': _('packets', 'nft unit'),
|
||||
'unit.mbytes': _('MiB', 'nft unit'),
|
||||
'unit.kbytes': _('KiB', 'nft unit'),
|
||||
'unit.week': _('week', 'nft unit'),
|
||||
'unit.day': _('day', 'nft unit'),
|
||||
'unit.hour': _('hour', 'nft unit'),
|
||||
'unit.minute': _('minute', 'nft unit'),
|
||||
|
||||
'payload.ll': _('Link layer header bits %d-%d', 'nft @ll,off,len'),
|
||||
'payload.nh': _('Network header bits %d-%d', 'nft @nh,off,len'),
|
||||
'payload.th': _('Transport header bits %d-%d', 'nft @th,off,len')
|
||||
};
|
||||
|
||||
const op_translations = {
|
||||
'==': _('<var>%s</var> is <strong>%s</strong>', 'nft relational "==" operator expression'),
|
||||
'!=': _('<var>%s</var> not <strong>%s</strong>', 'nft relational "!=" operator expression'),
|
||||
'>=': _('<var>%s</var> greater than or equal to <strong>%s</strong>', 'nft relational ">=" operator expression'),
|
||||
'<=': _('<var>%s</var> lower than or equal to <strong>%s</strong>', 'nft relational "<=" operator expression'),
|
||||
'>': _('<var>%s</var> greater than <strong>%s</strong>', 'nft relational ">" operator expression'),
|
||||
'<': _('<var>%s</var> lower than <strong>%s</strong>', 'nft relational "<" operator expression'),
|
||||
'in': _('<var>%s</var> is one of <strong>%s</strong>', 'nft relational "in" operator expression'),
|
||||
'in_set': _('<var>%s</var> in set <strong>%s</strong>', 'nft set match expression'),
|
||||
'not_in_set': _('<var>%s</var> not in set <strong>%s</strong>', 'nft not in set match expression'),
|
||||
};
|
||||
|
||||
const action_translations = {
|
||||
'accept': _('Accept packet', 'nft accept action'),
|
||||
'notrack': _('Do not track', 'nft notrack action'),
|
||||
'drop': _('Drop packet', 'nft drop action'),
|
||||
'jump': _('Continue in <strong><a href="#%q.%q">%h</a></strong>', 'nft jump action'),
|
||||
'goto': _('Goto chain <strong><a href="#%q.%q">%h</a></strong>', 'nft goto action'),
|
||||
'continue': _('Continue to next rule', 'nft continue action'),
|
||||
'log': _('Log event', 'nft log action'),
|
||||
'log.prefix': _('Log event "<strong>%h</strong>…"', 'nft log action'),
|
||||
|
||||
'reject.tcp reset': _('Reject packet with <strong>TCP reset</strong>', 'nft reject with tcp reset'),
|
||||
'reject.icmp': _('Reject IPv4 packet with <strong>ICMP type %h</strong>', 'nft reject with icmp type'),
|
||||
'reject.icmpv6': _('Reject packet with <strong>ICMPv6 type %h</strong>', 'nft reject with icmpv6 type'),
|
||||
'reject.icmpx': _('Reject packet with <strong>ICMP type %h</strong>', 'nft reject with icmpx type'),
|
||||
|
||||
'snat.ip.addr': _('Rewrite source to <strong>%h</strong>', 'nft snat ip to addr'),
|
||||
'snat.ip.addr.port': _('Rewrite source to <strong>%h</strong>, port <strong>%h</strong>', 'nft snat ip to addr:port'),
|
||||
|
||||
'snat.ip6.addr': _('Rewrite source to <strong>%h</strong>', 'nft snat ip6 to addr'),
|
||||
'snat.ip6.addr.port': _('Rewrite source to <strong>%h</strong>, port <strong>%h</strong>', 'nft snat ip6 to addr:port'),
|
||||
|
||||
'dnat.ip.addr': _('Rewrite destination to <strong>%h</strong>', 'nft dnat ip to addr'),
|
||||
'dnat.ip.addr.port': _('Rewrite destination to <strong>%h</strong>, port <strong>%h</strong>', 'nft dnat ip to addr:port'),
|
||||
|
||||
'dnat.ip6.addr': _('Rewrite destination to <strong>%h</strong>', 'nft dnat ip6 to addr'),
|
||||
'dnat.ip6.addr.port': _('Rewrite destination to <strong>%h</strong>, port <strong>%h</strong>', 'nft dnat ip6 to addr:port'),
|
||||
|
||||
'redirect': _('Redirect to local system', 'nft redirect'),
|
||||
'redirect.port': _('Redirect to local port <strong>%h</strong>', 'nft redirect to port'),
|
||||
|
||||
'masquerade': _('Rewrite to egress device address'),
|
||||
|
||||
'mangle': _('Set header field <var>%s</var> to <strong>%s</strong>', 'nft mangle'),
|
||||
|
||||
'limit': _('At most <strong>%h</strong> per <strong>%h</strong>, burst of <strong>%h</strong>'),
|
||||
'limit.burst': _('At most <strong>%h</strong> per <strong>%h</strong>, burst of <strong>%h</strong>'),
|
||||
'limit.inv': _('At least <strong>%h</strong> per <strong>%h</strong>, burst of <strong>%h</strong>'),
|
||||
'limit.inv.burst': _('At least <strong>%h</strong> per <strong>%h</strong>, burst of <strong>%h</strong>'),
|
||||
|
||||
'return': _('Continue in calling chain'),
|
||||
|
||||
'ct helper': _('Utilise <strong>%h</strong> conntrack helper'),
|
||||
'flow': _('Utilize flow table <strong>%h</strong>')
|
||||
};
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
return Promise.all([
|
||||
L.resolveDefault(fs.exec_direct('/usr/sbin/nft', [ '--terse', '--json', 'list', 'ruleset' ], 'json'), {}),
|
||||
fs.stat('/usr/sbin/iptables-legacy-save').then(function() {
|
||||
return L.resolveDefault(fs.exec_direct('/usr/sbin/iptables-legacy-save'), '');
|
||||
}).catch(function() {
|
||||
return L.resolveDefault(fs.exec_direct('/usr/sbin/iptables-save'), '');
|
||||
}),
|
||||
fs.stat('/usr/sbin/ip6tables-legacy-save').then(function() {
|
||||
return L.resolveDefault(fs.exec_direct('/usr/sbin/ip6tables-legacy-save'), '');
|
||||
}).catch(function() {
|
||||
return L.resolveDefault(fs.exec_direct('/usr/sbin/ip6tables-save'), '');
|
||||
})
|
||||
]);
|
||||
},
|
||||
|
||||
isActionExpression(expr) {
|
||||
for (let k in expr) {
|
||||
if (expr.hasOwnProperty(k)) {
|
||||
switch (k) {
|
||||
case 'accept':
|
||||
case 'notrack':
|
||||
case 'reject':
|
||||
case 'drop':
|
||||
case 'jump':
|
||||
case 'goto':
|
||||
case 'continue':
|
||||
case 'snat':
|
||||
case 'dnat':
|
||||
case 'redirect':
|
||||
case 'mangle':
|
||||
case 'masquerade':
|
||||
case 'return':
|
||||
case 'flow':
|
||||
case 'ct helper':
|
||||
case 'log':
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
exprToKey(expr) {
|
||||
let kind, spec;
|
||||
|
||||
if (!Array.isArray(expr) && typeof(expr) == 'object') {
|
||||
for (let k in expr) {
|
||||
if (expr.hasOwnProperty(k)) {
|
||||
kind = k;
|
||||
spec = expr[k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (kind || '-') {
|
||||
case 'meta':
|
||||
case 'ct':
|
||||
case 'rt':
|
||||
return '%h.%h'.format(kind, spec.key);
|
||||
|
||||
case 'tcp option':
|
||||
return 'tcpoption.%h.%h'.format(spec.name, spec.field);
|
||||
|
||||
case 'reject':
|
||||
return 'reject.%h'.format(spec.type);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
exprToString(expr, hint) {
|
||||
let kind, spec;
|
||||
|
||||
if (typeof(expr) != 'object') {
|
||||
let s;
|
||||
|
||||
if (hint)
|
||||
s = expr_translations['%s.%h'.format(hint, expr)];
|
||||
|
||||
return s || '%h'.format(expr);
|
||||
}
|
||||
|
||||
if (Array.isArray(expr)) {
|
||||
kind = 'list';
|
||||
spec = expr;
|
||||
}
|
||||
else {
|
||||
for (let k in expr) {
|
||||
if (expr.hasOwnProperty(k)) {
|
||||
kind = k;
|
||||
spec = expr[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!kind)
|
||||
return '';
|
||||
|
||||
const items = [];
|
||||
const lis = [];
|
||||
let tpl, k;
|
||||
|
||||
switch (kind) {
|
||||
case 'prefix':
|
||||
return '%h/%d'.format(spec.addr, spec.len);
|
||||
|
||||
case 'set':
|
||||
case 'list':
|
||||
for (var i = 0; i < spec.length; i++) {
|
||||
items.push('<span class="nft-set-item">%s</span>'.format(this.exprToString(spec[i])));
|
||||
lis.push('<span class="ifacebadge">%s</span>'.format(this.exprToString(spec[i])));
|
||||
}
|
||||
|
||||
|
||||
if (kind == 'set')
|
||||
tpl = '<div class="nft-set cbi-tooltip-container">{ <span class="nft-set-items">%s</span> }<div class="cbi-tooltip">%s</div></div>';
|
||||
else
|
||||
tpl = '<div class="nft-list cbi-tooltip-container"><span class="nft-list-items">%s</span><div class="cbi-tooltip">%s</div></div>';
|
||||
|
||||
return tpl.format(items.join(', '), lis.join('<br />'));
|
||||
|
||||
case 'concat':
|
||||
|
||||
for (let s of spec)
|
||||
items.push(this.exprToString(s));
|
||||
|
||||
return items.join('+');
|
||||
|
||||
case 'range':
|
||||
return '%s-%s'.format(this.exprToString(spec[0], hint), this.exprToString(spec[1], hint));
|
||||
|
||||
case 'payload':
|
||||
if (spec.protocol && spec.field) {
|
||||
k = '%h.%h'.format(spec.protocol, spec.field);
|
||||
return expr_translations[k] || '<em>%s</em>'.format(k);
|
||||
}
|
||||
else if (spec.base && spec.offset != null && spec.len != null) {
|
||||
k = 'payload.%h'.format(spec.base);
|
||||
return (expr_translations[k] || '<em>@%s,%%d,%%d</em>'.format(spec.base)).format(spec.offset + 1, spec.offset + spec.len + 1);
|
||||
}
|
||||
|
||||
return 'payload: %s'.format(kind, JSON.stringify(spec));
|
||||
|
||||
case '&':
|
||||
case '|':
|
||||
case '^':
|
||||
return '%s %h %s'.format(
|
||||
this.exprToString(spec[0], hint),
|
||||
kind,
|
||||
Array.isArray(spec[1]) ? '(%h)'.format(spec[1].join('|')) : this.exprToString(spec[1], hint));
|
||||
|
||||
default:
|
||||
k = this.exprToKey(expr);
|
||||
|
||||
if (k)
|
||||
return expr_translations[k] || '<em>%s</em>'.format(k);
|
||||
|
||||
return '%s: %s'.format(kind, JSON.stringify(spec));
|
||||
}
|
||||
},
|
||||
|
||||
renderVMap(spec, table) {
|
||||
// spec: { key: {...}, data: { set: [ [mapkey, actionSpec], ... ] } }
|
||||
const matchElem = E('span', { 'class': 'ifacebadge' },
|
||||
_('Verdict map: <var>%h</var> is').format(this.exprToString(spec.key)));
|
||||
|
||||
const actions = [];
|
||||
const keys = [];
|
||||
|
||||
if (spec && spec.data && Array.isArray(spec.data.set)) {
|
||||
// For each mapping entry, render key and the action
|
||||
for (var i = 0; i < spec.data.set.length; i++) {
|
||||
const entry = spec.data.set[i];
|
||||
const mapKey = entry[0];
|
||||
const actionSpec = entry[1];
|
||||
|
||||
// if actionSpec is an action (accept/drop/jump/...), render it as action element
|
||||
if (typeof(actionSpec) == 'object' && this.isActionExpression(actionSpec)) {
|
||||
// renderExpr expects an expr object like { accept: null } or { jump: {...} }
|
||||
const renderedAction = this.renderExpr(actionSpec, table);
|
||||
|
||||
actions.push(E('div', { }, [
|
||||
E('span', [ this.exprToString(mapKey) + ':' ]),
|
||||
' ',
|
||||
renderedAction
|
||||
]));
|
||||
keys.push('<strong>%s</strong>'.format(this.exprToString(mapKey)));
|
||||
} else {
|
||||
// fallback: render the actionSpec as generic JSON tooltip
|
||||
actions.push(E('span', { 'class': 'ifacebadge', 'data-tooltip': JSON.stringify(actionSpec) },
|
||||
this.exprToString(mapKey) + ':' + this.exprToString(actionSpec)));
|
||||
keys.push('<strong>%s</strong>'.format(this.exprToString(mapKey)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dom.append(matchElem, [ ' ', E('span', {}, keys.join(_(' or '))) ]);
|
||||
}
|
||||
|
||||
return { match: matchElem, actions: actions };
|
||||
},
|
||||
|
||||
renderMatchExpr(spec) {
|
||||
switch (spec.op) {
|
||||
case '==':
|
||||
case '!=':
|
||||
if ((typeof(spec.right) == 'object' && spec.right.set) ||
|
||||
(typeof(spec.right) == 'string' && spec.right.charAt(0) == '@'))
|
||||
spec.op = (spec.op == '==') ? 'in_set' : 'not_in_set';
|
||||
|
||||
break;
|
||||
|
||||
case 'in':
|
||||
if (typeof(spec.right) != 'object')
|
||||
spec.op = '==';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return E('span', { 'class': 'ifacebadge' },
|
||||
(op_translations[spec.op] || '<var>%%s</var> %h <strong>%%s</strong>'.format(spec.op)).format(
|
||||
this.exprToString(spec.left),
|
||||
this.exprToString(spec.right, this.exprToKey(spec.left))
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
renderNatFlags(spec) {
|
||||
const f = [];
|
||||
|
||||
if (spec && Array.isArray(spec.flags)) {
|
||||
for (let sf of spec.flags)
|
||||
f.push(expr_translations['natflag.%h'.format(sf)] || sf);
|
||||
}
|
||||
|
||||
return f.length ? E('small', { 'class': 'cbi-tooltip-container' }, [
|
||||
' (',
|
||||
N_(f.length, '1 flag', '%d flags', 'nft amount of flags').format(f.length),
|
||||
')',
|
||||
E('span', { 'class': 'cbi-tooltip' }, f.join('<br />'))
|
||||
]) : E([]);
|
||||
},
|
||||
|
||||
renderRateUnit(value, unit) {
|
||||
if (!unit)
|
||||
unit = 'packets';
|
||||
|
||||
return '%d\xa0%s'.format(
|
||||
value,
|
||||
expr_translations['unit.%h'.format(unit)] || unit
|
||||
);
|
||||
},
|
||||
|
||||
renderExpr(expr, table) {
|
||||
let kind, spec;
|
||||
|
||||
for (let k in expr) {
|
||||
if (expr.hasOwnProperty(k)) {
|
||||
kind = k;
|
||||
spec = expr[k];
|
||||
}
|
||||
}
|
||||
|
||||
if (!kind)
|
||||
return E([]);
|
||||
|
||||
let k;
|
||||
let a = [];
|
||||
|
||||
switch (kind) {
|
||||
case 'match':
|
||||
return this.renderMatchExpr(spec);
|
||||
|
||||
case 'ct helper':
|
||||
return E('span', {
|
||||
'class': 'ifacebadge'
|
||||
}, action_translations[kind].format(spec));
|
||||
|
||||
case 'reject':
|
||||
k = 'reject.%s'.format(spec.type);
|
||||
|
||||
return E('span', {
|
||||
'class': 'ifacebadge'
|
||||
}, (action_translations[k] || k).format(this.exprToString(spec.expr)));
|
||||
|
||||
case 'accept':
|
||||
case 'notrack':
|
||||
case 'drop':
|
||||
return E('span', {
|
||||
'class': 'ifacebadge'
|
||||
}, action_translations[kind] || '<em>%h</em>'.format(kind));
|
||||
|
||||
case 'jump':
|
||||
return E('span', {
|
||||
'class': 'ifacebadge'
|
||||
}, action_translations.jump.format(table, spec.target, spec.target));
|
||||
|
||||
case 'goto':
|
||||
return E('span', {
|
||||
'class': 'ifacebadge'
|
||||
}, action_translations.goto.format(table, spec.target, spec.target));
|
||||
|
||||
case 'continue':
|
||||
return E('span', {
|
||||
'class': 'ifacebadge'
|
||||
}, action_translations.continue);
|
||||
|
||||
case 'return':
|
||||
return E('span', {
|
||||
'class': 'ifacebadge'
|
||||
}, action_translations.return);
|
||||
|
||||
case 'snat':
|
||||
case 'dnat':
|
||||
k = '%h.%h'.format(kind, spec.family);
|
||||
|
||||
if (spec.addr) {
|
||||
k += '.addr';
|
||||
a.push(this.exprToString(spec.addr));
|
||||
}
|
||||
|
||||
if (spec.port) {
|
||||
k += '.port';
|
||||
a.push(this.exprToString(spec.port));
|
||||
}
|
||||
|
||||
return E('span', { 'class': 'ifacebadge' }, [
|
||||
E('span', ''.format.apply(action_translations[k] || k, a)),
|
||||
this.renderNatFlags(spec)
|
||||
]);
|
||||
|
||||
case 'redirect':
|
||||
k = 'redirect';
|
||||
|
||||
if (spec && spec.port) {
|
||||
k += '.port';
|
||||
a.push(this.exprToString(spec.port));
|
||||
}
|
||||
|
||||
return E('span', { 'class': 'ifacebadge' }, [
|
||||
E('span', ''.format.apply(action_translations[k] || k, a)),
|
||||
this.renderNatFlags(spec)
|
||||
]);
|
||||
|
||||
case 'masquerade':
|
||||
return E('span', { 'class': 'ifacebadge' }, [
|
||||
E('span', action_translations.masquerade),
|
||||
this.renderNatFlags(spec)
|
||||
]);
|
||||
|
||||
case 'mangle':
|
||||
return E('span', { 'class': 'ifacebadge' },
|
||||
action_translations.mangle.format(
|
||||
this.exprToString(spec.key),
|
||||
this.exprToString(spec.value)
|
||||
));
|
||||
|
||||
case 'limit':
|
||||
k = 'limit';
|
||||
a = [
|
||||
this.renderRateUnit(spec.rate, spec.rate_unit),
|
||||
expr_translations['unit.%h'.format(spec.per)] || spec.per
|
||||
];
|
||||
|
||||
if (spec.inv)
|
||||
k += '.inv';
|
||||
|
||||
if (spec.burst) {
|
||||
k += '.burst';
|
||||
a.push(this.renderRateUnit(spec.burst, spec.burst_unit));
|
||||
}
|
||||
|
||||
return E('span', { 'class': 'ifacebadge', 'cbi-tooltip': JSON.stringify(spec) },
|
||||
''.format.apply(action_translations[k] || k, a));
|
||||
|
||||
case 'flow':
|
||||
return E('span', {
|
||||
'class': 'ifacebadge'
|
||||
}, action_translations.flow.format(spec.flowtable.replace(/^@/, '')));
|
||||
|
||||
case 'log':
|
||||
return E('span', {
|
||||
'class': 'ifacebadge'
|
||||
}, spec?.prefix ?
|
||||
''.format.apply(action_translations['log.prefix'], [spec?.prefix])
|
||||
: action_translations.log);
|
||||
|
||||
default:
|
||||
return E('span', {
|
||||
'class': 'ifacebadge',
|
||||
'data-tooltip': JSON.stringify(spec)
|
||||
}, [ '{ ', E('strong', {}, [ kind ]), ' }' ]);
|
||||
}
|
||||
},
|
||||
|
||||
renderCounter(data) {
|
||||
return E('span', { 'class': 'ifacebadge cbi-tooltip-container nft-counter' }, [
|
||||
E('var', [ '%.1024mB'.format(data.bytes) ]),
|
||||
E('div', { 'class': 'cbi-tooltip' }, [
|
||||
_('Traffic matched by rule: %.1000mPackets, %.1024mBytes', 'nft counter').format(data.packets, data.bytes)
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderComment(comment) {
|
||||
return E('span', { 'class': 'ifacebadge cbi-tooltip-container nft-comment' }, [
|
||||
E('var', [ '#' ]),
|
||||
E('div', { 'class': 'cbi-tooltip' }, [
|
||||
_('Rule comment: %s', 'nft comment').format(comment.replace(/^!fw4: /, ''))
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderRule(data, spec) {
|
||||
let empty = true;
|
||||
|
||||
const row = E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td', 'style': 'width:60%' }),
|
||||
E('td', { 'class': 'td', 'style': 'width:40%' })
|
||||
]);
|
||||
|
||||
if (Array.isArray(spec.expr)) {
|
||||
for (let se of spec.expr) {
|
||||
// nftables JSON format bug, `flow` targets are currently not properly serialized
|
||||
if (typeof(se) == 'string' && se.match(/^flow add (@\S+)$/))
|
||||
se = { flow: { op: "add", flowtable: RegExp.$1 } };
|
||||
|
||||
// vmap special handling
|
||||
if (se && se.vmap) {
|
||||
const vm = this.renderVMap(se.vmap, spec.table);
|
||||
|
||||
// add match summary to left column
|
||||
dom.append(row.childNodes[0], [ vm.match ]);
|
||||
empty = false;
|
||||
|
||||
if (typeof(se) == 'object' && se.counter) {
|
||||
row.childNodes[0].appendChild(
|
||||
this.renderCounter(se.counter));
|
||||
}
|
||||
|
||||
// append each mapped action to the actions column
|
||||
for (let vma of vm.actions)
|
||||
dom.append(row.childNodes[1], [ vma ]);
|
||||
continue;
|
||||
}
|
||||
|
||||
const res = this.renderExpr(se, spec.table);
|
||||
|
||||
if (typeof(se) == 'object' && se.counter) {
|
||||
row.childNodes[0].insertBefore(
|
||||
this.renderCounter(se.counter),
|
||||
row.childNodes[0].firstChild);
|
||||
}
|
||||
else if (this.isActionExpression(se)) {
|
||||
dom.append(row.childNodes[1], [ res ]);
|
||||
}
|
||||
else {
|
||||
dom.append(row.childNodes[0], [ res ]);
|
||||
empty = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (spec.comment) {
|
||||
row.childNodes[0].insertBefore(
|
||||
this.renderComment(spec.comment),
|
||||
row.childNodes[0].firstChild);
|
||||
}
|
||||
|
||||
if (empty)
|
||||
dom.append(row.childNodes[0], E('span', { 'class': 'ifacebadge' }, '<em>%h</em>'.format(_('Any packet', 'nft match any traffic'))));
|
||||
|
||||
return row;
|
||||
},
|
||||
|
||||
renderChain(data, spec) {
|
||||
let title, policy, hook;
|
||||
|
||||
switch (spec.type) {
|
||||
case 'filter':
|
||||
title = _('Traffic filter chain "%h"').format(spec.name);
|
||||
break;
|
||||
|
||||
case 'route':
|
||||
title = _('Route action chain "%h"').format(spec.name);
|
||||
break;
|
||||
|
||||
case 'nat':
|
||||
title = _('NAT action chain "%h"').format(spec.name);
|
||||
break;
|
||||
|
||||
default:
|
||||
title = _('Rule container chain "%h"').format(spec.name);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (spec.policy) {
|
||||
case 'drop':
|
||||
policy = _('Drop unmatched packets', 'Chain policy: drop');
|
||||
break;
|
||||
|
||||
default:
|
||||
policy = _('Continue processing unmatched packets', 'Chain policy: accept');
|
||||
break;
|
||||
}
|
||||
|
||||
switch (spec.hook) {
|
||||
case 'ingress':
|
||||
hook = _('Capture packets directly after the NIC received them', 'Chain hook: ingress');
|
||||
break;
|
||||
|
||||
case 'prerouting':
|
||||
hook = _('Capture incoming packets before any routing decision', 'Chain hook: prerouting');
|
||||
break;
|
||||
|
||||
case 'input':
|
||||
hook = _('Capture incoming packets routed to the local system', 'Chain hook: input');
|
||||
break;
|
||||
|
||||
case 'forward':
|
||||
hook = _('Capture incoming packets addressed to other hosts', 'Chain hook: forward');
|
||||
break;
|
||||
|
||||
case 'output':
|
||||
hook = _('Capture outgoing packets originating from the local system', 'Chain hook: output');
|
||||
break;
|
||||
|
||||
case 'postrouting':
|
||||
hook = _('Capture outgoing packets after any routing decision', 'Chain hook: postrouting');
|
||||
break;
|
||||
|
||||
default:
|
||||
hook = _('Chain hook "%h"', 'Yet unknown nftables chain hook').format(spec.hook);
|
||||
break;
|
||||
}
|
||||
|
||||
const node = E('div', { 'class': 'nft-chain' }, [
|
||||
E('h4', {
|
||||
'id': '%h.%h'.format(spec.table, spec.name)
|
||||
}, [ title ])
|
||||
]);
|
||||
|
||||
if (spec.hook) {
|
||||
node.appendChild(E('div', { 'class': 'nft-chain-hook' }, [
|
||||
E('ul', {}, [
|
||||
E('li', {}, _('Hook: <strong>%h</strong> (%h), Priority: <strong>%d</strong>', 'Chain hook description').format(spec.hook, hook, spec.prio)),
|
||||
E('li', {}, _('Policy: <strong>%h</strong> (%h)', 'Chain hook policy').format(spec.policy, policy))
|
||||
])
|
||||
]));
|
||||
}
|
||||
|
||||
node.appendChild(E('table', { 'class': 'nft-rules table cbi-section-table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th', 'style': 'width:60%' }, [ _('Rule matches') ]),
|
||||
E('th', { 'class': 'th', 'style': 'width:40%' }, [ _('Rule actions') ])
|
||||
])
|
||||
]));
|
||||
|
||||
for (let d of data)
|
||||
if (typeof(d.rule) == 'object' && d.rule.table == spec.table && d.rule.chain == spec.name && d.rule.family == spec.family)
|
||||
node.lastElementChild.appendChild(this.renderRule(data, d.rule));
|
||||
|
||||
if (node.lastElementChild.childNodes.length == 1)
|
||||
node.lastElementChild.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td center', 'colspan': 3 }, [
|
||||
E('em', [ _('No rules in this chain', 'nft chain is empty') ])
|
||||
])
|
||||
]));
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
renderTable(data, spec) {
|
||||
let title;
|
||||
|
||||
switch (spec.family) {
|
||||
case 'ip':
|
||||
title = _('IPv4 traffic table "%h"').format(spec.name);
|
||||
break;
|
||||
|
||||
case 'ip6':
|
||||
title = _('IPv6 traffic table "%h"').format(spec.name);
|
||||
break;
|
||||
|
||||
case 'inet':
|
||||
title = _('IPv4/IPv6 traffic table "%h"').format(spec.name);
|
||||
break;
|
||||
|
||||
case 'arp':
|
||||
title = _('ARP traffic table "%h"').format(spec.name);
|
||||
break;
|
||||
|
||||
case 'bridge':
|
||||
title = _('Bridge traffic table "%h"').format(spec.name);
|
||||
break;
|
||||
|
||||
case 'netdev':
|
||||
title = _('Network device table "%h"').format(spec.name);
|
||||
break;
|
||||
|
||||
default:
|
||||
title = _('"%h" table "%h"', 'Yet unknown nftables table family ("family" table "name")').format(spec.family, spec.name);
|
||||
break;
|
||||
}
|
||||
|
||||
const node = E([], [
|
||||
E('style', { 'type': 'text/css' }, [
|
||||
'.nft-rules .ifacebadge { margin: .125em }',
|
||||
'.nft-rules tr > td { padding: .25em !important }',
|
||||
'.nft-set, .nft-list { display: inline-block; vertical-align: middle }',
|
||||
'.nft-set-items, .nft-list-items { display: inline-block; vertical-align: middle; max-width: 200px; overflow: hidden; text-overflow: ellipsis }',
|
||||
'.ifacebadge.cbi-tooltip-container { cursor: help }',
|
||||
'.ifacebadge.cbi-tooltip-container .cbi-tooltip { padding: .5em }'
|
||||
]),
|
||||
E('div', { 'class': 'nft-table' }, [
|
||||
E('h3', [ title ]),
|
||||
E('div', { 'class': 'nft-chains' })
|
||||
])
|
||||
]);
|
||||
|
||||
for (let d of data)
|
||||
if (typeof(d.chain) == 'object' && d.chain.table == spec.name && d.chain.family == spec.family)
|
||||
node.lastElementChild.lastElementChild.appendChild(this.renderChain(data, d.chain));
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
checkLegacyRules(ipt4save, ipt6save) {
|
||||
if (ipt4save.match(/\n-A /) || ipt6save.match(/\n-A /)) {
|
||||
ui.addNotification(_('Legacy rules detected'), [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button',
|
||||
'click': function() { location.href = 'nftables/iptables' }
|
||||
}, _('Open iptables rules overview…'))
|
||||
], 'warning');
|
||||
}
|
||||
},
|
||||
|
||||
render([nft, ipt, ipt6]) {
|
||||
const view = E('div');
|
||||
let has_tables = false;
|
||||
|
||||
this.checkLegacyRules(ipt, ipt6);
|
||||
|
||||
if (Array.isArray(nft.nftables))
|
||||
for (let t of nft.nftables)
|
||||
if (t.hasOwnProperty('table'))
|
||||
has_tables = true;
|
||||
|
||||
if (!has_tables) {
|
||||
window.location.replace(L.url('admin/status/nftables/iptables'));
|
||||
return E('em', _('Redirecting to iptables rules overview...'));
|
||||
}
|
||||
|
||||
for (let t of nft.nftables)
|
||||
if (t.hasOwnProperty('table'))
|
||||
view.appendChild(this.renderTable(nft.nftables, t.table));
|
||||
|
||||
return view;
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require fs';
|
||||
'require ui';
|
||||
'require rpc';
|
||||
|
||||
var callLuciProcessList = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getProcessList',
|
||||
expect: { result: [] }
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return callLuciProcessList();
|
||||
},
|
||||
|
||||
handleSignal: function(signum, pid, ev) {
|
||||
return fs.exec('/bin/kill', ['-%d'.format(signum), '%s'.format(pid)]).then(L.bind(function() {
|
||||
return callLuciProcessList().then(L.bind(function(processes) {
|
||||
this.updateTable('.table', processes);
|
||||
}, this));
|
||||
}, this)).catch(function(e) { ui.addNotification(null, E('p', e.message)) });
|
||||
},
|
||||
|
||||
updateTable: function(table, processes) {
|
||||
var rows = [];
|
||||
|
||||
processes.sort(function(a, b) {
|
||||
return (a.PID - b.PID);
|
||||
});
|
||||
|
||||
for (var i = 0; i < processes.length; i++) {
|
||||
var proc = processes[i];
|
||||
|
||||
rows.push([
|
||||
proc.PID,
|
||||
proc.USER,
|
||||
E('span', {'style': 'word-break: break-word'}, proc.COMMAND),
|
||||
proc['%CPU'],
|
||||
proc['%MEM'],
|
||||
E('div', {}, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-action',
|
||||
'click': ui.createHandlerFn(this, 'handleSignal', 1, proc.PID)
|
||||
}, _('Hang Up')), ' ',
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-negative',
|
||||
'click': ui.createHandlerFn(this, 'handleSignal', 15, proc.PID)
|
||||
}, _('Terminate')), ' ',
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-negative',
|
||||
'click': ui.createHandlerFn(this, 'handleSignal', 9, proc.PID)
|
||||
}, _('Kill'))
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
cbi_update_table(table, rows, E('em', _('No information available')));
|
||||
},
|
||||
|
||||
render: function(processes) {
|
||||
var v = E([], [
|
||||
E('h2', _('Processes')),
|
||||
E('div', { 'class': 'cbi-map-descr' }, _('This list gives an overview over currently running system processes and their status.')),
|
||||
|
||||
E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, _('PID')),
|
||||
E('th', { 'class': 'th' }, _('Owner')),
|
||||
E('th', { 'class': 'th' }, _('Command')),
|
||||
E('th', { 'class': 'th' }, _('CPU usage (%)')),
|
||||
E('th', { 'class': 'th' }, _('Memory usage (%)')),
|
||||
E('th', { 'class': 'th center nowrap cbi-section-actions' })
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
this.updateTable(v.lastElementChild, processes);
|
||||
|
||||
return v;
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,282 @@
|
||||
'use strict';
|
||||
'require fs';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require validation';
|
||||
'require view';
|
||||
|
||||
const callNetworkInterfaceDump = rpc.declare({
|
||||
object: 'network.interface',
|
||||
method: 'dump',
|
||||
expect: { interface: [] }
|
||||
});
|
||||
|
||||
const callUfpList = rpc.declare({
|
||||
object: 'fingerprint',
|
||||
method: 'fingerprint',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
function applyMask(addr, mask, v6) {
|
||||
const words = v6 ? validation.parseIPv6(addr) : validation.parseIPv4(addr);
|
||||
const bword = v6 ? 0xffff : 0xff;
|
||||
const bwlen = v6 ? 16 : 8;
|
||||
|
||||
if (!words || mask < 0 || mask > (v6 ? 128 : 32))
|
||||
return null;
|
||||
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
const b = Math.min(mask, bwlen);
|
||||
words[i] &= (bword << (bwlen - b)) & bword;
|
||||
mask -= b;
|
||||
}
|
||||
|
||||
return String.prototype.format.apply(
|
||||
v6 ? '%x:%x:%x:%x:%x:%x:%x:%x' : '%d.%d.%d.%d', words);
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
return Promise.all([
|
||||
callNetworkInterfaceDump(),
|
||||
L.resolveDefault(fs.exec('/sbin/ip', [ '-4', 'neigh', 'show' ]), { stdout: '' }),
|
||||
L.resolveDefault(fs.exec('/sbin/ip', [ '-4', 'route', 'show', 'table', 'all' ]), { stdout: '' }),
|
||||
L.resolveDefault(fs.exec('/sbin/ip', [ '-4', 'rule', 'show' ]), { stdout: '' }),
|
||||
L.resolveDefault(fs.exec('/sbin/ip', [ '-6', 'neigh', 'show' ]), { stdout: '' }),
|
||||
L.resolveDefault(fs.exec('/sbin/ip', [ '-6', 'route', 'show', 'table', 'all' ]), { stdout: '' }),
|
||||
L.resolveDefault(fs.exec('/sbin/ip', [ '-6', 'rule', 'show' ]), { stdout: '' }),
|
||||
L.hasSystemFeature('ufpd') ? callUfpList() : null
|
||||
]);
|
||||
},
|
||||
|
||||
getNetworkByDevice(networks, dev, addr, mask, v6) {
|
||||
const addr_arrays = [ 'ipv4-address', 'ipv6-address', 'ipv6-prefix', 'ipv6-prefix-assignment', 'route' ];
|
||||
let matching_iface = null;
|
||||
let matching_prefix = -1;
|
||||
|
||||
for (const net of networks) {
|
||||
if (!L.isObject(net) || (net.l3_device !== dev && net.device !== dev))
|
||||
continue;
|
||||
|
||||
for (const key of addr_arrays) {
|
||||
const list = net[key];
|
||||
if (!Array.isArray(list)) continue;
|
||||
|
||||
for (const { address, target, mask: cmp_mask } of list) {
|
||||
const cmp_addr = address || target;
|
||||
if (!cmp_addr) continue;
|
||||
|
||||
if (applyMask(cmp_addr, cmp_mask, v6) !== applyMask(addr, cmp_mask, v6) || mask < cmp_mask)
|
||||
continue;
|
||||
|
||||
if (cmp_mask > matching_prefix) {
|
||||
matching_iface = net.interface;
|
||||
matching_prefix = cmp_mask;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matching_iface;
|
||||
},
|
||||
|
||||
parseNeighbs(nbs, macs, networks, v6) {
|
||||
if (!nbs) return [];
|
||||
const res = [];
|
||||
|
||||
for (const line of nbs.trim().split(/\n/)) {
|
||||
const [, addr = null, f = [], state = null] = line.match(/^([0-9a-f:.]+) (.+) (\S+) *$/);
|
||||
const flags = f?.trim?.().split?.(/\s+/);
|
||||
let vendor;
|
||||
|
||||
if (!addr || !state || addr.match(/^fe[89a-f][0-9a-f]:/))
|
||||
continue;
|
||||
|
||||
for (let j = 0; j < flags.length; j += 2)
|
||||
flags[flags[j]] = flags[j + 1];
|
||||
|
||||
if (!flags.lladdr)
|
||||
continue;
|
||||
|
||||
for (let mac in macs) {
|
||||
if (flags.lladdr === mac)
|
||||
vendor = macs[mac].vendor;
|
||||
}
|
||||
|
||||
const net = this.getNetworkByDevice(networks, flags.dev, addr, v6 ? 128 : 32, v6);
|
||||
|
||||
res.push([
|
||||
addr,
|
||||
vendor ? flags.lladdr.toUpperCase() + ` (${vendor})` : flags.lladdr.toUpperCase(),
|
||||
E('span', { 'class': 'ifacebadge' }, [ net ? net : '(%s)'.format(flags.dev) ]),
|
||||
]);
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
|
||||
parseRoutes(routes, networks, v6) {
|
||||
if (!routes) return [];
|
||||
const res = [];
|
||||
|
||||
for (const line of routes.trim().split(/\n/)) {
|
||||
const [, type = 'unicast', d, f = [] ] = line.match(/^(?:([a-z_]+|\d+) )?(default|[0-9a-f:./]+) (.+)$/);
|
||||
const dest = d == 'default' ? (v6 ? '::/0' : '0.0.0.0/0') : d;
|
||||
const flags = f?.trim?.().split?.(/\s+/);
|
||||
|
||||
if (!dest || type != 'unicast' || dest == 'fe80::/64' || dest == 'ff00::/8')
|
||||
continue;
|
||||
|
||||
for (let j = 0; j < flags.length; j += 2)
|
||||
flags[flags[j]] = flags[j + 1];
|
||||
|
||||
const [addr, bits = (v6 ? 128 : 32)] = dest.split('/');
|
||||
const net = this.getNetworkByDevice(networks, flags.dev, addr, bits, v6);
|
||||
|
||||
res.push([
|
||||
E('span', { 'class': 'ifacebadge' }, [ net ? net : '(%s)'.format(flags.dev) ]),
|
||||
dest,
|
||||
flags.via || '-',
|
||||
flags.src || flags.from || '-',
|
||||
String(flags.metric || 0),
|
||||
flags.table || 'main',
|
||||
flags.proto,
|
||||
]);
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
|
||||
parseRules: rules => rules?.trim()?.split('\n')?.map(l => {
|
||||
const [, prio=null, rule=null] = l.match(/^(\d+):\s+(.+)$/) || [];
|
||||
return [prio, rule];
|
||||
}),
|
||||
|
||||
render([
|
||||
networks,
|
||||
{ stdout: ip4neigh = '' } = {},
|
||||
{ stdout: ip4route = '' } = {},
|
||||
{ stdout: ip4rule = '' } = {},
|
||||
{ stdout: ip6neigh = '' } = {},
|
||||
{ stdout: ip6route = '' } = {},
|
||||
{ stdout: ip6rule = '' } = {},
|
||||
macdata,
|
||||
]) {
|
||||
|
||||
const device_title = _('Which is used to access this %s').format(_('Target'));
|
||||
const target_title = _('Network and its mask that define the size of the destination');
|
||||
const gateway_title = _('The address through which this %s is reachable').format(_('Target'));
|
||||
const metric_title = _('Quantifies the cost or distance to a destination in a way that allows routers to make informed decisions about the optimal path to forward data packets');
|
||||
const table_title = _('Common name or numeric ID of the %s in which this route is found').format(_('Table'));
|
||||
const proto_title = _('The routing protocol identifier of this route');
|
||||
const source_title = _('Network and its mask that define which source addresses use this route');
|
||||
|
||||
const neigh4tbl = E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, [ _('IP address') ]),
|
||||
E('th', { 'class': 'th' }, [ _('MAC address') ]),
|
||||
E('th', { 'class': 'th' }, [ _('Interface') ]),
|
||||
])
|
||||
]);
|
||||
|
||||
const route4tbl = E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th', 'title': device_title }, [ _('Device') ]),
|
||||
E('th', { 'class': 'th', 'title': target_title }, [ _('Target') ]),
|
||||
E('th', { 'class': 'th', 'title': gateway_title }, [ _('Gateway') ]),
|
||||
E('th', { 'class': 'th', 'title': source_title }, [ _('Source') ]),
|
||||
E('th', { 'class': 'th', 'title': metric_title }, [ _('Metric') ]),
|
||||
E('th', { 'class': 'th', 'title': table_title }, [ _('Table') ]),
|
||||
E('th', { 'class': 'th', 'title': proto_title }, [ _('Protocol') ]),
|
||||
])
|
||||
]);
|
||||
|
||||
const rule4tbl = E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, [ _('Priority') ]),
|
||||
E('th', { 'class': 'th' }, [ _('Rule') ]),
|
||||
])
|
||||
]);
|
||||
|
||||
const neigh6tbl = E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, [ _('IP address') ]),
|
||||
E('th', { 'class': 'th' }, [ _('MAC address') ]),
|
||||
E('th', { 'class': 'th' }, [ _('Interface') ]),
|
||||
])
|
||||
]);
|
||||
|
||||
const route6tbl = E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th', 'title': device_title }, [ _('Device') ]),
|
||||
E('th', { 'class': 'th', 'title': target_title }, [ _('Target') ]),
|
||||
E('th', { 'class': 'th', 'title': gateway_title }, [ _('Gateway') ]),
|
||||
E('th', { 'class': 'th', 'title': source_title }, [ _('Source') ]),
|
||||
E('th', { 'class': 'th', 'title': metric_title }, [ _('Metric') ]),
|
||||
E('th', { 'class': 'th', 'title': table_title }, [ _('Table') ]),
|
||||
E('th', { 'class': 'th', 'title': proto_title }, [ _('Protocol') ]),
|
||||
])
|
||||
]);
|
||||
|
||||
const rule6tbl = E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, [ _('Priority') ]),
|
||||
E('th', { 'class': 'th' }, [ _('Rule') ]),
|
||||
])
|
||||
]);
|
||||
|
||||
cbi_update_table(neigh4tbl, this.parseNeighbs(ip4neigh, macdata, networks, false),
|
||||
E('em', _('No entries available'))
|
||||
);
|
||||
cbi_update_table(route4tbl, this.parseRoutes(ip4route, networks, false),
|
||||
E('em', _('No entries available'))
|
||||
);
|
||||
cbi_update_table(rule4tbl, this.parseRules(ip4rule),
|
||||
E('em', _('No entries available'))
|
||||
);
|
||||
cbi_update_table(neigh6tbl, this.parseNeighbs(ip6neigh, macdata, networks, true),
|
||||
E('em', _('No entries available'))
|
||||
);
|
||||
cbi_update_table(route6tbl, this.parseRoutes(ip6route, networks, true),
|
||||
E('em', _('No entries available'))
|
||||
);
|
||||
cbi_update_table(rule6tbl, this.parseRules(ip6rule),
|
||||
E('em', _('No entries available'))
|
||||
);
|
||||
|
||||
const view = E([], [
|
||||
E('h2', {}, [ _('Routing') ]),
|
||||
E('p', {}, [ _('The following rules are currently active on this system.') ]),
|
||||
E('div', {}, [
|
||||
E('div', { 'class': 'cbi-section', 'data-tab': 'ipv4routing', 'data-tab-title': _('IPv4 Routing') }, [
|
||||
E('h3', {}, [ _('IPv4 Neighbours') ]),
|
||||
neigh4tbl,
|
||||
|
||||
E('h3', {}, [ _('Active IPv4 Routes') ]),
|
||||
route4tbl,
|
||||
|
||||
E('h3', {}, [ _('Active IPv4 Rules') ]),
|
||||
rule4tbl,
|
||||
]),
|
||||
E('div', { 'class': 'cbi-section', 'data-tab': 'ipv6routing', 'data-tab-title': _('IPv6 Routing') }, [
|
||||
E('h3', {}, [ _('IPv6 Neighbours') ]),
|
||||
neigh6tbl,
|
||||
|
||||
E('h3', {}, [ _('Active IPv6 Routes') ]),
|
||||
route6tbl,
|
||||
|
||||
E('h3', {}, [ _('Active IPv6 Rules') ]),
|
||||
rule6tbl,
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
ui.tabs.initTabGroup(view.lastElementChild.childNodes);
|
||||
|
||||
return view;
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,334 @@
|
||||
'use strict';
|
||||
'require fs';
|
||||
'require rpc';
|
||||
'require tools.network as tn';
|
||||
'require ui';
|
||||
'require validation';
|
||||
'require view';
|
||||
|
||||
const callNetworkInterfaceDump = rpc.declare({
|
||||
object: 'network.interface',
|
||||
method: 'dump',
|
||||
expect: { interface: [] }
|
||||
});
|
||||
|
||||
const callUfpList = rpc.declare({
|
||||
object: 'fingerprint',
|
||||
method: 'fingerprint',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
function applyMask(addr, mask, v6) {
|
||||
const words = v6 ? validation.parseIPv6(addr) : validation.parseIPv4(addr);
|
||||
const bword = v6 ? 0xffff : 0xff;
|
||||
const bwlen = v6 ? 16 : 8;
|
||||
|
||||
if (!words || mask < 0 || mask > (v6 ? 128 : 32))
|
||||
return null;
|
||||
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
const b = Math.min(mask, bwlen);
|
||||
words[i] &= (bword << (bwlen - b)) & bword;
|
||||
mask -= b;
|
||||
}
|
||||
|
||||
return String.prototype.format.apply(
|
||||
v6 ? '%x:%x:%x:%x:%x:%x:%x:%x' : '%d.%d.%d.%d', words);
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
return Promise.all([
|
||||
callNetworkInterfaceDump(),
|
||||
L.resolveDefault(fs.exec('/sbin/ip', [ '-4', '-j', 'neigh', 'show' ]), {}),
|
||||
L.resolveDefault(fs.exec('/sbin/ip', [ '-4', '-j', 'route', 'show', 'table', 'all' ]), {}),
|
||||
L.resolveDefault(fs.exec('/sbin/ip', [ '-4', '-j', 'rule', 'show' ]), {}),
|
||||
L.resolveDefault(fs.exec('/sbin/ip', [ '-6', '-j', 'neigh', 'show' ]), {}),
|
||||
L.resolveDefault(fs.exec('/sbin/ip', [ '-6', '-j', 'route', 'show', 'table', 'all' ]), {}),
|
||||
L.resolveDefault(fs.exec('/sbin/ip', [ '-6', '-j', 'rule', 'show' ]), {}),
|
||||
L.hasSystemFeature('ufpd') ? callUfpList() : null,
|
||||
]);
|
||||
},
|
||||
|
||||
getNetworkByDevice(networks, dev, addr, mask, v6) {
|
||||
const addr_arrays = [ 'ipv4-address', 'ipv6-address', 'ipv6-prefix', 'ipv6-prefix-assignment', 'route' ];
|
||||
let matching_iface = null;
|
||||
let matching_prefix = -1;
|
||||
|
||||
for (const net of networks) {
|
||||
if (!L.isObject(net) || (net.l3_device !== dev && net.device !== dev))
|
||||
continue;
|
||||
|
||||
for (const key of addr_arrays) {
|
||||
const list = net[key];
|
||||
if (!Array.isArray(list)) continue;
|
||||
|
||||
for (const { address, target, mask: cmp_mask } of list) {
|
||||
const cmp_addr = address || target;
|
||||
if (!cmp_addr) continue;
|
||||
|
||||
if (applyMask(cmp_addr, cmp_mask, v6) !== applyMask(addr, cmp_mask, v6) || mask < cmp_mask)
|
||||
continue;
|
||||
|
||||
if (cmp_mask > matching_prefix) {
|
||||
matching_iface = net.interface;
|
||||
matching_prefix = cmp_mask;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matching_iface;
|
||||
},
|
||||
|
||||
parseJSON(string) {
|
||||
try {
|
||||
return JSON.parse(string);
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
parseNeighbs(nbs, macs, networks, v6) {
|
||||
const res = [];
|
||||
|
||||
for (const n of this.parseJSON(nbs)) {
|
||||
let vendor;
|
||||
if (n.dst.match(/^fe[89a-f][0-9a-f]:/))
|
||||
continue;
|
||||
|
||||
if (n.state.find(f => {return f == 'FAILED'}))
|
||||
continue;
|
||||
|
||||
for (let mac in macs) {
|
||||
if (n?.lladdr === mac)
|
||||
vendor = macs[mac].vendor;
|
||||
}
|
||||
|
||||
const net = this.getNetworkByDevice(networks, n?.dev, n?.dst, v6 ? 128 : 32, v6);
|
||||
|
||||
res.push([
|
||||
E('div', { 'data-tooltip': JSON.stringify(n) }, [
|
||||
'#',
|
||||
n?.nud ? `; ${_('NUD')}: ${n?.nud}` : '',
|
||||
n?.proxy === null ? `; ${_('Proxy')}: ✅` : '',
|
||||
n?.nomaster === null ? `; ${_('No master')} : ✅` : '',
|
||||
n?.vrf ? `; ${_('VRF')}: ${n?.vrf}` : '',
|
||||
]),
|
||||
|
||||
n?.dst,
|
||||
n?.lladdr?.toUpperCase() + (vendor ? ` (${vendor})` : ''),
|
||||
E('span', { 'class': 'ifacebadge' }, [ net ? net : '(%s)'.format(n?.dev) ]),
|
||||
]);
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
|
||||
parseRoutes(routes, networks, v6) {
|
||||
const res = [];
|
||||
|
||||
for (const rt of this.parseJSON(routes)) {
|
||||
const dest = rt.dst == 'default' ? (v6 ? '::/0' : '0.0.0.0/0') : rt.dst;
|
||||
if (dest == 'fe80::/64' || dest == 'ff00::/8')
|
||||
continue;
|
||||
|
||||
const [addr, bits = (v6 ? 128 : 32)] = dest.split('/');
|
||||
const net = this.getNetworkByDevice(networks, rt.dev, addr, bits, v6);
|
||||
|
||||
res.push([
|
||||
E('span', { 'class': 'ifacebadge' }, [ net ? net : '(%s)'.format(rt.dev) ]),
|
||||
dest,
|
||||
rt?.gateway || '-',
|
||||
rt?.prefsrc || rt?.from || '-',
|
||||
String(rt?.metric || '-'),
|
||||
rt?.table || 'main',
|
||||
rt?.protocol,
|
||||
]);
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
|
||||
parseRules(rules) {
|
||||
const r = [];
|
||||
for (const rl of this.parseJSON(rules)) {
|
||||
r.push([
|
||||
E('div', { 'data-tooltip': JSON.stringify(rl) }, [
|
||||
'#',
|
||||
rl?.not === null ? `; ${_('Not')}: ✅` : '',
|
||||
rl?.nop === null ? `; ${_('No-op')}: ✅` : '',
|
||||
rl?.l3mdev === null ? `; ${_('L3Mdev')}: ✅` : '',
|
||||
rl?.fwmark ? `; ${_('Fwmark')}:${rl?.fwmark}` : '',
|
||||
rl?.from ? `; ${_('From')}:${rl?.from}` : '',
|
||||
rl?.to ? `; ${_('To')}:${rl?.to}` : '',
|
||||
rl?.tos ? `; ${_('ToS')}:${rl?.tos}` : '',
|
||||
rl?.dsfield ? `; ${_('DSCP')}:${rl?.dsfield}` : '',
|
||||
rl?.uidrange ? `; ${_('UID-range')}:${rl?.uidrange}` : '',
|
||||
rl?.goto ? `; ${_('goto')}:${rl?.goto}` : '',
|
||||
rl?.nat ? `; ${_('NAT')}:${rl?.nat}` : '',
|
||||
]),
|
||||
|
||||
rl?.priority,
|
||||
rl?.iif ? E('span', { 'class': 'ifacebadge' }, [ rl?.iif ]) : '-',
|
||||
rl?.src ? (rl?.srclen ? rl?.src + '/' + rl?.srclen : rl?.src) : _('any'),
|
||||
rl?.sport || '-',
|
||||
rl?.action || '-',
|
||||
tn.protocols.find(f => {return f.i == rl?.ipproto?.split?.('-')[1] })?.d || '-',
|
||||
rl?.oif ? E('span', { 'class': 'ifacebadge' }, [ rl?.oif ]) : '-',
|
||||
rl?.dst ? (rl?.dstlen ? rl?.dst + '/' + rl?.dstlen : rl?.dst) : _('any'),
|
||||
rl?.dport || '-',
|
||||
rl?.table || '-',
|
||||
]);
|
||||
}
|
||||
return r;
|
||||
},
|
||||
|
||||
render([
|
||||
networks,
|
||||
{ stdout: ip4neighbs = '' } = {},
|
||||
{ stdout: ip4routes = '' } = {},
|
||||
{ stdout: ip4rules = '' } = {},
|
||||
{ stdout: ip6neighbs = '' } = {},
|
||||
{ stdout: ip6routes = '' } = {},
|
||||
{ stdout: ip6rules = '' } = {},
|
||||
macdata,
|
||||
]) {
|
||||
|
||||
const device_title = _('Which is used to access this %s').format(_('Target'));
|
||||
const target_title = _('Network and its mask that define the size of the destination');
|
||||
const gateway_title = _('The address through which this %s is reachable').format(_('Target'));
|
||||
const metric_title = _('Quantifies the cost or distance to a destination in a way that allows routers to make informed decisions about the optimal path to forward data packets');
|
||||
const table_title = _('Common name or numeric ID of the %s in which this route is found').format(_('Table'));
|
||||
const proto_title = _('The routing protocol identifier of this route');
|
||||
const source_title = _('Network and its mask that define which source addresses use this route');
|
||||
|
||||
const neigh4tbl = E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, [ _('Entry') ]),
|
||||
E('th', { 'class': 'th' }, [ _('IP address') ]),
|
||||
E('th', { 'class': 'th' }, [ _('MAC address') ]),
|
||||
E('th', { 'class': 'th' }, [ _('Interface') ]),
|
||||
])
|
||||
]);
|
||||
|
||||
const route4tbl = E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th', 'title': device_title }, [ _('Device') ]),
|
||||
E('th', { 'class': 'th', 'title': target_title }, [ _('Target') ]),
|
||||
E('th', { 'class': 'th', 'title': gateway_title }, [ _('Gateway') ]),
|
||||
E('th', { 'class': 'th', 'title': source_title }, [ _('Source') ]),
|
||||
E('th', { 'class': 'th', 'title': metric_title }, [ _('Metric') ]),
|
||||
E('th', { 'class': 'th', 'title': table_title }, [ _('Table') ]),
|
||||
E('th', { 'class': 'th', 'title': proto_title }, [ _('Protocol') ]),
|
||||
])
|
||||
]);
|
||||
|
||||
const rule4tbl = E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, [ _('Rule') ]),
|
||||
E('th', { 'class': 'th' }, [ _('Priority') ]),
|
||||
E('th', { 'class': 'th' }, [ _('Ingress') ]),
|
||||
E('th', { 'class': 'th' }, [ _('Source') ]),
|
||||
E('th', { 'class': 'th' }, [ _('Src Port') ]),
|
||||
E('th', { 'class': 'th' }, [ _('Action') ]),
|
||||
E('th', { 'class': 'th' }, [ _('IP Protocol') ]),
|
||||
E('th', { 'class': 'th' }, [ _('Egress') ]),
|
||||
E('th', { 'class': 'th' }, [ _('Destination') ]),
|
||||
E('th', { 'class': 'th' }, [ _('Dest Port') ]),
|
||||
E('th', { 'class': 'th' }, [ _('Table') ]),
|
||||
])
|
||||
]);
|
||||
|
||||
const neigh6tbl = E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, [ _('Entry') ]),
|
||||
E('th', { 'class': 'th' }, [ _('IP address') ]),
|
||||
E('th', { 'class': 'th' }, [ _('MAC address') ]),
|
||||
E('th', { 'class': 'th' }, [ _('Interface') ]),
|
||||
])
|
||||
]);
|
||||
|
||||
const route6tbl = E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th', 'title': device_title }, [ _('Device') ]),
|
||||
E('th', { 'class': 'th', 'title': target_title }, [ _('Target') ]),
|
||||
E('th', { 'class': 'th', 'title': gateway_title }, [ _('Gateway') ]),
|
||||
E('th', { 'class': 'th', 'title': source_title }, [ _('Source') ]),
|
||||
E('th', { 'class': 'th', 'title': metric_title }, [ _('Metric') ]),
|
||||
E('th', { 'class': 'th', 'title': table_title }, [ _('Table') ]),
|
||||
E('th', { 'class': 'th', 'title': proto_title }, [ _('Protocol') ]),
|
||||
])
|
||||
]);
|
||||
|
||||
const rule6tbl = E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, [ _('Rule') ]),
|
||||
E('th', { 'class': 'th' }, [ _('Priority') ]),
|
||||
E('th', { 'class': 'th' }, [ _('Ingress') ]),
|
||||
E('th', { 'class': 'th' }, [ _('Source') ]),
|
||||
E('th', { 'class': 'th' }, [ _('Src Port') ]),
|
||||
E('th', { 'class': 'th' }, [ _('Action') ]),
|
||||
E('th', { 'class': 'th' }, [ _('IP Protocol') ]),
|
||||
E('th', { 'class': 'th' }, [ _('Egress') ]),
|
||||
E('th', { 'class': 'th' }, [ _('Destination') ]),
|
||||
E('th', { 'class': 'th' }, [ _('Dest Port') ]),
|
||||
E('th', { 'class': 'th' }, [ _('Table') ]),
|
||||
])
|
||||
]);
|
||||
|
||||
cbi_update_table(neigh4tbl, this.parseNeighbs(ip4neighbs, macdata, networks, false),
|
||||
E('em', _('No entries available'))
|
||||
);
|
||||
cbi_update_table(route4tbl, this.parseRoutes(ip4routes, networks, false),
|
||||
E('em', _('No entries available'))
|
||||
);
|
||||
cbi_update_table(rule4tbl, this.parseRules(ip4rules),
|
||||
E('em', _('No entries available'))
|
||||
);
|
||||
cbi_update_table(neigh6tbl, this.parseNeighbs(ip6neighbs, macdata, networks, true),
|
||||
E('em', _('No entries available'))
|
||||
);
|
||||
cbi_update_table(route6tbl, this.parseRoutes(ip6routes, networks, true),
|
||||
E('em', _('No entries available'))
|
||||
);
|
||||
cbi_update_table(rule6tbl, this.parseRules(ip6rules),
|
||||
E('em', _('No entries available'))
|
||||
);
|
||||
|
||||
const view = E([], [
|
||||
E('h2', {}, [ _('Routing') ]),
|
||||
E('p', {}, [ _('The following rules are currently active on this system.') ]),
|
||||
E('div', {}, [
|
||||
E('div', { 'class': 'cbi-section', 'data-tab': 'ipv4routing', 'data-tab-title': _('IPv4 Routing') }, [
|
||||
E('h3', {}, [ _('IPv4 Neighbours') ]),
|
||||
neigh4tbl,
|
||||
|
||||
E('h3', {}, [ _('Active IPv4 Routes') ]),
|
||||
route4tbl,
|
||||
|
||||
E('h3', {}, [ _('Active IPv4 Rules') ]),
|
||||
rule4tbl,
|
||||
]),
|
||||
E('div', { 'class': 'cbi-section', 'data-tab': 'ipv6routing', 'data-tab-title': _('IPv6 Routing') }, [
|
||||
E('h3', {}, [ _('IPv6 Neighbours') ]),
|
||||
neigh6tbl,
|
||||
|
||||
E('h3', {}, [ _('Active IPv6 Routes') ]),
|
||||
route6tbl,
|
||||
|
||||
E('h3', {}, [ _('Active IPv6 Rules') ]),
|
||||
rule6tbl,
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
ui.tabs.initTabGroup(view.lastElementChild.childNodes);
|
||||
|
||||
return view;
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
'use strict';
|
||||
'require tools.views as views';
|
||||
|
||||
return views.LogreadBox(null, _('System Log'));
|
||||
@@ -0,0 +1,350 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require poll';
|
||||
'require request';
|
||||
'require ui';
|
||||
'require rpc';
|
||||
'require network';
|
||||
|
||||
const callLuciRealtimeStats = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getRealtimeStats',
|
||||
params: [ 'mode', 'device' ],
|
||||
expect: { result: [] }
|
||||
});
|
||||
|
||||
const graphPolls = [], pollInterval = 3;
|
||||
|
||||
Math.log2 = Math.log2 || function(x) { return Math.log(x) * Math.LOG2E; };
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
return Promise.all([
|
||||
this.loadSVG(L.resource('svg/wireless.svg')),
|
||||
this.loadSVG(L.resource('svg/wifirate.svg')),
|
||||
network.getWifiDevices().then((radios) => {
|
||||
const tasks = [], all_networks = [];
|
||||
|
||||
for (const radio of radios) {
|
||||
if (radio.isDisabled()) continue;
|
||||
tasks.push(radio.getWifiNetworks().then((networks) => {
|
||||
for (const net of networks) {
|
||||
if (net.isDisabled()) continue;
|
||||
all_networks.push(net);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
return Promise.all(tasks).then(() => {
|
||||
return all_networks;
|
||||
});
|
||||
})
|
||||
]);
|
||||
},
|
||||
|
||||
updateGraph(ifname, svg, lines, cb) {
|
||||
const G = svg.firstElementChild;
|
||||
|
||||
const view = document.querySelector('#view');
|
||||
|
||||
const width = view.offsetWidth - 2;
|
||||
const height = 300 - 2;
|
||||
const step = 5;
|
||||
|
||||
const data_wanted = Math.floor(width / step);
|
||||
|
||||
const data_values = [];
|
||||
|
||||
for (const line of lines)
|
||||
if (line)
|
||||
data_values.push([]);
|
||||
|
||||
const info = {
|
||||
line_current: [],
|
||||
line_average: [],
|
||||
line_peak: []
|
||||
};
|
||||
|
||||
/* prefill datasets */
|
||||
for (let i = 0; i < data_values.length; i++)
|
||||
for (let j = 0; j < data_wanted; j++)
|
||||
data_values[i][j] = 0;
|
||||
|
||||
/* plot horizontal time interval lines */
|
||||
for (let i = width % (step * 60); i < width; i += step * 60) {
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
line.setAttribute('x1', i);
|
||||
line.setAttribute('y1', 0);
|
||||
line.setAttribute('x2', i);
|
||||
line.setAttribute('y2', '100%');
|
||||
line.setAttribute('style', 'stroke:black;stroke-width:0.1');
|
||||
|
||||
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
text.setAttribute('x', i + 5);
|
||||
text.setAttribute('y', 15);
|
||||
text.setAttribute('style', 'fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000');
|
||||
text.appendChild(document.createTextNode(Math.round((width - i) / step / 60) + 'm'));
|
||||
|
||||
G.appendChild(line);
|
||||
G.appendChild(text);
|
||||
}
|
||||
|
||||
info.interval = pollInterval;
|
||||
info.timeframe = data_wanted / 60;
|
||||
|
||||
graphPolls.push({
|
||||
ifname: ifname,
|
||||
svg: svg,
|
||||
lines: lines,
|
||||
cb: cb,
|
||||
info: info,
|
||||
width: width,
|
||||
height: height,
|
||||
step: step,
|
||||
values: data_values,
|
||||
timestamp: 0,
|
||||
fill: 1
|
||||
});
|
||||
},
|
||||
|
||||
pollData() {
|
||||
poll.add(L.bind(function() {
|
||||
const tasks = [];
|
||||
|
||||
for (const ctx of graphPolls) {
|
||||
tasks.push(L.resolveDefault(callLuciRealtimeStats('wireless', ctx.ifname), []));
|
||||
}
|
||||
|
||||
return Promise.all(tasks).then(L.bind(function(datasets) {
|
||||
for (let gi = 0; gi < graphPolls.length; gi++) {
|
||||
let ctx = graphPolls[gi];
|
||||
const data = datasets[gi];
|
||||
const values = ctx.values;
|
||||
const lines = ctx.lines;
|
||||
const info = ctx.info;
|
||||
|
||||
let data_scale = 0;
|
||||
const data_wanted = Math.floor(ctx.width / ctx.step);
|
||||
let last_timestamp = NaN;
|
||||
|
||||
for (let i = 0, di = 0; di < lines.length; di++) {
|
||||
if (lines[di] == null)
|
||||
continue;
|
||||
|
||||
const multiply = (lines[di].multiply != null) ? lines[di].multiply : 1;
|
||||
const offset = (lines[di].offset != null) ? lines[di].offset : 0;
|
||||
|
||||
for (let j = ctx.timestamp ? 0 : 1; j < data.length; j++) {
|
||||
/* skip overlapping entries */
|
||||
if (data[j][0] <= ctx.timestamp)
|
||||
continue;
|
||||
|
||||
if (i == 0) {
|
||||
ctx.fill++;
|
||||
last_timestamp = data[j][0];
|
||||
}
|
||||
|
||||
info.line_current[i] = data[j][di + 1] * multiply;
|
||||
info.line_current[i] -= Math.min(info.line_current[i], offset);
|
||||
values[i].push(info.line_current[i]);
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
/* cut off outdated entries */
|
||||
ctx.fill = Math.min(ctx.fill, data_wanted);
|
||||
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const len = values[i].length;
|
||||
values[i] = values[i].slice(len - data_wanted, len);
|
||||
|
||||
/* find peaks, averages */
|
||||
info.line_peak[i] = NaN;
|
||||
info.line_average[i] = 0;
|
||||
|
||||
for (let j = 0; j < values[i].length; j++) {
|
||||
info.line_peak[i] = isNaN(info.line_peak[i]) ? values[i][j] : Math.max(info.line_peak[i], values[i][j]);
|
||||
info.line_average[i] += values[i][j];
|
||||
}
|
||||
|
||||
info.line_average[i] = info.line_average[i] / ctx.fill;
|
||||
}
|
||||
|
||||
info.peak = Math.max.apply(Math, info.line_peak);
|
||||
|
||||
/* remember current timestamp, calculate horizontal scale */
|
||||
if (!isNaN(last_timestamp))
|
||||
ctx.timestamp = last_timestamp;
|
||||
|
||||
const size = Math.floor(Math.log2(info.peak));
|
||||
const div = Math.pow(2, size - (size % 10));
|
||||
const p_o_d = info.peak / div;
|
||||
const mult = (p_o_d < 5) ? 2 : ((p_o_d < 50) ? 10 : ((p_o_d < 500) ? 100 : 1000));
|
||||
|
||||
info.peak = info.peak + (mult * div) - (info.peak % (mult * div));
|
||||
|
||||
data_scale = ctx.height / info.peak;
|
||||
|
||||
/* plot data */
|
||||
for (let i = 0, di = 0; di < lines.length; di++) {
|
||||
if (lines[di] == null)
|
||||
continue;
|
||||
|
||||
const el = ctx.svg.firstElementChild.getElementById(lines[di].line);
|
||||
let pt = `0,${ctx.height}`;
|
||||
let y = 0;
|
||||
|
||||
if (!el)
|
||||
continue;
|
||||
|
||||
for (let j = 0; j < values[i].length; j++) {
|
||||
const x = j * ctx.step;
|
||||
|
||||
y = ctx.height - Math.floor(values[i][j] * data_scale);
|
||||
//y -= Math.floor(y % (1 / data_scale));
|
||||
|
||||
y = isNaN(y) ? ctx.height : y;
|
||||
|
||||
pt += ` ${x},${y}`;
|
||||
}
|
||||
|
||||
pt += ` ${ctx.width},${y} ${ctx.width},${ctx.height}`;
|
||||
|
||||
el.setAttribute('points', pt);
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
info.label_25 = 0.25 * info.peak;
|
||||
info.label_50 = 0.50 * info.peak;
|
||||
info.label_75 = 0.75 * info.peak;
|
||||
|
||||
if (typeof(ctx.cb) == 'function')
|
||||
ctx.cb(ctx.svg, info);
|
||||
}
|
||||
}, this));
|
||||
}, this), pollInterval);
|
||||
},
|
||||
|
||||
loadSVG(src) {
|
||||
return request.get(src).then(function(response) {
|
||||
if (!response.ok)
|
||||
throw new Error(response.statusText);
|
||||
|
||||
return E('div', {
|
||||
'style': 'width:100%;height:300px;border:1px solid #000;background:#fff'
|
||||
}, E(response.text()));
|
||||
});
|
||||
},
|
||||
|
||||
render([svg1, svg2, wifidevs]) {
|
||||
|
||||
const v = E('div', { 'class': 'cbi-map', 'id': 'map' }, E('div'));
|
||||
|
||||
for (const wifidev of wifidevs) {
|
||||
const ifname = wifidev.getIfname();
|
||||
const ssid = wifidev.getSSID();
|
||||
|
||||
if (!ifname)
|
||||
continue;
|
||||
|
||||
const csvg1 = svg1.cloneNode(true);
|
||||
const csvg2 = svg2.cloneNode(true);
|
||||
|
||||
v.firstElementChild.appendChild(E('div', { 'class': 'cbi-section', 'data-tab': ifname, 'data-tab-title': `${ifname} ${ssid}` }, [
|
||||
csvg1,
|
||||
E('div', { 'class': 'right' }, E('small', { 'id': 'scale' }, '-')),
|
||||
E('br'),
|
||||
|
||||
E('table', { 'class': 'table', 'style': 'width:100%;table-layout:fixed' }, [
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td right top' }, E('strong', { 'style': 'border-bottom:2px solid blue' }, [ _('Signal:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'rssi_bw_cur' }, [ '0 ' + _('dBm') ]),
|
||||
|
||||
E('td', { 'class': 'td right top' }, E('strong', {}, [ _('Average:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'rssi_bw_avg' }, [ '0 ' + _('dBm') ]),
|
||||
|
||||
E('td', { 'class': 'td right top' }, E('strong', {}, [ _('Peak:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'rssi_bw_peak' }, [ '0 ' + _('dBm') ])
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td right top' }, E('strong', { 'style': 'border-bottom:2px solid red' }, [ _('Noise:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'noise_bw_cur' }, [ '0 ' + _('dBm') ]),
|
||||
|
||||
E('td', { 'class': 'td right top' }, E('strong', {}, [ _('Average:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'noise_bw_avg' }, [ '0 ' + _('dBm') ]),
|
||||
|
||||
E('td', { 'class': 'td right top' }, E('strong', {}, [ _('Peak:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'noise_bw_peak' }, [ '0 ' + _('dBm') ])
|
||||
])
|
||||
]),
|
||||
E('br'),
|
||||
|
||||
csvg2,
|
||||
E('div', { 'class': 'right' }, E('small', { 'id': 'scale2' }, '-')),
|
||||
E('br'),
|
||||
|
||||
E('table', { 'class': 'table', 'style': 'width:100%;table-layout:fixed' }, [
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td right top' }, E('strong', { 'style': 'border-bottom:2px solid green' }, [ _('Phy Rate:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'rate_bw_cur' }, [ '0 Mbit/s' ]),
|
||||
|
||||
E('td', { 'class': 'td right top' }, E('strong', {}, [ _('Average:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'rate_bw_avg' }, [ '0 Mbit/s' ]),
|
||||
|
||||
E('td', { 'class': 'td right top' }, E('strong', {}, [ _('Peak:') ])),
|
||||
E('td', { 'class': 'td', 'id': 'rate_bw_peak' }, [ '0 Mbit/s' ])
|
||||
])
|
||||
]),
|
||||
E('div', {'class': 'cbi-section-create'})
|
||||
]));
|
||||
|
||||
this.updateGraph(ifname, csvg1, [ null, { line: 'rssi', offset: 155 }, { line: 'noise', offset: 155 } ], function(svg, info) {
|
||||
var G = svg.firstElementChild, tab = svg.parentNode;
|
||||
|
||||
G.getElementById('label_25').firstChild.data = '%d %s'.format(info.label_25 - 100, _('dBm'));
|
||||
G.getElementById('label_50').firstChild.data = '%d %s'.format(info.label_50 - 100, _('dBm'));
|
||||
G.getElementById('label_75').firstChild.data = '%d %s'.format(info.label_75 - 100, _('dBm'));
|
||||
|
||||
tab.querySelector('#scale').firstChild.data = _('(%d minute window, %d second interval)').format(info.timeframe, info.interval);
|
||||
|
||||
tab.querySelector('#rssi_bw_cur').firstChild.data = '%d %s'.format(info.line_current[0] - 100, _('dBm'));
|
||||
tab.querySelector('#rssi_bw_avg').firstChild.data = '%d %s'.format(info.line_average[0] - 100, _('dBm'));
|
||||
tab.querySelector('#rssi_bw_peak').firstChild.data = '%d %s'.format(info.line_peak[0] - 100, _('dBm'));
|
||||
|
||||
tab.querySelector('#noise_bw_cur').firstChild.data = '%d %s'.format(info.line_current[1] - 100, _('dBm'));
|
||||
tab.querySelector('#noise_bw_avg').firstChild.data = '%d %s'.format(info.line_average[1] - 100, _('dBm'));
|
||||
tab.querySelector('#noise_bw_peak').firstChild.data = '%d %s'.format(info.line_peak[1] - 100, _('dBm'));
|
||||
});
|
||||
|
||||
this.updateGraph(ifname, csvg2, [ { line: 'rate', multiply: 0.001 } ], function(svg, info) {
|
||||
var G = svg.firstElementChild, tab = svg.parentNode;
|
||||
|
||||
G.getElementById('label_25').firstChild.data = '%.2f %s'.format(info.label_25, _('Mbit/s'));
|
||||
G.getElementById('label_50').firstChild.data = '%.2f %s'.format(info.label_50, _('Mbit/s'));
|
||||
G.getElementById('label_75').firstChild.data = '%.2f %s'.format(info.label_75, _('Mbit/s'));
|
||||
|
||||
tab.querySelector('#scale2').firstChild.data = _('(%d minute window, %d second interval)').format(info.timeframe, info.interval);
|
||||
|
||||
tab.querySelector('#rate_bw_cur').firstChild.data = '%d %s'.format(info.line_current[0], _('Mbit/s'));
|
||||
tab.querySelector('#rate_bw_avg').firstChild.data = '%d %s'.format(info.line_average[0], _('Mbit/s'));
|
||||
tab.querySelector('#rate_bw_peak').firstChild.data = '%d %s'.format(info.line_peak[0], _('Mbit/s'));
|
||||
});
|
||||
}
|
||||
|
||||
ui.tabs.initTabGroup(v.firstElementChild.childNodes);
|
||||
|
||||
this.pollData();
|
||||
|
||||
return E([], [
|
||||
E('h2', _('Wireless')),
|
||||
E('div', {'class': 'cbi-map-descr'}, _('This page displays the wireless metrics for each available radio interface.')),
|
||||
v
|
||||
]);
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
|
||||
. /lib/functions.sh
|
||||
|
||||
main() {
|
||||
local log_file
|
||||
|
||||
log_file="$(uci_get system @system[0] log_file "")"
|
||||
if [ -f "$log_file" ]; then
|
||||
cat "$log_file"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ -f /sbin/logread ]; then
|
||||
/sbin/logread -e ^
|
||||
else
|
||||
/usr/sbin/logread -e ^
|
||||
fi
|
||||
}
|
||||
|
||||
main
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"admin/status/overview": {
|
||||
"title": "Overview",
|
||||
"order": 1,
|
||||
"action": {
|
||||
"type": "template",
|
||||
"path": "admin_status/index"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-mod-status-index" ]
|
||||
}
|
||||
},
|
||||
|
||||
"admin/status/routes": {
|
||||
"title": "Routing",
|
||||
"order": 2,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "status/routes"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-mod-status-routes" ],
|
||||
"fs": [
|
||||
{ "/usr/libexec/ip-tiny": "absent", "/usr/libexec/ip-full": "absent" }
|
||||
]
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
"admin/status/routesj": {
|
||||
"title": "Routing",
|
||||
"order": 2,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "status/routesj"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-mod-status-routes-json" ],
|
||||
"fs": [
|
||||
{ "/usr/libexec/ip-tiny": "executable" },
|
||||
{ "/usr/libexec/ip-full": "executable" }
|
||||
]
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
"admin/status/iptables": {
|
||||
"title": "Firewall",
|
||||
"order": 3,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "status/iptables"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-mod-status-firewall" ],
|
||||
"fs": [
|
||||
{ "/usr/sbin/nft": "absent", "/usr/sbin/iptables": "executable" },
|
||||
{ "/usr/sbin/nft": "absent", "/usr/sbin/ip6tables": "executable" }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"admin/status/nftables": {
|
||||
"title": "Firewall",
|
||||
"order": 3,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "status/nftables"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-mod-status-firewall" ],
|
||||
"fs": { "/usr/sbin/nft": "executable" }
|
||||
}
|
||||
},
|
||||
|
||||
"admin/status/nftables/iptables": {
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "status/iptables"
|
||||
}
|
||||
},
|
||||
|
||||
"admin/status/logs": {
|
||||
"title": "System Log",
|
||||
"order": 4,
|
||||
"action": {
|
||||
"type": "alias",
|
||||
"path": "admin/status/logs/syslog"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-mod-status-logs" ]
|
||||
}
|
||||
},
|
||||
|
||||
"admin/status/logs/syslog": {
|
||||
"title": "System Log",
|
||||
"order": 1,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "status/syslog"
|
||||
}
|
||||
},
|
||||
|
||||
"admin/status/logs/dmesg": {
|
||||
"title": "Kernel Log",
|
||||
"order": 2,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "status/dmesg"
|
||||
}
|
||||
},
|
||||
|
||||
"admin/status/processes": {
|
||||
"title": "Processes",
|
||||
"order": 6,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "status/processes"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-mod-status-processes" ]
|
||||
}
|
||||
},
|
||||
|
||||
"admin/status/channel_analysis": {
|
||||
"title": "Channel Analysis",
|
||||
"order": 7,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "status/channel_analysis"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-mod-status-channel_analysis" ],
|
||||
"uci": { "wireless": { "@wifi-device": true } }
|
||||
}
|
||||
},
|
||||
|
||||
"admin/status/realtime": {
|
||||
"title": "Realtime Graphs",
|
||||
"order": 7,
|
||||
"action": {
|
||||
"type": "alias",
|
||||
"path": "admin/status/realtime/load"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-mod-status-realtime" ]
|
||||
}
|
||||
},
|
||||
|
||||
"admin/status/realtime/load": {
|
||||
"title": "Load",
|
||||
"order": 1,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "status/load"
|
||||
}
|
||||
},
|
||||
|
||||
"admin/status/realtime/bandwidth": {
|
||||
"title": "Bandwidth",
|
||||
"order": 2,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "status/bandwidth"
|
||||
}
|
||||
},
|
||||
|
||||
"admin/status/realtime/wireless": {
|
||||
"title": "Wireless",
|
||||
"order": 3,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "status/wireless"
|
||||
},
|
||||
"depends": {
|
||||
"uci": { "wireless": { "@wifi-device": true } }
|
||||
}
|
||||
},
|
||||
|
||||
"admin/status/realtime/connections": {
|
||||
"title": "Connections",
|
||||
"order": 4,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "status/connections"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"luci-mod-status-index": {
|
||||
"description": "Grant access to main status display",
|
||||
"read": {
|
||||
"file": {
|
||||
"/etc/board.json": [ "read" ],
|
||||
"/proc/sys/net/netfilter/nf_conntrack_count": [ "read" ],
|
||||
"/proc/sys/net/netfilter/nf_conntrack_max": [ "read" ],
|
||||
"/www/luci-static/resources/view/status/include": [ "list" ]
|
||||
},
|
||||
"ubus": {
|
||||
"luci": [ "getVersion", "getUnixtime", "getOdhcp6cStats", "getBuiltinEthernetPorts", "getCPUBench", "getCPUInfo", "getCPUUsage" ],
|
||||
"file": [ "list", "read" ],
|
||||
"system": [ "board", "info" ]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "dhcp" ]
|
||||
}
|
||||
},
|
||||
|
||||
"luci-mod-status-index-storage": {
|
||||
"description": "Grant access to Storage and Mount status display",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci": [ "getMountPoints" ],
|
||||
"system": [ "info" ]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"luci-mod-status-index-dhcp": {
|
||||
"description": "Grant access to DHCP status display",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci-rpc": [ "getDHCPLeases" ],
|
||||
"fingerprint": [ "fingerprint" ]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"luci-mod-status-index-dsl": {
|
||||
"description": "Grant access to DSL status display",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"dsl": [ "metrics" ]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"luci-mod-status-index-ports": {
|
||||
"description": "Grant access to port status display",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci": [ "getBuiltinEthernetPorts", "getSwconfigPortState" ]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"luci-mod-status-index-wifi": {
|
||||
"description": "Grant access to wireless status display",
|
||||
"read": {
|
||||
"file": {
|
||||
"/usr/sbin/wlanconfig": [ "exec" ]
|
||||
},
|
||||
"ubus": {
|
||||
"file": [ "exec" ],
|
||||
"iwinfo": [ "assoclist", "devices", "info" ]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"hostapd.*": [ "del_client", "wps_start", "wps_cancel", "wps_status" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"luci-mod-status-realtime": {
|
||||
"description": "Grant access to realtime statistics",
|
||||
"read": {
|
||||
"file": {
|
||||
"/etc/services": [ "read" ]
|
||||
},
|
||||
"ubus": {
|
||||
"luci": [ "getConntrackList", "getRealtimeStats", "getCPUBench", "getCPUUsage", "getOnlineUsers" ],
|
||||
"luci-rpc": [ "getHostHints", "getNetworkDevices", "getDHCPLeases" ],
|
||||
"network.rrdns": [ "lookup" ]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"luci-mod-status-processes": {
|
||||
"description": "Grant access to process status",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci": [ "getProcessList" ]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"file": {
|
||||
"/bin/kill": [ "exec" ]
|
||||
},
|
||||
"ubus": {
|
||||
"file": [ "exec" ]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"luci-mod-status-logs": {
|
||||
"description": "Grant access to system logs",
|
||||
"read": {
|
||||
"cgi-io": [ "exec" ],
|
||||
"file": {
|
||||
"/bin/dmesg -r": [ "exec" ],
|
||||
"/usr/libexec/syslog-wrapper": [ "exec" ]
|
||||
},
|
||||
"ubus": {
|
||||
"log": [ "read" ],
|
||||
"file": [ "stat" ]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"luci-mod-status-routes": {
|
||||
"description": "Grant access to routing status",
|
||||
"read": {
|
||||
"file": {
|
||||
"/sbin/ip -[46] neigh show": [ "exec" ],
|
||||
"/sbin/ip -[46] route show table all": [ "exec" ],
|
||||
"/sbin/ip -[46] rule show": [ "exec" ]
|
||||
},
|
||||
"ubus": {
|
||||
"file": [ "exec" ],
|
||||
"fingerprint": [ "fingerprint" ]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"luci-mod-status-routes-json": {
|
||||
"description": "Grant access to routing status",
|
||||
"read": {
|
||||
"file": {
|
||||
"/sbin/ip -[46] -j neigh show": [ "exec" ],
|
||||
"/sbin/ip -[46] -j route show table all": [ "exec" ],
|
||||
"/sbin/ip -[46] -j rule show": [ "exec" ]
|
||||
},
|
||||
"ubus": {
|
||||
"file": [ "exec" ],
|
||||
"fingerprint": [ "fingerprint" ]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"luci-mod-status-channel_analysis": {
|
||||
"description": "Grant access to wireless channel status",
|
||||
"read": {
|
||||
"file": {
|
||||
"/usr/sbin/ip": [ "exec" ],
|
||||
"/usr/sbin/iw": [ "exec" ]
|
||||
},
|
||||
"ubus": {
|
||||
"file": [ "exec" ],
|
||||
"iwinfo": [ "freqlist" ],
|
||||
"luci-rpc": [ "getNetworkDevices" ]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"luci-mod-status-firewall": {
|
||||
"description": "Grant access to firewall status",
|
||||
"read": {
|
||||
"cgi-io": [ "exec" ],
|
||||
"file": {
|
||||
"/usr/sbin/nft --terse --json list ruleset": [ "exec" ],
|
||||
"/usr/sbin/iptables --line-numbers -w -nvxL -t *": [ "exec" ],
|
||||
"/usr/sbin/ip6tables --line-numbers -w -nvxL -t *": [ "exec" ],
|
||||
"/usr/sbin/ip6tables": [ "list" ],
|
||||
"/usr/sbin/iptables-save": [ "exec" ],
|
||||
"/usr/sbin/ip6tables-save": [ "exec" ]
|
||||
},
|
||||
"ubus": {
|
||||
"file": [ "stat" ]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"cgi-io": [ "exec" ],
|
||||
"file": {
|
||||
"/etc/init.d/firewall restart": [ "exec" ],
|
||||
"/usr/sbin/iptables -Z": [ "exec" ],
|
||||
"/usr/sbin/ip6tables -Z": [ "exec" ]
|
||||
},
|
||||
"ubus": {
|
||||
"file": [ "exec" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
%.o: %.c
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(FPIC) -Wall -c -o $@ $<
|
||||
|
||||
clean:
|
||||
rm -f luci-bwc *.o
|
||||
|
||||
luci-bwc: luci-bwc.o
|
||||
$(CC) $(LDFLAGS) -o $@ $^ -ldl
|
||||
|
||||
compile: luci-bwc
|
||||
|
||||
install: compile
|
||||
mkdir -p $(DESTDIR)/usr/bin
|
||||
cp luci-bwc $(DESTDIR)/usr/bin/luci-bwc
|
||||
@@ -0,0 +1,813 @@
|
||||
/*
|
||||
* luci-bwc - Very simple bandwidth collector cache for LuCI realtime graphs
|
||||
*
|
||||
* Copyright (C) 2010 Jo-Philipp Wich <jow@openwrt.org>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#define _BSD_SOURCE
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <inttypes.h>
|
||||
#include <fcntl.h>
|
||||
#include <time.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <signal.h>
|
||||
#include <endian.h>
|
||||
#include <dirent.h>
|
||||
|
||||
#include <sys/stat.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/types.h>
|
||||
#include <arpa/inet.h>
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <iwinfo.h>
|
||||
|
||||
#define STEP_COUNT 60
|
||||
#define STEP_TIME 1
|
||||
#define TIMEOUT 10
|
||||
|
||||
#define PID_PATH "/var/run/luci-bwc.pid"
|
||||
|
||||
#define DB_PATH "/var/lib/luci-bwc"
|
||||
#define DB_IF_FILE DB_PATH "/if/%s"
|
||||
#define DB_RD_FILE DB_PATH "/radio/%s"
|
||||
#define DB_CN_FILE DB_PATH "/connections"
|
||||
#define DB_LD_FILE DB_PATH "/load"
|
||||
|
||||
#define LD_SCAN_PATTERN \
|
||||
"%f %f %f"
|
||||
|
||||
|
||||
struct file_map {
|
||||
int fd;
|
||||
int size;
|
||||
char *mmap;
|
||||
};
|
||||
|
||||
struct traffic_entry {
|
||||
uint32_t time;
|
||||
uint64_t rxb;
|
||||
uint64_t rxp;
|
||||
uint64_t txb;
|
||||
uint64_t txp;
|
||||
};
|
||||
|
||||
struct conn_entry {
|
||||
uint32_t time;
|
||||
uint32_t udp;
|
||||
uint32_t tcp;
|
||||
uint32_t other;
|
||||
};
|
||||
|
||||
struct load_entry {
|
||||
uint32_t time;
|
||||
uint16_t load1;
|
||||
uint16_t load5;
|
||||
uint16_t load15;
|
||||
};
|
||||
|
||||
struct radio_entry {
|
||||
uint32_t time;
|
||||
uint16_t rate;
|
||||
uint8_t rssi;
|
||||
uint8_t noise;
|
||||
};
|
||||
|
||||
static int readpid(void)
|
||||
{
|
||||
int fd;
|
||||
int pid = -1;
|
||||
char buf[9] = { 0 };
|
||||
|
||||
if ((fd = open(PID_PATH, O_RDONLY)) > -1)
|
||||
{
|
||||
if (read(fd, buf, sizeof(buf)))
|
||||
{
|
||||
buf[8] = 0;
|
||||
pid = atoi(buf);
|
||||
}
|
||||
|
||||
close(fd);
|
||||
}
|
||||
|
||||
return pid;
|
||||
}
|
||||
|
||||
static int writepid(void)
|
||||
{
|
||||
int fd;
|
||||
int wlen;
|
||||
char buf[9] = { 0 };
|
||||
|
||||
if ((fd = open(PID_PATH, O_WRONLY | O_CREAT | O_TRUNC, 0600)) > -1)
|
||||
{
|
||||
wlen = snprintf(buf, sizeof(buf), "%i", getpid());
|
||||
write(fd, buf, wlen);
|
||||
close(fd);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int timeout = TIMEOUT;
|
||||
static int countdown = -1;
|
||||
|
||||
static void reset_countdown(int sig)
|
||||
{
|
||||
countdown = timeout;
|
||||
|
||||
}
|
||||
|
||||
|
||||
static char *progname;
|
||||
static int prognamelen;
|
||||
|
||||
static struct iwinfo_ops *backend = NULL;
|
||||
|
||||
|
||||
static int init_directory(char *path)
|
||||
{
|
||||
char *p = path;
|
||||
|
||||
for (p = &path[1]; *p; p++)
|
||||
{
|
||||
if (*p == '/')
|
||||
{
|
||||
*p = 0;
|
||||
|
||||
if (mkdir(path, 0700) && (errno != EEXIST))
|
||||
return -1;
|
||||
|
||||
*p = '/';
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int init_file(char *path, int esize)
|
||||
{
|
||||
int i, file;
|
||||
char buf[sizeof(struct traffic_entry)] = { 0 };
|
||||
|
||||
if (init_directory(path))
|
||||
return -1;
|
||||
|
||||
if ((file = open(path, O_WRONLY | O_CREAT, 0600)) >= 0)
|
||||
{
|
||||
for (i = 0; i < STEP_COUNT; i++)
|
||||
{
|
||||
if (write(file, buf, esize) < 0)
|
||||
break;
|
||||
}
|
||||
|
||||
close(file);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static inline uint32_t timeof(void *entry)
|
||||
{
|
||||
return be32toh(((struct traffic_entry *)entry)->time);
|
||||
}
|
||||
|
||||
static int update_file(const char *path, void *entry, int esize)
|
||||
{
|
||||
int rv = -1;
|
||||
int file;
|
||||
char *map;
|
||||
|
||||
if ((file = open(path, O_RDWR)) >= 0)
|
||||
{
|
||||
map = mmap(NULL, esize * STEP_COUNT, PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED | MAP_LOCKED, file, 0);
|
||||
|
||||
if ((map != NULL) && (map != MAP_FAILED))
|
||||
{
|
||||
if (timeof(entry) > timeof(map + esize * (STEP_COUNT-1)))
|
||||
{
|
||||
memmove(map, map + esize, esize * (STEP_COUNT-1));
|
||||
memcpy(map + esize * (STEP_COUNT-1), entry, esize);
|
||||
}
|
||||
|
||||
munmap(map, esize * STEP_COUNT);
|
||||
|
||||
rv = 0;
|
||||
}
|
||||
|
||||
close(file);
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
static int mmap_file(const char *path, int esize, struct file_map *m)
|
||||
{
|
||||
m->fd = -1;
|
||||
m->size = -1;
|
||||
m->mmap = NULL;
|
||||
|
||||
if ((m->fd = open(path, O_RDONLY)) >= 0)
|
||||
{
|
||||
m->size = STEP_COUNT * esize;
|
||||
m->mmap = mmap(NULL, m->size, PROT_READ,
|
||||
MAP_SHARED | MAP_LOCKED, m->fd, 0);
|
||||
|
||||
if ((m->mmap != NULL) && (m->mmap != MAP_FAILED))
|
||||
return 0;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void umap_file(struct file_map *m)
|
||||
{
|
||||
if ((m->mmap != NULL) && (m->mmap != MAP_FAILED))
|
||||
munmap(m->mmap, m->size);
|
||||
|
||||
if (m->fd > -1)
|
||||
close(m->fd);
|
||||
}
|
||||
|
||||
static void * iw_open(void)
|
||||
{
|
||||
void *iwlib = NULL;
|
||||
glob_t paths;
|
||||
int i;
|
||||
|
||||
if (glob("/usr/lib/libiwinfo.so*", 0, NULL, &paths) != 0)
|
||||
return NULL;
|
||||
|
||||
for (i = 0; i < paths.gl_pathc && !iwlib; i++)
|
||||
iwlib = dlopen(paths.gl_pathv[i], RTLD_LAZY | RTLD_LOCAL);
|
||||
|
||||
globfree(&paths);
|
||||
|
||||
if (!iwlib)
|
||||
return NULL;
|
||||
return iwlib;
|
||||
}
|
||||
|
||||
static int iw_update(
|
||||
void *iw, const char *ifname, uint16_t *rate, uint8_t *rssi, uint8_t *noise
|
||||
) {
|
||||
struct iwinfo_ops *(*probe)(const char *);
|
||||
int val;
|
||||
|
||||
if (!backend)
|
||||
{
|
||||
probe = dlsym(iw, "iwinfo_backend");
|
||||
|
||||
if (!probe)
|
||||
return 0;
|
||||
|
||||
backend = probe(ifname);
|
||||
|
||||
if (!backend)
|
||||
return 0;
|
||||
}
|
||||
|
||||
*rate = (backend->bitrate && !backend->bitrate(ifname, &val)) ? val : 0;
|
||||
*rssi = (backend->signal && !backend->signal(ifname, &val)) ? val : 0;
|
||||
*noise = (backend->noise && !backend->noise(ifname, &val)) ? val : 0;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void iw_close(void *iw)
|
||||
{
|
||||
void (*finish)(void);
|
||||
|
||||
finish = dlsym(iw, "iwinfo_finish");
|
||||
|
||||
if (finish)
|
||||
finish();
|
||||
|
||||
dlclose(iw);
|
||||
}
|
||||
|
||||
|
||||
static int update_ifstat(
|
||||
const char *ifname, uint64_t rxb, uint64_t rxp, uint64_t txb, uint64_t txp
|
||||
) {
|
||||
char path[1024];
|
||||
|
||||
struct stat s;
|
||||
struct traffic_entry e;
|
||||
|
||||
snprintf(path, sizeof(path), DB_IF_FILE, ifname);
|
||||
|
||||
if (stat(path, &s))
|
||||
{
|
||||
if (init_file(path, sizeof(struct traffic_entry)))
|
||||
{
|
||||
fprintf(stderr, "Failed to init %s: %s\n",
|
||||
path, strerror(errno));
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
e.time = htobe32(time(NULL));
|
||||
e.rxb = htobe64(rxb);
|
||||
e.rxp = htobe64(rxp);
|
||||
e.txb = htobe64(txb);
|
||||
e.txp = htobe64(txp);
|
||||
|
||||
return update_file(path, &e, sizeof(struct traffic_entry));
|
||||
}
|
||||
|
||||
static int update_radiostat(
|
||||
const char *ifname, uint16_t rate, uint8_t rssi, uint8_t noise
|
||||
) {
|
||||
char path[1024];
|
||||
|
||||
struct stat s;
|
||||
struct radio_entry e;
|
||||
|
||||
snprintf(path, sizeof(path), DB_RD_FILE, ifname);
|
||||
|
||||
if (stat(path, &s))
|
||||
{
|
||||
if (init_file(path, sizeof(struct radio_entry)))
|
||||
{
|
||||
fprintf(stderr, "Failed to init %s: %s\n",
|
||||
path, strerror(errno));
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
e.time = htobe32(time(NULL));
|
||||
e.rate = htobe16(rate);
|
||||
e.rssi = rssi;
|
||||
e.noise = noise;
|
||||
|
||||
return update_file(path, &e, sizeof(struct radio_entry));
|
||||
}
|
||||
|
||||
static int update_cnstat(uint32_t udp, uint32_t tcp, uint32_t other)
|
||||
{
|
||||
char path[1024];
|
||||
|
||||
struct stat s;
|
||||
struct conn_entry e;
|
||||
|
||||
snprintf(path, sizeof(path), DB_CN_FILE);
|
||||
|
||||
if (stat(path, &s))
|
||||
{
|
||||
if (init_file(path, sizeof(struct conn_entry)))
|
||||
{
|
||||
fprintf(stderr, "Failed to init %s: %s\n",
|
||||
path, strerror(errno));
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
e.time = htobe32(time(NULL));
|
||||
e.udp = htobe32(udp);
|
||||
e.tcp = htobe32(tcp);
|
||||
e.other = htobe32(other);
|
||||
|
||||
return update_file(path, &e, sizeof(struct conn_entry));
|
||||
}
|
||||
|
||||
static int update_ldstat(uint16_t load1, uint16_t load5, uint16_t load15)
|
||||
{
|
||||
char path[1024];
|
||||
|
||||
struct stat s;
|
||||
struct load_entry e;
|
||||
|
||||
snprintf(path, sizeof(path), DB_LD_FILE);
|
||||
|
||||
if (stat(path, &s))
|
||||
{
|
||||
if (init_file(path, sizeof(struct load_entry)))
|
||||
{
|
||||
fprintf(stderr, "Failed to init %s: %s\n",
|
||||
path, strerror(errno));
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
e.time = htobe32(time(NULL));
|
||||
e.load1 = htobe16(load1);
|
||||
e.load5 = htobe16(load5);
|
||||
e.load15 = htobe16(load15);
|
||||
|
||||
return update_file(path, &e, sizeof(struct load_entry));
|
||||
}
|
||||
|
||||
static int run_daemon(void)
|
||||
{
|
||||
DIR *dir;
|
||||
FILE *info;
|
||||
uint64_t rxb, txb, rxp, txp;
|
||||
uint32_t udp, tcp, other;
|
||||
uint16_t rate;
|
||||
uint8_t rssi, noise;
|
||||
float lf1, lf5, lf15;
|
||||
char line[1024];
|
||||
char path[64];
|
||||
char buf[32];
|
||||
int i;
|
||||
void *iw;
|
||||
struct sigaction sa;
|
||||
struct dirent *e;
|
||||
|
||||
struct stat s;
|
||||
char *ipc = NULL;
|
||||
char *ipc_command = NULL;
|
||||
if(! stat("/proc/net/nf_conntrack", &s))
|
||||
ipc = "/proc/net/nf_conntrack";
|
||||
else if(! stat("/proc/net/ip_conntrack", &s))
|
||||
ipc = "/proc/net/ip_conntrack";
|
||||
else if(! stat("/usr/sbin/conntrack" , &s))
|
||||
ipc_command = "/usr/sbin/conntrack -L -o extended";
|
||||
|
||||
const struct {
|
||||
const char *file;
|
||||
uint64_t *value;
|
||||
} sysfs_stats[] = {
|
||||
{ "rx_packets", &rxp },
|
||||
{ "tx_packets", &txp },
|
||||
{ "rx_bytes", &rxb },
|
||||
{ "tx_bytes", &txb }
|
||||
};
|
||||
|
||||
switch (fork())
|
||||
{
|
||||
case -1:
|
||||
perror("fork()");
|
||||
return -1;
|
||||
|
||||
case 0:
|
||||
if (chdir("/") < 0)
|
||||
{
|
||||
perror("chdir()");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
close(0);
|
||||
close(1);
|
||||
close(2);
|
||||
break;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* setup USR1 signal handler to reset timer */
|
||||
sa.sa_handler = reset_countdown;
|
||||
sa.sa_flags = SA_RESTART;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sigaction(SIGUSR1, &sa, NULL);
|
||||
|
||||
/* write pid */
|
||||
if (writepid())
|
||||
{
|
||||
fprintf(stderr, "Failed to write pid file: %s\n", strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* initialize iwinfo */
|
||||
iw = iw_open();
|
||||
|
||||
/* go */
|
||||
for (reset_countdown(0); countdown >= 0; countdown--)
|
||||
{
|
||||
/* alter progname for ps, top */
|
||||
memset(progname, 0, prognamelen);
|
||||
snprintf(progname, prognamelen, "luci-bwc %d", countdown);
|
||||
|
||||
dir = opendir("/sys/class/net");
|
||||
|
||||
if (dir)
|
||||
{
|
||||
while ((e = readdir(dir)) != NULL)
|
||||
{
|
||||
if (!strcmp(e->d_name, "lo") || !strcmp(e->d_name, ".") || !strcmp(e->d_name, ".."))
|
||||
continue;
|
||||
|
||||
if (iw && iw_update(iw, e->d_name, &rate, &rssi, &noise))
|
||||
update_radiostat(e->d_name, rate, rssi, noise);
|
||||
|
||||
for (i = 0; i < sizeof(sysfs_stats)/sizeof(sysfs_stats[0]); i++)
|
||||
{
|
||||
*sysfs_stats[i].value = 0;
|
||||
|
||||
snprintf(path, sizeof(path), "/sys/class/net/%s/statistics/%s",
|
||||
e->d_name, sysfs_stats[i].file);
|
||||
|
||||
if ((info = fopen(path, "r")) != NULL)
|
||||
{
|
||||
memset(buf, 0, sizeof(buf));
|
||||
fread(buf, 1, sizeof(buf) - 1, info);
|
||||
fclose(info);
|
||||
|
||||
*sysfs_stats[i].value = (uint64_t)strtoull(buf, NULL, 10);
|
||||
}
|
||||
}
|
||||
|
||||
update_ifstat(e->d_name, rxb, rxp, txb, txp);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
}
|
||||
|
||||
if ((ipc && ((info = fopen(ipc, "r")) != NULL)) ||
|
||||
(ipc_command && ((info = popen(ipc_command, "r")) != NULL)))
|
||||
{
|
||||
udp = 0;
|
||||
tcp = 0;
|
||||
other = 0;
|
||||
|
||||
while (fgets(line, sizeof(line), info))
|
||||
{
|
||||
if (strstr(line, "TIME_WAIT"))
|
||||
continue;
|
||||
|
||||
if ((strstr(line, "src=127.0.0.1 ") && strstr(line, "dst=127.0.0.1 "))
|
||||
|| (strstr(line, "src=::1 ") && strstr(line, "dst=::1 ")))
|
||||
continue;
|
||||
|
||||
if (sscanf(line, "%*s %*d %s", buf) || sscanf(line, "%s %*d", buf))
|
||||
{
|
||||
if (!strcmp(buf, "tcp"))
|
||||
tcp++;
|
||||
else if (!strcmp(buf, "udp"))
|
||||
udp++;
|
||||
else
|
||||
other++;
|
||||
}
|
||||
}
|
||||
|
||||
update_cnstat(udp, tcp, other);
|
||||
|
||||
if (ipc)
|
||||
fclose(info);
|
||||
else
|
||||
pclose(info);
|
||||
}
|
||||
|
||||
if ((info = fopen("/proc/loadavg", "r")) != NULL)
|
||||
{
|
||||
if (fscanf(info, LD_SCAN_PATTERN, &lf1, &lf5, &lf15))
|
||||
{
|
||||
update_ldstat((uint16_t)(lf1 * 100),
|
||||
(uint16_t)(lf5 * 100),
|
||||
(uint16_t)(lf15 * 100));
|
||||
}
|
||||
|
||||
fclose(info);
|
||||
}
|
||||
|
||||
sleep(STEP_TIME);
|
||||
}
|
||||
|
||||
unlink(PID_PATH);
|
||||
|
||||
if (iw)
|
||||
iw_close(iw);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void check_daemon(void)
|
||||
{
|
||||
int pid;
|
||||
|
||||
if ((pid = readpid()) < 0 || kill(pid, 0) < 0)
|
||||
{
|
||||
/* daemon ping failed, try to start it up */
|
||||
if (run_daemon())
|
||||
{
|
||||
fprintf(stderr,
|
||||
"Failed to ping daemon and unable to start it up: %s\n",
|
||||
strerror(errno));
|
||||
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
else if (kill(pid, SIGUSR1))
|
||||
{
|
||||
fprintf(stderr, "Failed to send signal: %s\n", strerror(errno));
|
||||
exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
static int run_dump_ifname(const char *ifname)
|
||||
{
|
||||
int i;
|
||||
char path[1024];
|
||||
struct file_map m;
|
||||
struct traffic_entry *e;
|
||||
|
||||
check_daemon();
|
||||
snprintf(path, sizeof(path), DB_IF_FILE, ifname);
|
||||
|
||||
if (mmap_file(path, sizeof(struct traffic_entry), &m))
|
||||
{
|
||||
fprintf(stderr, "Failed to open %s: %s\n", path, strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (i = 0; i < m.size; i += sizeof(struct traffic_entry))
|
||||
{
|
||||
e = (struct traffic_entry *) &m.mmap[i];
|
||||
|
||||
if (!e->time)
|
||||
continue;
|
||||
|
||||
printf("[ %" PRIu32 ", %" PRIu64 ", %" PRIu64
|
||||
", %" PRIu64 ", %" PRIu64 " ]%s\n",
|
||||
be32toh(e->time),
|
||||
be64toh(e->rxb), be64toh(e->rxp),
|
||||
be64toh(e->txb), be64toh(e->txp),
|
||||
((i + sizeof(struct traffic_entry)) < m.size) ? "," : "");
|
||||
}
|
||||
|
||||
umap_file(&m);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int run_dump_radio(const char *ifname)
|
||||
{
|
||||
int i;
|
||||
char path[1024];
|
||||
struct file_map m;
|
||||
struct radio_entry *e;
|
||||
|
||||
check_daemon();
|
||||
snprintf(path, sizeof(path), DB_RD_FILE, ifname);
|
||||
|
||||
if (mmap_file(path, sizeof(struct radio_entry), &m))
|
||||
{
|
||||
fprintf(stderr, "Failed to open %s: %s\n", path, strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (i = 0; i < m.size; i += sizeof(struct radio_entry))
|
||||
{
|
||||
e = (struct radio_entry *) &m.mmap[i];
|
||||
|
||||
if (!e->time)
|
||||
continue;
|
||||
|
||||
printf("[ %" PRIu32 ", %" PRIu16 ", %" PRIu8 ", %" PRIu8 " ]%s\n",
|
||||
be32toh(e->time),
|
||||
be16toh(e->rate), e->rssi, e->noise,
|
||||
((i + sizeof(struct radio_entry)) < m.size) ? "," : "");
|
||||
}
|
||||
|
||||
umap_file(&m);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int run_dump_conns(void)
|
||||
{
|
||||
int i;
|
||||
char path[1024];
|
||||
struct file_map m;
|
||||
struct conn_entry *e;
|
||||
|
||||
check_daemon();
|
||||
snprintf(path, sizeof(path), DB_CN_FILE);
|
||||
|
||||
if (mmap_file(path, sizeof(struct conn_entry), &m))
|
||||
{
|
||||
fprintf(stderr, "Failed to open %s: %s\n", path, strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (i = 0; i < m.size; i += sizeof(struct conn_entry))
|
||||
{
|
||||
e = (struct conn_entry *) &m.mmap[i];
|
||||
|
||||
if (!e->time)
|
||||
continue;
|
||||
|
||||
printf("[ %" PRIu32 ", %" PRIu32 ", %" PRIu32 ", %" PRIu32 " ]%s\n",
|
||||
be32toh(e->time), be32toh(e->udp),
|
||||
be32toh(e->tcp), be32toh(e->other),
|
||||
((i + sizeof(struct conn_entry)) < m.size) ? "," : "");
|
||||
}
|
||||
|
||||
umap_file(&m);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int run_dump_load(void)
|
||||
{
|
||||
int i;
|
||||
char path[1024];
|
||||
struct file_map m;
|
||||
struct load_entry *e;
|
||||
|
||||
check_daemon();
|
||||
snprintf(path, sizeof(path), DB_LD_FILE);
|
||||
|
||||
if (mmap_file(path, sizeof(struct load_entry), &m))
|
||||
{
|
||||
fprintf(stderr, "Failed to open %s: %s\n", path, strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (i = 0; i < m.size; i += sizeof(struct load_entry))
|
||||
{
|
||||
e = (struct load_entry *) &m.mmap[i];
|
||||
|
||||
if (!e->time)
|
||||
continue;
|
||||
|
||||
printf("[ %" PRIu32 ", %" PRIu16 ", %" PRIu16 ", %" PRIu16 " ]%s\n",
|
||||
be32toh(e->time),
|
||||
be16toh(e->load1), be16toh(e->load5), be16toh(e->load15),
|
||||
((i + sizeof(struct load_entry)) < m.size) ? "," : "");
|
||||
}
|
||||
|
||||
umap_file(&m);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
int opt;
|
||||
|
||||
progname = argv[0];
|
||||
prognamelen = -1;
|
||||
|
||||
for (opt = 0; opt < argc; opt++)
|
||||
prognamelen += 1 + strlen(argv[opt]);
|
||||
|
||||
while ((opt = getopt(argc, argv, "t:i:r:cl")) > -1)
|
||||
{
|
||||
switch (opt)
|
||||
{
|
||||
case 't':
|
||||
timeout = atoi(optarg);
|
||||
break;
|
||||
|
||||
case 'i':
|
||||
if (optarg)
|
||||
return run_dump_ifname(optarg);
|
||||
break;
|
||||
|
||||
case 'r':
|
||||
if (optarg)
|
||||
return run_dump_radio(optarg);
|
||||
break;
|
||||
|
||||
case 'c':
|
||||
return run_dump_conns();
|
||||
|
||||
case 'l':
|
||||
return run_dump_load();
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stderr,
|
||||
"Usage:\n"
|
||||
" %s [-t timeout] -i ifname\n"
|
||||
" %s [-t timeout] -r radiodev\n"
|
||||
" %s [-t timeout] -c\n"
|
||||
" %s [-t timeout] -l\n",
|
||||
argv[0], argv[0], argv[0], argv[0]
|
||||
);
|
||||
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
{#
|
||||
Copyright 2008 Steven Barth <steven@midlink.org>
|
||||
Copyright 2008-2022 Jo-Philipp Wich <jo@mein.io>
|
||||
Licensed to the public under the Apache License 2.0.
|
||||
-#}
|
||||
|
||||
{% include('header') %}
|
||||
|
||||
<h2 name="content">{{ _('Status') }}</h2>
|
||||
|
||||
<div id="view">
|
||||
<div class="spinning">{{ _('Loading view…') }}</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function progressbar(query, value, max, byte)
|
||||
{
|
||||
var pg = document.querySelector(query),
|
||||
vn = parseInt(value) || 0,
|
||||
mn = parseInt(max) || 100,
|
||||
fv = byte ? String.format('%1024.2mB', value) : value,
|
||||
fm = byte ? String.format('%1024.2mB', max) : max,
|
||||
pc = Math.floor((100 / mn) * vn);
|
||||
|
||||
if (pg) {
|
||||
pg.firstElementChild.style.width = pc + '%';
|
||||
pg.setAttribute('title', '%s / %s (%d%%)'.format(fv, fm, pc));
|
||||
}
|
||||
}
|
||||
|
||||
function renderBox(title, active, childs) {
|
||||
childs = childs || [];
|
||||
childs.unshift(L.itemlist(E('span'), [].slice.call(arguments, 3)));
|
||||
|
||||
return E('div', { class: 'ifacebox' }, [
|
||||
E('div', { class: 'ifacebox-head center ' + (active ? 'active' : '') },
|
||||
E('strong', title)),
|
||||
E('div', { class: 'ifacebox-body left' }, childs)
|
||||
]);
|
||||
}
|
||||
|
||||
function renderBadge(icon, title) {
|
||||
return E('span', { class: 'ifacebadge' }, [
|
||||
E('img', { src: icon, title: title || '' }),
|
||||
L.itemlist(E('span'), [].slice.call(arguments, 2))
|
||||
]);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="includes" style="display:none">
|
||||
{%
|
||||
let lua_available = true;
|
||||
|
||||
try { require('lua'); }
|
||||
catch(err) { lua_available = false; }
|
||||
|
||||
if (lua_available)
|
||||
include('admin_status/luaindex');
|
||||
%}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
L.require('ui').then(function(ui) {
|
||||
ui.instantiateView('status/index');
|
||||
});
|
||||
</script>
|
||||
|
||||
{% include('footer') %}
|
||||
Reference in new issue
Block a user