diff --git a/luci-app-fwlive/Makefile b/luci-app-fwlive/Makefile index ef59d18fd..a46433936 100644 --- a/luci-app-fwlive/Makefile +++ b/luci-app-fwlive/Makefile @@ -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 diff --git a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js index abdd79814..c349dec8d 100644 --- a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js +++ b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js @@ -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')) ]) diff --git a/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js b/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js index 46378d5dd..26669f37c 100644 --- a/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js +++ b/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js @@ -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() ] ); }, diff --git a/luci-app-fwlive/root/usr/libexec/rpcd/fwlive b/luci-app-fwlive/root/usr/libexec/rpcd/fwlive index 25307a2e4..5a7d5bf54 100755 --- a/luci-app-fwlive/root/usr/libexec/rpcd/fwlive +++ b/luci-app-fwlive/root/usr/libexec/rpcd/fwlive @@ -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 } diff --git a/webd/Makefile b/webd/Makefile index f86b106d6..325b689e7 100755 --- a/webd/Makefile +++ b/webd/Makefile @@ -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)))