mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-26 18:12:51 +08:00
🐤 Sync 2026-09-18 23:02:02
This commit is contained in:
1 parent
d4bee34c32
commit
3587c9c61f
5 files changed
+363
-385
No files matched your search
@@ -12,7 +12,7 @@ LUCI_DEPENDS:=+luci-base +logd +jsonfilter
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_VERSION:=0.1.44
|
||||
PKG_RELEASE:=24
|
||||
PKG_RELEASE:=25
|
||||
|
||||
# Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI).
|
||||
ifdef SOURCE_DATE_EPOCH
|
||||
|
||||
@@ -46,6 +46,21 @@ function persistConsentDismissed() {
|
||||
}
|
||||
}
|
||||
|
||||
function enableLoggingButton(state, callbacks) {
|
||||
return E(
|
||||
'button',
|
||||
{
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'type': 'button',
|
||||
'disabled': state.loggingBusy ? '' : null,
|
||||
'click': function () {
|
||||
callbacks.onEnable();
|
||||
}
|
||||
},
|
||||
[state.loggingBusy ? _('Enabling…') : _('Enable WAN drop/reject logging')]
|
||||
);
|
||||
}
|
||||
|
||||
function blockerCode(state) {
|
||||
const blockers = (state.loggingStatus && state.loggingStatus.blockers) || [];
|
||||
if (blockers.indexOf('no_wan_zone') >= 0) return 'no_wan_zone';
|
||||
@@ -164,19 +179,7 @@ function buildConsentPanel(state, callbacks) {
|
||||
])
|
||||
]),
|
||||
E('p', { 'class': 'fwlive-consent-actions' }, [
|
||||
E(
|
||||
'button',
|
||||
{
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'type': 'button',
|
||||
'disabled': state.loggingBusy ? '' : null,
|
||||
'click': function () {
|
||||
persistConsentDismissed();
|
||||
callbacks.onEnable();
|
||||
}
|
||||
},
|
||||
[state.loggingBusy ? _('Enabling…') : _('Enable WAN drop/reject logging')]
|
||||
),
|
||||
enableLoggingButton(state, callbacks),
|
||||
' ',
|
||||
E(
|
||||
'button',
|
||||
@@ -289,19 +292,7 @@ function buildEmptyStateNodes(state, callbacks) {
|
||||
);
|
||||
nodes.push(
|
||||
E('p', {}, [
|
||||
E(
|
||||
'button',
|
||||
{
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'type': 'button',
|
||||
'disabled': state.loggingBusy ? '' : null,
|
||||
'click': function () {
|
||||
persistConsentDismissed();
|
||||
callbacks.onEnable();
|
||||
}
|
||||
},
|
||||
[state.loggingBusy ? _('Enabling…') : _('Enable WAN drop/reject logging')]
|
||||
),
|
||||
enableLoggingButton(state, callbacks),
|
||||
' ',
|
||||
links.firewallZonesLink(_('I’ll configure this under Network → Firewall'))
|
||||
])
|
||||
|
||||
@@ -687,40 +687,26 @@ return view.extend({
|
||||
this.renderRows(true);
|
||||
},
|
||||
|
||||
async handleEnableLogging() {
|
||||
async runLoggingToggle(opts) {
|
||||
if (this.loggingBusy) return;
|
||||
|
||||
this.loggingBusy = true;
|
||||
this.loggingNotice = '';
|
||||
this.updateEmptyStateUi();
|
||||
this.updateLoggingToolbarUi();
|
||||
opts.initialUi();
|
||||
|
||||
try {
|
||||
const res = await callFwliveEnableLogging();
|
||||
const res = await opts.call();
|
||||
if (!res || !res.ok) {
|
||||
if (res && res.error === 'nf_log_missing')
|
||||
this.loggingNotice = _(
|
||||
'Cannot enable logging until kernel log modules are installed.'
|
||||
);
|
||||
else if (res && res.error === 'firewall_changes_pending')
|
||||
this.loggingNotice = _(
|
||||
'Another change is staged for the firewall; apply or revert it first.'
|
||||
);
|
||||
else this.loggingNotice = _('Could not enable logging.');
|
||||
this.loggingNotice = opts.failureNotice(res);
|
||||
await this.loadLoggingStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.changed)
|
||||
this.loggingNotice = _(
|
||||
'WAN drop/reject logging is on. Blocked inbound traffic should appear here as it happens — not normal LAN browsing.'
|
||||
);
|
||||
else this.loggingNotice = _('WAN logging is already enabled.');
|
||||
|
||||
logging.persistConsentDismissed();
|
||||
this.loggingNotice = opts.successNotice(res);
|
||||
if (opts.onSuccess) opts.onSuccess(res);
|
||||
await this.loadLoggingStatus();
|
||||
} catch (e) {
|
||||
this.loggingNotice = _('Administrator access is required to enable logging.');
|
||||
this.loggingNotice = opts.catchNotice();
|
||||
await this.loadLoggingStatus();
|
||||
} finally {
|
||||
this.loggingBusy = false;
|
||||
@@ -729,35 +715,47 @@ return view.extend({
|
||||
}
|
||||
},
|
||||
|
||||
async handleDisableLogging() {
|
||||
if (this.loggingBusy) return;
|
||||
|
||||
this.loggingBusy = true;
|
||||
this.loggingNotice = '';
|
||||
this.updateLoggingToolbarUi();
|
||||
|
||||
try {
|
||||
const res = await callFwliveDisableLogging();
|
||||
if (!res || !res.ok) {
|
||||
async handleEnableLogging() {
|
||||
return this.runLoggingToggle({
|
||||
call: () => callFwliveEnableLogging(),
|
||||
initialUi: () => {
|
||||
this.updateEmptyStateUi();
|
||||
this.updateLoggingToolbarUi();
|
||||
},
|
||||
failureNotice: (res) => {
|
||||
if (res && res.error === 'nf_log_missing')
|
||||
return _('Cannot enable logging until kernel log modules are installed.');
|
||||
if (res && res.error === 'firewall_changes_pending')
|
||||
this.loggingNotice = _(
|
||||
return _(
|
||||
'Another change is staged for the firewall; apply or revert it first.'
|
||||
);
|
||||
else this.loggingNotice = _('Could not disable logging.');
|
||||
await this.loadLoggingStatus();
|
||||
return;
|
||||
}
|
||||
return _('Could not enable logging.');
|
||||
},
|
||||
successNotice: (res) =>
|
||||
res.changed
|
||||
? _(
|
||||
'WAN drop/reject logging is on. Blocked inbound traffic should appear here as it happens — not normal LAN browsing.'
|
||||
)
|
||||
: _('WAN logging is already enabled.'),
|
||||
onSuccess: () => logging.persistConsentDismissed(),
|
||||
catchNotice: () => _('Administrator access is required to enable logging.')
|
||||
});
|
||||
},
|
||||
|
||||
if (res.changed) this.loggingNotice = _('WAN drop/reject logging is off.');
|
||||
await this.loadLoggingStatus();
|
||||
} catch (e) {
|
||||
this.loggingNotice = _('Administrator access is required to disable logging.');
|
||||
await this.loadLoggingStatus();
|
||||
} finally {
|
||||
this.loggingBusy = false;
|
||||
this.updateEmptyStateUi();
|
||||
this.updateLoggingToolbarUi();
|
||||
}
|
||||
async handleDisableLogging() {
|
||||
return this.runLoggingToggle({
|
||||
call: () => callFwliveDisableLogging(),
|
||||
initialUi: () => this.updateLoggingToolbarUi(),
|
||||
failureNotice: (res) => {
|
||||
if (res && res.error === 'firewall_changes_pending')
|
||||
return _(
|
||||
'Another change is staged for the firewall; apply or revert it first.'
|
||||
);
|
||||
return _('Could not disable logging.');
|
||||
},
|
||||
successNotice: (res) => (res.changed ? _('WAN drop/reject logging is off.') : ''),
|
||||
catchNotice: () => _('Administrator access is required to disable logging.')
|
||||
});
|
||||
},
|
||||
|
||||
shouldShowLoggingConsent() {
|
||||
@@ -889,6 +887,13 @@ return view.extend({
|
||||
|
||||
/* Caller must discard stale epochs before this synchronous application.
|
||||
* This updates transport/adaptive state, summary/banner UI, rows, and buffer. */
|
||||
failPollReply(rtt) {
|
||||
this.lastPollError = true;
|
||||
this.fillingBuffer = false;
|
||||
this.notePollRtt(rtt, true);
|
||||
this.updateAdaptiveBanner();
|
||||
},
|
||||
|
||||
applyPollReply(poll, context) {
|
||||
const reply = poll.reply;
|
||||
const rtt = poll.rtt;
|
||||
@@ -899,25 +904,16 @@ return view.extend({
|
||||
this.lastPollEffectiveLimit = null;
|
||||
|
||||
if (!reply || typeof reply !== 'object' || Array.isArray(reply)) {
|
||||
this.lastPollError = true;
|
||||
this.fillingBuffer = false;
|
||||
this.notePollRtt(rtt, true);
|
||||
this.updateAdaptiveBanner();
|
||||
this.failPollReply(rtt);
|
||||
return;
|
||||
}
|
||||
if (reply.error) {
|
||||
this.lastPollError = true;
|
||||
this.fillingBuffer = false;
|
||||
this.notePollRtt(rtt, true);
|
||||
this.updateAdaptiveBanner();
|
||||
this.failPollReply(rtt);
|
||||
return;
|
||||
}
|
||||
const raw = reply.log;
|
||||
if (!Array.isArray(raw)) {
|
||||
this.lastPollError = true;
|
||||
this.fillingBuffer = false;
|
||||
this.notePollRtt(rtt, true);
|
||||
this.updateAdaptiveBanner();
|
||||
this.failPollReply(rtt);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1173,11 +1169,7 @@ return view.extend({
|
||||
}
|
||||
},
|
||||
|
||||
updateAdaptiveBanner() {
|
||||
const el = document.getElementById('fwlive-adaptive');
|
||||
if (!el) return;
|
||||
if (!el.style) el.style = { display: '' };
|
||||
|
||||
adaptiveBannerParts() {
|
||||
const parts = [];
|
||||
const mode = this.fetchMode === 'manual' ? _('Manual') : _('Auto');
|
||||
if (this.lastPollRequestedLines !== null && this.lastPollReturnedMessages !== null) {
|
||||
@@ -1218,6 +1210,15 @@ return view.extend({
|
||||
else if (this.serverTruncated) parts.push(_('Server truncated this poll (adaptive cap).'));
|
||||
if (this.resolveLoadShed)
|
||||
parts.push(_('Hostname resolve paused while the router is under load.'));
|
||||
return parts;
|
||||
},
|
||||
|
||||
updateAdaptiveBanner() {
|
||||
const el = document.getElementById('fwlive-adaptive');
|
||||
if (!el) return;
|
||||
if (!el.style) el.style = { display: '' };
|
||||
|
||||
const parts = this.adaptiveBannerParts();
|
||||
|
||||
if (parts.length) {
|
||||
el.style.display = 'block';
|
||||
@@ -1603,6 +1604,18 @@ return view.extend({
|
||||
return Array.from(ips);
|
||||
},
|
||||
|
||||
isLoadShedReply(res) {
|
||||
return res && typeof res === 'object' && res.disabled === 'load';
|
||||
},
|
||||
|
||||
resolveNamesFromReply(res) {
|
||||
return res && typeof res === 'object' && res.names && typeof res.names === 'object'
|
||||
? res.names
|
||||
: res && typeof res === 'object' && !Array.isArray(res)
|
||||
? res
|
||||
: {};
|
||||
},
|
||||
|
||||
async resolveHostnamesForEntries(entries) {
|
||||
if (!this.showHostnames || this.resolveInFlight) return;
|
||||
|
||||
@@ -1633,7 +1646,7 @@ return view.extend({
|
||||
const res = await callFwliveResolve({ addresses: need });
|
||||
if (gen !== this.resolveGeneration) return;
|
||||
|
||||
if (res && typeof res === 'object' && res.disabled === 'load') {
|
||||
if (this.isLoadShedReply(res)) {
|
||||
this.resolveLoadShed = true;
|
||||
this.resolveShedUntil = Date.now() + 60000;
|
||||
this.updateAdaptiveBanner();
|
||||
@@ -1649,12 +1662,7 @@ return view.extend({
|
||||
if (res && typeof res === 'object' && typeof res.error === 'string' && res.error)
|
||||
return;
|
||||
/* Full reply: names map under .names; legacy expect-unwrap was the map. */
|
||||
const names =
|
||||
res && typeof res === 'object' && res.names && typeof res.names === 'object'
|
||||
? res.names
|
||||
: res && typeof res === 'object' && !Array.isArray(res)
|
||||
? res
|
||||
: {};
|
||||
const names = this.resolveNamesFromReply(res);
|
||||
let updated = false;
|
||||
|
||||
for (let i = 0; i < need.length; i++) {
|
||||
@@ -2008,6 +2016,168 @@ return view.extend({
|
||||
});
|
||||
},
|
||||
|
||||
renderDisplayDrawer() {
|
||||
return E('div', { 'id': 'fwlive-display-drawer', 'class': 'fwlive-display-bar' }, [
|
||||
E('span', { 'class': 'fwlive-display-bar-label' }, [_('Display options')]),
|
||||
E('div', { 'class': 'fwlive-display-controls' }, [
|
||||
E('label', { 'class': 'fwlive-display-ctl', 'for': 'fwlive-limit' }, [
|
||||
_('Limit'),
|
||||
E(
|
||||
'select',
|
||||
{
|
||||
'id': 'fwlive-limit',
|
||||
'class': 'cbi-input-select'
|
||||
},
|
||||
this.limitSelectOptions()
|
||||
)
|
||||
]),
|
||||
E('label', { 'class': 'fwlive-display-ctl', 'for': 'fwlive-fetch-mode' }, [
|
||||
_('Fetch budget'),
|
||||
E(
|
||||
'select',
|
||||
{
|
||||
'id': 'fwlive-fetch-mode',
|
||||
'class': 'cbi-input-select',
|
||||
'aria-controls': 'fwlive-manual-lines'
|
||||
},
|
||||
this.fetchModeOptions()
|
||||
)
|
||||
]),
|
||||
E('label', { 'class': 'fwlive-display-ctl', 'for': 'fwlive-manual-lines' }, [
|
||||
_('Maximum raw lines'),
|
||||
E(
|
||||
'select',
|
||||
{
|
||||
'id': 'fwlive-manual-lines',
|
||||
'class': 'cbi-input-select',
|
||||
'title': _('Manual still uses server protection and poll cadence')
|
||||
},
|
||||
this.manualFetchLinesOptions()
|
||||
)
|
||||
]),
|
||||
E('label', { 'class': 'fwlive-display-ctl' }, [
|
||||
E('input', {
|
||||
'id': 'fwlive-row-tint-toggle',
|
||||
'type': 'checkbox',
|
||||
'title': _('Show pass/deny row background colors')
|
||||
}),
|
||||
_('Row tint')
|
||||
]),
|
||||
E(
|
||||
'label',
|
||||
{
|
||||
'id': 'fwlive-row-tint-palette-wrap',
|
||||
'class': 'fwlive-display-ctl',
|
||||
'for': 'fwlive-row-tint'
|
||||
},
|
||||
[
|
||||
_('Palette'),
|
||||
E(
|
||||
'select',
|
||||
{
|
||||
'id': 'fwlive-row-tint',
|
||||
'class': 'cbi-input-select',
|
||||
'title': _('Classic uses green/red; Accessible uses teal/orange')
|
||||
},
|
||||
this.rowTintPaletteOptions()
|
||||
)
|
||||
]
|
||||
),
|
||||
E('label', { 'class': 'fwlive-display-ctl' }, [
|
||||
E('input', {
|
||||
'id': 'fwlive-show-hostnames',
|
||||
'type': 'checkbox'
|
||||
}),
|
||||
_('Show hostnames')
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderHelpNodes() {
|
||||
return E('div', { 'class': 'fwlive-help-row' }, [
|
||||
E('details', { 'id': 'fwlive-help', 'class': 'fwlive-help' }, [
|
||||
E('summary', {}, [_('Help')]),
|
||||
E('ul', {}, [
|
||||
E('li', {}, [
|
||||
_(
|
||||
'The table updates automatically when your firewall logs traffic. Use Pause if it moves too fast.'
|
||||
)
|
||||
]),
|
||||
E('li', {}, [
|
||||
_(
|
||||
'Enable logging turns on WAN zone drop/reject logging only (same as Network → Firewall). It does not add rules or log normal LAN browsing.'
|
||||
)
|
||||
]),
|
||||
E('li', {}, [
|
||||
_('Display options on the bar set Limit, row tint, palette, and hostnames.')
|
||||
]),
|
||||
E('li', {}, [
|
||||
_(
|
||||
'Fetch budget controls raw log lines per poll. Auto derives from Limit; Manual selects a bounded maximum.'
|
||||
)
|
||||
]),
|
||||
E('li', {}, [
|
||||
_(
|
||||
'Manual still uses server protection and poll cadence; it changes the fetch budget, not the polling interval.'
|
||||
)
|
||||
]),
|
||||
E('li', {}, [
|
||||
_(
|
||||
'For a responsive table on a weak device, keep Limit at 250 rows or below. The weak-device cap affects rendered rows; the buffer can still retain more.'
|
||||
)
|
||||
]),
|
||||
E('li', {}, [
|
||||
_(
|
||||
'Switching tabs pauses polling; returning performs one catch-up poll. Hostnames are off by default because lookups add work.'
|
||||
)
|
||||
]),
|
||||
E('li', {}, [
|
||||
_(
|
||||
'The router log ring may evict older events before fwlive reads them. fwlive cannot recover evicted entries or change forwarding behavior.'
|
||||
)
|
||||
]),
|
||||
E('li', {}, [
|
||||
_(
|
||||
'The rate shown for WAN logging is the firewall zone log_limit. OpenWrt defaults to 10/minute when no explicit limit is configured; fwlive does not impose this cap.'
|
||||
)
|
||||
]),
|
||||
E('li', { 'id': 'fwlive-manual-test' }, []),
|
||||
E('li', {}, [
|
||||
_(
|
||||
'Click a row (Time or other non-link cells) to see the full log line (Simple view).'
|
||||
)
|
||||
]),
|
||||
E('li', {}, [
|
||||
_(
|
||||
'Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a chip) to exclude.'
|
||||
)
|
||||
]),
|
||||
E('li', {}, [
|
||||
_(
|
||||
'Row tint shows pass/deny row backgrounds when checked. Choose Classic (green/red, default) or Accessible (teal/orange). Action text stays colored either way.'
|
||||
)
|
||||
]),
|
||||
E('li', {}, [_('Use Detail for all columns (flags, length, raw message).')]),
|
||||
E('li', {}, [
|
||||
_(
|
||||
'If Row tint looks missing, the active LuCI theme may omit success/error or info/warn CSS variables; fwlive falls back to local colors (air-gapped, no data leaves the device).'
|
||||
)
|
||||
])
|
||||
])
|
||||
]),
|
||||
E(
|
||||
'span',
|
||||
{
|
||||
'id': 'fwlive-build',
|
||||
'class': 'fwlive-build',
|
||||
'title': 'luci-app-fwlive'
|
||||
},
|
||||
['v' + constants.APP_VERSION]
|
||||
)
|
||||
]);
|
||||
},
|
||||
|
||||
render() {
|
||||
/* LuCI may finish the load/render/addFooter sequence after pagehide. */
|
||||
if (this.viewDisposed) return E('div', { 'class': 'cbi-map' });
|
||||
@@ -2164,89 +2334,7 @@ return view.extend({
|
||||
])
|
||||
]
|
||||
),
|
||||
E('div', { 'id': 'fwlive-display-drawer', 'class': 'fwlive-display-bar' }, [
|
||||
E('span', { 'class': 'fwlive-display-bar-label' }, [_('Display options')]),
|
||||
E('div', { 'class': 'fwlive-display-controls' }, [
|
||||
E('label', { 'class': 'fwlive-display-ctl', 'for': 'fwlive-limit' }, [
|
||||
_('Limit'),
|
||||
E(
|
||||
'select',
|
||||
{
|
||||
'id': 'fwlive-limit',
|
||||
'class': 'cbi-input-select'
|
||||
},
|
||||
this.limitSelectOptions()
|
||||
)
|
||||
]),
|
||||
E('label', { 'class': 'fwlive-display-ctl', 'for': 'fwlive-fetch-mode' }, [
|
||||
_('Fetch budget'),
|
||||
E(
|
||||
'select',
|
||||
{
|
||||
'id': 'fwlive-fetch-mode',
|
||||
'class': 'cbi-input-select',
|
||||
'aria-controls': 'fwlive-manual-lines'
|
||||
},
|
||||
this.fetchModeOptions()
|
||||
)
|
||||
]),
|
||||
E(
|
||||
'label',
|
||||
{ 'class': 'fwlive-display-ctl', 'for': 'fwlive-manual-lines' },
|
||||
[
|
||||
_('Maximum raw lines'),
|
||||
E(
|
||||
'select',
|
||||
{
|
||||
'id': 'fwlive-manual-lines',
|
||||
'class': 'cbi-input-select',
|
||||
'title': _(
|
||||
'Manual still uses server protection and poll cadence'
|
||||
)
|
||||
},
|
||||
this.manualFetchLinesOptions()
|
||||
)
|
||||
]
|
||||
),
|
||||
E('label', { 'class': 'fwlive-display-ctl' }, [
|
||||
E('input', {
|
||||
'id': 'fwlive-row-tint-toggle',
|
||||
'type': 'checkbox',
|
||||
'title': _('Show pass/deny row background colors')
|
||||
}),
|
||||
_('Row tint')
|
||||
]),
|
||||
E(
|
||||
'label',
|
||||
{
|
||||
'id': 'fwlive-row-tint-palette-wrap',
|
||||
'class': 'fwlive-display-ctl',
|
||||
'for': 'fwlive-row-tint'
|
||||
},
|
||||
[
|
||||
_('Palette'),
|
||||
E(
|
||||
'select',
|
||||
{
|
||||
'id': 'fwlive-row-tint',
|
||||
'class': 'cbi-input-select',
|
||||
'title': _(
|
||||
'Classic uses green/red; Accessible uses teal/orange'
|
||||
)
|
||||
},
|
||||
this.rowTintPaletteOptions()
|
||||
)
|
||||
]
|
||||
),
|
||||
E('label', { 'class': 'fwlive-display-ctl' }, [
|
||||
E('input', {
|
||||
'id': 'fwlive-show-hostnames',
|
||||
'type': 'checkbox'
|
||||
}),
|
||||
_('Show hostnames')
|
||||
])
|
||||
])
|
||||
]),
|
||||
this.renderDisplayDrawer(),
|
||||
E(
|
||||
'div',
|
||||
{ 'id': 'fwlive-filter-panel', 'class': 'fwlive-filter-panel fwlive-find-row' },
|
||||
@@ -2348,91 +2436,7 @@ return view.extend({
|
||||
E('tbody', {}, [])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'fwlive-help-row' }, [
|
||||
E('details', { 'id': 'fwlive-help', 'class': 'fwlive-help' }, [
|
||||
E('summary', {}, [_('Help')]),
|
||||
E('ul', {}, [
|
||||
E('li', {}, [
|
||||
_(
|
||||
'The table updates automatically when your firewall logs traffic. Use Pause if it moves too fast.'
|
||||
)
|
||||
]),
|
||||
E('li', {}, [
|
||||
_(
|
||||
'Enable logging turns on WAN zone drop/reject logging only (same as Network → Firewall). It does not add rules or log normal LAN browsing.'
|
||||
)
|
||||
]),
|
||||
E('li', {}, [
|
||||
_(
|
||||
'Display options on the bar set Limit, row tint, palette, and hostnames.'
|
||||
)
|
||||
]),
|
||||
E('li', {}, [
|
||||
_(
|
||||
'Fetch budget controls raw log lines per poll. Auto derives from Limit; Manual selects a bounded maximum.'
|
||||
)
|
||||
]),
|
||||
E('li', {}, [
|
||||
_(
|
||||
'Manual still uses server protection and poll cadence; it changes the fetch budget, not the polling interval.'
|
||||
)
|
||||
]),
|
||||
E('li', {}, [
|
||||
_(
|
||||
'For a responsive table on a weak device, keep Limit at 250 rows or below. The weak-device cap affects rendered rows; the buffer can still retain more.'
|
||||
)
|
||||
]),
|
||||
E('li', {}, [
|
||||
_(
|
||||
'Switching tabs pauses polling; returning performs one catch-up poll. Hostnames are off by default because lookups add work.'
|
||||
)
|
||||
]),
|
||||
E('li', {}, [
|
||||
_(
|
||||
'The router log ring may evict older events before fwlive reads them. fwlive cannot recover evicted entries or change forwarding behavior.'
|
||||
)
|
||||
]),
|
||||
E('li', {}, [
|
||||
_(
|
||||
'The rate shown for WAN logging is the firewall zone log_limit. OpenWrt defaults to 10/minute when no explicit limit is configured; fwlive does not impose this cap.'
|
||||
)
|
||||
]),
|
||||
E('li', { 'id': 'fwlive-manual-test' }, []),
|
||||
E('li', {}, [
|
||||
_(
|
||||
'Click a row (Time or other non-link cells) to see the full log line (Simple view).'
|
||||
)
|
||||
]),
|
||||
E('li', {}, [
|
||||
_(
|
||||
'Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a chip) to exclude.'
|
||||
)
|
||||
]),
|
||||
E('li', {}, [
|
||||
_(
|
||||
'Row tint shows pass/deny row backgrounds when checked. Choose Classic (green/red, default) or Accessible (teal/orange). Action text stays colored either way.'
|
||||
)
|
||||
]),
|
||||
E('li', {}, [
|
||||
_('Use Detail for all columns (flags, length, raw message).')
|
||||
]),
|
||||
E('li', {}, [
|
||||
_(
|
||||
'If Row tint looks missing, the active LuCI theme may omit success/error or info/warn CSS variables; fwlive falls back to local colors (air-gapped, no data leaves the device).'
|
||||
)
|
||||
])
|
||||
])
|
||||
]),
|
||||
E(
|
||||
'span',
|
||||
{
|
||||
'id': 'fwlive-build',
|
||||
'class': 'fwlive-build',
|
||||
'title': 'luci-app-fwlive'
|
||||
},
|
||||
['v' + constants.APP_VERSION]
|
||||
)
|
||||
])
|
||||
this.renderHelpNodes()
|
||||
]
|
||||
);
|
||||
},
|
||||
|
||||
@@ -280,6 +280,64 @@ _fwlive_mktemp() {
|
||||
printf '%s' "$_mk_file"
|
||||
}
|
||||
|
||||
enrich_rules_dump() {
|
||||
_dump_prefix="$1"
|
||||
_dump_kind="$2"
|
||||
|
||||
case "$_dump_kind" in
|
||||
nft)
|
||||
_dump_error='nft_failed'
|
||||
;;
|
||||
iptables)
|
||||
_dump_error='iptables_failed'
|
||||
;;
|
||||
ip6tables)
|
||||
_dump_error='ip6tables_failed'
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
_rules_dump=$(_fwlive_mktemp "$_dump_prefix") || _rules_dump=''
|
||||
if [ -n "$_rules_dump" ]; then
|
||||
# Keep the dump command and stream parser paired by backend. The
|
||||
# ip6tables caller is availability-gated below; its empty-error
|
||||
# precedence is intentionally preserved in the mktemp branch.
|
||||
if case "$_dump_kind" in
|
||||
nft)
|
||||
nft_list_ruleset >"$_rules_dump" 2>/dev/null
|
||||
;;
|
||||
iptables|ip6tables)
|
||||
run_with_timeout "$IPTABLES_TIMEOUT" "${_dump_kind}-save" >"$_rules_dump" 2>/dev/null
|
||||
;;
|
||||
esac
|
||||
then
|
||||
case "$_dump_kind" in
|
||||
nft)
|
||||
map_from_nft_stream labeled <"$_rules_dump"
|
||||
map_from_nft_stream unlabeled <"$_rules_dump"
|
||||
;;
|
||||
iptables|ip6tables)
|
||||
map_from_iptables_save_stream labeled <"$_rules_dump"
|
||||
map_from_iptables_save_stream unlabeled <"$_rules_dump"
|
||||
;;
|
||||
esac
|
||||
elif [ -z "$_enrich_error" ]; then
|
||||
_enrich_error="$_dump_error"
|
||||
fi
|
||||
rm -f "$_rules_dump"
|
||||
elif [ "$_dump_kind" = ip6tables ]; then
|
||||
# IPv6 enrichment is optional and must not replace an earlier
|
||||
# IPv4/backend error when its temp file cannot be allocated.
|
||||
if [ -z "$_enrich_error" ]; then
|
||||
_enrich_error='mktemp_failed'
|
||||
fi
|
||||
else
|
||||
_enrich_error='mktemp_failed'
|
||||
fi
|
||||
}
|
||||
|
||||
build_rules_map() {
|
||||
OUT=''
|
||||
_map_keys=0
|
||||
@@ -293,47 +351,12 @@ build_rules_map() {
|
||||
|
||||
case "$backend" in
|
||||
nft)
|
||||
_rules_dump=$(_fwlive_mktemp fwlive-nft) || _rules_dump=''
|
||||
if [ -n "$_rules_dump" ]; then
|
||||
# Dumps land on /tmp (tmpfs). Duration bounded by
|
||||
# NFT_TIMEOUT / IPTABLES_TIMEOUT; key/byte caps in map_add.
|
||||
if nft_list_ruleset >"$_rules_dump" 2>/dev/null; then
|
||||
map_from_nft_stream labeled <"$_rules_dump"
|
||||
map_from_nft_stream unlabeled <"$_rules_dump"
|
||||
elif [ -z "$_enrich_error" ]; then
|
||||
_enrich_error='nft_failed'
|
||||
fi
|
||||
rm -f "$_rules_dump"
|
||||
else
|
||||
_enrich_error='mktemp_failed'
|
||||
fi
|
||||
enrich_rules_dump fwlive-nft nft
|
||||
;;
|
||||
iptables)
|
||||
_rules_dump=$(_fwlive_mktemp fwlive-ipt) || _rules_dump=''
|
||||
if [ -n "$_rules_dump" ]; then
|
||||
if run_with_timeout "$IPTABLES_TIMEOUT" iptables-save >"$_rules_dump" 2>/dev/null; then
|
||||
map_from_iptables_save_stream labeled <"$_rules_dump"
|
||||
map_from_iptables_save_stream unlabeled <"$_rules_dump"
|
||||
elif [ -z "$_enrich_error" ]; then
|
||||
_enrich_error='iptables_failed'
|
||||
fi
|
||||
rm -f "$_rules_dump"
|
||||
else
|
||||
_enrich_error='mktemp_failed'
|
||||
fi
|
||||
enrich_rules_dump fwlive-ipt iptables
|
||||
if command -v ip6tables-save >/dev/null 2>&1; then
|
||||
_rules_dump=$(_fwlive_mktemp fwlive-ip6t) || _rules_dump=''
|
||||
if [ -n "$_rules_dump" ]; then
|
||||
if run_with_timeout "$IPTABLES_TIMEOUT" ip6tables-save >"$_rules_dump" 2>/dev/null; then
|
||||
map_from_iptables_save_stream labeled <"$_rules_dump"
|
||||
map_from_iptables_save_stream unlabeled <"$_rules_dump"
|
||||
elif [ -z "$_enrich_error" ]; then
|
||||
_enrich_error='ip6tables_failed'
|
||||
fi
|
||||
rm -f "$_rules_dump"
|
||||
elif [ -z "$_enrich_error" ]; then
|
||||
_enrich_error='mktemp_failed'
|
||||
fi
|
||||
enrich_rules_dump fwlive-ip6t ip6tables
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
@@ -696,32 +719,36 @@ EOF
|
||||
printf '{"names":{%s}}' "$OUT"
|
||||
}
|
||||
|
||||
check_eq() {
|
||||
_check_name="$1"
|
||||
_check_got="$2"
|
||||
_check_expected="$3"
|
||||
_check_failure="$4"
|
||||
if [ "$_check_got" != "$_check_expected" ]; then
|
||||
# Keep the failure suffix at each call site so stderr stays byte-for-byte
|
||||
# identical to the pre-extraction selftest messages.
|
||||
echo "$_check_name: $_check_failure" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
run_selftest() {
|
||||
input=$(printf 'a\\b"c\nd\te')
|
||||
escaped=$(printf '%s' "$input" | json_escape)
|
||||
expected=$(printf 'a\\\\b\\"c\\nd\\te')
|
||||
if [ "$escaped" != "$expected" ]; then
|
||||
echo "json_escape: mismatch" >&2
|
||||
return 1
|
||||
fi
|
||||
check_eq json_escape "$escaped" "$expected" mismatch || return 1
|
||||
|
||||
# Remaining C0 controls must become \u00XX (RFC 8259), not raw bytes.
|
||||
input=$(printf 'x\001y\037z')
|
||||
escaped=$(printf '%s' "$input" | json_escape)
|
||||
expected='x\u0001y\u001fz'
|
||||
if [ "$escaped" != "$expected" ]; then
|
||||
echo "json_escape C0: expected $expected got $escaped" >&2
|
||||
return 1
|
||||
fi
|
||||
check_eq 'json_escape C0' "$escaped" "$expected" "expected $expected got $escaped" || return 1
|
||||
|
||||
# Blank lines are data (paragraph-mode RS="" used to drop them).
|
||||
input=$(printf 'a\n\nb')
|
||||
escaped=$(printf '%s' "$input" | json_escape)
|
||||
expected=$(printf 'a\\n\\nb')
|
||||
if [ "$escaped" != "$expected" ]; then
|
||||
echo "json_escape blank line: expected $expected got $escaped" >&2
|
||||
return 1
|
||||
fi
|
||||
check_eq 'json_escape blank line' "$escaped" "$expected" "expected $expected got $escaped" || return 1
|
||||
|
||||
if is_resolvable_address 'not an ip'; then
|
||||
echo 'is_resolvable_address: expected reject for spaces' >&2
|
||||
@@ -812,25 +839,13 @@ run_selftest() {
|
||||
fi
|
||||
|
||||
got=$(poll_clamp_lines "99999999999999999999")
|
||||
if [ "$got" != "$POLL_LINES_MAX" ]; then
|
||||
echo "poll_clamp_lines: over-long expected $POLL_LINES_MAX got $got" >&2
|
||||
return 1
|
||||
fi
|
||||
check_eq poll_clamp_lines "$got" "$POLL_LINES_MAX" "over-long expected $POLL_LINES_MAX got $got" || return 1
|
||||
got=$(poll_clamp_lines "0")
|
||||
if [ "$got" != "50" ]; then
|
||||
echo "poll_clamp_lines: zero expected 50 got $got" >&2
|
||||
return 1
|
||||
fi
|
||||
check_eq poll_clamp_lines "$got" "50" "zero expected 50 got $got" || return 1
|
||||
got=$(poll_clamp_lines "2001")
|
||||
if [ "$got" != "$POLL_LINES_MAX" ]; then
|
||||
echo "poll_clamp_lines: 2001 expected $POLL_LINES_MAX got $got" >&2
|
||||
return 1
|
||||
fi
|
||||
check_eq poll_clamp_lines "$got" "$POLL_LINES_MAX" "2001 expected $POLL_LINES_MAX got $got" || return 1
|
||||
got=$(poll_clamp_lines "500")
|
||||
if [ "$got" != "500" ]; then
|
||||
echo "poll_clamp_lines: expected 500 got $got" >&2
|
||||
return 1
|
||||
fi
|
||||
check_eq poll_clamp_lines "$got" "500" "expected 500 got $got" || return 1
|
||||
|
||||
if ! _fwlive_tmp_dir_ok; then
|
||||
echo "_fwlive_tmp_dir_ok: /tmp must be a sticky directory" >&2
|
||||
@@ -841,18 +856,12 @@ run_selftest() {
|
||||
Address: 127.0.0.1:53
|
||||
|
||||
8.8.8.8.in-addr.arpa name = dns.google.")
|
||||
if [ "$got" != "dns.google" ]; then
|
||||
echo "parse_nslookup_name: bind-style expected dns.google got '$got'" >&2
|
||||
return 1
|
||||
fi
|
||||
check_eq parse_nslookup_name "$got" "dns.google" "bind-style expected dns.google got '$got'" || return 1
|
||||
got=$(parse_nslookup_name "Server: 127.0.0.1
|
||||
Address: 127.0.0.1:53
|
||||
|
||||
Address 1: 8.8.8.8 dns.google.")
|
||||
if [ "$got" != "dns.google" ]; then
|
||||
echo "parse_nslookup_name: busybox Address N expected dns.google got '$got'" >&2
|
||||
return 1
|
||||
fi
|
||||
check_eq parse_nslookup_name "$got" "dns.google" "busybox Address N expected dns.google got '$got'" || return 1
|
||||
got=$(parse_nslookup_name "Server: 192.168.1.1
|
||||
Address 1: 192.168.1.1 router.lan
|
||||
|
||||
@@ -864,10 +873,7 @@ Address 1: 8.8.8.8 dns.google")
|
||||
fi
|
||||
got=$(parse_nslookup_name "Server: 127.0.0.1
|
||||
Address: 127.0.0.1:53")
|
||||
if [ -n "$got" ]; then
|
||||
echo "parse_nslookup_name: server-only expected empty got '$got'" >&2
|
||||
return 1
|
||||
fi
|
||||
check_eq parse_nslookup_name "$got" '' "server-only expected empty got '$got'" || return 1
|
||||
|
||||
run_logging_selftest || return 1
|
||||
|
||||
@@ -888,41 +894,20 @@ Address: 127.0.0.1:53")
|
||||
fi
|
||||
|
||||
got=$(poll_lines_from_input '{"addresses":["999999"]}')
|
||||
if [ "$got" != "$POLL_LINES_MAX" ]; then
|
||||
echo "poll_lines_from_input: expected $POLL_LINES_MAX got $got" >&2
|
||||
return 1
|
||||
fi
|
||||
check_eq poll_lines_from_input "$got" "$POLL_LINES_MAX" "expected $POLL_LINES_MAX got $got" || return 1
|
||||
|
||||
got=$(poll_lines_from_input '{"addresses":["99999999999999999999"]}')
|
||||
if [ "$got" != "$POLL_LINES_MAX" ]; then
|
||||
echo "poll_lines_from_input: over-long expected $POLL_LINES_MAX got $got" >&2
|
||||
return 1
|
||||
fi
|
||||
check_eq poll_lines_from_input "$got" "$POLL_LINES_MAX" "over-long expected $POLL_LINES_MAX got $got" || return 1
|
||||
got=$(poll_lines_from_input '{"addresses":["18446744073709551616"]}')
|
||||
if [ "$got" != "$POLL_LINES_MAX" ]; then
|
||||
echo "poll_lines_from_input: 18446744073709551616 expected $POLL_LINES_MAX got $got" >&2
|
||||
return 1
|
||||
fi
|
||||
check_eq poll_lines_from_input "$got" "$POLL_LINES_MAX" "18446744073709551616 expected $POLL_LINES_MAX got $got" || return 1
|
||||
got=$(poll_lines_from_input '{"addresses":["0"]}')
|
||||
if [ "$got" != "50" ]; then
|
||||
echo "poll_lines_from_input: zero expected 50 got $got" >&2
|
||||
return 1
|
||||
fi
|
||||
check_eq poll_lines_from_input "$got" "50" "zero expected 50 got $got" || return 1
|
||||
got=$(poll_lines_from_input '{"addresses":["2001"]}')
|
||||
if [ "$got" != "$POLL_LINES_MAX" ]; then
|
||||
echo "poll_lines_from_input: 2001 expected $POLL_LINES_MAX got $got" >&2
|
||||
return 1
|
||||
fi
|
||||
check_eq poll_lines_from_input "$got" "$POLL_LINES_MAX" "2001 expected $POLL_LINES_MAX got $got" || return 1
|
||||
got=$(poll_lines_from_input '{"addresses":["500"]}')
|
||||
if [ "$got" != "500" ]; then
|
||||
echo "poll_lines_from_input: expected 500 got $got" >&2
|
||||
return 1
|
||||
fi
|
||||
check_eq poll_lines_from_input "$got" "500" "expected 500 got $got" || return 1
|
||||
got=$(poll_lines_from_input '{"addresses":["100"]}')
|
||||
if [ "$got" != "100" ]; then
|
||||
echo "poll_lines_from_input: expected 100 got $got" >&2
|
||||
return 1
|
||||
fi
|
||||
check_eq poll_lines_from_input "$got" "100" "expected 100 got $got" || return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
+12
-14
@@ -1,17 +1,18 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=webd
|
||||
PKG_RELEASE:=1
|
||||
PKG_RELEASE:=3
|
||||
|
||||
PKG_VERSION:=20240223
|
||||
PKG_VERSION_MEDIATEK_FILO:=20240710
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/$(PKG_NAME)
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
TITLE:=A Lightweight self-hosted netdisk application
|
||||
URL:=https://webd90kb.github.io/webd/
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
TITLE:=A Lightweight self-hosted netdisk application
|
||||
URL:=https://webd.cf
|
||||
endef
|
||||
|
||||
TARGET_LIBC:=musl
|
||||
@@ -79,12 +80,14 @@ endif
|
||||
|
||||
ifeq ($(ARCH),aarch64)
|
||||
TARGET_ARCH:=aarch64
|
||||
|
||||
ifeq ($(BOARD),rockchip)
|
||||
TARGET_BOARD:=musl.rockchip-armv8
|
||||
endif
|
||||
ifeq ($(BOARD),brcm2708)
|
||||
TARGET_BOARD:=musl.brcm2708-bcm2710
|
||||
endif
|
||||
|
||||
ifeq ($(BOARD),mediatek)
|
||||
ifneq (,$(filter filogic,$(SUBTARGET)))
|
||||
TARGET_BOARD:=musl.mediatek-filogic
|
||||
@@ -93,16 +96,8 @@ ifeq ($(ARCH),aarch64)
|
||||
endif
|
||||
endif
|
||||
|
||||
ifneq ($(DUMP),1)
|
||||
ifneq ($(ARCH),)
|
||||
ifeq ($(strip $(TARGET_ARCH)$(TARGET_BOARD)),)
|
||||
$(error Unsupported ARCH/BOARD combo: ARCH=$(ARCH) BOARD=$(BOARD) SUBTARGET=$(SUBTARGET))
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION)-$(TARGET_ARCH)-openwrt-linux-$(TARGET_BOARD).tar.gz
|
||||
PKG_SOURCE_URL:=https://webd90kb.github.io/webd/webd/webd_dl/20240223/
|
||||
PKG_SOURCE_URL:=https://webd90kb.github.io/webd/webd/webd_dl/$(PKG_VERSION)/
|
||||
PKG_HASH:=skip
|
||||
|
||||
UNTAR_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
|
||||
@@ -118,10 +113,13 @@ endef
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) $(UNTAR_DIR)/webd/webd $(1)/usr/bin/webd
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/$(PKG_NAME)/web
|
||||
$(CP) -r $(UNTAR_DIR)/webd/web/* $(1)/usr/share/$(PKG_NAME)/web/
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc
|
||||
$(INSTALL_DATA) $(UNTAR_DIR)/webd/webd.conf $(1)/etc/webd.conf.example
|
||||
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
Reference in new issue
Block a user