diff --git a/dae/Makefile b/dae/Makefile index f1e13fee..66585194 100644 --- a/dae/Makefile +++ b/dae/Makefile @@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=dae PKG_VERSION:=2026.08.28 -PKG_RELEASE:=40 +PKG_RELEASE:=41 PKG_SOURCE:=dae-src-2026.08.28-d6aca8a10b35.tar.gz PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src diff --git a/dae/files/dae.init b/dae/files/dae.init index 41fc7d49..a47ea99a 100644 --- a/dae/files/dae.init +++ b/dae/files/dae.init @@ -30,7 +30,6 @@ start_service() { procd_set_param env DAE_LOCATION_ASSET="/usr/share/v2ray" TZ="$(uci -q get system.@system[0].zonename)" procd_set_param command "$PROG" run procd_append_param command --config "$config_file" - procd_append_param command --disable-timestamp procd_append_param command --logfile "$LOG_DIR/dae.log" procd_append_param command --logfile-maxbackups "$log_maxbackups" procd_append_param command --logfile-maxsize "$log_maxsize" diff --git a/filebrowser-q/Makefile b/filebrowser-q/Makefile new file mode 100644 index 00000000..81d57543 --- /dev/null +++ b/filebrowser-q/Makefile @@ -0,0 +1,61 @@ +# +# Copyright (C) 2017-2024 +# +# This is free software, licensed under the GNU General Public License v2. +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=filebrowser-q +PKG_VERSION:=1.5.6-stable +PKG_RELEASE=1 + +ifeq ($(ARCH),aarch64) + PKG_ARCH:=arm64 + PKG_HASH:=skip +else ifeq ($(ARCH),arm) + PKG_ARCH:=armv7 + PKG_HASH:=skip +else ifeq ($(ARCH),x86_64) + PKG_ARCH:=amd64 + PKG_HASH:=skip +endif + +include $(INCLUDE_DIR)/package.mk + +define Package/filebrowser-q + SECTION:=net + CATEGORY:=Network + DEPENDS:=@(arm||aarch64||x86_64) + TITLE:=FileBrowser Quantum +endef + +define Package/filebrowser-q/description + The best free self-hosted web-based file manager. +endef + +PKG_SOURCE:=linux-$(PKG_ARCH)-filebrowser +PKG_SOURCE_URL:=https://github.com/gtsteffaniak/filebrowser/releases/download/v$(PKG_VERSION) + +define Build/Prepare + $(call Build/Prepare/Default) +endef + +define Build/Compile +endef + +define Package/filebrowser-q/conffiles +/etc/filebrowser-q/ +/etc/config/filebrowser-q +endef + +define Package/filebrowser-q/install + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) $(CURDIR)/files/filebrowser.init $(1)/etc/init.d/filebrowser-q + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) $(CURDIR)/files/filebrowser.config $(1)/etc/config/filebrowser-q + $(INSTALL_DIR) $(1)/usr/bin/ + $(INSTALL_BIN) $(DL_DIR)/linux-$(PKG_ARCH)-filebrowser $(1)/usr/bin/filebrowser-q +endef + +$(eval $(call BuildPackage,filebrowser-q)) diff --git a/filebrowser-q/files/config.yaml b/filebrowser-q/files/config.yaml new file mode 100644 index 00000000..f290aec7 --- /dev/null +++ b/filebrowser-q/files/config.yaml @@ -0,0 +1,13 @@ +server: + port: 8989 + database: "/etc/filebrowser-q/database.db" + sources: + - path: "/" # Do not use a root "/" directory or include the "/var" folder + config: + defaultEnabled: true +auth: + adminUsername: admin + adminPassword: "admin" +userDefaults: + ui: + locale: "zhCN" diff --git a/filebrowser-q/files/filebrowser.config b/filebrowser-q/files/filebrowser.config new file mode 100644 index 00000000..b28f95d3 --- /dev/null +++ b/filebrowser-q/files/filebrowser.config @@ -0,0 +1,5 @@ +config filebrowser 'config' + option enabled '0' + option listen_port '8989' + option root_path '/' + diff --git a/filebrowser-q/files/filebrowser.init b/filebrowser-q/files/filebrowser.init new file mode 100644 index 00000000..a91522a4 --- /dev/null +++ b/filebrowser-q/files/filebrowser.init @@ -0,0 +1,60 @@ +#!/bin/sh /etc/rc.common + +START=99 +STOP=10 + +CONF="filebrowser-q" +PROG="/usr/bin/filebrowser-q" +CONF_PATH="/etc/filebrowser-q/config.yaml" +PID_FILE="/var/run/filebrowser-q.pid" + +start() { + config_load "$CONF" + + local enabled + config_get_bool enabled "config" "enabled" "0" + [ "$enabled" -eq "1" ] || return 1 + + local listen_port root_path root_name + config_get listen_port "config" "listen_port" "8989" + config_get root_path "config" "root_path" "/" + + # 路径为 / 时 name 设为 root,否则留空 + root_name="" + [ "$root_path" = "/" ] && root_name="root" + + if [ ! -f "$CONF_PATH" ]; then + mkdir -p "$(dirname "$CONF_PATH")" + cat < "$CONF_PATH" +server: + port: $listen_port + database: "/etc/filebrowser-q/database.db" + sources: + - path: "$root_path" + name: "$root_name" + config: + defaultEnabled: true +auth: + adminUsername: "admin" + adminPassword: "admin" +userDefaults: + ui: + locale: "zhCN" +EOF + else + grep -q " name:" "$CONF_PATH" || sed -i "s,.*- path:.*,&\n name: \"\"," "$CONF_PATH" + sed -e "s/.*port:.*/ port: $listen_port/" \ + -e "s,.*- path:.*, - path: \"$root_path\"," \ + -e "s/.*name:.*/ name: \"$root_name\"/" \ + -i "$CONF_PATH" + fi + + echo "Starting filebrowser..." + start-stop-daemon -S -q -b -m -p "$PID_FILE" -x "$PROG" -- -c "$CONF_PATH" +} + +stop() { + kill -9 `pidof filebrowser-q | sed "s/$$//g"` 2>/dev/null + rm -f "$PID_FILE" + echo "filebrowser stopped" +} diff --git a/luci-app-daede/Makefile b/luci-app-daede/Makefile index 63991690..27969a5c 100644 --- a/luci-app-daede/Makefile +++ b/luci-app-daede/Makefile @@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-daede PKG_VERSION:=1.14.7 -PKG_RELEASE:=41 +PKG_RELEASE:=42 PKG_MAINTAINER:=kenzok8 PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) @@ -78,6 +78,11 @@ define Package/$(PKG_NAME)/install $(INSTALL_BIN) ./root/usr/share/luci-app-daede/proxy-check.sh $(1)/usr/share/luci-app-daede/proxy-check.sh $(INSTALL_BIN) ./root/usr/share/luci-app-daede/fetch-clash-yaml.sh $(1)/usr/share/luci-app-daede/fetch-clash-yaml.sh $(INSTALL_BIN) ./root/usr/share/luci-app-daede/config-backup.sh $(1)/usr/share/luci-app-daede/config-backup.sh + $(INSTALL_BIN) ./root/usr/share/luci-app-daede/config-defaults.sh $(1)/usr/share/luci-app-daede/config-defaults.sh + $(INSTALL_DIR) $(1)/usr/share/luci-app-daede/defaults + $(INSTALL_DATA) $(CURDIR)/../dae/files/dae.config $(1)/usr/share/luci-app-daede/defaults/dae + $(INSTALL_DATA) $(CURDIR)/../daed/files/daed.config $(1)/usr/share/luci-app-daede/defaults/daed + $(INSTALL_DATA) ./root/etc/config/daede $(1)/usr/share/luci-app-daede/defaults/daede $(INSTALL_BIN) ./root/usr/share/luci-app-daede/refresh-index.sh $(1)/usr/share/luci-app-daede/refresh-index.sh $(INSTALL_BIN) ./root/usr/share/luci-app-daede/geo-cron.sh $(1)/usr/share/luci-app-daede/geo-cron.sh $(INSTALL_BIN) ./root/usr/share/luci-app-daede/daed-sub-update.sh $(1)/usr/share/luci-app-daede/daed-sub-update.sh diff --git a/luci-app-daede/htdocs/luci-static/resources/view/daede/log.js b/luci-app-daede/htdocs/luci-static/resources/view/daede/log.js index 36fdc305..961539eb 100644 --- a/luci-app-daede/htdocs/luci-static/resources/view/daede/log.js +++ b/luci-app-daede/htdocs/luci-static/resources/view/daede/log.js @@ -6,6 +6,7 @@ 'require ui'; 'require view'; 'require view.daede.backend as backend'; +'require view.daede.styles as styles'; const MAX_LINES = 5000; @@ -54,6 +55,7 @@ const CSS = [ /* 拆字段:time="May 25 07:04:59" level=info msg="..." key=val key="val with space" ... */ const RE_LINE = /^time="([^"]*)"\s+level=(\w+)\s+msg=(?:"((?:[^"\\]|\\.)*)"|(\S+))\s*(.*)$/; const RE_PREFIXED_LINE = /^\[([^\]]+)\]\s+(DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|PANIC)\s*(.*)$/i; +const RE_PLAIN_LEVEL_LINE = /^\s*(DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|PANIC)\s+(.*)$/i; function detectLevel(line) { // daed/dae logs use lvl=info / [INFO] / level=warning style @@ -117,13 +119,21 @@ function parseLine(line) { } m = line.match(RE_PREFIXED_LINE); - if (!m) return null; - - const body = m[3] || ''; + let ts = '', lvl = '', body = ''; + if (m) { + ts = formatTs(m[1]); + lvl = m[2]; + body = m[3] || ''; + } else { + m = line.match(RE_PLAIN_LEVEL_LINE); + if (!m) return null; + lvl = m[1]; + body = m[2] || ''; + } const kvStart = body.search(/(?:^|\s)(?=[A-Za-z_][\w.-]*=)/); return { - ts: formatTs(m[1]), - lvl: m[2], + ts: ts, + lvl: lvl, msg: kvStart === -1 ? body : body.slice(0, kvStart).trimEnd(), kv: kvStart === -1 ? '' : body.slice(kvStart).trim() }; @@ -135,11 +145,10 @@ function buildLine(ln) { if (!parsed) { return E('div', { 'class': 'dd-line ' + cls }, ln); } - const parts = [ - E('span', { 'class': 'dd-ts' }, parsed.ts), - E('span', { 'class': 'dd-lvl ' + lvlClass(parsed.lvl) }, lvlShort(parsed.lvl)), - E('span', { 'class': 'dd-msg' }, parsed.msg) - ]; + const parts = []; + if (parsed.ts) parts.push(E('span', { 'class': 'dd-ts' }, parsed.ts)); + parts.push(E('span', { 'class': 'dd-lvl ' + lvlClass(parsed.lvl) }, lvlShort(parsed.lvl))); + parts.push(E('span', { 'class': 'dd-msg' }, parsed.msg)); if (parsed.kv) parts.push(E('span', { 'class': 'dd-kv' }, parsed.kv)); return E('div', { 'class': 'dd-line ' + cls }, parts); } @@ -158,7 +167,8 @@ return view.extend({ paused: false, autoScroll: true, filter: '', - userScrolled: false + userScrolled: false, + reading: false }; const pane = E('div', { 'class': 'dd-log-pane', 'id': 'dd-log-pane' }, [ @@ -173,19 +183,29 @@ return view.extend({ const meta = E('span', { 'class': 'dd-log-meta' }, ''); - const cbAuto = E('input', { 'type': 'checkbox', 'checked': 'checked' }); - cbAuto.addEventListener('change', function() { - state.autoScroll = cbAuto.checked; + const btnAuto = E('button', { 'type': 'button', 'class': 'dd-log-btn dd-log-toggle', 'id': 'dd-log-auto' }); + const btnPause = E('button', { 'type': 'button', 'class': 'dd-log-btn dd-log-toggle', 'id': 'dd-log-pause' }); + const syncControls = function() { + btnAuto.textContent = state.autoScroll ? '✓ ' + _('Auto-scroll: On') : '○ ' + _('Auto-scroll: Off'); + btnAuto.setAttribute('aria-pressed', String(state.autoScroll)); + btnPause.textContent = state.paused ? '▶ ' + _('Resume') : 'Ⅱ ' + _('Pause'); + btnPause.setAttribute('aria-pressed', String(state.paused)); + meta.textContent = state.paused ? _('Paused') : ''; + }; + btnAuto.addEventListener('click', function() { + state.autoScroll = !state.autoScroll; if (state.autoScroll) { pane.scrollTop = pane.scrollHeight; state.userScrolled = false; } + syncControls(); }); - - const cbPause = E('input', { 'type': 'checkbox' }); - cbPause.addEventListener('change', function() { - state.paused = cbPause.checked; + btnPause.addEventListener('click', function() { + state.paused = !state.paused; + syncControls(); + if (!state.paused) tick(); }); + syncControls(); const selFilter = E('select', { 'class': 'dd-log-btn' }, [ E('option', { 'value': '' }, _('All')), @@ -287,9 +307,11 @@ return view.extend({ } function tick() { - if (state.paused) return Promise.resolve(); + if (state.paused || state.reading) return Promise.resolve(); + state.reading = true; return fs.stat(LOG_PATH).then(function(st) { + if (state.paused) return; const size = st.size || 0; if (size === state.lastSize) { meta.textContent = '%d bytes · live'.format(size); @@ -299,6 +321,7 @@ return view.extend({ // File rotated/truncated → full reload const rotated = size < state.lastSize; return fs.read_direct(LOG_PATH, 'text').then(function(content) { + if (state.paused) return; content = content || ''; let delta; if (rotated || state.lastContent === '') { @@ -323,6 +346,7 @@ return view.extend({ pane.scrollTop = pane.scrollHeight; }); }).catch(function(e) { + if (state.paused) return; const msg = String(e); if (msg.indexOf('NotFoundError') !== -1 || msg.indexOf('No such') !== -1) renderEmpty(_('Log file does not exist yet.')); @@ -331,15 +355,15 @@ return view.extend({ state.lastSize = -1; state.lastContent = ''; meta.textContent = ''; - }); + }).finally(function() { state.reading = false; }); } poll.add(tick); tick(); const toolbarItems = [ - E('label', {}, [ cbAuto, _('Auto-scroll') ]), - E('label', {}, [ cbPause, _('Pause') ]), + btnAuto, + btnPause, selFilter, btnClear, btnDownload, @@ -350,7 +374,7 @@ return view.extend({ const toolbar = E('div', { 'class': 'dd-log-toolbar' }, toolbarItems); return E('div', { 'class': 'dd-log-wrap' }, [ - E('style', {}, CSS), + E('style', {}, CSS + styles.CSS), E('div', { 'class': 'dd-log-card' }, [ E('h4', { 'class': 'dd-log-card-title' }, _('%s Realtime Log').format(ctx.name)), toolbar, diff --git a/luci-app-daede/htdocs/luci-static/resources/view/daede/styles.js b/luci-app-daede/htdocs/luci-static/resources/view/daede/styles.js index 954bde6c..2525f899 100644 --- a/luci-app-daede/htdocs/luci-static/resources/view/daede/styles.js +++ b/luci-app-daede/htdocs/luci-static/resources/view/daede/styles.js @@ -4,6 +4,9 @@ 'require baseclass'; const CSS = [ + '.dd-log-toolbar .dd-log-toggle[aria-pressed="true"]{background:rgba(74,160,101,.16);border-color:#4aa065;box-shadow:inset 0 0 0 1px rgba(74,160,101,.2)}', + '.dd-log-toolbar .dd-log-toggle:focus-visible{outline:2px solid #4aa065;outline-offset:2px}', + '.dd-wrap{padding:4px 0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif}', '.dd-card{border:1px solid rgba(0,0,0,.06);border-radius:10px;padding:9px 14px;margin-bottom:7px;box-shadow:0 2px 8px rgba(0,0,0,.03);background:rgba(255,255,255,.02)}', /* padding:0 neutralizes Argon's h4{padding:.75rem 1.25rem}, which otherwise diff --git a/luci-app-daede/htdocs/luci-static/resources/view/daede/updates.js b/luci-app-daede/htdocs/luci-static/resources/view/daede/updates.js index 92bf1bbf..20992cad 100644 --- a/luci-app-daede/htdocs/luci-static/resources/view/daede/updates.js +++ b/luci-app-daede/htdocs/luci-static/resources/view/daede/updates.js @@ -7,6 +7,7 @@ 'require ui'; 'require view'; 'require view.daede.backend as backend'; +'require view.daede.daed-session as daedSession'; const DATA_PATHS = { geoip: '/usr/share/v2ray/geoip.dat', @@ -53,6 +54,9 @@ const CSS = [ '.dd-up-btn:hover{background:rgba(128,128,128,.12)}', '.dd-up-btn:disabled{opacity:.45;cursor:not-allowed}', '.dd-up-btn-primary{border-color:#4aa065;color:#4aa065}', + '.dd-backup-buttons{display:flex;align-items:center;gap:8px}', + '.dd-backup-reset{border-color:rgba(217,109,109,.55);color:#d96d6d}', + '@media(max-width:640px){.dd-backup-row{grid-template-columns:24px minmax(0,1fr)}.dd-backup-row .dd-up-meta{grid-column:2;white-space:normal}.dd-backup-row>span:nth-child(4){display:none}.dd-backup-buttons{grid-column:2;flex-wrap:wrap}}', '.dd-geo-row{display:grid;grid-template-columns:96px 1fr;gap:10px;align-items:center;font-size:12px;padding:6px 0}', '.dd-geo-row label{opacity:.75;font-weight:600}', '.dd-geo-row input[type=text],.dd-geo-row select{font-size:12px;padding:4px 8px;border:1px solid rgba(128,128,128,.35);border-radius:5px;background:transparent;color:inherit;width:100%}', @@ -202,65 +206,126 @@ return view.extend({ return runJob('update-pkg.sh', pkg, btn, '/tmp/luci-app-daede.pkg-' + pkg + '.log'); }; - // === Config backup (export / import the whole daede config) === - const exportBtn = E('button', { 'class': 'dd-up-btn' }, _('Export')); - const importBtn = E('button', { 'class': 'dd-up-btn' }, _('Import')); + // Configuration operations share a backend lock and one UI busy state. + const backupScript = '/usr/share/luci-app-daede/config-backup.sh'; + const exportBtn = E('button', { 'class': 'dd-up-btn', 'type': 'button' }, _('Export')); + const importBtn = E('button', { 'class': 'dd-up-btn', 'type': 'button' }, _('Import')); + const resetBtn = E('button', { 'class': 'dd-up-btn dd-backup-reset', 'type': 'button' }, _('Restore Defaults')); const fileInput = E('input', { 'type': 'file', 'accept': '.tar.gz,.gz,application/gzip', 'style': 'display:none' }); - - exportBtn.addEventListener('click', function() { - const orig = exportBtn.textContent; - exportBtn.disabled = true; exportBtn.textContent = '...'; - fs.exec('/usr/share/luci-app-daede/config-backup.sh', ['export']).then(function(res) { - if (res.code !== 0 || !res.stdout) { - logPane.textContent = String(res.stderr || res.stdout || 'export failed').trim(); + let backupBusy = false; + const showBackupError = function(e) { + ui.addNotification(null, E('p', {}, e.message || String(e)), 'error'); + }; + const configOperation = function(button, operation) { + if (backupBusy) return Promise.resolve(); + backupBusy = true; + const label = button.textContent; + [exportBtn, importBtn, resetBtn].forEach(function(b) { b.disabled = true; }); + button.textContent = _('Working…'); + return uci.changes().then(function(changes) { + if (['dae', 'daed', 'daede'].some(function(name) { return changes && changes[name] && changes[name].length; })) + throw new Error(_('Apply or discard pending daede changes first.')); + return operation(); + }).catch(showBackupError).finally(function() { + backupBusy = false; + [exportBtn, importBtn, resetBtn].forEach(function(b) { b.disabled = false; }); + button.textContent = label; + fileInput.value = ''; + }); + }; + const confirmBackup = function(message) { + return new Promise(function(resolve) { + const answer = function(value) { ui.hideModal(); resolve(value); }; + ui.showModal(_('Confirm configuration operation'), [ + E('p', {}, message), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'cbi-button', 'click': function() { answer(false); } }, _('Cancel')), + ' ', + E('button', { 'class': 'cbi-button cbi-button-negative', 'click': function() { answer(true); } }, _('Continue')) + ]) + ]); + }); + }; + const execBackup = function(args) { + return fs.exec(backupScript, args).then(function(res) { + if (!res || res.code !== 0) + throw new Error(String(res && (res.stderr || res.stdout) || _('Configuration operation failed.')).trim()); + return res; + }); + }; + const waitBackup = function() { + let tries = 0; + const check = function() { + return fs.read_direct('/tmp/luci-app-daede.backup.log', 'text').then(function(content) { + logPane.textContent = content; logPane.classList.add('show'); - return; - } - let bin; - try { - bin = atob(res.stdout.trim()); - } catch (e) { - logPane.textContent = _('Export failed: invalid base64 data from server'); - logPane.classList.add('show'); - return; - } - if (!bin || bin.length === 0) { - logPane.textContent = _('Export failed: empty archive'); - logPane.classList.add('show'); - return; - } - const arr = new Uint8Array(bin.length); - for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i); - const url = URL.createObjectURL(new Blob([arr], { 'type': 'application/gzip' })); - const a = E('a', { 'href': url, 'download': 'daede-config-' + stamp() + '.tar.gz' }); - document.body.appendChild(a); a.click(); document.body.removeChild(a); - setTimeout(function() { URL.revokeObjectURL(url); }, 1000); - }).catch(function(e) { - logPane.textContent = _('Export failed') + ': ' + (e ? (e.message || String(e)) : _('script not found')); - logPane.classList.add('show'); - }).finally(function() { - exportBtn.disabled = false; exportBtn.textContent = orig; + if (/^✗/m.test(content)) throw new Error(_('Configuration operation failed. See the log below.')); + if (/^✓/m.test(content)) return; + if (++tries > 90) throw new Error(_('Operation is still running. Check the log before retrying.')); + return new Promise(function(resolve) { setTimeout(resolve, 2000); }).then(check); + }); + }; + return check(); + }; + const reloadConfig = function(message) { + daedSession.clear(window.localStorage); + ['dae', 'daed', 'daede'].forEach(function(name) { uci.unload(name); }); + ui.showModal(_('Configuration restored'), [ + E('p', {}, message), + E('div', { 'class': 'right' }, E('button', { + 'class': 'cbi-button cbi-button-positive', 'click': function() { window.location.reload(); } + }, _('Reload Page'))) + ]); + }; + exportBtn.addEventListener('click', async function() { + if (!await confirmBackup(_('Export briefly stops running dae/daed services to back up the database safely, then resumes them. Continue?'))) return; + configOperation(exportBtn, function() { + return execBackup(['export']).then(function(res) { + const bin = atob((res.stdout || '').trim()); + if (!bin) throw new Error(_('Export failed: empty archive')); + const bytes = Uint8Array.from(bin, function(c) { return c.charCodeAt(0); }); + const url = URL.createObjectURL(new Blob([bytes], { 'type': 'application/gzip' })); + const a = E('a', { 'href': url, 'download': 'daede-config-' + stamp() + '.tar.gz' }); + document.body.appendChild(a); a.click(); a.remove(); + setTimeout(function() { URL.revokeObjectURL(url); }, 1000); + }); }); }); - importBtn.addEventListener('click', function() { fileInput.click(); }); - fileInput.addEventListener('change', function(ev) { + fileInput.addEventListener('change', async function(ev) { const file = ev.target.files && ev.target.files[0]; if (!file) return; - if (!confirm(_('Import overwrites the current daede config and restarts the backend. Continue?'))) { + if (file.size > 184320) { + showBackupError(new Error(_('Backup exceeds the 180 KiB limit. Use System Backup for larger files.'))); fileInput.value = ''; return; } - const reader = new FileReader(); - reader.onload = function(e) { - const b64 = String(e.target.result).split(',')[1] || ''; - fs.write('/tmp/daede-import.b64', b64).then(function() { - return runJob('config-backup.sh', 'import', importBtn, '/tmp/luci-app-daede.backup.log'); - }).catch(function(e) { - logPane.textContent = _('Import failed') + ': ' + (e ? (e.message || String(e)) : _('unknown error')); - logPane.classList.add('show'); - }).finally(function() { fileInput.value = ''; }); - }; - reader.readAsDataURL(file); + if (!await confirmBackup(_('Import replaces daede settings, nodes, subscriptions and dashboard data. Services stop first; the saved active backend starts only if enabled in the backup. Continue?'))) { + fileInput.value = ''; return; + } + configOperation(importBtn, function() { + return new Promise(function(resolve, reject) { + const reader = new FileReader(); + reader.onerror = function() { reject(new Error(_('Unable to read backup file.'))); }; + reader.onload = function() { resolve(String(reader.result).split(',')[1] || ''); }; + reader.readAsDataURL(file); + }).then(function(b64) { + // Per-tab uploads cannot overwrite each other before the backend locks. + const token = Array.from(window.crypto.getRandomValues(new Uint8Array(16)), function(n) { return n.toString(16).padStart(2, '0'); }).join(''); + const upload = '/tmp/daede-import.' + token + '.b64'; + return fs.write(upload, b64).then(function() { return execBackup(['import', upload]); }) + .finally(function() { return fs.remove(upload).catch(function() {}); }); + }).then(waitBackup).then(function() { + reloadConfig(_('Backup restored. The active backend follows the saved enabled setting.')); + }); + }); + }); + resetBtn.addEventListener('click', async function() { + if (!await confirmBackup(_('Restore ALL daede defaults? This clears dae/daed settings, nodes, subscriptions and daed dashboard accounts/data. Export a backup first if needed. Both backends will remain disabled. Installed programs and GeoData are kept.'))) return; + configOperation(resetBtn, function() { + return execBackup(['reset']).then(waitBackup).then(function() { + reloadConfig(_('All defaults restored. Both backends are disabled. Configure your nodes and routing before starting manually.')); + }); + }); }); const refresh = function() { @@ -508,12 +573,15 @@ return view.extend({ ]), E('div', { 'class': 'dd-card' }, [ E('h4', { 'class': 'dd-card-title' }, _('Config Backup')), - E('div', { 'class': 'dd-up-row' }, [ - E('span', { 'class': 'dd-up-icon dd-up-new' }, '⤓'), + E('div', { 'class': 'dd-up-row dd-backup-row' }, [ + E('span', { 'class': 'dd-up-icon dd-up-new' }, '⇩'), E('span', { 'class': 'dd-up-name' }, _('dae + daed')), - E('span', { 'class': 'dd-up-meta' }, _('Back up / restore the whole daede config (kernels excluded)')), - exportBtn, - importBtn + E('span', { + 'class': 'dd-up-meta', + 'title': _('Backups contain account and subscription credentials. Store them securely.') + }, _('Back up / restore the whole daede config (kernels excluded)')), + E('span', {}, ''), + E('div', { 'class': 'dd-backup-buttons' }, [exportBtn, importBtn, resetBtn]) ]), fileInput ]), diff --git a/luci-app-daede/po/zh-cn/daede.po b/luci-app-daede/po/zh-cn/daede.po index 0d957a44..67cb0230 100644 --- a/luci-app-daede/po/zh-cn/daede.po +++ b/luci-app-daede/po/zh-cn/daede.po @@ -1025,6 +1025,12 @@ msgstr "导出" msgid "Import" msgstr "导入" +msgid "Restore Defaults" +msgstr "恢复默认" + +msgid "Backups contain account and subscription credentials. Store them securely." +msgstr "备份中包含账号和订阅凭据,请妥善保管。" + msgid "Back up / restore the whole daede config (kernels excluded)" msgstr "备份 / 还原整个 daede 配置(不含内核)" @@ -1086,3 +1092,75 @@ msgid "" "Uplink. auto = detect by default route; on legacy swconfig or multi-WAN set " "it, e.g. eth0.2." msgstr "上行 WAN 接口。auto 按默认路由自动检测;老式 swconfig 交换机或多 WAN 时手动指定,例如 eth0.2。" + +msgid "Export Backup" +msgstr "导出备份" + +msgid "Import Backup" +msgstr "导入还原" + +msgid "Restore All Defaults" +msgstr "恢复全部默认配置" + +msgid "Working…" +msgstr "正在处理…" + +msgid "Apply or discard pending daede changes first." +msgstr "请先应用或放弃尚未保存的 daede 修改。" + +msgid "Configuration operation failed." +msgstr "配置操作失败。" + +msgid "Configuration operation failed. See the log below." +msgstr "配置操作失败,请查看下方日志。" + +msgid "Operation is still running. Check the log before retrying." +msgstr "操作仍在进行,请检查日志后再重试。" + +msgid "Configuration restored" +msgstr "配置已恢复" + +msgid "Reload Page" +msgstr "刷新页面" + +msgid "Export briefly stops running dae/daed services to back up the database safely, then resumes them. Continue?" +msgstr "导出时会短暂停止正在运行的 dae/daed,以保证数据库备份完整,完成后恢复运行。是否继续?" + +msgid "Backup exceeds the 180 KiB limit. Use System Backup for larger files." +msgstr "备份文件超过 180 KiB 限制,请使用系统备份功能处理较大的文件。" + +msgid "Import replaces daede settings, nodes, subscriptions and dashboard data. Services stop first; the saved active backend starts only if enabled in the backup. Continue?" +msgstr "导入将覆盖 daede 设置、节点、订阅和面板数据。服务会先停止,仅在备份中为启用状态时启动对应后端。是否继续?" + +msgid "Unable to read backup file." +msgstr "无法读取备份文件。" + +msgid "Backup restored. The active backend follows the saved enabled setting." +msgstr "备份已还原,当前后端按备份中的启用状态运行。" + +msgid "Restore ALL daede defaults? This clears dae/daed settings, nodes, subscriptions and daed dashboard accounts/data. Export a backup first if needed. Both backends will remain disabled. Installed programs and GeoData are kept." +msgstr "确定恢复 daede 全部默认配置?此操作会清空 dae/daed 设置、节点、订阅以及 daed 面板账号和数据。需要保留时请先导出备份。完成后两个后端均保持关闭,已安装程序和 GeoData 数据文件保留。" + +msgid "All defaults restored. Both backends are disabled. Configure your nodes and routing before starting manually." +msgstr "已恢复全部默认配置,两个后端均已关闭。请重新配置节点和路由后手动开启。" + +msgid "Back up settings, nodes, subscriptions and dashboard data. Backups contain credentials; store them securely. Kernels and GeoData are excluded." +msgstr "备份包括设置、节点、订阅和面板数据,不包括内核和 GeoData。备份中含有账号和订阅凭据,请妥善保管。" + +msgid "Auto-scroll: On" +msgstr "自动滚动:开启" + +msgid "Auto-scroll: Off" +msgstr "自动滚动:关闭" + +msgid "Resume" +msgstr "继续" + +msgid "Paused" +msgstr "已暂停" + +msgid "Confirm configuration operation" +msgstr "确认配置操作" + +msgid "Continue" +msgstr "继续" diff --git a/luci-app-daede/po/zh_Hans/daede.po b/luci-app-daede/po/zh_Hans/daede.po index 0d957a44..67cb0230 100644 --- a/luci-app-daede/po/zh_Hans/daede.po +++ b/luci-app-daede/po/zh_Hans/daede.po @@ -1025,6 +1025,12 @@ msgstr "导出" msgid "Import" msgstr "导入" +msgid "Restore Defaults" +msgstr "恢复默认" + +msgid "Backups contain account and subscription credentials. Store them securely." +msgstr "备份中包含账号和订阅凭据,请妥善保管。" + msgid "Back up / restore the whole daede config (kernels excluded)" msgstr "备份 / 还原整个 daede 配置(不含内核)" @@ -1086,3 +1092,75 @@ msgid "" "Uplink. auto = detect by default route; on legacy swconfig or multi-WAN set " "it, e.g. eth0.2." msgstr "上行 WAN 接口。auto 按默认路由自动检测;老式 swconfig 交换机或多 WAN 时手动指定,例如 eth0.2。" + +msgid "Export Backup" +msgstr "导出备份" + +msgid "Import Backup" +msgstr "导入还原" + +msgid "Restore All Defaults" +msgstr "恢复全部默认配置" + +msgid "Working…" +msgstr "正在处理…" + +msgid "Apply or discard pending daede changes first." +msgstr "请先应用或放弃尚未保存的 daede 修改。" + +msgid "Configuration operation failed." +msgstr "配置操作失败。" + +msgid "Configuration operation failed. See the log below." +msgstr "配置操作失败,请查看下方日志。" + +msgid "Operation is still running. Check the log before retrying." +msgstr "操作仍在进行,请检查日志后再重试。" + +msgid "Configuration restored" +msgstr "配置已恢复" + +msgid "Reload Page" +msgstr "刷新页面" + +msgid "Export briefly stops running dae/daed services to back up the database safely, then resumes them. Continue?" +msgstr "导出时会短暂停止正在运行的 dae/daed,以保证数据库备份完整,完成后恢复运行。是否继续?" + +msgid "Backup exceeds the 180 KiB limit. Use System Backup for larger files." +msgstr "备份文件超过 180 KiB 限制,请使用系统备份功能处理较大的文件。" + +msgid "Import replaces daede settings, nodes, subscriptions and dashboard data. Services stop first; the saved active backend starts only if enabled in the backup. Continue?" +msgstr "导入将覆盖 daede 设置、节点、订阅和面板数据。服务会先停止,仅在备份中为启用状态时启动对应后端。是否继续?" + +msgid "Unable to read backup file." +msgstr "无法读取备份文件。" + +msgid "Backup restored. The active backend follows the saved enabled setting." +msgstr "备份已还原,当前后端按备份中的启用状态运行。" + +msgid "Restore ALL daede defaults? This clears dae/daed settings, nodes, subscriptions and daed dashboard accounts/data. Export a backup first if needed. Both backends will remain disabled. Installed programs and GeoData are kept." +msgstr "确定恢复 daede 全部默认配置?此操作会清空 dae/daed 设置、节点、订阅以及 daed 面板账号和数据。需要保留时请先导出备份。完成后两个后端均保持关闭,已安装程序和 GeoData 数据文件保留。" + +msgid "All defaults restored. Both backends are disabled. Configure your nodes and routing before starting manually." +msgstr "已恢复全部默认配置,两个后端均已关闭。请重新配置节点和路由后手动开启。" + +msgid "Back up settings, nodes, subscriptions and dashboard data. Backups contain credentials; store them securely. Kernels and GeoData are excluded." +msgstr "备份包括设置、节点、订阅和面板数据,不包括内核和 GeoData。备份中含有账号和订阅凭据,请妥善保管。" + +msgid "Auto-scroll: On" +msgstr "自动滚动:开启" + +msgid "Auto-scroll: Off" +msgstr "自动滚动:关闭" + +msgid "Resume" +msgstr "继续" + +msgid "Paused" +msgstr "已暂停" + +msgid "Confirm configuration operation" +msgstr "确认配置操作" + +msgid "Continue" +msgstr "继续" diff --git a/luci-app-daede/root/etc/uci-defaults/90-luci-app-daede-init b/luci-app-daede/root/etc/uci-defaults/90-luci-app-daede-init index 0b61fe2b..94d1f9bc 100644 --- a/luci-app-daede/root/etc/uci-defaults/90-luci-app-daede-init +++ b/luci-app-daede/root/etc/uci-defaults/90-luci-app-daede-init @@ -79,27 +79,7 @@ fi # Idempotent: only add what's missing. Lives in the `dae` UCI package, # consumed by gen-dae-config.sh. if [ -f /etc/config/dae ]; then - if ! uci -q get dae.config.lan_interface >/dev/null 2>&1; then - uci -q set dae.config.lan_interface='br-lan' - fi - if ! uci -q show dae | grep -q "=group$"; then - g="$(uci add dae group)" - uci -q set "dae.$g.name=proxy" - uci -q set "dae.$g.policy=min_moving_avg" - fi - if ! uci -q get dae.routing >/dev/null 2>&1; then - uci -q set dae.routing=routing - uci -q set dae.routing.private_direct=1 - uci -q set dae.routing.cn_direct=1 - uci -q set dae.routing.block_ads=0 - uci -q set dae.routing.fallback=proxy - fi - if ! uci -q get dae.dns >/dev/null 2>&1; then - uci -q set dae.dns=dns - uci -q set dae.dns.cn_upstream='udp://dns.alidns.com:53' - uci -q set dae.dns.fallback_upstream='tcp+udp://dns.google:53' - uci -q set dae.dns.response_ttl='0' - fi + /usr/share/luci-app-daede/config-defaults.sh # Migrate legacy group filters to the unified source / name_filter fields, # then drop the old keys so the form shows real, editable values (the old # fields used to re-populate the new ones and looked un-deletable). diff --git a/luci-app-daede/root/usr/share/luci-app-daede/config-backup.sh b/luci-app-daede/root/usr/share/luci-app-daede/config-backup.sh index 59994209..c76838ad 100755 --- a/luci-app-daede/root/usr/share/luci-app-daede/config-backup.sh +++ b/luci-app-daede/root/usr/share/luci-app-daede/config-backup.sh @@ -1,71 +1,250 @@ #!/bin/sh -# config-backup.sh export|import — back up/restore the whole daede config -# (dae + daed + active backend). export: prints base64(tar.gz) to stdout. -# import: decodes /tmp/daede-import.b64, validates, restores, restarts backend. +# Whole-plugin backup/import/reset. Only fixed configuration paths are managed. set -eu +umask 077 ACTION="${1:-}" -LOG="/tmp/luci-app-daede.backup.log" -IMPORT_B64="/tmp/daede-import.b64" -MAX_TAR=184320 # 180 KiB tar.gz -> ~240 KiB base64, under the ubus limit - -# relative paths (no leading /) so tar entries match the extraction whitelist -WHITELIST="etc/config/dae etc/config/daed etc/config/daede etc/dae/config.dae etc/daed/wing.db" +UPLOAD="${2:-/tmp/daede-import.b64}" +LOG=/tmp/luci-app-daede.backup.log +LOCK=/tmp/luci-app-daede.config.lock +SHARE=/usr/share/luci-app-daede +MAX_TAR=184320 # Base64 must fit inside the ubus reply (~240 KiB). +MAX_RAW=16777216 +FILES="etc/config/dae etc/config/daed etc/config/daede etc/dae/config.dae etc/daed/wing.db etc/daed/wing.db-wal etc/daed/wing.db-shm" +WORK="" +SNAPSHOT=0 +MUTATING=0 +KEEP=0 +RESTART="" +fail() { echo "$*" >&2; exit 1; } b64enc() { ucode -e 'let f=require("fs"); print(b64enc(f.open(ARGV[0],"r").read("all")));' -- "$1"; } -b64dec() { ucode -e 'let f=require("fs"); let r=b64dec(trim(f.open(ARGV[0],"r").read("all"))); if(!r)exit(1); let o=f.open(ARGV[1],"w"); o.write(r); o.close();' -- "$1" "$2"; } +b64dec() { ucode -e 'let f=require("fs"); let r=b64dec(trim(f.open(ARGV[0],"r").read("all"))); if(!r)exit(1); let o=f.open(ARGV[1],"w"); if(!o || o.write(r)!=length(r))exit(1); o.close();' -- "$1" "$2"; } +# Do not merge pending CLI UCI deltas into snapshots/defaults. +config() { /sbin/uci -c /etc/config -t "$WORK/uci" "$@"; } -case "$ACTION" in -export) - exist="" - for p in $WHITELIST; do [ -e "/$p" ] && exist="$exist $p"; done - [ -n "$exist" ] || { echo "no config to back up" >&2; exit 1; } - tmp="$(mktemp)" - ( cd / && tar -czf "$tmp" $exist ) 2>/dev/null || { rm -f "$tmp"; echo "tar failed" >&2; exit 1; } - if [ "$(wc -c < "$tmp")" -gt "$MAX_TAR" ]; then - rm -f "$tmp"; echo "config too large; use System Backup instead" >&2; exit 2 - fi - b64enc "$tmp" - rm -f "$tmp" - ;; - -import) - [ -f "$IMPORT_B64" ] || { echo "no upload found" >&2; exit 1; } - ( - exec >"$LOG" 2>&1 - echo "$(date '+%F %T') begin import" - rc=0 - tmp="$(mktemp)" - if ! b64dec "$IMPORT_B64" "$tmp"; then - echo "decode failed"; rc=1 - elif ! gzip -t "$tmp" 2>/dev/null; then - echo "not a valid backup archive"; rc=1 - else - # reject any entry outside the whitelist (path-traversal guard) - bad="$(tar -tzf "$tmp" 2>/dev/null | grep -vxE 'etc/config/dae|etc/config/daed|etc/config/daede|etc/dae/config\.dae|etc/daed/wing\.db' | head -1)" - if [ -n "$bad" ]; then - echo "rejected: unexpected entry '$bad'"; rc=1 - else - ab="$(uci -q get daede.config.active_backend || echo dae)" - [ -x "/etc/init.d/$ab" ] && /etc/init.d/"$ab" stop 2>/dev/null || true - if ( cd / && tar -xzf "$tmp" ); then - echo "restored config" - else - echo "extract failed"; rc=1 - fi - ab="$(uci -q get daede.config.active_backend || echo "$ab")" - [ -x "/etc/init.d/$ab" ] && { /etc/init.d/"$ab" enabled || /etc/init.d/"$ab" enable; /etc/init.d/"$ab" restart 2>/dev/null || /etc/init.d/"$ab" start 2>/dev/null; } || true - fi +paths() { + printf '%s\n' $FILES + for sub in "$1"/etc/dae/subscriptions/*.sub; do + [ -e "$sub" ] || [ -L "$sub" ] || continue + name="${sub##*/}" + case "${name%.sub}" in ''|*[!A-Za-z0-9_]*) fail 'unexpected subscription filename' ;; esac + printf 'etc/dae/subscriptions/%s\n' "$name" + done +} +check_paths() { + for dir in /etc/config /etc/dae /etc/daed /etc/dae/subscriptions; do + [ ! -L "$dir" ] || fail "refusing symlink: $dir" + done + paths / > "$WORK/paths" + while IFS= read -r p; do + [ ! -L "/$p" ] || fail "refusing symlink: /$p" + [ ! -e "/$p" ] || [ -f "/$p" ] || fail "not a regular file: /$p" + done < "$WORK/paths" +} +snapshot() { + mkdir -p "$WORK/before" + while IFS= read -r p; do + [ -f "/$p" ] || continue + mkdir -p "$WORK/before/${p%/*}" + cp -p "/$p" "$WORK/before/$p" + done < "$WORK/paths" + SNAPSHOT=1 +} +stop_backends() { + for svc in dae daed; do + [ -x "/etc/init.d/$svc" ] || continue + if /etc/init.d/"$svc" running >/dev/null 2>&1; then RESTART="$RESTART $svc"; fi + /etc/init.d/"$svc" stop >/dev/null 2>&1 || fail "failed to stop $svc; configuration unchanged" + done + # procd termination can be asynchronous. Never copy a live SQLite database. + for attempt in 1 2 3 4 5; do + if ! pidof dae daed daed-guard >/dev/null 2>&1; then return; fi + sleep 1 + done + fail 'backend still running; configuration unchanged' +} +disable_backends() { + for svc in dae daed; do + if [ -f "/etc/config/$svc" ]; then + config -q set "$svc.config=$svc" || return 1 + config -q set "$svc.config.enabled=0" || return 1 + config -q commit "$svc" || return 1 fi - rm -f "$tmp" "$IMPORT_B64" - if [ "$rc" = 0 ]; then echo "result: config restored, backend restarted"; else echo "result: import failed"; fi - if [ "$rc" = 0 ]; then echo "$(date '+%F %T') ✓ 完成"; else echo "$(date '+%F %T') ✗ 失败 (rc=$rc)"; fi - ) /dev/null 2>&1 & - echo "started in background, see $LOG" - ;; + if [ -x "/etc/init.d/$svc" ]; then + /etc/init.d/"$svc" disable >/dev/null 2>&1 || return 1 + /etc/init.d/"$svc" stop >/dev/null 2>&1 || return 1 + fi + done +} +restore_snapshot() { + # Remove files created by the failed operation as well as original files. + paths / > "$WORK/current" || return 1 + while IFS= read -r p; do rm -f "/$p" || return 1; done < "$WORK/current" + while IFS= read -r p; do + [ -f "$WORK/before/$p" ] || continue + mkdir -p "/${p%/*}" || return 1 + cp -p "$WORK/before/$p" "/$p" || return 1 + done < "$WORK/paths" +} +sync_cron() { + geo=disable; sub=disable + [ "$(config -q get daede.config.geo_auto || :)" != 1 ] || geo=enable + [ "$(config -q get daed.config.subscribe_auto_update || :)" != 1 ] || sub=enable + "$SHARE/geo-cron.sh" "$geo" >/dev/null 2>&1 + "$SHARE/daed-sub-cron.sh" "$sub" >/dev/null 2>&1 +} +finish() { + rc=$? + trap - EXIT HUP INT TERM + if [ "$rc" -ne 0 ] && [ "$MUTATING" = 1 ]; then + disable_backends || true + if [ "$SNAPSHOT" = 1 ] && ! restore_snapshot; then + KEEP=1 + echo "rollback failed; root-only recovery files retained at $WORK" >&2 + else + echo 'previous configuration restored; backends remain disabled' >&2 + fi + disable_backends || { KEEP=1; echo 'failed to disable backend; check service state' >&2; } + sync_cron || true + fi + if [ "$ACTION" = export ]; then + for svc in $RESTART; do + /etc/init.d/"$svc" start >/dev/null 2>&1 || { rc=1; echo "failed to resume $svc" >&2; } + done + fi + if [ "$ACTION" != export ]; then + if [ "$rc" = 0 ]; then echo '✓ 完成'; else echo '✗ 失败'; fi + fi + [ "$KEEP" = 1 ] || rm -rf "$WORK" + rmdir "$LOCK" + exit "$rc" +} +validate_archive() { + [ "$(wc -c < "$WORK/upload.b64")" -le 245760 ] || fail 'upload too large' + b64dec "$WORK/upload.b64" "$WORK/import.gz" || fail 'invalid base64 upload' + [ "$(wc -c < "$WORK/import.gz")" -le "$MAX_TAR" ] || fail 'archive too large' + gzip -t "$WORK/import.gz" 2>/dev/null || fail 'invalid gzip archive' + gzip -dc "$WORK/import.gz" | head -c 16777217 > "$WORK/import.tar" + [ "$(wc -c < "$WORK/import.tar")" -le "$MAX_RAW" ] || fail 'unpacked backup exceeds 16 MiB' + tar -tf "$WORK/import.tar" > "$WORK/entries" 2>/dev/null || fail 'invalid tar archive' + [ -s "$WORK/entries" ] || fail 'empty archive' + # Reject links, directories, devices and duplicate entries before extraction. + tar -tvf "$WORK/import.tar" > "$WORK/types" 2>/dev/null || fail 'invalid tar headers' + if grep -qv '^-' "$WORK/types"; then fail 'only regular files are allowed'; fi + [ -z "$(sort "$WORK/entries" | uniq -d)" ] || fail 'duplicate archive entry' + while IFS= read -r p; do + case "$p" in + etc/config/dae|etc/config/daed|etc/config/daede|etc/dae/config.dae|etc/daed/wing.db|etc/daed/wing.db-wal|etc/daed/wing.db-shm) ;; + etc/dae/subscriptions/*.sub) + name="${p#etc/dae/subscriptions/}" + case "${name%.sub}" in ''|*[!A-Za-z0-9_]*) fail 'invalid subscription path' ;; esac ;; + *) fail 'unexpected archive entry' ;; + esac + done < "$WORK/entries" + mkdir -p "$WORK/after" + tar -xf "$WORK/import.tar" -C "$WORK/after" + found=0 + for svc in dae daed daede; do + [ -f "$WORK/after/etc/config/$svc" ] || continue + found=1 + /sbin/uci -c "$WORK/after/etc/config" -t "$WORK/uci" -q export "$svc" >/dev/null || fail "invalid $svc configuration" + done + [ "$found" = 1 ] || fail 'backup has no UCI configuration' + if [ -f "$WORK/after/etc/daed/wing.db-wal" ]; then + [ -f "$WORK/after/etc/daed/wing.db" ] || fail 'database WAL has no database' + fi + ab="$(/sbin/uci -c "$WORK/after/etc/config" -t "$WORK/uci" -q get daede.config.active_backend || echo dae)" + case "$ab" in dae|daed) ;; *) fail 'invalid backend in backup' ;; esac + [ -x "/etc/init.d/$ab" ] || fail "backup backend $ab is not installed" +} +run_action() { + trap finish EXIT + trap 'exit 1' HUP INT TERM + check_paths + if [ "$ACTION" = import ]; then validate_archive; fi + if [ "$ACTION" = reset ]; then + mkdir -p "$WORK/after/etc/config" + for svc in dae daed daede; do + cp "$SHARE/defaults/$svc" "$WORK/after/etc/config/$svc" + done + "$SHARE/config-defaults.sh" "$WORK/after/etc/config" + if [ ! -x /etc/init.d/daed ]; then + /sbin/uci -c "$WORK/after/etc/config" -t "$WORK/uci" set daede.config.active_backend=dae + /sbin/uci -c "$WORK/after/etc/config" -t "$WORK/uci" commit daede + fi + fi + stop_backends + snapshot + if [ "$ACTION" = export ]; then + paths "$WORK/before" > "$WORK/candidates" + : > "$WORK/entries" + while IFS= read -r p; do [ ! -f "$WORK/before/$p" ] || echo "$p" >> "$WORK/entries"; done < "$WORK/candidates" + [ -s "$WORK/entries" ] || fail 'no configuration to export' + tar -czf "$WORK/export.gz" -C "$WORK/before" -T "$WORK/entries" + [ "$(wc -c < "$WORK/export.gz")" -le "$MAX_TAR" ] || fail 'config too large; use System Backup instead' + b64enc "$WORK/export.gz" + return + fi + MUTATING=1 + disable_backends + # Old backups may omit an uninstalled backend. Keep its UCI file, but remove + # all managed data first so stale WAL/SHM and local subscriptions cannot leak in. + while IFS= read -r p; do + case "$p" in etc/config/*) continue ;; esac + rm -f "/$p" + done < "$WORK/paths" + paths "$WORK/after" > "$WORK/replacement" + while IFS= read -r p; do + [ -f "$WORK/after/$p" ] || continue + mkdir -p "/${p%/*}" + cp "$WORK/after/$p" "/$p" + chmod 600 "/$p" + done < "$WORK/replacement" + if [ "$ACTION" = reset ]; then + disable_backends + sync_cron + echo 'All defaults restored. Backends are disabled; configure before starting manually.' + else + # Preserve the imported active backend's enabled flag. The other backend + # stays disabled even when the archive was made on a dual-backend router. + for svc in dae daed; do + [ "$svc" = "$ab" ] || [ ! -f "/etc/config/$svc" ] || { config -q set "$svc.config.enabled=0"; config -q commit "$svc"; } + done + sync_cron + if [ "$(config -q get "$ab.config.enabled" || :)" = 1 ]; then + /etc/init.d/"$ab" enable + /etc/init.d/"$ab" start + /etc/init.d/"$ab" running >/dev/null 2>&1 || fail 'restored backend failed to start' + else + /etc/init.d/"$ab" disable + fi + echo 'Backup restored. Active backend follows the saved enabled setting.' + fi +} -*) - echo "usage: $0 export|import" >&2 - exit 64 - ;; -esac +case "$ACTION" in export|import|reset) ;; *) fail "usage: $0 export|import [upload]|reset" ;; esac +# An atomic lock covers the complete operation, including background execution. +mkdir "$LOCK" 2>/dev/null || fail 'another configuration operation is running' +WORK="$(mktemp -d /tmp/daede-config.XXXXXX)" || { rmdir "$LOCK"; exit 1; } +mkdir -p "$WORK/uci" +if [ "$ACTION" = import ]; then + case "$UPLOAD" in + /tmp/daede-import.b64) ;; + /tmp/daede-import.*.b64) + token="${UPLOAD#/tmp/daede-import.}"; token="${token%.b64}" + case "$token" in ''|*[!a-f0-9]*) rm -rf "$WORK"; rmdir "$LOCK"; fail 'invalid upload path' ;; esac ;; + *) rm -rf "$WORK"; rmdir "$LOCK"; fail 'invalid upload path' ;; + esac + if [ ! -f "$UPLOAD" ] || [ -L "$UPLOAD" ]; then rm -rf "$WORK"; rmdir "$LOCK"; fail 'upload missing or unsafe'; fi + mv "$UPLOAD" "$WORK/upload.b64" || { rm -rf "$WORK"; rmdir "$LOCK"; exit 1; } +fi +if [ "$ACTION" = export ]; then + run_action +else + # Truncate before returning, so the frontend cannot mistake an earlier job + # for this one. The child owns the lock and all cleanup from here on. + : > "$LOG" + (run_action) "$LOG" 2>&1 & + echo "started in background, see $LOG" +fi diff --git a/luci-app-daede/root/usr/share/luci-app-daede/config-defaults.sh b/luci-app-daede/root/usr/share/luci-app-daede/config-defaults.sh new file mode 100755 index 00000000..6c4c9df2 --- /dev/null +++ b/luci-app-daede/root/usr/share/luci-app-daede/config-defaults.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# Shared installation/reset defaults. Optional directory is an isolated UCI tree. +set -eu +if [ "$#" -gt 0 ]; then + config_dir="$1" + mkdir -p "$config_dir/.uci" + uci() { /sbin/uci -c "$config_dir" -t "$config_dir/.uci" "$@"; } +fi + +if ! uci -q get dae.config.lan_interface >/dev/null 2>&1; then + uci -q set dae.config.lan_interface='br-lan' +fi +if ! uci -q show dae | grep -q "=group$"; then + g="$(uci add dae group)" + uci -q set "dae.$g.name=proxy" + uci -q set "dae.$g.policy=min_moving_avg" +fi +if ! uci -q get dae.routing >/dev/null 2>&1; then + uci -q set dae.routing=routing + uci -q set dae.routing.private_direct=1 + uci -q set dae.routing.cn_direct=1 + uci -q set dae.routing.block_ads=0 + uci -q set dae.routing.fallback=proxy +fi +if ! uci -q get dae.dns >/dev/null 2>&1; then + uci -q set dae.dns=dns + uci -q set dae.dns.cn_upstream='udp://dns.alidns.com:53' + uci -q set dae.dns.fallback_upstream='tcp+udp://dns.google:53' + uci -q set dae.dns.response_ttl='0' +fi +uci -q commit dae diff --git a/luci-app-daede/root/usr/share/rpcd/acl.d/luci-app-daede.json b/luci-app-daede/root/usr/share/rpcd/acl.d/luci-app-daede.json index 1dfe53a3..7ad9aa86 100644 --- a/luci-app-daede/root/usr/share/rpcd/acl.d/luci-app-daede.json +++ b/luci-app-daede/root/usr/share/rpcd/acl.d/luci-app-daede.json @@ -30,8 +30,7 @@ "/usr/share/luci-app-daede/pkg-info.sh daed": [ "exec" ], "/usr/share/luci-app-daede/pkg-info.sh luci-app-daede": [ "exec" ], "/usr/share/luci-app-daede/refresh-index.sh": [ "exec" ], - "/usr/share/luci-app-daede/proxy-check.sh": [ "exec" ], - "/usr/share/luci-app-daede/config-backup.sh export": [ "exec" ] + "/usr/share/luci-app-daede/proxy-check.sh": [ "exec" ] }, "ubus": { "service": [ "list" ] @@ -83,7 +82,11 @@ "/usr/share/luci-app-daede/gen-dae-config.sh import": [ "exec" ], "/usr/share/luci-app-daede/daed-sub-update.sh": [ "exec" ], "/usr/share/luci-app-daede/daed-sub-cron.sh enable": [ "exec" ], - "/usr/share/luci-app-daede/daed-sub-cron.sh disable": [ "exec" ] + "/usr/share/luci-app-daede/daed-sub-cron.sh disable": [ "exec" ], + "/usr/share/luci-app-daede/config-backup.sh export": [ "exec" ], + "/usr/share/luci-app-daede/config-backup.sh reset": [ "exec" ], + "/usr/share/luci-app-daede/config-backup.sh import /tmp/daede-import.*.b64": [ "exec" ], + "/tmp/daede-import.*.b64": [ "write" ] }, "uci": [ "dae", "daed", "daede" ] } diff --git a/luci-app-filebrowser-q/Makefile b/luci-app-filebrowser-q/Makefile new file mode 100644 index 00000000..b719294c --- /dev/null +++ b/luci-app-filebrowser-q/Makefile @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: Apache-2.0 +# + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI app for FileBrowser Quantum +LUCI_DEPENDS:=+filebrowser-q + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-filebrowser-q/htdocs/luci-static/resources/view/filebrowser-q.js b/luci-app-filebrowser-q/htdocs/luci-static/resources/view/filebrowser-q.js new file mode 100644 index 00000000..aa126971 --- /dev/null +++ b/luci-app-filebrowser-q/htdocs/luci-static/resources/view/filebrowser-q.js @@ -0,0 +1,88 @@ +'use strict'; +'require form'; +'require poll'; +'require rpc'; +'require uci'; +'require view'; +'require fs'; + +function getServiceStatus() { + let isRunning = false; + return isRunning; +} + +function renderStatus(isRunning, port) { + let spanTemp = '%s %s'; + let renderHTML; + if (isRunning) { + let button = String.format(' %s', + window.location.hostname, port, _('Open Web Interface')); + renderHTML = spanTemp.format('green', _('FileBrowser'), _('RUNNING')) + button; + } else { + renderHTML = spanTemp.format('red', _('FileBrowser'), _('NOT RUNNING')); + } + + return renderHTML; +} + +return view.extend({ + load: async function () { + const promises = await Promise.all([ + L.resolveDefault(fs.stat('/var/run/filebrowser-q.pid'), null), + uci.load('filebrowser-q') + ]); + const data = { + isRunning: promises[0], + conf: promises[1] + }; + return data; + }, + + render(data) { + let m, s, o; + let webport = (uci.get(data.conf, 'config', 'listen_port') || '8989'); + + m = new form.Map('filebrowser-q', _('FileBrowser Quantum'), + _('The best free self-hosted web-based file manager.') + '
'+ + _('Default login username is %s and password is %s.').format('admin', 'admin')); + + s = m.section(form.TypedSection); + s.anonymous = true; + s.render = function() { + poll.add(function() { + return fs.stat('/var/run/filebrowser-q.pid').then(function(stat) { + let view = document.getElementById('service_status'); + if (view) { + view.innerHTML = renderStatus(stat, webport); + } + }).catch(function() { + let view = document.getElementById('service_status'); + if (view) { + view.innerHTML = renderStatus(null, webport); + } + }); + }); + + return E('div', { class: 'cbi-section', id: 'status_bar' }, [ + E('p', { id: 'service_status' }, _('Collecting data...')) + ]); +}; + + s = m.section(form.NamedSection, 'config', 'filebrowser'); + + o = s.option(form.Flag, 'enabled', _('Enable')); + o.default = o.disabled; + o.rmempty = false; + + o = s.option(form.Value, 'listen_port', _('Listen port')); + o.datatype = 'port'; + o.default = '8989'; + o.rmempty = false; + + o = s.option(form.Value, 'root_path', _('Root directory')); + o.default = '/'; + o.rmempty = false; + + return m.render(); + } +}); diff --git a/luci-app-filebrowser-q/po/zh-cn b/luci-app-filebrowser-q/po/zh-cn new file mode 120000 index 00000000..8d69574d --- /dev/null +++ b/luci-app-filebrowser-q/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-app-filebrowser-q/po/zh_Hans/filebrowser-q.po b/luci-app-filebrowser-q/po/zh_Hans/filebrowser-q.po new file mode 100644 index 00000000..0edd024e --- /dev/null +++ b/luci-app-filebrowser-q/po/zh_Hans/filebrowser-q.po @@ -0,0 +1,33 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" +"Project-Id-Version: PACKAGE VERSION\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: zh-Hans\n" +"MIME-Version: 1.0\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "Collecting data..." +msgstr "正在收集数据中..." + +msgid "Default login username is %s and password is %s." +msgstr "默认登录用户名为 %s,密码为 %s。" + +msgid "Enable" +msgstr "启用" + +msgid "Listen port" +msgstr "监听端口" + +msgid "NOT RUNNING" +msgstr "未运行" + +msgid "Open Web Interface" +msgstr "打开 Web 界面" + +msgid "RUNNING" +msgstr "运行中" + +msgid "Root directory" +msgstr "根目录" diff --git a/luci-app-filebrowser-q/root/usr/share/luci/menu.d/luci-app-filebrowser-q.json b/luci-app-filebrowser-q/root/usr/share/luci/menu.d/luci-app-filebrowser-q.json new file mode 100644 index 00000000..43da0114 --- /dev/null +++ b/luci-app-filebrowser-q/root/usr/share/luci/menu.d/luci-app-filebrowser-q.json @@ -0,0 +1,14 @@ +{ + "admin/services/filebrowser-q": { + "title": "FileBrowser Quantum", + "action": { + "order": 30, + "type": "view", + "path": "filebrowser-q" + }, + "depends": { + "acl": [ "luci-app-filebrowser-q" ], + "uci": { "filebrowser-q": true } + } + } +} diff --git a/luci-app-filebrowser-q/root/usr/share/rpcd/acl.d/luci-app-filebrowser-q.json b/luci-app-filebrowser-q/root/usr/share/rpcd/acl.d/luci-app-filebrowser-q.json new file mode 100644 index 00000000..ac1c1948 --- /dev/null +++ b/luci-app-filebrowser-q/root/usr/share/rpcd/acl.d/luci-app-filebrowser-q.json @@ -0,0 +1,14 @@ +{ + "luci-app-filebrowser-q": { + "description": "Grant UCI access for luci-app-filebrowser-q", + "read": { + "ubus": { + "service": [ "list" ] + }, + "uci": [ "filebrowser-q" ] + }, + "write": { + "uci": [ "filebrowser-q" ] + } + } +} diff --git a/luci-app-filebrowser-q/root/usr/share/ucitrack/luci-app-filebrowser-q.json b/luci-app-filebrowser-q/root/usr/share/ucitrack/luci-app-filebrowser-q.json new file mode 100644 index 00000000..819c0907 --- /dev/null +++ b/luci-app-filebrowser-q/root/usr/share/ucitrack/luci-app-filebrowser-q.json @@ -0,0 +1,4 @@ +{ + "config": "filebrowser-q", + "init": "filebrowser-q" +} \ No newline at end of file diff --git a/luci-app-filetransfer/luasrc/model/cbi/filetransfer.lua b/luci-app-filetransfer/luasrc/model/cbi/filetransfer.lua index ac8ff70a..5606fe5c 100644 --- a/luci-app-filetransfer/luasrc/model/cbi/filetransfer.lua +++ b/luci-app-filetransfer/luasrc/model/cbi/filetransfer.lua @@ -92,16 +92,17 @@ elseif luci.http.formvalue("download") then end local inits, attr = {} -for i, f in ipairs(fs.glob("/tmp/upload/*")) do +for f in fs.glob("/tmp/upload/*") do attr = fs.stat(f) if attr then - inits[i] = {} - inits[i].name = fs.basename(f) - inits[i].mtime = os.date("%Y-%m-%d %H:%M:%S", attr.mtime) - inits[i].modestr = attr.modestr - inits[i].size = tostring(attr.size) - inits[i].remove = 0 - inits[i].install = false + inits[#inits + 1] = { + name = fs.basename(f), + mtime = os.date("%Y-%m-%d %H:%M:%S", attr.mtime), + modestr = attr.modestr, + size = tostring(attr.size), + remove = 0, + install = false + } end end diff --git a/luci-app-fwlive/Makefile b/luci-app-fwlive/Makefile index 964fa0be..2c6d00bf 100644 --- a/luci-app-fwlive/Makefile +++ b/luci-app-fwlive/Makefile @@ -11,8 +11,8 @@ LUCI_DESCRIPTION:=Live firewall log table (nftables/fw4 and iptables LOG) with f LUCI_DEPENDS:=+luci-base +logd +jsonfilter LUCI_PKGARCH:=all -PKG_VERSION:=0.1.37 -PKG_RELEASE:=7 +PKG_VERSION:=0.1.38 +PKG_RELEASE:=8 # 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/constants.js b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/constants.js index f972c2f8..b5ddb588 100644 --- a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/constants.js +++ b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/constants.js @@ -7,7 +7,7 @@ */ return baseclass.extend({ /* Keep in sync with openwrt-feed/luci-app-fwlive/Makefile PKG_VERSION. */ - APP_VERSION: '0.1.37', + APP_VERSION: '0.1.38', ROW_LIMIT_OPTIONS: [ 25, 50, 100, 250, 500, 1000, 2000 ], DEFAULT_ROW_LIMIT: 100, /* Row pass/deny tint (#40): classic green/red default; accessible teal/orange */ diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile index 1a6c935d..3902fa8b 100644 --- a/luci-app-passwall2/Makefile +++ b/luci-app-passwall2/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall2 PKG_VERSION:=26.9.2 -PKG_RELEASE:=110 +PKG_RELEASE:=111 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall2/htdocs/luci-static/resources/view/passwall2/func.js b/luci-app-passwall2/htdocs/luci-static/resources/view/passwall2/func.js index 8c9e27c8..26eeabb7 100644 --- a/luci-app-passwall2/htdocs/luci-static/resources/view/passwall2/func.js +++ b/luci-app-passwall2/htdocs/luci-static/resources/view/passwall2/func.js @@ -47,6 +47,11 @@ function waitForElement(selector, callback) { observer.observe(document.body, { childList: true, subtree: true }); } +function waitForElementId(id, callback) { + waitForElement("#" + CSS.escape(id), callback); + //waitForElement('[id="' + id + '"]', callback); +} + function get_current_url() { return window.location.origin + window.location.pathname; } diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl_config.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl_config.lua index a0f69da5..57c79078 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl_config.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl_config.lua @@ -221,6 +221,16 @@ o.template = m:template_path("/cbi/hidevalue") o.value = "1" o:depends({ node = "", ['!reverse'] = true }) +o = s:option(DummyValue, "_is_singbox", "") +o.template = m:template_path("/cbi/hidevalue") +o.value = "1" +o:depends("_hide", "1") + +o = s:option(DummyValue, "_is_xray", "") +o.template = m:template_path("/cbi/hidevalue") +o.value = "1" +o:depends("_hide", "1") + o = s:option(Flag, "log", translate("Enable Node Log")) o:depends("_node", "1") @@ -249,7 +259,7 @@ o:depends("log", "1") o = s:option(DummyValue, "_show_dns_option", "") o.template = m:template_path("/cbi/hidevalue") o.value = "1" -o:depends("_hide", "1") +o:depends({ mode = "1", _node = "1" }) o = s:option(ListValue, "direct_dns_query_strategy", translate("Direct Query Strategy")) o.default = "UseIP" @@ -262,11 +272,9 @@ o = s:option(ListValue, "remote_dns_protocol", translate("Remote DNS Protocol")) o:value("tcp", "TCP") o:value("doh", "DoH") o:value("udp", "UDP") -if current_node.type == "sing-box" then - o:value("tls", "TLS(DoT)") - o:value("quic", "QUIC(DoQ)") - o:value("http3", "HTTP3(DoH3)") -end +o:value("tls", "TLS(DoT)", { _is_singbox = "1" }) +o:value("quic", "QUIC(DoQ)", { _is_singbox = "1" }) +o:value("http3", "HTTP3(DoH3)", { _is_singbox = "1" }) o:depends("_show_dns_option", "1") ---- DNS over TCP or UDP or TLS (DoT) or QUIC (DoQ) @@ -307,44 +315,30 @@ o = s:option(Value, "remote_dns_client_ip", translate("Remote DNS EDNS Client Su o.description = translate("Notify the DNS server when the DNS query is notified, the location of the client (cannot be a private IP address).") .. "
" .. translate("This feature requires the DNS server to support the Edns Client Subnet (RFC7871).") o.datatype = "ipaddr" -o:depends("remote_dns_protocol", "tcp") -o:depends("remote_dns_protocol", "doh") -o:depends("remote_dns_protocol", "udp") -o:depends("remote_dns_protocol", "http3") -o:depends("remote_dns_protocol", "quic") -o:depends("remote_dns_protocol", "tls") +o:depends("_show_dns_option", "1") o = s:option(ListValue, "remote_dns_detour", translate("Remote DNS Outbound")) o.default = "remote" o:value("remote", translate("Remote")) o:value("direct", translate("Direct")) -o:depends("remote_dns_protocol", "tcp") -o:depends("remote_dns_protocol", "doh") -o:depends("remote_dns_protocol", "udp") -o:depends("remote_dns_protocol", "http3") -o:depends("remote_dns_protocol", "quic") -o:depends("remote_dns_protocol", "tls") +o:depends("_show_dns_option", "1") o = s:option(Flag, "remote_fakedns", "FakeDNS", translate("Use FakeDNS work in the domain that proxy.")) o.default = "0" o.rmempty = false +o:depends("_hide", "1") o = s:option(ListValue, "remote_dns_query_strategy", translate("Remote Query Strategy")) o.default = "UseIPv4" o:value("UseIP") o:value("UseIPv4") o:value("UseIPv6") -o:depends("remote_dns_protocol", "tcp") -o:depends("remote_dns_protocol", "doh") -o:depends("remote_dns_protocol", "udp") -o:depends("remote_dns_protocol", "http3") -o:depends("remote_dns_protocol", "quic") -o:depends("remote_dns_protocol", "tls") +o:depends("_show_dns_option", "1") o = s:option(Value, "remote_rewrite_ttl", translate("Remote DNS") .. " TTL") o.datatype = "min(1)" o.default = "30" -o:depends("_hide", "1") +o:depends({ _show_dns_option = "1", _is_singbox = "1" }) o = s:option(ListValue, "dns_hosts_mode", translate("Domain Override")) o:value("default", translate("Use global config")) @@ -368,22 +362,39 @@ end local o_node = s.fields["node"] +local shunt_list = {} + for k, v in pairs(nodes_table) do o_node:value(v.id, v["remark"]) o_node.group[#o_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default") + if v.type == "Xray" then + --s.fields["_is_xray"]:depends({ node = v.id }) + end if v.type == "sing-box" then - s.fields["remote_rewrite_ttl"]:depends({ node = v.id }) + s.fields["_is_singbox"]:depends({ node = v.id }) end - s.fields["_show_dns_option"]:depends({ mode = "1", node = v.id }) if v.node_type == "normal" or v.protocol == "_balancing" or v.protocol == "_urltest" then --Shunt node has its own separate options. - s.fields["remote_fakedns"]:depends({ node = v.id, remote_dns_protocol = "tcp" }) - s.fields["remote_fakedns"]:depends({ node = v.id, remote_dns_protocol = "doh" }) - s.fields["remote_fakedns"]:depends({ node = v.id, remote_dns_protocol = "udp" }) - s.fields["remote_fakedns"]:depends({ node = v.id, remote_dns_protocol = "http3" }) - s.fields["remote_fakedns"]:depends({ node = v.id, remote_dns_protocol = "quic" }) - s.fields["remote_fakedns"]:depends({ node = v.id, remote_dns_protocol = "tls" }) + s.fields["remote_fakedns"]:depends({ node = v.id, _show_dns_option = "1" }) + end + if v.protocol and v.protocol == "_shunt" then + shunt_list[#shunt_list + 1] = v end end +--m:appendTemplate("/acl/options", {section = arg[1]}) + +-- Shunt Start +if current_node.protocol == "_shunt" then + local shunt_lua = loadfile("/usr/lib/lua/luci/model/cbi/passwall2/client/include/shunt_options.lua") + setfenv(shunt_lua, getfenv(1))(m, s, { + s_cfgid = s.section, + node_id = current_node_id, + node = current_node, + verify_option = s.fields["node"] + }) +end + +m:appendTemplate("/acl/shunt", { shunt_list = api.jsonc.stringify(shunt_list), section = s.section }) + return api.return_map(m) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/global.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/global.lua index 5613523e..e041a8d3 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/global.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/global.lua @@ -85,6 +85,16 @@ o.template = m:template_path("/cbi/nodes_listvalue") o:value("", translate("Close")) o.group = {""} +o = s:taboption("Main", DummyValue, "_node", "") +o.template = m:template_path("/cbi/hidevalue") +o.value = "1" +o:depends({ node = "", ['!reverse'] = true }) + +o = s:taboption("Main", DummyValue, "_is_singbox", "") +o.template = m:template_path("/cbi/hidevalue") +o.value = "1" +o:depends("_hide", "1") + current_node_id = m:get(s.section, "node") current_node = current_node_id and m:get(current_node_id) or {} @@ -113,7 +123,7 @@ if (has_singbox or has_xray) and #nodes_table > 0 then tips.cfgvalue = function(t, n) return string.format('%s', translate("There are no available nodes, please add or subscribe nodes first.")) end - tips:depends({ node = "", ["!reverse"] = true }) + tips:depends("_node", "1") for k, v in pairs(shunt_list) do tips:depends("node", v.id) end @@ -157,10 +167,11 @@ end node_socks_port = s:taboption("Main", Value, "node_socks_port", translate("Node") .. " Socks " .. translate("Listen Port")) node_socks_port.default = 1070 node_socks_port.datatype = "port" +node_socks_port:depends("_node", "1") node_socks_bind_local = s:taboption("Main", Flag, "node_socks_bind_local", translate("Node") .. " Socks " .. translate("Bind Local"), translate("When selected, it can only be accessed localhost.")) node_socks_bind_local.default = "1" -node_socks_bind_local:depends({ node = "", ["!reverse"] = true }) +node_socks_bind_local:depends("_node", "1") s:tab("DNS", translate("DNS")) @@ -190,11 +201,9 @@ o = s:taboption("DNS", ListValue, "remote_dns_protocol", translate("Remote DNS P o:value("tcp", "TCP") o:value("doh", "DoH") o:value("udp", "UDP") -if current_node.type == "sing-box" then - o:value("tls", "TLS(DoT)") - o:value("quic", "QUIC(DoQ)") - o:value("http3", "HTTP3(DoH3)") -end +o:value("tls", "TLS(DoT)", { _is_singbox = "1" }) +o:value("quic", "QUIC(DoQ)", { _is_singbox = "1" }) +o:value("http3", "HTTP3(DoH3)", { _is_singbox = "1" }) ---- DNS over TCP or UDP or TLS (DoT) or QUIC (DoQ) o = s:taboption("DNS", Value, "remote_dns", translate("Remote DNS")) @@ -388,6 +397,9 @@ for k, v in pairs(nodes_table) do if #normal_list == 0 and #iface_list == 0 then break end + if v.type == "sing-box" then + s.fields["_is_singbox"]:depends({ node = v.id }) + end o_node:value(v.id, v["remark"]) o_node.group[#o_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default") o_socks:value(v.id, v["remark"]) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/include/shunt_options.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/include/shunt_options.lua index 15e17eb1..947b8268 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/include/shunt_options.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/include/shunt_options.lua @@ -4,6 +4,7 @@ if not data.node_id or not data.node then return end +local api = m.api local s_cfgid = data.s_cfgid local current_node_id = data.node_id local node_list = data.node_list or api.get_node_list() diff --git a/luci-app-passwall2/luasrc/view/passwall2/acl/options.htm b/luci-app-passwall2/luasrc/view/passwall2/acl/options.htm new file mode 100644 index 00000000..e8610a40 --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/acl/options.htm @@ -0,0 +1,56 @@ +<% +local map = self.map +local api = map.api +local config = map.config +local section = self.section +-%> + \ No newline at end of file diff --git a/luci-app-passwall2/luasrc/view/passwall2/acl/shunt.htm b/luci-app-passwall2/luasrc/view/passwall2/acl/shunt.htm new file mode 100644 index 00000000..fc3bcd5d --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/acl/shunt.htm @@ -0,0 +1,38 @@ +<% +local map = self.map +local api = map.api +local appname = map.config +local section = self.section +-%> + \ No newline at end of file diff --git a/luci-app-passwall2/luasrc/view/passwall2/node_list/node_list.htm b/luci-app-passwall2/luasrc/view/passwall2/node_list/node_list.htm index f779b32c..0dc59b23 100644 --- a/luci-app-passwall2/luasrc/view/passwall2/node_list/node_list.htm +++ b/luci-app-passwall2/luasrc/view/passwall2/node_list/node_list.htm @@ -419,10 +419,10 @@ table td, .table .td { <%- else %> (function() { if (typeof(cbi_t_switch) === "function") { - var old_switch = cbi_t_switch; + const ori_cbi_t_switch = cbi_t_switch; cbi_t_switch = function(section, tab) { dechecked_all_node(); - return old_switch(section, tab); + return ori_cbi_t_switch(section, tab); }; } })(); @@ -1521,22 +1521,47 @@ table td, .table .td { } }); } - - waitForElement('input[type="checkbox"][name*="<%=appname%>"][name*="show_node_info"]', function(el) { - el.addEventListener("change", () => { - el.blur(); - show_node_info = el.checked ? "1" : "0"; - onChange("show_node_info", show_node_info); + + function dom_event() { + waitForElement('input[type="checkbox"][name*="<%=appname%>"][name*="show_node_info"]', function(el) { + el.addEventListener("change", () => { + el.blur(); + show_node_info = el.checked ? "1" : "0"; + onChange("show_node_info", show_node_info); + }); }); - }); - waitForElement('select[name*="<%=appname%>"][name*="auto_detection_time"]', function(el) { - el.addEventListener("change", () => { - el.blur(); - auto_detection_time = el.value; - onChange("auto_detection_time", auto_detection_time); + waitForElement('select[name*="<%=appname%>"][name*="auto_detection_time"]', function(el) { + el.addEventListener("change", () => { + el.blur(); + auto_detection_time = el.value; + onChange("auto_detection_time", auto_detection_time); + }); }); - }); + + waitForElement('select[id*="<%=appname%>"][id*="url_test_url"]', function(el) { + el.addEventListener("change", () => { + if (el.value) { + onChange("url_test_url", el.value); + } + }); + }); + + waitForElement('input[id*="<%=appname%>"][id*="url_test_url"]', function(el) { + el.addEventListener("change", () => { + if (el.value) { + onChange("url_test_url", el.value); + } + }); + }); + } + + const ori_cbi_d_update = cbi_d_update; + cbi_d_update = function() { + ori_cbi_d_update(); + dom_event(); + }; + dom_event(); const links = document.querySelectorAll('a'); links.forEach(link => { diff --git a/luci-app-passwall2/root/usr/share/passwall2/haproxy.lua b/luci-app-passwall2/root/usr/share/passwall2/haproxy.lua index 81c507cb..5167adb3 100644 --- a/luci-app-passwall2/root/usr/share/passwall2/haproxy.lua +++ b/luci-app-passwall2/root/usr/share/passwall2/haproxy.lua @@ -111,7 +111,7 @@ api.uci_foreach_c("haproxy_config", function(t) if server_node.type ~= "Socks" then local relay_port = server_node.port local new_port = api.get_new_port() - local config_file = string.format("haproxy_%s_%s.json", t[".name"], new_port) + local config_file = string.format("%s_%s.json", t[".name"], new_port) sys.call(string.format('/usr/share/%s/app.sh run_socks "%s"> /dev/null', appname, string.format("flag=%s node=%s bind=%s socks_port=%s config_file=%s", diff --git a/luci-app-passwall2/root/usr/share/passwall2/lease2hosts.sh b/luci-app-passwall2/root/usr/share/passwall2/lease2hosts.sh index be667b37..310326f3 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/lease2hosts.sh +++ b/luci-app-passwall2/root/usr/share/passwall2/lease2hosts.sh @@ -3,7 +3,7 @@ . /usr/share/passwall2/utils.sh LOCK_FILE=${LOCK_PATH}/${CONFIG}_lease2hosts.lock -LEASE_FILE="/tmp/dhcp.leases" +LEASE_FILE=$(config_t_get dnsmasq leasefile "/tmp/dhcp.leases") HOSTS_FILE="$TMP_PATH2/dhcp-hosts" TMP_FILE="/tmp/dhcp-hosts.tmp" @@ -15,7 +15,7 @@ fi reload_dnsmasq_pids() { local pidfile pid - find $TMP_PATH/acl -type f -name 'dnsmasq.pid' 2>/dev/null | while read pidfile; do + find $TMP_PATH/acl -type f -name '*_dnsmasq.pid' 2>/dev/null | while read pidfile; do if [ -s "$pidfile" ]; then read pid < "$pidfile" if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then