diff --git a/filebrowser/Makefile b/filebrowser/Makefile index 4eacb52b..f74ad5cd 100644 --- a/filebrowser/Makefile +++ b/filebrowser/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=filebrowser -PKG_VERSION:=1.5.1-stable +PKG_VERSION:=1.5.2-stable PKG_RELEASE=1 ifeq ($(ARCH),aarch64) diff --git a/luci-app-amlogic/Makefile b/luci-app-amlogic/Makefile index b7ec4e50..afee61c1 100644 --- a/luci-app-amlogic/Makefile +++ b/luci-app-amlogic/Makefile @@ -16,8 +16,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-amlogic -PKG_VERSION:=3.1.320 -PKG_RELEASE:=17 +PKG_VERSION:=3.1.321 +PKG_RELEASE:=18 PKG_LICENSE:=GPL-2.0 License PKG_MAINTAINER:=ophub diff --git a/luci-app-amlogic/root/usr/sbin/openwrt-kernel b/luci-app-amlogic/root/usr/sbin/openwrt-kernel index eddbdc98..20000853 100755 --- a/luci-app-amlogic/root/usr/sbin/openwrt-kernel +++ b/luci-app-amlogic/root/usr/sbin/openwrt-kernel @@ -68,7 +68,7 @@ get_textoffset() { temp_dir="$(mktemp -d)" ( cd ${temp_dir} - tar -xf "${boot_tgz_file}" "${vmlinuz_name}" + tar --no-same-owner -xf "${boot_tgz_file}" "${vmlinuz_name}" ) # With TEXT_OFFSET patch is [ 0108 ], without TEXT_OFFSET patch is [ 0000 ] [[ "$(hexdump -n 15 -x "${temp_dir}/${vmlinuz_name}" 2>/dev/null | head -n 1 | awk '{print $7}')" == "0108" ]] && K510="0" @@ -274,7 +274,7 @@ update_kernel() { dtb* 2>/dev/null ) # Extract the new kernel - tar -xf ${P4_PATH}/boot-${kernel_name}.tar.gz -C /boot + tar --no-same-owner -xf ${P4_PATH}/boot-${kernel_name}.tar.gz -C /boot # Check if the file exists local valid_files @@ -319,14 +319,14 @@ update_kernel() { else (cd /boot && mkdir -p dtb-${kernel_name}/${PLATFORM} && ln -sf dtb-${kernel_name} dtb) fi - tar -xf ${P4_PATH}/dtb-${PLATFORM}-${kernel_name}.tar.gz -C /boot/dtb/${PLATFORM} + tar --no-same-owner -xf ${P4_PATH}/dtb-${PLATFORM}-${kernel_name}.tar.gz -C /boot/dtb/${PLATFORM} [[ "$(ls /boot/dtb/${PLATFORM} -l 2>/dev/null | grep "^-" | wc -l)" -ge "1" ]] || error_msg "/boot/dtb/${PLATFORM} file is missing." echo -e "(2/3) Unpacking [ dtb-${PLATFORM}-${kernel_name}.tar.gz ] done." fi # 03. for /lib/modules/* rm -rf /lib/modules/* - tar -xf ${P4_PATH}/modules-${kernel_name}.tar.gz -C /lib/modules + tar --no-same-owner -xf ${P4_PATH}/modules-${kernel_name}.tar.gz -C /lib/modules ( cd /lib/modules/${kernel_name} rm -f *.ko diff --git a/luci-app-amlogic/root/usr/sbin/openwrt-update-rockchip b/luci-app-amlogic/root/usr/sbin/openwrt-update-rockchip index da6fe93f..a6521c17 100755 --- a/luci-app-amlogic/root/usr/sbin/openwrt-update-rockchip +++ b/luci-app-amlogic/root/usr/sbin/openwrt-update-rockchip @@ -210,6 +210,14 @@ case $MYDEVICE_NAME in fi SOC="ht2" ;; +"XYLink XR3528") + if [ -n "${CURRENT_FDTFILE}" ]; then + MYDTB_FDTFILE="${CURRENT_FDTFILE}" + else + MYDTB_FDTFILE="rk3528-xylink-xr3528.dtb" + fi + SOC="xylink-xr3528" + ;; "Radxa E20C") if [ -n "${CURRENT_FDTFILE}" ]; then MYDTB_FDTFILE="${CURRENT_FDTFILE}" diff --git a/luci-app-dnsproxy/README.md b/luci-app-dnsproxy/README.md index 4cacca4d..5a517e98 100644 --- a/luci-app-dnsproxy/README.md +++ b/luci-app-dnsproxy/README.md @@ -4,6 +4,15 @@ `DNS-over-TLS`, `DNS-over-HTTPS`, `DNSCrypt`, and `DNS-over-QUIC`. Moreover, it can work as a `DNS-over-HTTPS`, `DNS-over-TLS` or `DNS-over-QUIC` server. +The LuCI interface also supports reusable DNS profiles. Each profile stores +its own bootstrap, upstream, and fallback server lists and can apply all three +lists at once. Editable example profiles for Cloudflare, Quad9, Google, and a +mixed parallel setup are included to demonstrate the feature. + +## Requirements + +- OpenWrt >= 24.10 + ## How to install 1. Go to [here](https://fantastic-packages.github.io/releases/) diff --git a/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js b/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js index e6f11512..c9e95772 100644 --- a/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js +++ b/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js @@ -2,13 +2,22 @@ 'require form'; 'require fs'; 'require uci'; +'require dom'; 'require rpc'; 'require poll'; +'require ui'; 'require view'; +'require validation'; 'require tools.widgets as widgets'; const conf = 'dnsproxy'; const instance = 'dnsproxy'; +const profileListOptions = ['bootstrap', 'upstream', 'fallback']; +const upstream_mode = [ + ['load_balance', _('Load balance'), _('one upstream per request.')], + ['parallel', _('Parallel'), _('first DNS response wins.')], + ['fastest_addr', _('Fastest address'), _('tests returned IP addresses.')] +]; const callServiceList = rpc.declare({ object: 'service', @@ -34,6 +43,91 @@ function getServiceStatus() { }); } +function profileTitle(profile) { + return profile.label || profile['.name']; +} + +function applyProfile(select) { + const profile = uci.sections(conf, 'profile').find((item) => item['.name'] === select.value); + + if (!profile) + return ui.addNotification(null, E('p', _('The selected DNS profile no longer exists. Reload the page and try again.')), 'error'); + + const upstreams = L.toArray(profile.upstream).filter(Boolean); + if (!upstreams.length) + return ui.addNotification(null, E('p', _('The selected DNS profile has no upstream servers.')), 'error'); + + profileListOptions.forEach((option) => { + const values = L.toArray(profile[option]).filter(Boolean); + uci.set(conf, 'servers', option, values.length ? values : null); + }); + + if (profile.upstream_mode) + uci.set(conf, 'global', 'upstream_mode', profile.upstream_mode); + + return uci.save() + .then(L.bind(this.map.load, this.map)) + .then(L.bind(this.map.reset, this.map)) + .then(() => { + ui.addNotification(null, E('p', _('DNS profile “%s” has been applied.').format(profileTitle(profile))), 'info'); + }) + .catch((err) => { + ui.addNotification(null, E('p', _('Failed to apply DNS profile: %s').format(err.message || err)), 'error'); + }); +} + +function validateUniqueValue(section_id, value) { + if (!value) + return _('Expecting: %s').format(_('non-empty value')); + + let duplicate = false; + uci.sections(this.config, this.section.sectiontype, (res) => { + if (res['.name'] !== section_id) + if (res[this.option] === value) + duplicate = true; + }); + if (duplicate) + return _('Expecting: %s').format(_('unique value')); + + return true; +} + +function validateServerValue(section_id, value) { + if (!value) + return true; + + if (!value.includes('://')) { + let stubValidator = { + factory: validation, + apply(type, value, args) { + if (value != null) + this.value = value; + + return validation.types[type].apply(this, args); + }, + assert(condition) { + return !!condition; + } + }; + + if (!stubValidator.apply('host', value) && !stubValidator.apply('hostport', value)) + return _('Expecting: %s').format(_('valid host or hostport')); + + return true; + } + + try { + const url = new URL(value); + if (!url.hostname) + return _('Expecting: %s').format(_('valid URL')); + + return true; + } + catch (e) { + return _('Invalid URI'); + } +} + return view.extend({ load() { @@ -116,7 +210,8 @@ return view.extend({ o = s.taboption('main', form.Flag, 'insecure', _('Disable secure TLS cert validation')); - o = s.taboption('main', form.Flag, 'http3', _('DoH uses H3 first')); + o = s.taboption('main', form.Flag, 'http3', _('Enable HTTP/3 for DoH')); + o.description = _('Use HTTP/3 when it is faster. HTTPS fallback remains available.'); o = s.taboption('main', form.Value, 'timeout', _('Timeout for queries to remote upstream (default: 10s)')); o.datatype = 'string'; @@ -127,14 +222,17 @@ return view.extend({ o = s.taboption('main', form.Value, 'udp_buf_size', _('Size of the UDP buffer in bytes. Set 0 use the system default')); o.datatype = 'uinteger'; - o = s.taboption('main', form.Flag, 'all_servers', _('Parallel queries all upstream')); - - o = s.taboption('main', form.Flag, 'fastest_addr', _('Respond to A or AAAA requests only with the fastest IP address')); - o.depends('all_servers', '1'); + o = s.taboption('main', form.RichListValue, 'upstream_mode', _('Upstream selection mode'), + _('For the lowest DNS response time choose Parallel.') + '
' + + _('Fastest address performs additional IP reachability tests and is a different, slower operation.')); + upstream_mode.forEach((res) => { + o.value.apply(o, res); + }) + o.rmempty = false; s.tab('cache', _('Cache')); - o = s.taboption('cache', form.SectionValue, '_cache', form.NamedSection, 'cache', 'homeproxy'); + o = s.taboption('cache', form.SectionValue, '_cache', form.NamedSection, 'cache', 'dnsproxy'); ss = o.subsection; so = ss.option(form.Flag, 'enabled', _('Enable Cache')); @@ -161,7 +259,7 @@ return view.extend({ s.tab('dns64', _('DNS64')); - o = s.taboption('dns64', form.SectionValue, '_dns64', form.NamedSection, 'dns64', 'homeproxy'); + o = s.taboption('dns64', form.SectionValue, '_dns64', form.NamedSection, 'dns64', 'dnsproxy'); ss = o.subsection; so = ss.option(form.Flag, 'enabled', _('Enable DNS64')); @@ -174,7 +272,7 @@ return view.extend({ s.tab('edns', _('EDNS')); - o = s.taboption('edns', form.SectionValue, '_edns', form.NamedSection, 'edns', 'homeproxy'); + o = s.taboption('edns', form.SectionValue, '_edns', form.NamedSection, 'edns', 'dnsproxy'); ss = o.subsection; so = ss.option(form.Flag, 'enabled', _('Enable EDNS')); @@ -186,7 +284,7 @@ return view.extend({ s.tab('bogus_nxdomain', _('Bogus-NXDOMAIN')); - o = s.taboption('bogus_nxdomain', form.SectionValue, '_bogus_nxdomain', form.NamedSection, 'bogus_nxdomain', 'homeproxy'); + o = s.taboption('bogus_nxdomain', form.SectionValue, '_bogus_nxdomain', form.NamedSection, 'bogus_nxdomain', 'dnsproxy'); ss = o.subsection; so = ss.option(form.DynamicList, 'ip_addr', _('Convert matching single IP responses to NXDOMAIN')); @@ -194,15 +292,107 @@ return view.extend({ s.tab('servers', _('Upstreams')); - o = s.taboption('servers', form.SectionValue, '_servers', form.NamedSection, 'servers', 'homeproxy'); + o = s.taboption('servers', form.DummyValue, '_profile_switcher', _('DNS profiles'), + _('A profile replaces Bootstrap, Upstream and Fallback lists together. Unsaved changes elsewhere on this page are not included.')); + o.renderWidget = function () { + const profiles = uci.sections(conf, 'profile'); + const select = E('select', { + 'id': 'dnsproxy-profile-select', + 'class': 'cbi-input-select', + 'disabled': profiles.length ? null : '' + }, profiles.map((profile) => E('option', { + 'value': profile['.name'] + }, profileTitle(profile)))); + + return E('div', {}, [ + E('div', { 'style': 'display:flex;gap:.75em;align-items:center;flex-wrap:wrap' }, [ + select, + E('button', { + 'class': 'cbi-button cbi-button-positive important', + 'disabled': profiles.length ? null : '', + 'click': ui.createHandlerFn(this, applyProfile, select) + }, _('Use selected profile')) + ]), + profiles.length ? '' : E('p', {}, _('Create and save a profile in the “DNS profile templates” section below first.')) + ]); + }; + + o = s.taboption('servers', form.SectionValue, '_servers', form.NamedSection, 'servers', 'dnsproxy'); ss = o.subsection; so = ss.option(form.DynamicList, 'bootstrap', _('Bootstrap DNS Server')); + so.readonly = true so = ss.option(form.DynamicList, 'upstream', _('Upstream DNS Server')); + so.readonly = true so.rmempty = false; so = ss.option(form.DynamicList, 'fallback', _('Fallback DNS Server')); + so.readonly = true + + o = s.taboption('servers', form.SectionValue, '_profiles', form.GridSection, 'profile', _('DNS profile templates'), + _('Create reusable templates here. Applying a template does not modify the template itself.')); + ss = o.subsection; + ss.anonymous = true; + ss.addremove = true; + ss.rowcolors = true; + ss.sortable = true; + ss.nodescriptions = true; + ss.addbtntitle = _('Add DNS profile'); + ss.renderRowActions = function(section_id, more_label, trEl) { + trEl = form.GridSection.prototype.renderRowActions.apply(this, arguments); + const preset_ids = [ + 'default' + ]; + + if (preset_ids.includes(section_id)) + trEl?.lastChild.querySelector('.cbi-button-remove')?.remove(); + + return trEl; + } + + so = ss.option(form.Value, 'label', _('Profile name')); + so.rmempty = false; + so.validate = validateUniqueValue; + + so = ss.option(form.RichListValue, 'upstream_mode', _('Upstream selection mode')); + so.value(' ', _('Keep current mode')); + upstream_mode.forEach((res) => { + so.value.apply(so, res); + }) + so.load = function(section_id) { + let value = this.super('load', section_id); + if (!value) + return ' '; + + return value; + } + so.write = function(section_id, value) { + if (!value.trim()) + return this.super('remove', section_id); + + return form.RichListValue.prototype.write.call(this, section_id, value); + } + so.textvalue = function(section_id) { + let cval = this.cfgvalue(section_id); + let i = this.keylist.indexOf(cval); + let val = this.vallist[i]; + + return dom.elem(val) ? val.firstChild.textContent : val ?? cval; + } + + so = ss.option(form.DynamicList, 'bootstrap', _('Bootstrap DNS')); + so.validate = validateServerValue; + so.modalonly = true; + + so = ss.option(form.DynamicList, 'upstream', _('Upstream DNS')); + so.rmempty = false; + so.validate = validateServerValue; + so.modalonly = true; + + so = ss.option(form.DynamicList, 'fallback', _('Fallback DNS')); + so.validate = validateServerValue; + so.modalonly = true; return m.render() .then(L.bind(function(m, nodes) { diff --git a/luci-app-dnsproxy/po/templates/dnsproxy.pot b/luci-app-dnsproxy/po/templates/dnsproxy.pot index 869fa122..3ab591ca 100644 --- a/luci-app-dnsproxy/po/templates/dnsproxy.pot +++ b/luci-app-dnsproxy/po/templates/dnsproxy.pot @@ -1,155 +1,287 @@ msgid "" msgstr "Content-Type: text/plain; charset=UTF-8" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:187 +#: htdocs/luci-static/resources/view/dnsproxy.js:296 +msgid "" +"A profile replaces Bootstrap, Upstream and Fallback lists together. Unsaved " +"changes elsewhere on this page are not included." +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:341 +msgid "Add DNS profile" +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:285 msgid "Bogus-NXDOMAIN" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:200 +#: htdocs/luci-static/resources/view/dnsproxy.js:384 +msgid "Bootstrap DNS" +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:323 msgid "Bootstrap DNS Server" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:135 +#: htdocs/luci-static/resources/view/dnsproxy.js:233 msgid "Cache" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:146 +#: htdocs/luci-static/resources/view/dnsproxy.js:244 msgid "Cache size (in bytes)" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:70 +#: htdocs/luci-static/resources/view/dnsproxy.js:164 msgid "Collecting data ..." msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:192 +#: htdocs/luci-static/resources/view/dnsproxy.js:290 msgid "Convert matching single IP responses to NXDOMAIN" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:65 -#: applications/luci-app-dnsproxy/root/usr/share/luci/menu.d/luci-app-dnsproxy.json:3 +#: htdocs/luci-static/resources/view/dnsproxy.js:316 +msgid "" +"Create and save a profile in the “DNS profile templates” section below first." +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:334 +msgid "" +"Create reusable templates here. Applying a template does not modify the " +"template itself." +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:159 +#: root/usr/share/luci/menu.d/luci-app-dnsproxy.json:3 msgid "DNS Proxy" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:162 +#: htdocs/luci-static/resources/view/dnsproxy.js:333 +msgid "DNS profile templates" +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:72 +msgid "DNS profile “%s” has been applied." +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:295 +msgid "DNS profiles" +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:260 msgid "DNS64" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:169 +#: htdocs/luci-static/resources/view/dnsproxy.js:267 msgid "DNS64 Prefix" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:113 +#: htdocs/luci-static/resources/view/dnsproxy.js:207 msgid "Disable IPv6" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:117 +#: htdocs/luci-static/resources/view/dnsproxy.js:211 msgid "Disable secure TLS cert validation" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:119 -msgid "DoH uses H3 first" -msgstr "" - -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:175 +#: htdocs/luci-static/resources/view/dnsproxy.js:273 msgid "EDNS" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:182 +#: htdocs/luci-static/resources/view/dnsproxy.js:280 msgid "EDNS Client Address" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:78 +#: htdocs/luci-static/resources/view/dnsproxy.js:172 msgid "Enable" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:140 +#: htdocs/luci-static/resources/view/dnsproxy.js:238 msgid "Enable Cache" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:167 +#: htdocs/luci-static/resources/view/dnsproxy.js:265 msgid "Enable DNS64" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:180 +#: htdocs/luci-static/resources/view/dnsproxy.js:278 msgid "Enable EDNS" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:205 +#: htdocs/luci-static/resources/view/dnsproxy.js:213 +msgid "Enable HTTP/3 for DoH" +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:81 +#: htdocs/luci-static/resources/view/dnsproxy.js:90 +#: htdocs/luci-static/resources/view/dnsproxy.js:114 +#: htdocs/luci-static/resources/view/dnsproxy.js:122 +msgid "Expecting: %s" +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:75 +msgid "Failed to apply DNS profile: %s" +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:393 +msgid "Fallback DNS" +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:330 msgid "Fallback DNS Server" msgstr "" -#: applications/luci-app-dnsproxy/root/usr/share/rpcd/acl.d/luci-app-dnsproxy.json:3 +#: htdocs/luci-static/resources/view/dnsproxy.js:19 +msgid "Fastest address" +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:227 +msgid "" +"Fastest address performs additional IP reachability tests and is a " +"different, slower operation." +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:226 +msgid "For the lowest DNS response time choose Parallel." +msgstr "" + +#: root/usr/share/rpcd/acl.d/luci-app-dnsproxy.json:3 msgid "Grant access to LuCI app dnsproxy" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:86 +#: htdocs/luci-static/resources/view/dnsproxy.js:127 +msgid "Invalid URI" +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:359 +msgid "Keep current mode" +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:180 msgid "Listen address" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:108 +#: htdocs/luci-static/resources/view/dnsproxy.js:202 msgid "Listen ports" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:83 +#: htdocs/luci-static/resources/view/dnsproxy.js:17 +msgid "Load balance" +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:177 msgid "Log file path" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:76 +#: htdocs/luci-static/resources/view/dnsproxy.js:170 msgid "Main" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:157 +#: htdocs/luci-static/resources/view/dnsproxy.js:255 msgid "Max TTL value for DNS entries, in seconds" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:152 +#: htdocs/luci-static/resources/view/dnsproxy.js:250 msgid "Min TTL value for DNS entries, in seconds" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:142 +#: htdocs/luci-static/resources/view/dnsproxy.js:240 msgid "Optimistic Cache" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:130 -msgid "Parallel queries all upstream" +#: htdocs/luci-static/resources/view/dnsproxy.js:18 +msgid "Parallel" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:124 +#: htdocs/luci-static/resources/view/dnsproxy.js:354 +msgid "Profile name" +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:219 msgid "Ratelimit (requests per second)" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:115 +#: htdocs/luci-static/resources/view/dnsproxy.js:209 msgid "Refuse ANY requests" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:132 -msgid "Respond to A or AAAA requests only with the fastest IP address" -msgstr "" - -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:54 +#: htdocs/luci-static/resources/view/dnsproxy.js:148 msgid "SERVER NOT RUNNING" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:52 +#: htdocs/luci-static/resources/view/dnsproxy.js:146 msgid "SERVER RUNNING" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:127 +#: htdocs/luci-static/resources/view/dnsproxy.js:222 msgid "Size of the UDP buffer in bytes. Set 0 use the system default" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:121 +#: htdocs/luci-static/resources/view/dnsproxy.js:58 +msgid "The selected DNS profile has no upstream servers." +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:54 +msgid "" +"The selected DNS profile no longer exists. Reload the page and try again." +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:216 msgid "Timeout for queries to remote upstream (default: 10s)" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:202 +#: htdocs/luci-static/resources/view/dnsproxy.js:388 +msgid "Upstream DNS" +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:326 msgid "Upstream DNS Server" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:195 +#: htdocs/luci-static/resources/view/dnsproxy.js:225 +#: htdocs/luci-static/resources/view/dnsproxy.js:358 +msgid "Upstream selection mode" +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:293 msgid "Upstreams" msgstr "" -#: applications/luci-app-dnsproxy/htdocs/luci-static/resources/view/dnsproxy.js:81 +#: htdocs/luci-static/resources/view/dnsproxy.js:214 +msgid "Use HTTP/3 when it is faster. HTTPS fallback remains available." +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:314 +msgid "Use selected profile" +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:175 msgid "Verbose" msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:18 +msgid "first DNS response wins." +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:81 +msgid "non-empty value" +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:17 +msgid "one upstream per request." +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:19 +msgid "tests returned IP addresses." +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:90 +msgid "unique value" +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:122 +msgid "valid URL" +msgstr "" + +#: htdocs/luci-static/resources/view/dnsproxy.js:114 +msgid "valid host or hostport" +msgstr "" diff --git a/luci-app-dnsproxy/root/etc/uci-defaults/99-luci-app-dnsproxy-defaults b/luci-app-dnsproxy/root/etc/uci-defaults/99-luci-app-dnsproxy-defaults new file mode 100755 index 00000000..199d5fef --- /dev/null +++ b/luci-app-dnsproxy/root/etc/uci-defaults/99-luci-app-dnsproxy-defaults @@ -0,0 +1,58 @@ +#!/bin/sh + +add_cloudflare_profile() { + uci set dnsproxy.preset_cloudflare='profile' + uci set dnsproxy.preset_cloudflare.label='Preset: Cloudflare DoH' + uci add_list dnsproxy.preset_cloudflare.bootstrap='1.1.1.1:53' + uci add_list dnsproxy.preset_cloudflare.bootstrap='1.0.0.1:53' + uci add_list dnsproxy.preset_cloudflare.upstream='https://cloudflare-dns.com/dns-query' + uci add_list dnsproxy.preset_cloudflare.fallback='https://dns.quad9.net/dns-query' + uci set dnsproxy.preset_cloudflare.upstream_mode='load_balance' +} + +add_quad9_profile() { + uci set dnsproxy.preset_quad9='profile' + uci set dnsproxy.preset_quad9.label='Preset: Quad9 DoH' + uci add_list dnsproxy.preset_quad9.bootstrap='9.9.9.9:53' + uci add_list dnsproxy.preset_quad9.bootstrap='149.112.112.112:53' + uci add_list dnsproxy.preset_quad9.upstream='https://dns.quad9.net/dns-query' + uci add_list dnsproxy.preset_quad9.fallback='https://cloudflare-dns.com/dns-query' + uci set dnsproxy.preset_quad9.upstream_mode='load_balance' +} + +add_google_profile() { + uci set dnsproxy.preset_google='profile' + uci set dnsproxy.preset_google.label='Preset: Google DoH' + uci add_list dnsproxy.preset_google.bootstrap='8.8.8.8:53' + uci add_list dnsproxy.preset_google.bootstrap='8.8.4.4:53' + uci add_list dnsproxy.preset_google.upstream='https://dns.google/dns-query' + uci add_list dnsproxy.preset_google.fallback='https://cloudflare-dns.com/dns-query' + uci set dnsproxy.preset_google.upstream_mode='load_balance' +} + +add_fast_mixed_profile() { + uci set dnsproxy.preset_fast_mixed='profile' + uci set dnsproxy.preset_fast_mixed.label='Preset: Fast mixed DoH' + uci add_list dnsproxy.preset_fast_mixed.bootstrap='1.1.1.1:53' + uci add_list dnsproxy.preset_fast_mixed.bootstrap='9.9.9.9:53' + uci add_list dnsproxy.preset_fast_mixed.bootstrap='8.8.8.8:53' + uci add_list dnsproxy.preset_fast_mixed.upstream='https://cloudflare-dns.com/dns-query' + uci add_list dnsproxy.preset_fast_mixed.upstream='https://dns.quad9.net/dns-query' + uci add_list dnsproxy.preset_fast_mixed.fallback='https://dns.google/dns-query' + uci set dnsproxy.preset_fast_mixed.upstream_mode='parallel' +} + +# preset +if [ -z "$(uci -q get dnsproxy.default)" ]; then + uci -q get dnsproxy.preset_cloudflare >/dev/null || add_cloudflare_profile + uci -q get dnsproxy.preset_quad9 >/dev/null || add_quad9_profile + uci -q get dnsproxy.preset_google >/dev/null || add_google_profile + uci -q get dnsproxy.preset_fast_mixed >/dev/null || add_fast_mixed_profile + uci commit dnsproxy + # default + echo "config profile 'default'" >> /etc/config/dnsproxy + echo " option label 'Default'" >> /etc/config/dnsproxy + sed -n "/config dnsproxy 'servers'/,/^[^\t]/{/^\t/p}" /etc/config/dnsproxy >> /etc/config/dnsproxy +fi + +exit 0 diff --git a/luci-app-fchomo/htdocs/luci-static/resources/fchomo/listeners.js b/luci-app-fchomo/htdocs/luci-static/resources/fchomo/listeners.js index 34505da1..00d3230f 100644 --- a/luci-app-fchomo/htdocs/luci-static/resources/fchomo/listeners.js +++ b/luci-app-fchomo/htdocs/luci-static/resources/fchomo/listeners.js @@ -313,16 +313,24 @@ function renderListeners(s, uciconfig, isClient) { o.depends({sudoku_table_type: /^(prefer_entropy|up_ascii_down_entropy|up_entropy_down_ascii)$/}); o.modalonly = true; - o = s.taboption('field_general', form.Value, 'sudoku_padding_min', _('Minimum padding rate')); + o = s.taboption('field_general', form.RangeSliderValue, 'sudoku_padding_min', _('Minimum padding rate')); o.datatype = 'and(uinteger, range(0, 100))'; o.default = 1; + o.min = 0; + o.max = 100; + o.step = 1; + o.calcunits = '%'; o.rmempty = false; o.depends('type', 'sudoku'); o.modalonly = true; - o = s.taboption('field_general', form.Value, 'sudoku_padding_max', _('Maximum padding rate')); + o = s.taboption('field_general', form.RangeSliderValue, 'sudoku_padding_max', _('Maximum padding rate')); o.datatype = 'and(uinteger, range(0, 100))'; o.default = 15; + o.min = 0; + o.max = 100; + o.step = 1; + o.calcunits = '%'; o.rmempty = false; o.validate = function(section_id, value) { const padding_min = this.section.getOption('sudoku_padding_min').formvalue(section_id); diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index 05cc504c..9002615e 100644 --- a/luci-app-passwall/Makefile +++ b/luci-app-passwall/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall PKG_VERSION:=26.8.12 -PKG_RELEASE:=220 +PKG_RELEASE:=221 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall/luasrc/controller/passwall.lua b/luci-app-passwall/luasrc/controller/passwall.lua index 03e24e47..6cff27e7 100644 --- a/luci-app-passwall/luasrc/controller/passwall.lua +++ b/luci-app-passwall/luasrc/controller/passwall.lua @@ -6,8 +6,7 @@ module("luci.controller.passwall", package.seeall) local api = require "luci.passwall.api" local appname = api.appname -- not available local c_config = api.c_config -- not available -local s_config = api.s_config -- not available -local uci = api.uci -- in funtion index() +local uci, uci_get, uci_set, uci_del, uci_foreach, uci_save = api.uci, api.uci_get_c, api.uci_set_c, api.uci_del_c, api.uci_foreach_c, api.uci_save_c local fs = api.fs local http = require "luci.http" local util = require "luci.util" @@ -23,9 +22,6 @@ function index() end local api = require "luci.passwall.api" local appname = api.appname -- global definitions not available - local c_config = api.c_config -- not available - local s_config = api.s_config -- not available - local uci = api.uci -- in function index() local fs = api.fs entry({"admin", "services", appname}).dependent = true entry({"admin", "services", appname, "show"}, call("show_menu")).leaf = true @@ -33,7 +29,7 @@ function index() entry({"admin", "services", appname, "ip"}, call('check_ip')).leaf = true entry({"admin", "services", appname, "adblock_refresh"}, call('adblock_refresh')).leaf = true local e - if uci:get(c_config, "@global[0]", "hide_from_luci") ~= "1" then + if api.uci_get_c("@global[0]", "hide_from_luci") ~= "1" then e = entry({"admin", "services", appname}, alias("admin", "services", appname, "settings"), _("Pass Wall"), -1) else e = entry({"admin", "services", appname}, alias("admin", "services", appname, "settings"), nil, -1) @@ -201,10 +197,10 @@ function socks_autoswitch_add_node() local id = http.formvalue("id") local key = http.formvalue("key") if id and id ~= "" and key and key ~= "" then - uci:set(c_config, id, "enable_autoswitch", "1") - local new_list = uci:get(c_config, id, "autoswitch_backup_node") or {} + uci_set(id, "enable_autoswitch", "1") + local new_list = uci_get(id, "autoswitch_backup_node") or {} for i = #new_list, 1, -1 do - if (uci:get(c_config, new_list[i], "remarks") or ""):find(key) then + if (uci_get(new_list[i], "remarks") or ""):find(key) then table.remove(new_list, i) end end @@ -213,8 +209,8 @@ function socks_autoswitch_add_node() table.insert(new_list, e.id) end end - uci:set_list(c_config, id, "autoswitch_backup_node", new_list) - api.uci_save(uci, c_config) + uci_set(id, "autoswitch_backup_node", new_list) + uci_save() end http.redirect(api.url("socks_config", id)) end @@ -223,15 +219,15 @@ function socks_autoswitch_remove_node() local id = http.formvalue("id") local key = http.formvalue("key") if id and id ~= "" and key and key ~= "" then - uci:set(c_config, id, "enable_autoswitch", "1") - local new_list = uci:get(c_config, id, "autoswitch_backup_node") or {} + uci_set(id, "enable_autoswitch", "1") + local new_list = uci_get(id, "autoswitch_backup_node") or {} for i = #new_list, 1, -1 do - if (uci:get(c_config, new_list[i], "remarks") or ""):find(key) then + if (uci_get(new_list[i], "remarks") or ""):find(key) then table.remove(new_list, i) end end - uci:set_list(c_config, id, "autoswitch_backup_node", new_list) - api.uci_save(uci, c_config) + uci_set(id, "autoswitch_backup_node", new_list) + uci_save() end http.redirect(api.url("socks_config", id)) end @@ -274,25 +270,25 @@ function get_redir_log() end if name == "default" then - if proto == "UDP" and (uci:get(appname, "@global[0]", "udp_node") or "nil") == "tcp" and not fs.access(path .. "/" .. proto .. ".log") then + if proto == "UDP" and (uci_get("@global[0]", "udp_node") or "nil") == "tcp" and not fs.access(path .. "/" .. proto .. ".log") then proto = "TCP" end else - local global_tcp = uci:get(appname, "@global[0]", "tcp_node") or "nil" - local global_udp = uci:get(appname, "@global[0]", "udp_node") or "nil" - local acl_tcp = uci:get(appname, name, "tcp_node") or "nil" - local acl_udp = uci:get(appname, name, "udp_node") or "nil" - local global_enabled = uci:get(appname, "@global[0]", "enabled") == "1" + local global_tcp = uci_get("@global[0]", "tcp_node") or "nil" + local global_udp = uci_get("@global[0]", "udp_node") or "nil" + local acl_tcp = uci_get(name, "tcp_node") or "nil" + local acl_udp = uci_get(name, "udp_node") or "nil" + local global_enabled = uci_get("@global[0]", "enabled") == "1" if proto == "TCP" and acl_tcp == global_tcp and global_enabled then path = "/tmp/etc/passwall/acl/default" - if uci:get(appname, "@global[0]", "log_tcp") ~= "1" then + if uci_get("@global[0]", "log_tcp") ~= "1" then alert("The access control node is the same as the global node. Please enable global logging.") return end end if proto == "UDP" and acl_udp == global_udp and global_enabled then path = "/tmp/etc/passwall/acl/default" - if uci:get(appname, "@global[0]", "log_udp") ~= "1" then + if uci_get("@global[0]", "log_udp") ~= "1" then alert("The access control node is the same as the global node. Please enable global logging.") return end @@ -327,11 +323,11 @@ function get_chinadns_log() local flag = http.formvalue("flag") local path = "/tmp/etc/passwall/acl/" .. flag .. "/chinadns_ng.log" if flag ~= "default" then - local global_tcp = uci:get(appname, "@global[0]", "tcp_node") or "nil" - local acl_tcp = uci:get(appname, flag, "tcp_node") or "nil" - if acl_tcp == global_tcp and uci:get(appname, "@global[0]", "enabled") == "1" then + local global_tcp = uci_get("@global[0]", "tcp_node") or "nil" + local acl_tcp = uci_get(flag, "tcp_node") or "nil" + if acl_tcp == global_tcp and uci_get("@global[0]", "enabled") == "1" then path = "/tmp/etc/passwall/acl/default/chinadns_ng.log" - if uci:get(appname, "@global[0]", "log_chinadns_ng") ~= "1" then + if uci_get("@global[0]", "log_chinadns_ng") ~= "1" then http.write(string.format("", i18n.translate("The access control node is the same as the global node. Please enable global logging."))) return end @@ -433,7 +429,7 @@ end function index_status() local e = {} - local dns_shunt = uci:get(appname, "@global[0]", "dns_shunt") or "dnsmasq" + local dns_shunt = uci_get("@global[0]", "dns_shunt") or "dnsmasq" if dns_shunt == "smartdns" then local port = api.get_cache_var("SMARTDNS_LOCAL_PORT") or 0 e.dns_mode_status = (port ~= 0) and luci.sys.call(string.format("netstat -apn | grep ':%s ' >/dev/null", port)) == 0 or false @@ -450,7 +446,7 @@ function index_status() e["tcp_node_status"] = luci.sys.call("/bin/busybox top -bn1 | grep -v 'grep' | grep '/tmp/etc/passwall/bin/' | grep 'default' | grep 'TCP' >/dev/null") == 0 - if (uci:get(appname, "@global[0]", "udp_node") or "nil") == "tcp" then + if (uci_get("@global[0]", "udp_node") or "nil") == "tcp" then e["udp_node_status"] = e["tcp_node_status"] else e["udp_node_status"] = luci.sys.call("/bin/busybox top -bn1 | grep -v 'grep' | grep '/tmp/etc/passwall/bin/' | grep 'default' | grep 'UDP' >/dev/null") == 0 @@ -470,7 +466,7 @@ function socks_status() local id = http.formvalue("id") e.index = index e.socks_status = luci.sys.call(string.format("/bin/busybox top -bn1 | grep -v -E 'grep|acl/|acl_' | grep '%s/bin/' | grep '%s' > /dev/null", appname, id)) == 0 - local use_http = uci:get(c_config, id, "http_port") or 0 + local use_http = uci_get(id, "http_port") or 0 e.use_http = 0 if tonumber(use_http) > 0 then e.use_http = 1 @@ -484,10 +480,10 @@ function connect_status() e.use_time = "" local url = http.formvalue("url") local aliyun = string.find(url, "aliyun") - local chn_list = uci:get(appname, "@global[0]", "chn_list") or "direct" - local gfw_list = uci:get(appname, "@global[0]", "use_gfw_list") or "1" - local proxy_mode = uci:get(appname, "@global[0]", "tcp_proxy_mode") or "proxy" - local localhost_proxy = uci:get(appname, "@global[0]", "localhost_proxy") or "1" + local chn_list = uci_get("@global[0]", "chn_list") or "direct" + local gfw_list = uci_get("@global[0]", "use_gfw_list") or "1" + local proxy_mode = uci_get("@global[0]", "tcp_proxy_mode") or "proxy" + local localhost_proxy = uci_get("@global[0]", "localhost_proxy") or "1" local socks_server = (localhost_proxy == "0") and api.get_cache_var("GLOBAL_TCP_SOCKS_server") or "" url = "-w %{http_code}:%{time_pretransfer} " .. url if socks_server and socks_server ~= "" then @@ -562,9 +558,9 @@ function update_config() local data_t = jsonParse(data) or {} if next(data_t) then for k, v in pairs(data_t) do - uci:set(c_config, id, k, v) + uci_set(id, k, v) end - api.uci_save(uci, c_config) + uci_save() http_write_json_ok() return end @@ -580,16 +576,16 @@ function add_node() local group = http.formvalue("group") if group and group ~= "default" then - uci:set(c_config, uid, "group", group) + uci_set(uid, "group", group) end - uci:set(c_config, uid, "type", "Socks") + uci_set(uid, "type", "Socks") if redirect == "1" then - api.uci_save(uci, c_config) + uci_save() http.redirect(api.url("node_config", uid)) else - api.uci_save(uci, c_config, true, true) + uci_save(true, true) http_write_json({result = uid}) end end @@ -597,22 +593,22 @@ end function set_node() local protocol = http.formvalue("protocol") local section = http.formvalue("section") - uci:set(c_config, "@global[0]", protocol .. "_node", section) + uci_set("@global[0]", protocol .. "_node", section) if protocol == "tcp" then - local node_protocol = uci:get(c_config, section, "protocol") + local node_protocol = uci_get(section, "protocol") if node_protocol == "_shunt" then - local type = uci:get(c_config, section, "type") - local dns_shunt = uci:get(c_config, "@global[0]", "dns_shunt") + local type = uci_get(section, "type") + local dns_shunt = uci_get("@global[0]", "dns_shunt") local dns_key = (dns_shunt == "smartdns") and "smartdns_dns_mode" or "dns_mode" - local dns_mode = uci:get(c_config, "@global[0]", dns_key) + local dns_mode = uci_get("@global[0]", dns_key) local new_dns_mode = (type == "Xray") and "xray" or "sing-box" if dns_mode ~= new_dns_mode then - uci:set(c_config, "@global[0]", dns_key, new_dns_mode) - uci:set(c_config, "@global[0]", "v2ray_dns_mode", "tcp") + uci_set("@global[0]", dns_key, new_dns_mode) + uci_set("@global[0]", "v2ray_dns_mode", "tcp") end end end - api.uci_save(uci, c_config, true, true) + uci_save(true, true) http.redirect(api.url("log")) end @@ -620,57 +616,57 @@ function copy_node() local section = http.formvalue("section") local uid = api.gen_random_char() uci:section(c_config, "nodes", uid) - for k, v in pairs(uci:get_all(c_config, section)) do + for k, v in pairs(uci_get(section)) do if not k:match("^%.") and k ~= "group" then if k == "remarks" then v = (v or "") .. "(1)" end - uci:set(c_config, uid, k, v) + uci_set(uid, k, v) end end - uci:set(c_config, uid, "add_mode", 1) - api.uci_save(uci, c_config) + uci_set(uid, "add_mode", 1) + uci_save() http.redirect(api.url("node_config", uid)) end function clear_all_nodes() - uci:set(c_config, '@global[0]', "enabled", "0") - uci:set(c_config, '@global[0]', "socks_enabled", "0") - uci:set(c_config, '@global_haproxy[0]', "balancing_enable", "0") - uci:delete(c_config, '@global[0]', "tcp_node") - uci:delete(c_config, '@global[0]', "udp_node") - uci:foreach(c_config, "socks", function(t) - uci:delete(c_config, t[".name"]) - uci:set_list(c_config, t[".name"], "autoswitch_backup_node", {}) + uci_set('@global[0]', "enabled", "0") + uci_set('@global[0]', "socks_enabled", "0") + uci_set('@global_haproxy[0]', "balancing_enable", "0") + uci_del('@global[0]', "tcp_node") + uci_del('@global[0]', "udp_node") + uci_foreach("socks", function(t) + uci_del(t[".name"]) + uci_set(t[".name"], "autoswitch_backup_node", {}) end) - uci:foreach(c_config, "haproxy_config", function(t) - uci:delete(c_config, t[".name"]) + uci_foreach("haproxy_config", function(t) + uci_del(t[".name"]) end) - uci:foreach(appname, "acl_rule", function(t) - uci:delete(c_config, t[".name"], "tcp_node") - uci:delete(c_config, t[".name"], "udp_node") + uci_foreach("acl_rule", function(t) + uci_del(t[".name"], "tcp_node") + uci_del(t[".name"], "udp_node") end) - uci:foreach(c_config, "nodes", function(node) - uci:delete(c_config, node['.name']) + uci_foreach("nodes", function(node) + uci_del(node['.name']) end) - uci:foreach(c_config, "subscribe_list", function(t) - uci:delete(c_config, t[".name"], "md5") - uci:delete(c_config, t[".name"], "chain_proxy") - uci:delete(c_config, t[".name"], "preproxy_node") - uci:delete(c_config, t[".name"], "to_node") + uci_foreach("subscribe_list", function(t) + uci_del(t[".name"], "md5") + uci_del(t[".name"], "chain_proxy") + uci_del(t[".name"], "preproxy_node") + uci_del(t[".name"], "to_node") end) - api.uci_save(uci, c_config, true, true) + uci_save(true, true) end function delete_select_nodes() local ids = http.formvalue("ids") local redirect = http.formvalue("redirect") string.gsub(ids, '[^' .. "," .. ']+', function(w) - uci:foreach(c_config, "socks", function(t) + uci_foreach("socks", function(t) if t["node"] == w then - uci:delete(c_config, t[".name"]) + uci_del(t[".name"]) end local changed = false - local auto_switch_node_list = uci:get(c_config, t[".name"], "autoswitch_backup_node") or {} + local auto_switch_node_list = uci_get(t[".name"], "autoswitch_backup_node") or {} for i = #auto_switch_node_list, 1, -1 do if w == auto_switch_node_list[i] then table.remove(auto_switch_node_list, i) @@ -678,36 +674,36 @@ function delete_select_nodes() end end if changed then - uci:set_list(c_config, t[".name"], "autoswitch_backup_node", auto_switch_node_list) + uci_set(t[".name"], "autoswitch_backup_node", auto_switch_node_list) end end) - if (uci:get(c_config, "@global[0]", "tcp_node") or "") == w then - uci:delete(c_config, '@global[0]', "tcp_node") + if (uci_get("@global[0]", "tcp_node") or "") == w then + uci_del('@global[0]', "tcp_node") end - if (uci:get(c_config, "@global[0]", "udp_node") or "") == w then - uci:delete(c_config, '@global[0]', "udp_node") + if (uci_get("@global[0]", "udp_node") or "") == w then + uci_del('@global[0]', "udp_node") end - uci:foreach(c_config, "haproxy_config", function(t) + uci_foreach("haproxy_config", function(t) if t["lbss"] == w then - uci:delete(c_config, t[".name"]) + uci_del(t[".name"]) end end) - uci:foreach(c_config, "acl_rule", function(t) + uci_foreach("acl_rule", function(t) if t["tcp_node"] == w then - uci:delete(c_config, t[".name"], "tcp_node") + uci_del(t[".name"], "tcp_node") end if t["udp_node"] == w then - uci:delete(c_config, t[".name"], "udp_node") + uci_del(t[".name"], "udp_node") end end) - uci:foreach(c_config, "nodes", function(t) + uci_foreach("nodes", function(t) if t["preproxy_node"] == w then - uci:delete(c_config, t[".name"], "preproxy_node") - uci:delete(c_config, t[".name"], "chain_proxy") + uci_del(t[".name"], "preproxy_node") + uci_del(t[".name"], "chain_proxy") end if t["to_node"] == w then - uci:delete(c_config, t[".name"], "to_node") - uci:delete(c_config, t[".name"], "chain_proxy") + uci_del(t[".name"], "to_node") + uci_del(t[".name"], "chain_proxy") end local list_name = t["urltest_node"] and "urltest_node" or (t["balancing_node"] and "balancing_node") if list_name then @@ -723,48 +719,48 @@ function delete_select_nodes() end end if changed then - uci:set_list(c_config, t[".name"], list_name, new_nodes) + uci_set(t[".name"], list_name, new_nodes) end end end if t["fallback_node"] == w then - uci:delete(c_config, t[".name"], "fallback_node") + uci_del(t[".name"], "fallback_node") end end) - uci:foreach(c_config, "subscribe_list", function(t) + uci_foreach("subscribe_list", function(t) if t["preproxy_node"] == w then - uci:delete(c_config, t[".name"], "preproxy_node") - uci:delete(c_config, t[".name"], "chain_proxy") + uci_del(t[".name"], "preproxy_node") + uci_del(t[".name"], "chain_proxy") end if t["to_node"] == w then - uci:delete(c_config, t[".name"], "to_node") - uci:delete(c_config, t[".name"], "chain_proxy") + uci_del(t[".name"], "to_node") + uci_del(t[".name"], "chain_proxy") end end) - if (uci:get(c_config, w, "add_mode") or "0") == "2" then - local group = uci:get(c_config, w, "group") or "" + if (uci_get(w, "add_mode") or "0") == "2" then + local group = uci_get(w, "group") or "" if group ~= "" then - uci:foreach(c_config, "subscribe_list", function(t) + uci_foreach("subscribe_list", function(t) if t["remark"] == group then - uci:delete(c_config, t[".name"], "md5") + uci_del(t[".name"], "md5") end end) end end - uci:delete(c_config, w) + uci_del(w) end) if redirect == "1" then - api.uci_save(uci, c_config) + uci_save() http.redirect(api.url("node_list")) else - api.uci_save(uci, c_config, true, true) + uci_save(true, true) end end function get_node() local id = http.formvalue("id") local result = {} - local show_node_info = uci:get(c_config, "@global_other[0]", "show_node_info") or "0" + local show_node_info = uci_get("@global_other[0]", "show_node_info") or "0" local function add_is_ipv6_key(o) if o and o.address and show_node_info == "1" then @@ -777,12 +773,12 @@ function get_node() end if id then - result = uci:get_all(c_config, id) + result = uci_get(id) add_is_ipv6_key(result) else local default_nodes = {} local other_nodes = {} - uci:foreach(c_config, "nodes", function(t) + uci_foreach("nodes", function(t) add_is_ipv6_key(t) if not t.group or t.group == "" then default_nodes[#default_nodes + 1] = t @@ -837,8 +833,8 @@ function rollback_rules() return end local bak_dir = "/tmp/bak_v2ray/" - local geo_dir = (uci:get(c_config, "@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/") - local geo2rule = uci:get(c_config, "@global_rules[0]", "geo2rule") or "0" + local geo_dir = (uci_get("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/") + local geo2rule = uci_get("@global_rules[0]", "geo2rule") or "0" fs.move(bak_dir .. arg_type .. ".dat", geo_dir .. arg_type .. ".dat") fs.rmdir(bak_dir) if geo2rule == "1" and rules ~= "" then @@ -854,9 +850,9 @@ function server_update_config() local data_t = jsonParse(data) or {} if next(data_t) then for k, v in pairs(data_t) do - uci:set(c_config .. "_server", id, k, v) + api.uci_set_s(id, k, v) end - api.uci_save(uci, c_config .. "_server") + api.uci_save_s() http_write_json_ok() return end @@ -1034,7 +1030,7 @@ function geo_view() end local function get_rules(str, type) local rules_id = {} - uci:foreach(c_config, "shunt_rules", function(s) + uci_foreach("shunt_rules", function(s) local list if type == "geoip" then list = s.ip_list else list = s.domain_list end for line in string.gmatch((list or ""), "[^\r\n]+") do @@ -1051,7 +1047,7 @@ function geo_view() end) return rules_id end - local geo_dir = (uci:get(c_config, "@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"):match("^(.*)/") + local geo_dir = (uci_get("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"):match("^(.*)/") local geosite_path = geo_dir .. "/geosite.dat" local geoip_path = geo_dir .. "/geoip.dat" local geo_type, file_path, cmd @@ -1168,8 +1164,8 @@ function flush_set() local redirect = http.formvalue("redirect") or "0" local reload = http.formvalue("reload") or "0" if reload == "1" then - uci:set(c_config, '@global[0]', "flush_set", "1") - api.uci_save(uci, c_config, true, true) + uci_set('@global[0]', "flush_set", "1") + uci_save(true, true) else api.sh_uci_set(c_config, "@global[0]", "flush_set", "1", true) end @@ -1180,17 +1176,17 @@ end function fetch_certsha256() local id = http.formvalue("id") or "" - local address = (id ~= "") and uci:get(c_config, id, "address") or "" - local port = (id ~= "") and uci:get(c_config, id, "port") or 0 - local sni = (id ~= "") and uci:get(c_config, id, "tls_serverName") or "" + local address = (id ~= "") and uci_get(id, "address") or "" + local port = (id ~= "") and uci_get(id, "port") or 0 + local sni = (id ~= "") and uci_get(id, "tls_serverName") or "" sni = (sni ~= "") and sni or address - local protocol = uci:get(appname, id, "protocol") + local protocol = uci_get(id, "protocol") local h3, timeout = false, 10 if protocol == "hysteria2" then h3 = true timeout = 60 if port == 0 then - local hop = uci:get(appname, id, "hysteria2_hop") or "0" + local hop = uci_get(id, "hysteria2_hop") or "0" port = tonumber(hop:match("^%s*(%d+)")) end end @@ -1207,11 +1203,11 @@ function get_shunt_rules() local result = {} if id then - result = uci:get_all(c_config, id) + result = uci_get(id) else local default_items = {} local other_items = {} - uci:foreach(c_config, "shunt_rules", function(t) + uci_foreach("shunt_rules", function(t) if not t.group or t.group == "" then default_items[#default_items + 1] = t else @@ -1230,7 +1226,7 @@ function add_shunt_rule() local uid = add_name if add_name then - local has = uci:get(c_config, uid) + local has = uci_get(uid) if has then http_write_json_error({ message = i18n.translate("This ID already exists.") }) return @@ -1242,14 +1238,14 @@ function add_shunt_rule() local group = http.formvalue("group") if group and group ~= "default" then - uci:set(c_config, uid, "group", group) + uci_set(uid, "group", group) end if redirect == "1" then - api.uci_save(uci, c_config) + uci_save() http.redirect(api.url("shunt_rules", uid)) else - api.uci_save(uci, c_config) + uci_save() http_write_json_ok({uid = uid, redirect_url = api.url("shunt_rules", uid)}) end end @@ -1258,17 +1254,17 @@ function delete_select_shunt_rules() local ids = http.formvalue("ids") local redirect = http.formvalue("redirect") string.gsub(ids, '[^' .. "," .. ']+', function(w) - uci:foreach(c_config, "nodes", function(s) + uci_foreach("nodes", function(s) if s["protocol"] and s["protocol"] == "_shunt" then - uci:delete(c_config, s[".name"], w) + uci_del(s[".name"], w) end end) - uci:delete(c_config, w) + uci_del(w) end) if redirect == "1" then - api.uci_save(uci, c_config) + uci_save() http.redirect(api.url("rule")) else - api.uci_save(uci, c_config, true, true) + uci_save(true, true) end end diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua index a9e220b9..fbbcbbe6 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua @@ -740,7 +740,7 @@ function s2.remove(e, t) m:foreach("nodes", function(s) local list_name = s["urltest_node"] and "urltest_node" or (s["balancing_node"] and "balancing_node") if list_name then - local nodes = m.uci:get_list(appname, s[".name"], list_name) + local nodes = m.uci:get_list(api.c_config, s[".name"], list_name) if nodes then local changed = false local new_nodes = {} @@ -752,7 +752,7 @@ function s2.remove(e, t) end end if changed then - m.uci:set_list(appname, s[".name"], list_name, new_nodes) + api.uci_set_c(s[".name"], list_name, new_nodes) end end end diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/rule_list.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/rule_list.lua index d517135b..aeaca78a 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/rule_list.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/rule_list.lua @@ -319,7 +319,7 @@ end m:appendTemplate("/rule_list/js") -local geo_dir = (uci:get(api.c_config, "@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"):match("^(.*)/") +local geo_dir = (api.uci_get_c("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"):match("^(.*)/") local geosite_path = geo_dir .. "/geosite.dat" local geoip_path = geo_dir .. "/geoip.dat" if api.finded_com("geoview") and fs.access(geosite_path) and fs.access(geoip_path) then diff --git a/luci-app-passwall/luasrc/passwall/api.lua b/luci-app-passwall/luasrc/passwall/api.lua index 04ba433b..02d81ef2 100644 --- a/luci-app-passwall/luasrc/passwall/api.lua +++ b/luci-app-passwall/luasrc/passwall/api.lua @@ -19,9 +19,10 @@ OPENWRT_ARCH = nil DISTRIB_ARCH = nil OPENWRT_BOARD = nil -CACHE_PATH = "/tmp/etc/" .. appname .. "_tmp" -LOG_FILE = "/tmp/log/" .. appname .. ".log" -TMP_PATH = "/tmp/etc/" .. appname +LOCK_PREFIX = "/tmp/lock/passwall" +LOG_FILE = "/tmp/log/passwall.log" +TMP_PATH = "/tmp/etc/passwall" +CACHE_PATH = TMP_PATH .. "_tmp" TMP_IFACE_PATH = TMP_PATH .. "/iface" function log(...) @@ -37,7 +38,85 @@ function is_old_uci() return sys.call("grep -E 'require[ \t]*\"uci\"' /usr/lib/lua/luci/model/uci.lua >/dev/null 2>&1") == 0 end +function uci_del(config, section, option) + if option then + return uci:delete(config, section, option) + else + return uci:delete(config, section) + end +end + +function uci_get(config, section, option) + if not section then + return uci:get_all(config) + elseif option then + return uci:get(config, section, option) or nil + else + return uci:get_all(config, section) + end +end + +function uci_set(config, section, option, value) + if type(value) == "number" then + value = value .. "" + end + if #value > 0 then + if option then + if type(value) == "table" then + return uci:set_list(config, section, option, value) + else + return uci:set(config, section, option, value) + end + else + return uci:set(config, section, value) + end + else + return uci_del(config, section, option) + end +end + +function uci_del_c(section, option) + return uci_del(c_config, section, option) +end + +function uci_foreach_c(stype, func) + uci:foreach(c_config, stype, func) +end + +function uci_get_c(section, option) + return uci_get(c_config, section, option) +end + +function uci_set_c(section, option, value) + return uci_set(c_config, section, option, value) +end + +function uci_save_c(commit, apply) + return uci_save(uci, c_config, commit, apply) +end + +function uci_del_s(section, option) + return uci_del(s_config, section, option) +end + +function uci_foreach_s(stype, func) + uci:foreach(s_config, stype, func) +end + +function uci_get_s(section, option) + return uci_get(s_config, section, option) +end + +function uci_set_s(section, option, value) + return uci_set(s_config, section, option, value) +end + +function uci_save_s(commit, apply) + return uci_save(uci, s_config, commit, apply) +end + function uci_save(cursor, config, commit, apply) + if not cursor then cursor = uci end if is_old_uci() then cursor:save(config) if commit then @@ -198,7 +277,7 @@ end function curl_direct(url, file, args) --直连访问 - local chn_list = uci:get(appname, "@global[0]", "chn_list") or "direct" + local chn_list = uci_get_c("@global[0]", "chn_list") or "direct" local Dns = (chn_list == "proxy") and "1.1.1.1" or "223.5.5.5" if not args then args = {} end local tmp_args = clone(args) @@ -213,7 +292,7 @@ function curl_direct(url, file, args) end function curl_auto(url, file, args) - local localhost_proxy = uci:get(c_config, "@global[0]", "localhost_proxy") or "1" + local localhost_proxy = uci_get_c("@global[0]", "localhost_proxy") or "1" if localhost_proxy == "1" then return curl_base(url, file, args) -- 当路由器本机开启代理时,采用passwall规则进行访问 else @@ -481,7 +560,7 @@ function get_domain_from_url(url) end function get_valid_nodes() - local show_node_info = uci:get(c_config, "@global_other[0]", "show_node_info") or "0" + local show_node_info = uci_get_c("@global_other[0]", "show_node_info") or "0" local nodes = {} local default_nodes = {} local other_nodes = {} @@ -666,7 +745,7 @@ function chmod_755(file) end function get_customed_path(e) - return uci:get(c_config, "@global_app[0]", e .. "_file") + return uci_get_c("@global_app[0]", e .. "_file") end function finded_com(e) @@ -725,7 +804,7 @@ end function get_app_path(app_name) if com[app_name] then local def_path = com[app_name].default_path - local path = uci:get(c_config, "@global_app[0]", app_name:gsub("%-","_") .. "_file") + local path = uci_get_c("@global_app[0]", app_name:gsub("%-","_") .. "_file") path = path and (#path>0 and path or def_path) or def_path return path end @@ -1003,7 +1082,7 @@ local default_file_tree = { } local function get_api_json(url) - local gh_proxy = uci:get(c_config, "@global_app[0]", "github_proxy") or "0" + local gh_proxy = uci_get_c("@global_app[0]", "github_proxy") or "0" local return_code, content if gh_proxy == "1" then url = "https://gh-proxy.org/" .. url @@ -1127,7 +1206,7 @@ function to_download(app_name, url, size) local _curl_args = clone(curl_args) table.insert(_curl_args, "--speed-limit 51200 --speed-time 15 --max-time 300") - local gh_proxy = uci:get(c_config, "@global_app[0]", "github_proxy") or "0" + local gh_proxy = uci_get_c("@global_app[0]", "github_proxy") or "0" local return_code, result if gh_proxy == "1" then url = "https://gh-proxy.org/" .. url @@ -1285,7 +1364,7 @@ end function to_check_self() local url = "https://raw.githubusercontent.com/Openwrt-Passwall/openwrt-passwall/main/luci-app-passwall/Makefile" local tmp_file = "/tmp/passwall_makefile" - local gh_proxy = uci:get(c_config, "@global_app[0]", "github_proxy") or "0" + local gh_proxy = uci_get_c("@global_app[0]", "github_proxy") or "0" local return_code, result if gh_proxy == "1" then url = "https://gh-proxy.org/" .. url @@ -1741,7 +1820,7 @@ end function get_socks_backup_nodes(id) id = trim(id) if id == "" then return "" end - local socks = uci:get_all(c_config, id) + local socks = uci_get_c(id) local nodes if socks.backup_node_add_mode and socks.backup_node_add_mode == "batch" then local node = {} @@ -1762,7 +1841,7 @@ function get_socks_backup_nodes(id) end function get_core(field, candidates) - local v = uci:get(c_config, "@global_subscribe[0]", field) + local v = uci_get_c("@global_subscribe[0]", field) if v and v ~= "" then for _, c in ipairs(candidates) do if c[2] == v and c[1] then diff --git a/luci-app-passwall/luasrc/passwall/server_app.lua b/luci-app-passwall/luasrc/passwall/server_app.lua index e9372a78..962d389b 100644 --- a/luci-app-passwall/luasrc/passwall/server_app.lua +++ b/luci-app-passwall/luasrc/passwall/server_app.lua @@ -84,7 +84,7 @@ local function gen_include() end local function start() - local enabled = tonumber(uci:get(CONFIG, "@global[0]", "enable") or 0) + local enabled = tonumber(api.uci_get_s("@global[0]", "enable") or 0) if enabled == nil or enabled == 0 then return end @@ -102,7 +102,7 @@ local function start() nft_file:write('flush chain inet fw4 PSW-SERVER\n') nft_file:write('insert rule inet fw4 input position 0 jump PSW-SERVER comment "PSW-SERVER"\n') end - uci:foreach(CONFIG, "server", function(server) + api.uci_foreach_s("server", function(server) local id = server[".name"] local enable = server.enable if enable and tonumber(enable) == 1 then @@ -125,7 +125,7 @@ local function start() if server.auth and server.auth == "1" then local user = nil if server.user then - user = uci:get_all("passwall_server", server.user) + user = api.uci_get_s(server.user) end local username = user and user.username or "" local password = user and user.password or "" diff --git a/luci-app-passwall/luasrc/passwall/util_hysteria2.lua b/luci-app-passwall/luasrc/passwall/util_hysteria2.lua index e0495978..e61b72e0 100644 --- a/luci-app-passwall/luasrc/passwall/util_hysteria2.lua +++ b/luci-app-passwall/luasrc/passwall/util_hysteria2.lua @@ -1,15 +1,13 @@ module("luci.passwall.util_hysteria2", package.seeall) local api = require "luci.passwall.api" -local uci = api.uci local jsonc = api.jsonc function gen_config_server(node) - local users = node.users or {} local users = nil if node.users and #node.users > 0 then users = {} for i, v in ipairs(node.users) do - local user = uci:get_all(api.s_config, v) or {} + local user = api.uci_get_s(v) or {} if user[".type"] == "user" then users[user.username] = user.password end @@ -74,7 +72,7 @@ function gen_config(var) print("node 不能为空") return end - local node = uci:get_all(api.c_config, node_id) + local node = api.uci_get_c(node_id) local local_tcp_redir_port = var["local_tcp_redir_port"] local local_udp_redir_port = var["local_udp_redir_port"] local local_socks_address = var["local_socks_address"] or "0.0.0.0" diff --git a/luci-app-passwall/luasrc/passwall/util_naiveproxy.lua b/luci-app-passwall/luasrc/passwall/util_naiveproxy.lua index 49ca7acd..a29cfafe 100644 --- a/luci-app-passwall/luasrc/passwall/util_naiveproxy.lua +++ b/luci-app-passwall/luasrc/passwall/util_naiveproxy.lua @@ -1,6 +1,5 @@ module("luci.passwall.util_naiveproxy", package.seeall) local api = require "luci.passwall.api" -local uci = api.uci local jsonc = api.jsonc function gen_config(var) @@ -9,7 +8,7 @@ function gen_config(var) print("node 不能为空") return end - local node = uci:get_all(api.c_config, node_id) + local node = api.uci_get_c(node_id) local run_type = var["run_type"] local local_addr = var["local_addr"] local local_port = var["local_port"] diff --git a/luci-app-passwall/luasrc/passwall/util_shadowsocks.lua b/luci-app-passwall/luasrc/passwall/util_shadowsocks.lua index b22d914c..9e618a76 100644 --- a/luci-app-passwall/luasrc/passwall/util_shadowsocks.lua +++ b/luci-app-passwall/luasrc/passwall/util_shadowsocks.lua @@ -1,12 +1,11 @@ module("luci.passwall.util_shadowsocks", package.seeall) local api = require "luci.passwall.api" -local uci = api.uci local jsonc = api.jsonc function gen_config_server(node) local user = nil if node.user then - user = uci:get_all(api.s_config, node.user) + user = api.uci_get_s(node.user) end local config = {} config.server_port = tonumber(node.port) @@ -40,7 +39,7 @@ function gen_config(var) print("node 不能为空") return end - local node = uci:get_all(api.c_config, node_id) + local node = api.uci_get_c(node_id) local server_host = var["server_host"] or (node.address or ""):lower() local server_port = var["server_port"] or node.port local local_addr = var["local_addr"] diff --git a/luci-app-passwall/luasrc/passwall/util_sing-box.lua b/luci-app-passwall/luasrc/passwall/util_sing-box.lua index a73845b4..34e310c0 100644 --- a/luci-app-passwall/luasrc/passwall/util_sing-box.lua +++ b/luci-app-passwall/luasrc/passwall/util_sing-box.lua @@ -1,10 +1,9 @@ module("luci.passwall.util_sing-box", package.seeall) local api = require "luci.passwall.api" -local uci = api.uci local sys = api.sys local jsonc = api.jsonc -local appname = api.appname local fs = api.fs +local CACHE_PATH = api.CACHE_PATH local ech_domain = {} local local_version = api.get_app_version("sing-box"):match("[^v]+") @@ -23,7 +22,7 @@ local GEO_VAR = { IP_PATH = nil, SITE_TAGS = {}, IP_TAGS = {}, - TO_SRS_PATH = "/tmp/etc/" .. appname .."_tmp/singbox_srss/" + TO_SRS_PATH = CACHE_PATH .."/singbox_srss/" } function check_geoview() @@ -34,7 +33,7 @@ function check_geoview() if GEO_VAR.OK == 0 then api.log("!!!注意:缺少 Geoview 组件或版本过低,Sing-Box 分流无法启用!") else - GEO_VAR.DIR = GEO_VAR.DIR or (uci:get(api.c_config, "@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"):match("^(.*)/") + GEO_VAR.DIR = GEO_VAR.DIR or (api.uci_get_c("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"):match("^(.*)/") GEO_VAR.SITE_PATH = GEO_VAR.SITE_PATH or (GEO_VAR.DIR .. "/geosite.dat") GEO_VAR.IP_PATH = GEO_VAR.IP_PATH or (GEO_VAR.DIR .. "/geoip.dat") if not fs.access(GEO_VAR.TO_SRS_PATH) then @@ -120,8 +119,7 @@ function gen_outbound(flag, node, tag, proxy_table) config_file = string.format("%s_%s_%s_%s.json", flag, tag, node_id, new_port) end if run_socks_instance then - sys.call(string.format('/usr/share/%s/app.sh run_socks "%s"> /dev/null', - appname, + sys.call(string.format('/usr/share/passwall/app.sh run_socks "%s"> /dev/null', string.format("flag=%s node=%s bind=%s socks_port=%s config_file=%s relay_port=%s", new_port, --flag node_id, --node @@ -771,7 +769,7 @@ function gen_config_server(node) if node.users and #node.users > 0 then users = {} for i, v in ipairs(node.users) do - local user = uci:get_all(api.s_config, v) or {} + local user = api.uci_get_s(v) or {} if user[".type"] == "user" then local u = {} if node.protocol == "mixed" or node.protocol == "socks" or node.protocol == "http" or node.protocol == "naive" then @@ -1008,7 +1006,7 @@ function gen_config_server(node) } sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, node.outbound_node_iface)) else - local outbound_node_t = uci:get_all(api.c_config, node.outbound_node) + local outbound_node_t = api.uci_get_c(node.outbound_node) if node.outbound_node == "_socks" or node.outbound_node == "_http" then outbound_node_t = { type = node.type, @@ -1104,7 +1102,7 @@ function gen_config(var) local rule_set_table = {} local COMMON = {} - local singbox_settings = uci:get_all(api.c_config, "@global_singbox[0]") or {} + local singbox_settings = api.uci_get_c("@global_singbox[0]") or {} local route = { rules = {} @@ -1166,7 +1164,7 @@ function gen_config(var) end if node_id then - local node = uci:get_all(api.c_config, node_id) + local node = api.uci_get_c(node_id) if node then if server_host and server_port then node.address = server_host @@ -1256,7 +1254,7 @@ function gen_config(var) function get_node_by_id(node_id) if not node_id or node_id == "" or node_id == "nil" then return nil end - local section = uci:get_all(api.c_config, node_id) or {} + local section = api.uci_get_c(node_id) or {} if section[".type"] == "socks" then local result = { [".name"] = node_id, @@ -1533,7 +1531,7 @@ function gen_config(var) --shunt rule local function foreach_shunt_rule(callback) - uci:foreach(api.c_config, "shunt_rules", callback) + api.uci_foreach_c("shunt_rules", callback) if use_gfw_list ~= "1" or chn_list ~= "0" then return end @@ -1559,7 +1557,7 @@ function gen_config(var) local bin = api.finded_com("geoview") if bin then - local geo_file = (uci:get(api.c_config, "@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"):match("^(.*)/") .. "/geosite.dat" + local geo_file = (api.uci_get_c("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"):match("^(.*)/") .. "/geosite.dat" if luci.sys.call('"' .. bin .. '" -type geosite -input "' .. geo_file .. '" | grep -q "^GFW$"') == 0 then domain_list = (domain_list == "") and "geosite:gfw" or domain_list .. "\ngeosite:gfw" end @@ -1975,7 +1973,7 @@ function gen_config(var) end end dns.final = default_dns_flag - dns.strategy = default_dns_flag == "remote" and remote_strategy or direct_strategy + dns.strategy = "prefer_ipv6" --按分流顺序DNS if dns_domain_rules and #dns_domain_rules > 0 then diff --git a/luci-app-passwall/luasrc/passwall/util_xray.lua b/luci-app-passwall/luasrc/passwall/util_xray.lua index b7091185..08bbb167 100644 --- a/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -1,9 +1,7 @@ module("luci.passwall.util_xray", package.seeall) local api = require "luci.passwall.api" -local uci = api.uci local sys = api.sys local jsonc = api.jsonc -local appname = api.appname local fs = api.fs local GLOBAL = { @@ -17,7 +15,7 @@ local xray_version = api.get_app_version("xray") local xray_min_version = "26.3.27" local function get_domain_excluded() - local path = string.format("/usr/share/%s/rules/domains_excluded", appname) + local path = "/usr/share/passwall/domains_excluded" local content = fs.readfile(path) if not content then return nil end local hosts = {} @@ -68,8 +66,7 @@ function gen_outbound(flag, node, tag, proxy_table) config_file = string.format("%s_%s_%s_%s.json", flag, tag, node_id, new_port) end if run_socks_instance then - sys.call(string.format('/usr/share/%s/app.sh run_socks "%s"> /dev/null', - appname, + sys.call(string.format('/usr/share/passwall/app.sh run_socks "%s"> /dev/null', string.format("flag=%s node=%s bind=%s socks_port=%s config_file=%s relay_port=%s", new_port, --flag node_id, --node @@ -493,7 +490,7 @@ function gen_config_server(node) if node.users and #node.users > 0 then users = {} for i, v in ipairs(node.users) do - local user = uci:get_all(api.s_config, v) or {} + local user = api.uci_get_s(v) or {} if user[".type"] == "user" then local u = {} if node.protocol == "socks" or node.protocol == "http" then @@ -625,7 +622,7 @@ function gen_config_server(node) } sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, node.outbound_node_iface)) else - local outbound_node_t = uci:get_all(api.c_config, node.outbound_node) + local outbound_node_t = api.uci_get_c(node.outbound_node) if node.outbound_node == "_socks" or node.outbound_node == "_http" then outbound_node_t = { type = node.type, @@ -901,7 +898,7 @@ function gen_config(var) local outbounds = {} local COMMON = {} - local xray_settings = uci:get_all(api.c_config, "@global_xray[0]") or {} + local xray_settings = api.uci_get_c("@global_xray[0]") or {} if xray_settings.fragment == "1" then local lengths, delays = {}, {} @@ -926,7 +923,7 @@ function gen_config(var) if xray_settings.noise == "1" then local noises = {} - uci:foreach(api.c_config, "xray_noise_packets", function(n) + api.uci_foreach_c("xray_noise_packets", function(n) if n.enabled == "1" then local noise = { rand = (n.type == "rand" and n.packet) and (n.packet:find("-", 1, true) and n.packet or tonumber(n.packet)) or nil, @@ -944,7 +941,7 @@ function gen_config(var) end if node_id then - local node = uci:get_all(api.c_config, node_id) + local node = api.uci_get_c(node_id) local balancers = {} local rules = {} if node then @@ -1001,7 +998,7 @@ function gen_config(var) function get_node_by_id(node_id) if not node_id or node_id == "" or node_id == "nil" then return nil end - local section = uci:get_all(api.c_config, node_id) or {} + local section = api.uci_get_c(node_id) or {} if section[".type"] == "socks" then local result = { [".name"] = node_id, @@ -1374,7 +1371,7 @@ function gen_config(var) --shunt rule local function foreach_shunt_rule(callback) - uci:foreach(api.c_config, "shunt_rules", callback) + api.uci_foreach_c("shunt_rules", callback) if use_gfw_list ~= "1" or chn_list ~= "0" then return end @@ -1400,7 +1397,7 @@ function gen_config(var) local bin = api.finded_com("geoview") if bin then - local geo_file = (uci:get(api.c_config, "@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"):match("^(.*)/") .. "/geosite.dat" + local geo_file = (api.uci_get_c("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"):match("^(.*)/") .. "/geosite.dat" if luci.sys.call('"' .. bin .. '" -type geosite -input "' .. geo_file .. '" | grep -q "^GFW$"') == 0 then domain_list = (domain_list == "") and "geosite:gfw" or domain_list .. "\ngeosite:gfw" end @@ -1788,7 +1785,7 @@ function gen_config(var) }) -- remote dns outbound - local chn_list = uci:get(api.c_config, "@global[0]", "chn_list") or "direct" + local chn_list = api.uci_get_c("@global[0]", "chn_list") or "direct" remote_dns_outbound = { tag = "dns-out", protocol = "dns", @@ -1979,11 +1976,11 @@ function gen_config(var) if inbounds or outbounds then local config = { env = (function() - local asset_location = uci:get(api.c_config, "@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/" + local asset_location = api.uci_get_c("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/" return { XRAY_LOCATION_ASSET = asset_location } end)(), log = { - -- error = string.format("/tmp/etc/%s/%s.log", appname, node[".name"]), + -- error = string.format("/tmp/etc/passwall/%s.log", node[".name"]), loglevel = get_log_level(loglevel) }, -- DNS diff --git a/luci-app-passwall/root/usr/share/passwall/haproxy.lua b/luci-app-passwall/root/usr/share/passwall/haproxy.lua index 3c14af32..521a0a91 100644 --- a/luci-app-passwall/root/usr/share/passwall/haproxy.lua +++ b/luci-app-passwall/root/usr/share/passwall/haproxy.lua @@ -3,7 +3,6 @@ local api = require ("luci.passwall.api") local appname = api.appname local fs = api.fs -local jsonc = api.jsonc local uci = api.uci local sys = api.sys @@ -23,11 +22,11 @@ local haproxy_conf = var["-conf"] local haproxy_dns = var["-dns"] or "119.29.29.29:53,223.5.5.5:53" local cpu_thread = sys.exec('echo -n $(cat /proc/cpuinfo | grep "processor" | wc -l)') or "1" -local health_check_type = uci:get(api.c_config, "@global_haproxy[0]", "health_check_type") or "tcp" -local health_check_inter = uci:get(api.c_config, "@global_haproxy[0]", "health_check_inter") or "20" -local balancingStrategy = uci:get(api.c_config, "@global_haproxy[0]", "balancingStrategy") or "roundrobin" -local console_port = uci:get(api.c_config, "@global_haproxy[0]", "console_port") -local bind_local = uci:get(api.c_config, "@global_haproxy[0]", "bind_local") or "0" +local health_check_type = api.uci_get_c("@global_haproxy[0]", "health_check_type") or "tcp" +local health_check_inter = api.uci_get_c("@global_haproxy[0]", "health_check_inter") or "20" +local balancingStrategy = api.uci_get_c("@global_haproxy[0]", "balancingStrategy") or "roundrobin" +local console_port = api.uci_get_c("@global_haproxy[0]", "console_port") +local bind_local = api.uci_get_c("@global_haproxy[0]", "bind_local") or "0" local bind_address = "0.0.0.0" if bind_local == "1" then bind_address = "127.0.0.1" end @@ -94,14 +93,14 @@ f_out:write(haproxy_config) local listens = {} -uci:foreach(api.c_config, "haproxy_config", function(t) +api.uci_foreach_c("haproxy_config", function(t) if t.enabled == "1" then local server_remark local server_address local server_port local lbss = t.lbss local listen_port = tonumber(t.haproxy_port) or 0 - local server_node = uci:get_all(api.c_config, lbss) + local server_node = api.uci_get_c(lbss) local hop = (health_check_type == "script_logic") and (server_node.hysteria_hop or server_node.hysteria2_hop) or nil hop = hop and hop:gsub(":", "-") or nil if server_node and server_node.address and (server_node.port or hop) then @@ -214,8 +213,8 @@ listen %s end --控制台配置 -local console_user = uci:get(api.c_config, "@global_haproxy[0]", "console_user") -local console_password = uci:get(api.c_config, "@global_haproxy[0]", "console_password") +local console_user = api.uci_get_c("@global_haproxy[0]", "console_user") +local console_password = api.uci_get_c("@global_haproxy[0]", "console_password") local str = [[ listen console bind 0.0.0.0:%s @@ -231,7 +230,7 @@ f_out:close() --passwall内置健康检查URL if health_check_type == "script_logic" then - local probeUrl = uci:get(api.c_config, "@global_haproxy[0]", "health_probe_url") or "https://www.google.com/generate_204" + local probeUrl = api.uci_get_c("@global_haproxy[0]", "health_probe_url") or "https://www.google.com/generate_204" local f_url = io.open(haproxy_path .. "/Probe_URL", "w") f_url:write(probeUrl) f_url:close() diff --git a/luci-app-passwall/root/usr/share/passwall/helper_chinadns_add.lua b/luci-app-passwall/root/usr/share/passwall/helper_chinadns_add.lua index 64873d08..32e557e6 100644 --- a/luci-app-passwall/root/usr/share/passwall/helper_chinadns_add.lua +++ b/luci-app-passwall/root/usr/share/passwall/helper_chinadns_add.lua @@ -1,5 +1,4 @@ local api = require "luci.passwall.api" -local appname = api.appname local var = api.get_args(arg) local FLAG = var["-FLAG"] @@ -20,19 +19,18 @@ local NFTFLAG = var["-NFTFLAG"] local FILTER_HTTPS = var["-FILTER_HTTPS"] local LOG_FILE = var["-LOG_FILE"] -local uci = api.uci local sys = api.sys local fs = api.fs local datatypes = api.datatypes -local TMP_PATH = "/tmp/etc/" .. appname +local TMP_PATH = api.TMP_PATH local TMP_ACL_PATH = TMP_PATH .. "/acl" -local RULES_PATH = "/usr/share/" .. appname .. "/rules" +local RULES_PATH = "/usr/share/passwall/rules" local FLAG_PATH = TMP_ACL_PATH .. "/" .. FLAG local config_lines = {} local tmp_lines = {} -local USE_GEOVIEW = uci:get(api.c_config, "@global_rules[0]", "enable_geoview") -local IS_SHUNT_NODE = uci:get(api.c_config, TCP_NODE, "protocol") == "_shunt" +local USE_GEOVIEW = api.uci_get_c("@global_rules[0]", "enable_geoview") +local IS_SHUNT_NODE = api.uci_get_c(TCP_NODE, "protocol") == "_shunt" if not api.is_finded("geoview") then USE_GEOVIEW = "0" @@ -92,7 +90,7 @@ local function insert_array_after(array1, array2, target) --将array2插入到ar end local function get_geosite(list_arg, out_path) - local geosite_path = uci:get(api.c_config, "@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/" + local geosite_path = api.uci_get_c("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/" geosite_path = geosite_path:match("^(.*)/") .. "/geosite.dat" if not is_file_nonzero(geosite_path) then return 1 end local bin = api.finded_com("geoview") @@ -183,7 +181,7 @@ if not is_file_nonzero(file_vpslist) then written_domains[address] = true end end - uci:foreach(api.c_config, "nodes", function(t) + api.uci_foreach_c("nodes", function(t) process_address(t.address) process_address(t.download_address) local dns, _ = api.get_domain_port_from_url(t.domain_resolver_dns or t.domain_resolver_dns_https or "") @@ -191,7 +189,7 @@ if not is_file_nonzero(file_vpslist) then process_address(dns) end end) - uci:foreach(api.c_config, "subscribe_list", function(t) --订阅链接 + api.uci_foreach_c("subscribe_list", function(t) --订阅链接 local url, _ = api.get_domain_port_from_url(t.url or "") if url and url ~= "" then process_address(url) @@ -387,9 +385,9 @@ if IS_SHUNT_NODE then local file_shunt_host = FLAG_PATH .. "/shunt_proxy_host" local geosite_white_arg, geosite_shunt_arg = "", "" - local t = uci:get_all(api.c_config, TCP_NODE) + local t = api.uci_get_c(TCP_NODE) local default_node_id = t["default_node"] or "_direct" - uci:foreach(api.c_config, "shunt_rules", function(s) + api.uci_foreach_c("shunt_rules", function(s) local _node_id = t[s[".name"]] if _node_id and _node_id ~= "_blackhole" and t["shunt_group"] == s.group then if _node_id == "_default" then diff --git a/luci-app-passwall/root/usr/share/passwall/helper_dnsmasq.lua b/luci-app-passwall/root/usr/share/passwall/helper_dnsmasq.lua index 02c77f91..770a772f 100644 --- a/luci-app-passwall/root/usr/share/passwall/helper_dnsmasq.lua +++ b/luci-app-passwall/root/usr/share/passwall/helper_dnsmasq.lua @@ -1,6 +1,4 @@ local api = require "luci.passwall.api" -local appname = "passwall" -local uci = api.uci local sys = api.sys local fs = api.fs local datatypes = api.datatypes @@ -22,38 +20,38 @@ local function tinsert(table_name, val) end local function backup_servers() - local DNSMASQ_DNS = uci:get("dhcp", "@dnsmasq[0]", "server") + local DNSMASQ_DNS = api.uci_get("dhcp", "@dnsmasq[0]", "server") if DNSMASQ_DNS and #DNSMASQ_DNS > 0 then - uci:set(api.c_config, "@global[0]", "dnsmasq_servers", DNSMASQ_DNS) - api.uci_save(uci, api.c_config, true) + api.uci_set_c("@global[0]", "dnsmasq_servers", DNSMASQ_DNS) + api.uci_save_c(true) end end local function restore_servers() local dns_table = {} - local DNSMASQ_DNS = uci:get("dhcp", "@dnsmasq[0]", "server") + local DNSMASQ_DNS = api.uci_get("dhcp", "@dnsmasq[0]", "server") if DNSMASQ_DNS and #DNSMASQ_DNS > 0 then for k, v in ipairs(DNSMASQ_DNS) do tinsert(dns_table, v) end end - local OLD_SERVER = uci:get(api.c_config, "@global[0]", "dnsmasq_servers") + local OLD_SERVER = api.uci_get(api.c_config, "@global[0]", "dnsmasq_servers") if OLD_SERVER and #OLD_SERVER > 0 then for k, v in ipairs(OLD_SERVER) do tinsert(dns_table, v) end - uci:delete(api.c_config, "@global[0]", "dnsmasq_servers") - api.uci_save(uci, api.c_config, true) + api.uci_del_c("@global[0]", "dnsmasq_servers") + api.uci_save_c(true) end if dns_table and #dns_table > 0 then - uci:set_list("dhcp", "@dnsmasq[0]", "server", dns_table) - api.uci_save(uci, "dhcp", true) + api.uci_set("dhcp", "@dnsmasq[0]", "server", dns_table) + api.uci_save(nil, "dhcp", true) end end function stretch() - local dnsmasq_server = uci:get("dhcp", "@dnsmasq[0]", "server") - local dnsmasq_noresolv = uci:get("dhcp", "@dnsmasq[0]", "noresolv") + local dnsmasq_server = api.uci_get("dhcp", "@dnsmasq[0]", "server") + local dnsmasq_noresolv = api.uci_get("dhcp", "@dnsmasq[0]", "noresolv") local _flag if dnsmasq_server and #dnsmasq_server > 0 then for k, v in ipairs(dnsmasq_server) do @@ -63,7 +61,7 @@ function stretch() end end if not _flag and dnsmasq_noresolv == "1" then - uci:delete("dhcp", "@dnsmasq[0]", "noresolv") + api.uci_del("dhcp", "@dnsmasq[0]", "noresolv") local RESOLVFILE = "/tmp/resolv.conf.d/resolv.conf.auto" local file = io.open(RESOLVFILE, "r") if not file then @@ -75,8 +73,8 @@ function stretch() RESOLVFILE = "/tmp/resolv.conf.auto" end end - uci:set("dhcp", "@dnsmasq[0]", "resolvfile", RESOLVFILE) - api.uci_save(uci, "dhcp", true) + api.uci_set("dhcp", "@dnsmasq[0]", "resolvfile", RESOLVFILE) + api.uci_save(nil, "dhcp", true) end end @@ -95,15 +93,15 @@ function logic_restart(var) backup_servers() --sys.call("sed -i '/list server/d' /etc/config/dhcp >/dev/null 2>&1") local dns_table = {} - local dnsmasq_server = uci:get("dhcp", "@dnsmasq[0]", "server") + local dnsmasq_server = api.uci_get("dhcp", "@dnsmasq[0]", "server") if dnsmasq_server and #dnsmasq_server > 0 then for k, v in ipairs(dnsmasq_server) do if v:find("/") then tinsert(dns_table, v) end end - uci:set_list("dhcp", "@dnsmasq[0]", "server", dns_table) - api.uci_save(uci, "dhcp", true) + api.uci_set("dhcp", "@dnsmasq[0]", "server", dns_table) + api.uci_save(nil, "dhcp", true) end sys.call("/etc/init.d/dnsmasq restart >/dev/null 2>&1") restore_servers() @@ -180,8 +178,8 @@ function add_rule(var) local CACHE_DNS_PATH = CACHE_PATH .. "/" .. CACHE_FLAG local CACHE_TEXT_FILE = CACHE_DNS_PATH .. ".txt" local USE_CHINADNS_NG = "0" - local IS_SHUNT_NODE = uci:get(api.c_config, TCP_NODE, "protocol") == "_shunt" - local USE_GEOVIEW = uci:get(api.c_config, "@global_rules[0]", "enable_geoview") + local IS_SHUNT_NODE = api.uci_get_c(TCP_NODE, "protocol") == "_shunt" + local USE_GEOVIEW = api.uci_get_c("@global_rules[0]", "enable_geoview") local list1 = {} local excluded_domain = {} @@ -307,7 +305,7 @@ function add_rule(var) end local function foreach_geosite(list_arg, callback) - local geosite_path = uci:get(api.c_config, "@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/" + local geosite_path = api.uci_get_c("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/" geosite_path = geosite_path:match("^(.*)/") .. "/geosite.dat" if not fs.access(geosite_path) then return end local bin = api.finded_com("geoview") @@ -412,7 +410,7 @@ function add_rule(var) set_domain_ipset(address, table.concat(sets, ",")) end end - uci:foreach(api.c_config, "nodes", function(t) + api.uci_foreach_c("nodes", function(t) process_address(t.address) process_address(t.download_address) local dns, _ = api.get_domain_port_from_url(t.domain_resolver_dns or t.domain_resolver_dns_https or "") @@ -420,7 +418,7 @@ function add_rule(var) process_address(dns) end end) - uci:foreach(api.c_config, "subscribe_list", function(t) --订阅链接 + api.uci_foreach_c("subscribe_list", function(t) --订阅链接 local url, _ = api.get_domain_port_from_url(t.url or "") if url and url ~= "" then process_address(url) @@ -615,9 +613,9 @@ function add_rule(var) --分流规则 if IS_SHUNT_NODE and USE_CHINADNS_NG == "0" then - local t = uci:get_all(api.c_config, TCP_NODE) + local t = api.uci_get_c(TCP_NODE) local default_node_id = t["default_node"] or "_direct" - uci:foreach(api.c_config, "shunt_rules", function(s) + api.uci_foreach_c("shunt_rules", function(s) local _node_id = t[s[".name"]] if _node_id and _node_id ~= "_blackhole" and t["shunt_group"] == s.group then if _node_id == "_default" then @@ -763,7 +761,7 @@ function add_rule(var) --Copy dnsmasq instance conf_lines = copy_instance({["-LISTEN_PORT"] = LISTEN_PORT, ["-TMP_DNSMASQ_PATH"] = TMP_DNSMASQ_PATH, ["-return"] = "1"}) --dhcp.leases to hosts - local hosts = "/tmp/etc/" .. appname .. "_tmp/dhcp-hosts" + local hosts = api.CACHE_PATH .. "/dhcp-hosts" sys.call("touch " .. hosts) tinsert(conf_lines, "addn-hosts=" .. hosts) else diff --git a/luci-app-passwall/root/usr/share/passwall/helper_smartdns_add.lua b/luci-app-passwall/root/usr/share/passwall/helper_smartdns_add.lua index 233f5300..9937c496 100644 --- a/luci-app-passwall/root/usr/share/passwall/helper_smartdns_add.lua +++ b/luci-app-passwall/root/usr/share/passwall/helper_smartdns_add.lua @@ -1,5 +1,4 @@ local api = require "luci.passwall.api" -local appname = api.appname local var = api.get_args(arg) local FLAG = var["-FLAG"] @@ -25,20 +24,19 @@ local SUBNET = var["-SUBNET"] local LISTEN_PORT = var["-LISTEN_PORT"] local LOCAL_PORT = var["-LOCAL_PORT"] -local uci = api.uci local sys = api.sys local fs = api.fs local datatypes = api.datatypes -local TMP_PATH = "/tmp/etc/" .. appname +local TMP_PATH = api.TMP_PATH local TMP_ACL_PATH = TMP_PATH .. "/acl" -local RULES_PATH = "/usr/share/" .. appname .. "/rules" +local RULES_PATH = "/usr/share/passwall/rules" local FLAG_PATH = TMP_ACL_PATH .. "/" .. FLAG local TMP_CONF_FILE = FLAG_PATH .. "/smartdns.conf" local config_lines = {} local tmp_lines = {} -local USE_GEOVIEW = uci:get(api.c_config, "@global_rules[0]", "enable_geoview") -local IS_SHUNT_NODE = uci:get(api.c_config, TCP_NODE, "protocol") == "_shunt" +local USE_GEOVIEW = api.uci_get_c("@global_rules[0]", "enable_geoview") +local IS_SHUNT_NODE = api.uci_get_c(TCP_NODE, "protocol") == "_shunt" if not api.is_finded("geoview") then USE_GEOVIEW = "0" @@ -98,7 +96,7 @@ local function insert_array_after(array1, array2, target) --将array2插入到ar end local function get_geosite(list_arg, out_path) - local geosite_path = uci:get(api.c_config, "@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/" + local geosite_path = api.uci_get_c("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/" geosite_path = geosite_path:match("^(.*)/") .. "/geosite.dat" if not is_file_nonzero(geosite_path) then return 1 end local bin = api.finded_com("geoview") @@ -139,7 +137,7 @@ else local soa = custom_config["force-qtype-SOA"] return ((soa and soa:match("(^|%s)28(%s|$)")) or custom_config["force-AAAA-SOA"] == "yes" - or uci:get("smartdns", "@smartdns[0]", "force_aaaa_soa") == "1") + or api.uci_get("smartdns", "@smartdns[0]", "force_aaaa_soa") == "1") and "#6" or "-6" end } @@ -163,7 +161,7 @@ else if opt.get_value then val = opt.get_value(custom_config) else - val = custom_config[opt.config_key] or uci:get("smartdns", "@smartdns[0]", opt.key) or opt.default + val = custom_config[opt.config_key] or api.uci_get("smartdns", "@smartdns[0]", opt.key) or opt.default end if val == "yes" then val = "1" elseif val == "no" then val = "0" end if opt.yes_no then @@ -187,7 +185,7 @@ if not REMOTE_GROUP or REMOTE_GROUP == "nil" then sys.call('sed -i "/passwall/d" /etc/smartdns/custom.conf >/dev/null 2>&1') end -local force_https_soa = uci:get(api.c_config, "@global[0]", "force_https_soa") or 0 +local force_https_soa = api.uci_get_c("@global[0]", "force_https_soa") or 0 local proxy_server_name = "passwall-proxy-server" config_lines = { tonumber(LISTEN_PORT) ~= 0 and "bind [::]:" .. LISTEN_PORT .. "@lo" or "", @@ -342,7 +340,7 @@ if not is_file_nonzero(file_vpslist) then written_domains[address] = true end end - uci:foreach(api.c_config, "nodes", function(t) + api.uci_foreach_c("nodes", function(t) process_address(t.address) process_address(t.download_address) local dns, _ = api.get_domain_port_from_url(t.domain_resolver_dns or t.domain_resolver_dns_https or "") @@ -350,7 +348,7 @@ if not is_file_nonzero(file_vpslist) then process_address(dns) end end) - uci:foreach(api.c_config, "subscribe_list", function(t) --订阅链接 + api.uci_foreach_c("subscribe_list", function(t) --订阅链接 local url, _ = api.get_domain_port_from_url(t.url or "") if url and url ~= "" then process_address(url) @@ -555,9 +553,9 @@ if IS_SHUNT_NODE then local file_shunt_host = FLAG_PATH .. "/shunt_proxy_host" local geosite_white_arg, geosite_shunt_arg = "", "" - local t = uci:get_all(api.c_config, TCP_NODE) + local t = api.uci_get_c(TCP_NODE) local default_node_id = t["default_node"] or "_direct" - uci:foreach(api.c_config, "shunt_rules", function(s) + api.uci_foreach_c("shunt_rules", function(s) local _node_id = t[s[".name"]] if _node_id and _node_id ~= "_blackhole" and t["shunt_group"] == s.group then if _node_id == "_default" then @@ -694,5 +692,5 @@ if DEFAULT_DNS_GROUP then end fs.symlink(TMP_CONF_FILE, SMARTDNS_CONF) -sys.call(string.format('echo "conf-file %s" >> /etc/smartdns/custom.conf', string.gsub(SMARTDNS_CONF, appname, appname .. "*"))) +sys.call(string.format('echo "conf-file %s" >> /etc/smartdns/custom.conf', string.gsub(SMARTDNS_CONF, "passwall", "passwall*"))) log(" - SmartDNS已作为Dnsmasq上游,如果你自行配置了错误的DNS流程,将会导致域名(直连/代理域名)分流失效!!!") diff --git a/luci-app-passwall/root/usr/share/passwall/rule_update.lua b/luci-app-passwall/root/usr/share/passwall/rule_update.lua index 0758accc..b7aea51f 100755 --- a/luci-app-passwall/root/usr/share/passwall/rule_update.lua +++ b/luci-app-passwall/root/usr/share/passwall/rule_update.lua @@ -1,19 +1,18 @@ #!/usr/bin/lua local api = require ("luci.passwall.api") -local name = api.appname -local c_config = api.c_config -local uci = api.uci +local appname = api.appname local sys = api.sys local jsonc = api.jsonc local fs = api.fs +local uci, uci_get, uci_set, uci_del, uci_foreach, uci_save = api.uci, api.uci_get_c, api.uci_set_c, api.uci_del_c, api.uci_foreach_c, api.uci_save_c local arg1 = arg[1] local arg2 = arg[2] local arg3 = arg[3] local nftable_name = "inet passwall" -local rule_path = "/usr/share/" .. name .. "/rules" +local rule_path = "/usr/share/passwall/rules" local reboot = 0 local gfwlist_update = "0" local chnroute_update = "0" @@ -24,14 +23,14 @@ local geosite_update = "0" local excluded_domain = {"apple.com","sina.cn","sina.com.cn","baidu.com","byr.cn","jlike.com","weibo.com","zhongsou.com","youdao.com","sogou.com","so.com","soso.com","aliyun.com","taobao.com","jd.com","qq.com","bing.com"} -local gfwlist_url = uci:get(c_config, "@global_rules[0]", "gfwlist_url") or {"https://fastly.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/gfw.txt"} -local chnroute_url = uci:get(c_config, "@global_rules[0]", "chnroute_url") or {"https://ispip.clang.cn/all_cn.txt"} -local chnroute6_url = uci:get(c_config, "@global_rules[0]", "chnroute6_url") or {"https://ispip.clang.cn/all_cn_ipv6.txt"} -local chnlist_url = uci:get(c_config, "@global_rules[0]", "chnlist_url") or {"https://fastly.jsdelivr.net/gh/felixonmars/dnsmasq-china-list/accelerated-domains.china.conf","https://fastly.jsdelivr.net/gh/felixonmars/dnsmasq-china-list/apple.china.conf","https://fastly.jsdelivr.net/gh/felixonmars/dnsmasq-china-list/google.china.conf"} -local geoip_url = uci:get(c_config, "@global_rules[0]", "geoip_url") or "https://github.com/Loyalsoldier/geoip/releases/latest/download/geoip.dat" -local geosite_url = uci:get(c_config, "@global_rules[0]", "geosite_url") or "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" -local asset_location = uci:get(c_config, "@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/" -local geo2rule = uci:get(c_config, "@global_rules[0]", "geo2rule") or "0" +local gfwlist_url = uci_get("@global_rules[0]", "gfwlist_url") or {"https://fastly.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/gfw.txt"} +local chnroute_url = uci_get("@global_rules[0]", "chnroute_url") or {"https://ispip.clang.cn/all_cn.txt"} +local chnroute6_url = uci_get("@global_rules[0]", "chnroute6_url") or {"https://ispip.clang.cn/all_cn_ipv6.txt"} +local chnlist_url = uci_get("@global_rules[0]", "chnlist_url") or {"https://fastly.jsdelivr.net/gh/felixonmars/dnsmasq-china-list/accelerated-domains.china.conf","https://fastly.jsdelivr.net/gh/felixonmars/dnsmasq-china-list/apple.china.conf","https://fastly.jsdelivr.net/gh/felixonmars/dnsmasq-china-list/google.china.conf"} +local geoip_url = uci_get("@global_rules[0]", "geoip_url") or "https://github.com/Loyalsoldier/geoip/releases/latest/download/geoip.dat" +local geosite_url = uci_get("@global_rules[0]", "geosite_url") or "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" +local asset_location = uci_get("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/" +local geo2rule = uci_get("@global_rules[0]", "geo2rule") or "0" local geoip_update_ok, geosite_update_ok = false, false asset_location = asset_location:match("/$") and asset_location or (asset_location .. "/") local backup_path = "/tmp/bak_v2ray/" @@ -660,20 +659,20 @@ if arg2 then end) if rollback then arg2 = nil end else - gfwlist_update = uci:get(c_config, "@global_rules[0]", "gfwlist_update") or "1" - chnroute_update = uci:get(c_config, "@global_rules[0]", "chnroute_update") or "1" - chnroute6_update = uci:get(c_config, "@global_rules[0]", "chnroute6_update") or "1" - chnlist_update = uci:get(c_config, "@global_rules[0]", "chnlist_update") or "1" - geoip_update = uci:get(c_config, "@global_rules[0]", "geoip_update") or "1" - geosite_update = uci:get(c_config, "@global_rules[0]", "geosite_update") or "1" + gfwlist_update = uci_get("@global_rules[0]", "gfwlist_update") or "1" + chnroute_update = uci_get("@global_rules[0]", "chnroute_update") or "1" + chnroute6_update = uci_get("@global_rules[0]", "chnroute6_update") or "1" + chnlist_update = uci_get("@global_rules[0]", "chnlist_update") or "1" + geoip_update = uci_get("@global_rules[0]", "geoip_update") or "1" + geosite_update = uci_get("@global_rules[0]", "geosite_update") or "1" end if geo2rule ~= "1" and gfwlist_update == "0" and chnroute_update == "0" and chnroute6_update == "0" and chnlist_update == "0" and geoip_update == "0" and geosite_update == "0" then os.exit(0) end local function check_instance(action) - local rule_lock = "/var/lock/" .. name .. "_rule_update.lock" - local sub_lock = "/var/lock/" .. name .. "_subscribe.lock" + local rule_lock = api.LOCK_PREFIX .. "_rule_update.lock" + local sub_lock = api.LOCK_PREFIX .. "_subscribe.lock" if action == "start" then math.randomseed(os.time() + math.floor(os.clock() * 1000)) @@ -778,25 +777,25 @@ else end if not rollback then - uci:set(c_config, "@global_rules[0]", "gfwlist_update", gfwlist_update) - uci:set(c_config, "@global_rules[0]", "chnroute_update", chnroute_update) - uci:set(c_config, "@global_rules[0]", "chnroute6_update", chnroute6_update) - uci:set(c_config, "@global_rules[0]", "chnlist_update", chnlist_update) - uci:set(c_config, "@global_rules[0]", "geoip_update", geoip_update) - uci:set(c_config, "@global_rules[0]", "geosite_update", geosite_update) - api.uci_save(uci, c_config, true) + uci_set("@global_rules[0]", "gfwlist_update", gfwlist_update) + uci_set("@global_rules[0]", "chnroute_update", chnroute_update) + uci_set("@global_rules[0]", "chnroute6_update", chnroute6_update) + uci_set("@global_rules[0]", "chnlist_update", chnlist_update) + uci_set("@global_rules[0]", "geoip_update", geoip_update) + uci_set("@global_rules[0]", "geosite_update", geosite_update) + uci_save(true) end if reboot == 1 then if arg3 == "cron" then - if not fs.access("/var/lock/" .. name .. ".lock") then - sys.call("touch /tmp/lock/" .. name .. "_cron.lock") + if not fs.access(api.LOCK_PREFIX .. ".lock") then + luci.sys.call("touch %s_cron.lock" % api.LOCK_PREFIX) end end log("重启服务,应用新的规则。") - uci:set(c_config, "@global[0]", "flush_set", "1") - api.uci_save(uci, c_config, true, true) + uci_set("@global[0]", "flush_set", "1") + uci_save(true, true) end log("规则更新完毕...\n") diff --git a/luci-app-passwall/root/usr/share/passwall/subscribe.lua b/luci-app-passwall/root/usr/share/passwall/subscribe.lua index 4c38a64f..d53dbd3d 100755 --- a/luci-app-passwall/root/usr/share/passwall/subscribe.lua +++ b/luci-app-passwall/root/usr/share/passwall/subscribe.lua @@ -7,23 +7,22 @@ require 'luci.util' require 'luci.jsonc' require 'luci.sys' local api = require "luci.passwall.api" -local appname = api.appname local c_config = api.c_config -local datatypes = require "luci.cbi.datatypes" +local datatypes = api.datatypes -loadfile("/usr/share/" .. appname .. "/clash_subconverter.lua")() +loadfile("/usr/share/passwall/clash_subconverter.lua")() + +local split = api.split +local base64Decode = api.base64Decode +local jsonParse, jsonStringify = api.jsonc.parse, api.jsonc.stringify +local UrlEncode, UrlDecode = api.UrlEncode, api.UrlDecode +local fs = api.fs +local uci, uci_get, uci_set, uci_del, uci_foreach, uci_save = api.uci, api.uci_get_c, api.uci_set_c, api.uci_del_c, api.uci_foreach_c, api.uci_save_c -- these global functions are accessed all the time by the event handler -- so caching them is worth the effort local tinsert = table.insert local ssub, slen, schar, sbyte, sformat, sgsub = string.sub, string.len, string.char, string.byte, string.format, string.gsub -local split = api.split -local jsonParse, jsonStringify = luci.jsonc.parse, luci.jsonc.stringify -local base64Decode = api.base64Decode -local UrlEncode = api.UrlEncode -local UrlDecode = api.UrlDecode -local uci = api.uci -local fs = api.fs local has_ss_rust = api.is_finded("sslocal") local has_ssr = api.is_finded("ssr-local") and api.is_finded("ssr-redir") @@ -31,9 +30,9 @@ local has_singbox = api.finded_com("sing-box") local has_xray = api.finded_com("xray") local has_hysteria2 = api.finded_com("hysteria") local DEFAULT_ALLOWINSECURE = true -local DEFAULT_FILTER_KEYWORD_MODE = uci:get(c_config, "@global_subscribe[0]", "filter_keyword_mode") or "0" -local DEFAULT_FILTER_KEYWORD_DISCARD_LIST = uci:get(c_config, "@global_subscribe[0]", "filter_discard_list") or {} -local DEFAULT_FILTER_KEYWORD_KEEP_LIST = uci:get(c_config, "@global_subscribe[0]", "filter_keep_list") or {} +local DEFAULT_FILTER_KEYWORD_MODE = uci_get("@global_subscribe[0]", "filter_keyword_mode") or "0" +local DEFAULT_FILTER_KEYWORD_DISCARD_LIST = uci_get("@global_subscribe[0]", "filter_discard_list") or {} +local DEFAULT_FILTER_KEYWORD_KEEP_LIST = uci_get("@global_subscribe[0]", "filter_keep_list") or {} -- 取节点使用core类型(节点订阅页面未设置时,自动取默认) local DEFAULT_SS_TYPE = api.get_core("ss_type", {{has_ss_rust,"shadowsocks-rust"},{has_singbox,"sing-box"},{has_xray,"xray"}}) local DEFAULT_TROJAN_TYPE = api.get_core("trojan_type", {{has_singbox,"sing-box"},{has_xray,"xray"}}) @@ -142,13 +141,13 @@ do local szType = "@global[0]" local option = protocol .. "_node" - local node_id = uci:get(c_config, szType, option) + local node_id = uci_get(szType, option) CONFIG[#CONFIG + 1] = { log = true, remarks = name .. "节点", - currentNode = node_id and uci:get_all(c_config, node_id) or nil, + currentNode = node_id and uci_get(node_id) or nil, set = function(o, server) - uci:set(c_config, szType, option, server) + uci_set(szType, option, server) o.newNodeId = server end } @@ -159,7 +158,7 @@ do if true then local i = 0 local option = "node" - uci:foreach(c_config, "socks", function(t) + uci_foreach("socks", function(t) i = i + 1 local id = t[".name"] local node_id = t[option] @@ -167,14 +166,14 @@ do log = true, id = id, remarks = "Socks节点列表[" .. i .. "]", - currentNode = node_id and uci:get_all(c_config, node_id) or nil, + currentNode = node_id and uci_get(node_id) or nil, set = function(o, server) if not server or server == "" then if #nodes_table > 0 then server = nodes_table[1][".name"] end end - uci:set(c_config, t[".name"], option, server) + uci_set(t[".name"], option, server) o.newNodeId = server end } @@ -184,7 +183,7 @@ do local newNodes = {} for k, asb_node_id in ipairs(t.autoswitch_backup_node) do if asb_node_id then - local currentNode = uci:get_all(c_config, asb_node_id) or {} + local currentNode = uci_get(asb_node_id) or {} if currentNode[".type"] == "nodes" then currentNodes[#currentNodes + 1] = { log = true, @@ -206,7 +205,7 @@ do set = function(o, newNodes) if o then if not newNodes then newNodes = o.newNodes end - uci:set_list(c_config, id, "autoswitch_backup_node", newNodes or {}) + uci_set(id, "autoswitch_backup_node", newNodes or {}) end end } @@ -222,25 +221,25 @@ do local ip, port = str:match("^([%d%.]+):(%d+)$") return ip and datatypes.ipaddr(ip) and tonumber(port) and tonumber(port) <= 65535 end - uci:foreach(c_config, "haproxy_config", function(t) + uci_foreach("haproxy_config", function(t) i = i + 1 local node_id = t[option] CONFIG[#CONFIG + 1] = { log = true, id = t[".name"], remarks = "HAProxy负载均衡节点列表[" .. i .. "]", - currentNode = node_id and uci:get_all(c_config, node_id) or nil, + currentNode = node_id and uci_get(node_id) or nil, set = function(o, server) -- 如果当前 lbss 值不是 ip:port 格式,才进行修改 if not is_ip_port(t[option]) then - uci:set(c_config, t[".name"], option, server) + uci_set(t[".name"], option, server) o.newNodeId = server end end, delete = function(o) -- 如果当前 lbss 值不是 ip:port 格式,才进行删除 if not is_ip_port(t[option]) then - uci:delete(c_config, t[".name"]) + uci_del(t[".name"]) end end } @@ -250,7 +249,7 @@ do if true then local i = 0 local options = {"tcp", "udp"} - uci:foreach(c_config, "acl_rule", function(t) + uci_foreach("acl_rule", function(t) i = i + 1 for index, value in ipairs(options) do local option = value .. "_node" @@ -259,9 +258,9 @@ do log = true, id = t[".name"], remarks = "访问控制列表[" .. i .. "]", - currentNode = node_id and uci:get_all(c_config, node_id) or nil, + currentNode = node_id and uci_get(node_id) or nil, set = function(o, server) - uci:set(c_config, t[".name"], option, server) + uci_set(t[".name"], option, server) o.newNodeId = server end } @@ -269,11 +268,11 @@ do end) end - uci:foreach(c_config, "nodes", function(node) + uci_foreach("nodes", function(node) local node_id = node[".name"] if node.protocol and node.protocol == '_shunt' then local rules = {} - uci:foreach(c_config, "shunt_rules", function(e) + uci_foreach("shunt_rules", function(e) if e[".name"] and e.remarks then table.insert(rules, e) table.insert(rules, { @@ -294,7 +293,7 @@ do for k, e in pairs(rules) do local _node_id = node[e[".name"]] or nil if _node_id then - local section = uci:get_all(c_config, _node_id) or {} + local section = uci_get(_node_id) or {} if section[".type"] == "nodes" then CONFIG[#CONFIG + 1] = { log = false, @@ -302,7 +301,7 @@ do remarks = "分流" .. e.remarks .. "节点", set = function(o, server) if not server then server = "" end - uci:set(c_config, node_id, e[".name"], server) + uci_set(node_id, e[".name"], server) o.newNodeId = server end } @@ -319,7 +318,7 @@ do log = true, node = b_node_id, currentNode = (function() - local section = uci:get_all(c_config, b_node_id) or {} + local section = uci_get(b_node_id) or {} if section[".type"] == "socks" then return { Socks = b_node_id } end @@ -341,15 +340,15 @@ do set = function(o, newNodes) if o then if not newNodes then newNodes = o.newNodes end - uci:set_list(c_config, node_id, "balancing_node", newNodes or {}) + uci_set(node_id, "balancing_node", newNodes or {}) end end } --后备节点 - local currentNode = uci:get_all(c_config, node_id) or nil + local currentNode = uci_get(node_id) or nil if currentNode and currentNode.fallback_node then - local section = uci:get_all(c_config, currentNode.fallback_node) or {} + local section = uci_get(currentNode.fallback_node) or {} if section[".type"] == "nodes" then CONFIG[#CONFIG + 1] = { log = true, @@ -357,11 +356,11 @@ do remarks = "Xray负载均衡节点[" .. node_id .. "]后备节点", currentNode = section, set = function(o, server) - uci:set(c_config, node_id, "fallback_node", server) + uci_set(node_id, "fallback_node", server) o.newNodeId = server end, delete = function(o) - uci:delete(c_config, node_id, "fallback_node") + uci_del(node_id, "fallback_node") end } end @@ -376,7 +375,7 @@ do log = true, node = u_node_id, currentNode = (function() - local section = uci:get_all(c_config, u_node_id) or {} + local section = uci_get(u_node_id) or {} if section[".type"] == "socks" then return { Socks = u_node_id } end @@ -398,47 +397,47 @@ do set = function(o, newNodes) if o then if not newNodes then newNodes = o.newNodes end - uci:set_list(c_config, node_id, "urltest_node", newNodes or {}) + uci_set(node_id, "urltest_node", newNodes or {}) end end } else --前置代理节点 - local currentNode = uci:get_all(c_config, node_id) or nil + local currentNode = uci_get(node_id) or nil if currentNode and currentNode.preproxy_node then - local section = uci:get_all(c_config, currentNode.preproxy_node) or {} + local section = uci_get(currentNode.preproxy_node) or {} if section[".type"] == "nodes" then CONFIG[#CONFIG + 1] = { log = true, id = node_id, remarks = "节点[" .. node_id .. "]前置代理节点", - currentNode = uci:get_all(c_config, currentNode.preproxy_node) or nil, + currentNode = uci_get(currentNode.preproxy_node) or nil, set = function(o, server) - uci:set(c_config, node_id, "preproxy_node", server) + uci_set(node_id, "preproxy_node", server) o.newNodeId = server end, delete = function(o) - uci:delete(c_config, node_id, "preproxy_node") + uci_del(node_id, "preproxy_node") end } end end --落地节点 - local currentNode = uci:get_all(c_config, node_id) or nil + local currentNode = uci_get(node_id) or nil if currentNode and currentNode.to_node then - local section = uci:get_all(c_config, currentNode.to_node) or {} + local section = uci_get(currentNode.to_node) or {} if section[".type"] == "nodes" then CONFIG[#CONFIG + 1] = { log = true, id = node_id, remarks = "节点[" .. node_id .. "]落地节点", - currentNode = uci:get_all(c_config, currentNode.to_node) or nil, + currentNode = uci_get(currentNode.to_node) or nil, set = function(o, server) - uci:set(c_config, node_id, "to_node", server) + uci_set(node_id, "to_node", server) o.newNodeId = server end, delete = function(o) - uci:delete(c_config, node_id, "to_node") + uci_del(node_id, "to_node") end } end @@ -1757,19 +1756,19 @@ local function truncate_nodes(group) end end end - uci:foreach(c_config, "nodes", function(node) + uci_foreach("nodes", function(node) if node.add_mode == "2" then if (not group) or (group:lower() == (node.group or ""):lower()) then - uci:delete(c_config, node['.name']) + uci_del(node['.name']) end end end) - uci:foreach(c_config, "subscribe_list", function(o) + uci_foreach("subscribe_list", function(o) if (not group) or (group:lower() == (o.remark or ""):lower()) then - uci:delete(c_config, o['.name'], "md5") + uci_del(o['.name'], "md5") end end) - api.uci_save(uci, c_config, true) + uci_save(true) end local function select_node(nodes, config, parentConfig) @@ -1908,10 +1907,10 @@ local function update_node(manual) end if manual == 0 and next(group) then - uci:foreach(c_config, "nodes", function(node) + uci_foreach("nodes", function(node) -- 如果未发现新节点或手动导入的节点就不要删除了... if node.add_mode == "2" and (node.group and group[node.group:lower()] == true) then - uci:delete(c_config, node['.name']) + uci_del(node['.name']) end end) end @@ -1924,9 +1923,9 @@ local function update_node(manual) -- Subscription Group Chain Agent local function valid_chain_node(node) if not node then return "" end - local cp = uci:get(c_config, node, "chain_proxy") or "" - local am = uci:get(c_config, node, "add_mode") or "0" - chain_node_type = (cp == "" and am ~= "2") and (uci:get(c_config, node, "type") or "") or "" + local cp = uci_get(node, "chain_proxy") or "" + local am = uci_get(node, "add_mode") or "0" + chain_node_type = (cp == "" and am ~= "2") and (uci_get(node, "type") or "") or "" if chain_node_type ~= "Xray" and chain_node_type ~= "sing-box" then chain_node_type = "" return "" @@ -1948,19 +1947,19 @@ local function update_node(manual) local cfgid = uci:section(c_config, "nodes", api.gen_random_char()) for kkk, vvv in pairs(vv) do if type(vvv) == "table" and next(vvv) ~= nil then - uci:set_list(c_config, cfgid, kkk, vvv) + uci_set(cfgid, kkk, vvv) else if kkk ~= "group" or vvv ~= "default" then - uci:set(c_config, cfgid, kkk, vvv) + uci_set(cfgid, kkk, vvv) end -- sing-box/xray 节点域名解析 if kkk == "type" and (vvv == "Xray" or vvv == "sing-box") then if domain_resolver then - uci:set(c_config, cfgid, "domain_resolver", domain_resolver) + uci_set(cfgid, "domain_resolver", domain_resolver) if domain_resolver_dns then - uci:set(c_config, cfgid, "domain_resolver_dns", domain_resolver_dns) + uci_set(cfgid, "domain_resolver_dns", domain_resolver_dns) elseif domain_resolver_dns_https then - uci:set(c_config, cfgid, "domain_resolver_dns_https", domain_resolver_dns_https) + uci_set(cfgid, "domain_resolver_dns_https", domain_resolver_dns_https) end end if domain_strategy then @@ -1969,20 +1968,20 @@ local function update_node(manual) local map = { UseIPv4v6 = "prefer_ipv4", UseIPv6v4 = "prefer_ipv6", UseIPv4 = "ipv4_only", UseIPv6 = "ipv6_only" } ds = map[ds] or "" end - uci:set(c_config, cfgid, "domain_strategy", ds) + uci_set(cfgid, "domain_strategy", ds) end end -- 订阅组链式代理 if chain_node_type ~= "" and kkk == "type" and (vvv == "Xray" or vvv == "sing-box") then if preproxy_node_group ~="" then - uci:set(c_config, cfgid, "chain_proxy", "1") - uci:set(c_config, cfgid, "preproxy_node", preproxy_node_group) + uci_set(cfgid, "chain_proxy", "1") + uci_set(cfgid, "preproxy_node", preproxy_node_group) elseif to_node_group ~= "" then - uci:set(c_config, cfgid, "chain_proxy", "2") - uci:set(c_config, cfgid, "to_node", to_node_group) + uci_set(cfgid, "chain_proxy", "2") + uci_set(cfgid, "to_node", to_node_group) elseif outbound_iface_group ~= "" then - uci:set(c_config, cfgid, "chain_proxy", "3") - uci:set(c_config, cfgid, "outbound_iface", outbound_iface_group) + uci_set(cfgid, "chain_proxy", "3") + uci_set(cfgid, "outbound_iface", outbound_iface_group) end end end @@ -1993,16 +1992,16 @@ local function update_node(manual) for cfgid, info in pairs(subscribe_info) do for key, value in pairs(info) do if value ~= "" then - uci:set(c_config, cfgid, key, value) + uci_set(cfgid, key, value) else - uci:delete(c_config, cfgid, key) + uci_del(cfgid, key) end end end if next(CONFIG) then local nodes = {} - uci:foreach(c_config, "nodes", function(node) + uci_foreach("nodes", function(node) nodes[#nodes + 1] = node end) @@ -2021,16 +2020,16 @@ local function update_node(manual) end end - api.uci_save(uci, c_config, true) + uci_save(true) if arg[3] == "cron" then - if not fs.access("/var/lock/" .. c_config .. ".lock") then - luci.sys.call("touch /tmp/lock/" .. c_config .. "_cron.lock") + if not fs.access(api.LOCK_PREFIX .. ".lock") then + luci.sys.call("touch %s_cron.lock" % api.LOCK_PREFIX) end end if manual ~= 1 then - luci.sys.call("/etc/init.d/" .. appname .. " restart > /dev/null 2>&1 &") + luci.sys.call("/etc/init.d/passwall restart > /dev/null 2>&1 &") end end @@ -2134,10 +2133,10 @@ local execute = function() local fail_list = {} if arg[2] ~= "all" then string.gsub(arg[2], '[^' .. "," .. ']+', function(w) - subscribe_list[#subscribe_list + 1] = uci:get_all(c_config, w) or {} + subscribe_list[#subscribe_list + 1] = uci_get(w) or {} end) else - uci:foreach(c_config, "subscribe_list", function(o) + uci_foreach("subscribe_list", function(o) subscribe_list[#subscribe_list + 1] = o end) end @@ -2183,7 +2182,7 @@ local execute = function() raw_data = parseClashNode(raw_data) subscribe_info[cfgid] = parse_clash_sub_info(headers) parse_link(raw_data, "2", remark, value) - uci:set(c_config, cfgid, "md5", new_md5) + uci_set(cfgid, "md5", new_md5) end else fail_list[#fail_list + 1] = value @@ -2206,8 +2205,8 @@ local execute = function() end local function check_instance(action) - local sub_lock = "/var/lock/" .. appname .. "_subscribe.lock" - local rule_lock = "/var/lock/" .. appname .. "_rule_update.lock" + local sub_lock = api.LOCK_PREFIX .. "_subscribe.lock" + local rule_lock = api.LOCK_PREFIX .. "_rule_update.lock" if action == "start" then math.randomseed(os.time() + math.floor(os.clock() * 1000)) diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile index f0dd4967..c3aeb062 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.8.14 -PKG_RELEASE:=83 +PKG_RELEASE:=84 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall2/luasrc/controller/passwall2.lua b/luci-app-passwall2/luasrc/controller/passwall2.lua index e7f3d225..60185e85 100644 --- a/luci-app-passwall2/luasrc/controller/passwall2.lua +++ b/luci-app-passwall2/luasrc/controller/passwall2.lua @@ -120,6 +120,7 @@ function index() entry({"admin", "services", appname, "geo_view"}, call("geo_view")).leaf = true entry({"admin", "services", appname, "fetch_certsha256"}, call("fetch_certsha256")).leaf = true + entry({"admin", "services", appname, "gen_wireguard_key"}, call("gen_wireguard_key")).leaf = true end local function http_write_json(content) @@ -304,7 +305,7 @@ end function get_redir_log() local id = http.formvalue("id") local name = http.formvalue("name") - local file_path = "/tmp/etc/passwall2/acl/" .. id .. "/" .. name .. ".log" + local file_path = api.TMP_PATH .. "/acl/" .. id .. "/" .. name .. ".log" if nixio.fs.access(file_path) then local content = luci.sys.exec("tail -n 19999 '" .. file_path .. "'") content = content:gsub("\n", "
") @@ -316,7 +317,7 @@ end function get_socks_log() local name = http.formvalue("name") - local path = "/tmp/etc/passwall2/" .. name .. ".log" + local path = api.TMP_PATH .. "/" .. name .. ".log" if nixio.fs.access(path) then local content = luci.sys.exec("tail -n 5000 ".. path) content = content:gsub("\n", "
") @@ -349,7 +350,7 @@ end function index_status() local e = {} - e["global_status"] = luci.sys.call("/bin/busybox top -bn1 | grep -v 'grep' | grep '/tmp/etc/passwall2/bin/' | grep 'default' | grep 'global' >/dev/null") == 0 + e["global_status"] = luci.sys.call("/bin/busybox top -bn1 | grep -v 'grep' | grep '%s/bin/' | grep 'default' | grep 'global' >/dev/null" % api.TMP_PATH) == 0 http_write_json(e) end @@ -720,8 +721,9 @@ end function server_log() local id = http.formvalue("id") - if nixio.fs.access("/tmp/etc/passwall2_server/" .. id .. ".log") then - local content = luci.sys.exec("cat /tmp/etc/passwall2_server/" .. id .. ".log") + local f_file = api.S_TMP_PATH .. "/" .. id .. ".log" + if nixio.fs.access(f_file) then + local content = luci.sys.exec("cat " .. f_file) content = content:gsub("\n", "
") http.write(content) else @@ -1085,3 +1087,12 @@ function delete_select_shunt_rules() uci_save(true, true) end end + +function gen_wireguard_key() + local key = api.gen_wireguard_key() + if key then + http_write_json_ok(key) + else + http_write_json_error() + end +end 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 e32df176..cdf7870e 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 @@ -2,9 +2,10 @@ local api = require "luci.passwall2.api" api.set_default_cbi() m = Map() +m.redirect = api.url("acl") if not arg[1] or not m:get(arg[1]) then - luci.http.redirect(api.url("acl")) + luci.http.redirect(m.redirect) end m:appendTemplate("/cbi/nodes_listvalue_com") diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_config.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_config.lua index 3bbe3cb0..242126af 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_config.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_config.lua @@ -2,13 +2,12 @@ api = require "luci.passwall2.api" api.set_default_cbi() m = Map() -m.redirect = api.url() +m.redirect = api.url("node_list") if not arg[1] or not m:get(arg[1]) then - luci.http.redirect(api.url("node_list")) + luci.http.redirect(m.redirect) end -fs = require "nixio.fs" formvalue_key = "cbid." .. m.config .. "." .. arg[1] .. "." m:appendTemplate("/node_config/header", {section = arg[1]}) @@ -24,7 +23,7 @@ m:foreach("nodes", function(s) end end) -s = m:section(NamedSection, arg[1], "nodes", translate("Node Config")) +local s = m:section(NamedSection, arg[1], "nodes", translate("Node Config")) s.addremove = false s.dynamic = false @@ -74,7 +73,7 @@ end o = s:option(ListValue, "type", translate("Type")) local type_table = {} -for filename in fs.dir(types_dir) do +for filename in api.fs.dir(types_dir) do table.insert(type_table, filename) end table.sort(type_table) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/socks_config.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/socks_config.lua index 5237e4e6..65dd0ed8 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/socks_config.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/socks_config.lua @@ -2,9 +2,10 @@ local api = require "luci.passwall2.api" api.set_default_cbi() m = Map() +m.redirect = api.url() if not arg[1] or not m:get(arg[1]) then - luci.http.redirect(api.url()) + luci.http.redirect(m.redirect) end m:appendTemplate("/cbi/nodes_dynamiclist_com") @@ -75,6 +76,20 @@ o = s:option(Flag, "log", translate("Enable") .. " " .. translate("Log")) o.default = 1 o.rmempty = false +o = s:option(DummyValue, "_log", translate("Log File")) +o.rawhtml = true +o.cfgvalue = function(t, n) + local log_path = api.TMP_PATH .. "/" .. arg[1] .. ".log" + local log_url = api.url("get_socks_log") .. "?name=" .. arg[1] + return string.format( + '%s  ', + log_path, + translate("View Log"), + log_url + ) +end +o:depends("log", true) + o = s:option(Flag, "enable_autoswitch", translate("Auto Switch")) o.default = 0 o.rmempty = false diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/hysteria2.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/hysteria2.lua index bc3c4946..753c748a 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/hysteria2.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/hysteria2.lua @@ -1,51 +1,43 @@ -local m, s = ... - if not api.finded_com("hysteria") then return end -type_name = "Hysteria2" - -- [[ Hysteria2 ]] +local m, s1 = ... +local type_name = "Hysteria2" -s.fields["type"]:value(type_name, "Hysteria2") +s1.fields["type"]:value(type_name, "Hysteria2") -if s.val["type"] ~= type_name then +if s1.val["type"] ~= type_name then return end -local option_prefix = "hysteria2_" +local s = NamedSection(m, arg[1], "server") +s.type_name = type_name +s.option_prefix = "hysteria2_" -local function _n(name) - return option_prefix .. name -end +o = s:option(Value, "address", translate("Address (Support Domain Name)")) +o:depends({ realms = false }) -o = s:option(Value, _n("address"), translate("Address (Support Domain Name)")) -o:depends({ [_n("realms")] = false }) - -o = s:option(Value, _n("port"), translate("Port")) +o = s:option(Value, "port", translate("Port")) o.datatype = "port" -o:depends({ [_n("realms")] = false }) +o:depends({ realms = false }) -o = s:option(Value, _n("hop"), translate("Port hopping range")) +o = s:option(Value, "hop", translate("Port hopping range")) o.description = translate("Format as 1000:2000 or 1000-2000 Multiple groups are separated by commas (,).") -o.rewrite_option = o.option -o:depends({ [_n("realms")] = false }) +o:depends({ realms = false }) -o = s:option(Value, _n("hop_interval"), translate("Hop Interval(second)"), translate("Supports a fixed value or a random range (e.g., 30, 5-30), minimum 5.")) +o = s:option(Value, "hop_interval", translate("Hop Interval(second)"), translate("Supports a fixed value or a random range (e.g., 30, 5-30), minimum 5.")) o.datatype = "or(uinteger,portrange)" o.placeholder = "30" o.default = "30" -o.rewrite_option = o.option -o:depends({ [_n("realms")] = false }) +o:depends({ realms = false }) -o = s:option(Flag, _n("realms"), translate("Realms")) +o = s:option(Flag, "realms", translate("Realms")) o.default = "0" -o.rewrite_option = o.option -o = s:option(Value, _n("realm_url"), translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name") -o.rewrite_option = o.option -o:depends({ [_n("realms")] = "1" }) +o = s:option(Value, "realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name") +o:depends({ realms = "1" }) o.validate = function(self, value) value = api.trim(value) local realm = api.parse_realm_uri(value) @@ -53,76 +45,62 @@ o.validate = function(self, value) return nil, translate("Invalid Realm URL.") end -o = s:option(DynamicList, _n("realm_stun"), translate("Realm STUN")) +o = s:option(DynamicList, "realm_stun", translate("Realm STUN")) o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" } -o.rewrite_option = o.option -o:depends({ [_n("realms")] = "1" }) +o:depends({ realms = "1" }) -o = s:option(Value, _n("auth_password"), translate("Auth Password")) +o = s:option(Value, "auth_password", translate("Auth Password")) o.password = true -o.rewrite_option = o.option -o = s:option(ListValue, _n("obfs_type"), translate("Obfs Type")) +o = s:option(ListValue, "obfs_type", translate("Obfs Type")) o:value("", translate("Disable")) o:value("salamander") o:value("gecko") -o.rewrite_option = o.option -o = s:option(Value, _n("obfs_password"), translate("Obfs Password")) -o.rewrite_option = o.option -o:depends({ [_n("obfs_type")] = "salamander" }) -o:depends({ [_n("obfs_type")] = "gecko" }) +o = s:option(Value, "obfs_password", translate("Obfs Password")) +o:depends({ obfs_type = "salamander" }) +o:depends({ obfs_type = "gecko" }) -o = s:option(Value, _n("obfs_MinPacketSize"), translate("Gecko Packet Size (min)")) +o = s:option(Value, "obfs_MinPacketSize", translate("Gecko Packet Size (min)")) o.datatype = "uinteger" o.placeholder = "512" o.default = "512" -o:depends({ [_n("obfs_type")] = "gecko" }) -o.rewrite_option = o.option +o:depends({ obfs_type = "gecko" }) -o = s:option(Value, _n("obfs_MaxPacketSize"), translate("Gecko Packet Size (max)")) +o = s:option(Value, "obfs_MaxPacketSize", translate("Gecko Packet Size (max)")) o.datatype = "uinteger" o.placeholder = "1200" o.default = "1200" -o:depends({ [_n("obfs_type")] = "gecko" }) -o.rewrite_option = o.option +o:depends({ obfs_type = "gecko" }) -o = s:option(Flag, _n("fast_open"), translate("Fast Open")) +o = s:option(Flag, "fast_open", translate("Fast Open")) o.default = "0" -o = s:option(Value, _n("tls_serverName"), translate("Domain")) +o = s:option(Value, "tls_serverName", translate("Domain")) -o = s:option(Flag, _n("tls_allowInsecure"), translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped.")) +o = s:option(Flag, "tls_allowInsecure", translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped.")) o.default = "0" -o = s:option(Value, _n("tls_pinSHA256"), translate("PinSHA256"),translate("Certificate fingerprint")) +o = s:option(Value, "tls_pinSHA256", translate("PinSHA256"),translate("Certificate fingerprint")) -o = s:option(Value, _n("up_mbps"), translate("Max upload Mbps")) -o.rewrite_option = o.option +o = s:option(Value, "up_mbps", translate("Max upload Mbps")) -o = s:option(Value, _n("down_mbps"), translate("Max download Mbps")) -o.rewrite_option = o.option +o = s:option(Value, "down_mbps", translate("Max download Mbps")) -o = s:option(Value, _n("recv_window"), translate("QUIC stream receive window")) -o.rewrite_option = o.option +o = s:option(Value, "recv_window", translate("QUIC stream receive window")) -o = s:option(Value, _n("recv_window_conn"), translate("QUIC connection receive window")) -o.rewrite_option = o.option +o = s:option(Value, "recv_window_conn", translate("QUIC connection receive window")) -o = s:option(Value, _n("idle_timeout"), translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)") +o = s:option(Value, "idle_timeout", translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)") o.datatype = "range(4,120)" -o.rewrite_option = o.option -o = s:option(Value, _n("keep_alive_period"), translate("QUIC KeepAlive interval"), translate("Units:seconds") .. " (2~60)") +o = s:option(Value, "keep_alive_period", translate("QUIC KeepAlive interval"), translate("Units:seconds") .. " (2~60)") o.datatype = "range(2,60)" -o.rewrite_option = o.option -o = s:option(Flag, _n("disable_mtu_discovery"), translate("Disable MTU detection")) +o = s:option(Flag, "disable_mtu_discovery", translate("Disable MTU detection")) o.default = "0" -o.rewrite_option = o.option -o = s:option(Flag, _n("lazy_start"), translate("Lazy Start")) +o = s:option(Flag, "lazy_start", translate("Lazy Start")) o.default = "0" -o.rewrite_option = o.option -api.luci_types(arg[1], m, s, type_name, option_prefix) +api.luci_types(s1, s) \ No newline at end of file diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/naive.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/naive.lua index 621596fd..4d5c55ac 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/naive.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/naive.lua @@ -1,37 +1,33 @@ -local m, s = ... - if not api.is_finded("naive") then return end -type_name = "Naiveproxy" - -- [[ Naive ]] +local m, s1 = ... +local type_name = "Naiveproxy" -s.fields["type"]:value(type_name, "NaiveProxy") +s1.fields["type"]:value(type_name, "NaiveProxy") -if s.val["type"] ~= type_name then +if s1.val["type"] ~= type_name then return end -local option_prefix = "naive_" +local s = NamedSection(m, arg[1], "server") +s.type_name = type_name +s.option_prefix = "naive_" -local function _n(name) - return option_prefix .. name -end - -o = s:option(ListValue, _n("protocol"), translate("Protocol")) +o = s:option(ListValue, "protocol", translate("Protocol")) o:value("https", translate("HTTPS")) o:value("quic", translate("QUIC")) -o = s:option(Value, _n("address"), translate("Address (Support Domain Name)")) +o = s:option(Value, "address", translate("Address (Support Domain Name)")) -o = s:option(Value, _n("port"), translate("Port")) +o = s:option(Value, "port", translate("Port")) o.datatype = "port" -o = s:option(Value, _n("username"), translate("Username")) +o = s:option(Value, "username", translate("Username")) -o = s:option(Value, _n("password"), translate("Password")) +o = s:option(Value, "password", translate("Password")) o.password = true -api.luci_types(arg[1], m, s, type_name, option_prefix) +api.luci_types(s1, s) \ No newline at end of file diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua index 113209b3..d7827092 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua @@ -1,33 +1,27 @@ -local m, s = ... - if not api.finded_com("xray") then return end -local jsonc = api.jsonc - -type_name = "Xray" - -- [[ Xray ]] +local m, s1 = ... +local type_name = "Xray" -s.fields["type"]:value(type_name, "Xray") -if not s.fields["type"].default then - s.fields["type"].default = type_name +s1.fields["type"]:value(type_name, "Xray") +if not s1.fields["type"].default then + s1.fields["type"].default = type_name end -if s.val["type"] ~= type_name then +if s1.val["type"] ~= type_name then return end -local option_prefix = "xray_" +local s = NamedSection(m, arg[1], "server") +s.type_name = type_name +s.option_prefix = "xray_" -local function _n(name) - return option_prefix .. name -end +local formvalue_proto = luci.http.formvalue(formvalue_key .. "protocol") -local formvalue_proto = luci.http.formvalue(formvalue_key .. _n("protocol")) - -if formvalue_proto then s.val["protocol"] = formvalue_proto end +if formvalue_proto then s1.val["protocol"] = formvalue_proto end local arg_select_proto = luci.http.formvalue("select_proto") or "" @@ -43,7 +37,7 @@ local header_type_list = { local xray_version = api.get_app_version("xray") -o = s:option(ListValue, _n("protocol"), translate("Protocol")) +o = s:option(ListValue, "protocol", translate("Protocol")) o:value("vmess", translate("Vmess")) o:value("vless", translate("VLESS")) o:value("http", translate("HTTP")) @@ -63,13 +57,13 @@ function o.custom_cfgvalue(self, section) if arg_select_proto ~= "" then return arg_select_proto else - return m:get(section, self.option:sub(1 + #option_prefix)) + return m:get(section, self.config_option) end end -local load_balancing_options = s.val["protocol"] == "_balancing" or arg_select_proto == "_balancing" -local load_shunt_options = s.val["protocol"] == "_shunt" or arg_select_proto == "_shunt" -local load_iface_options = s.val["protocol"] == "_iface" or arg_select_proto == "_iface" +local load_balancing_options = s1.val["protocol"] == "_balancing" or arg_select_proto == "_balancing" +local load_shunt_options = s1.val["protocol"] == "_shunt" or arg_select_proto == "_shunt" +local load_iface_options = s1.val["protocol"] == "_iface" or arg_select_proto == "_iface" local load_normal_options = true if load_balancing_options or load_shunt_options or load_iface_options then load_normal_options = nil @@ -96,14 +90,14 @@ for k, e in ipairs(node_list.balancing_list or {}) do end if load_balancing_options then -- [[ Load balancing Start ]] - o = s:option(ListValue, _n("node_add_mode"), translate("Node Addition Method")) - o:depends({ [_n("protocol")] = "_balancing" }) + o = s:option(ListValue, "node_add_mode", translate("Node Addition Method")) + o:depends({ protocol = "_balancing" }) o.default = "manual" o:value("manual", translate("Manual")) o:value("batch", translate("Batch")) - o = s:option(MultiValue, _n("balancing_node"), translate("Load balancing node list"), translate("Load balancing node list, document")) - o:depends({ [_n("node_add_mode")] = "manual" }) + o = s:option(MultiValue, "balancing_node", translate("Load balancing node list"), translate("Load balancing node list, document")) + o:depends({ node_add_mode = "manual" }) o.widget = "checkbox" o.template = m:template_path("/cbi/nodes_multivalue") o.group = {} @@ -140,16 +134,16 @@ if load_balancing_options then -- [[ Load balancing Start ]] end end - o = s:option(MultiValue, _n("node_group"), translate("Select Group")) - o:depends({ [_n("node_add_mode")] = "batch" }) + o = s:option(MultiValue, "node_group", translate("Select Group")) + o:depends({ node_add_mode = "batch" }) o.widget = "checkbox" o:value("default", translate("default")) for k, v in pairs(groups) do o:value(api.UrlEncode(k), k) end - o = s:option(Value, _n("node_match_rule"), translate("Node Matching Rules")) - o:depends({ [_n("node_add_mode")] = "batch" }) + o = s:option(Value, "node_match_rule", translate("Node Matching Rules")) + o:depends({ node_add_mode = "batch" }) local descrStr = "Example: ^A && B && !C && D$
" descrStr = descrStr .. "This means the node remark must start with A (^), include B, exclude C (!), and end with D ($).
" descrStr = descrStr .. "Conditions are joined by && (AND), and their order does not affect the result.
" @@ -157,8 +151,8 @@ if load_balancing_options then -- [[ Load balancing Start ]] descrStr = descrStr .. "Example: A && B || C && D means (A AND B) OR (C AND D)." o.description = translate(descrStr) - o = s:option(ListValue, _n("balancingStrategy"), translate("Balancing Strategy")) - o:depends({ [_n("protocol")] = "_balancing" }) + o = s:option(ListValue, "balancingStrategy", translate("Balancing Strategy")) + o:depends({ protocol = "_balancing" }) o:value("random") o:value("roundRobin") o:value("leastPing") @@ -166,11 +160,11 @@ if load_balancing_options then -- [[ Load balancing Start ]] o.default = "random" -- Fallback Node - o = s:option(ListValue, _n("fallback_node"), translate("Fallback Node")) + o = s:option(ListValue, "fallback_node", translate("Fallback Node")) o.group = {"",""} o:value("", translate("Close(Not use)")) o:value("_direct", translate("Direct Connection")) - o:depends({ [_n("protocol")] = "_balancing" }) + o:depends({ protocol = "_balancing" }) o.template = m:template_path("/cbi/nodes_listvalue") -- Maximum number of fallback nesting layers local MAX_FALLBACK_DEPTH = 3 @@ -234,11 +228,11 @@ if load_balancing_options then -- [[ Load balancing Start ]] end end - o = s:option(Flag, _n("useCustomProbeUrl"), translate("Use Custom Probe URL"), translate("By default the built-in probe URL will be used, enable this option to use a custom probe URL.")) - o:depends({ [_n("protocol")] = "_balancing" }) + o = s:option(Flag, "useCustomProbeUrl", translate("Use Custom Probe URL"), translate("By default the built-in probe URL will be used, enable this option to use a custom probe URL.")) + o:depends({ protocol = "_balancing" }) - o = s:option(Value, _n("probeUrl"), translate("Probe URL")) - o:depends({ [_n("useCustomProbeUrl")] = true }) + o = s:option(Value, "probeUrl", translate("Probe URL")) + o:depends({ useCustomProbeUrl = true }) o:value("https://cp.cloudflare.com/", "Cloudflare") o:value("https://www.gstatic.com/generate_204", "Gstatic") o:value("https://www.google.com/generate_204", "Google") @@ -249,23 +243,23 @@ if load_balancing_options then -- [[ Load balancing Start ]] o.default = o.keylist[3] o.description = translate("The URL used to detect the connection status.") - o = s:option(Value, _n("probeInterval"), translate("Probe Interval")) - o:depends({ [_n("protocol")] = "_balancing" }) + o = s:option(Value, "probeInterval", translate("Probe Interval")) + o:depends({ protocol = "_balancing" }) o.default = "1m" o.placeholder = "1m" o.description = translate("The interval between initiating probes.") .. "
" .. translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are s, m, h, which correspond to seconds, minutes, and hours, respectively.") .. "
" .. translate("When the unit is not filled in, it defaults to seconds.") - o = s:option(Value, _n("expected"), translate("Preferred Node Count")) - o:depends({ [_n("balancingStrategy")] = "leastLoad" }) + o = s:option(Value, "expected", translate("Preferred Node Count")) + o:depends({ balancingStrategy = "leastLoad" }) o.datatype = "uinteger" o.default = "2" o.placeholder = "2" o.description = translate("The load balancer selects the optimal number of nodes, and traffic is randomly distributed among them.") - o = s:option(Value, _n("tolerance"), translate("Failure Tolerance (%)")) - o:depends({ [_n("balancingStrategy")] = "leastLoad" }) + o = s:option(Value, "tolerance", translate("Failure Tolerance (%)")) + o:depends({ balancingStrategy = "leastLoad" }) o.datatype = "uinteger" o.default = "10" o.placeholder = "10" @@ -273,8 +267,8 @@ if load_balancing_options then -- [[ Load balancing Start ]] end -- [[ Load balancing End ]] if load_iface_options then -- [[ Custom Interface Start ]] - o = s:option(Value, _n("iface"), translate("Interface")) - o:depends({ [_n("protocol")] = "_iface" }) + o = s:option(Value, "iface", translate("Interface")) + o:depends({ protocol = "_iface" }) for _, d in ipairs(netdev_list) do o:value(d.name, d.label) end @@ -284,73 +278,72 @@ end -- [[ Custom Interface End ]] -- [[ Normal single node Start ]] if load_normal_options then -o = s:option(Value, _n("address"), translate("Address (Support Domain Name)")) +o = s:option(Value, "address", translate("Address (Support Domain Name)")) -o = s:option(Value, _n("port"), translate("Port")) +o = s:option(Value, "port", translate("Port")) o.datatype = "port" -o = s:option(Value, _n("uuid"), translate("ID")) +o = s:option(Value, "uuid", translate("ID")) o.password = true -o:depends({ [_n("protocol")] = "vmess" }) -o:depends({ [_n("protocol")] = "vless" }) +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) -o = s:option(Value, _n("username"), translate("Username")) -o:depends({ [_n("protocol")] = "http" }) -o:depends({ [_n("protocol")] = "socks" }) +o = s:option(Value, "username", translate("Username")) +o:depends({ protocol = "http" }) +o:depends({ protocol = "socks" }) -o = s:option(Value, _n("password"), translate("Password")) +o = s:option(Value, "password", translate("Password")) o.password = true -o:depends({ [_n("protocol")] = "http" }) -o:depends({ [_n("protocol")] = "socks" }) -o:depends({ [_n("protocol")] = "shadowsocks" }) -o:depends({ [_n("protocol")] = "trojan" }) +o:depends({ protocol = "http" }) +o:depends({ protocol = "socks" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "trojan" }) -o = s:option(ListValue, _n("security"), translate("Encrypt Method")) +o = s:option(ListValue, "security", translate("Encrypt Method")) for a, t in ipairs(security_list) do o:value(t) end -o:depends({ [_n("protocol")] = "vmess" }) +o:depends({ protocol = "vmess" }) -o = s:option(Value, _n("encryption"), translate("Encrypt Method") .. " (encryption)") +o = s:option(Value, "encryption", translate("Encrypt Method") .. " (encryption)") o.default = "none" o.placeholder = "none" -o:depends({ [_n("protocol")] = "vless" }) +o:depends({ protocol = "vless" }) o.validate = function(self, value) value = api.trim(value) return (value == "" and "none" or value) end -o = s:option(ListValue, _n("ss_method"), translate("Encrypt Method")) -o.rewrite_option = "method" +o = s:option(ListValue, "ss_method", translate("Encrypt Method")) for a, t in ipairs(ss_method_list) do o:value(t) end -o:depends({ [_n("protocol")] = "shadowsocks" }) +o:depends({ protocol = "shadowsocks" }) -o = s:option(ListValue, _n("flow"), translate("flow")) +o = s:option(ListValue, "flow", translate("flow")) o.default = "" o:value("", translate("Disable")) o:value("xtls-rprx-vision") o:value("xtls-rprx-vision-udp443") -o:depends({ [_n("protocol")] = "vless" }) +o:depends({ protocol = "vless" }) ---- [[hysteria2]] -o = s:option(Value, _n("hysteria2_hop"), translate("Port hopping range")) +o = s:option(Value, "hysteria2_hop", translate("Port hopping range")) o.description = translate("Format as 1000:2000 or 1000-2000 Multiple groups are separated by commas (,).") -o:depends({ [_n("protocol")] = "hysteria2", [_n("hysteria2_realms")] = false }) +o:depends({ protocol = "hysteria2", hysteria2_realms = false }) -o = s:option(Value, _n("hysteria2_hop_interval"), translate("Hop Interval(second)"), translate("Supports a fixed value or a random range (e.g., 30, 5-30), minimum 5.")) +o = s:option(Value, "hysteria2_hop_interval", translate("Hop Interval(second)"), translate("Supports a fixed value or a random range (e.g., 30, 5-30), minimum 5.")) o.datatype = "or(uinteger,portrange)" o.placeholder = "30" o.default = "30" -o:depends({ [_n("protocol")] = "hysteria2", [_n("hysteria2_realms")] = false }) +o:depends({ protocol = "hysteria2", hysteria2_realms = false }) -o = s:option(Flag, _n("hysteria2_realms"), translate("Realms")) +o = s:option(Flag, "hysteria2_realms", translate("Realms")) o.default = "0" if api.compare_versions(xray_version, ">", "26.5.9") then - o:depends({ [_n("protocol")] = "hysteria2"}) + o:depends({ protocol = "hysteria2"}) else - o:depends({ [_n("protocol")] = "__hide"}) + o:depends({ protocol = "__hide"}) end -o = s:option(Value, _n("hysteria2_realm_url"), translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name") -o:depends({ [_n("hysteria2_realms")] = "1" }) +o = s:option(Value, "hysteria2_realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name") +o:depends({ hysteria2_realms = "1" }) o.validate = function(self, value) value = api.trim(value) local realm = api.parse_realm_uri(value) @@ -358,73 +351,73 @@ o.validate = function(self, value) return nil, translate("Invalid Realm URL.") end -o = s:option(DynamicList, _n("hysteria2_realm_stun"), translate("Realm STUN")) +o = s:option(DynamicList, "hysteria2_realm_stun", translate("Realm STUN")) o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" } -o:depends({ [_n("hysteria2_realms")] = "1" }) +o:depends({ hysteria2_realms = "1" }) -o = s:option(Value, _n("hysteria2_auth_password"), translate("Auth Password")) +o = s:option(Value, "hysteria2_auth_password", translate("Auth Password")) o.password = true -o:depends({ [_n("protocol")] = "hysteria2"}) +o:depends({ protocol = "hysteria2"}) -o = s:option(ListValue, _n("hysteria2_obfs_type"), translate("Obfs Type")) +o = s:option(ListValue, "hysteria2_obfs_type", translate("Obfs Type")) o:value("", translate("Disable")) o:value("salamander") o:value("gecko") -o:depends({ [_n("protocol")] = "hysteria2" }) +o:depends({ protocol = "hysteria2" }) -o = s:option(Value, _n("hysteria2_obfs_password"), translate("Obfs Password")) -o:depends({ [_n("hysteria2_obfs_type")] = "salamander" }) -o:depends({ [_n("hysteria2_obfs_type")] = "gecko" }) +o = s:option(Value, "hysteria2_obfs_password", translate("Obfs Password")) +o:depends({ hysteria2_obfs_type = "salamander" }) +o:depends({ hysteria2_obfs_type = "gecko" }) -o = s:option(Value, _n("hysteria2_obfs_MinPacketSize"), translate("Gecko Packet Size (min)")) +o = s:option(Value, "hysteria2_obfs_MinPacketSize", translate("Gecko Packet Size (min)")) o.datatype = "uinteger" o.placeholder = "512" o.default = "512" -o:depends({ [_n("hysteria2_obfs_type")] = "gecko" }) +o:depends({ hysteria2_obfs_type = "gecko" }) -o = s:option(Value, _n("hysteria2_obfs_MaxPacketSize"), translate("Gecko Packet Size (max)")) +o = s:option(Value, "hysteria2_obfs_MaxPacketSize", translate("Gecko Packet Size (max)")) o.datatype = "uinteger" o.placeholder = "1200" o.default = "1200" -o:depends({ [_n("hysteria2_obfs_type")] = "gecko" }) +o:depends({ hysteria2_obfs_type = "gecko" }) -o = s:option(Value, _n("hysteria2_up_mbps"), translate("Max upload Mbps")) -o:depends({ [_n("protocol")] = "hysteria2" }) +o = s:option(Value, "hysteria2_up_mbps", translate("Max upload Mbps")) +o:depends({ protocol = "hysteria2" }) -o = s:option(Value, _n("hysteria2_down_mbps"), translate("Max download Mbps")) -o:depends({ [_n("protocol")] = "hysteria2" }) +o = s:option(Value, "hysteria2_down_mbps", translate("Max download Mbps")) +o:depends({ protocol = "hysteria2" }) -o = s:option(Value, _n("hysteria2_idle_timeout"), translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)") +o = s:option(Value, "hysteria2_idle_timeout", translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)") o.datatype = "range(4,120)" -o:depends({ [_n("protocol")] = "hysteria2"}) +o:depends({ protocol = "hysteria2"}) -o = s:option(Value, _n("hysteria2_keep_alive_period"), translate("QUIC KeepAlive interval"), translate("Units:seconds") .. " (2~60)") +o = s:option(Value, "hysteria2_keep_alive_period", translate("QUIC KeepAlive interval"), translate("Units:seconds") .. " (2~60)") o.datatype = "range(2,60)" -o:depends({ [_n("protocol")] = "hysteria2"}) +o:depends({ protocol = "hysteria2"}) -o = s:option(Flag, _n("hysteria2_disable_mtu_discovery"), translate("Disable MTU detection")) +o = s:option(Flag, "hysteria2_disable_mtu_discovery", translate("Disable MTU detection")) o.default = "0" -o:depends({ [_n("protocol")] = "hysteria2"}) +o:depends({ protocol = "hysteria2"}) ---- [[hysteria2 end]] -o = s:option(Flag, _n("tls"), translate("TLS")) +o = s:option(Flag, "tls", translate("TLS")) o.default = 0 -o:depends({ [_n("protocol")] = "vmess" }) -o:depends({ [_n("protocol")] = "vless" }) -o:depends({ [_n("protocol")] = "http" }) -o:depends({ [_n("protocol")] = "socks" }) -o:depends({ [_n("protocol")] = "trojan" }) -o:depends({ [_n("protocol")] = "shadowsocks" }) +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) +o:depends({ protocol = "http" }) +o:depends({ protocol = "socks" }) +o:depends({ protocol = "trojan" }) +o:depends({ protocol = "shadowsocks" }) -o = s:option(Flag, _n("reality"), translate("REALITY")) +o = s:option(Flag, "reality", translate("REALITY")) o.default = 0 -o:depends({ [_n("tls")] = true, [_n("transport")] = "raw" }) -o:depends({ [_n("tls")] = true, [_n("transport")] = "ws" }) -o:depends({ [_n("tls")] = true, [_n("transport")] = "grpc" }) -o:depends({ [_n("tls")] = true, [_n("transport")] = "httpupgrade" }) -o:depends({ [_n("tls")] = true, [_n("transport")] = "xhttp" }) +o:depends({ tls = true, transport = "raw" }) +o:depends({ tls = true, transport = "ws" }) +o:depends({ tls = true, transport = "grpc" }) +o:depends({ tls = true, transport = "httpupgrade" }) +o:depends({ tls = true, transport = "xhttp" }) -o = s:option(ListValue, _n("alpn"), translate("alpn")) +o = s:option(ListValue, "alpn", translate("alpn")) o.default = "default" o:value("default", translate("Default")) o:value("h3") @@ -433,72 +426,72 @@ o:value("h3,h2") o:value("http/1.1") o:value("h2,http/1.1") o:value("h3,h2,http/1.1") -o:depends({ [_n("tls")] = true, [_n("reality")] = false }) +o:depends({ tls = true, reality = false }) --- o = s:option(Value, _n("minversion"), translate("minversion")) +-- o = s:option(Value, "minversion", translate("minversion")) -- o.default = "1.3" -- o:value("1.3") --- o:depends({ [_n("tls")] = true }) +-- o:depends({ tls = true }) -o = s:option(Value, _n("tls_serverName"), "SNI " .. translate("Domain")) -o:depends({ [_n("tls")] = true }) -o:depends({ [_n("protocol")] = "hysteria2" }) +o = s:option(Value, "tls_serverName", "SNI " .. translate("Domain")) +o:depends({ tls = true }) +o:depends({ protocol = "hysteria2" }) -o = s:option(Value, _n("tls_pinSHA256"), translate("TLS Chain Fingerprint (SHA256)")) -o:depends({ [_n("tls")] = true, [_n("reality")] = false }) -o:depends({ [_n("protocol")] = "hysteria2" }) +o = s:option(Value, "tls_pinSHA256", translate("TLS Chain Fingerprint (SHA256)")) +o:depends({ tls = true, reality = false }) +o:depends({ protocol = "hysteria2" }) o.description = translate("Once set, connects only when the server’s chain fingerprint matches.") .. string.format("%s", "→ " .. translate("Fetch Manually")) -o = s:option(Value, _n("tls_CertByName"), translate("TLS Certificate Name (CertName)"), translate("TLS is used to verify the leaf certificate name.")) -o:depends({ [_n("tls")] = true, [_n("reality")] = false }) -o:depends({ [_n("protocol")] = "hysteria2" }) +o = s:option(Value, "tls_CertByName", translate("TLS Certificate Name (CertName)"), translate("TLS is used to verify the leaf certificate name.")) +o:depends({ tls = true, reality = false }) +o:depends({ protocol = "hysteria2" }) -o = s:option(Flag, _n("tls_certificate"), translate("TLS Certificate (PEM)")) +o = s:option(Flag, "tls_certificate", translate("TLS Certificate (PEM)")) o.default = "0" -o:depends({ [_n("tls")] = true, [_n("reality")] = false }) -o:depends({ [_n("protocol")] = "hysteria2" }) +o:depends({ tls = true, reality = false }) +o:depends({ protocol = "hysteria2" }) -o = s:option(TextValue, _n("tls_certificate_pem"), " ", translate("Full certificate (chain), PEM format.")) +o = s:option(TextValue, "tls_certificate_pem", " ", translate("Full certificate (chain), PEM format.")) o.default = "" o.rows = 5 o.wrap = "off" -o:depends({ [_n("tls_certificate")] = true }) +o:depends({ tls_certificate = true }) o.validate = function(self, value) value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n") return value end -o = s:option(Flag, _n("ech"), translate("ECH")) +o = s:option(Flag, "ech", translate("ECH")) o.default = "0" -o:depends({ [_n("tls")] = true, [_n("reality")] = false }) -o:depends({ [_n("protocol")] = "hysteria2", [_n("hysteria2_realms")] = false }) +o:depends({ tls = true, reality = false }) +o:depends({ protocol = "hysteria2", hysteria2_realms = false }) -o = s:option(TextValue, _n("ech_config"), translate("ECH Config")) +o = s:option(TextValue, "ech_config", translate("ECH Config")) o.default = "" o.rows = 5 o.wrap = "soft" -o:depends({ [_n("ech")] = true }) +o:depends({ ech = true }) o.validate = function(self, value) return api.trim(value:gsub("[\r\n]", "")) end -- [[ REALITY ]] -- -o = s:option(Value, _n("reality_publicKey"), translate("Public Key")) -o:depends({ [_n("tls")] = true, [_n("reality")] = true }) +o = s:option(Value, "reality_publicKey", translate("Public Key")) +o:depends({ tls = true, reality = true }) -o = s:option(Value, _n("reality_shortId"), translate("Short Id")) -o:depends({ [_n("tls")] = true, [_n("reality")] = true }) +o = s:option(Value, "reality_shortId", translate("Short Id")) +o:depends({ tls = true, reality = true }) -o = s:option(Value, _n("reality_spiderX"), translate("Spider X")) +o = s:option(Value, "reality_spiderX", translate("Spider X")) o.placeholder = "/" -o:depends({ [_n("tls")] = true, [_n("reality")] = true }) +o:depends({ tls = true, reality = true }) -o = s:option(Flag, _n("utls"), translate("uTLS")) +o = s:option(Flag, "utls", translate("uTLS")) o.default = "0" -o:depends({ [_n("tls")] = true, [_n("reality")] = false }) +o:depends({ tls = true, reality = false }) -o = s:option(ListValue, _n("fingerprint"), translate("Finger Print")) +o = s:option(ListValue, "fingerprint", translate("Finger Print")) o:value("chrome") o:value("firefox") o:value("edge") @@ -511,14 +504,14 @@ o:value("random") o:value("randomized") o:value("unsafe") o.default = "chrome" -o:depends({ [_n("tls")] = true, [_n("utls")] = true }) -o:depends({ [_n("tls")] = true, [_n("reality")] = true }) +o:depends({ tls = true, utls = true }) +o:depends({ tls = true, reality = true }) -o = s:option(Flag, _n("use_mldsa65Verify"), translate("ML-DSA-65")) +o = s:option(Flag, "use_mldsa65Verify", translate("ML-DSA-65")) o.default = "0" -o:depends({ [_n("tls")] = true, [_n("reality")] = true }) +o:depends({ tls = true, reality = true }) -o = s:option(DynamicList, _n("cipherSuites"), translate("Cipher Suites"), '***' .. " " .. translate("Configures the list of supported cipher suites.")) +o = s:option(DynamicList, "cipherSuites", translate("Cipher Suites"), '***' .. " " .. translate("Configures the list of supported cipher suites.")) o:value("TLS_AES_128_GCM_SHA256") o:value("TLS_AES_256_GCM_SHA384") o:value("TLS_CHACHA20_POLY1305_SHA256") @@ -532,154 +525,154 @@ o:value("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256") o:value("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384") o:value("TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256") o:value("TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256") -o:depends({ [_n("tls")] = true, [_n("reality")] = false }) +o:depends({ tls = true, reality = false }) -o = s:option(TextValue, _n("reality_mldsa65Verify"), "ML-DSA-65 " .. translate("Public key")) +o = s:option(TextValue, "reality_mldsa65Verify", "ML-DSA-65 " .. translate("Public key")) o.default = "" o.rows = 5 o.wrap = "soft" -o:depends({ [_n("use_mldsa65Verify")] = true }) +o:depends({ use_mldsa65Verify = true }) o.validate = function(self, value) return api.trim(value:gsub("[\r\n]", "")) end -o = s:option(ListValue, _n("transport"), translate("Transport")) +o = s:option(ListValue, "transport", translate("Transport")) o:value("raw", "RAW (TCP)") o:value("mkcp", "mKCP") o:value("ws", "WebSocket") o:value("grpc", "gRPC") o:value("httpupgrade", "HttpUpgrade") o:value("xhttp", "XHTTP") -o:depends({ [_n("protocol")] = "vmess" }) -o:depends({ [_n("protocol")] = "vless" }) -o:depends({ [_n("protocol")] = "socks" }) -o:depends({ [_n("protocol")] = "shadowsocks" }) -o:depends({ [_n("protocol")] = "trojan" }) +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) +o:depends({ protocol = "socks" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "trojan" }) -o = s:option(Value, _n("wireguard_public_key"), translate("Public Key")) -o:depends({ [_n("protocol")] = "wireguard" }) +o = s:option(Value, "wireguard_public_key", translate("Public Key")) +o:depends({ protocol = "wireguard" }) -o = s:option(Value, _n("wireguard_secret_key"), translate("Private Key")) -o:depends({ [_n("protocol")] = "wireguard" }) +o = s:option(Value, "wireguard_secret_key", translate("Private Key")) +o:depends({ protocol = "wireguard" }) -o = s:option(Value, _n("wireguard_preSharedKey"), translate("Pre shared key")) -o:depends({ [_n("protocol")] = "wireguard" }) +o = s:option(Value, "wireguard_preSharedKey", translate("Pre shared key")) +o:depends({ protocol = "wireguard" }) -o = s:option(DynamicList, _n("wireguard_local_address"), translate("Local Address")) -o:depends({ [_n("protocol")] = "wireguard" }) +o = s:option(DynamicList, "wireguard_local_address", translate("Local Address")) +o:depends({ protocol = "wireguard" }) -o = s:option(Value, _n("wireguard_mtu"), translate("MTU")) +o = s:option(Value, "wireguard_mtu", translate("MTU")) o.default = "1420" -o:depends({ [_n("protocol")] = "wireguard" }) +o:depends({ protocol = "wireguard" }) if api.compare_versions(xray_version, ">=", "1.8.0") then - o = s:option(Value, _n("wireguard_reserved"), translate("Reserved"), translate("Decimal numbers separated by \",\" or Base64-encoded strings.")) - o:depends({ [_n("protocol")] = "wireguard" }) + o = s:option(Value, "wireguard_reserved", translate("Reserved"), translate("Decimal numbers separated by \",\" or Base64-encoded strings.")) + o:depends({ protocol = "wireguard" }) end -o = s:option(Value, _n("wireguard_keepAlive"), translate("Keep Alive")) +o = s:option(Value, "wireguard_keepAlive", translate("Keep Alive")) o.default = "0" -o:depends({ [_n("protocol")] = "wireguard" }) +o:depends({ protocol = "wireguard" }) -- [[ RAW ]]-- -o = s:option(ListValue, _n("tcp_guise"), translate("Camouflage Type")) +o = s:option(ListValue, "tcp_guise", translate("Camouflage Type")) o:value("none", "none") o:value("http", "http") -o:depends({ [_n("transport")] = "raw" }) +o:depends({ transport = "raw" }) -o = s:option(DynamicList, _n("tcp_guise_http_host"), translate("HTTP Host")) -o:depends({ [_n("tcp_guise")] = "http" }) +o = s:option(DynamicList, "tcp_guise_http_host", translate("HTTP Host")) +o:depends({ tcp_guise = "http" }) -o = s:option(DynamicList, _n("tcp_guise_http_path"), translate("HTTP Path")) +o = s:option(DynamicList, "tcp_guise_http_path", translate("HTTP Path")) o.placeholder = "/" -o:depends({ [_n("tcp_guise")] = "http" }) +o:depends({ tcp_guise = "http" }) -- [[ mKCP ]]-- -o = s:option(ListValue, _n("mkcp_guise"), translate("Camouflage Type"), translate('
none: default, no masquerade, data sent is packets with no characteristics.
srtp: disguised as an SRTP packet, it will be recognized as video call data (such as FaceTime).
utp: packets disguised as uTP will be recognized as bittorrent downloaded data.
wechat-video: packets disguised as WeChat video calls.
dtls: disguised as DTLS 1.2 packet.
wireguard: disguised as a WireGuard packet. (not really WireGuard protocol)
dns: Disguising traffic as DNS requests.')) +o = s:option(ListValue, "mkcp_guise", translate("Camouflage Type"), translate('
none: default, no masquerade, data sent is packets with no characteristics.
srtp: disguised as an SRTP packet, it will be recognized as video call data (such as FaceTime).
utp: packets disguised as uTP will be recognized as bittorrent downloaded data.
wechat-video: packets disguised as WeChat video calls.
dtls: disguised as DTLS 1.2 packet.
wireguard: disguised as a WireGuard packet. (not really WireGuard protocol)
dns: Disguising traffic as DNS requests.')) for a, t in ipairs(header_type_list) do o:value(t) end -o:depends({ [_n("transport")] = "mkcp" }) +o:depends({ transport = "mkcp" }) -o = s:option(Value, _n("mkcp_domain"), translate("Camouflage Domain"), translate("Use it together with the DNS disguised type. You can fill in any domain.")) -o:depends({ [_n("mkcp_guise")] = "dns" }) +o = s:option(Value, "mkcp_domain", translate("Camouflage Domain"), translate("Use it together with the DNS disguised type. You can fill in any domain.")) +o:depends({ mkcp_guise = "dns" }) -o = s:option(Value, _n("mkcp_mtu"), translate("KCP MTU")) +o = s:option(Value, "mkcp_mtu", translate("KCP MTU")) o.datatype = "uinteger" o.default = 1350 -o:depends({ [_n("transport")] = "mkcp" }) +o:depends({ transport = "mkcp" }) -o = s:option(Value, _n("mkcp_seed"), translate("KCP Seed")) -o:depends({ [_n("transport")] = "mkcp" }) +o = s:option(Value, "mkcp_seed", translate("KCP Seed")) +o:depends({ transport = "mkcp" }) -- [[ WebSocket ]]-- -o = s:option(Value, _n("ws_host"), translate("WebSocket Host")) -o:depends({ [_n("transport")] = "ws" }) +o = s:option(Value, "ws_host", translate("WebSocket Host")) +o:depends({ transport = "ws" }) -o = s:option(Value, _n("ws_path"), translate("WebSocket Path")) +o = s:option(Value, "ws_path", translate("WebSocket Path")) o.placeholder = "/" -o:depends({ [_n("transport")] = "ws" }) +o:depends({ transport = "ws" }) -o = s:option(Value, _n("ws_heartbeatPeriod"), translate("HeartbeatPeriod(second)")) +o = s:option(Value, "ws_heartbeatPeriod", translate("HeartbeatPeriod(second)")) o.datatype = "integer" -o:depends({ [_n("transport")] = "ws" }) +o:depends({ transport = "ws" }) -- [[ gRPC ]]-- -o = s:option(Value, _n("grpc_serviceName"), "ServiceName") -o:depends({ [_n("transport")] = "grpc" }) +o = s:option(Value, "grpc_serviceName", "ServiceName") +o:depends({ transport = "grpc" }) -o = s:option(ListValue, _n("grpc_mode"), "gRPC " .. translate("Transfer mode")) +o = s:option(ListValue, "grpc_mode", "gRPC " .. translate("Transfer mode")) o:value("gun") o:value("multi") -o:depends({ [_n("transport")] = "grpc" }) +o:depends({ transport = "grpc" }) -o = s:option(Flag, _n("grpc_health_check"), translate("Health check")) -o:depends({ [_n("transport")] = "grpc" }) +o = s:option(Flag, "grpc_health_check", translate("Health check")) +o:depends({ transport = "grpc" }) -o = s:option(Value, _n("grpc_idle_timeout"), translate("Idle timeout")) +o = s:option(Value, "grpc_idle_timeout", translate("Idle timeout")) o.default = "10" -o:depends({ [_n("grpc_health_check")] = true }) +o:depends({ grpc_health_check = true }) -o = s:option(Value, _n("grpc_health_check_timeout"), translate("Health check timeout")) +o = s:option(Value, "grpc_health_check_timeout", translate("Health check timeout")) o.default = "20" -o:depends({ [_n("grpc_health_check")] = true }) +o:depends({ grpc_health_check = true }) -o = s:option(Flag, _n("grpc_permit_without_stream"), translate("Permit without stream")) +o = s:option(Flag, "grpc_permit_without_stream", translate("Permit without stream")) o.default = "0" -o:depends({ [_n("grpc_health_check")] = true }) +o:depends({ grpc_health_check = true }) -o = s:option(Value, _n("grpc_initial_windows_size"), translate("Initial Windows Size")) +o = s:option(Value, "grpc_initial_windows_size", translate("Initial Windows Size")) o.default = "0" -o:depends({ [_n("transport")] = "grpc" }) +o:depends({ transport = "grpc" }) -- [[ HttpUpgrade ]]-- -o = s:option(Value, _n("httpupgrade_host"), translate("HttpUpgrade Host")) -o:depends({ [_n("transport")] = "httpupgrade" }) +o = s:option(Value, "httpupgrade_host", translate("HttpUpgrade Host")) +o:depends({ transport = "httpupgrade" }) -o = s:option(Value, _n("httpupgrade_path"), translate("HttpUpgrade Path")) +o = s:option(Value, "httpupgrade_path", translate("HttpUpgrade Path")) o.placeholder = "/" -o:depends({ [_n("transport")] = "httpupgrade" }) +o:depends({ transport = "httpupgrade" }) -- [[ XHTTP ]]-- -o = s:option(ListValue, _n("xhttp_mode"), "XHTTP " .. translate("Mode")) -o:depends({ [_n("transport")] = "xhttp" }) +o = s:option(ListValue, "xhttp_mode", "XHTTP " .. translate("Mode")) +o:depends({ transport = "xhttp" }) o.default = "auto" o:value("auto") o:value("packet-up") o:value("stream-up") o:value("stream-one") -o = s:option(Value, _n("xhttp_host"), translate("XHTTP Host")) -o:depends({ [_n("transport")] = "xhttp" }) +o = s:option(Value, "xhttp_host", translate("XHTTP Host")) +o:depends({ transport = "xhttp" }) -o = s:option(Value, _n("xhttp_path"), translate("XHTTP Path")) +o = s:option(Value, "xhttp_path", translate("XHTTP Path")) o.placeholder = "/" -o:depends({ [_n("transport")] = "xhttp" }) +o:depends({ transport = "xhttp" }) -o = s:option(Flag, _n("use_xhttp_extra"), translate("XHTTP Extra")) +o = s:option(Flag, "use_xhttp_extra", translate("XHTTP Extra")) o.default = "0" -o:depends({ [_n("transport")] = "xhttp" }) +o:depends({ transport = "xhttp" }) -o = s:option(TextValue, _n("xhttp_extra"), " ", translate("An XHttpObject in JSON format, used for sharing.")) -o:depends({ [_n("use_xhttp_extra")] = true }) +o = s:option(TextValue, "xhttp_extra", " ", translate("An XHttpObject in JSON format, used for sharing.")) +o:depends({ use_xhttp_extra = true }) o.rows = 10 o.wrap = "off" o.datatype = "json" @@ -698,7 +691,7 @@ o.custom_cfgvalue = function(self, section, value) end o.custom_write = function(self, section, value) m:set(section, "xhttp_extra", api.base64Encode(value) or "") - local success, data = pcall(jsonc.parse, value) + local success, data = pcall(api.jsonc.parse, value) if success and data then local address = (data.extra and data.extra.downloadSettings and data.extra.downloadSettings.address) or (data.downloadSettings and data.downloadSettings.address) @@ -718,7 +711,7 @@ o.custom_remove = function(self, section, value) end -- [[ User-Agent ]]-- -o = s:option(Value, _n("user_agent"), translate("User-Agent")) +o = s:option(Value, "user_agent", translate("User-Agent")) o.default = "" o:value("", translate("default")) o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36", "chrome") @@ -727,44 +720,44 @@ o:value("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (K o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.70", "edge") o:value("Go-http-client/1.1", "golang") o:value("curl/7.68.0", "curl") -o:depends({ [_n("tcp_guise")] = "http" }) -o:depends({ [_n("transport")] = "ws" }) -o:depends({ [_n("transport")] = "httpupgrade" }) -o:depends({ [_n("transport")] = "xhttp" }) -o:depends({ [_n("transport")] = "grpc" }) +o:depends({ tcp_guise = "http" }) +o:depends({ transport = "ws" }) +o:depends({ transport = "httpupgrade" }) +o:depends({ transport = "xhttp" }) +o:depends({ transport = "grpc" }) -- [[ Mux.Cool ]]-- -o = s:option(Flag, _n("mux"), "Mux", translate("Enable Mux.Cool")) -o:depends({ [_n("protocol")] = "vmess" }) -o:depends({ [_n("protocol")] = "vless", [_n("transport")] = "raw" }) -o:depends({ [_n("protocol")] = "vless", [_n("transport")] = "ws" }) -o:depends({ [_n("protocol")] = "vless", [_n("transport")] = "grpc" }) -o:depends({ [_n("protocol")] = "vless", [_n("transport")] = "httpupgrade" }) -o:depends({ [_n("protocol")] = "http" }) -o:depends({ [_n("protocol")] = "socks" }) -o:depends({ [_n("protocol")] = "shadowsocks" }) -o:depends({ [_n("protocol")] = "trojan" }) +o = s:option(Flag, "mux", "Mux", translate("Enable Mux.Cool")) +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless", transport = "raw" }) +o:depends({ protocol = "vless", transport = "ws" }) +o:depends({ protocol = "vless", transport = "grpc" }) +o:depends({ protocol = "vless", transport = "httpupgrade" }) +o:depends({ protocol = "http" }) +o:depends({ protocol = "socks" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "trojan" }) -o = s:option(Value, _n("mux_concurrency"), translate("Mux concurrency")) +o = s:option(Value, "mux_concurrency", translate("Mux concurrency")) o.default = -1 -o:depends({ [_n("mux")] = true }) +o:depends({ mux = true }) -o = s:option(Value, _n("xudp_concurrency"), translate("XUDP Mux concurrency")) +o = s:option(Value, "xudp_concurrency", translate("XUDP Mux concurrency")) o.default = 8 -o:depends({ [_n("mux")] = true }) +o:depends({ mux = true }) --[[FinalMask]] -o = s:option(Flag, _n("use_finalmask"), "FinalMask") +o = s:option(Flag, "use_finalmask", "FinalMask") o.default = "0" -o:depends({ [_n("protocol")] = "vmess" }) -o:depends({ [_n("protocol")] = "vless" }) -o:depends({ [_n("protocol")] = "trojan" }) -o:depends({ [_n("protocol")] = "shadowsocks" }) -o:depends({ [_n("protocol")] = "wireguard" }) -o:depends({ [_n("protocol")] = "hysteria2", [_n("hysteria2_realms")] = false }) +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) +o:depends({ protocol = "trojan" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "wireguard" }) +o:depends({ protocol = "hysteria2", hysteria2_realms = false }) -o = s:option(TextValue, _n("finalmask"), " ") -o:depends({ [_n("use_finalmask")] = true }) +o = s:option(TextValue, "finalmask", " ") +o:depends({ use_finalmask = true }) o.rows = 10 o.wrap = "off" o.description = translate("An FinalMaskObject in JSON format, used for sharing.") .. "
" .. @@ -788,62 +781,62 @@ o.custom_write = function(self, section, value) end --[[Fast Open]] -o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required")) +o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open"), translate("Need node support required")) o.default = 0 --[[tcpMptcp]] -o = s:option(Flag, _n("tcpMptcp"), "tcpMptcp", translate("Enable Multipath TCP, need to be enabled in both server and client configuration.")) +o = s:option(Flag, "tcpMptcp", "tcpMptcp", translate("Enable Multipath TCP, need to be enabled in both server and client configuration.")) o.default = 0 -o = s:option(ListValue, _n("domain_resolver"), translate("Domain DNS Resolve"), translate("If the node address is a domain name, this DNS will be used for resolution.")) +o = s:option(ListValue, "domain_resolver", translate("Domain DNS Resolve"), translate("If the node address is a domain name, this DNS will be used for resolution.")) o:value("", translate("Auto")) o:value("tcp", "TCP") o:value("udp", "UDP") o:value("https", "HTTPS") -o = s:option(Value, _n("domain_resolver_dns"), "DNS") +o = s:option(Value, "domain_resolver_dns", "DNS") o.datatype = "or(ipaddr,ipaddrport)" o:value("114.114.114.114") o:value("223.5.5.5:53") o.default = o.keylist[1] -o:depends({ [_n("domain_resolver")] = "tcp" }) -o:depends({ [_n("domain_resolver")] = "udp" }) +o:depends({ domain_resolver = "tcp" }) +o:depends({ domain_resolver = "udp" }) -o = s:option(Value, _n("domain_resolver_dns_https"), "DNS") +o = s:option(Value, "domain_resolver_dns_https", "DNS") o:value("https://120.53.53.53/dns-query", "DNSPod") o:value("https://223.5.5.5/dns-query", "AliDNS") o.default = o.keylist[1] -o:depends({ [_n("domain_resolver")] = "https" }) +o:depends({ domain_resolver = "https" }) -o = s:option(ListValue, _n("domain_strategy"), translate("Domain Strategy"), translate("If is domain name, The requested domain name will be resolved to IP before connect.")) +o = s:option(ListValue, "domain_strategy", translate("Domain Strategy"), translate("If is domain name, The requested domain name will be resolved to IP before connect.")) o.default = "" o:value("", translate("Auto")) o:value("UseIPv4", translate("IPv4 Only")) o:value("UseIPv6", translate("IPv6 Only")) -o = s:option(Flag, _n("happy_eyeballs"), translate("Enable Happy Eyeballs"), translate("Attempts IPv4 and IPv6 simultaneously; automatically uses the faster connection.")) +o = s:option(Flag, "happy_eyeballs", translate("Enable Happy Eyeballs"), translate("Attempts IPv4 and IPv6 simultaneously; automatically uses the faster connection.")) o.default = 0 -local protocols = s.fields[_n("protocol")].keylist +local protocols = s.fields["protocol"].keylist if #protocols > 0 then for i, v in ipairs(protocols) do if not v:find("^_") then - local depends_condition = { [_n("protocol")] = v } + local depends_condition = { protocol = v } if v == "hysteria2" then - depends_condition[_n("hysteria2_realms")] = false + depends_condition["hysteria2_realms"] = false end - s.fields[_n("address")]:depends(depends_condition) - s.fields[_n("port")]:depends(depends_condition) - s.fields[_n("domain_resolver")]:depends(depends_condition) - s.fields[_n("happy_eyeballs")]:depends(depends_condition) + s.fields["address"]:depends(depends_condition) + s.fields["port"]:depends(depends_condition) + s.fields["domain_resolver"]:depends(depends_condition) + s.fields["happy_eyeballs"]:depends(depends_condition) local strategy_depends = api.clone(depends_condition) - strategy_depends[_n("happy_eyeballs")] = false - s.fields[_n("domain_strategy")]:depends(strategy_depends) + strategy_depends["happy_eyeballs"] = false + s.fields["domain_strategy"]:depends(strategy_depends) if v ~= "hysteria2" then - s.fields[_n("tcp_fast_open")]:depends({ [_n("protocol")] = v }) - s.fields[_n("tcpMptcp")]:depends({ [_n("protocol")] = v }) + s.fields["tcp_fast_open"]:depends({ protocol = v }) + s.fields["tcpMptcp"]:depends({ protocol = v }) end end end @@ -852,7 +845,7 @@ end -- [[ Normal single node End ]] if not load_shunt_options then - o = s:option(ListValue, _n("chain_proxy"), translate("Chain Proxy")) + o = s:option(ListValue, "chain_proxy", translate("Chain Proxy")) o:value("", translate("Close(Not use)")) if not (load_iface_options or load_balancing_options) then -- Special node cannot be use pre-proxy. @@ -861,20 +854,20 @@ if not load_shunt_options then end o:value("2", translate("Landing Node")) - o1 = s:option(ListValue, _n("preproxy_node"), translate("Preproxy Node"), translate("Only support a layer of proxy.")) - o1:depends({ [_n("chain_proxy")] = "1", [_n("hysteria2_realms")] = false }) + o1 = s:option(ListValue, "preproxy_node", translate("Preproxy Node"), translate("Only support a layer of proxy.")) + o1:depends({ chain_proxy = "1", hysteria2_realms = false }) o1.template = m:template_path("/cbi/nodes_listvalue") o1.group = {} - o3 = s:option(Value, _n("outbound_iface"), translate("Outbound Interface")) - o3:depends({ [_n("chain_proxy")] = "3" }) + o3 = s:option(Value, "outbound_iface", translate("Outbound Interface")) + o3:depends({ chain_proxy = "3" }) o3:value("", translate("All")) for _, d in ipairs(netdev_list) do o3:value(d.name, d.label) end - o2 = s:option(ListValue, _n("to_node"), translate("Landing Node"), translate("Only support a layer of proxy.")) - o2:depends({ [_n("chain_proxy")] = "2", [_n("hysteria2_realms")] = false }) + o2 = s:option(ListValue, "to_node", translate("Landing Node"), translate("Only support a layer of proxy.")) + o2:depends({ chain_proxy = "2", hysteria2_realms = false }) o2.template = m:template_path("/cbi/nodes_listvalue") o2.group = {} @@ -895,12 +888,12 @@ if not load_shunt_options then end end -api.luci_types(arg[1], m, s, type_name, option_prefix) +api.luci_types(s1, s) if load_shunt_options then local current_node = m:get(arg[1]) or {} local shunt_lua = loadfile("/usr/lib/lua/luci/model/cbi/passwall2/client/include/shunt_options.lua") - setfenv(shunt_lua, getfenv(1))(m, s, { + setfenv(shunt_lua, getfenv(1))(m, s1, { node_id = arg[1], node = current_node, node_list = node_list, diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua index 70b3144a..c2fc0347 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua @@ -1,33 +1,29 @@ -local m, s = ... - local singbox_bin = api.finded_com("sing-box") if not singbox_bin then return end -type_name = "sing-box" - -- [[ sing-box ]] +local m, s1 = ... +local type_name = "sing-box" -s.fields["type"]:value(type_name, "Sing-Box") -if not s.fields["type"].default then - s.fields["type"].default = type_name +s1.fields["type"]:value(type_name, "Sing-Box") +if not s1.fields["type"].default then + s1.fields["type"].default = type_name end -if s.val["type"] ~= type_name then +if s1.val["type"] ~= type_name then return end -local option_prefix = "singbox_" +local s = NamedSection(m, arg[1], "server") +s.type_name = type_name +s.option_prefix = "singbox_" -local function _n(name) - return option_prefix .. name -end +local formvalue_proto = luci.http.formvalue(formvalue_key .. "protocol") -local formvalue_proto = luci.http.formvalue(formvalue_key .. _n("protocol")) - -if formvalue_proto then s.val["protocol"] = formvalue_proto end +if formvalue_proto then s1.val["protocol"] = formvalue_proto end local arg_select_proto = luci.http.formvalue("select_proto") or "" @@ -45,7 +41,7 @@ local singbox_tags = luci.sys.exec(singbox_bin .. " version | grep 'Tags:' | aw local singbox_version = api.get_app_version("sing-box"):match("[^v]+") -o = s:option(ListValue, _n("protocol"), translate("Protocol")) +o = s:option(ListValue, "protocol", translate("Protocol")) o:value("socks", "Socks") o:value("http", "HTTP") o:value("shadowsocks", "Shadowsocks") @@ -79,13 +75,13 @@ function o.custom_cfgvalue(self, section) if arg_select_proto ~= "" then return arg_select_proto else - return m:get(section, self.option:sub(1 + #option_prefix)) + return m:get(section, self.config_option) end end -local load_urltest_options = s.val["protocol"] == "_urltest" or arg_select_proto == "_urltest" -local load_shunt_options = s.val["protocol"] == "_shunt" or arg_select_proto == "_shunt" -local load_iface_options = s.val["protocol"] == "_iface" or arg_select_proto == "_iface" +local load_urltest_options = s1.val["protocol"] == "_urltest" or arg_select_proto == "_urltest" +local load_shunt_options = s1.val["protocol"] == "_shunt" or arg_select_proto == "_shunt" +local load_iface_options = s1.val["protocol"] == "_iface" or arg_select_proto == "_iface" local load_normal_options = true if load_urltest_options or load_shunt_options or load_iface_options then load_normal_options = nil @@ -98,14 +94,14 @@ local netdev_list = api.get_network_devices() local node_list = api.get_node_list() if load_urltest_options then -- [[ URLTest Start ]] - o = s:option(ListValue, _n("node_add_mode"), translate("Node Addition Method")) - o:depends({ [_n("protocol")] = "_urltest" }) + o = s:option(ListValue, "node_add_mode", translate("Node Addition Method")) + o:depends({ protocol = "_urltest" }) o.default = "manual" o:value("manual", translate("Manual")) o:value("batch", translate("Batch")) - o = s:option(MultiValue, _n("urltest_node"), translate("URLTest node list"), translate("List of nodes to test, document")) - o:depends({ [_n("node_add_mode")] = "manual" }) + o = s:option(MultiValue, "urltest_node", translate("URLTest node list"), translate("List of nodes to test, document")) + o:depends({ node_add_mode = "manual" }) o.widget = "checkbox" o.template = m:template_path("/cbi/nodes_multivalue") o.group = {} @@ -142,16 +138,16 @@ if load_urltest_options then -- [[ URLTest Start ]] end end - o = s:option(MultiValue, _n("node_group"), translate("Select Group")) - o:depends({ [_n("node_add_mode")] = "batch" }) + o = s:option(MultiValue, "node_group", translate("Select Group")) + o:depends({ node_add_mode = "batch" }) o.widget = "checkbox" o:value("default", translate("default")) for k, v in pairs(groups) do o:value(api.UrlEncode(k), k) end - o = s:option(Value, _n("node_match_rule"), translate("Node Matching Rules")) - o:depends({ [_n("node_add_mode")] = "batch" }) + o = s:option(Value, "node_match_rule", translate("Node Matching Rules")) + o:depends({ node_add_mode = "batch" }) local descrStr = "Example: ^A && B && !C && D$
" descrStr = descrStr .. "This means the node remark must start with A (^), include B, exclude C (!), and end with D ($).
" descrStr = descrStr .. "Conditions are joined by && (AND), and their order does not affect the result.
" @@ -159,8 +155,8 @@ if load_urltest_options then -- [[ URLTest Start ]] descrStr = descrStr .. "Example: A && B || C && D means (A AND B) OR (C AND D)." o.description = translate(descrStr) - o = s:option(Value, _n("urltest_url"), translate("Probe URL")) - o:depends({ [_n("protocol")] = "_urltest" }) + o = s:option(Value, "urltest_url", translate("Probe URL")) + o:depends({ protocol = "_urltest" }) o:value("https://cp.cloudflare.com/", "Cloudflare") o:value("https://www.gstatic.com/generate_204", "Gstatic") o:value("https://www.google.com/generate_204", "Google") @@ -171,8 +167,8 @@ if load_urltest_options then -- [[ URLTest Start ]] o.default = o.keylist[3] o.description = translate("The URL used to detect the connection status.") - o = s:option(Value, _n("urltest_interval"), translate("Test interval")) - o:depends({ [_n("protocol")] = "_urltest" }) + o = s:option(Value, "urltest_interval", translate("Test interval")) + o:depends({ protocol = "_urltest" }) o.default = "3m" o.placeholder = "3m" o.description = translate("The interval between initiating probes.") .. "
" .. @@ -180,29 +176,29 @@ if load_urltest_options then -- [[ URLTest Start ]] translate("When the unit is not filled in, it defaults to seconds.") .. "
" .. translate("Test interval must be less or equal than idle timeout.") - o = s:option(Value, _n("urltest_tolerance"), translate("Test tolerance"), translate("The test tolerance in milliseconds.")) - o:depends({ [_n("protocol")] = "_urltest" }) + o = s:option(Value, "urltest_tolerance", translate("Test tolerance"), translate("The test tolerance in milliseconds.")) + o:depends({ protocol = "_urltest" }) o.datatype = "uinteger" o.placeholder = "50" o.default = "50" - o = s:option(Value, _n("urltest_idle_timeout"), translate("Idle timeout")) - o:depends({ [_n("protocol")] = "_urltest" }) + o = s:option(Value, "urltest_idle_timeout", translate("Idle timeout")) + o:depends({ protocol = "_urltest" }) o.placeholder = "30m" o.default = "30m" o.description = translate("The idle timeout.") .. "
" .. translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are s, m, h, which correspond to seconds, minutes, and hours, respectively.") .. "
" .. translate("When the unit is not filled in, it defaults to seconds.") - o = s:option(Flag, _n("urltest_interrupt_exist_connections"), translate("Interrupt existing connections")) - o:depends({ [_n("protocol")] = "_urltest" }) + o = s:option(Flag, "urltest_interrupt_exist_connections", translate("Interrupt existing connections")) + o:depends({ protocol = "_urltest" }) o.default = "0" o.description = translate("Interrupt existing connections when the selected outbound has changed.") end -- [[ URLTest End ]] if load_iface_options then -- [[ Custom Interface Start ]] - o = s:option(Value, _n("iface"), translate("Interface")) - o:depends({ [_n("protocol")] = "_iface" }) + o = s:option(Value, "iface", translate("Interface")) + o:depends({ protocol = "_iface" }) for _, d in ipairs(netdev_list) do o:value(d.name, d.label) end @@ -212,50 +208,48 @@ end -- [[ Custom Interface End ]] -- [[ Normal single node Start ]] if load_normal_options then -o = s:option(Value, _n("address"), translate("Address (Support Domain Name)")) +o = s:option(Value, "address", translate("Address (Support Domain Name)")) -o = s:option(Value, _n("port"), translate("Port")) +o = s:option(Value, "port", translate("Port")) o.datatype = "port" -o = s:option(Value, _n("uuid"), translate("ID")) +o = s:option(Value, "uuid", translate("ID")) o.password = true -o:depends({ [_n("protocol")] = "vmess" }) -o:depends({ [_n("protocol")] = "vless" }) -o:depends({ [_n("protocol")] = "tuic" }) +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) +o:depends({ protocol = "tuic" }) -o = s:option(Value, _n("username"), translate("Username")) -o:depends({ [_n("protocol")] = "http" }) -o:depends({ [_n("protocol")] = "socks" }) -o:depends({ [_n("protocol")] = "ssh" }) -o:depends({ [_n("protocol")] = "naive" }) +o = s:option(Value, "username", translate("Username")) +o:depends({ protocol = "http" }) +o:depends({ protocol = "socks" }) +o:depends({ protocol = "ssh" }) +o:depends({ protocol = "naive" }) -o = s:option(Value, _n("password"), translate("Password")) +o = s:option(Value, "password", translate("Password")) o.password = true -o:depends({ [_n("protocol")] = "http" }) -o:depends({ [_n("protocol")] = "socks" }) -o:depends({ [_n("protocol")] = "shadowsocks" }) -o:depends({ [_n("protocol")] = "shadowsocksr" }) -o:depends({ [_n("protocol")] = "trojan" }) -o:depends({ [_n("protocol")] = "tuic" }) -o:depends({ [_n("protocol")] = "anytls" }) -o:depends({ [_n("protocol")] = "ssh" }) -o:depends({ [_n("protocol")] = "naive" }) +o:depends({ protocol = "http" }) +o:depends({ protocol = "socks" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "shadowsocksr" }) +o:depends({ protocol = "trojan" }) +o:depends({ protocol = "tuic" }) +o:depends({ protocol = "anytls" }) +o:depends({ protocol = "ssh" }) +o:depends({ protocol = "naive" }) -o = s:option(ListValue, _n("security"), translate("Encrypt Method")) +o = s:option(ListValue, "security", translate("Encrypt Method")) for a, t in ipairs(security_list) do o:value(t) end -o:depends({ [_n("protocol")] = "vmess" }) +o:depends({ protocol = "vmess" }) -o = s:option(ListValue, _n("ss_method"), translate("Encrypt Method")) -o.rewrite_option = "method" +o = s:option(ListValue, "ss_method", translate("Encrypt Method")) for a, t in ipairs(ss_method_new_list) do o:value(t) end for a, t in ipairs(ss_method_old_list) do o:value(t) end -o:depends({ [_n("protocol")] = "shadowsocks" }) +o:depends({ protocol = "shadowsocks" }) if singbox_tags:find("with_shadowsocksr") then - o = s:option(ListValue, _n("ssr_method"), translate("Encrypt Method")) - o.rewrite_option = "method" + o = s:option(ListValue, "ssr_method", translate("Encrypt Method")) for a, t in ipairs(ss_method_old_list) do o:value(t) end - o:depends({ [_n("protocol")] = "shadowsocksr" }) + o:depends({ protocol = "shadowsocksr" }) local ssr_protocol_list = { "origin", "verify_simple", "verify_deflate", "verify_sha1", "auth_simple", @@ -264,122 +258,122 @@ if singbox_tags:find("with_shadowsocksr") then "auth_chain_d", "auth_chain_e", "auth_chain_f" } - o = s:option(ListValue, _n("ssr_protocol"), translate("Protocol")) + o = s:option(ListValue, "ssr_protocol", translate("Protocol")) for a, t in ipairs(ssr_protocol_list) do o:value(t) end - o:depends({ [_n("protocol")] = "shadowsocksr" }) + o:depends({ protocol = "shadowsocksr" }) - o = s:option(Value, _n("ssr_protocol_param"), translate("Protocol_param")) - o:depends({ [_n("protocol")] = "shadowsocksr" }) + o = s:option(Value, "ssr_protocol_param", translate("Protocol_param")) + o:depends({ protocol = "shadowsocksr" }) local ssr_obfs_list = { "plain", "http_simple", "http_post", "random_head", "tls_simple", "tls1.0_session_auth", "tls1.2_ticket_auth" } - o = s:option(ListValue, _n("ssr_obfs"), translate("Obfs")) + o = s:option(ListValue, "ssr_obfs", translate("Obfs")) for a, t in ipairs(ssr_obfs_list) do o:value(t) end - o:depends({ [_n("protocol")] = "shadowsocksr" }) + o:depends({ protocol = "shadowsocksr" }) - o = s:option(Value, _n("ssr_obfs_param"), translate("Obfs_param")) - o:depends({ [_n("protocol")] = "shadowsocksr" }) + o = s:option(Value, "ssr_obfs_param", translate("Obfs_param")) + o:depends({ protocol = "shadowsocksr" }) end -o = s:option(Flag, _n("uot"), translate("UDP over TCP")) -o:depends({ [_n("protocol")] = "socks" }) -o:depends({ [_n("protocol")] = "shadowsocks" }) -o:depends({ [_n("protocol")] = "naive" }) +o = s:option(Flag, "uot", translate("UDP over TCP")) +o:depends({ protocol = "socks" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "naive" }) -o = s:option(Value, _n("alter_id"), "Alter ID") +o = s:option(Value, "alter_id", "Alter ID") o.datatype = "uinteger" o.default = "0" -o:depends({ [_n("protocol")] = "vmess" }) +o:depends({ protocol = "vmess" }) -o = s:option(Flag, _n("global_padding"), "global_padding", translate("Protocol parameter. Will waste traffic randomly if enabled.")) +o = s:option(Flag, "global_padding", "global_padding", translate("Protocol parameter. Will waste traffic randomly if enabled.")) o.default = "0" -o:depends({ [_n("protocol")] = "vmess" }) +o:depends({ protocol = "vmess" }) -o = s:option(Flag, _n("authenticated_length"), "authenticated_length", translate("Protocol parameter. Enable length block encryption.")) +o = s:option(Flag, "authenticated_length", "authenticated_length", translate("Protocol parameter. Enable length block encryption.")) o.default = "0" -o:depends({ [_n("protocol")] = "vmess" }) +o:depends({ protocol = "vmess" }) -o = s:option(ListValue, _n("flow"), translate("flow")) +o = s:option(ListValue, "flow", translate("flow")) o.default = "" o:value("", translate("Disable")) o:value("xtls-rprx-vision") -o:depends({ [_n("protocol")] = "vless", [_n("tls")] = true }) +o:depends({ protocol = "vless", tls = true }) if singbox_tags:find("with_quic") then - o = s:option(Value, _n("hysteria_hop"), translate("Port hopping range")) + o = s:option(Value, "hysteria_hop", translate("Port hopping range")) o.description = translate("Format as 1000:2000 or 1000-2000 Multiple groups are separated by commas (,).") - o:depends({ [_n("protocol")] = "hysteria" }) + o:depends({ protocol = "hysteria" }) - o = s:option(Value, _n("hysteria_hop_interval"), translate("Hop Interval(second)"), translate("Example:") .. "30 (≥5)") + o = s:option(Value, "hysteria_hop_interval", translate("Hop Interval(second)"), translate("Example:") .. "30 (≥5)") o.datatype = "uinteger" o.placeholder = "30" o.default = "30" - o:depends({ [_n("protocol")] = "hysteria" }) + o:depends({ protocol = "hysteria" }) - o = s:option(Value, _n("hysteria_obfs"), translate("Obfs Password")) - o:depends({ [_n("protocol")] = "hysteria" }) + o = s:option(Value, "hysteria_obfs", translate("Obfs Password")) + o:depends({ protocol = "hysteria" }) - o = s:option(ListValue, _n("hysteria_auth_type"), translate("Auth Type")) + o = s:option(ListValue, "hysteria_auth_type", translate("Auth Type")) o:value("disable", translate("Disable")) o:value("string", translate("STRING")) o:value("base64", translate("BASE64")) - o:depends({ [_n("protocol")] = "hysteria" }) + o:depends({ protocol = "hysteria" }) - o = s:option(Value, _n("hysteria_auth_password"), translate("Auth Password")) + o = s:option(Value, "hysteria_auth_password", translate("Auth Password")) o.password = true - o:depends({ [_n("protocol")] = "hysteria", [_n("hysteria_auth_type")] = "string"}) - o:depends({ [_n("protocol")] = "hysteria", [_n("hysteria_auth_type")] = "base64"}) + o:depends({ protocol = "hysteria", hysteria_auth_type = "string"}) + o:depends({ protocol = "hysteria", hysteria_auth_type = "base64"}) - o = s:option(Value, _n("hysteria_up_mbps"), translate("Max upload Mbps")) + o = s:option(Value, "hysteria_up_mbps", translate("Max upload Mbps")) o.default = "10" - o:depends({ [_n("protocol")] = "hysteria" }) + o:depends({ protocol = "hysteria" }) - o = s:option(Value, _n("hysteria_down_mbps"), translate("Max download Mbps")) + o = s:option(Value, "hysteria_down_mbps", translate("Max download Mbps")) o.default = "50" - o:depends({ [_n("protocol")] = "hysteria" }) + o:depends({ protocol = "hysteria" }) - o = s:option(Value, _n("hysteria_recv_window_conn"), translate("QUIC stream receive window")) - o:depends({ [_n("protocol")] = "hysteria" }) + o = s:option(Value, "hysteria_recv_window_conn", translate("QUIC stream receive window")) + o:depends({ protocol = "hysteria" }) - o = s:option(Value, _n("hysteria_recv_window"), translate("QUIC connection receive window")) - o:depends({ [_n("protocol")] = "hysteria" }) + o = s:option(Value, "hysteria_recv_window", translate("QUIC connection receive window")) + o:depends({ protocol = "hysteria" }) - o = s:option(Flag, _n("hysteria_disable_mtu_discovery"), translate("Disable MTU detection")) - o:depends({ [_n("protocol")] = "hysteria" }) + o = s:option(Flag, "hysteria_disable_mtu_discovery", translate("Disable MTU detection")) + o:depends({ protocol = "hysteria" }) end if singbox_tags:find("with_quic") then - o = s:option(ListValue, _n("tuic_congestion_control"), translate("Congestion control algorithm")) + o = s:option(ListValue, "tuic_congestion_control", translate("Congestion control algorithm")) o.default = "cubic" o:value("bbr", translate("BBR")) o:value("cubic", translate("CUBIC")) o:value("new_reno", translate("New Reno")) - o:depends({ [_n("protocol")] = "tuic" }) + o:depends({ protocol = "tuic" }) - o = s:option(ListValue, _n("tuic_udp_relay_mode"), translate("UDP relay mode")) + o = s:option(ListValue, "tuic_udp_relay_mode", translate("UDP relay mode")) o.default = "native" o:value("native", translate("native")) o:value("quic", translate("QUIC")) - o:depends({ [_n("protocol")] = "tuic" }) + o:depends({ protocol = "tuic" }) --[[ - o = s:option(Flag, _n("tuic_udp_over_stream"), translate("UDP over stream")) - o:depends({ [_n("protocol")] = "tuic" }) + o = s:option(Flag, "tuic_udp_over_stream", translate("UDP over stream")) + o:depends({ protocol = "tuic" }) ]]-- - o = s:option(Flag, _n("tuic_zero_rtt_handshake"), translate("Enable 0-RTT QUIC handshake")) + o = s:option(Flag, "tuic_zero_rtt_handshake", translate("Enable 0-RTT QUIC handshake")) o.default = 0 - o:depends({ [_n("protocol")] = "tuic" }) + o:depends({ protocol = "tuic" }) - o = s:option(Value, _n("tuic_heartbeat"), translate("Heartbeat interval(second)")) + o = s:option(Value, "tuic_heartbeat", translate("Heartbeat interval(second)")) o.datatype = "uinteger" o.default = "3" - o:depends({ [_n("protocol")] = "tuic" }) + o:depends({ protocol = "tuic" }) - o = s:option(ListValue, _n("tuic_alpn"), translate("QUIC TLS ALPN")) + o = s:option(ListValue, "tuic_alpn", translate("QUIC TLS ALPN")) o.default = "default" o:value("default", translate("Default")) o:value("h3") @@ -390,30 +384,30 @@ if singbox_tags:find("with_quic") then o:value("h3,h2,http/1.1") o:value("spdy/3.1") o:value("h3,spdy/3.1") - o:depends({ [_n("protocol")] = "tuic" }) + o:depends({ protocol = "tuic" }) end if singbox_tags:find("with_quic") then - o = s:option(Value, _n("hysteria2_hop"), translate("Port hopping range")) + o = s:option(Value, "hysteria2_hop", translate("Port hopping range")) o.description = translate("Format as 1000:2000 or 1000-2000 Multiple groups are separated by commas (,).") - o:depends({ [_n("protocol")] = "hysteria2", [_n("hysteria2_realms")] = false }) + o:depends({ protocol = "hysteria2", hysteria2_realms = false }) - o = s:option(Value, _n("hysteria2_hop_interval"), translate("Hop Interval(second)"), translate("Supports a fixed value or a random range (e.g., 30, 5-30), minimum 5.")) + o = s:option(Value, "hysteria2_hop_interval", translate("Hop Interval(second)"), translate("Supports a fixed value or a random range (e.g., 30, 5-30), minimum 5.")) o.datatype = "or(uinteger,portrange)" o.placeholder = "30" o.default = "30" - o:depends({ [_n("protocol")] = "hysteria2", [_n("hysteria2_realms")] = false }) + o:depends({ protocol = "hysteria2", hysteria2_realms = false }) - o = s:option(Flag, _n("hysteria2_realms"), translate("Realms")) + o = s:option(Flag, "hysteria2_realms", translate("Realms")) o.default = "0" if api.compare_versions(singbox_version, ">=", "1.14.0") then - o:depends({ [_n("protocol")] = "hysteria2"}) + o:depends({ protocol = "hysteria2"}) else - o:depends({ [_n("protocol")] = "__hide"}) + o:depends({ protocol = "__hide"}) end - o = s:option(Value, _n("hysteria2_realm_url"), translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name") - o:depends({ [_n("hysteria2_realms")] = "1" }) + o = s:option(Value, "hysteria2_realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name") + o:depends({ hysteria2_realms = "1" }) o.validate = function(self, value) value = api.trim(value) local realm = api.parse_realm_uri(value) @@ -421,109 +415,109 @@ if singbox_tags:find("with_quic") then return nil, translate("Invalid Realm URL.") end - o = s:option(DynamicList, _n("hysteria2_realm_stun"), translate("Realm STUN")) + o = s:option(DynamicList, "hysteria2_realm_stun", translate("Realm STUN")) o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" } - o:depends({ [_n("hysteria2_realms")] = "1" }) + o:depends({ hysteria2_realms = "1" }) - o = s:option(Value, _n("hysteria2_auth_password"), translate("Auth Password")) + o = s:option(Value, "hysteria2_auth_password", translate("Auth Password")) o.password = true - o:depends({ [_n("protocol")] = "hysteria2"}) + o:depends({ protocol = "hysteria2"}) - o = s:option(ListValue, _n("hysteria2_obfs_type"), translate("Obfs Type")) + o = s:option(ListValue, "hysteria2_obfs_type", translate("Obfs Type")) o:value("", translate("Disable")) o:value("salamander") o:value("gecko") - o:depends({ [_n("protocol")] = "hysteria2" }) + o:depends({ protocol = "hysteria2" }) - o = s:option(Value, _n("hysteria2_obfs_password"), translate("Obfs Password")) - o:depends({ [_n("hysteria2_obfs_type")] = "salamander" }) - o:depends({ [_n("hysteria2_obfs_type")] = "gecko" }) + o = s:option(Value, "hysteria2_obfs_password", translate("Obfs Password")) + o:depends({ hysteria2_obfs_type = "salamander" }) + o:depends({ hysteria2_obfs_type = "gecko" }) - o = s:option(Value, _n("hysteria2_obfs_MinPacketSize"), translate("Gecko Packet Size (min)")) + o = s:option(Value, "hysteria2_obfs_MinPacketSize", translate("Gecko Packet Size (min)")) o.datatype = "uinteger" o.placeholder = "512" o.default = "512" - o:depends({ [_n("hysteria2_obfs_type")] = "gecko" }) + o:depends({ hysteria2_obfs_type = "gecko" }) - o = s:option(Value, _n("hysteria2_obfs_MaxPacketSize"), translate("Gecko Packet Size (max)")) + o = s:option(Value, "hysteria2_obfs_MaxPacketSize", translate("Gecko Packet Size (max)")) o.datatype = "uinteger" o.placeholder = "1200" o.default = "1200" - o:depends({ [_n("hysteria2_obfs_type")] = "gecko" }) + o:depends({ hysteria2_obfs_type = "gecko" }) - o = s:option(Value, _n("hysteria2_up_mbps"), translate("Max upload Mbps")) - o:depends({ [_n("protocol")] = "hysteria2" }) + o = s:option(Value, "hysteria2_up_mbps", translate("Max upload Mbps")) + o:depends({ protocol = "hysteria2" }) - o = s:option(Value, _n("hysteria2_down_mbps"), translate("Max download Mbps")) - o:depends({ [_n("protocol")] = "hysteria2" }) + o = s:option(Value, "hysteria2_down_mbps", translate("Max download Mbps")) + o:depends({ protocol = "hysteria2" }) - o = s:option(Value, _n("hysteria2_idle_timeout"), translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)") + o = s:option(Value, "hysteria2_idle_timeout", translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)") o.datatype = "range(4,120)" - o:depends({ [_n("protocol")] = "hysteria2"}) + o:depends({ protocol = "hysteria2"}) - o = s:option(Value, _n("hysteria2_keep_alive_period"), translate("QUIC KeepAlive interval"), translate("Units:seconds") .. " (2~60)") + o = s:option(Value, "hysteria2_keep_alive_period", translate("QUIC KeepAlive interval"), translate("Units:seconds") .. " (2~60)") o.datatype = "range(2,60)" - o:depends({ [_n("protocol")] = "hysteria2"}) + o:depends({ protocol = "hysteria2"}) - o = s:option(Flag, _n("hysteria2_disable_mtu_discovery"), translate("Disable MTU detection")) + o = s:option(Flag, "hysteria2_disable_mtu_discovery", translate("Disable MTU detection")) o.default = "0" - o:depends({ [_n("protocol")] = "hysteria2"}) + o:depends({ protocol = "hysteria2"}) end -- [[ SSH config start ]] -- -o = s:option(TextValue, _n("ssh_priv_key"), translate("Private Key")) +o = s:option(TextValue, "ssh_priv_key", translate("Private Key")) o.rows = 5 o.wrap = "off" -o:depends({ [_n("protocol")] = "ssh" }) +o:depends({ protocol = "ssh" }) o.validate = function(self, value) value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n") return value end -o = s:option(Value, _n("ssh_priv_key_pp"), translate("Private Key Passphrase")) +o = s:option(Value, "ssh_priv_key_pp", translate("Private Key Passphrase")) o.password = true -o:depends({ [_n("protocol")] = "ssh" }) +o:depends({ protocol = "ssh" }) -o = s:option(DynamicList, _n("ssh_host_key"), translate("Host Key"), translate("Accept any if empty.")) -o:depends({ [_n("protocol")] = "ssh" }) +o = s:option(DynamicList, "ssh_host_key", translate("Host Key"), translate("Accept any if empty.")) +o:depends({ protocol = "ssh" }) -o = s:option(DynamicList, _n("ssh_host_key_algo"), translate("Host Key Algorithms")) -o:depends({ [_n("protocol")] = "ssh" }) +o = s:option(DynamicList, "ssh_host_key_algo", translate("Host Key Algorithms")) +o:depends({ protocol = "ssh" }) -o = s:option(Value, _n("ssh_client_version"), translate("Client Version"), translate("Random version will be used if empty.")) -o:depends({ [_n("protocol")] = "ssh" }) +o = s:option(Value, "ssh_client_version", translate("Client Version"), translate("Random version will be used if empty.")) +o:depends({ protocol = "ssh" }) -- [[ SSH config end ]] -- -- [[ naive start ]] -- -o = s:option(Value, _n("naive_insecure_concurrency"), translate("Concurrent Tunnels")) +o = s:option(Value, "naive_insecure_concurrency", translate("Concurrent Tunnels")) o.datatype = "uinteger" o.placeholder = "0" o.default = "0" -o:depends({ [_n("protocol")] = "naive" }) +o:depends({ protocol = "naive" }) -o = s:option(Flag, _n("naive_quic"), translate("QUIC")) +o = s:option(Flag, "naive_quic", translate("QUIC")) o.default = 0 -o:depends({ [_n("protocol")] = "naive" }) +o:depends({ protocol = "naive" }) -o = s:option(ListValue, _n("naive_congestion_control"), translate("Congestion control algorithm")) +o = s:option(ListValue, "naive_congestion_control", translate("Congestion control algorithm")) o.default = "bbr" o:value("bbr", translate("BBR")) o:value("bbr2", translate("BBRv2")) o:value("cubic", translate("CUBIC")) o:value("reno", translate("New Reno")) -o:depends({ [_n("naive_quic")] = "1" }) +o:depends({ naive_quic = "1" }) -- [[ naive end ]] -- -o = s:option(Flag, _n("tls"), translate("TLS")) +o = s:option(Flag, "tls", translate("TLS")) o.default = 0 -o:depends({ [_n("protocol")] = "vmess" }) -o:depends({ [_n("protocol")] = "vless" }) -o:depends({ [_n("protocol")] = "http" }) -o:depends({ [_n("protocol")] = "trojan" }) -o:depends({ [_n("protocol")] = "shadowsocks" }) -o:depends({ [_n("protocol")] = "anytls" }) +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) +o:depends({ protocol = "http" }) +o:depends({ protocol = "trojan" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "anytls" }) -o = s:option(ListValue, _n("alpn"), translate("ALPN")) +o = s:option(ListValue, "alpn", translate("ALPN")) o.default = "default" o:value("default", translate("Default")) o:value("h3") @@ -532,49 +526,49 @@ o:value("h3,h2") o:value("http/1.1") o:value("h2,http/1.1") o:value("h3,h2,http/1.1") -o:depends({ [_n("tls")] = true }) -o:depends({ [_n("protocol")] = "hysteria" }) +o:depends({ tls = true }) +o:depends({ protocol = "hysteria" }) -o = s:option(Flag, _n("tls_disable_sni"), translate("Disable SNI"), translate("Do not send server name in ClientHello.")) +o = s:option(Flag, "tls_disable_sni", translate("Disable SNI"), translate("Do not send server name in ClientHello.")) o.default = "0" -o:depends({ [_n("tls")] = true }) -o:depends({ [_n("protocol")] = "hysteria"}) -o:depends({ [_n("protocol")] = "tuic" }) -o:depends({ [_n("protocol")] = "hysteria2" }) +o:depends({ tls = true }) +o:depends({ protocol = "hysteria"}) +o:depends({ protocol = "tuic" }) +o:depends({ protocol = "hysteria2" }) -o = s:option(Value, _n("tls_serverName"), "SNI " .. translate("Domain")) -o:depends({ [_n("tls")] = true }) -o:depends({ [_n("protocol")] = "hysteria"}) -o:depends({ [_n("protocol")] = "tuic" }) -o:depends({ [_n("protocol")] = "hysteria2" }) -o:depends({ [_n("protocol")] = "naive" }) +o = s:option(Value, "tls_serverName", "SNI " .. translate("Domain")) +o:depends({ tls = true }) +o:depends({ protocol = "hysteria"}) +o:depends({ protocol = "tuic" }) +o:depends({ protocol = "hysteria2" }) +o:depends({ protocol = "naive" }) -o = s:option(Flag, _n("tls_allowInsecure"), translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped.")) +o = s:option(Flag, "tls_allowInsecure", translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped.")) o.default = "0" -o:depends({ [_n("tls")] = true }) -o:depends({ [_n("protocol")] = "hysteria"}) -o:depends({ [_n("protocol")] = "tuic" }) -o:depends({ [_n("protocol")] = "hysteria2" }) +o:depends({ tls = true }) +o:depends({ protocol = "hysteria"}) +o:depends({ protocol = "tuic" }) +o:depends({ protocol = "hysteria2" }) -o = s:option(Flag, _n("tls_certificate"), translate("TLS Certificate (PEM)")) +o = s:option(Flag, "tls_certificate", translate("TLS Certificate (PEM)")) o.default = "0" -o:depends({ [_n("tls")] = true, [_n("reality")] = false }) -o:depends({ [_n("protocol")] = "hysteria"}) -o:depends({ [_n("protocol")] = "tuic" }) -o:depends({ [_n("protocol")] = "hysteria2" }) -o:depends({ [_n("protocol")] = "naive" }) +o:depends({ tls = true, reality = false }) +o:depends({ protocol = "hysteria"}) +o:depends({ protocol = "tuic" }) +o:depends({ protocol = "hysteria2" }) +o:depends({ protocol = "naive" }) -o = s:option(TextValue, _n("tls_certificate_pem"), " ", translate("Full certificate (chain), PEM format.")) +o = s:option(TextValue, "tls_certificate_pem", " ", translate("Full certificate (chain), PEM format.")) o.default = "" o.rows = 5 o.wrap = "off" -o:depends({ [_n("tls_certificate")] = true }) +o:depends({ tls_certificate = true }) o.validate = function(self, value) value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n") return value end -o = s:option(DynamicList, _n("cipherSuites"), translate("Cipher Suites"), '***' .. " " .. translate("Configures the list of supported cipher suites.")) +o = s:option(DynamicList, "cipherSuites", translate("Cipher Suites"), '***' .. " " .. translate("Configures the list of supported cipher suites.")) o:value("TLS_AES_128_GCM_SHA256") o:value("TLS_AES_256_GCM_SHA384") o:value("TLS_CHACHA20_POLY1305_SHA256") @@ -588,35 +582,35 @@ o:value("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256") o:value("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384") o:value("TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256") o:value("TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256") -o:depends({ [_n("tls")] = true }) +o:depends({ tls = true }) -o = s:option(Flag, _n("ech"), translate("ECH")) +o = s:option(Flag, "ech", translate("ECH")) o.default = "0" -o:depends({ [_n("tls")] = true, [_n("flow")] = "", [_n("reality")] = false }) -o:depends({ [_n("protocol")] = "tuic" }) -o:depends({ [_n("protocol")] = "hysteria" }) -o:depends({ [_n("protocol")] = "hysteria2", [_n("hysteria2_realms")] = false }) -o:depends({ [_n("protocol")] = "naive" }) +o:depends({ tls = true, flow = "", reality = false }) +o:depends({ protocol = "tuic" }) +o:depends({ protocol = "hysteria" }) +o:depends({ protocol = "hysteria2", hysteria2_realms = false }) +o:depends({ protocol = "naive" }) -o = s:option(TextValue, _n("ech_config"), translate("ECH Config")) +o = s:option(TextValue, "ech_config", translate("ECH Config")) o.default = "" o.rows = 5 o.wrap = "off" -o:depends({ [_n("ech")] = true }) +o:depends({ ech = true }) o.validate = function(self, value) value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n") return value end -o = s:option(Value, _n("ech_query_server_name"), translate("ECH Query Domain"), translate("Overrides the domain name used for ECH HTTPS record queries.")) -o:depends({ [_n("ech")] = true }) +o = s:option(Value, "ech_query_server_name", translate("ECH Query Domain"), translate("Overrides the domain name used for ECH HTTPS record queries.")) +o:depends({ ech = true }) if singbox_tags:find("with_utls") then - o = s:option(Flag, _n("utls"), translate("uTLS")) + o = s:option(Flag, "utls", translate("uTLS")) o.default = "0" - o:depends({ [_n("tls")] = true }) + o:depends({ tls = true }) - o = s:option(ListValue, _n("fingerprint"), translate("Finger Print")) + o = s:option(ListValue, "fingerprint", translate("Finger Print")) o:value("chrome") o:value("firefox") o:value("edge") @@ -628,30 +622,30 @@ if singbox_tags:find("with_utls") then o:value("random") o:value("randomized") o.default = "chrome" - o:depends({ [_n("utls")] = true }) + o:depends({ utls = true }) -- [[ REALITY ]] -- - o = s:option(Flag, _n("reality"), translate("REALITY")) + o = s:option(Flag, "reality", translate("REALITY")) o.default = 0 - o:depends({ [_n("protocol")] = "vless", [_n("tls")] = true }) - o:depends({ [_n("protocol")] = "vmess", [_n("tls")] = true }) - o:depends({ [_n("protocol")] = "shadowsocks", [_n("tls")] = true }) - o:depends({ [_n("protocol")] = "socks", [_n("tls")] = true }) - o:depends({ [_n("protocol")] = "trojan", [_n("tls")] = true }) - o:depends({ [_n("protocol")] = "anytls", [_n("tls")] = true }) + o:depends({ protocol = "vless", tls = true }) + o:depends({ protocol = "vmess", tls = true }) + o:depends({ protocol = "shadowsocks", tls = true }) + o:depends({ protocol = "socks", tls = true }) + o:depends({ protocol = "trojan", tls = true }) + o:depends({ protocol = "anytls", tls = true }) - o = s:option(Value, _n("reality_publicKey"), translate("Public Key")) - o:depends({ [_n("reality")] = true }) + o = s:option(Value, "reality_publicKey", translate("Public Key")) + o:depends({ reality = true }) - o = s:option(Value, _n("reality_shortId"), translate("Short Id")) - o:depends({ [_n("reality")] = true }) + o = s:option(Value, "reality_shortId", translate("Short Id")) + o:depends({ reality = true }) end -o = s:option(Flag, _n("anytls_disable_reuse"), translate("Disable TLS Reuse")) +o = s:option(Flag, "anytls_disable_reuse", translate("Disable TLS Reuse")) o.default = 0 -o:depends({ [_n("protocol")] = "anytls" }) +o:depends({ protocol = "anytls" }) -o = s:option(ListValue, _n("transport"), translate("Transport")) +o = s:option(ListValue, "transport", translate("Transport")) o:value("tcp", "TCP") o:value("http", "HTTP") o:value("ws", "WebSocket") @@ -663,119 +657,119 @@ if singbox_tags:find("with_grpc") then o:value("grpc", "gRPC") else o:value("grpc", "gRPC-lite") end -o:depends({ [_n("protocol")] = "vmess" }) -o:depends({ [_n("protocol")] = "vless" }) -o:depends({ [_n("protocol")] = "socks" }) -o:depends({ [_n("protocol")] = "shadowsocks" }) -o:depends({ [_n("protocol")] = "trojan" }) +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) +o:depends({ protocol = "socks" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "trojan" }) if singbox_tags:find("with_wireguard") then - o = s:option(Value, _n("wireguard_public_key"), translate("Public Key")) - o:depends({ [_n("protocol")] = "wireguard" }) + o = s:option(Value, "wireguard_public_key", translate("Public Key")) + o:depends({ protocol = "wireguard" }) - o = s:option(Value, _n("wireguard_secret_key"), translate("Private Key")) - o:depends({ [_n("protocol")] = "wireguard" }) + o = s:option(Value, "wireguard_secret_key", translate("Private Key")) + o:depends({ protocol = "wireguard" }) - o = s:option(Value, _n("wireguard_preSharedKey"), translate("Pre shared key")) - o:depends({ [_n("protocol")] = "wireguard" }) + o = s:option(Value, "wireguard_preSharedKey", translate("Pre shared key")) + o:depends({ protocol = "wireguard" }) - o = s:option(DynamicList, _n("wireguard_local_address"), translate("Local Address")) - o:depends({ [_n("protocol")] = "wireguard" }) + o = s:option(DynamicList, "wireguard_local_address", translate("Local Address")) + o:depends({ protocol = "wireguard" }) - o = s:option(Value, _n("wireguard_mtu"), translate("MTU")) + o = s:option(Value, "wireguard_mtu", translate("MTU")) o.default = "1420" - o:depends({ [_n("protocol")] = "wireguard" }) + o:depends({ protocol = "wireguard" }) - o = s:option(Flag, _n("wireguard_system_interface"), translate("System interface")) + o = s:option(Flag, "wireguard_system_interface", translate("System interface")) o.default = 0 - o:depends({ [_n("protocol")] = "wireguard" }) + o:depends({ protocol = "wireguard" }) - o = s:option(Value, _n("wireguard_interface_name"), translate("System interface name")) - o:depends({ [_n("protocol")] = "wireguard" }) + o = s:option(Value, "wireguard_interface_name", translate("System interface name")) + o:depends({ protocol = "wireguard" }) - o = s:option(Value, _n("wireguard_reserved"), translate("Reserved"), translate("Decimal numbers separated by \",\" or Base64-encoded strings.")) - o:depends({ [_n("protocol")] = "wireguard" }) + o = s:option(Value, "wireguard_reserved", translate("Reserved"), translate("Decimal numbers separated by \",\" or Base64-encoded strings.")) + o:depends({ protocol = "wireguard" }) end -- [[ TCP ]]-- -o = s:option(ListValue, _n("tcp_guise"), translate("Camouflage Type")) +o = s:option(ListValue, "tcp_guise", translate("Camouflage Type")) o:value("none", "none") o:value("http", "http") -o:depends({ [_n("transport")] = "tcp" }) +o:depends({ transport = "tcp" }) -o = s:option(DynamicList, _n("tcp_guise_http_host"), translate("HTTP Host")) -o:depends({ [_n("tcp_guise")] = "http" }) +o = s:option(DynamicList, "tcp_guise_http_host", translate("HTTP Host")) +o:depends({ tcp_guise = "http" }) -o = s:option(DynamicList, _n("tcp_guise_http_path"), translate("HTTP Path")) +o = s:option(DynamicList, "tcp_guise_http_path", translate("HTTP Path")) o.placeholder = "/" -o:depends({ [_n("tcp_guise")] = "http" }) +o:depends({ tcp_guise = "http" }) -- [[ HTTP ]]-- -o = s:option(DynamicList, _n("http_host"), translate("HTTP Host")) -o:depends({ [_n("transport")] = "http" }) +o = s:option(DynamicList, "http_host", translate("HTTP Host")) +o:depends({ transport = "http" }) -o = s:option(Value, _n("http_path"), translate("HTTP Path")) +o = s:option(Value, "http_path", translate("HTTP Path")) o.placeholder = "/" -o:depends({ [_n("transport")] = "http" }) +o:depends({ transport = "http" }) -o = s:option(Flag, _n("http_h2_health_check"), translate("Health check")) -o:depends({ [_n("tls")] = true, [_n("transport")] = "http" }) +o = s:option(Flag, "http_h2_health_check", translate("Health check")) +o:depends({ tls = true, transport = "http" }) -o = s:option(Value, _n("http_h2_read_idle_timeout"), translate("Idle timeout")) +o = s:option(Value, "http_h2_read_idle_timeout", translate("Idle timeout")) o.default = "15" -o:depends({ [_n("http_h2_health_check")] = true }) +o:depends({ http_h2_health_check = true }) -o = s:option(Value, _n("http_h2_health_check_timeout"), translate("Health check timeout")) +o = s:option(Value, "http_h2_health_check_timeout", translate("Health check timeout")) o.default = "15" -o:depends({ [_n("http_h2_health_check")] = true }) +o:depends({ http_h2_health_check = true }) -- [[ WebSocket ]]-- -o = s:option(Value, _n("ws_host"), translate("WebSocket Host")) -o:depends({ [_n("transport")] = "ws" }) +o = s:option(Value, "ws_host", translate("WebSocket Host")) +o:depends({ transport = "ws" }) -o = s:option(Value, _n("ws_path"), translate("WebSocket Path")) +o = s:option(Value, "ws_path", translate("WebSocket Path")) o.placeholder = "/" -o:depends({ [_n("transport")] = "ws" }) +o:depends({ transport = "ws" }) -o = s:option(Flag, _n("ws_enableEarlyData"), translate("Enable early data")) -o:depends({ [_n("transport")] = "ws" }) +o = s:option(Flag, "ws_enableEarlyData", translate("Enable early data")) +o:depends({ transport = "ws" }) -o = s:option(Value, _n("ws_maxEarlyData"), translate("Early data length")) +o = s:option(Value, "ws_maxEarlyData", translate("Early data length")) o.default = "1024" -o:depends({ [_n("ws_enableEarlyData")] = true }) +o:depends({ ws_enableEarlyData = true }) -o = s:option(Value, _n("ws_earlyDataHeaderName"), translate("Early data header name"), translate("Recommended value: Sec-WebSocket-Protocol")) -o:depends({ [_n("ws_enableEarlyData")] = true }) +o = s:option(Value, "ws_earlyDataHeaderName", translate("Early data header name"), translate("Recommended value: Sec-WebSocket-Protocol")) +o:depends({ ws_enableEarlyData = true }) -- [[ HTTPUpgrade ]]-- -o = s:option(Value, _n("httpupgrade_host"), translate("HTTPUpgrade Host")) -o:depends({ [_n("transport")] = "httpupgrade" }) +o = s:option(Value, "httpupgrade_host", translate("HTTPUpgrade Host")) +o:depends({ transport = "httpupgrade" }) -o = s:option(Value, _n("httpupgrade_path"), translate("HTTPUpgrade Path")) +o = s:option(Value, "httpupgrade_path", translate("HTTPUpgrade Path")) o.placeholder = "/" -o:depends({ [_n("transport")] = "httpupgrade" }) +o:depends({ transport = "httpupgrade" }) -- [[ gRPC ]]-- -o = s:option(Value, _n("grpc_serviceName"), "ServiceName") -o:depends({ [_n("transport")] = "grpc" }) +o = s:option(Value, "grpc_serviceName", "ServiceName") +o:depends({ transport = "grpc" }) -o = s:option(Flag, _n("grpc_health_check"), translate("Health check")) -o:depends({ [_n("transport")] = "grpc" }) +o = s:option(Flag, "grpc_health_check", translate("Health check")) +o:depends({ transport = "grpc" }) -o = s:option(Value, _n("grpc_idle_timeout"), translate("Idle timeout")) +o = s:option(Value, "grpc_idle_timeout", translate("Idle timeout")) o.default = "15" -o:depends({ [_n("grpc_health_check")] = true }) +o:depends({ grpc_health_check = true }) -o = s:option(Value, _n("grpc_health_check_timeout"), translate("Health check timeout")) +o = s:option(Value, "grpc_health_check_timeout", translate("Health check timeout")) o.default = "15" -o:depends({ [_n("grpc_health_check")] = true }) +o:depends({ grpc_health_check = true }) -o = s:option(Flag, _n("grpc_permit_without_stream"), translate("Permit without stream")) +o = s:option(Flag, "grpc_permit_without_stream", translate("Permit without stream")) o.default = "0" -o:depends({ [_n("grpc_health_check")] = true }) +o:depends({ grpc_health_check = true }) -- [[ User-Agent ]]-- -o = s:option(Value, _n("user_agent"), translate("User-Agent")) +o = s:option(Value, "user_agent", translate("User-Agent")) o.default = "" o:value("", translate("default")) o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36", "chrome") @@ -784,73 +778,73 @@ o:value("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (K o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.70", "edge") o:value("Go-http-client/1.1", "golang") o:value("curl/7.68.0", "curl") -o:depends({ [_n("tcp_guise")] = "http" }) -o:depends({ [_n("transport")] = "http" }) -o:depends({ [_n("transport")] = "ws" }) -o:depends({ [_n("transport")] = "httpupgrade" }) -o:depends({ [_n("protocol")] = "naive" }) +o:depends({ tcp_guise = "http" }) +o:depends({ transport = "http" }) +o:depends({ transport = "ws" }) +o:depends({ transport = "httpupgrade" }) +o:depends({ protocol = "naive" }) -- [[ Mux ]]-- -o = s:option(Flag, _n("mux"), translate("Mux")) +o = s:option(Flag, "mux", translate("Mux")) o.rmempty = false -o:depends({ [_n("protocol")] = "vmess" }) -o:depends({ [_n("protocol")] = "vless", [_n("flow")] = "" }) -o:depends({ [_n("protocol")] = "shadowsocks", [_n("uot")] = "" }) -o:depends({ [_n("protocol")] = "trojan" }) +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless", flow = "" }) +o:depends({ protocol = "shadowsocks", uot = "" }) +o:depends({ protocol = "trojan" }) -o = s:option(ListValue, _n("mux_type"), translate("Mux")) +o = s:option(ListValue, "mux_type", translate("Mux")) o:value("smux") o:value("yamux") o:value("h2mux") -o:depends({ [_n("mux")] = true }) +o:depends({ mux = true }) -o = s:option(Value, _n("mux_concurrency"), translate("Mux concurrency")) +o = s:option(Value, "mux_concurrency", translate("Mux concurrency")) o.default = 4 -o:depends({ [_n("mux")] = true, [_n("tcpbrutal")] = false }) +o:depends({ mux = true, tcpbrutal = false }) -o = s:option(Flag, _n("mux_padding"), translate("Padding")) +o = s:option(Flag, "mux_padding", translate("Padding")) o.default = 0 -o:depends({ [_n("mux")] = true }) +o:depends({ mux = true }) -- [[ TCP Brutal ]]-- -o = s:option(Flag, _n("tcpbrutal"), translate("TCP Brutal")) +o = s:option(Flag, "tcpbrutal", translate("TCP Brutal")) o.default = 0 -o:depends({ [_n("mux")] = true }) +o:depends({ mux = true }) -o = s:option(Value, _n("tcpbrutal_up_mbps"), translate("Max upload Mbps")) +o = s:option(Value, "tcpbrutal_up_mbps", translate("Max upload Mbps")) o.default = "10" -o:depends({ [_n("tcpbrutal")] = true }) +o:depends({ tcpbrutal = true }) -o = s:option(Value, _n("tcpbrutal_down_mbps"), translate("Max download Mbps")) +o = s:option(Value, "tcpbrutal_down_mbps", translate("Max download Mbps")) o.default = "50" -o:depends({ [_n("tcpbrutal")] = true }) +o:depends({ tcpbrutal = true }) -o = s:option(Flag, _n("shadowtls"), "ShadowTLS") +o = s:option(Flag, "shadowtls", "ShadowTLS") o.default = 0 -o:depends({ [_n("protocol")] = "vmess", [_n("tls")] = false }) -o:depends({ [_n("protocol")] = "shadowsocks", [_n("tls")] = false }) +o:depends({ protocol = "vmess", tls = false }) +o:depends({ protocol = "shadowsocks", tls = false }) -o = s:option(ListValue, _n("shadowtls_version"), "ShadowTLS " .. translate("Version")) +o = s:option(ListValue, "shadowtls_version", "ShadowTLS " .. translate("Version")) o.default = "1" o:value("1", "ShadowTLS v1") o:value("2", "ShadowTLS v2") o:value("3", "ShadowTLS v3") -o:depends({ [_n("shadowtls")] = true }) +o:depends({ shadowtls = true }) -o = s:option(Value, _n("shadowtls_password"), "ShadowTLS " .. translate("Password")) +o = s:option(Value, "shadowtls_password", "ShadowTLS " .. translate("Password")) o.password = true -o:depends({ [_n("shadowtls")] = true, [_n("shadowtls_version")] = "2" }) -o:depends({ [_n("shadowtls")] = true, [_n("shadowtls_version")] = "3" }) +o:depends({ shadowtls = true, shadowtls_version = "2" }) +o:depends({ shadowtls = true, shadowtls_version = "3" }) -o = s:option(Value, _n("shadowtls_serverName"), "ShadowTLS " .. translate("Domain")) -o:depends({ [_n("shadowtls")] = true }) +o = s:option(Value, "shadowtls_serverName", "ShadowTLS " .. translate("Domain")) +o:depends({ shadowtls = true }) if singbox_tags:find("with_utls") then - o = s:option(Flag, _n("shadowtls_utls"), "ShadowTLS " .. translate("uTLS")) + o = s:option(Flag, "shadowtls_utls", "ShadowTLS " .. translate("uTLS")) o.default = "0" - o:depends({ [_n("shadowtls")] = true }) + o:depends({ shadowtls = true }) - o = s:option(ListValue, _n("shadowtls_fingerprint"), "ShadowTLS " .. translate("Finger Print")) + o = s:option(ListValue, "shadowtls_fingerprint", "ShadowTLS " .. translate("Finger Print")) o:value("chrome") o:value("firefox") o:value("edge") @@ -862,44 +856,44 @@ if singbox_tags:find("with_utls") then o:value("random") -- o:value("randomized") o.default = "chrome" - o:depends({ [_n("shadowtls")] = true, [_n("shadowtls_utls")] = true }) + o:depends({ shadowtls = true, shadowtls_utls = true }) end -- [[ SIP003 plugin ]]-- -o = s:option(Flag, _n("plugin_enabled"), translate("plugin")) +o = s:option(Flag, "plugin_enabled", translate("plugin")) o.default = 0 -o:depends({ [_n("protocol")] = "shadowsocks" }) +o:depends({ protocol = "shadowsocks" }) -o = s:option(ListValue, _n("plugin"), "SIP003 " .. translate("plugin")) +o = s:option(ListValue, "plugin", "SIP003 " .. translate("plugin")) o.default = "obfs-local" -o:depends({ [_n("plugin_enabled")] = true }) +o:depends({ plugin_enabled = true }) o:value("obfs-local") o:value("v2ray-plugin") -o = s:option(Value, _n("plugin_opts"), translate("opts")) -o:depends({ [_n("plugin_enabled")] = true }) +o = s:option(Value, "plugin_opts", translate("opts")) +o:depends({ plugin_enabled = true }) -o = s:option(ListValue, _n("domain_resolver"), translate("Domain DNS Resolve"), translate("If the node address is a domain name, this DNS will be used for resolution.")) +o = s:option(ListValue, "domain_resolver", translate("Domain DNS Resolve"), translate("If the node address is a domain name, this DNS will be used for resolution.")) o:value("", translate("Auto")) o:value("tcp", "TCP") o:value("udp", "UDP") o:value("https", "HTTPS") -o = s:option(Value, _n("domain_resolver_dns"), "DNS") +o = s:option(Value, "domain_resolver_dns", "DNS") o.datatype = "or(ipaddr,ipaddrport)" o:value("114.114.114.114") o:value("223.5.5.5:53") o.default = o.keylist[1] -o:depends({ [_n("domain_resolver")] = "tcp" }) -o:depends({ [_n("domain_resolver")] = "udp" }) +o:depends({ domain_resolver = "tcp" }) +o:depends({ domain_resolver = "udp" }) -o = s:option(Value, _n("domain_resolver_dns_https"), "DNS") +o = s:option(Value, "domain_resolver_dns_https", "DNS") o:value("https://120.53.53.53/dns-query", "DNSPod") o:value("https://223.5.5.5/dns-query", "AliDNS") o.default = o.keylist[1] -o:depends({ [_n("domain_resolver")] = "https" }) +o:depends({ domain_resolver = "https" }) -o = s:option(ListValue, _n("domain_strategy"), translate("Domain Strategy"), translate("If is domain name, The requested domain name will be resolved to IP before connect.")) +o = s:option(ListValue, "domain_strategy", translate("Domain Strategy"), translate("If is domain name, The requested domain name will be resolved to IP before connect.")) o.default = "" o:value("", translate("Auto")) o:value("prefer_ipv4", translate("Prefer IPv4")) @@ -907,18 +901,18 @@ o:value("prefer_ipv6", translate("Prefer IPv6")) o:value("ipv4_only", translate("IPv4 Only")) o:value("ipv6_only", translate("IPv6 Only")) -local protocols = s.fields[_n("protocol")].keylist +local protocols = s.fields["protocol"].keylist if #protocols > 0 then for i, v in ipairs(protocols) do if not v:find("^_") then - local depends_condition = { [_n("protocol")] = v } + local depends_condition = { protocol = v } if v == "hysteria2" then - depends_condition[_n("hysteria2_realms")] = false + depends_condition["hysteria2_realms"] = false end - s.fields[_n("address")]:depends(depends_condition) - s.fields[_n("port")]:depends(depends_condition) - s.fields[_n("domain_resolver")]:depends(depends_condition) - s.fields[_n("domain_strategy")]:depends(depends_condition) + s.fields["address"]:depends(depends_condition) + s.fields["port"]:depends(depends_condition) + s.fields["domain_resolver"]:depends(depends_condition) + s.fields["domain_strategy"]:depends(depends_condition) end end end @@ -926,7 +920,7 @@ end -- [[ Normal single node End ]] if not load_shunt_options then - o = s:option(ListValue, _n("chain_proxy"), translate("Chain Proxy")) + o = s:option(ListValue, "chain_proxy", translate("Chain Proxy")) o:value("", translate("Close(Not use)")) if not (load_iface_options or load_urltest_options) then -- Special node cannot be use pre-proxy. @@ -935,20 +929,20 @@ if not load_shunt_options then end o:value("2", translate("Landing Node")) - o1 = s:option(ListValue, _n("preproxy_node"), translate("Preproxy Node"), translate("Only support a layer of proxy.")) - o1:depends({ [_n("chain_proxy")] = "1", [_n("hysteria2_realms")] = false }) + o1 = s:option(ListValue, "preproxy_node", translate("Preproxy Node"), translate("Only support a layer of proxy.")) + o1:depends({ chain_proxy = "1", hysteria2_realms = false }) o1.template = m:template_path("/cbi/nodes_listvalue") o1.group = {} - o3 = s:option(Value, _n("outbound_iface"), translate("Outbound Interface")) - o3:depends({ [_n("chain_proxy")] = "3" }) + o3 = s:option(Value, "outbound_iface", translate("Outbound Interface")) + o3:depends({ chain_proxy = "3" }) o3:value("", translate("All")) for _, d in ipairs(netdev_list) do o3:value(d.name, d.label) end - o2 = s:option(ListValue, _n("to_node"), translate("Landing Node"), translate("Only support a layer of proxy.")) - o2:depends({ [_n("chain_proxy")] = "2", [_n("hysteria2_realms")] = false }) + o2 = s:option(ListValue, "to_node", translate("Landing Node"), translate("Only support a layer of proxy.")) + o2:depends({ chain_proxy = "2", hysteria2_realms = false }) o2.template = m:template_path("/cbi/nodes_listvalue") o2.group = {} @@ -969,12 +963,12 @@ if not load_shunt_options then end end -api.luci_types(arg[1], m, s, type_name, option_prefix) +api.luci_types(s1, s) if load_shunt_options then local current_node = m:get(arg[1]) or {} local shunt_lua = loadfile("/usr/lib/lua/luci/model/cbi/passwall2/client/include/shunt_options.lua") - setfenv(shunt_lua, getfenv(1))(m, s, { + setfenv(shunt_lua, getfenv(1))(m, s1, { node_id = arg[1], node = current_node, node_list = node_list, diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ss-rust.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ss-rust.lua index ae18e369..e3bc6907 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ss-rust.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ss-rust.lua @@ -1,24 +1,20 @@ -local m, s = ... - if not api.is_finded("sslocal") then return end -type_name = "SS-Rust" - -- [[ Shadowsocks Rust ]] +local m, s1 = ... +local type_name = "SS-Rust" -s.fields["type"]:value(type_name, "Shadowsocks Rust") +s1.fields["type"]:value(type_name, "Shadowsocks Rust") -if s.val["type"] ~= type_name then +if s1.val["type"] ~= type_name then return end -local option_prefix = "ssrust_" - -local function _n(name) - return option_prefix .. name -end +local s = NamedSection(m, arg[1], "server") +s.type_name = type_name +s.option_prefix = "ssrust_" local ssrust_encrypt_method_list = { "none", "plain", @@ -26,35 +22,35 @@ local ssrust_encrypt_method_list = { "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha8-poly1305", "2022-blake3-chacha20-poly1305" } -o = s:option(Value, _n("address"), translate("Address (Support Domain Name)")) +o = s:option(Value, "address", translate("Address (Support Domain Name)")) -o = s:option(Value, _n("port"), translate("Port")) +o = s:option(Value, "port", translate("Port")) o.datatype = "port" -o = s:option(Value, _n("password"), translate("Password")) +o = s:option(Value, "password", translate("Password")) o.password = true -o = s:option(Value, _n("method"), translate("Encrypt Method")) +o = s:option(Value, "method", translate("Encrypt Method")) for a, t in ipairs(ssrust_encrypt_method_list) do o:value(t) end -o = s:option(Value, _n("timeout"), translate("Connection Timeout")) +o = s:option(Value, "timeout", translate("Connection Timeout")) o.datatype = "uinteger" o.default = 300 -o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required")) +o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open"), translate("Need node support required")) o.default = 0 -o = s:option(Flag, _n("plugin_enabled"), translate("plugin")) +o = s:option(Flag, "plugin_enabled", translate("plugin")) o.default = 0 -o = s:option(Value, _n("plugin"), "SIP003 " .. translate("plugin"), translate("Supports custom SIP003 plugins, Make sure the plugin is installed.")) +o = s:option(Value, "plugin", "SIP003 " .. translate("plugin"), translate("Supports custom SIP003 plugins, Make sure the plugin is installed.")) o.default = "none" o:value("none", translate("none")) if api.is_finded("xray-plugin") then o:value("xray-plugin") end if api.is_finded("v2ray-plugin") then o:value("v2ray-plugin") end if api.is_finded("obfs-local") then o:value("obfs-local") end if api.is_finded("shadow-tls") then o:value("shadow-tls") end -o:depends({ [_n("plugin_enabled")] = true }) +o:depends({ plugin_enabled = true }) o.validate = function(self, value, t) if value and value ~= "" and value ~= "none" then if not api.is_finded(value) then @@ -66,7 +62,7 @@ o.validate = function(self, value, t) return nil end -o = s:option(Value, _n("plugin_opts"), translate("opts")) -o:depends({ [_n("plugin_enabled")] = true }) +o = s:option(Value, "plugin_opts", translate("opts")) +o:depends({ plugin_enabled = true }) -api.luci_types(arg[1], m, s, type_name, option_prefix) +api.luci_types(s1, s) \ No newline at end of file diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ssr.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ssr.lua index f23e8efa..4d18c36e 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ssr.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ssr.lua @@ -1,24 +1,20 @@ -local m, s = ... - if not api.is_finded("ssr-local") then return end -type_name = "SSR" - -- [[ ShadowsocksR Libev ]] +local m, s1 = ... +local type_name = "SSR" -s.fields["type"]:value(type_name, "ShadowsocksR Libev") +s1.fields["type"]:value(type_name, "ShadowsocksR Libev") -if s.val["type"] ~= type_name then +if s1.val["type"] ~= type_name then return end -local option_prefix = "ssr_" - -local function _n(name) - return option_prefix .. name -end +local s = NamedSection(m, arg[1], "server") +s.type_name = type_name +s.option_prefix = "ssr_" local ssr_encrypt_method_list = { "none", "table", "rc2-cfb", "rc4", "rc4-md5", "rc4-md5-6", "aes-128-cfb", @@ -39,32 +35,32 @@ local ssr_obfs_list = { "tls1.0_session_auth", "tls1.2_ticket_auth" } -o = s:option(Value, _n("address"), translate("Address (Support Domain Name)")) +o = s:option(Value, "address", translate("Address (Support Domain Name)")) -o = s:option(Value, _n("port"), translate("Port")) +o = s:option(Value, "port", translate("Port")) o.datatype = "port" -o = s:option(Value, _n("password"), translate("Password")) +o = s:option(Value, "password", translate("Password")) o.password = true -o = s:option(ListValue, _n("method"), translate("Encrypt Method")) +o = s:option(ListValue, "method", translate("Encrypt Method")) for a, t in ipairs(ssr_encrypt_method_list) do o:value(t) end -o = s:option(ListValue, _n("protocol"), translate("Protocol")) +o = s:option(ListValue, "protocol", translate("Protocol")) for a, t in ipairs(ssr_protocol_list) do o:value(t) end -o = s:option(Value, _n("protocol_param"), translate("Protocol_param")) +o = s:option(Value, "protocol_param", translate("Protocol_param")) -o = s:option(ListValue, _n("obfs"), translate("Obfs")) +o = s:option(ListValue, "obfs", translate("Obfs")) for a, t in ipairs(ssr_obfs_list) do o:value(t) end -o = s:option(Value, _n("obfs_param"), translate("Obfs_param")) +o = s:option(Value, "obfs_param", translate("Obfs_param")) -o = s:option(Value, _n("timeout"), translate("Connection Timeout")) +o = s:option(Value, "timeout", translate("Connection Timeout")) o.datatype = "uinteger" o.default = 300 -o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required")) +o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open"), translate("Need node support required")) o.default = 0 -api.luci_types(arg[1], m, s, type_name, option_prefix) +api.luci_types(s1, s) \ No newline at end of file diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/server_config.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/server_config.lua index 79f6b54f..816d911d 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/server_config.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/server_config.lua @@ -17,7 +17,7 @@ m:foreach("user", function(s) user_list[#user_list + 1] = s end) -s = m:section(NamedSection, arg[1], "server", translate("Server Config")) +local s = m:section(NamedSection, arg[1], "server", translate("Server Config")) s.addremove = false s.dynamic = false diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/hysteria2.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/hysteria2.lua index 998682af..8c6114e8 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/hysteria2.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/hysteria2.lua @@ -1,135 +1,27 @@ -local m, s = ... - if not api.finded_com("hysteria") then return end +-- [[ Hysteria2 ]] +local m, s1 = ... local type_name = "Hysteria2" --- [[ Hysteria2 ]] +s1.fields["type"]:value(type_name, "Hysteria2") -s.fields["type"]:value(type_name, "Hysteria2") - -if s.val["type"] and s.val["type"] ~= type_name then +if s1.val["type"] and s1.val["type"] ~= type_name then return end -local option_prefix = "hysteria2_" +local s = NamedSection(m, arg[1], "server") +s.type_name = type_name +s.option_prefix = "hysteria2_" -local function _n(name) - return option_prefix .. name -end +o = s:option(Flag, "custom", translate("Use Custom Config")) -o = s:option(Flag, _n("custom"), translate("Use Custom Config")) - -o = s:option(Value, _n("port"), translate("Listen Port")) -o.datatype = "port" -o:depends({ [_n("custom")] = false }) - -o = s:option(DynamicList, _n("users"), translate("User")) -for i, v in ipairs(user_list) do - o:value(v[".name"], v.username) -end -o:depends({ [_n("custom")] = false }) - -o = s:option(Flag, _n("realms"), translate("Realms")) -o.default = "0" -o.rewrite_option = o.option -o:depends({ [_n("custom")] = false }) - -o = s:option(Value, _n("realm_url"), translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name") -o.rewrite_option = o.option -o:depends({ [_n("realms")] = "1" }) -o.validate = function(self, value) - value = api.trim(value) - local realm = api.parse_realm_uri(value) - if realm then return value end - return nil, translate("Invalid Realm URL.") -end - -o = s:option(DynamicList, _n("realm_stun"), translate("Realm STUN")) -o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" } -o.rewrite_option = o.option -o:depends({ [_n("realms")] = "1" }) - -o = s:option(ListValue, _n("obfs_type"), translate("Obfs Type")) -o:value("", translate("Disable")) -o:value("salamander") -o:value("gecko") -o.rewrite_option = o.option -o:depends({ [_n("custom")] = false }) - -o = s:option(Value, _n("obfs_password"), translate("Obfs Password")) -o.rewrite_option = o.option -o:depends({ [_n("obfs_type")] = "salamander" }) -o:depends({ [_n("obfs_type")] = "gecko" }) - -o = s:option(Value, _n("obfs_MinPacketSize"), translate("Gecko Packet Size (min)")) -o.datatype = "uinteger" -o.placeholder = "512" -o.default = "512" -o:depends({ [_n("obfs_type")] = "gecko" }) -o.rewrite_option = o.option - -o = s:option(Value, _n("obfs_MaxPacketSize"), translate("Gecko Packet Size (max)")) -o.datatype = "uinteger" -o.placeholder = "1200" -o.default = "1200" -o:depends({ [_n("obfs_type")] = "gecko" }) -o.rewrite_option = o.option - -o = s:option(Flag, _n("udp"), translate("UDP")) -o.default = "1" -o.rewrite_option = o.option -o:depends({ [_n("custom")] = false }) - -o = s:option(Value, _n("up_mbps"), translate("Max upload Mbps")) -o.rewrite_option = o.option -o:depends({ [_n("custom")] = false }) - -o = s:option(Value, _n("down_mbps"), translate("Max download Mbps")) -o.rewrite_option = o.option -o:depends({ [_n("custom")] = false }) - -o = s:option(Flag, _n("ignoreClientBandwidth"), translate("ignoreClientBandwidth")) -o.default = "0" -o.rewrite_option = o.option -o:depends({ [_n("custom")] = false }) - -o = s:option(FileUpload, _n("tls_certificateFile"), translate("Public key absolute path"), translate("as:") .. "/etc/ssl/fullchain.pem") -o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. arg[1] .. ".pem" -if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end -o.validate = function(self, value, t) - if value and value ~= "" then - if not nixio.fs.access(value) then - return nil, translate("Can't find this file!") - else - return value - end - end - return nil -end -o:depends({ [_n("custom")] = false }) - -o = s:option(FileUpload, _n("tls_keyFile"), translate("Private key absolute path"), translate("as:") .. "/etc/ssl/private.key") -o.default = m:get(s.section, "tls_keyFile") or "/etc/config/ssl/" .. arg[1] .. ".key" -if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end -o.validate = function(self, value, t) - if value and value ~= "" then - if not nixio.fs.access(value) then - return nil, translate("Can't find this file!") - else - return value - end - end - return nil -end -o:depends({ [_n("custom")] = false }) - -o = s:option(TextValue, _n("custom_config"), translate("Custom Config")) +o = s:option(TextValue, "custom_config", translate("Custom Config")) o.rows = 10 o.wrap = "off" -o:depends({ [_n("custom")] = true }) +o:depends({ custom = true }) o.datatype = "json" local o_validate = o.validate o.validate = function(self, value) @@ -147,8 +39,101 @@ o.custom_write = function(self, section, value) m:set(section, "config_str", api.base64Encode(value) or "") end -o = s:option(Flag, _n("log"), translate("Log")) +o = s:option(Value, "port", translate("Listen Port")) +o.datatype = "port" +o:depends({ custom = false }) + +o = s:option(DynamicList, "users", translate("User")) +for i, v in ipairs(user_list) do + o:value(v[".name"], v.username) +end +o:depends({ custom = false }) + +o = s:option(Flag, "realms", translate("Realms")) +o.default = "0" +o:depends({ custom = false }) + +o = s:option(Value, "realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name") +o:depends({ realms = true }) +o.validate = function(self, value) + value = api.trim(value) + local realm = api.parse_realm_uri(value) + if realm then return value end + return nil, translate("Invalid Realm URL.") +end + +o = s:option(DynamicList, "realm_stun", translate("Realm STUN")) +o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" } +o:depends({ realms = true }) + +o = s:option(ListValue, "obfs_type", translate("Obfs Type")) +o:value("", translate("Disable")) +o:value("salamander") +o:value("gecko") +o:depends({ custom = false }) + +o = s:option(Value, "obfs_password", translate("Obfs Password")) +o:depends({ obfs_type = "salamander" }) +o:depends({ obfs_type = "gecko" }) + +o = s:option(Value, "obfs_MinPacketSize", translate("Gecko Packet Size (min)")) +o.datatype = "uinteger" +o.placeholder = "512" +o.default = "512" +o:depends({ obfs_type = "gecko" }) + +o = s:option(Value, "obfs_MaxPacketSize", translate("Gecko Packet Size (max)")) +o.datatype = "uinteger" +o.placeholder = "1200" +o.default = "1200" +o:depends({ obfs_type = "gecko" }) + +o = s:option(Flag, "udp", translate("UDP")) +o.default = "1" +o:depends({ custom = false }) + +o = s:option(Value, "up_mbps", translate("Max upload Mbps")) +o:depends({ custom = false }) + +o = s:option(Value, "down_mbps", translate("Max download Mbps")) +o:depends({ custom = false }) + +o = s:option(Flag, "ignoreClientBandwidth", translate("ignoreClientBandwidth")) +o.default = "0" +o:depends({ custom = false }) + +o = s:option(FileUpload, "tls_certificateFile", translate("Public key absolute path"), translate("as:") .. "/etc/ssl/fullchain.pem") +o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. arg[1] .. ".pem" +if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end +o.validate = function(self, value, t) + if value and value ~= "" then + if not api.fs.access(value) then + return nil, translate("Can't find this file!") + else + return value + end + end + return nil +end +o:depends({ custom = false }) + +o = s:option(FileUpload, "tls_keyFile", translate("Private key absolute path"), translate("as:") .. "/etc/ssl/private.key") +o.default = m:get(s.section, "tls_keyFile") or "/etc/config/ssl/" .. arg[1] .. ".key" +if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end +o.validate = function(self, value, t) + if value and value ~= "" then + if not api.fs.access(value) then + return nil, translate("Can't find this file!") + else + return value + end + end + return nil +end +o:depends({ custom = false }) + +o = s:option(Flag, "log", translate("Log")) o.default = "1" o.rmempty = false -api.luci_types(arg[1], m, s, type_name, option_prefix) +api.luci_types(s1, s) \ No newline at end of file diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua index 20ac33e8..ae79afb3 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua @@ -1,29 +1,25 @@ -local m, s = ... - if not api.finded_com("xray") then return end +-- [[ Xray ]] +local m, s1 = ... local type_name = "Xray" --- [[ Xray ]] - -s.fields["type"]:value(type_name, "Xray") -if not s.fields["type"].default then - s.fields["type"].default = type_name +s1.fields["type"]:value(type_name, "Xray") +if not s1.fields["type"].default then + s1.fields["type"].default = type_name end -if s.val["type"] and s.val["type"] ~= type_name then +if s1.val["type"] and s1.val["type"] ~= type_name then return end -local option_prefix = "xray_" +local s = NamedSection(m, arg[1], "server") +s.type_name = type_name +s.option_prefix = "xray_" -local function _n(name) - return option_prefix .. name -end - -local x_ss_method_list = { +local ss_method_list = { "aes-128-gcm", "aes-256-gcm", "chacha20-poly1305", "xchacha20-poly1305", "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305" } @@ -31,90 +27,91 @@ local header_type_list = { "none", "srtp", "utp", "wechat-video", "dtls", "wireguard", "dns" } -o = s:option(Flag, _n("custom"), translate("Use Custom Config")) +o = s:option(Flag, "custom", translate("Use Custom Config")) -o = s:option(ListValue, _n("protocol"), translate("Protocol")) +o = s:option(ListValue, "protocol", translate("Protocol")) o:value("vmess", "Vmess") o:value("vless", "VLESS") o:value("http", "HTTP") o:value("socks", "Socks") o:value("shadowsocks", "Shadowsocks") o:value("trojan", "Trojan") -o:value("hysteria2", translate("Hysteria2")) +o:value("hysteria2", "Hysteria2") +o:value("wireguard", "WireGuard") o:value("dokodemo-door", "dokodemo-door") -o:depends({ [_n("custom")] = false }) +o:depends({ custom = false }) -o = s:option(Value, _n("port"), translate("Listen Port")) +o = s:option(Value, "port", translate("Listen Port")) o.datatype = "port" -o:depends({ [_n("custom")] = false }) +o:depends({ custom = false }) -o = s:option(DynamicList, _n("users"), translate("User")) +o = s:option(DynamicList, "users", translate("User")) for i, v in ipairs(user_list) do o:value(v[".name"], v.username) end -o:depends({ [_n("protocol")] = "http" }) -o:depends({ [_n("protocol")] = "socks" }) -o:depends({ [_n("protocol")] = "shadowsocks" }) -o:depends({ [_n("protocol")] = "vmess" }) -o:depends({ [_n("protocol")] = "vless" }) -o:depends({ [_n("protocol")] = "trojan" }) -o:depends({ [_n("protocol")] = "hysteria2" }) +o:depends({ protocol = "http" }) +o:depends({ protocol = "socks" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) +o:depends({ protocol = "trojan" }) +o:depends({ protocol = "hysteria2" }) +o:depends({ protocol = "wireguard" }) -o = s:option(ListValue, _n("d_protocol"), translate("Destination protocol")) +o = s:option(ListValue, "d_protocol", translate("Destination protocol")) o:value("tcp", "TCP") o:value("udp", "UDP") o:value("tcp,udp", "TCP,UDP") -o:depends({ [_n("protocol")] = "dokodemo-door" }) +o:depends({ protocol = "dokodemo-door" }) -o = s:option(Value, _n("d_address"), translate("Destination address")) -o:depends({ [_n("protocol")] = "dokodemo-door" }) +o = s:option(Value, "d_address", translate("Destination address")) +o:depends({ protocol = "dokodemo-door" }) -o = s:option(Value, _n("d_port"), translate("Destination port")) +o = s:option(Value, "d_port", translate("Destination port")) o.datatype = "port" -o:depends({ [_n("protocol")] = "dokodemo-door" }) +o:depends({ protocol = "dokodemo-door" }) -o = s:option(Value, _n("decryption"), translate("Encrypt Method") .. " (decryption)") +o = s:option(Value, "decryption", translate("Encrypt Method") .. " (decryption)") o.default = "none" o.placeholder = "none" -o:depends({ [_n("protocol")] = "vless" }) +o:depends({ protocol = "vless" }) o.validate = function(self, value) value = api.trim(value) return (value == "" and "none" or value) end -o = s:option(ListValue, _n("x_ss_method"), translate("Encrypt Method")) -o.rewrite_option = "method" -for a, t in ipairs(x_ss_method_list) do o:value(t) end -o:depends({ [_n("protocol")] = "shadowsocks" }) +o = s:option(ListValue, "ss_method", translate("Encrypt Method")) +for a, t in ipairs(ss_method_list) do o:value(t) end +o:depends({ protocol = "shadowsocks" }) -o = s:option(Value, _n("ss_password"), translate("Password")) -o:depends({ [_n("protocol")] = "shadowsocks" }) +o = s:option(Value, "ss_password", translate("Password")) +o:depends({ protocol = "shadowsocks" }) -o = s:option(ListValue, _n("ss_network"), translate("Transport")) +o = s:option(ListValue, "ss_network", translate("Transport")) o.default = "tcp,udp" o:value("tcp", "TCP") o:value("udp", "UDP") o:value("tcp,udp", "TCP,UDP") -o:depends({ [_n("protocol")] = "shadowsocks" }) +o:depends({ protocol = "shadowsocks" }) -o = s:option(Flag, _n("udp_forward"), translate("UDP Forward")) +o = s:option(Flag, "udp_forward", translate("UDP Forward")) o.default = "1" o.rmempty = false -o:depends({ [_n("protocol")] = "socks" }) +o:depends({ protocol = "socks" }) -o = s:option(ListValue, _n("flow"), translate("flow")) +o = s:option(ListValue, "flow", translate("flow")) o.default = "" o:value("", translate("Disable")) o:value("xtls-rprx-vision") -o:depends({ [_n("protocol")] = "vless" }) +o:depends({ protocol = "vless" }) ---- [[ hysteria2 ]] -o = s:option(Flag, _n("hysteria2_realms"), translate("Realms")) +o = s:option(Flag, "hysteria2_realms", translate("Realms")) o.default = "0" -o:depends({ [_n("protocol")] = "hysteria2"}) +o:depends({ protocol = "hysteria2"}) -o = s:option(Value, _n("hysteria2_realm_url"), translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name") -o:depends({ [_n("hysteria2_realms")] = "1" }) +o = s:option(Value, "hysteria2_realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name") +o:depends({ hysteria2_realms = "1" }) o.validate = function(self, value) value = api.trim(value) local realm = api.parse_realm_uri(value) @@ -122,52 +119,52 @@ o.validate = function(self, value) return nil, translate("Invalid Realm URL.") end -o = s:option(DynamicList, _n("hysteria2_realm_stun"), translate("Realm STUN")) +o = s:option(DynamicList, "hysteria2_realm_stun", translate("Realm STUN")) o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" } -o:depends({ [_n("hysteria2_realms")] = "1" }) +o:depends({ hysteria2_realms = "1" }) -o = s:option(ListValue, _n("hysteria2_obfs_type"), translate("Obfs Type")) +o = s:option(ListValue, "hysteria2_obfs_type", translate("Obfs Type")) o:value("", translate("Disable")) o:value("salamander") o:value("gecko") -o:depends({ [_n("protocol")] = "hysteria2" }) +o:depends({ protocol = "hysteria2" }) -o = s:option(Value, _n("hysteria2_obfs_password"), translate("Obfs Password")) -o:depends({ [_n("hysteria2_obfs_type")] = "salamander" }) -o:depends({ [_n("hysteria2_obfs_type")] = "gecko" }) +o = s:option(Value, "hysteria2_obfs_password", translate("Obfs Password")) +o:depends({ hysteria2_obfs_type = "salamander" }) +o:depends({ hysteria2_obfs_type = "gecko" }) -o = s:option(Value, _n("hysteria2_obfs_MinPacketSize"), translate("Gecko Packet Size (min)")) +o = s:option(Value, "hysteria2_obfs_MinPacketSize", translate("Gecko Packet Size (min)")) o.datatype = "uinteger" o.placeholder = "512" o.default = "512" -o:depends({ [_n("hysteria2_obfs_type")] = "gecko" }) +o:depends({ hysteria2_obfs_type = "gecko" }) -o = s:option(Value, _n("hysteria2_obfs_MaxPacketSize"), translate("Gecko Packet Size (max)")) +o = s:option(Value, "hysteria2_obfs_MaxPacketSize", translate("Gecko Packet Size (max)")) o.datatype = "uinteger" o.placeholder = "1200" o.default = "1200" -o:depends({ [_n("hysteria2_obfs_type")] = "gecko" }) +o:depends({ hysteria2_obfs_type = "gecko" }) -o = s:option(Flag, _n("hysteria2_ignore_client_bandwidth"), translate("Client BBR Flow Control")) +o = s:option(Flag, "hysteria2_ignore_client_bandwidth", translate("Client BBR Flow Control")) o.default = 0 -o:depends({ [_n("protocol")] = "hysteria2" }) +o:depends({ protocol = "hysteria2" }) -o = s:option(Value, _n("hysteria2_up_mbps"), translate("Max upload Mbps")) -o:depends({ [_n("protocol")] = "hysteria2", [_n("hysteria2_ignore_client_bandwidth")] = false }) +o = s:option(Value, "hysteria2_up_mbps", translate("Max upload Mbps")) +o:depends({ protocol = "hysteria2", hysteria2_ignore_client_bandwidth = false }) -o = s:option(Value, _n("hysteria2_down_mbps"), translate("Max download Mbps")) -o:depends({ [_n("protocol")] = "hysteria2", [_n("hysteria2_ignore_client_bandwidth")] = false }) +o = s:option(Value, "hysteria2_down_mbps", translate("Max download Mbps")) +o:depends({ protocol = "hysteria2", hysteria2_ignore_client_bandwidth = false }) ---- [[ TLS ]] -o = s:option(Flag, _n("tls"), translate("TLS")) +o = s:option(Flag, "tls", "TLS") o.default = 0 o.validate = function(self, value, t) if value then - local reality = s.fields[_n("reality")] and s.fields[_n("reality")]:formvalue(t) or nil + local reality = s.fields["reality"] and s.fields["reality"]:formvalue(t) or nil if reality and reality == "1" then return value end if value == "1" then - local ca = s.fields[_n("tls_certificateFile")] and s.fields[_n("tls_certificateFile")]:formvalue(t) or "" - local key = s.fields[_n("tls_keyFile")] and s.fields[_n("tls_keyFile")]:formvalue(t) or "" + local ca = s.fields["tls_certificateFile"] and s.fields["tls_certificateFile"]:formvalue(t) or "" + local key = s.fields["tls_keyFile"] and s.fields["tls_keyFile"]:formvalue(t) or "" if ca == "" or key == "" then return nil, translate("Public key and Private key path can not be empty!") end @@ -175,32 +172,32 @@ o.validate = function(self, value, t) return value end end -o:depends({ [_n("protocol")] = "vmess" }) -o:depends({ [_n("protocol")] = "vless" }) -o:depends({ [_n("protocol")] = "http" }) -o:depends({ [_n("protocol")] = "socks" }) -o:depends({ [_n("protocol")] = "shadowsocks" }) -o:depends({ [_n("protocol")] = "trojan" }) +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) +o:depends({ protocol = "http" }) +o:depends({ protocol = "socks" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "trojan" }) -- [[ REALITY ]] -- -o = s:option(Flag, _n("reality"), translate("REALITY")) +o = s:option(Flag, "reality", translate("REALITY")) o.default = 0 -o:depends({ [_n("tls")] = true }) +o:depends({ tls = true }) -o = s:option(Value, _n("reality_private_key"), translate("Private Key")) -o:depends({ [_n("reality")] = true }) +o = s:option(Value, "reality_private_key", translate("Private Key")) +o:depends({ reality = true }) -o = s:option(DynamicList, _n("reality_shortId"), translate("Short Id")) -o:depends({ [_n("reality")] = true }) +o = s:option(DynamicList, "reality_shortId", translate("Short Id")) +o:depends({ reality = true }) -o = s:option(Value, _n("reality_dest"), translate("Dest")) +o = s:option(Value, "reality_dest", translate("Dest")) o.default = "google.com:443" -o:depends({ [_n("reality")] = true }) +o:depends({ reality = true }) -o = s:option(DynamicList, _n("reality_serverNames"), translate("serverNames")) -o:depends({ [_n("reality")] = true }) +o = s:option(DynamicList, "reality_serverNames", translate("serverNames")) +o:depends({ reality = true }) -o = s:option(ListValue, _n("alpn"), translate("alpn")) +o = s:option(ListValue, "alpn", translate("alpn")) o.default = "default" o:value("default", translate("Default")) o:value("h3") @@ -209,35 +206,35 @@ o:value("h3,h2") o:value("http/1.1") o:value("h2,http/1.1") o:value("h3,h2,http/1.1") -o:depends({ [_n("tls")] = true, [_n("reality")] = false }) +o:depends({ tls = true, reality = false }) -o = s:option(Flag, _n("use_mldsa65Seed"), translate("ML-DSA-65")) +o = s:option(Flag, "use_mldsa65Seed", translate("ML-DSA-65")) o.default = "0" -o:depends({ [_n("reality")] = true }) +o:depends({ reality = true }) -o = s:option(TextValue, _n("reality_mldsa65Seed"), "ML-DSA-65 " .. translate("Private Key")) +o = s:option(TextValue, "reality_mldsa65Seed", "ML-DSA-65 " .. translate("Private Key")) o.default = "" o.rows = 5 o.wrap = "soft" -o:depends({ [_n("use_mldsa65Seed")] = true }) +o:depends({ use_mldsa65Seed = true }) o.validate = function(self, value) return api.trim(value:gsub("[\r\n]", "")) end --- o = s:option(Value, _n("minversion"), translate("minversion")) +-- o = s:option(Value, "minversion", translate("minversion")) -- o.default = "1.3" -- o:value("1.3") ---o:depends({ [_n("tls")] = true }) +--o:depends({ tls = true }) -- [[ TLS ]] -- -o = s:option(FileUpload, _n("tls_certificateFile"), translate("Public key absolute path"), translate("as:") .. "/etc/ssl/fullchain.pem") +o = s:option(FileUpload, "tls_certificateFile", translate("Public key absolute path"), translate("as:") .. "/etc/ssl/fullchain.pem") o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. arg[1] .. ".pem" if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end -o:depends({ [_n("tls")] = true, [_n("reality")] = false }) -o:depends({ [_n("protocol")] = "hysteria2"}) +o:depends({ tls = true, reality = false }) +o:depends({ protocol = "hysteria2"}) o.validate = function(self, value, t) if value and value ~= "" then - if not nixio.fs.access(value) then + if not api.fs.access(value) then return nil, translate("Can't find this file!") else return value @@ -246,14 +243,14 @@ o.validate = function(self, value, t) return nil end -o = s:option(FileUpload, _n("tls_keyFile"), translate("Private key absolute path"), translate("as:") .. "/etc/ssl/private.key") +o = s:option(FileUpload, "tls_keyFile", translate("Private key absolute path"), translate("as:") .. "/etc/ssl/private.key") o.default = m:get(s.section, "tls_keyFile") or "/etc/config/ssl/" .. arg[1] .. ".key" if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end -o:depends({ [_n("tls")] = true, [_n("reality")] = false }) -o:depends({ [_n("protocol")] = "hysteria2"}) +o:depends({ tls = true, reality = false }) +o:depends({ protocol = "hysteria2"}) o.validate = function(self, value, t) if value and value ~= "" then - if not nixio.fs.access(value) then + if not api.fs.access(value) then return nil, translate("Can't find this file!") else return value @@ -262,117 +259,116 @@ o.validate = function(self, value, t) return nil end -o = s:option(Flag, _n("ech"), translate("ECH")) +o = s:option(Flag, "ech", translate("ECH")) o.default = "0" -o:depends({ [_n("tls")] = true, [_n("reality")] = false }) +o:depends({ tls = true, reality = false }) -o = s:option(TextValue, _n("ech_key"), translate("ECH Key")) +o = s:option(TextValue, "ech_key", translate("ECH Key")) o.default = "" o.rows = 5 o.wrap = "soft" -o:depends({ [_n("ech")] = true }) +o:depends({ ech = true }) o.validate = function(self, value) return api.trim(value:gsub("[\r\n]", "")) end -o = s:option(ListValue, _n("transport"), translate("Transport")) +o = s:option(ListValue, "transport", translate("Transport")) o:value("raw", "RAW") o:value("mkcp", "mKCP") o:value("ws", "WebSocket") o:value("grpc", "gRPC") o:value("httpupgrade", "HttpUpgrade") o:value("xhttp", "XHTTP") -o:depends({ [_n("protocol")] = "vmess" }) -o:depends({ [_n("protocol")] = "vless" }) -o:depends({ [_n("protocol")] = "socks" }) -o:depends({ [_n("protocol")] = "shadowsocks" }) -o:depends({ [_n("protocol")] = "trojan" }) +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) +o:depends({ protocol = "socks" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "trojan" }) -- [[ WebSocket ]]-- -o = s:option(Value, _n("ws_host"), translate("WebSocket Host")) -o:depends({ [_n("transport")] = "ws" }) +o = s:option(Value, "ws_host", translate("WebSocket Host")) +o:depends({ transport = "ws" }) -o = s:option(Value, _n("ws_path"), translate("WebSocket Path")) -o:depends({ [_n("transport")] = "ws" }) +o = s:option(Value, "ws_path", translate("WebSocket Path")) +o:depends({ transport = "ws" }) -- [[ HttpUpgrade ]]-- -o = s:option(Value, _n("httpupgrade_host"), translate("HttpUpgrade Host")) -o:depends({ [_n("transport")] = "httpupgrade" }) +o = s:option(Value, "httpupgrade_host", translate("HttpUpgrade Host")) +o:depends({ transport = "httpupgrade" }) -o = s:option(Value, _n("httpupgrade_path"), translate("HttpUpgrade Path")) +o = s:option(Value, "httpupgrade_path", translate("HttpUpgrade Path")) o.placeholder = "/" -o:depends({ [_n("transport")] = "httpupgrade" }) +o:depends({ transport = "httpupgrade" }) -- [[ XHTTP ]]-- -o = s:option(Value, _n("xhttp_host"), translate("XHTTP Host")) -o:depends({ [_n("transport")] = "xhttp" }) +o = s:option(Value, "xhttp_host", translate("XHTTP Host")) +o:depends({ transport = "xhttp" }) -o = s:option(Value, _n("xhttp_path"), translate("XHTTP Path")) +o = s:option(Value, "xhttp_path", translate("XHTTP Path")) o.placeholder = "/" -o:depends({ [_n("transport")] = "xhttp" }) +o:depends({ transport = "xhttp" }) -o = s:option(Value, _n("xhttp_maxuploadsize"), translate("maxUploadSize")) +o = s:option(Value, "xhttp_maxuploadsize", translate("maxUploadSize")) o.default = "1000000" -o:depends({ [_n("transport")] = "xhttp" }) +o:depends({ transport = "xhttp" }) -o = s:option(Value, _n("xhttp_maxconcurrentuploads"), translate("maxConcurrentUploads")) +o = s:option(Value, "xhttp_maxconcurrentuploads", translate("maxConcurrentUploads")) o.default = "10" -o:depends({ [_n("transport")] = "xhttp" }) +o:depends({ transport = "xhttp" }) -o = s:option(Value, _n("splithttp_maxuploadsize"), translate("maxUploadSize")) +o = s:option(Value, "splithttp_maxuploadsize", translate("maxUploadSize")) o.default = "1000000" -o:depends({ [_n("transport")] = "splithttp" }) +o:depends({ transport = "splithttp" }) -o = s:option(Value, _n("splithttp_maxconcurrentuploads"), translate("maxConcurrentUploads")) +o = s:option(Value, "splithttp_maxconcurrentuploads", translate("maxConcurrentUploads")) o.default = "10" -o:depends({ [_n("transport")] = "splithttp" }) +o:depends({ transport = "splithttp" }) -- [[ TCP ]]-- -o = s:option(ListValue, _n("tcp_guise"), translate("Camouflage Type")) +o = s:option(ListValue, "tcp_guise", translate("Camouflage Type")) o:value("none", "none") o:value("http", "http") -o:depends({ [_n("transport")] = "raw" }) +o:depends({ transport = "raw" }) -o = s:option(DynamicList, _n("tcp_guise_http_host"), translate("HTTP Host")) -o:depends({ [_n("tcp_guise")] = "http" }) +o = s:option(DynamicList, "tcp_guise_http_host", translate("HTTP Host")) +o:depends({ tcp_guise = "http" }) -o = s:option(DynamicList, _n("tcp_guise_http_path"), translate("HTTP Path")) -o:depends({ [_n("tcp_guise")] = "http" }) +o = s:option(DynamicList, "tcp_guise_http_path", translate("HTTP Path")) +o:depends({ tcp_guise = "http" }) -- [[ mKCP ]]-- -o = s:option(ListValue, _n("mkcp_guise"), translate("Camouflage Type"), translate('
none: default, no masquerade, data sent is packets with no characteristics.
srtp: disguised as an SRTP packet, it will be recognized as video call data (such as FaceTime).
utp: packets disguised as uTP will be recognized as bittorrent downloaded data.
wechat-video: packets disguised as WeChat video calls.
dtls: disguised as DTLS 1.2 packet.
wireguard: disguised as a WireGuard packet. (not really WireGuard protocol)
dns: Disguising traffic as DNS requests.')) +o = s:option(ListValue, "mkcp_guise", translate("Camouflage Type"), translate('
none: default, no masquerade, data sent is packets with no characteristics.
srtp: disguised as an SRTP packet, it will be recognized as video call data (such as FaceTime).
utp: packets disguised as uTP will be recognized as bittorrent downloaded data.
wechat-video: packets disguised as WeChat video calls.
dtls: disguised as DTLS 1.2 packet.
wireguard: disguised as a WireGuard packet. (not really WireGuard protocol)
dns: Disguising traffic as DNS requests.')) for a, t in ipairs(header_type_list) do o:value(t) end -o:depends({ [_n("transport")] = "mkcp" }) +o:depends({ transport = "mkcp" }) -o = s:option(Value, _n("mkcp_domain"), translate("Camouflage Domain"), translate("Use it together with the DNS disguised type. You can fill in any domain.")) -o:depends({ [_n("mkcp_guise")] = "dns" }) +o = s:option(Value, "mkcp_domain", translate("Camouflage Domain"), translate("Use it together with the DNS disguised type. You can fill in any domain.")) +o:depends({ mkcp_guise = "dns" }) -o = s:option(Value, _n("mkcp_mtu"), translate("KCP MTU")) +o = s:option(Value, "mkcp_mtu", translate("KCP MTU")) o.datatype = "uinteger" o.default = 1350 -o:depends({ [_n("transport")] = "mkcp" }) +o:depends({ transport = "mkcp" }) -o = s:option(Value, _n("mkcp_seed"), translate("KCP Seed")) -o:depends({ [_n("transport")] = "mkcp" }) +o = s:option(Value, "mkcp_seed", translate("KCP Seed")) +o:depends({ transport = "mkcp" }) -- [[ gRPC ]]-- -o = s:option(Value, _n("grpc_serviceName"), "ServiceName") -o:depends({ [_n("transport")] = "grpc" }) +o = s:option(Value, "grpc_serviceName", "ServiceName") +o:depends({ transport = "grpc" }) --[[FinalMask]] -o = s:option(Flag, _n("use_finalmask"), "FinalMask") +o = s:option(Flag, "use_finalmask", "FinalMask") o.default = "0" -o:depends({ [_n("custom")] = false, [_n("protocol")] = "vmess" }) -o:depends({ [_n("custom")] = false, [_n("protocol")] = "vless" }) -o:depends({ [_n("custom")] = false, [_n("protocol")] = "trojan" }) -o:depends({ [_n("custom")] = false, [_n("protocol")] = "shadowsocks" }) -o:depends({ [_n("custom")] = false, [_n("protocol")] = "wireguard" }) -o:depends({ [_n("custom")] = false, [_n("protocol")] = "hysteria2", [_n("hysteria2_realms")] = false }) +o:depends({ custom = false, protocol = "vmess" }) +o:depends({ custom = false, protocol = "vless" }) +o:depends({ custom = false, protocol = "trojan" }) +o:depends({ custom = false, protocol = "shadowsocks" }) +o:depends({ custom = false, protocol = "hysteria2", hysteria2_realms = false }) -o = s:option(TextValue, _n("finalmask"), "FinalMask JSON") -o:depends({ [_n("use_finalmask")] = true }) +o = s:option(TextValue, "finalmask", "FinalMask JSON") +o:depends({ use_finalmask = true }) o.rows = 10 o.wrap = "off" o.datatype = "json" @@ -387,48 +383,78 @@ o.custom_write = function(self, section, value) end --[[acceptProxyProtocol]] -o = s:option(Flag, _n("acceptProxyProtocol"), translate("acceptProxyProtocol"), translate("Whether to receive PROXY protocol, when this node want to be fallback or forwarded by proxy, it must be enable, otherwise it cannot be used.")) +o = s:option(Flag, "acceptProxyProtocol", translate("acceptProxyProtocol"), translate("Whether to receive PROXY protocol, when this node want to be fallback or forwarded by proxy, it must be enable, otherwise it cannot be used.")) o.default = "0" -o:depends({ [_n("custom")] = false }) +o:depends({ custom = false }) --[[Fast Open]] -o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open")) +o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open")) o.default = "0" -o:depends({ [_n("protocol")] = "vmess", [_n("custom")] = false }) -o:depends({ [_n("protocol")] = "vless", [_n("custom")] = false }) -o:depends({ [_n("protocol")] = "shadowsocks", [_n("custom")] = false }) -o:depends({ [_n("protocol")] = "trojan", [_n("custom")] = false }) +o:depends({ protocol = "vmess", custom = false }) +o:depends({ protocol = "vless", custom = false }) +o:depends({ protocol = "shadowsocks", custom = false }) +o:depends({ protocol = "trojan", custom = false }) -- [[ Fallback ]]-- -o = s:option(Flag, _n("fallback"), translate("Fallback")) -o:depends({ [_n("protocol")] = "vless", [_n("transport")] = "raw" }) -o:depends({ [_n("protocol")] = "trojan", [_n("transport")] = "raw" }) +o = s:option(Flag, "fallback", translate("Fallback")) +o:depends({ protocol = "vless", transport = "raw" }) +o:depends({ protocol = "trojan", transport = "raw" }) --[[ -o = s:option(Value, _n("fallback_alpn"), "Fallback alpn") -o:depends({ [_n("fallback")] = true }) +o = s:option(Value, "fallback_alpn", "Fallback alpn") +o:depends({ fallback = true }) -o = s:option(Value, _n("fallback_path"), "Fallback path") -o:depends({ [_n("fallback")] = true }) +o = s:option(Value, "fallback_path", "Fallback path") +o:depends({ fallback = true }) -o = s:option(Value, _n("fallback_dest"), "Fallback dest") -o:depends({ [_n("fallback")] = true }) +o = s:option(Value, "fallback_dest", "Fallback dest") +o:depends({ fallback = true }) -o = s:option(Value, _n("fallback_xver"), "Fallback xver") +o = s:option(Value, "fallback_xver", "Fallback xver") o.default = 0 -o:depends({ [_n("fallback")] = true }) +o:depends({ fallback = true }) ]]-- -o = s:option(DynamicList, _n("fallback_list"), "Fallback", translate("format: dest,path,xver")) -o:depends({ [_n("fallback")] = true }) +o = s:option(DynamicList, "fallback_list", "Fallback", translate("format: dest,path,xver")) +o:depends({ fallback = true }) -o = s:option(Flag, _n("bind_local"), translate("Bind Local"), translate("When selected, it can only be accessed localhost.")) -o.default = "0" -o:depends({ [_n("custom")] = false }) +-- Not supported at present +--[[ +o = s:option(Flag, "wireguard_system_interface", translate("System interface")) +o.default = 0 +o:depends({ protocol = "wireguard" }) +]]-- -o = s:option(Flag, _n("accept_lan"), translate("Accept LAN Access"), translate("When selected, it can accessed lan , this will not be safe!")) +o = s:option(Value, "wireguard_mtu", "MTU") +o.default = "1420" +o:depends({ protocol = "wireguard" }) + +-- Not supported at present +--[[ +o = s:option(DynamicList, "wireguard_local_address", translate("Local Address")) +o:depends({ protocol = "wireguard" }) +]]-- + +o = s:option(Value, "wireguard_private_key", translate("Private Key")) +o.datatype = "base64" +o:depends({ protocol = "wireguard" }) + +o = s:option(Value, "wireguard_public_key", translate("Public Key")) +o.datatype = "base64" +o:depends({ protocol = "wireguard" }) + +o = s:option(DummyValue, "gen_wireguard_key") +o.prefix = option_prefix +o.template = m:template_path("/server/server_wireguard") +o:depends({ protocol = "wireguard" }) + +o = s:option(Flag, "bind_local", translate("Bind Local"), translate("When selected, it can only be accessed localhost.")) o.default = "0" -o:depends({ [_n("custom")] = false }) +o:depends({ custom = false }) + +o = s:option(Flag, "accept_lan", translate("Accept LAN Access"), translate("When selected, it can accessed lan , this will not be safe!")) +o.default = "0" +o:depends({ custom = false }) local nodes_table = {} for k, e in ipairs(api.get_valid_nodes()) do @@ -441,7 +467,7 @@ for k, e in ipairs(api.get_valid_nodes()) do end end -o = s:option(ListValue, _n("outbound_node"), translate("outbound node")) +o = s:option(ListValue, "outbound_node", translate("outbound node")) o:value("", translate("Close")) o:value("_socks", translate("Custom Socks")) o:value("_http", translate("Custom HTTP")) @@ -452,37 +478,37 @@ for k, v in pairs(nodes_table) do o:value(v.id, v.remarks) o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default") end -o:depends({ [_n("custom")] = false }) +o:depends({ custom = false }) -o = s:option(Value, _n("outbound_node_address"), translate("Address (Support Domain Name)")) -o:depends({ [_n("outbound_node")] = "_socks"}) -o:depends({ [_n("outbound_node")] = "_http"}) +o = s:option(Value, "outbound_node_address", translate("Address (Support Domain Name)")) +o:depends({ outbound_node = "_socks"}) +o:depends({ outbound_node = "_http"}) -o = s:option(Value, _n("outbound_node_port"), translate("Port")) +o = s:option(Value, "outbound_node_port", translate("Port")) o.datatype = "port" -o:depends({ [_n("outbound_node")] = "_socks"}) -o:depends({ [_n("outbound_node")] = "_http"}) +o:depends({ outbound_node = "_socks"}) +o:depends({ outbound_node = "_http"}) -o = s:option(Value, _n("outbound_node_username"), translate("Username")) -o:depends({ [_n("outbound_node")] = "_socks"}) -o:depends({ [_n("outbound_node")] = "_http"}) +o = s:option(Value, "outbound_node_username", translate("Username")) +o:depends({ outbound_node = "_socks"}) +o:depends({ outbound_node = "_http"}) -o = s:option(Value, _n("outbound_node_password"), translate("Password")) +o = s:option(Value, "outbound_node_password", translate("Password")) o.password = true -o:depends({ [_n("outbound_node")] = "_socks"}) -o:depends({ [_n("outbound_node")] = "_http"}) +o:depends({ outbound_node = "_socks"}) +o:depends({ outbound_node = "_http"}) -o = s:option(Value, _n("outbound_node_iface"), translate("Interface")) -o:depends({ [_n("outbound_node")] = "_iface"}) +o = s:option(Value, "outbound_node_iface", translate("Interface")) +o:depends({ outbound_node = "_iface"}) local netdev_list = api.get_network_devices() for _, d in ipairs(netdev_list) do o:value(d.name, d.label) end -o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)") +o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)") o.rows = 10 o.wrap = "off" -o:depends({ [_n("custom")] = true }) +o:depends({ custom = true }) o.datatype = "json" local o_validate = o.validate o.validate = function(self, value) @@ -500,16 +526,16 @@ o.custom_write = function(self, section, value) m:set(section, "config_str", api.base64Encode(value) or "") end -o = s:option(Flag, _n("log"), translate("Log")) +o = s:option(Flag, "log", translate("Log")) o.default = "1" o.rmempty = false -o = s:option(ListValue, _n("loglevel"), translate("Log Level")) +o = s:option(ListValue, "loglevel", translate("Log Level")) o.default = "warning" o:value("debug") o:value("info") o:value("warning") o:value("error") -o:depends({ [_n("log")] = true }) +o:depends({ log = true }) -api.luci_types(arg[1], m, s, type_name, option_prefix) +api.luci_types(s1, s) \ No newline at end of file diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/sing-box.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/sing-box.lua index 103315a7..59b772e3 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/sing-box.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/sing-box.lua @@ -1,29 +1,25 @@ -local m, s = ... - local singbox_bin = api.finded_com("sing-box") if not singbox_bin then return end +-- [[ Sing-Box ]] +local m, s1 = ... local type_name = "sing-box" --- [[ Sing-Box ]] - -s.fields["type"]:value(type_name, "Sing-Box") -if not s.fields["type"].default then - s.fields["type"].default = type_name +s1.fields["type"]:value(type_name, "Sing-Box") +if not s1.fields["type"].default then + s1.fields["type"].default = type_name end -if s.val["type"] and s.val["type"] ~= type_name then +if s1.val["type"] and s1.val["type"] ~= type_name then return end -local option_prefix = "singbox_" - -local function _n(name) - return option_prefix .. name -end +local s = NamedSection(m, arg[1], "server") +s.type_name = type_name +s.option_prefix = "singbox_" local singbox_tags = luci.sys.exec(singbox_bin .. " version | grep 'Tags:' | awk '{print $2}'") @@ -32,9 +28,9 @@ local ss_method_list = { "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305" } -o = s:option(Flag, _n("custom"), translate("Use Custom Config")) +o = s:option(Flag, "custom", translate("Use Custom Config")) -o = s:option(ListValue, _n("protocol"), translate("Protocol")) +o = s:option(ListValue, "protocol", translate("Protocol")) o:value("mixed", "Mixed") o:value("socks", "Socks") o:value("http", "HTTP") @@ -53,74 +49,78 @@ if singbox_tags:find("with_quic") then o:value("hysteria2", "Hysteria2") end o:value("anytls", "AnyTLS") +if singbox_tags:find("with_wireguard") then + o:value("wireguard", "WireGuard") +end o:value("direct", "Direct") -o:depends({ [_n("custom")] = false }) +o:depends({ custom = false }) -o = s:option(Value, _n("port"), translate("Listen Port")) +o = s:option(Value, "port", translate("Listen Port")) o.datatype = "port" -o:depends({ [_n("custom")] = false }) +o:depends({ custom = false }) -o = s:option(DynamicList, _n("users"), translate("User")) +o = s:option(DynamicList, "users", translate("User")) for i, v in ipairs(user_list) do o:value(v[".name"], v.username) end -o:depends({ [_n("protocol")] = "mixed" }) -o:depends({ [_n("protocol")] = "socks" }) -o:depends({ [_n("protocol")] = "http" }) -o:depends({ [_n("protocol")] = "shadowsocks" }) -o:depends({ [_n("protocol")] = "vmess" }) -o:depends({ [_n("protocol")] = "vless" }) -o:depends({ [_n("protocol")] = "trojan" }) -o:depends({ [_n("protocol")] = "naive" }) -o:depends({ [_n("protocol")] = "hysteria" }) -o:depends({ [_n("protocol")] = "tuic" }) -o:depends({ [_n("protocol")] = "hysteria2" }) -o:depends({ [_n("protocol")] = "anytls" }) +o:depends({ protocol = "mixed" }) +o:depends({ protocol = "socks" }) +o:depends({ protocol = "http" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) +o:depends({ protocol = "trojan" }) +o:depends({ protocol = "naive" }) +o:depends({ protocol = "hysteria" }) +o:depends({ protocol = "tuic" }) +o:depends({ protocol = "hysteria2" }) +o:depends({ protocol = "anytls" }) +o:depends({ protocol = "wireguard" }) if singbox_tags:find("with_quic") then - o = s:option(Value, _n("hysteria_obfs"), translate("Obfs Password")) - o:depends({ [_n("protocol")] = "hysteria" }) + o = s:option(Value, "hysteria_obfs", translate("Obfs Password")) + o:depends({ protocol = "hysteria" }) - o = s:option(Value, _n("hysteria_up_mbps"), translate("Max upload Mbps")) + o = s:option(Value, "hysteria_up_mbps", translate("Max upload Mbps")) o.default = "100" - o:depends({ [_n("protocol")] = "hysteria" }) + o:depends({ protocol = "hysteria" }) - o = s:option(Value, _n("hysteria_down_mbps"), translate("Max download Mbps")) + o = s:option(Value, "hysteria_down_mbps", translate("Max download Mbps")) o.default = "100" - o:depends({ [_n("protocol")] = "hysteria" }) + o:depends({ protocol = "hysteria" }) - o = s:option(Value, _n("hysteria_recv_window_conn"), translate("QUIC stream receive window")) - o:depends({ [_n("protocol")] = "hysteria" }) + o = s:option(Value, "hysteria_recv_window_conn", translate("QUIC stream receive window")) + o:depends({ protocol = "hysteria" }) - o = s:option(Value, _n("hysteria_recv_window_client"), translate("QUIC connection receive window")) - o:depends({ [_n("protocol")] = "hysteria" }) + o = s:option(Value, "hysteria_recv_window_client", translate("QUIC connection receive window")) + o:depends({ protocol = "hysteria" }) - o = s:option(Value, _n("hysteria_max_conn_client"), translate("QUIC concurrent bidirectional streams")) + o = s:option(Value, "hysteria_max_conn_client", translate("QUIC concurrent bidirectional streams")) o.default = "1024" - o:depends({ [_n("protocol")] = "hysteria" }) + o:depends({ protocol = "hysteria" }) - o = s:option(Flag, _n("hysteria_disable_mtu_discovery"), translate("Disable MTU detection")) - o:depends({ [_n("protocol")] = "hysteria" }) + o = s:option(Flag, "hysteria_disable_mtu_discovery", translate("Disable MTU detection")) + o:depends({ protocol = "hysteria" }) end if singbox_tags:find("with_quic") then - o = s:option(ListValue, _n("tuic_congestion_control"), translate("Congestion control algorithm")) + o = s:option(ListValue, "tuic_congestion_control", translate("Congestion control algorithm")) o.default = "cubic" o:value("bbr", translate("BBR")) o:value("cubic", translate("CUBIC")) o:value("new_reno", translate("New Reno")) - o:depends({ [_n("protocol")] = "tuic" }) + o:depends({ protocol = "tuic" }) - o = s:option(Flag, _n("tuic_zero_rtt_handshake"), translate("Enable 0-RTT QUIC handshake")) + o = s:option(Flag, "tuic_zero_rtt_handshake", translate("Enable 0-RTT QUIC handshake")) o.default = 0 - o:depends({ [_n("protocol")] = "tuic" }) + o:depends({ protocol = "tuic" }) - o = s:option(Value, _n("tuic_heartbeat"), translate("Heartbeat interval(second)")) + o = s:option(Value, "tuic_heartbeat", translate("Heartbeat interval(second)")) o.datatype = "uinteger" o.default = "3" - o:depends({ [_n("protocol")] = "tuic" }) + o:depends({ protocol = "tuic" }) - o = s:option(ListValue, _n("tuic_alpn"), translate("QUIC TLS ALPN")) + o = s:option(ListValue, "tuic_alpn", translate("QUIC TLS ALPN")) o.default = "default" o:value("default", translate("Default")) o:value("h3") @@ -131,16 +131,16 @@ if singbox_tags:find("with_quic") then o:value("h3,h2,http/1.1") o:value("spdy/3.1") o:value("h3,spdy/3.1") - o:depends({ [_n("protocol")] = "tuic" }) + o:depends({ protocol = "tuic" }) end if singbox_tags:find("with_quic") then - o = s:option(Flag, _n("hysteria2_realms"), translate("Realms")) + o = s:option(Flag, "hysteria2_realms", translate("Realms")) o.default = "0" - o:depends({ [_n("protocol")] = "hysteria2"}) + o:depends({ protocol = "hysteria2"}) - o = s:option(Value, _n("hysteria2_realm_url"), translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name") - o:depends({ [_n("hysteria2_realms")] = "1" }) + o = s:option(Value, "hysteria2_realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name") + o:depends({ hysteria2_realms = "1" }) o.validate = function(self, value) value = api.trim(value) local realm = api.parse_realm_uri(value) @@ -148,83 +148,82 @@ if singbox_tags:find("with_quic") then return nil, translate("Invalid Realm URL.") end - o = s:option(DynamicList, _n("hysteria2_realm_stun"), translate("Realm STUN")) + o = s:option(DynamicList, "hysteria2_realm_stun", translate("Realm STUN")) o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" } - o:depends({ [_n("hysteria2_realms")] = "1" }) + o:depends({ hysteria2_realms = "1" }) - o = s:option(ListValue, _n("hysteria2_obfs_type"), translate("Obfs Type")) + o = s:option(ListValue, "hysteria2_obfs_type", translate("Obfs Type")) o:value("", translate("Disable")) o:value("salamander") o:value("gecko") - o:depends({ [_n("protocol")] = "hysteria2" }) + o:depends({ protocol = "hysteria2" }) - o = s:option(Value, _n("hysteria2_obfs_password"), translate("Obfs Password")) - o:depends({ [_n("hysteria2_obfs_type")] = "salamander" }) - o:depends({ [_n("hysteria2_obfs_type")] = "gecko" }) + o = s:option(Value, "hysteria2_obfs_password", translate("Obfs Password")) + o:depends({ hysteria2_obfs_type = "salamander" }) + o:depends({ hysteria2_obfs_type = "gecko" }) - o = s:option(Value, _n("hysteria2_obfs_MinPacketSize"), translate("Gecko Packet Size (min)")) + o = s:option(Value, "hysteria2_obfs_MinPacketSize", translate("Gecko Packet Size (min)")) o.datatype = "uinteger" o.placeholder = "512" o.default = "512" - o:depends({ [_n("hysteria2_obfs_type")] = "gecko" }) + o:depends({ hysteria2_obfs_type = "gecko" }) - o = s:option(Value, _n("hysteria2_obfs_MaxPacketSize"), translate("Gecko Packet Size (max)")) + o = s:option(Value, "hysteria2_obfs_MaxPacketSize", translate("Gecko Packet Size (max)")) o.datatype = "uinteger" o.placeholder = "1200" o.default = "1200" - o:depends({ [_n("hysteria2_obfs_type")] = "gecko" }) + o:depends({ hysteria2_obfs_type = "gecko" }) - o = s:option(Flag, _n("hysteria2_ignore_client_bandwidth"), translate("Client BBR Flow Control"), translate("Commands the client to use the BBR flow control algorithm")) + o = s:option(Flag, "hysteria2_ignore_client_bandwidth", translate("Client BBR Flow Control"), translate("Commands the client to use the BBR flow control algorithm")) o.default = 0 - o:depends({ [_n("protocol")] = "hysteria2" }) + o:depends({ protocol = "hysteria2" }) - o = s:option(Value, _n("hysteria2_up_mbps"), translate("Max upload Mbps")) - o:depends({ [_n("protocol")] = "hysteria2", [_n("hysteria2_ignore_client_bandwidth")] = false }) + o = s:option(Value, "hysteria2_up_mbps", translate("Max upload Mbps")) + o:depends({ protocol = "hysteria2", hysteria2_ignore_client_bandwidth = false }) - o = s:option(Value, _n("hysteria2_down_mbps"), translate("Max download Mbps")) - o:depends({ [_n("protocol")] = "hysteria2", [_n("hysteria2_ignore_client_bandwidth")] = false }) + o = s:option(Value, "hysteria2_down_mbps", translate("Max download Mbps")) + o:depends({ protocol = "hysteria2", hysteria2_ignore_client_bandwidth = false }) end -o = s:option(ListValue, _n("d_protocol"), translate("Destination protocol")) +o = s:option(ListValue, "d_protocol", translate("Destination protocol")) o:value("tcp", "TCP") o:value("udp", "UDP") o:value("tcp,udp", "TCP,UDP") -o:depends({ [_n("protocol")] = "direct" }) +o:depends({ protocol = "direct" }) -o = s:option(Value, _n("d_address"), translate("Destination address")) -o:depends({ [_n("protocol")] = "direct" }) +o = s:option(Value, "d_address", translate("Destination address")) +o:depends({ protocol = "direct" }) -o = s:option(Value, _n("d_port"), translate("Destination port")) +o = s:option(Value, "d_port", translate("Destination port")) o.datatype = "port" -o:depends({ [_n("protocol")] = "direct" }) +o:depends({ protocol = "direct" }) -o = s:option(Value, _n("decryption"), translate("Encrypt Method")) +o = s:option(Value, "decryption", translate("Encrypt Method")) o.default = "none" -o:depends({ [_n("protocol")] = "vless" }) +o:depends({ protocol = "vless" }) -o = s:option(ListValue, _n("ss_method"), translate("Encrypt Method")) -o.rewrite_option = "method" +o = s:option(ListValue, "ss_method", translate("Encrypt Method")) for a, t in ipairs(ss_method_list) do o:value(t) end -o:depends({ [_n("protocol")] = "shadowsocks" }) +o:depends({ protocol = "shadowsocks" }) -o = s:option(Value, _n("ss_password"), translate("Password")) -o:depends({ [_n("protocol")] = "shadowsocks" }) +o = s:option(Value, "ss_password", translate("Password")) +o:depends({ protocol = "shadowsocks" }) -o = s:option(ListValue, _n("flow"), translate("flow")) +o = s:option(ListValue, "flow", translate("flow")) o.default = "" o:value("", translate("Disable")) o:value("xtls-rprx-vision") -o:depends({ [_n("protocol")] = "vless" , [_n("tls")] = true }) +o:depends({ protocol = "vless" , tls = true }) -o = s:option(Flag, _n("tls"), translate("TLS")) +o = s:option(Flag, "tls", translate("TLS")) o.default = 0 o.validate = function(self, value, t) if value then - local reality = s.fields[_n("reality")] and s.fields[_n("reality")]:formvalue(t) or nil + local reality = s.fields["reality"] and s.fields["reality"]:formvalue(t) or nil if reality and reality == "1" then return value end if value == "1" then - local ca = s.fields[_n("tls_certificateFile")] and s.fields[_n("tls_certificateFile")]:formvalue(t) or "" - local key = s.fields[_n("tls_keyFile")] and s.fields[_n("tls_keyFile")]:formvalue(t) or "" + local ca = s.fields["tls_certificateFile"] and s.fields["tls_certificateFile"]:formvalue(t) or "" + local key = s.fields["tls_keyFile"] and s.fields["tls_keyFile"]:formvalue(t) or "" if ca == "" or key == "" then return nil, translate("Public key and Private key path can not be empty!") end @@ -232,40 +231,40 @@ o.validate = function(self, value, t) return value end end -o:depends({ [_n("protocol")] = "http" }) -o:depends({ [_n("protocol")] = "vmess" }) -o:depends({ [_n("protocol")] = "vless" }) -o:depends({ [_n("protocol")] = "trojan" }) -o:depends({ [_n("protocol")] = "anytls" }) +o:depends({ protocol = "http" }) +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) +o:depends({ protocol = "trojan" }) +o:depends({ protocol = "anytls" }) -- https://github.com/SagerNet/sing-box/commit/d2a04c4e41e6cef0937331cb6d10211f431caaab if singbox_tags:find("with_utls") then -- [[ REALITY ]] -- - o = s:option(Flag, _n("reality"), translate("REALITY")) + o = s:option(Flag, "reality", translate("REALITY")) o.default = 0 - o:depends({ [_n("protocol")] = "http", [_n("tls")] = true }) - o:depends({ [_n("protocol")] = "vmess", [_n("tls")] = true }) - o:depends({ [_n("protocol")] = "vless", [_n("tls")] = true }) - o:depends({ [_n("protocol")] = "trojan", [_n("tls")] = true }) - o:depends({ [_n("protocol")] = "anytls", [_n("tls")] = true }) + o:depends({ protocol = "http", tls = true }) + o:depends({ protocol = "vmess", tls = true }) + o:depends({ protocol = "vless", tls = true }) + o:depends({ protocol = "trojan", tls = true }) + o:depends({ protocol = "anytls", tls = true }) - o = s:option(Value, _n("reality_private_key"), translate("Private Key")) - o:depends({ [_n("reality")] = true }) + o = s:option(Value, "reality_private_key", translate("Private Key")) + o:depends({ reality = true }) - o = s:option(Value, _n("reality_shortId"), translate("Short Id")) - o:depends({ [_n("reality")] = true }) + o = s:option(Value, "reality_shortId", translate("Short Id")) + o:depends({ reality = true }) - o = s:option(Value, _n("reality_handshake_server"), translate("Handshake Server")) + o = s:option(Value, "reality_handshake_server", translate("Handshake Server")) o.default = "google.com" - o:depends({ [_n("reality")] = true }) + o:depends({ reality = true }) - o = s:option(Value, _n("reality_handshake_server_port"), translate("Handshake Server Port")) + o = s:option(Value, "reality_handshake_server_port", translate("Handshake Server Port")) o.datatype = "port" o.default = "443" - o:depends({ [_n("reality")] = true }) + o:depends({ reality = true }) end -o = s:option(ListValue, _n("alpn"), translate("ALPN")) +o = s:option(ListValue, "alpn", translate("ALPN")) o.default = "default" o:value("default", translate("Default")) o:value("h3") @@ -274,22 +273,22 @@ o:value("h3,h2") o:value("http/1.1") o:value("h2,http/1.1") o:value("h3,h2,http/1.1") -o:depends({ [_n("tls")] = true, [_n("reality")] = false }) -o:depends({ [_n("protocol")] = "hysteria" }) +o:depends({ tls = true, reality = false }) +o:depends({ protocol = "hysteria" }) -- [[ TLS ]] -- -o = s:option(FileUpload, _n("tls_certificateFile"), translate("Public key absolute path"), translate("as:") .. "/etc/ssl/fullchain.pem") +o = s:option(FileUpload, "tls_certificateFile", translate("Public key absolute path"), translate("as:") .. "/etc/ssl/fullchain.pem") o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. arg[1] .. ".pem" if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end -o:depends({ [_n("tls")] = true, [_n("reality")] = false }) -o:depends({ [_n("protocol")] = "naive" }) -o:depends({ [_n("protocol")] = "hysteria" }) -o:depends({ [_n("protocol")] = "tuic" }) -o:depends({ [_n("protocol")] = "hysteria2" }) +o:depends({ tls = true, reality = false }) +o:depends({ protocol = "naive" }) +o:depends({ protocol = "hysteria" }) +o:depends({ protocol = "tuic" }) +o:depends({ protocol = "hysteria2" }) o.validate = function(self, value, t) if value and value ~= "" then - if not fs.access(value) then + if not api.fs.access(value) then return nil, translate("Can't find this file!") else return value @@ -298,17 +297,17 @@ o.validate = function(self, value, t) return nil end -o = s:option(FileUpload, _n("tls_keyFile"), translate("Private key absolute path"), translate("as:") .. "/etc/ssl/private.key") +o = s:option(FileUpload, "tls_keyFile", translate("Private key absolute path"), translate("as:") .. "/etc/ssl/private.key") o.default = m:get(s.section, "tls_keyFile") or "/etc/config/ssl/" .. arg[1] .. ".key" if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end -o:depends({ [_n("tls")] = true, [_n("reality")] = false }) -o:depends({ [_n("protocol")] = "naive" }) -o:depends({ [_n("protocol")] = "hysteria" }) -o:depends({ [_n("protocol")] = "tuic" }) -o:depends({ [_n("protocol")] = "hysteria2" }) +o:depends({ tls = true, reality = false }) +o:depends({ protocol = "naive" }) +o:depends({ protocol = "hysteria" }) +o:depends({ protocol = "tuic" }) +o:depends({ protocol = "hysteria2" }) o.validate = function(self, value, t) if value and value ~= "" then - if not fs.access(value) then + if not api.fs.access(value) then return nil, translate("Can't find this file!") else return value @@ -317,19 +316,19 @@ o.validate = function(self, value, t) return nil end -o = s:option(Flag, _n("ech"), translate("ECH")) +o = s:option(Flag, "ech", translate("ECH")) o.default = "0" -o:depends({ [_n("tls")] = true, [_n("flow")] = "", [_n("reality")] = false }) -o:depends({ [_n("protocol")] = "naive" }) -o:depends({ [_n("protocol")] = "hysteria" }) -o:depends({ [_n("protocol")] = "tuic" }) -o:depends({ [_n("protocol")] = "hysteria2", [_n("hysteria2_realms")] = false }) +o:depends({ tls = true, flow = "", reality = false }) +o:depends({ protocol = "naive" }) +o:depends({ protocol = "hysteria" }) +o:depends({ protocol = "tuic" }) +o:depends({ protocol = "hysteria2", hysteria2_realms = false }) -o = s:option(TextValue, _n("ech_key"), translate("ECH Key")) +o = s:option(TextValue, "ech_key", translate("ECH Key")) o.default = "" o.rows = 5 o.wrap = "off" -o:depends({ [_n("ech")] = true }) +o:depends({ ech = true }) o.validate = function(self, value) value = value:gsub("^%s+", ""):gsub("%s+$","\n"):gsub("\r\n","\n"):gsub("[ \t]*\n[ \t]*", "\n") value = value:gsub("^%s*\n", "") @@ -339,74 +338,100 @@ o.validate = function(self, value) return value end -o = s:option(ListValue, _n("transport"), translate("Transport")) +o = s:option(ListValue, "transport", translate("Transport")) o:value("tcp", "TCP") o:value("http", "HTTP") o:value("ws", "WebSocket") o:value("httpupgrade", "HTTPUpgrade") o:value("quic", "QUIC") o:value("grpc", "gRPC") -o:depends({ [_n("protocol")] = "shadowsocks" }) -o:depends({ [_n("protocol")] = "vmess" }) -o:depends({ [_n("protocol")] = "vless" }) -o:depends({ [_n("protocol")] = "trojan" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) +o:depends({ protocol = "trojan" }) -- [[ HTTP ]]-- -o = s:option(DynamicList, _n("http_host"), translate("HTTP Host")) -o:depends({ [_n("transport")] = "http" }) +o = s:option(DynamicList, "http_host", translate("HTTP Host")) +o:depends({ transport = "http" }) -o = s:option(Value, _n("http_path"), translate("HTTP Path")) -o:depends({ [_n("transport")] = "http" }) +o = s:option(Value, "http_path", translate("HTTP Path")) +o:depends({ transport = "http" }) -- [[ WebSocket ]]-- -o = s:option(Value, _n("ws_host"), translate("WebSocket Host")) -o:depends({ [_n("transport")] = "ws" }) +o = s:option(Value, "ws_host", translate("WebSocket Host")) +o:depends({ transport = "ws" }) -o = s:option(Value, _n("ws_path"), translate("WebSocket Path")) -o:depends({ [_n("transport")] = "ws" }) +o = s:option(Value, "ws_path", translate("WebSocket Path")) +o:depends({ transport = "ws" }) -- [[ HTTPUpgrade ]]-- -o = s:option(Value, _n("httpupgrade_host"), translate("HTTPUpgrade Host")) -o:depends({ [_n("transport")] = "httpupgrade" }) +o = s:option(Value, "httpupgrade_host", translate("HTTPUpgrade Host")) +o:depends({ transport = "httpupgrade" }) -o = s:option(Value, _n("httpupgrade_path"), translate("HTTPUpgrade Path")) -o:depends({ [_n("transport")] = "httpupgrade" }) +o = s:option(Value, "httpupgrade_path", translate("HTTPUpgrade Path")) +o:depends({ transport = "httpupgrade" }) -- [[ gRPC ]]-- -o = s:option(Value, _n("grpc_serviceName"), "ServiceName") -o:depends({ [_n("transport")] = "grpc" }) +o = s:option(Value, "grpc_serviceName", "ServiceName") +o:depends({ transport = "grpc" }) -- [[ Mux ]]-- -o = s:option(Flag, _n("mux"), translate("Mux")) +o = s:option(Flag, "mux", translate("Mux")) o.rmempty = false -o:depends({ [_n("protocol")] = "vmess" }) -o:depends({ [_n("protocol")] = "vless", [_n("flow")] = "" }) -o:depends({ [_n("protocol")] = "shadowsocks" }) -o:depends({ [_n("protocol")] = "trojan" }) +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless", flow = "" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "trojan" }) -- [[ TCP Brutal ]]-- -o = s:option(Flag, _n("tcpbrutal"), translate("TCP Brutal")) +o = s:option(Flag, "tcpbrutal", translate("TCP Brutal")) o.default = 0 -o:depends({ [_n("mux")] = true }) +o:depends({ mux = true }) -o = s:option(Value, _n("tcpbrutal_up_mbps"), translate("Max upload Mbps")) +o = s:option(Value, "tcpbrutal_up_mbps", translate("Max upload Mbps")) o.default = "10" -o:depends({ [_n("tcpbrutal")] = true }) +o:depends({ tcpbrutal = true }) -o = s:option(Value, _n("tcpbrutal_down_mbps"), translate("Max download Mbps")) +o = s:option(Value, "tcpbrutal_down_mbps", translate("Max download Mbps")) o.default = "50" -o:depends({ [_n("tcpbrutal")] = true }) +o:depends({ tcpbrutal = true }) -o = s:option(Flag, _n("bind_local"), translate("Bind Local"), translate("When selected, it can only be accessed localhost.")) -o.default = "0" -o:depends({ [_n("custom")] = false }) +if singbox_tags:find("with_wireguard") then + o = s:option(Flag, "wireguard_system_interface", translate("System interface")) + o.default = 0 + o:depends({ protocol = "wireguard" }) -o = s:option(Flag, _n("accept_lan"), translate("Accept LAN Access"), translate("When selected, it can accessed lan , this will not be safe!")) + o = s:option(Value, "wireguard_mtu", "MTU") + o.default = "1408" + o:depends({ protocol = "wireguard" }) + + o = s:option(DynamicList, "wireguard_local_address", translate("Local Address")) + o:depends({ protocol = "wireguard" }) + + o = s:option(Value, "wireguard_private_key", translate("Private Key")) + o.datatype = "base64" + o:depends({ protocol = "wireguard" }) + + o = s:option(Value, "wireguard_public_key", translate("Public Key")) + o.datatype = "base64" + o:depends({ protocol = "wireguard" }) + + o = s:option(DummyValue, "gen_wireguard_key") + o.prefix = option_prefix + o.template = m:template_path("/server/server_wireguard") + o:depends({ protocol = "wireguard" }) +end + +o = s:option(Flag, "bind_local", translate("Bind Local"), translate("When selected, it can only be accessed localhost.")) o.default = "0" -o:depends({ [_n("custom")] = false }) +o:depends({ custom = false }) + +o = s:option(Flag, "accept_lan", translate("Accept LAN Access"), translate("When selected, it can accessed lan , this will not be safe!")) +o.default = "0" +o:depends({ custom = false }) local nodes_table = {} for k, e in ipairs(api.get_valid_nodes()) do @@ -419,7 +444,7 @@ for k, e in ipairs(api.get_valid_nodes()) do end end -o = s:option(ListValue, _n("outbound_node"), translate("outbound node")) +o = s:option(ListValue, "outbound_node", translate("outbound node")) o:value("", translate("Close")) o:value("_socks", translate("Custom Socks")) o:value("_http", translate("Custom HTTP")) @@ -430,37 +455,37 @@ for k, v in pairs(nodes_table) do o:value(v.id, v.remarks) o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default") end -o:depends({ [_n("custom")] = false }) +o:depends({ custom = false }) -o = s:option(Value, _n("outbound_node_address"), translate("Address (Support Domain Name)")) -o:depends({ [_n("outbound_node")] = "_socks" }) -o:depends({ [_n("outbound_node")] = "_http" }) +o = s:option(Value, "outbound_node_address", translate("Address (Support Domain Name)")) +o:depends({ outbound_node = "_socks" }) +o:depends({ outbound_node = "_http" }) -o = s:option(Value, _n("outbound_node_port"), translate("Port")) +o = s:option(Value, "outbound_node_port", translate("Port")) o.datatype = "port" -o:depends({ [_n("outbound_node")] = "_socks" }) -o:depends({ [_n("outbound_node")] = "_http" }) +o:depends({ outbound_node = "_socks" }) +o:depends({ outbound_node = "_http" }) -o = s:option(Value, _n("outbound_node_username"), translate("Username")) -o:depends({ [_n("outbound_node")] = "_socks" }) -o:depends({ [_n("outbound_node")] = "_http" }) +o = s:option(Value, "outbound_node_username", translate("Username")) +o:depends({ outbound_node = "_socks" }) +o:depends({ outbound_node = "_http" }) -o = s:option(Value, _n("outbound_node_password"), translate("Password")) +o = s:option(Value, "outbound_node_password", translate("Password")) o.password = true -o:depends({ [_n("outbound_node")] = "_socks" }) -o:depends({ [_n("outbound_node")] = "_http" }) +o:depends({ outbound_node = "_socks" }) +o:depends({ outbound_node = "_http" }) -o = s:option(Value, _n("outbound_node_iface"), translate("Interface")) -o:depends({ [_n("outbound_node")] = "_iface" }) +o = s:option(Value, "outbound_node_iface", translate("Interface")) +o:depends({ outbound_node = "_iface" }) local netdev_list = api.get_network_devices() for _, d in ipairs(netdev_list) do o:value(d.name, d.label) end -o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)") +o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)") o.rows = 10 o.wrap = "off" -o:depends({ [_n("custom")] = true }) +o:depends({ custom = true }) o.datatype = "json" local o_validate = o.validate o.validate = function(self, value) @@ -478,16 +503,16 @@ o.custom_write = function(self, section, value) m:set(section, "config_str", api.base64Encode(value) or "") end -o = s:option(Flag, _n("log"), translate("Log")) +o = s:option(Flag, "log", translate("Log")) o.default = "1" o.rmempty = false -o = s:option(ListValue, _n("loglevel"), translate("Log Level")) +o = s:option(ListValue, "loglevel", translate("Log Level")) o.default = "info" o:value("debug") o:value("info") o:value("warn") o:value("error") -o:depends({ [_n("log")] = true }) +o:depends({ log = true }) -api.luci_types(arg[1], m, s, type_name, option_prefix) +api.luci_types(s1, s) \ No newline at end of file diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ss-rust.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ss-rust.lua index d94c237e..1c7e88c0 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ss-rust.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ss-rust.lua @@ -1,24 +1,20 @@ -local m, s = ... - if not api.is_finded("ssserver") then return end +-- [[ Shadowsocks Rust ]] +local m, s1 = ... local type_name = "SS-Rust" --- [[ Shadowsocks Rust ]] +s1.fields["type"]:value(type_name, translate("Shadowsocks Rust")) -s.fields["type"]:value(type_name, translate("Shadowsocks Rust")) - -if s.val["type"] and s.val["type"] ~= type_name then +if s1.val["type"] and s1.val["type"] ~= type_name then return end -local option_prefix = "ssrust_" - -local function _n(name) - return option_prefix .. name -end +local s = NamedSection(m, arg[1], "server") +s.type_name = type_name +s.option_prefix = "ssrust_" local ssrust_encrypt_method_list = { "plain", "none", @@ -26,35 +22,12 @@ local ssrust_encrypt_method_list = { "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha8-poly1305", "2022-blake3-chacha20-poly1305" } -o = s:option(Flag, _n("custom"), translate("Use Custom Config")) +o = s:option(Flag, "custom", translate("Use Custom Config")) -o = s:option(Value, _n("port"), translate("Listen Port")) -o.datatype = "port" -o:depends({ [_n("custom")] = false }) - -o = s:option(ListValue, _n("user"), translate("User")) -for i, v in ipairs(user_list) do - o:value(v[".name"], v.username) -end -o:depends({ [_n("custom")] = false }) - -o = s:option(ListValue, _n("method"), translate("Encrypt Method")) -for a, t in ipairs(ssrust_encrypt_method_list) do o:value(t) end -o:depends({ [_n("custom")] = false }) - -o = s:option(Value, _n("timeout"), translate("Connection Timeout")) -o.datatype = "uinteger" -o.default = 300 -o:depends({ [_n("custom")] = false }) - -o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open")) -o.default = "0" -o:depends({ [_n("custom")] = false }) - -o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)") +o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)") o.rows = 10 o.wrap = "off" -o:depends({ [_n("custom")] = true }) +o:depends({ custom = true }) o.datatype = "json" local o_validate = o.validate o.validate = function(self, value) @@ -72,8 +45,31 @@ o.custom_write = function(self, section, value) m:set(section, "config_str", api.base64Encode(value) or "") end -o = s:option(Flag, _n("log"), translate("Log")) +o = s:option(Value, "port", translate("Listen Port")) +o.datatype = "port" +o:depends({ custom = false }) + +o = s:option(ListValue, "user", translate("User")) +for i, v in ipairs(user_list) do + o:value(v[".name"], v.username) +end +o:depends({ custom = false }) + +o = s:option(ListValue, "method", translate("Encrypt Method")) +for a, t in ipairs(ssrust_encrypt_method_list) do o:value(t) end +o:depends({ custom = false }) + +o = s:option(Value, "timeout", translate("Connection Timeout")) +o.datatype = "uinteger" +o.default = 300 +o:depends({ custom = false }) + +o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open")) +o.default = "0" +o:depends({ custom = false }) + +o = s:option(Flag, "log", translate("Log")) o.default = "1" o.rmempty = false -api.luci_types(arg[1], m, s, type_name, option_prefix) +api.luci_types(s1, s) \ No newline at end of file diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ssr.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ssr.lua index d1db102f..32494fb7 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ssr.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ssr.lua @@ -1,24 +1,20 @@ -local m, s = ... - if not api.is_finded("ssr-server") then return end +-- [[ ShadowsocksR ]] +local m, s1 = ... local type_name = "SSR" --- [[ ShadowsocksR ]] +s1.fields["type"]:value(type_name, translate("ShadowsocksR")) -s.fields["type"]:value(type_name, translate("ShadowsocksR")) - -if s.val["type"] and s.val["type"] ~= type_name then +if s1.val["type"] and s1.val["type"] ~= type_name then return end -local option_prefix = "ssr_" - -local function _n(name) - return option_prefix .. name -end +local s = NamedSection(m, arg[1], "server") +s.type_name = type_name +s.option_prefix = "ssr_" local ssr_encrypt_method_list = { "none", "table", "rc2-cfb", "rc4", "rc4-md5", "rc4-md5-6", "aes-128-cfb", @@ -39,49 +35,12 @@ local ssr_obfs_list = { "tls1.0_session_auth", "tls1.2_ticket_auth" } -o = s:option(Flag, _n("custom"), translate("Use Custom Config")) +o = s:option(Flag, "custom", translate("Use Custom Config")) -o = s:option(Value, _n("port"), translate("Listen Port")) -o.datatype = "port" -o:depends({ [_n("custom")] = false }) - -o = s:option(ListValue, _n("user"), translate("User")) -for i, v in ipairs(user_list) do - o:value(v[".name"], v.username) -end -o:depends({ [_n("custom")] = false }) - -o = s:option(ListValue, _n("method"), translate("Encrypt Method")) -for a, t in ipairs(ssr_encrypt_method_list) do o:value(t) end -o:depends({ [_n("custom")] = false }) - -o = s:option(ListValue, _n("protocol"), translate("Protocol")) -for a, t in ipairs(ssr_protocol_list) do o:value(t) end -o:depends({ [_n("custom")] = false }) - -o = s:option(Value, _n("protocol_param"), translate("Protocol_param")) -o:depends({ [_n("custom")] = false }) - -o = s:option(ListValue, _n("obfs"), translate("Obfs")) -for a, t in ipairs(ssr_obfs_list) do o:value(t) end -o:depends({ [_n("custom")] = false }) - -o = s:option(Value, _n("obfs_param"), translate("Obfs_param")) -o:depends({ [_n("custom")] = false }) - -o = s:option(Value, _n("timeout"), translate("Connection Timeout")) -o.datatype = "uinteger" -o.default = 300 -o:depends({ [_n("custom")] = false }) - -o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open")) -o.default = "0" -o:depends({ [_n("custom")] = false }) - -o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)") +o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)") o.rows = 10 o.wrap = "off" -o:depends({ [_n("custom")] = true }) +o:depends({ custom = true }) o.datatype = "json" local o_validate = o.validate o.validate = function(self, value) @@ -99,12 +58,49 @@ o.custom_write = function(self, section, value) m:set(section, "config_str", api.base64Encode(value) or "") end -o = s:option(Flag, _n("udp_forward"), translate("UDP Forward")) +o = s:option(Value, "port", translate("Listen Port")) +o.datatype = "port" +o:depends({ custom = false }) + +o = s:option(ListValue, "user", translate("User")) +for i, v in ipairs(user_list) do + o:value(v[".name"], v.username) +end +o:depends({ custom = false }) + +o = s:option(ListValue, "method", translate("Encrypt Method")) +for a, t in ipairs(ssr_encrypt_method_list) do o:value(t) end +o:depends({ custom = false }) + +o = s:option(ListValue, "protocol", translate("Protocol")) +for a, t in ipairs(ssr_protocol_list) do o:value(t) end +o:depends({ custom = false }) + +o = s:option(Value, "protocol_param", translate("Protocol_param")) +o:depends({ custom = false }) + +o = s:option(ListValue, "obfs", translate("Obfs")) +for a, t in ipairs(ssr_obfs_list) do o:value(t) end +o:depends({ custom = false }) + +o = s:option(Value, "obfs_param", translate("Obfs_param")) +o:depends({ custom = false }) + +o = s:option(Value, "timeout", translate("Connection Timeout")) +o.datatype = "uinteger" +o.default = 300 +o:depends({ custom = false }) + +o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open")) +o.default = "0" +o:depends({ custom = false }) + +o = s:option(Flag, "udp_forward", translate("UDP Forward")) o.default = "1" o.rmempty = false -o = s:option(Flag, _n("log"), translate("Log")) +o = s:option(Flag, "log", translate("Log")) o.default = "1" o.rmempty = false -api.luci_types(arg[1], m, s, type_name, option_prefix) +api.luci_types(s1, s) \ No newline at end of file diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/user_config.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/user_config.lua index 9c0b67d9..0cd4a369 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/user_config.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/user_config.lua @@ -37,4 +37,18 @@ o.datatype = "uuid" o.default = api.gen_uuid() o.rmempty = false +o = s:option(Value, "wireguard_public_key", "Wireguard " .. translate("Public Key")) +o.datatype = "base64" + +o = s:option(Value, "wireguard_private_key", "Wireguard " .. translate("Private Key")) +o.datatype = "base64" + +o = s:option(DummyValue, "gen_wireguard_key") +o.template = m:template_path("/server/server_wireguard") + +o = s:option(Value, "wireguard_pre_shared_key", "Wireguard " .. translate("Pre shared key")) +o.datatype = "base64" + +o = s:option(DynamicList, "allowed_ips", "Wireguard " .. translate("Allowed IPs")) + return api.return_map(m) diff --git a/luci-app-passwall2/luasrc/passwall2/api.lua b/luci-app-passwall2/luasrc/passwall2/api.lua index fa2d2b39..533824f6 100644 --- a/luci-app-passwall2/luasrc/passwall2/api.lua +++ b/luci-app-passwall2/luasrc/passwall2/api.lua @@ -1,3 +1,6 @@ +-- Copyright (C) 2022-2025 xiaorouji +-- Copyright (C) 2026 Openwrt-Passwall Organization + module("luci.passwall2.api", package.seeall) appname = "passwall2" c_config = "passwall2" @@ -17,10 +20,11 @@ command_timeout = 300 OPENWRT_ARCH = nil DISTRIB_ARCH = nil -LOCK_PREFIX = "/tmp/lock/passwall2" -LOG_FILE = "/tmp/log/passwall2.log" -TMP_PATH = "/tmp/etc/passwall2" +LOCK_PREFIX = "/tmp/lock/" .. c_config +LOG_FILE = "/tmp/log/" .. c_config .. ".log" +TMP_PATH = "/tmp/etc/" .. c_config CACHE_PATH = TMP_PATH .. "_tmp" +S_TMP_PATH = "/tmp/etc/" .. s_config TMP_IFACE_PATH = TMP_PATH .. "/iface" local lang = uci:get("luci", "main", "lang") or "auto" @@ -814,16 +818,16 @@ function clone(org) end local function get_bin_version_cache(file, cmd) - sys.call("mkdir -p /tmp/etc/passwall2_tmp") + sys.call("mkdir -p " .. CACHE_PATH) if fs.access(file) then chmod_755(file) local md5 = sys.exec("echo -n $(md5sum " .. file .. " | awk '{print $1}')") - if fs.access("/tmp/etc/passwall2_tmp/" .. md5) then - return sys.exec("echo -n $(cat /tmp/etc/passwall2_tmp/%s)" % md5) + if fs.access(CACHE_PATH .. "/" .. md5) then + return sys.exec("echo -n $(cat %s)" % { CACHE_PATH .. "/" .. md5 }) else local version = sys.exec(string.format("echo -n $(%s %s)", file, cmd)) if version and version ~= "" then - sys.call("echo '" .. version .. "' > " .. "/tmp/etc/passwall2_tmp/" .. md5) + sys.call("echo '%s' > %s" % { version, CACHE_PATH .. "/" .. md5}) return version end end @@ -1415,12 +1419,24 @@ function set_default_cbi() if true then --Map local Map = cbi.Map - local original_init = Map.__init__ + local default_init = Map.__init__ function Map.__init__(self, config, ...) if not config then config = c_config end - original_init(self, config, ...) + default_init(self, config, ...) self.api = require "luci.passwall2.api" end + if is_js_luci() == true then + local default_parse = Map.parse + function Map.parse(self, ...) + apply_redirect(self) + local old = self.on_after_save + self.on_after_save = function(self) + if old then old(self) end + self:set("@global[0]", "timestamp", os.time()) + end + return default_parse(self, ...) + end + end function Map.foreach(self, stype, func) self.uci:foreach(self.config, stype, func) end @@ -1459,25 +1475,6 @@ function set_default_cbi() sh_uci_set(c_config, "@global[0]", "auto_lang", lang, true) end end - if is_js_luci() == true then - local hide_popup_box = nil - if hide_popup_box == true then - Map.apply_on_parse = false - Map.on_after_apply = function(self) - if self.redirect then - os.execute("sleep 1") - luci.http.redirect(self.redirect) - end - end - else - apply_redirect(Map) - local old = Map.on_after_save - Map.on_after_save = function(self) - if old then old(self) end - self:set("@global[0]", "timestamp", os.time()) - end - end - end end if true then --AbstractSection @@ -1498,9 +1495,9 @@ function set_default_cbi() if true then --TextValue local TextValue = cbi.TextValue - local original_init = TextValue.__init__ + local default_init = TextValue.__init__ function TextValue.__init__(self, ...) - original_init(self, ...) + default_init(self, ...) self.template = appname .. "/cbi/tvalue" end end @@ -1543,102 +1540,112 @@ function return_map(map) return map end -function luci_types(id, m, s, type_name, option_prefix) +function luci_types(s, s2) local cbi = require "luci.cbi" + local m = s.map + local id = s2.section + local type_name = s2.type_name + local option_prefix = s2.option_prefix local fv_type local field_type = s.fields["type"] if field_type then fv_type = field_type:formvalue(id) end - local rewrite_option_table = {} - for key, value in pairs(s.fields) do - if key:find(option_prefix) == 1 then - if not s.fields[key].not_rewrite then - if s.fields[key].rewrite_option then - if not rewrite_option_table[s.fields[key].rewrite_option] then - rewrite_option_table[s.fields[key].rewrite_option] = 1 + for i, v in ipairs(s2.children) do + local o = s2.children[i] + o.config_option = o.option + o.option = option_prefix .. o.option + if not o.not_rewrite then + o.cfgvalue = function(self, section) + -- Add a custom `custom_cfgvalue` attribute. If a custom `custom_cfgvalue` function exists, the custom `cfgvalue` logic will be used. + if self.custom_cfgvalue then + return self:custom_cfgvalue(section) + else + if self.rewrite_option then + return m:get(section, self.rewrite_option) else - rewrite_option_table[s.fields[key].rewrite_option] = rewrite_option_table[s.fields[key].rewrite_option] + 1 + return m:get(section, self.config_option) end end - - s.fields[key].cfgvalue = function(self, section) - -- Add a custom `custom_cfgvalue` attribute. If a custom `custom_cfgvalue` function exists, the custom `cfgvalue` logic will be used. - if self.custom_cfgvalue then - return self:custom_cfgvalue(section) + end + o.write = function(self, section, value) + if s.fields["type"]:formvalue(id) == type_name then + -- Add a custom `custom_write` attribute; if a custom `custom_write` function exists, then use the custom write logic. + if self.custom_write then + self:custom_write(section, value) + else + local new_val = value + if util.instanceof(self, cbi.DynamicList) then + local new_t = {} + if type(value) == "table" then + new_t = table_remove_duplicates(value) + else + new_t = { value } + end + if self.cast == "string" then + new_val = table.concat(new_t, " ") + else + new_val = new_t + end + end + if self.rewrite_option then + m:set(section, self.rewrite_option, new_val) + else + m:set(section, self.config_option, new_val) + end + end + end + end + o.remove = function(self, section) + if s.fields["type"]:formvalue(id) == type_name then + -- Add a custom `custom_remove` attribute; if a custom `custom_remove` function exists, use the custom remove logic. + if self.custom_remove then + self:custom_remove(section) else if self.rewrite_option then - return m:get(section, self.rewrite_option) + m:del(section, self.rewrite_option) else - if self.option:find(option_prefix) == 1 then - return m:get(section, self.option:sub(1 + #option_prefix)) - end + m:del(section, self.config_option) end end end - s.fields[key].write = function(self, section, value) - if s.fields["type"]:formvalue(id) == type_name then - -- Add a custom `custom_write` attribute; if a custom `custom_write` function exists, then use the custom write logic. - if self.custom_write then - self:custom_write(section, value) - else - local new_val = value - if util.instanceof(self, cbi.DynamicList) then - local new_t = {} - if type(value) == "table" then - new_t = table_remove_duplicates(value) - else - new_t = { value } - end - if self.cast == "string" then - new_val = table.concat(new_t, " ") - else - new_val = new_t - end - end - if self.rewrite_option then - m:set(section, self.rewrite_option, new_val) - else - if self.option:find(option_prefix) == 1 then - m:set(section, self.option:sub(1 + #option_prefix), new_val) - end - end - end - end - end - s.fields[key].remove = function(self, section) - if s.fields["type"]:formvalue(id) == type_name then - -- Add a custom `custom_remove` attribute; if a custom `custom_remove` function exists, use the custom remove logic. - if self.custom_remove then - self:custom_remove(section) - else - if self.rewrite_option and rewrite_option_table[self.rewrite_option] == 1 then - m:del(section, self.rewrite_option) - else - if self.option:find(option_prefix) == 1 then - m:del(section, self.option:sub(1 + #option_prefix)) - end - end - end - end - end - end - - local deps = s.fields[key].deps - if #deps > 0 then - for index, value in ipairs(deps) do - deps[index]["type"] = type_name - end - else - s.fields[key]:depends({ type = type_name }) - end - - if fv_type and fv_type ~= type_name then - s.fields[key].rmempty = true end end + + local deps = o.deps + if #deps > 0 then + local function process_deps(dep) + local rewrite_deps = {} + for k, v in pairs(dep) do + if k:find("!") then + rewrite_deps[k] = v + else + rewrite_deps[option_prefix .. k] = v + end + end + if not rewrite_deps['!reverse'] then + rewrite_deps["type"] = type_name + end + return rewrite_deps + end + for index, value in ipairs(deps) do + local rewrite_deps = process_deps(value) + if rewrite_deps then + deps[index] = rewrite_deps + end + end + else + o:depends({ type = type_name }) + end + + if fv_type and fv_type ~= type_name then + o.rmempty = true + end + + s:append(o) end end + function format_go_time(input, default) input = input and trim(input) local N = 0 @@ -2018,3 +2025,36 @@ function table_remove_duplicates(t) end return new_t end + +function gen_wireguard_key() + if sys.call("command -v wg >/dev/null") == 0 then + local private_key = sys.exec('echo -n $(wg genkey)') + local public_key = sys.exec('echo -n $(echo "%s" | wg pubkey)' % private_key) + return { + private_key = private_key, + public_key = public_key + } + end + local xray = finded_com("xray") + if xray then + local result = sys.exec(xray .. " wg | awk -F ': ' '{print $2}'") + local s = split(result, "\n") + local private_key = s[1] + local public_key = s[2] + return { + private_key = private_key, + public_key = public_key + } + end + local sb = finded_com("sing-box") + if sb then + local result = sys.exec(sb .. " generate wg-keypair | awk '{print $2}'") + local s = split(result, "\n") + local private_key = s[1] + local public_key = s[2] + return { + private_key = private_key, + public_key = public_key + } + end +end \ No newline at end of file diff --git a/luci-app-passwall2/luasrc/passwall2/server_app.lua b/luci-app-passwall2/luasrc/passwall2/server_app.lua index 1135f6b8..783ee4f9 100644 --- a/luci-app-passwall2/luasrc/passwall2/server_app.lua +++ b/luci-app-passwall2/luasrc/passwall2/server_app.lua @@ -7,7 +7,7 @@ local uci = api.uci local jsonc = api.jsonc local CONFIG = api.s_config -local CONFIG_PATH = "/tmp/etc/" .. CONFIG +local CONFIG_PATH = api.S_TMP_PATH local NFT_INCLUDE_FILE = CONFIG_PATH .. "/" .. CONFIG .. ".nft" local LOG_APP_FILE = "/tmp/log/" .. CONFIG .. ".log" local TMP_BIN_PATH = CONFIG_PATH .. "/bin" @@ -53,7 +53,7 @@ local function ln_run(s, d, command, output) end local function gen_include() - cmd(string.format("echo '#!/bin/sh' > /tmp/etc/%s.include", CONFIG)) + cmd(string.format("echo '#!/bin/sh' > %s.include", CONFIG_PATH)) local function extract_rules(n, a) local _ipt = ipt_bin if n == "6" then @@ -64,7 +64,7 @@ local function gen_include() result = result .. "COMMIT" return result end - local f, err = io.open("/tmp/etc/" .. CONFIG .. ".include", "a") + local f, err = io.open(CONFIG_PATH .. ".include", "a") if f and err == nil then if nft_flag == "0" then f:write(ipt_bin .. '-save -c | grep -v "PSW2-SERVER" | ' .. ipt_bin .. '-restore -c' .. "\n") @@ -232,7 +232,7 @@ local function stop() cmd("nft flush chain inet fw4 PSW2-SERVER 2>/dev/null") cmd("nft delete chain inet fw4 PSW2-SERVER 2>/dev/null") end - cmd(string.format("rm -rf %s %s /tmp/etc/%s.include", CONFIG_PATH, LOG_APP_FILE, CONFIG)) + cmd(string.format("rm -rf %s %s %s", CONFIG_PATH, LOG_APP_FILE, CONFIG_PATH .. ".include")) end if action then diff --git a/luci-app-passwall2/luasrc/passwall2/util_hysteria2.lua b/luci-app-passwall2/luasrc/passwall2/util_hysteria2.lua index 9811daab..2d557bcd 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_hysteria2.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_hysteria2.lua @@ -18,8 +18,8 @@ function gen_config_server(node) end local config = { listen = (function() - if node.hysteria2_realms and node.hysteria2_realm_url then - local url = node.hysteria2_realm_url:gsub("/+$", "") + if node.realms and node.realm_url then + local url = node.realm_url:gsub("/+$", "") if node.port then url = url .. (url:find("?") and "&lport=" or "?lport=") .. node.port end @@ -31,30 +31,30 @@ function gen_config_server(node) cert = node.tls_certificateFile, key = node.tls_keyFile, }, - obfs = (node.hysteria2_obfs_type and node.hysteria2_obfs_password) and { - type = node.hysteria2_obfs_type, - [node.hysteria2_obfs_type] = { - password = node.hysteria2_obfs_password + obfs = (node.obfs_type and node.obfs_password) and { + type = node.obfs_type, + [node.obfs_type] = { + password = node.obfs_password } } or nil, auth = users and { type = "userpass", userpass = users } or nil, - bandwidth = (node.hysteria2_up_mbps or node.hysteria2_down_mbps) and { - up = node.hysteria2_up_mbps and node.hysteria2_up_mbps .. " mbps" or nil, - down = node.hysteria2_down_mbps and node.hysteria2_down_mbps .. " mbps" or nil + bandwidth = (node.up_mbps or node.down_mbps) and { + up = node.up_mbps and node.up_mbps .. " mbps" or nil, + down = node.down_mbps and node.down_mbps .. " mbps" or nil } or nil, - ignoreClientBandwidth = (node.hysteria2_ignoreClientBandwidth == "1") and true or false, - disableUDP = (node.hysteria2_udp == "0") and true or false, - realm = (node.hysteria2_realms and node.hysteria2_realm_stun) and { - stunServers = node.hysteria2_realm_stun + ignoreClientBandwidth = (node.ignoreClientBandwidth == "1") and true or false, + disableUDP = (node.udp == "0") and true or false, + realm = (node.realms and node.realm_stun) and { + stunServers = node.realm_stun } or nil } if config.obfs and config.obfs.gecko then - local min = tonumber(node.hysteria2_obfs_MinPacketSize) or 512 - local max = tonumber(node.hysteria2_obfs_MaxPacketSize) or 1200 + local min = tonumber(node.obfs_MinPacketSize) or 512 + local max = tonumber(node.obfs_MaxPacketSize) or 1200 if min <= 0 or min > max or max > 2048 then min = 512 max = 1200 diff --git a/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua b/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua index c293887c..a5e81a55 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua @@ -402,7 +402,7 @@ function gen_outbound(flag, node, tag, proxy_table) if node.protocol == "shadowsocks" then protocol_table = { - method = node.method or nil, + method = node.ss_method or nil, password = node.password or "", plugin = (node.plugin_enabled and node.plugin) or nil, plugin_opts = (node.plugin_enabled and node.plugin_opts) or nil, @@ -416,7 +416,7 @@ function gen_outbound(flag, node, tag, proxy_table) if node.protocol == "shadowsocksr" then protocol_table = { - method = node.method or nil, + method = node.ssr_method or nil, password = node.password or "", obfs = node.ssr_obfs, obfs_param = node.ssr_obfs_param, @@ -683,6 +683,8 @@ function gen_outbound(flag, node, tag, proxy_table) end function gen_config_server(node) + local endpoints = {} + local inbounds = {} local outbounds = { { type = "direct", tag = "direct" } } @@ -825,6 +827,12 @@ function gen_config_server(node) u.name = user.username u.password = user.password end + if node.protocol == "wireguard" then + u.public_key = user.wireguard_public_key + u.pre_shared_key = user.wireguard_pre_shared_key + u.allowed_ips = user.allowed_ips or {} + u.persistent_keepalive_interval = 0 + end users[#users + 1] = u end end @@ -857,7 +865,7 @@ function gen_config_server(node) if node.protocol == "shadowsocks" then protocol_table = { - method = node.method, + method = node.ss_method, password = node.ss_password, users = users, multiplex = mux, @@ -992,6 +1000,18 @@ function gen_config_server(node) end end + if node.protocol == "wireguard" then + inbound.listen = nil + inbound.system = node.wireguard_system_interface == "1" and true or false + inbound.name = "sbwg_" .. node[".name"] + inbound.mtu = tonumber(node.wireguard_mtu or 1408) + inbound.address = node.wireguard_local_address + inbound.private_key = node.wireguard_private_key + if users then + inbound.peers = users + end + end + if node.protocol == "direct" then protocol_table = { network = (node.d_protocol ~= "TCP,UDP") and node.d_protocol or nil, @@ -1006,6 +1026,13 @@ function gen_config_server(node) end end + if node.protocol == "wireguard" then + inbound.listen = nil + table.insert(endpoints, inbound) + else + table.insert(inbounds, inbound) + end + local route = { rules = { { @@ -1061,7 +1088,8 @@ function gen_config_server(node) } } }, - inbounds = { inbound }, + endpoints = endpoints, + inbounds = inbounds, outbounds = outbounds, route = route } diff --git a/luci-app-passwall2/luasrc/passwall2/util_xray.lua b/luci-app-passwall2/luasrc/passwall2/util_xray.lua index ebe68a89..6b496f0e 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_xray.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_xray.lua @@ -388,8 +388,8 @@ function gen_outbound(flag, node, tag, proxy_table) user = (node.protocol == "socks" or node.protocol == "http") and node.username or nil, pass = (node.protocol == "socks" or node.protocol == "http") and node.password or nil, password = (node.protocol == "shadowsocks" or node.protocol == "trojan") and node.password or nil, - method = (node.protocol == "shadowsocks") and ((node.method == "chacha20-ietf-poly1305" and "chacha20-poly1305") or - (node.method == "xchacha20-ietf-poly1305" and "xchacha20-poly1305") or (node.method ~= "" and node.method) or nil) or nil, + method = (node.protocol == "shadowsocks") and ((node.ss_method == "chacha20-ietf-poly1305" and "chacha20-poly1305") or + (node.ss_method == "xchacha20-ietf-poly1305" and "xchacha20-poly1305") or (node.ss_method ~= "" and node.ss_method) or nil) or nil, secretKey = (node.protocol == "wireguard") and node.wireguard_secret_key or nil, peers = (node.protocol == "wireguard") and { { @@ -523,6 +523,12 @@ function gen_config_server(node) u.email = user.username u.auth = user.password end + if node.protocol == "wireguard" then + u.publicKey = user.wireguard_public_key + u.preSharedKey = user.wireguard_pre_shared_key + u.keepAlive = 0 + u.allowedIPs = user.allowed_ips + end users[#users + 1] = u end end @@ -553,7 +559,7 @@ function gen_config_server(node) node.tcp_guise = "none" elseif node.protocol == "shadowsocks" then settings = { - method = node.method, + method = node.ss_method, password = node.ss_password, users = users, network = node.ss_network or "tcp,udp" @@ -575,6 +581,14 @@ function gen_config_server(node) address = node.d_address, port = tonumber(node.d_port) } + elseif node.protocol == "wireguard" then + settings = { + secretKey = node.wireguard_private_key, + --address = node.wireguard_local_address, + --noKernelTun = node.wireguard_system_interface ~= "1" and true or false, + mtu = tonumber(node.wireguard_mtu or 1420), + peers = users + } end if node.fallback and node.fallback == "1" then diff --git a/luci-app-passwall2/luasrc/view/passwall2/server/server_wireguard.htm b/luci-app-passwall2/luasrc/view/passwall2/server/server_wireguard.htm new file mode 100644 index 00000000..ff73bb97 --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/server/server_wireguard.htm @@ -0,0 +1,28 @@ +<% +local map = self.map +local api = map.api +local prefix = self.prefix +-%> +
" data-index="<%=self.index%>" data-depends="<%=pcdata(self:deplist2json(section))%>"> + +
+ +
+
+ + diff --git a/luci-app-passwall2/po/fa/passwall2.po b/luci-app-passwall2/po/fa/passwall2.po index 9cf5de87..db6cc80a 100644 --- a/luci-app-passwall2/po/fa/passwall2.po +++ b/luci-app-passwall2/po/fa/passwall2.po @@ -1306,6 +1306,12 @@ msgstr "کلید خصوصی" msgid "Pre shared key" msgstr "کلید از پیش به اشتراک گذاشته شده" +msgid "Allowed IPs" +msgstr "آی‌پی‌های مجاز" + +msgid "Generate key" +msgstr "کلید تولید کنید" + msgid "Local Address" msgstr "آدرس محلی" diff --git a/luci-app-passwall2/po/ru/passwall2.po b/luci-app-passwall2/po/ru/passwall2.po index c6c75bc1..bdf699a6 100644 --- a/luci-app-passwall2/po/ru/passwall2.po +++ b/luci-app-passwall2/po/ru/passwall2.po @@ -1307,6 +1307,12 @@ msgstr "Приватный ключ" msgid "Pre shared key" msgstr "Дополнительный симметричный ключ" +msgid "Allowed IPs" +msgstr "Разрешенные IP-адреса" + +msgid "Generate key" +msgstr "Сгенерировать ключ" + msgid "Local Address" msgstr "Локальный адрес" diff --git a/luci-app-passwall2/po/zh-cn/passwall2.po b/luci-app-passwall2/po/zh-cn/passwall2.po index ebf1ddf1..4f9fc1d3 100644 --- a/luci-app-passwall2/po/zh-cn/passwall2.po +++ b/luci-app-passwall2/po/zh-cn/passwall2.po @@ -1298,6 +1298,12 @@ msgstr "私钥" msgid "Pre shared key" msgstr "额外的对称加密密钥" +msgid "Allowed IPs" +msgstr "允许的 IP" + +msgid "Generate key" +msgstr "生成密钥" + msgid "Local Address" msgstr "本地地址" diff --git a/luci-app-passwall2/po/zh-tw/passwall2.po b/luci-app-passwall2/po/zh-tw/passwall2.po index 027e3815..fb961065 100644 --- a/luci-app-passwall2/po/zh-tw/passwall2.po +++ b/luci-app-passwall2/po/zh-tw/passwall2.po @@ -1298,6 +1298,12 @@ msgstr "私鑰" msgid "Pre shared key" msgstr "额外的對称加密密鑰" +msgid "Allowed IPs" +msgstr "允許的 IP" + +msgid "Generate key" +msgstr "生成密鑰" + msgid "Local Address" msgstr "本地地址" diff --git a/luci-app-passwall2/root/usr/share/passwall2/app.sh b/luci-app-passwall2/root/usr/share/passwall2/app.sh index dab2fc00..49ae8c66 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/app.sh +++ b/luci-app-passwall2/root/usr/share/passwall2/app.sh @@ -1120,7 +1120,7 @@ acl_app() { } start() { - busybox pgrep -f /tmp/etc/passwall2/bin > /dev/null 2>&1 && { + busybox pgrep -f ${TMP_PATH}/bin > /dev/null 2>&1 && { logger -t PW2-RESTART "Upgrade or overload residue is detected, and the subprocess is being called to perform complete cleaning..." (stop) sleep 2 diff --git a/luci-app-passwall2/root/usr/share/passwall2/socks_auto_switch.sh b/luci-app-passwall2/root/usr/share/passwall2/socks_auto_switch.sh index db9594fc..c9bcb2e9 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/socks_auto_switch.sh +++ b/luci-app-passwall2/root/usr/share/passwall2/socks_auto_switch.sh @@ -81,10 +81,10 @@ test_node() { local curlx="socks5h://127.0.0.1:${_tmp_port}" local _proxy_status=$(test_url "${probe_url}" ${retry_num} ${connect_timeout} "-x $curlx" 1) # Kill the SS plugin process - local pid_file="/tmp/etc/${CONFIG}/test_node_${node_id}_plugin.pid" + local pid_file="${TMP_PATH}/test_node_${node_id}_plugin.pid" [ -s "$pid_file" ] && kill -9 "$(head -n 1 "$pid_file")" >/dev/null 2>&1 busybox pgrep -af "test_node_${node_id}" | awk '! /socks_auto_switch\.sh/{print $1}' | xargs kill -9 >/dev/null 2>&1 - rm -rf /tmp/etc/${CONFIG}/test_node_${node_id}*.* + rm -rf ${TMP_PATH}/test_node_${node_id}*.* if [ "${_proxy_status}" -eq 200 ]; then return 0 fi diff --git a/luci-app-passwall2/root/usr/share/passwall2/test.sh b/luci-app-passwall2/root/usr/share/passwall2/test.sh index cd1084e9..844b42f1 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/test.sh +++ b/luci-app-passwall2/root/usr/share/passwall2/test.sh @@ -53,10 +53,10 @@ url_test_node() { local probeUrl=$(config_t_get global_other url_test_url https://www.google.com/generate_204) result=$(curl --connect-timeout 3 --max-time 5 -o /dev/null -I -skL -w "%{http_code}:%{time_pretransfer}" -x ${curlx} "${probeUrl}") # End the SS plugin process - local pid_file="/tmp/etc/${CONFIG}/url_test_${node_id}_plugin.pid" + local pid_file="${TMP_PATH}/url_test_${node_id}_plugin.pid" [ -s "$pid_file" ] && kill -9 "$(head -n 1 "$pid_file")" >/dev/null 2>&1 busybox pgrep -af "url_test_${node_id}" | awk '! /test\.sh/{print $1}' | xargs kill -9 >/dev/null 2>&1 - rm -rf /tmp/etc/${CONFIG}/*url_test_${node_id}*.* + rm -rf ${TMP_PATH}/*url_test_${node_id}*.* } echo $result } diff --git a/luci-theme-footstrap/Makefile b/luci-theme-footstrap/Makefile index 0de6bc01..233d4a5e 100644 --- a/luci-theme-footstrap/Makefile +++ b/luci-theme-footstrap/Makefile @@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap FOOTSTRAP_VERSION?= ifneq ($(FOOTSTRAP_VERSION),) PKG_VERSION:=$(FOOTSTRAP_VERSION) -PKG_RELEASE:=18 +PKG_RELEASE:=19 endif LUCI_TITLE:=Footstrap Theme diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js index e6d4cd62..8d08c795 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js @@ -252,6 +252,29 @@ function fitChrome() { * did NOT fit, .fs-bar-stack has already given it a row of its own. */ if (bar && (topBar || document.documentElement.hasAttribute('data-narrow'))) fitCluster(bar, menu); + + publishBarHeight(bar); +} + +/* ---- HOW TALL THE BAR ACTUALLY IS, for whoever has to stick underneath it ---- + * + * A data table's header row sticks while its rows scroll past (theme/30-tables.css). Where the + * DOCUMENT scrolls — the top layout, and the sidebar layout once it has become a bar — the sticky + * bar is what the header has to clear, and `--fs-bar-h` is only its DESIGNED height: the bar grows + * when the brand wraps, when the menu takes a row of its own (.fs-bar-stack) or when the cluster + * does (.fs-bar-actrow), and every one of those is decided a few lines above by measurement. + * So the measurement is published, and the CSS falls back to the token when it is missing. + * + * Written only on a CHANGE and rounded to the pixel: this runs on every fit pass, and a custom + * property write on :root invalidates style for the whole document. */ +let _barH = 0; +function publishBarHeight(bar) { + const root = document.documentElement; + if (!bar) return; + const h = Math.round(bar.getBoundingClientRect().height); + if (!(h > 0) || h === _barH) return; + _barH = h; + root.style.setProperty('--fs-bar-live', `${h}px`); } /* ---- does the right-hand cluster still fit beside the brand? ---- diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js index cf655821..b00a9f38 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js @@ -1,5 +1,6 @@ 'use strict'; 'require baseclass'; +'require ui'; /* fs-fit — the theme's ONE "does it still fit?" engine; add fit logic here, do not grow a second * observer. No CSS query can ask what the CONTENT needs (media = viewport, container = @@ -21,21 +22,7 @@ const _fitters = []; let _rafPending = false; -let _ro = null, _mo = null; -/* one canvas for the whole document: wordFloor() measures text with it, and creating one per call - * is the expensive half of that measurement. `null` = not asked yet, `false` = asked and refused — - * the same three-state slot fs-widgets.js's rasterCtx() keeps, and for the same reason: a browser - * with canvas turned off (an anti-fingerprinting extension, a WebView, memory pressure) answers - * null, and retrying that on every fit pass is a cost with no chance of a different answer. */ -let _cx = null; -function textCtx() { - if (_cx === null) { - try { _cx = document.createElement('canvas').getContext('2d') || false; } - catch (e) { _cx = false; } - } - return _cx; -} -const SPACE_RE = /\s+/; +let _ro = null, _mo = null, _moFlag = null; /* Run every fitter NOW, synchronously. A fitter must be idempotent — this fires on every * relevant mutation. */ @@ -69,12 +56,42 @@ function watch(el) { /* Rule 2's mutation side. Deliberately NOT filtered by node type: a filter is a second place to * get wrong (the table fitter's own once said `table.table`, and LuCI renders most of its tables * as DIVs — so the poll never re-measured at all), and run() is a handful of measurements. */ +/* THE CONTENT IS IN TWO PLACES, and watching one of them was a bug with a phone screenshot behind it. + * + * `ui.showModal` builds its dialog inside `#modal_overlay`, which ui's `__init__` appends to `` + * beside #view — so a dialog's content mutates NOTHING inside the observed host. A table opened in the + * wireless scan dialog was therefore measured only if some unrelated mutation in #view happened to + * run the fitters, and its rows (which that dialog re-renders once a second) were never re-measured + * at all. Both roots get the same observer and the same ResizeObserver. + * + * `require ui` above is what makes the overlay exist by the time this runs: it is created in ui's + * constructor, and luci-base instantiates a class exactly once, at the first require. */ function observeContent() { if (_mo) return; - const host = document.getElementById('view') || document.body; _mo = new MutationObserver(run); - _mo.observe(host, { childList: true, subtree: true }); - watch(host); + for (const host of [ document.getElementById('view') || document.body, document.getElementById('modal_overlay') ]) { + if (!host) continue; + _mo.observe(host, { childList: true, subtree: true }); + watch(host); + } + /* AND THE MOMENT THE DIALOG BECOMES VISIBLE, which no mutation inside it announces: `showModal` + * writes the content FIRST and adds `modal-overlay-active` to after, so the pass that the + * content mutation triggers still sees a closed dialog (fs-select.js skips one — a hidden overlay + * shrink-fits, so it would be measuring a width the dialog will never have). Nothing else changes + * afterwards, so without this the dialog's table would wait for its first poll to be fitted. One + * attribute, on one element. + * + * A SECOND OBSERVER, and it has to be one — this is a spec fact, not a preference. + * `MutationObserver.observe()` REPLACES the options of an existing registration for the same node, + * so calling it here on `document.body` would have silently dropped the `{childList, subtree}` + * registration above on any page where `#view` does not exist and `body` IS the content host: the + * fitters would then never run on a content mutation again, and nothing would say so. + * + * Merging the two into one call is the other wrong answer: `subtree: true` plus an attribute + * filter wakes `run()` on every class change anywhere in the document, and the poll rewrites + * classes on rows once a second. Two observers, one narrow node each, same callback. */ + _moFlag = new MutationObserver(run); + _moFlag.observe(document.body, { attributes: true, attributeFilter: [ 'class' ] }); } return baseclass.extend({ @@ -128,114 +145,18 @@ return baseclass.extend({ return p.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight); }, - /* Does `el` need more width than it has been given? */ + /* Does `el` need more width than it has been given? + * + * THE ONE QUESTION LEFT, and it is the browser's own answer. `wordFloor()` and `textLines()` + * used to stand beside it — a canvas reimplementation of min-content and a line counter — and + * both existed only because the stylesheet lowered every cell's min-content to one character, + * so a starved column produced no overflow for this to read. theme/30-tables.css gives a data + * table an honest floor for as long as it is a table, so the starvation cannot happen and the + * reconstructions are gone: 75 lines of approximation (whitespace-split words, one font per + * column, `iiii` ranked above `WWW`) that cost about 1 ms per pass on a 114-row table and, on + * `WPA2-PSK/CCMP`, claimed 144px where the engine's own floor is 93. */ overflows(el) { return el.scrollWidth > this.roomFor(el) + 1; /* +1: sub-pixel rounding */ - }, - - /* THE NARROWEST THIS TABLE CAN BE WITHOUT BREAKING A WORD THROUGH — its own breakpoint, in px. - * - * Every other "does it fit" question here is asked of the browser. This one cannot be: it is - * exactly the table's min-content width, and `overflow-wrap: anywhere` (base/40-tables.css) makes - * the browser answer ONE CHARACTER per column. That is deliberate — an unbreakable ICCID must not - * push a table nobody measures out of its card — but it means a measured table can be starved to - * a ribbon of fragments and still report, truthfully, that it fits. Asking the engine the honest - * question by flipping `overflow-wrap` for one layout does not work either: Blink returns the same - * min-content for `normal`, `break-word` and `anywhere` on a table (measured — 645px in all three, - * with the widest word alone needing 367), so the number simply is not available from layout. - * - * So compute it: per COLUMN, the width of the widest WORD any of its cells has to show, plus that - * cell's own side padding; summed across columns. A `nowrap`/`pre` column takes its whole text - * instead, because that is what it has committed to showing. Nothing here is a threshold — the - * result is the table's content measured in the table's own fonts, so every table gets its own - * number and none was picked by anyone. - * - * Two approximations, both stated rather than hidden: - * - the FONT is sampled once per column, from the first data row. A column that changes face - * row by row would be measured in the first row's; no LuCI table does that, and the - * alternative is a `getComputedStyle` per cell, which is the expensive call here. - * - the widest word is picked by CHARACTER COUNT and only that one is measured, so a column of - * one font is one `measureText` per new maximum instead of one per cell. Within a single font - * that ranks `iiii` above `WWW`; it costs a few px on the floor and takes the walk over - * Processes (114 rows) from 6ms to about 1ms. - * - * A `colSpan` cell contributes its whole floor to one column, which over-states that column and - * under-states its neighbours by the same amount — the sum, which is what the caller compares, is - * unaffected. */ - wordFloor(t) { - const rows = t.querySelectorAll('.tr:not(.table-titles):not(.cbi-section-table-titles):not(.placeholder)'); - if (!rows.length) return 0; - /* No 2D context, no floor to report — and 0 is the honest answer, not a failure: it says - * "this test has nothing to add", leaving the other two stack tests (does it overflow, is a - * one-token column shredded) to decide. Throwing here would cost far more than the test is - * worth: fs-select's fitTables() strips `fs-stacked` BEFORE it measures (rule 1), and this is - * the last term of that `||`, so the throw would escape the row walk with the class already - * gone — every stackable table left un-stacked, every later table in the pass unfitted, once - * a second on a polled page. */ - const cx = textCtx(); - if (!cx) return 0; - const floors = [], longest = []; - let cols = null; - for (const row of rows) { - const cells = row.children; - if (!cols) { - cols = []; - for (const c of cells) { - const s = getComputedStyle(c); - cols.push({ - font: `${s.fontStyle} ${s.fontWeight} ${s.fontSize} ${s.fontFamily}`, - tracking: parseFloat(s.letterSpacing) || 0, - pad: parseFloat(s.paddingLeft) + parseFloat(s.paddingRight), - whole: (s.whiteSpace === 'nowrap' || s.whiteSpace === 'pre') - }); - } - } - for (let i = 0; i < cells.length; i++) { - const text = (cells[i].textContent || '').trim(); - if (!text) continue; - const col = cols[i] || cols[cols.length - 1]; - let word = ''; - if (col.whole) word = text; - else for (const w of text.split(SPACE_RE)) if (w.length > word.length) word = w; - if (!(word.length > (longest[i] || 0))) continue; - longest[i] = word.length; - cx.font = col.font; - const need = cx.measureText(word).width + (col.tracking * word.length) + col.pad; - if (!(floors[i] >= need)) floors[i] = need; - } - } - let sum = 0; - for (const f of floors) sum += (f || 0); - return sum; - }, - - /* How many LINE BOXES of text does `el` render? The fact behind "this column has been - * squeezed into a tower" — a cell that has to break its own words is a column that has run - * out of width, which is a thing no viewport query can ask. - * - * NOT height / line-height: the assoclist's first cell is an `.ifacebadge`, a flex COLUMN - * with a 32px icon over the text, so a third of that height is not text at all. Ranges over - * the TEXT NODES only, so an cannot be counted as a line. - * - * Cluster by the rect's TOP, not by vertical overlap: consecutive lines OVERLAP. Measured on - * the router — tops 15-16px apart while each rect is 17-18px tall (the font's box is taller - * than the line advance), so an overlap test merged an 8-line tower into ONE line and the - * whole check silently never fired. Half a line of tolerance also merges what belongs on one - * line: a `` shares the baseline but sits a few px lower, and is not a new line. */ - textLines(el) { - const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT); - const range = document.createRange(); - const rects = []; - for (let n; (n = walker.nextNode());) { - if (!n.nodeValue.trim()) continue; - range.selectNodeContents(n); - for (const r of range.getClientRects()) - if (r.width > 0.5 && r.height > 0.5) rects.push(r); - } - rects.sort((a, b) => a.top - b.top); - let lines = 0, top = -Infinity; - for (const r of rects) - if (r.top - top > r.height * 0.5) { lines++; top = r.top; } - return lines; } + }); diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-select.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-select.js index 849ce1bf..eae87c8b 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-select.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-select.js @@ -170,7 +170,7 @@ function enhance(sel) { * `:has(.tr.table-titles)` the style engine re-evaluated on every mutation of these polled tables * (Processes/routes/leases). Not a .cbi-section-table — config forms keep their own layout. * - * `.table`, not `table.table` — the SAME selector relevant() and STACKABLE use. Stock LuCI + * `.table`, not `table.table` — the SAME selector relevant() and stackables() use. Stock LuCI * happens to emit only real s, but a third-party luci-app-* may emit a
* (coverage rule, docs/conventions.md), which a tag qualifier would pass over so it could never card. * @@ -198,8 +198,46 @@ function enhance(sel) { * Re-running is what the two functions below are already written for: both are additive, both skip * what is already done, and neither re-decides anything (adoptMarkup() settles the "is this ours to * rewrite?" question once, at claim time, and remembers the answer on the element). */ -const FOREIGN_TABLE = '#view .table:not(.cbi-section-table), ' + - '#view table:not(.table):not(.cbi-section-table)'; +/* ---- AND `#view` IS NOT THE ONLY PLACE A TABLE LANDS ---- + * + * `ui.showModal` (luci-base ui.js) parks its dialog on `#modal_overlay`, which ui's own `__init__` + * appends to `` — a SIBLING of #view, not a descendant. Every selector in this file said + * `#view`, so a table in a dialog was reached by none of it: never tagged `.fs-dt`, never + * captioned, never measured, never carded. The dialog that shows it is the wireless scan + * ("Join Network: Wireless Scan", luci-mod-network/wireless.js — a seven-column table built with + * `E('table', {class: 'table'})` and fed to `cbi_update_table`). + * + * Measured on the stand with that exact markup, at a 390px viewport: in #view the table cards + * (`.fs-stacked`, 358px wide, every value on its own labelled line); in the dialog it stayed a + * table 373px wide inside 317px of room — 56px past the dialog's edge, with the Encryption column + * given 10px and `mixed WPA/WPA2 PSK (CCMP…)` spelled one character per line. Reported from a + * phone, and the screenshot is exactly that tower. + * + * So the roots are listed once, here, and every table query in this file is built from them. The + * overlay is a scroll container (base/60-modal.css) and theme/70-modal.css already spends a block + * on keeping a wide child from scrolling the dialog off the screen — a table is the widest child + * there is, and it belongs to the same rule. + * + * ---- AND A CLOSED DIALOG IS NOT A CONTENT ROOT, THOUGH IT IS STILL IN THE DOM ---- + * + * `hideModal()` only drops a class off ``; the dialog KEEPS the last markup it was given until + * the next `showModal()` overwrites it, and `visibility: hidden` leaves a box behind. Measured after + * closing a dialog: the overlay shrink-fits to 270px (its `inset-inline: 0` comes with the open + * class), so the table inside reports 236px of room and stacks — a decision taken about a width the + * dialog will never have, on rows nobody can see, re-taken on every pass a polled page makes. So the + * dialog counts as a root only while it is open, which is the same fact `body.modal-overlay-active` + * already carries for the CSS. + * + * The flag flips AFTER the content is written (`showModal` appends, then adds the class), so a + * mutation-driven pass would run one moment too early and never look again — which is why + * `fs-fit.js` also watches that class, and why this is a question asked per PASS rather than a + * selector fixed at load. */ +const ROOTS = [ '#view', '#modal_overlay' ]; +const inRoots = (sel, roots) => roots.map((r) => `${r} ${sel}`).join(', '); +const liveRoots = () => (document.body.classList.contains('modal-overlay-active') ? ROOTS : ROOTS.slice(0, 1)); + +const foreignTables = () => inRoots('.table:not(.cbi-section-table)', liveRoots()) + ', ' + + inRoots('table:not(.table):not(.cbi-section-table)', liveRoots()); /* The FOURTH header markup, and the one only a foreign table produces: `
` for the parser to imply and none of LuCI's class names. It is the exact shape the phone @@ -217,7 +255,7 @@ function headerRow(t) { } function tagDataTables() { - document.querySelectorAll(FOREIGN_TABLE).forEach((t) => { + document.querySelectorAll(foreignTables()).forEach((t) => { /* FOUR header markups, and each missing one cost a page. L.ui.Table emits * `.tr.table-titles`; the apk Software page emits `.tr.cbi-section-table-titles` (missing * it is why the package list once needed a stacking block of its own); and a third-party @@ -270,7 +308,7 @@ function tagDataTables() { function adoptMarkup(t, head) { /* DECIDED ONCE, AT CLAIM TIME, and only for a table that speaks none of this vocabulary — then * READ on every pass, because the caller now revisits a table it has already claimed (see - * FOREIGN_TABLE). Asking the question afresh each pass instead would answer "already adopted" + * foreignTables()). Asking the question afresh each pass instead would answer "already adopted" * the moment we adopted it, and the fresh rows a poll brings in bare would never be taken. * Asking it at all is what keeps the theme's hands off LuCI's own markup: the apk Software list * heads its table with `.tr.cbi-section-table-titles`, and blindly adding `table-titles` to that @@ -382,99 +420,243 @@ function labelCells(t, head) { * table then reported needing 1747px where it really needs 1190px and overflowed its section by * 557px — an overflow the CSS-only version never had. A data table has no widgets, which is why * it is the one that gets measured. */ -const STACKABLE = '#view .table.fs-dt'; +const stackables = () => inRoots('.table.fs-dt', liveRoots()); -/* "Too cramped to be a table any more" — a DESIGN judgement. It has to be one: these tables do - * NOT overflow when the room runs out (their cells break anywhere), they compress into an - * unreadable ribbon. Do NOT give the cells a min-width so that "cramped" MANUFACTURES an - * overflow: tried, and it carded the firewall's zone table at 1420px and still overflowed by - * 39px once carded — a floor big enough to force the overflow is big enough to break the card. */ +/* "Too cramped to be a table any more" — a DESIGN judgement, and the only number in this file. + * + * It survives the honest floor below rather than being replaced by it: a four-column table of short + * values (Startup, Connections) still FITS at 380px of room, technically, as four ribbons of two + * words each. Nothing measures "unreadable", so somebody has to say it, and stock LuCI says 600px of + * viewport. Do NOT give the cells a min-width so that "cramped" MANUFACTURES an overflow: tried, and + * it carded the firewall's zone table at 1420px and still overflowed by 39px once carded — a floor + * big enough to force the overflow is big enough to break the card. */ const CRAMPED = 568; /* stock LuCI cards its tables at a 600px viewport; below the 767px tier * .fs-content pads var(--fs-space-4) a side, 16px at the default density, * so 600 -> 568 of room. A fixed number and not a re-read of that token on * purpose: the threshold is the DESIGN judgement above, and Compact density * shrinking the gutter to 10px is not a reason to keep a table wider. */ -/* The ribbon has one more shape, and CRAMPED cannot see it: the table has room by the number - * above and still shreds its FIRST column, because auto table layout hands width out by what - * each column DEMANDS. The leftmost column is the row's identity and usually the least greedy — - * a wide neighbour (a hostname plus an IPv6, a modulation string) simply takes the width, and - * `overflow-wrap: anywhere` (theme/30-tables.css) lets the identity be squeezed with no floor: - * it breaks mid-word rather than overflow, so there is no overflow for fit.overflows() to read. +/* ---- THE REMEDY LADDER: cheapest first, re-measured at every rung ---- * - * Measured on the router (Wireless, one station, `Access Point "vaka_devices" (phy6-ap0)`): - * viewport 900 -> the column is 101px and 5 lines, 850 -> 80px and 7, 800 -> 76px and 8, and at - * NO width did the table card — a nine-line tower of half-words next to columns with room to - * spare (issue #7). Below 767 the MAC column drops out (the stock phone contract) and the - * column springs back to 167px, which is why this only ever bit between roughly 780 and 900. + * There used to be four terms in one `||`: `room < CRAMPED`, `overflows()`, `idTower()` (is the first + * column a tower of half-words?) and `wordFloor() > room` (does the table need more width than its + * own content can be squeezed into?). Three of the four existed because the CSS had told the engine + * that any value may break anywhere, which lowers a column's min-content to ONE CHARACTER: with no + * floor, a column could be starved instead of the table overflowing, and `overflows()` — the one + * question the browser answers exactly — went blind. `wordFloor()` reconstructed the floor with a + * canvas and a whitespace split, which is UAX #14 done by hand and wrong in both directions + * (`WPA2-PSK/CCMP` measured 144px against a real 93px, so tables carded that had room). * - * So: past this many lines the identity has stopped being readable and the card view — which - * gives every field its own labelled row — is simply better. A DESIGN judgement like CRAMPED, - * and it has to be one: any number of lines is legible in isolation. 5 is what the reporter - * asked for and what the measurements above bracket. Only the first column, and deliberately: - * a value column wrapping to a few lines is a value being shown, not a table falling apart. */ -const MAX_ID_LINES = 5; + * theme/30-tables.css now gives a data table `overflow-wrap: break-word` for as long as it is a + * table, so the floor is real again and the overflow is honest. That leaves ONE question — does it + * fit? — and a ladder of answers when it does not, each one re-measured: + * + * 1. it fits -> a table, and nothing was written + * 2. drop the columns the VIEW marked droppable (`hide-xs`/`hide-sm`) and ask again + * 3. let the widest breakable column shred, and ask again + * 4. card it + * + * Rungs 2 and 3 need no threshold, and that is the point: the guard is the second measurement. On a + * wide desktop where one base64 key or one process command line is the entire problem, breaking that + * column makes the table fit and it stays a table; where every column is over its share, breaking one + * changes nothing and the card is right. Nobody picks a fraction. + * + * Rung 2 is upstream's own priority hint, honoured by measurement instead of by viewport: wireless.js, + * connections.js and channel_analysis.js mark their least valuable columns `hide-xs`, `ui.Table` + * copies the class from the header cell onto every body cell (ui.js), and until now the theme only + * obeyed it below 767px or in a card. */ -/* Is any row's leftmost cell a tower? Text lines, not height — fs-fit.textLines() explains why. - * The height gate in front of it is not premature: this runs on every poll tick (once a second, - * every mutation), and Processes/Connections render hundreds of rows whose first cell is a PID - * that cannot be a tower — one cheap read each keeps the Range walk for the cells that could. */ -function idTower(t) { - const cells = t.querySelectorAll('.tr > .td:first-child'); - if (!cells.length) return false; - const cs = getComputedStyle(cells[0]); - const lh = parseFloat(cs.lineHeight) || parseFloat(cs.fontSize) * 1.2 || 16; - for (const cell of cells) { - if (cell.clientHeight < MAX_ID_LINES * lh) continue; - if (fit.textLines(cell) > MAX_ID_LINES) return true; +/* Which cells of a table carry the "may shred" mark, as a column index (-1 = none). Kept on the + * element so the common case — the same column as last pass — writes nothing at all. */ +function markBreakColumn(t, rows, col) { + if (t._fsBreakCol === col) return; + t._fsBreakCol = col; + for (const row of rows) { + const cells = row.children; + for (let i = 0; i < cells.length; i++) + cells[i].classList.toggle('fs-td-break', i === col); } +} + +/* Rung 3. The widest column that CAN be shredded, which is never the first (it is the row's + * identity — issue #36 is precisely that column being starved) and never a `nowrap`/`pre` one + * (`overflow-wrap` is inert there, so marking it would buy a layout and no width). + * + * With honest floors nothing is starved, so when the table overflows every column sits at its own + * min-content and the widest one IS the column holding the longest unbreakable token. No canvas, no + * text measurement — one `getComputedStyle` and one rect per column of one row. */ +function breakWidestColumn(t) { + const rows = t.querySelectorAll('.tr:not(.table-titles):not(.cbi-section-table-titles):not(.placeholder)'); + if (!rows.length) return false; + const cells = rows[0].children; + let col = -1, widest = 0; + for (let i = 1; i < cells.length; i++) { + const ws = getComputedStyle(cells[i]).whiteSpace; + if (ws === 'nowrap' || ws === 'pre') continue; + const w = cells[i].getBoundingClientRect().width; + if (w > widest) { widest = w; col = i; } + } + if (col < 0) return false; + markBreakColumn(t, rows, col); + if (!fit.overflows(t)) return true; + markBreakColumn(t, rows, -1); return false; } -/* ---- AND THE SAME RIBBON IN A COLUMN THAT IS NOT THE FIRST ---- - * - * There is no second test for it, and that is the fix rather than an omission. - * - * `idTower` above is deliberately first-column-only, on the grounds that "a value column wrapping to - * a few lines is a value being shown, not a table falling apart". That held right up until the value - * was one unbreakable token: `overflow-wrap: anywhere` gave such a cell a min-content of ONE - * CHARACTER, so auto table layout was free to starve its column to one character, and `overflows()` - * then reported — truthfully, uselessly — that the table fit. Reported from a hardware router at - * 700-790px of window: the v4 lease table cards there (its `nowrap` columns give it a floor, so it - * really does overflow) while the v6 table beside it shredded the DUID, measured at 5 lines with - * 674px of room and 7 at 654px, against 1 line at 1160px. - * - * A first pass answered it with a line count, which is a number somebody picks. This asks the table - * instead: `fit.wordFloor()` returns the narrowest the table can be without breaking a word through — - * per column, the widest WORD it must show, in that column's own font, summed. Past that width the - * browser has to cut through a value, and the card view is what shows values whole. So every table - * carries its own breakpoint, derived from its own content, and no threshold was chosen anywhere. - * - * On the reporting router, at 1190px of room: leases6 asks for 935, the associated-stations table - * for 966, the v4 leases for 645, Processes for 794, Connections for 550 and Startup for 381 — so - * the two that were unreadable card at roughly a 1000px window, and the four that were fine keep - * being tables until the room they actually need runs out. */ - function fitTables() { - document.querySelectorAll(STACKABLE).forEach((t) => { + document.querySelectorAll(stackables()).forEach((t) => { const was = t.classList.contains('fs-stacked'); + const wasDrop = t.classList.contains('fs-drop-xs'); /* fs-fit rule 1: a stacked table is a pile of flex rows and always "fits", so reading - * it as it stands un-stacks it and the next frame stacks it again — oscillation. */ - t.classList.remove('fs-stacked'); + * it as it stands un-stacks it and the next frame stacks it again — oscillation. Every + * mark the ladder can write comes off here, for the same reason. */ + t.classList.remove('fs-stacked', 'fs-drop-xs'); + if (t._fsBreakCol !== undefined && t._fsBreakCol !== -1) + markBreakColumn(t, t.querySelectorAll('.tr'), -1); + const room = fit.roomFor(t); - if (!(room > 0)) { if (was) t.classList.add('fs-stacked'); return; } + if (!(room > 0)) { + /* detached, hidden, or a dialog that is closed: keep what it had rather than decide + * against a width it does not have */ + if (was) t.classList.add('fs-stacked'); + if (wasDrop) t.classList.add('fs-drop-xs'); + return; + } + + /* the one judgement a measurement cannot make, and the only number in this file */ + let stack = room < CRAMPED; + if (!stack && fit.overflows(t)) { + /* rung 2 — the view author already said which columns are expendable */ + if (t.querySelector('.hide-xs, .hide-sm')) t.classList.add('fs-drop-xs'); + /* rung 3 — one column may shred rather than the whole table becoming a stack of cards */ + if (fit.overflows(t) && !breakWidestColumn(t)) stack = true; + } - /* the two row walks last, cheapest first: idTower reads one column, wordFloor every cell */ - const stack = room < CRAMPED || fit.overflows(t) || idTower(t) || fit.wordFloor(t) > room; /* write only on a real change: the poll re-renders these tables once a second, and * toggling the class off and on each tick would invalidate style for every row of * Processes/Leases for nothing */ - if (stack) t.classList.add('fs-stacked'); + if (stack) { + t.classList.add('fs-stacked'); + /* a card gives every value the whole row, so neither remedy has anything left to do */ + t.classList.remove('fs-drop-xs'); + if (t._fsBreakCol !== undefined && t._fsBreakCol !== -1) + markBreakColumn(t, t.querySelectorAll('.tr'), -1); + } else if (was) t.classList.remove('fs-stacked'); }); } +/* ---- AND THE TABLE THAT CANNOT CARD: it scrolls, and it says so ---- + * + * A table with no header row has no captions to print, so a card would give a column of values with + * nothing saying what they are — a log, a statistics matrix, a key/value include. Comparison across + * rows IS that shape's purpose, which is the one case WCAG 2.2 names in the exception to SC 1.4.10 + * ("data tables and grids… it is acceptable to provide two-dimensional scrolling for such parts"). + * theme/30-tables.css already scrolls a BARE `
…`, with no * `
` of that shape; this reaches the one it cannot + * name — an app that renders the same thing as `
` — and it reaches it by + * measurement rather than by selector, which is what makes the widget refusal below possible. + * + * A SCROLLING TABLE CANNOT HOLD A POPUP. `overflow-x: auto` computes `overflow-y` to `auto` too + * (css-overflow-3 §3.1: there is no `auto`/`visible` pair), so it CLIPS every absolutely positioned + * thing inside it, and luci-base's `openDropdown()` additionally sizes an open list against the + * nearest scroll parent — which would now be the table. + * + * SO THE ONES THAT HOLD CONTROLS STACK INSTEAD, and refusing to do anything for them was a real + * defect rather than caution: Network → Diagnostics is three controls in one header-less row, and at + * 320px of room it needs 338 — measured, the row simply ran past `.fs-main`'s clip and the Ping + * button was unreachable. Nothing about that shape wants scrolling either: its rows are a FORM, not + * data, so there is no comparison across rows to preserve. Rows and cells become blocks, each + * control takes the width it was given, the table cannot overflow anything, and no popup is clipped + * because nothing became a scroll container. + * + * That is also what `theme/90-responsive.css` had been doing for this one page since the phone + * sweep, keyed by `body[data-page="admin-network-diagnostics"]` and only below 767px. The shape is + * not that page's — any app can put a select and a button in a `
` — and the width + * that matters is the room, not the viewport, so the page rule is gone and the measurement covers + * every one of them. */ +const scrollables = () => inRoots('.table:not(.fs-dt):not(.cbi-section-table)', liveRoots()); +const HOLDS_CONTROLS = '.cbi-dropdown, .cbi-dynlist, .cbi-tooltip-container, .cbi-progressbar, select, input, textarea, [data-tooltip]'; + +/* ---- MAKING A SCROLL BOX REACHABLE, AND HANDING THE MARKUP BACK EXACTLY AS IT WAS FOUND ---- + * + * Firefox has made scrollers focusable for years and Chrome since 132, but only when they hold no + * focusable child — a table row full of buttons disqualifies itself — and WebKit has not shipped it + * at all (bug 190870, open since 2018). So say it in the markup: a tab stop (SC 2.1.1), and a name so + * what receives focus can be announced (SC 4.1.2). + * + * A ROLE IS WRITTEN ONLY WHERE THERE IS NONE TO LOSE. `
` has no implicit role and + * takes `group`. A real `
` must keep the role it already has: HTML-AAM maps `
` to `cell` + * and `` to `columnheader`/`rowheader` ONLY while the table element's role is `table` (or + * `grid`/`treegrid`), so overwriting it with `group` drops every cell to generic — a screen reader + * would then read a flat run of text with no rows and no columns, which is the very structure the + * SC 1.4.10 exception exists to preserve. `aria-label` works on either, so the name and the tab stop + * are unaffected. Reported in review. + * + * WHAT IS REMOVED IS WHAT WAS WRITTEN, remembered per element rather than inferred from the value: + * an app that had its own `tabindex="0"` or `role="group"` on that table got them taken away when the + * table later fitted, because "is it 0 / is it group" cannot tell whose it is. */ +function reach(t) { + if (!t.hasAttribute('tabindex')) { t.tabIndex = 0; t._fsTab = true; } + if (!t.hasAttribute('role') && !(t instanceof HTMLTableElement)) { + t.setAttribute('role', 'group'); + t._fsRole = true; + } + if (!t.hasAttribute('aria-label') && !t.hasAttribute('aria-labelledby')) { + const head = t.closest('.cbi-section, fieldset, #view')?.querySelector('h2, h3, h4, legend'); + t.setAttribute('aria-label', (head && head.textContent.trim()) || _('Table')); + t._fsNamed = true; + } +} + +function unreach(t) { + if (t._fsTab) { t.removeAttribute('tabindex'); delete t._fsTab; } + if (t._fsRole) { t.removeAttribute('role'); delete t._fsRole; } + if (t._fsNamed) { t.removeAttribute('aria-label'); delete t._fsNamed; } +} + +function fitScrollables() { + document.querySelectorAll(scrollables()).forEach((t) => { + const was = t.classList.contains('fs-xscroll'); + const wasStack = t.classList.contains('fs-rowstack'); + /* rule 1 again: a scrolling box always "fits" — its overflow is inside it — and so does a + * stacked one, whose rows are blocks */ + t.classList.remove('fs-xscroll', 'fs-rowstack'); + if (!(fit.roomFor(t) > 0)) { + if (was) t.classList.add('fs-xscroll'); + if (wasStack) t.classList.add('fs-rowstack'); + return; + } + const over = fit.overflows(t); + const controls = over && !!t.querySelector(HOLDS_CONTROLS); + /* a table of CONTROLS stacks; a table of VALUES scrolls and keeps its shape */ + if (controls) { + t.classList.add('fs-rowstack'); + if (was) unreach(t); + return; + } + const scroll = over; + if (scroll === was) { if (was) t.classList.add('fs-xscroll'); return; } + if (scroll) { + t.classList.add('fs-xscroll'); + /* A SCROLL BOX THE KEYBOARD CANNOT REACH IS CONTENT THE KEYBOARD CANNOT READ. + * + * Firefox has made scrollers focusable for years and Chrome since 132, but only when they + * hold no focusable child — a table row full of buttons disqualifies itself — and WebKit + * has not shipped it at all (bug 190870, open since 2018). So say it in the markup: a + * tab stop (SC 2.1.1), a role and a name so what receives focus can be announced + * (SC 4.1.2). `group`, not `region`: a region is a landmark, and a status page with four + * scrolling tables would put four of them in the landmark list. + * + * The name comes from the heading the table sits under, which is what a sighted user + * reads it as; only the fallback is ours to translate. Never overwritten: an app that + * labelled its own table knows better. */ + reach(t); + } + else if (was) unreach(t); + }); +} + /* ---- A PINNED ACTIONS COLUMN IS ONLY VALID FOR THE LAYOUT MODE IT WAS MEASURED IN ---- * * luci-base's `form.js` (stabilizeActionColumnWidth) measures the widest @@ -511,7 +693,7 @@ function fitTables() { * into — so wiping the pin cannot change the key and set this oscillating. It fires once per CHANGE, * never per tick, so it does not fight upstream for the pin on a polled page. */ function unpinActionColumn() { - for (const t of document.querySelectorAll('#view .table.cbi-section-table')) { + for (const t of document.querySelectorAll(inRoots('.table.cbi-section-table', liveRoots()))) { if (!t.querySelector('.cbi-section-actions')) continue; /* ---- and CLAIM upstream's resize hook, because under SPA navigation it is a leak ---- * @@ -678,7 +860,7 @@ return baseclass.extend({ /* A table must be TAGGED .fs-dt before it can be fitted, and re-tagged whenever the poll * brings a fresh one back — so the two travel as one fitter, which fs-fit runs now, on * every content mutation (synchronously, pre-paint) and on every resize of #view. */ - fit.add(() => { tagDataTables(); fitTables(); unpinActionColumn(); resyncValues(); }); + fit.add(() => { tagDataTables(); fitTables(); fitScrollables(); unpinActionColumn(); resyncValues(); }); /* one scan per frame, however many mutations arrive (fit.frame — the theme's shared * coalescer) */ diff --git a/luci-theme-footstrap/po/es/footstrap.po b/luci-theme-footstrap/po/es/footstrap.po index d2b55011..637a20ab 100644 --- a/luci-theme-footstrap/po/es/footstrap.po +++ b/luci-theme-footstrap/po/es/footstrap.po @@ -70,8 +70,8 @@ msgctxt "footstrap" msgid "Clearly visible %s" msgstr "Bien visible %s" -#: htdocs/luci-static/resources/fs-chrome.js:372 -#: ucode/template/themes/footstrap/header.ut:144 +#: htdocs/luci-static/resources/fs-chrome.js:407 +#: ucode/template/themes/footstrap/header.ut:150 msgid "Collapse menu" msgstr "Contraer menú" @@ -144,7 +144,7 @@ msgctxt "footstrap" msgid "Easy to read %s" msgstr "Se lee bien %s" -#: htdocs/luci-static/resources/fs-chrome.js:372 +#: htdocs/luci-static/resources/fs-chrome.js:407 msgid "Expand menu" msgstr "Expandir menú" @@ -213,7 +213,7 @@ msgctxt "footstrap" msgid "Light" msgstr "Claro" -#: htdocs/luci-static/resources/fs-router.js:501 +#: htdocs/luci-static/resources/fs-router.js:651 msgid "Loading view…" msgstr "Cargando vista…" @@ -234,8 +234,8 @@ msgstr "Modo de compatibilidad Lua activo" msgid "Memory" msgstr "Memoria" -#: ucode/template/themes/footstrap/header.ut:141 -#: ucode/template/themes/footstrap/header.ut:152 +#: ucode/template/themes/footstrap/header.ut:147 +#: ucode/template/themes/footstrap/header.ut:158 msgid "Menu" msgstr "Menú" @@ -248,7 +248,7 @@ msgstr "" "reiniciar el sistema. Este modo solo debe usarse para instalar una " "actualización de firmware" -#: htdocs/luci-static/resources/fs-search.js:320 +#: htdocs/luci-static/resources/fs-search.js:327 msgctxt "footstrap" msgid "No pages found" msgstr "No se encontraron páginas" @@ -267,7 +267,7 @@ msgctxt "footstrap" msgid "Off" msgstr "Apagado" -#: htdocs/luci-static/resources/fs-search.js:237 +#: htdocs/luci-static/resources/fs-search.js:244 msgctxt "footstrap" msgid "Pages" msgstr "Páginas" @@ -303,7 +303,7 @@ msgstr "Por favor, seleccione un archivo de imagen." msgid "Please enter your username and password." msgstr "Por favor, ingrese su nombre de usuario y contraseña." -#: htdocs/luci-static/resources/fs-search.js:319 +#: htdocs/luci-static/resources/fs-search.js:326 msgctxt "footstrap" msgid "Recently visited" msgstr "Visitado recientemente" @@ -358,13 +358,13 @@ msgstr "Escala" msgid "Search" msgstr "Buscar" -#: htdocs/luci-static/resources/fs-search.js:231 -#: htdocs/luci-static/resources/fs-search.js:258 +#: htdocs/luci-static/resources/fs-search.js:238 +#: htdocs/luci-static/resources/fs-search.js:265 msgctxt "footstrap" msgid "Search pages" msgstr "Buscar páginas" -#: htdocs/luci-static/resources/fs-search.js:232 +#: htdocs/luci-static/resources/fs-search.js:239 msgctxt "footstrap" msgid "Search pages…" msgstr "Buscar páginas…" @@ -379,11 +379,11 @@ msgctxt "footstrap" msgid "Sidebar and bar" msgstr "Barra lateral y superior" -#: ucode/template/themes/footstrap/header.ut:120 +#: ucode/template/themes/footstrap/header.ut:126 msgid "Skip to content" msgstr "Saltar al contenido" -#: htdocs/luci-static/resources/fs-search.js:320 +#: htdocs/luci-static/resources/fs-search.js:327 msgctxt "footstrap" msgid "Start typing to find a page" msgstr "Empiece a escribir para encontrar una página" @@ -415,6 +415,10 @@ msgstr "Sistema" msgid "System running in recovery (initramfs) mode." msgstr "El sistema se está ejecutando en modo de recuperación (initramfs)." +#: htdocs/luci-static/resources/fs-select.js:587 +msgid "Table" +msgstr "Tabla" + #: htdocs/luci-static/resources/fs-prefs.js:893 msgctxt "footstrap" msgid "" @@ -470,8 +474,14 @@ msgstr "" #: htdocs/luci-static/resources/fs-appearance.js:504 msgctxt "footstrap" -msgid "This browser is not storing preferences (site data is blocked), so a change here lasts until you reload. Saving as default still works and applies to every browser." -msgstr "Este navegador no guarda preferencias (los datos del sitio están bloqueados), así que un cambio aquí dura hasta que recargues. Guardar como predeterminado sigue funcionando y se aplica a todos los navegadores." +msgid "" +"This browser is not storing preferences (site data is blocked), so a change " +"here lasts until you reload. Saving as default still works and applies to " +"every browser." +msgstr "" +"Este navegador no guarda preferencias (los datos del sitio están " +"bloqueados), así que un cambio aquí dura hasta que recargues. Guardar como " +"predeterminado sigue funcionando y se aplica a todos los navegadores." #: htdocs/luci-static/resources/fs-appearance.js:214 msgctxt "footstrap" @@ -555,7 +565,8 @@ msgstr "en el lienzo" #: htdocs/luci-static/resources/fs-prefs.js:1121 msgctxt "footstrap" -msgid "the uploaded file could not be removed either; it is still on the router." +msgid "" +"the uploaded file could not be removed either; it is still on the router." msgstr "tampoco se pudo eliminar el archivo subido; sigue en el router." #~ msgid "Logout" diff --git a/luci-theme-footstrap/po/ru/footstrap.po b/luci-theme-footstrap/po/ru/footstrap.po index 34dd1cb6..baa1ab8b 100644 --- a/luci-theme-footstrap/po/ru/footstrap.po +++ b/luci-theme-footstrap/po/ru/footstrap.po @@ -66,8 +66,8 @@ msgctxt "footstrap" msgid "Clearly visible %s" msgstr "Хорошо видна %s" -#: htdocs/luci-static/resources/fs-chrome.js:372 -#: ucode/template/themes/footstrap/header.ut:144 +#: htdocs/luci-static/resources/fs-chrome.js:407 +#: ucode/template/themes/footstrap/header.ut:150 msgid "Collapse menu" msgstr "Свернуть меню" @@ -138,7 +138,7 @@ msgctxt "footstrap" msgid "Easy to read %s" msgstr "Хорошо читается %s" -#: htdocs/luci-static/resources/fs-chrome.js:372 +#: htdocs/luci-static/resources/fs-chrome.js:407 msgid "Expand menu" msgstr "Развернуть меню" @@ -207,7 +207,7 @@ msgctxt "footstrap" msgid "Light" msgstr "Светлая" -#: htdocs/luci-static/resources/fs-router.js:501 +#: htdocs/luci-static/resources/fs-router.js:651 msgid "Loading view…" msgstr "Загрузка представления…" @@ -228,8 +228,8 @@ msgstr "Включён режим совместимости с Lua" msgid "Memory" msgstr "Память" -#: ucode/template/themes/footstrap/header.ut:141 -#: ucode/template/themes/footstrap/header.ut:152 +#: ucode/template/themes/footstrap/header.ut:147 +#: ucode/template/themes/footstrap/header.ut:158 msgid "Menu" msgstr "Меню" @@ -241,7 +241,7 @@ msgstr "" "Изменения настроек не сохраняются и будут потеряны после перезагрузки. Этот " "режим следует использовать только для установки обновления прошивки" -#: htdocs/luci-static/resources/fs-search.js:320 +#: htdocs/luci-static/resources/fs-search.js:327 msgctxt "footstrap" msgid "No pages found" msgstr "Страницы не найдены" @@ -260,7 +260,7 @@ msgctxt "footstrap" msgid "Off" msgstr "Выкл." -#: htdocs/luci-static/resources/fs-search.js:237 +#: htdocs/luci-static/resources/fs-search.js:244 msgctxt "footstrap" msgid "Pages" msgstr "Страницы" @@ -296,7 +296,7 @@ msgstr "Выберите файл изображения." msgid "Please enter your username and password." msgstr "Введите имя пользователя и пароль." -#: htdocs/luci-static/resources/fs-search.js:319 +#: htdocs/luci-static/resources/fs-search.js:326 msgctxt "footstrap" msgid "Recently visited" msgstr "Недавние" @@ -351,13 +351,13 @@ msgstr "Масштаб" msgid "Search" msgstr "Поиск" -#: htdocs/luci-static/resources/fs-search.js:231 -#: htdocs/luci-static/resources/fs-search.js:258 +#: htdocs/luci-static/resources/fs-search.js:238 +#: htdocs/luci-static/resources/fs-search.js:265 msgctxt "footstrap" msgid "Search pages" msgstr "Поиск по страницам" -#: htdocs/luci-static/resources/fs-search.js:232 +#: htdocs/luci-static/resources/fs-search.js:239 msgctxt "footstrap" msgid "Search pages…" msgstr "Поиск по страницам…" @@ -372,11 +372,11 @@ msgctxt "footstrap" msgid "Sidebar and bar" msgstr "Боковая панель и шапка" -#: ucode/template/themes/footstrap/header.ut:120 +#: ucode/template/themes/footstrap/header.ut:126 msgid "Skip to content" msgstr "Перейти к содержимому" -#: htdocs/luci-static/resources/fs-search.js:320 +#: htdocs/luci-static/resources/fs-search.js:327 msgctxt "footstrap" msgid "Start typing to find a page" msgstr "Начните вводить название страницы" @@ -408,6 +408,10 @@ msgstr "Система" msgid "System running in recovery (initramfs) mode." msgstr "Система работает в режиме восстановления (initramfs)." +#: htdocs/luci-static/resources/fs-select.js:587 +msgid "Table" +msgstr "Таблица" + #: htdocs/luci-static/resources/fs-prefs.js:893 msgctxt "footstrap" msgid "" @@ -463,8 +467,14 @@ msgstr "" #: htdocs/luci-static/resources/fs-appearance.js:504 msgctxt "footstrap" -msgid "This browser is not storing preferences (site data is blocked), so a change here lasts until you reload. Saving as default still works and applies to every browser." -msgstr "Этот браузер не хранит настройки (данные сайта заблокированы), поэтому изменение здесь действует до перезагрузки страницы. «Сохранить как умолчание» по-прежнему работает и применяется ко всем браузерам." +msgid "" +"This browser is not storing preferences (site data is blocked), so a change " +"here lasts until you reload. Saving as default still works and applies to " +"every browser." +msgstr "" +"Этот браузер не хранит настройки (данные сайта заблокированы), поэтому " +"изменение здесь действует до перезагрузки страницы. «Сохранить как " +"умолчание» по-прежнему работает и применяется ко всем браузерам." #: htdocs/luci-static/resources/fs-appearance.js:214 msgctxt "footstrap" @@ -548,7 +558,8 @@ msgstr "на холсте" #: htdocs/luci-static/resources/fs-prefs.js:1121 msgctxt "footstrap" -msgid "the uploaded file could not be removed either; it is still on the router." +msgid "" +"the uploaded file could not be removed either; it is still on the router." msgstr "загруженный файл тоже не удалось удалить — он остался на роутере." #~ msgid "Logout" diff --git a/luci-theme-footstrap/po/templates/footstrap.pot b/luci-theme-footstrap/po/templates/footstrap.pot index 854a0297..56c809b6 100644 --- a/luci-theme-footstrap/po/templates/footstrap.pot +++ b/luci-theme-footstrap/po/templates/footstrap.pot @@ -60,8 +60,8 @@ msgctxt "footstrap" msgid "Clearly visible %s" msgstr "" -#: htdocs/luci-static/resources/fs-chrome.js:372 -#: ucode/template/themes/footstrap/header.ut:144 +#: htdocs/luci-static/resources/fs-chrome.js:407 +#: ucode/template/themes/footstrap/header.ut:150 msgid "Collapse menu" msgstr "" @@ -132,7 +132,7 @@ msgctxt "footstrap" msgid "Easy to read %s" msgstr "" -#: htdocs/luci-static/resources/fs-chrome.js:372 +#: htdocs/luci-static/resources/fs-chrome.js:407 msgid "Expand menu" msgstr "" @@ -199,7 +199,7 @@ msgctxt "footstrap" msgid "Light" msgstr "" -#: htdocs/luci-static/resources/fs-router.js:501 +#: htdocs/luci-static/resources/fs-router.js:651 msgid "Loading view…" msgstr "" @@ -220,8 +220,8 @@ msgstr "" msgid "Memory" msgstr "" -#: ucode/template/themes/footstrap/header.ut:141 -#: ucode/template/themes/footstrap/header.ut:152 +#: ucode/template/themes/footstrap/header.ut:147 +#: ucode/template/themes/footstrap/header.ut:158 msgid "Menu" msgstr "" @@ -231,7 +231,7 @@ msgid "" "mode should only be used to install a firmware upgrade" msgstr "" -#: htdocs/luci-static/resources/fs-search.js:320 +#: htdocs/luci-static/resources/fs-search.js:327 msgctxt "footstrap" msgid "No pages found" msgstr "" @@ -250,7 +250,7 @@ msgctxt "footstrap" msgid "Off" msgstr "" -#: htdocs/luci-static/resources/fs-search.js:237 +#: htdocs/luci-static/resources/fs-search.js:244 msgctxt "footstrap" msgid "Pages" msgstr "" @@ -286,7 +286,7 @@ msgstr "" msgid "Please enter your username and password." msgstr "" -#: htdocs/luci-static/resources/fs-search.js:319 +#: htdocs/luci-static/resources/fs-search.js:326 msgctxt "footstrap" msgid "Recently visited" msgstr "" @@ -341,13 +341,13 @@ msgstr "" msgid "Search" msgstr "" -#: htdocs/luci-static/resources/fs-search.js:231 -#: htdocs/luci-static/resources/fs-search.js:258 +#: htdocs/luci-static/resources/fs-search.js:238 +#: htdocs/luci-static/resources/fs-search.js:265 msgctxt "footstrap" msgid "Search pages" msgstr "" -#: htdocs/luci-static/resources/fs-search.js:232 +#: htdocs/luci-static/resources/fs-search.js:239 msgctxt "footstrap" msgid "Search pages…" msgstr "" @@ -362,11 +362,11 @@ msgctxt "footstrap" msgid "Sidebar and bar" msgstr "" -#: ucode/template/themes/footstrap/header.ut:120 +#: ucode/template/themes/footstrap/header.ut:126 msgid "Skip to content" msgstr "" -#: htdocs/luci-static/resources/fs-search.js:320 +#: htdocs/luci-static/resources/fs-search.js:327 msgctxt "footstrap" msgid "Start typing to find a page" msgstr "" @@ -398,6 +398,10 @@ msgstr "" msgid "System running in recovery (initramfs) mode." msgstr "" +#: htdocs/luci-static/resources/fs-select.js:587 +msgid "Table" +msgstr "" + #: htdocs/luci-static/resources/fs-prefs.js:893 msgctxt "footstrap" msgid "" diff --git a/luci-theme-footstrap/styles/base/40-tables.css b/luci-theme-footstrap/styles/base/40-tables.css index 3e16d775..0bd0e4f2 100644 --- a/luci-theme-footstrap/styles/base/40-tables.css +++ b/luci-theme-footstrap/styles/base/40-tables.css @@ -63,6 +63,14 @@ * token and the column stays open — tried, and the table stayed 355px wide in a 346px * column. The narrower `anywhere` rules in theme/ keep winning where they apply. * + * THAT IS A CONTAINMENT DEFAULT, and it is right for every tier that has nowhere to put an + * overflow: a key/value include inside a card, the config table (which is never measured), the + * Overview meter rows, the realtime legend. It is exactly wrong for the one tier that IS + * measured — a data table can card, break one column or scroll, so it wants the truth instead, + * and `theme/30-tables.css` gives it `break-word` for as long as it is still a table. The split + * is the whole design: honest floors where a measurement reads them, containment everywhere + * else. Do not "unify" these two by moving either one. + * * SET PER ROLE, and that is the whole of the second rule: a VALUE may break anywhere, a LABEL * breaks at words. `.th` and `.td` never match the same element, so neither line depends on the * other's source order, and the pair rule above keeps only what both cells share. diff --git a/luci-theme-footstrap/styles/base/95-luci.css b/luci-theme-footstrap/styles/base/95-luci.css index 7ec9911a..b5d2e577 100644 --- a/luci-theme-footstrap/styles/base/95-luci.css +++ b/luci-theme-footstrap/styles/base/95-luci.css @@ -563,7 +563,12 @@ } .cbi-filebrowser { - min-width: 210px; + /* `min(210px, 100%)`, not a bare 210: the floor keeps the file list readable, and a floor + * wider than the field it sits in is not a floor but an overflow — measured at a 300px + * viewport, the panel held 210px inside a 203px field and its whole upload row (the input, + * Upload file, Cancel) stood 7px outside the card. Same shape as the dynlist's floor one file + * over, and the same fix. Nothing moves at any width where the field is wider than 210. */ + min-width: min(210px, 100%); max-width: 100%; /* width/style only; radius: theme/60-inputs.css — absorbed */ border: 1px solid var(--fs-border); diff --git a/luci-theme-footstrap/styles/pages/60-assoclist.css b/luci-theme-footstrap/styles/pages/60-assoclist.css index 42484f3c..59bac02c 100644 --- a/luci-theme-footstrap/styles/pages/60-assoclist.css +++ b/luci-theme-footstrap/styles/pages/60-assoclist.css @@ -1,27 +1,18 @@ @layer page { /* Associated stations (Overview and Network → Wireless render the same `.assoclist` table). * - * theme/30-tables.css lets ANY data cell break to fit (`overflow-wrap: anywhere`) — which is - * what reflows a wide table into the content column instead of scrolling it. On a MAC address - * that is wrong: `48:E1:5C:9F:26:20` split over two lines at whatever character happened to - * land on the edge (issue #5), and the column is only ~103px, so it split every time. Stock - * LuCI keeps it on one line. + * `white-space: nowrap` on column 2 used to stand here, because `overflow-wrap: anywhere` split + * `48:E1:5C:9F:26:20` at whatever character landed on the edge (issue #5) in a ~103px column, + * every time. * - * Exactly the shape pages/50-leases.css already uses, and for the same reason — see the note - * there about why the nowrap is also the table's floor, and why `:not(.fs-stacked)` is the - * whole guard: once fs-select.js has measured that the table no longer fits and carded it, the - * cells must be free to wrap again. + * A MAC has no break opportunity in it — a colon is not one — so the honest floor + * theme/30-tables.css now gives a data table keeps the whole address in one piece without a + * per-page rule, and keeps it for every table that shows one rather than for this one. * - * Column 2, NOT `[data-title="MAC address"]` — that literal is a translated string and the rule - * was dead on every localised router, which is precisely how the bug was still being reported - * (#7) after it had been "fixed". See the note in pages/50-leases.css. - * Columns: 1 Network | 2 MAC address | 3 Host | 4 Signal/Noise | 5 RX/TX Rate | 6 Disconnect. - * - * ONLY the MAC. Signal/Noise (4) and RX/TX Rate (5) were nowrapped too (v0.9.1, #7) to stop - * `anywhere` splitting "-54/-90 dBm" mid-character — and that was worse: nowrap makes the - * modulation string ("229 Mbit/s, 20 MHz, HE-MCS 9, HE-NSS 2") an unbreakable ~300px block, so - * it ate the row and crushed the Network column to a few characters (reported on #7 against - * 0.9.1). It also raised the table's floor, carding the table on desktops that had room. A wide - * cell that CAN break is the lesser evil: the columns keep their share. */ - #view table.fs-dt.assoclist:not(.fs-stacked) .td:nth-child(2) { white-space: nowrap; } + * What the old note here was right about, and what the ladder inherits: nowrapping the Signal + * and Rate columns as well (v0.9.1) was WORSE, because it made the modulation string + * ("229 Mbit/s, 20 MHz, HE-MCS 9, HE-NSS 2") an unbreakable ~300px block that ate the row and + * crushed the Network column. A wide cell that CAN break is the lesser evil, and that is exactly + * what rung 3 chooses when it has to choose: the widest breakable column gives way, never the + * identity column. */ } diff --git a/luci-theme-footstrap/styles/pages/90-processes.css b/luci-theme-footstrap/styles/pages/90-processes.css index 7cd09763..10dd92cc 100644 --- a/luci-theme-footstrap/styles/pages/90-processes.css +++ b/luci-theme-footstrap/styles/pages/90-processes.css @@ -1,27 +1,19 @@ @layer page { /* Processes (luci-mod-status): the PID column, which must not split. * - * Every data cell may break to fit — `overflow-wrap: anywhere` in base/40-tables.css, which is - * what reflows a wide table into the content column instead of scrolling it. A PID is the same - * kind of value a MAC address is: atomic, short, meaningless once a line break lands in the - * middle of it. `384` came out as "38/4" and `4810` as "481/0" — 23 of 26 rows on the router, - * measured at 1228px against a Russian catalogue, where the actions column is `nowrap` and - * 477px wide and the table has no width left to give. Stock luci-theme-bootstrap wraps 0 of the - * same 26, so this is the theme's own break to take back (issue #32, and issue #5 before it). + * `white-space: nowrap` used to stand here, and its job was to put a floor back under a column + * that base/40-tables.css had taken it away from: with `overflow-wrap: anywhere` on every cell, + * `384` rendered as "38/4" and `4810` as "481/0" — 23 of 26 rows on the router, measured at + * 1228px against a Russian catalogue (issue #32, and issue #5 before it). * - * Exactly the shape pages/50-leases.css and pages/60-assoclist.css already use, for exactly - * their reason: `white-space: nowrap` re-raises the column's floor to the value itself, and the - * floor is the point — a PID is four or five digits, so it costs the table ~40px it would - * otherwise spend splitting a number. `:not(.fs-stacked)` is the whole guard: once fs-select.js - * has measured that the table no longer fits and carded it, every cell must be free to wrap - * again. + * The floor is general now. theme/30-tables.css gives a data table `overflow-wrap: break-word` + * for as long as it is a table, so every column keeps the width of its own widest unbreakable + * token — a PID is one token and cannot be split by anything short of the ladder deciding that + * this column, specifically, must give way. Three per-page rules said the same thing three + * times; the one that survives (pages/30-software.css) survives because a hyphen IS a legal + * break opportunity and `break-word` will use it, which is a different fact. * - * FIRST child, not counted from the end, and that is safe here where it would not be on the - * leases tables: `processes.js` renders one fixed header row — PID, Owner, Command, CPU usage, - * Memory usage, actions — with no feature-dependent leading column of the kind `40_dhcp.js` - * inserts. If upstream ever prepends one, this rule nowraps that column instead, which is why - * it is scoped to the page rather than to `.fs-dt .td:first-child` at large. */ - body[data-page="admin-status-processes"] #view table.fs-dt:not(.fs-stacked) .td:first-child { - white-space: nowrap; - } + * The measurement that replaced it, on the router at 720px: the Command column reports 353px of + * need against 126px of width, the table asks for 963px in 688px of room, and the ladder marks + * exactly one column breakable — leaving the PID whole without a rule of its own. */ } diff --git a/luci-theme-footstrap/styles/theme/30-tables.css b/luci-theme-footstrap/styles/theme/30-tables.css index 98a9081d..74b5b48a 100644 --- a/luci-theme-footstrap/styles/theme/30-tables.css +++ b/luci-theme-footstrap/styles/theme/30-tables.css @@ -86,10 +86,13 @@ * now the DEFAULT for every cell (base/40-tables.css), so the copy that used to sit here — * `#view .table[id] .td, table.fs-dt .td { overflow-wrap: anywhere }` — said nothing the * cascade did not already say: switched off against 1941 matching cells across eight pages at - * three widths, not one computed style moved. It is gone rather than kept "for clarity", - * because a second copy of a value is the drift this sheet is written to avoid. DHCP leases - * still need finer control (only DUID/IPv6 may break, not IPv4/MAC) and get it from - * `white-space: nowrap`, not from this property: pages/50-leases.css. */ + * three widths, not one computed style moved. + * + * A rule on this property is back a few lines down, and it is the INVERSE of that one rather than + * a copy: a data table gets `break-word` while it is still a table, so its columns keep a floor + * and an overflow can be seen. Read that block before touching either. DHCP leases still need + * finer control (only DUID/IPv6 may break, not IPv4/MAC) and get it from `white-space: nowrap`, + * not from this property: pages/50-leases.css. */ .cbi-section .table .td, .cbi-section .table .th { padding: var(--fs-space-2-5) var(--fs-space-4) var(--fs-space-2-5) 0; @@ -120,8 +123,107 @@ .cbi-section .table:is([id], .fs-dt) .th, table.fs-dt .td, table.fs-dt .th { padding: var(--fs-space-2-5) var(--fs-space-3); } .cbi-section .table:is([id], .fs-dt) .td { font-family: var(--fs-font-mono); font-size: var(--fs-type); color: var(--fs-text); } - /* No horizontal-scroll wrapper: long values break (overflow-wrap above), so the table reflows - * into the column. Whether it cards instead is MEASURED (.fs-stacked, fs-select.js), any width. */ + + /* ---- THE FLOOR, AND IT BELONGS TO THE ONE TIER A MEASUREMENT READS ---- + * + * `base/40-tables.css` breaks every value `anywhere`, which is the ONE break value that also + * lowers the cell's min-content contribution — to a single character. That is what keeps a table + * nobody measures inside its card, and it is why the key/value includes, the config table and the + * meter rows are safe today. It is also a LIE told to the engine: with no floor, auto table layout + * is free to starve a column, and the browser then answers "it fits" — truthfully, uselessly. + * `pages/30-software.css` states the consequence outright: "`anywhere` hides the overflow by + * shredding a cell, so nothing is left for fit.overflows() to read". Four per-page rules and three + * JS heuristics existed to reconstruct the number the engine had before the theme threw it away. + * + * So the floor goes back exactly where it is READ: a data table, while it is still a table. + * `break-word` breaks the same values in the same places — the two values differ ONLY in whether + * the break counts towards min-content (css-text-3 §5.4) — so nothing about the rendering of a + * long token changes; what changes is that the column can no longer be starved below it, and an + * overflow becomes visible to `fit.overflows()`. + * + * `:not(.fs-stacked)` is the whole guard, and it is the state the fitter measures in (it strips + * the class first, fs-fit rule 1). Once carded, every cell has the row to itself, there is no + * neighbouring column left to starve, and `anywhere` is right again — theme/30-tables.css's card + * block restates it a few hundred lines down. + * + * base/40-tables.css is deliberately NOT touched: `break-word` there was tried and recorded as a + * failure (a table stayed 355px wide in a 346px column, and .fs-main clips), because a tier with + * no card and no scroll has nowhere to put the overflow. */ + .table.fs-dt:not(.fs-stacked) .td { overflow-wrap: break-word; } + /* …and the ONE column the fitter decided may be shredded rather than card the whole table. Written + * by fs-select.js on the cells of a single column, stripped before every measurement. (0,5,0) — it + * beats the floor above on specificity, not on source order. */ + .table.fs-dt:not(.fs-stacked) .td.fs-td-break { overflow-wrap: anywhere; } + + /* ---- AND THE ONE FLAG, BECAUSE THE VIEW WRITES ITS SHREDDING INLINE ---- + * + * `luci-mod-status`'s processes.js:39 emits `E('span', {'style': 'word-break: break-word'}, + * proc.COMMAND)`. `word-break: break-word` is the deprecated alias whose defined meaning is + * "`word-break: normal` plus `overflow-wrap: anywhere`" (css-text-3 §5.1) — so it lowers the + * cell's min-content to a single character, from an INLINE declaration that no cascade layer can + * outrank. Measured on the router at a 720px viewport: the Command column sat at 126px against a + * widest token of 353px, and forcing the column's `overflow-wrap` to break-word, normal and + * anywhere in turn changed nothing at all — the floor was being erased one level down, inside the + * span. With `word-break` neutralised the same table asks for 963px in 688px of room, which is the + * truth, and the fitter can act on it. + * + * This does not overrule the view's intent, it re-times it: the column may still shred, but now + * when the fitter marks it (`.fs-td-break`, one rung before carding) rather than always. Buttons + * and inputs are excluded — `base/10-reset.css` gives them `break-all` deliberately, and a control + * label is not a value being read. + * + * The `!important` is the sanctioned kind (docs/conventions.md): it fights an inline `style=`, + * which is the one adversary a layer cannot reach. */ + .table.fs-dt:not(.fs-stacked) .td, + .table.fs-dt:not(.fs-stacked) .td :not(button):not(.btn):not(input):not(select):not(textarea) { + word-break: normal !important; + } + + /* Whether a table cards, breaks one column or scrolls is MEASURED (fs-select.js), at any width. */ + + /* ---- THE HEADER ROW STAYS WHILE THE ROWS GO PAST ---- + * + * Processes renders 114 rows, Connections a few hundred, the lease tables dozens: past the first + * screenful the column headings are gone and every value is an unlabelled string of digits. The + * card view solves that by printing the heading above each value; a table that FITS had no + * equivalent, and scrolling back up to read which column is which is the oldest complaint about + * data tables there is. + * + * On the CELLS, not the row: `position: sticky` needs a box with a background, and in the + * separated border model a row's background does not travel with a sticky cell — the heading + * would float over the rows with the values showing through it. So each `.th` sticks, and each + * carries the header's own fill. + * + * WHAT IT STICKS TO depends on which box is scrolling. In the vertical sidebar `.fs-main` is its + * own scroller and nothing overlaps its top edge, so the offset is 0. In the bar layouts the + * DOCUMENT scrolls under a sticky bar, and that bar's height is not `--fs-bar-h`: it grows when + * the brand wraps, when the menu takes a row of its own or when the right-hand cluster does, all + * of which fs-chrome.js decides by measurement — so it publishes the measured height as + * `--fs-bar-live` and this reads it, with the designed height as the fallback for the frame + * before the chrome has been fitted. + * + * AND A DIALOG IS NEITHER, which is why every one of these selectors names its root. `#modal_overlay` + * is its own scrollport (`overflow: auto`, base/60-modal.css) and the bar does not overlap it, so a + * table in a dialog sticks at 0 in every layout. Given the bar offset instead — which it would have + * been, since the same change made a dialog a table root and its tables `.fs-dt` — the header row + * would pin a whole bar height BELOW the dialog's top edge, with rows scrolling through the empty + * band above it. The wireless scan dialog is exactly that shape. Reported in review. + * + * The ROOT is named inside `:where()`, which costs nothing: naming it plainly took the selector to + * [1,9,0] against the sheet's [1,7,0] ceiling (tools/css-metrics.mjs), and the id is here to say + * WHICH scrollport this offset belongs to, not to win anything. What has to win is the cell rule + * itself, and at [0,6,0] it does. */ + :where(:root[data-layout="sidebar"]:not([data-narrow]) #view, #modal_overlay) + .table.fs-dt:not(.fs-stacked) .tr.table-titles .th { + position: sticky; top: 0; z-index: 1; + background: var(--fs-panel2); + } + :where(:root[data-layout="top"] #view, :root[data-narrow] #view) + .table.fs-dt:not(.fs-stacked) .tr.table-titles .th { + position: sticky; top: var(--fs-bar-live, var(--fs-bar-h)); z-index: 1; + background: var(--fs-panel2); + } + .cbi-section .table:is([id], .fs-dt) .tr:not(.table-titles):not(.placeholder):hover .td { background: var(--fs-panel2); } .cbi-section .table:is([id], .fs-dt) .tr.table-titles .th { border-bottom-color: var(--fs-border); } /* Contour around the whole data table. A real needs border-collapse:separate or the @@ -399,8 +501,16 @@ * The media query keeps its own copies, for the tables that do NOT stack on a phone and for the * config tables (@container, never .fs-stacked). The `.col-N` weights need no guard — see below. */ .table.fs-stacked { display: block; width: 100%; } + /* `.fs-drop-xs` is the SAME drop, one rung earlier: fs-select.js writes it on a table that has + * overflowed, before it decides to card, because the view author has already said which columns + * are expendable and a table with two fewer columns often fits. Same declaration, so the two + * selectors share the block rather than repeating it — and the class is what makes the drop a + * MEASURED decision instead of the 767px viewport guess it used to be (theme/90-responsive.css + * keeps that copy for the tables that never card). */ .table.fs-stacked .td.hide-sm, .table.fs-stacked .th.hide-sm, - .table.fs-stacked .td.hide-xs, .table.fs-stacked .th.hide-xs { display: none; } + .table.fs-stacked .td.hide-xs, .table.fs-stacked .th.hide-xs, + .table.fs-drop-xs .td.hide-sm, .table.fs-drop-xs .th.hide-sm, + .table.fs-drop-xs .td.hide-xs, .table.fs-drop-xs .th.hide-xs { display: none; } .table.fs-stacked .tr.table-titles, .table.fs-stacked .thead, @@ -462,8 +572,16 @@ * overflow:visible, so a striped row's SQUARE background poked past the 12px corners. Safe once * stacked: a flex-wrap row fits the width, so there is no horizontal column left to clip — the * only thing overflow:visible protected. `#view .table.fs-dt.fs-stacked` (1,3,0) outranks the - * frame's `#view .table[id]` (1,2,0) on specificity, not order. */ - #view .table.fs-dt.fs-stacked { overflow: hidden; } + * frame's `#view .table[id]` (1,2,0) on specificity, not order. + * + * `#modal_overlay` as well, because the FRAME already reaches a dialog and this clip did not: a + * data table inside a dialog's `.cbi-section` (a form modal, an app's own dialog) takes the frame + * from `.cbi-section .table[id]` above, so it has the same 12px corners with the same zebra + * painted square across them. Measured on the stand with one table built twice, at 390px: both + * copies `border-radius: 12px`, both rows `rgb(246, 248, 251)`, `overflow: hidden` in #view and + * `visible` in the dialog. Same markup, same frame, one of them clipped. */ + #view .table.fs-dt.fs-stacked, + #modal_overlay .table.fs-dt.fs-stacked { overflow: hidden; } /* `:not(.placeholder)` — the plate is out of flow (base/40-tables.css) and its row exists only to * reserve the height it needs. Padding is subtracted from that reservation (border-box), so the * card's row inset left 41px of the 63px the plate is: measured on Wireless as 10px of text @@ -521,12 +639,82 @@ * 388px table in a 356px card. But the viewport was never the fact that mattered: a narrow panel, * a split window or a 960px tablet reaches the same wall with the query switched off, and the * clip is just as silent there. Cost at a width where the table fits: nothing at all — `overflow` - * only paints a scrollbar once there is something to scroll. */ - #view table:not(.table):not(.cbi-section-table):not(.fs-dt) { + * only paints a scrollbar once there is something to scroll. + * + * And a dialog is the worst place to leave it out. `#modal_overlay` is the SCROLL CONTAINER + * (base/60-modal.css) and the dialog is centred in it with `margin: auto`, so an over-wide child + * does not scroll itself — it scrolls the dialog's own left edge off the screen (theme/70-modal.css + * spends a block on the two stock children that did it). A table is the widest child there is. + * Measured at 390px with a four-column `nowrap` log table, no header row, built twice: in #view it + * scrolls inside itself (358px shown of 636px, page scroll 0); in a dialog it rendered 636px wide + * in 317px of room and gave the overlay 282px of horizontal scroll. */ + /* `.fs-xscroll` is the same outcome reached the other way: fs-select.js writes it on a `.table` + * that has NO header row and has actually been measured to overflow — the shape a bare `
` + * selector cannot reach, because a third-party app may render its log as `
`. + * A class rather than a selector, and MEASURED rather than declared, for one reason that is + * spec-level: `overflow-x: auto` forces `overflow-y` to compute to `auto` as well + * (css-overflow-3 §3.1 — there is no `auto`/`visible` pair), so a table given this becomes a clip + * for every popup inside it. LuCI's own `ui.js` then walks up from an open dropdown and finds the + * TABLE as the scroll parent, and sizes the list against it. So the fitter refuses the class to any + * table holding a widget, and this rule can stay free of a `:has()` exclusion ladder. */ + #view table:not(.table):not(.cbi-section-table):not(.fs-dt), + #modal_overlay table:not(.table):not(.cbi-section-table):not(.fs-dt), + #view .table.fs-xscroll, #modal_overlay .table.fs-xscroll { display: block; max-width: 100%; overflow-x: auto; /* the scroll gesture stops at this table instead of dragging the page sideways */ overscroll-behavior-x: contain; + /* the scrollbar IS the affordance here. A gradient "there is more to the right" shadow is the + * usual trick and does not work on these tables: it has to paint on the scroll container, + * behind rows that carry their own zebra fill, so it shows through every second row only. */ + scrollbar-width: thin; + } + + /* ---- A TABLE OF CONTROLS STACKS: it is a form that happens to be laid out as a table ---- + * + * Written by fs-select.js on a header-less table that overflows and holds a control (Network → + * Diagnostics: a host field, a method select and a Ping button in one row). Scrolling one of these + * is not an option — the popup inside would be clipped by the scroll container it just became — + * and doing nothing left it cut: measured at 320px of room, the row needed 338 and the button was + * past `.fs-main`'s clip with no way to reach it. + * + * `display: block` on all three levels, which is the ONE place this theme wants it: the rows are a + * form, so there is nothing to compare across them and nothing to keep in columns. Each control + * then takes the width of the line it was given, and the table cannot exceed its parent by + * construction. Nothing here creates a scroll container, so an open dropdown still escapes. */ + .table.fs-rowstack, + .table.fs-rowstack .tr, + .table.fs-rowstack .td, + .table.fs-rowstack .th { display: block; width: auto; } + /* the cells were table-cells a moment ago and carried its side padding; as blocks they need the + * vertical rhythm instead, or the controls sit flush against each other */ + .table.fs-rowstack .td { padding-inline: 0; padding-block: var(--fs-space-1); } + + /* ---- WHAT MAKES A SCROLLED TABLE READABLE: the row keeps its name ---- + * + * Scrolling a table sideways takes the identity column away first, which leaves a screenful of + * values belonging to nothing. Pinning the first cell of every row is the one thing a card view + * gives for free and a scrolled table does not, and no LuCI theme does it — a survey of 45 of + * them found sticky used on section titles and never on a column. + * + * The cells need their own background (the row's does not travel with a sticky cell in the + * separated model, same as the header above) and a hairline on the inline end, so the pinned + * column reads as an edge rather than as text sliding over text. `z-index: 2` keeps it over the + * sticky header's `1` where the two cross, which is the corner cell. */ + #view .table.fs-xscroll .tr > :first-child, + #modal_overlay .table.fs-xscroll .tr > :first-child, + #view .table.fs-xscroll tr > :first-child, + #modal_overlay .table.fs-xscroll tr > :first-child { + position: sticky; left: 0; z-index: 2; + background: var(--fs-panel); + border-inline-end: 1px solid var(--fs-border); + } + /* a keyboard user reaches the scroll box by tabbing to it (fs-select.js gives it tabindex and a + * name); it has to show that it has focus — outline, not box-shadow, so forced-colors keeps it */ + #view .table.fs-xscroll:focus-visible, + #modal_overlay .table.fs-xscroll:focus-visible { + outline: 2px solid var(--fs-accent); + outline-offset: 2px; } } diff --git a/luci-theme-footstrap/styles/theme/60-inputs.css b/luci-theme-footstrap/styles/theme/60-inputs.css index af74b278..5f5b3a9e 100644 --- a/luci-theme-footstrap/styles/theme/60-inputs.css +++ b/luci-theme-footstrap/styles/theme/60-inputs.css @@ -140,8 +140,13 @@ .cbi-filebrowser > ul { display: flex; flex-direction: column; gap: var(--fs-space-1); } .cbi-filebrowser > ul > li { align-items: center; flex-wrap: wrap; gap: var(--fs-space-2); } .cbi-filebrowser > ul > li > div:first-child { flex: 1 1 auto; min-width: 0; } + /* `0 1 auto`, not `0 0 auto`: the actions block is three buttons (Deselect / Download / Delete) and + * it may WRAP but may not shrink, so on a 320px screen it kept its one-line width and stuck 23px + * out of the browser box — measured in the gallery at 320px, where `Delete` ended past the card. + * Shrinking is what lets its own `flex-wrap` fire; `min-width: 0` is what lets shrinking reach + * below the buttons' min-content, which is the pair every flex item in this theme needs. */ .cbi-filebrowser > ul > li > div:last-child { - flex: 0 0 auto; + flex: 0 1 auto; min-width: 0; display: flex; flex-wrap: wrap; gap: var(--fs-space-2); justify-content: flex-end; } .cbi-filebrowser > ul > li > div:last-child > button { margin: 0; } @@ -544,7 +549,12 @@ } /* width/font-size absorbed from base's generic field box, same numbers — a textarea has * always sat at the fixed 210px (the elastic rule above names only text/url/search) */ - textarea { min-height: 84px; line-height: var(--fs-leading); width: 210px; font-size: var(--fs-type); } + /* `max-width: 100%` beside the width, because a textarea is the one field whose intrinsic width + * comes from an ATTRIBUTE (`cols`, default 20) rather than from the box: measured at a 300px + * viewport, two textareas in the gallery stood 19px outside their card while every other control + * in the same column fitted. The 210px is the designed width and still applies wherever there is + * room for it. */ + textarea { min-height: 84px; line-height: var(--fs-leading); width: 210px; max-width: 100%; font-size: var(--fs-type); } /* A textarea's native scrollbar is a square strip and a rounded corner does not clip it, so * on a scrolling config field the grey bar poked past the radius — a square notch out of the * frame. Fix: a slim self-rounded thumb floated off the edges (transparent border + @@ -655,7 +665,12 @@ .cbi-dynlist { gap: var(--fs-space-1-5); background: transparent; border: 0; padding: 0; - min-height: 0; min-width: 240px; max-width: var(--fs-field-max); width: 100%; + /* `min(240px, 100%)`, not a bare 240: the floor is there so a chip's × sits near its text + * rather than floating across a full-width field, and it becomes a defect the moment the + * column is narrower than the floor — measured in the gallery at a 320px viewport, the list + * held 240px inside a 210px field and every chip, the add row and its input stood 29px + * outside the card. A floor that exceeds the room is not a floor, it is an overflow. */ + min-height: 0; min-width: min(240px, 100%); max-width: var(--fs-field-max); width: 100%; vertical-align: top; } .cbi-dynlist > .item { diff --git a/luci-theme-footstrap/styles/theme/70-modal.css b/luci-theme-footstrap/styles/theme/70-modal.css index 6ace99b7..d6054074 100644 --- a/luci-theme-footstrap/styles/theme/70-modal.css +++ b/luci-theme-footstrap/styles/theme/70-modal.css @@ -65,7 +65,24 @@ * sentence: it cannot wrap at any width. Scoped to `label` — a real button keeps `pre`. * - a `
  • ` carrying a checksum ("SHA256: 5d27b68d…") — one unbreakable 64-char token, * nothing to wrap at, so it needs `anywhere` rather than a normal break. */ - #modal_overlay .modal label.btn { white-space: normal; } + /* …and `overflow-wrap` with it: `white-space: normal` lets the sentence wrap at its spaces, which + * is not enough for the one it ends with — `/etc/backup/installed_packages.txt` is a single + * ~200px token, so in any dialog narrower than that the label still ran past the box. Measured in + * the gallery at a 320px viewport: the label was 277px inside a 264px column, 13px out. A label + * is prose; a real button keeps `pre` and its intact label. + * + * `min-width: 0` is the other half, and without it the wrap never fires: `.btn` is `inline-flex`, + * so the label is a flex CONTAINER whose automatic minimum size is its content's min-content — + * the same trap the file browser's action block was in one file over. + * + * `anywhere` rather than `break-word`, and that follows the tier doctrine rather than contradicting + * it (base/40-tables.css states it for cells): `break-word` leaves min-content at the widest token, + * and the widest token here is `/etc/backup/installed_packages.txt` — about 240px, which is more + * than the dialog has at 320px, and the anonymous text flex item cannot be given a `min-width` of + * its own. A dialog label has no card to fold into and no scroll to escape into, so containment is + * the only outcome available, exactly as for a key/value cell. Measured at 320px: 277px inside a + * 236px column before, inside it after. */ + #modal_overlay .modal label.btn { white-space: normal; overflow-wrap: anywhere; min-width: 0; } #modal_overlay .modal li { overflow-wrap: anywhere; } #modal_overlay .uci-dialog div > ins, #modal_overlay .uci-dialog div > del, diff --git a/luci-theme-footstrap/styles/theme/90-responsive.css b/luci-theme-footstrap/styles/theme/90-responsive.css index d19a5d31..c7e50709 100644 --- a/luci-theme-footstrap/styles/theme/90-responsive.css +++ b/luci-theme-footstrap/styles/theme/90-responsive.css @@ -66,18 +66,31 @@ body[data-page="admin-system-package-manager"] #view .controls .control-group { display: flex; flex-wrap: wrap; min-width: 0; gap: var(--fs-space-2); } body[data-page="admin-system-package-manager"] #view .controls .control-group input { flex: 1 1 140px; min-width: 0; max-width: 100%; } - /* Diagnostics tool (input + method select + button in one
  • row) has no id and no - * .cbi-section-table, so nothing stacks it — a fixed table layout stayed ~365px wide. */ - body[data-page="admin-network-diagnostics"] #view .table:not([id]):not(.cbi-section-table), - body[data-page="admin-network-diagnostics"] #view .table:not([id]):not(.cbi-section-table) .tr, - body[data-page="admin-network-diagnostics"] #view .table:not([id]):not(.cbi-section-table) .td { display: block; width: auto; } + /* The Diagnostics stack used to be here, keyed by `body[data-page="admin-network-diagnostics"]` + * and by this 767px query — a page name and a viewport for a fact that is neither. Any app can + * put a field, a select and a button in one header-less `
    `, and what decides + * whether that row fits is the ROOM: measured at 320px the row needs 338px, and at 900px of + * viewport inside a narrow panel it needs the same. fs-select.js writes `.fs-rowstack` on any + * such table that overflows, at any width, and theme/30-tables.css carries the declarations. */ - /* fs-dt data tables (Startup init.d list, leases, processes) that do NOT card on a phone: - * the row stacking is MEASURED (.fs-stacked, fs-select.js) and carries its own - * `display: block` (theme/30-tables.css), but a table that still fits as a table keeps - * display: table and with it an intrinsic min-width. Block it so its width cannot exceed - * the column. */ - table.fs-dt { display: block; width: 100%; } + /* `table.fs-dt { display: block; width: 100% }` used to sit here, to stop a data table that + * still fitted as a table from exceeding the column with its intrinsic min-width. It is gone, + * and its removal is the reason the rest of this redesign works at phone widths at all. + * + * `display: block` on a
    DISCARDS the table formatting context: the rows are wrapped in + * an anonymous table sized to the block, so the table can no longer be wider than its parent — + * which means it can never OVERFLOW, which means `fit.overflows()` can never see anything, at + * any width below 767px. Measured on Processes at a 720px viewport with the rule in place: the + * Command column was 126px against a widest token of 353px, and forcing that column's + * `overflow-wrap` to `break-word`, `normal` and `anywhere` in turn produced the same 126px and + * the same "the table fits" every time. The floor could not be read because there was no table + * left to read it from, so every path in the list was shredded mid-word while the fitter, + * truthfully, saw nothing to fix. + * + * What replaces it is the ladder: below 568px of room a data table cards on the design + * judgement alone, and between 568 and 767 it now overflows honestly and the ladder drops the + * expendable columns, breaks one column, or cards. `.fs-main`'s `overflow-x: clip` stays the + * backstop it always was, and nothing reaches it in the sweep. */ /* generic caps: an input/select must never exceed its field, and long key/value strings * in the Overview cards must break. */ diff --git a/natflow/Makefile b/natflow/Makefile index e96a148e..24927149 100644 --- a/natflow/Makefile +++ b/natflow/Makefile @@ -10,12 +10,12 @@ include $(INCLUDE_DIR)/kernel.mk PKG_NAME:=natflow PKG_VERSION:=20260531 -PKG_RELEASE:=67 +PKG_RELEASE:=68 PKG_SOURCE:=$(PKG_VERSION).tar.xz PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git PKG_SOURCE_PROTO:=git -PKG_SOURCE_VERSION:=3439f751b7451808d001d937ca1ad18bd59ae2a1 +PKG_SOURCE_VERSION:=cdc2ffdafcf04a64f3a4ea8f87d5cbe9e368a44e PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) PKG_MAINTAINER:=Chen Minqiang PKG_LICENSE:=GPL-2.0 diff --git a/netdata/Makefile b/netdata/Makefile index d56cec12..42532d55 100644 --- a/netdata/Makefile +++ b/netdata/Makefile @@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=netdata PKG_VERSION:=2.11.0 -PKG_RELEASE:=16 +PKG_RELEASE:=17 PKG_MAINTAINER:=Josef Schlehofer , Daniel Engberg PKG_LICENSE:=GPL-3.0-or-later @@ -83,6 +83,7 @@ CMAKE_OPTIONS += \ -DENABLE_PLUGIN_SYSTEMD_JOURNAL=Off \ -DENABLE_PLUGIN_SYSTEMD_UNITS=Off \ -DENABLE_PLUGIN_XENSTAT=Off \ + -DENABLE_PLUGIN_NETWORK_VIEWER=Off \ -DSQLITE_USE_GIT=On \ -DUSE_MOLD=Off \ -DENABLE_WEBRTC=Off