mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-10-31 01:45:13 +08:00
update 2026-09-26 11:07:27
This commit is contained in:
1 parent
7b88b273d0
commit
97ae9f68da
4898 files changed
-1712274
No files matched your search
@@ -1,183 +0,0 @@
|
||||
# Copyright (C) 2018-2020 L-WRT Team
|
||||
# Copyright (C) 2021-2025 xiaorouji
|
||||
# Copyright (C) 2026 Openwrt-Passwall Organization
|
||||
#
|
||||
# This is free software, licensed under the GNU General Public License v3.
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall
|
||||
PKG_VERSION:=26.9.16
|
||||
PKG_RELEASE:=1
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_Nftables_Transparent_Proxy \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Geoview \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Haproxy \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadow_TLS \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Simple_Obfs \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_SingBox \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Geodata \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Plugin \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Xray \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Xray_Plugin
|
||||
|
||||
LUCI_TITLE:=LuCI support for PassWall
|
||||
LUCI_PKGARCH:=all
|
||||
LUCI_DEPENDS:=+coreutils +coreutils-base64 +coreutils-nohup +coreutils-timeout +curl \
|
||||
+chinadns-ng +dns2socks +dnsmasq-full +ip-full \
|
||||
+libuci-lua +lua +luci-compat +luci-lib-jsonc \
|
||||
+microsocks +resolveip +tcping +lyaml
|
||||
|
||||
define Package/$(PKG_NAME)/config
|
||||
menu "Configuration"
|
||||
depends on PACKAGE_$(PKG_NAME)
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy
|
||||
bool "Iptables Transparent Proxy"
|
||||
select PACKAGE_ipset
|
||||
select PACKAGE_ipt2socks
|
||||
select PACKAGE_iptables
|
||||
select PACKAGE_iptables-zz-legacy
|
||||
select PACKAGE_iptables-mod-conntrack-extra
|
||||
select PACKAGE_iptables-mod-iprange
|
||||
select PACKAGE_iptables-mod-socket
|
||||
select PACKAGE_iptables-mod-tproxy
|
||||
select PACKAGE_kmod-ipt-nat
|
||||
default y if ! PACKAGE_firewall4
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_Nftables_Transparent_Proxy
|
||||
bool "Nftables Transparent Proxy"
|
||||
select PACKAGE_ipt2socks
|
||||
select PACKAGE_nftables
|
||||
select PACKAGE_kmod-nft-socket
|
||||
select PACKAGE_kmod-nft-tproxy
|
||||
select PACKAGE_kmod-nft-nat
|
||||
select PACKAGE_kmod-nf-reject
|
||||
select PACKAGE_kmod-nf-reject6
|
||||
default y if PACKAGE_firewall4
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_Geoview
|
||||
bool "Include Geoview"
|
||||
select PACKAGE_geoview
|
||||
default y if aarch64||arm||i386||x86_64
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_Haproxy
|
||||
bool "Include Haproxy"
|
||||
select PACKAGE_haproxy
|
||||
default y if aarch64||arm||i386||x86_64
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria
|
||||
bool "Include Hysteria"
|
||||
select PACKAGE_hysteria
|
||||
default n
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy
|
||||
bool "Include NaiveProxy"
|
||||
depends on !(arc||armeb||loongarch64||mips||mips64||powerpc||TARGET_gemini)
|
||||
select PACKAGE_naiveproxy
|
||||
default n
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client
|
||||
bool "Include Shadowsocks Rust Client"
|
||||
depends on !i386
|
||||
select PACKAGE_shadowsocks-rust-sslocal
|
||||
default y if aarch64||x86_64
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server
|
||||
bool "Include Shadowsocks Rust Server"
|
||||
depends on !i386
|
||||
select PACKAGE_shadowsocks-rust-ssserver
|
||||
default n
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client
|
||||
bool "Include ShadowsocksR Libev Client"
|
||||
select PACKAGE_shadowsocksr-libev-ssr-local
|
||||
select PACKAGE_shadowsocksr-libev-ssr-redir
|
||||
default y
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server
|
||||
bool "Include ShadowsocksR Libev Server"
|
||||
select PACKAGE_shadowsocksr-libev-ssr-server
|
||||
default n
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadow_TLS
|
||||
bool "Include Shadow-TLS"
|
||||
select PACKAGE_shadow-tls
|
||||
depends on aarch64||arm||x86_64
|
||||
default n
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_Simple_Obfs
|
||||
bool "Include Simple-Obfs (Shadowsocks Plugin)"
|
||||
select PACKAGE_simple-obfs-client
|
||||
default y
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_SingBox
|
||||
bool "Include Sing-Box"
|
||||
select PACKAGE_sing-box
|
||||
default y if aarch64||arm||i386||x86_64
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Geodata
|
||||
bool "Include V2ray_Geodata"
|
||||
select PACKAGE_v2ray-geoip
|
||||
select PACKAGE_v2ray-geosite
|
||||
default n
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Plugin
|
||||
bool "Include V2ray-Plugin (Shadowsocks Plugin)"
|
||||
select PACKAGE_v2ray-plugin
|
||||
default y if aarch64||arm||i386||x86_64
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_Xray
|
||||
bool "Include Xray"
|
||||
select PACKAGE_xray-core
|
||||
select PACKAGE_unzip
|
||||
default y if aarch64||arm||i386||x86_64
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_Xray_Plugin
|
||||
bool "Include Xray-Plugin (Shadowsocks Plugin)"
|
||||
select PACKAGE_xray-plugin
|
||||
default n
|
||||
|
||||
endmenu
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/conffiles
|
||||
/etc/config/passwall
|
||||
/etc/config/passwall_server
|
||||
/usr/share/passwall/rules/direct_host
|
||||
/usr/share/passwall/rules/direct_ip
|
||||
/usr/share/passwall/rules/proxy_host
|
||||
/usr/share/passwall/rules/proxy_ip
|
||||
/usr/share/passwall/rules/block_host
|
||||
/usr/share/passwall/rules/block_ip
|
||||
/usr/share/passwall/rules/lanlist_ipv4
|
||||
/usr/share/passwall/rules/lanlist_ipv6
|
||||
/usr/share/passwall/rules/domains_excluded
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postinst
|
||||
#!/bin/sh
|
||||
rm -f $${IPKG_INSTROOT}/usr/share/passwall/rules/*.nft
|
||||
rm -rf $${IPKG_INSTROOT}/tmp/etc/passwall_tmp
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postrm
|
||||
#!/bin/sh
|
||||
rm -f $${IPKG_INSTROOT}/usr/share/passwall/rules/*.nft
|
||||
rm -rf $${IPKG_INSTROOT}/tmp/etc/passwall_tmp
|
||||
exit 0
|
||||
endef
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
@@ -1,2 +0,0 @@
|
||||
/*! Sortable 1.15.7 - MIT | git://github.com/SortableJS/Sortable.git */
|
||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function o(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=Array(e);n<e;n++)o[n]=t[n];return o}function i(t,e,n){return(e=function(t){t=function(t,e){if("object"!=typeof t||!t)return t;var n=t[Symbol.toPrimitive];if(void 0===n)return("string"===e?String:Number)(t);e=n.call(t,e||"default");if("object"!=typeof e)return e;throw new TypeError("@@toPrimitive must return a primitive value.")}(t,"string");return"symbol"==typeof t?t:t+""}(e))in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function a(){return(a=Object.assign?Object.assign.bind():function(t){for(var e=1;e<arguments.length;e++){var n,o=arguments[e];for(n in o)!{}.hasOwnProperty.call(o,n)||(t[n]=o[n])}return t}).apply(null,arguments)}function r(e,t){var n,o=Object.keys(e);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(e),t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),o.push.apply(o,n)),o}function I(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?r(Object(n),!0).forEach(function(t){i(e,t,n[t])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):r(Object(n)).forEach(function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))})}return e}function l(t,e){if(null==t)return{};var n,o=function(t,e){if(null==t)return{};var n,o={};for(n in t)if({}.hasOwnProperty.call(t,n)){if(-1!==e.indexOf(n))continue;o[n]=t[n]}return o}(t,e);if(Object.getOwnPropertySymbols)for(var i=Object.getOwnPropertySymbols(t),r=0;r<i.length;r++)n=i[r],-1===e.indexOf(n)&&{}.propertyIsEnumerable.call(t,n)&&(o[n]=t[n]);return o}function e(t){return function(t){if(Array.isArray(t))return o(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,e){if(t){if("string"==typeof t)return o(t,e);var n={}.toString.call(t).slice(8,-1);return"Map"===(n="Object"===n&&t.constructor?t.constructor.name:n)||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?o(t,e):void 0}}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function s(t){return(s="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function t(t){if("undefined"!=typeof window&&window.navigator)return!!navigator.userAgent.match(t)}var y=t(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i),w=t(/Edge/i),c=t(/firefox/i),u=t(/safari/i)&&!t(/chrome/i)&&!t(/android/i),d=t(/iP(ad|od|hone)/i),n=t(/chrome/i)&&t(/android/i),h={capture:!1,passive:!1};function f(t,e,n){t.addEventListener(e,n,!y&&h)}function p(t,e,n){t.removeEventListener(e,n,!y&&h)}function g(t,e){if(e&&(">"===e[0]&&(e=e.substring(1)),t))try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return}}function m(t){return t.host&&t!==document&&t.host.nodeType&&t.host!==t?t.host:t.parentNode}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"!==e[0]||t.parentNode===n)&&g(t,e)||o&&t===n)return t}while(t!==n&&(t=m(t)))}return null}var v,b=/\s+/g;function k(t,e,n){var o;t&&e&&(t.classList?t.classList[n?"add":"remove"](e):(o=(" "+t.className+" ").replace(b," ").replace(" "+e+" "," "),t.className=(o+(n?" "+e:"")).replace(b," ")))}function R(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];o[e=!(e in o||-1!==e.indexOf("webkit"))?"-webkit-"+e:e]=n+("string"==typeof n?"":"px")}}function D(t,e){var n="";if("string"==typeof t)n=t;else do{var o=R(t,"transform")}while(o&&"none"!==o&&(n=o+" "+n),!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function E(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i<r;i++)n(o[i],i);return o}return[]}function O(){var t=document.scrollingElement;return t||document.documentElement}function X(t,e,n,o,i){if(t.getBoundingClientRect||t===window){var r,a,l,s,c,u,d=t!==window&&t.parentNode&&t!==O()?(a=(r=t.getBoundingClientRect()).top,l=r.left,s=r.bottom,c=r.right,u=r.height,r.width):(l=a=0,s=window.innerHeight,c=window.innerWidth,u=window.innerHeight,window.innerWidth);if((e||n)&&t!==window&&(i=i||t.parentNode,!y))do{if(i&&i.getBoundingClientRect&&("none"!==R(i,"transform")||n&&"static"!==R(i,"position"))){var h=i.getBouLine truncated
|
||||
@@ -1,50 +0,0 @@
|
||||
function is_json(str) {
|
||||
try {
|
||||
const result = JSON.parse(str);
|
||||
return typeof result === 'object' && result !== null;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function is_timehhmm(str) {
|
||||
const match = String(str).match(/^(\d?\d):(\d\d)$/);
|
||||
if (match) {
|
||||
const hour = parseInt(match[1], 10);
|
||||
const minute = parseInt(match[2], 10);
|
||||
if (hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof cbi_validators !== 'undefined' && cbi_validators !== null) {
|
||||
cbi_validators['base64'] = function() {
|
||||
return isBase64(this);
|
||||
}
|
||||
cbi_validators['json'] = function() {
|
||||
return is_json(this);
|
||||
}
|
||||
cbi_validators['timehhmm'] = function() {
|
||||
return is_timehhmm(this);
|
||||
}
|
||||
cbi_validators['uuid'] = function() {
|
||||
return isUUID(this);
|
||||
}
|
||||
} else {
|
||||
L.require('validation').then(function(validation) {
|
||||
validation.types['base64'] = function() {
|
||||
return this.assert(isBase64(this.value), _('Must be Base64 text!'));
|
||||
}
|
||||
validation.types['json'] = function() {
|
||||
return this.assert(is_json(this.value), _('Must be JSON text!'));
|
||||
}
|
||||
validation.types['timehhmm'] = function() {
|
||||
return this.assert(is_timehhmm(this.value), _('valid time (hh:mm)'));
|
||||
}
|
||||
validation.types['uuid'] = function() {
|
||||
return this.assert(isUUID(this.value), _('Must be UUID format!'));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
function arraysEqual(a, b) {
|
||||
if (a === b) return true;
|
||||
if (a == null || b == null) return false;
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isBase64(str) {
|
||||
const base64Regex = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/;
|
||||
return base64Regex.test(str);
|
||||
}
|
||||
|
||||
function decodeIfBase64(str) {
|
||||
try {
|
||||
let s = str.replace(/-/g, '+').replace(/_/g, '/');
|
||||
while (s.length % 4) s += '=';
|
||||
const decoded = decodeURIComponent(
|
||||
atob(s).split('').map(c =>
|
||||
'%' + c.charCodeAt(0).toString(16).padStart(2, '0')
|
||||
).join('')
|
||||
);
|
||||
if (btoa(unescape(encodeURIComponent(decoded))).replace(/=+$/, '') === s.replace(/=+$/, '')) {
|
||||
return decoded;
|
||||
}
|
||||
} catch (e) {}
|
||||
return str;
|
||||
}
|
||||
|
||||
function isUUID(str) {
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
return uuidRegex.test(str);
|
||||
}
|
||||
|
||||
function waitForElement(selector, callback) {
|
||||
const el = document.querySelector(selector);
|
||||
if (el) return callback(el);
|
||||
const observer = new MutationObserver(() => {
|
||||
const el = document.querySelector(selector);
|
||||
if (el) {
|
||||
observer.disconnect();
|
||||
callback(el);
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
function waitForElementId(id, callback) {
|
||||
waitForElement("#" + CSS.escape(id), callback);
|
||||
//waitForElement('[id="' + id + '"]', callback);
|
||||
}
|
||||
|
||||
function get_current_url() {
|
||||
return window.location.origin + window.location.pathname;
|
||||
}
|
||||
|
||||
function getOption(config, section, opt) {
|
||||
let obj;
|
||||
const id = `cbid.${config}.${section}.${opt}`;
|
||||
obj = document.getElementsByName(id)[0] || document.getElementById(id);
|
||||
if (obj) {
|
||||
//lua luci combobox
|
||||
const combobox = document.getElementById('cbi.combobox.' + id);
|
||||
if (combobox) {
|
||||
obj.combobox = combobox;
|
||||
}
|
||||
return obj;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
var QRCode;!function(){function t(t){this.mode=r.MODE_8BIT_BYTE,this.data=t,this.parsedData=[];for(var e=0,o=this.data.length;e<o;e++){var i=[],n=this.data.charCodeAt(e);n>65536?(i[0]=240|(1835008&n)>>>18,i[1]=128|(258048&n)>>>12,i[2]=128|(4032&n)>>>6,i[3]=128|63&n):n>2048?(i[0]=224|(61440&n)>>>12,i[1]=128|(4032&n)>>>6,i[2]=128|63&n):n>128?(i[0]=192|(1984&n)>>>6,i[1]=128|63&n):i[0]=n,this.parsedData.push(i)}this.parsedData=Array.prototype.concat.apply([],this.parsedData),this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function e(t,e){this.typeNumber=t,this.errorCorrectLevel=e,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}t.prototype={getLength:function(t){return this.parsedData.length},write:function(t){for(var e=0,r=this.parsedData.length;e<r;e++)t.put(this.parsedData[e],8)}},e.prototype={addData:function(e){var r=new t(e);this.dataList.push(r),this.dataCache=null},isDark:function(t,e){if(t<0||this.moduleCount<=t||e<0||this.moduleCount<=e)throw new Error(t+","+e);return this.modules[t][e]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(t,r){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var o=0;o<this.moduleCount;o++){this.modules[o]=new Array(this.moduleCount);for(var i=0;i<this.moduleCount;i++)this.modules[o][i]=null}this.setupPositionProbePattern(0,0),this.setupPositionProbePattern(this.moduleCount-7,0),this.setupPositionProbePattern(0,this.moduleCount-7),this.setupPositionAdjustPattern(),this.setupTimingPattern(),this.setupTypeInfo(t,r),this.typeNumber>=7&&this.setupTypeNumber(t),null==this.dataCache&&(this.dataCache=e.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,r)},setupPositionProbePattern:function(t,e){for(var r=-1;r<=7;r++)if(!(t+r<=-1||this.moduleCount<=t+r))for(var o=-1;o<=7;o++)e+o<=-1||this.moduleCount<=e+o||(this.modules[t+r][e+o]=0<=r&&r<=6&&(0==o||6==o)||0<=o&&o<=6&&(0==r||6==r)||2<=r&&r<=4&&2<=o&&o<=4)},getBestMaskPattern:function(){for(var t=0,e=0,r=0;r<8;r++){this.makeImpl(!0,r);var o=g.getLostPoint(this);(0==r||t>o)&&(t=o,e=r)}return e},createMovieClip:function(t,e,r){var o=t.createEmptyMovieClip(e,r);this.make();for(var i=0;i<this.modules.length;i++)for(var n=1*i,a=0;a<this.modules[i].length;a++){var s=1*a;this.modules[i][a]&&(o.beginFill(0,100),o.moveTo(s,n),o.lineTo(s+1,n),o.lineTo(s+1,n+1),o.lineTo(s,n+1),o.endFill())}return o},setupTimingPattern:function(){for(var t=8;t<this.moduleCount-8;t++)null==this.modules[t][6]&&(this.modules[t][6]=t%2==0);for(var e=8;e<this.moduleCount-8;e++)null==this.modules[6][e]&&(this.modules[6][e]=e%2==0)},setupPositionAdjustPattern:function(){for(var t=g.getPatternPosition(this.typeNumber),e=0;e<t.length;e++)for(var r=0;r<t.length;r++){var o=t[e],i=t[r];if(null==this.modules[o][i])for(var n=-2;n<=2;n++)for(var a=-2;a<=2;a++)this.modules[o+n][i+a]=-2==n||2==n||-2==a||2==a||0==n&&0==a}},setupTypeNumber:function(t){for(var e=g.getBCHTypeNumber(this.typeNumber),r=0;r<18;r++){var o=!t&&1==(e>>r&1);this.modules[Math.floor(r/3)][r%3+this.moduleCount-8-3]=o}for(r=0;r<18;r++){o=!t&&1==(e>>r&1);this.modules[r%3+this.moduleCount-8-3][Math.floor(r/3)]=o}},setupTypeInfo:function(t,e){for(var r=this.errorCorrectLevel<<3|e,o=g.getBCHTypeInfo(r),i=0;i<15;i++){var n=!t&&1==(o>>i&1);i<6?this.modules[i][8]=n:i<8?this.modules[i+1][8]=n:this.modules[this.moduleCount-15+i][8]=n}for(i=0;i<15;i++){n=!t&&1==(o>>i&1);i<8?this.modules[8][this.moduleCount-i-1]=n:i<9?this.modules[8][15-i-1+1]=n:this.modules[8][15-i-1]=n}this.modules[this.moduleCount-8][8]=!t},mapData:function(t,e){for(var r=-1,o=this.moduleCount-1,i=7,n=0,a=this.moduleCount-1;a>0;a-=2)for(6==a&&a--;;){for(var s=0;s<2;s++)if(null==this.modules[o][a-s]){var h=!1;n<t.length&&(h=1==(t[n]>>>i&1)),g.getMask(e,o,a-s)&&(h=!h),this.modules[o][a-s]=h,-1==--i&&(n++,i=7)}if((o+=r)<0||this.moduleCount<=o){o-=r,r=-r;break}}}},e.PAD0=236,e.PAD1=17,e.createData=function(t,r,o){for(var i=m.getRSBlocks(t,r),n=new _,a=0;a<o.length;a++){var s=o[a];n.put(s.mode,4),n.put(s.getLength(),g.getLengthInBits(s.mode,t)),s.write(n)}var h=0;for(a=0;a<i.length;a++)h+=i[a].dataCount;if(n.getLengthInBits()>8*h)throw new Error("code length overflow. ("+n.getLengthInBits()+">"+8*h+")");for(n.getLengthInBits()+4<=8*h&&n.put(0,4);n.getLengthInBits()%8!=0;)n.putBit(!1);for(;!(n.getLengthInBits()>=8*h||(n.put(e.PAD0,8),n.getLengthInBits()>=8*h));)n.put(e.PAD1,8);return e.createBytes(n,i)},e.createBytes=function(t,e){for(var r=0,o=0,i=0,n=new Array(e.length),a=new Array(e.length),s=0;s<e.length;s++){var h=e[s].dataCount,l=e[s].totalCount-h;o=Math.max(o,h),i=Math.max(i,l),n[s]=new Array(h);for(var u=0;u<n[s].length;u++)n[s][u]=255&t.buffer[u+r];r+=h;var f=g.getErrorCorrectPolynomial(l),d=new p(n[s],f.getLength()-1).mod(f);a[s]=new Array(f.getLength()-1);for(u=0;u<a[s].length;u++)Line truncated
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,85 +0,0 @@
|
||||
local api = require "luci.passwall.api"
|
||||
|
||||
api.set_default_cbi()
|
||||
|
||||
m = Map()
|
||||
|
||||
s = m:section(NamedSection, "@global[0]", "global", translate("ACLs"), "<font color='red'>" .. translate("ACLs is a tools which used to designate specific IP proxy mode.") .. "</font>")
|
||||
|
||||
o = s:option(Flag, "acl_enable", translate("Main switch"))
|
||||
o.rmempty = false
|
||||
o.default = false
|
||||
|
||||
-- [[ ACLs Settings ]]--
|
||||
s = m:section(TypedSection, "acl_rule")
|
||||
s.template = "cbi/tblsection"
|
||||
s.sortable = true
|
||||
s.anonymous = true
|
||||
s.addremove = true
|
||||
s.extedit = api.url("acl_config", "%s")
|
||||
function s.create(e, t)
|
||||
local uid = "acl_" .. api.gen_random_char(5)
|
||||
TypedSection.create(e, uid)
|
||||
luci.http.redirect(e.extedit:format(uid))
|
||||
end
|
||||
function s.remove(e, t)
|
||||
api.sys.call("rm -rf /tmp/etc/passwall_tmp/dns_" .. t .. "*")
|
||||
TypedSection.remove(e, t)
|
||||
end
|
||||
|
||||
---- Enable
|
||||
o = s:option(Flag, "enabled", translate("Enable"))
|
||||
o.default = 1
|
||||
o.rmempty = false
|
||||
|
||||
---- Remarks
|
||||
o = s:option(Value, "remarks", translate("Remarks"))
|
||||
o.rmempty = true
|
||||
|
||||
local mac_t = {}
|
||||
api.sys.net.mac_hints(function(e, t)
|
||||
mac_t[e] = {
|
||||
ip = t,
|
||||
mac = e
|
||||
}
|
||||
end)
|
||||
|
||||
i = s:option(DummyValue, "interface", translate("Source Interface"))
|
||||
i.cfgvalue = function(t, n)
|
||||
local v = Value.cfgvalue(t, n) or ''
|
||||
if v == "" then
|
||||
return translate("All")
|
||||
end
|
||||
return v
|
||||
end
|
||||
|
||||
o = s:option(DummyValue, "sources", translate("Source"))
|
||||
o.rawhtml = true
|
||||
o.cfgvalue = function(t, n)
|
||||
local e = ''
|
||||
local v = Value.cfgvalue(t, n) or '-'
|
||||
string.gsub(v, '[^' .. " " .. ']+', function(w)
|
||||
local a = w
|
||||
if mac_t[w] then
|
||||
a = a .. ' (' .. mac_t[w].ip .. ')'
|
||||
end
|
||||
if #e > 0 then
|
||||
e = e .. "<br />"
|
||||
end
|
||||
e = e .. a
|
||||
end)
|
||||
return e
|
||||
end
|
||||
|
||||
i = s:option(DummyValue, "mode", translate("Mode"))
|
||||
i.cfgvalue = function(t, n)
|
||||
local v = Value.cfgvalue(t, n) or '0'
|
||||
if v == "1" then
|
||||
return translate("Proxy")
|
||||
end
|
||||
return translate("No Proxy")
|
||||
end
|
||||
|
||||
m:appendTemplate("/cbi/sortable", {sectiontype = s.sectiontype})
|
||||
|
||||
return api.return_map(m)
|
||||
@@ -1,628 +0,0 @@
|
||||
local api = require "luci.passwall.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(m.redirect)
|
||||
end
|
||||
|
||||
m:appendTemplate("/cbi/nodes_listvalue_com")
|
||||
|
||||
local has_singbox = api.finded_com("sing-box")
|
||||
local has_xray = api.finded_com("xray")
|
||||
local has_gfwlist = api.fs.access("/usr/share/passwall/rules/gfwlist")
|
||||
local has_chnlist = api.fs.access("/usr/share/passwall/rules/chnlist")
|
||||
local has_chnroute = api.fs.access("/usr/share/passwall/rules/chnroute")
|
||||
|
||||
local port_validate = function(self, value, t)
|
||||
return value:gsub("-", ":")
|
||||
end
|
||||
|
||||
local nodes_table = {}
|
||||
for _, e in ipairs(api.get_valid_nodes()) do
|
||||
nodes_table[#nodes_table + 1] = e
|
||||
end
|
||||
|
||||
local normal_list = {}
|
||||
for _, v in pairs(nodes_table) do
|
||||
if v.node_type == "normal" then
|
||||
normal_list[#normal_list + 1] = v
|
||||
end
|
||||
end
|
||||
|
||||
local socks_list = {}
|
||||
if has_singbox or has_xray then
|
||||
m:foreach("socks", function(s)
|
||||
if s.enabled == "1" and s.node then
|
||||
socks_list[#socks_list + 1] = {
|
||||
id = s[".name"],
|
||||
remark = translate("Socks Config") .. " " .. string.format("[%s %s]", s.port, translate("Port")),
|
||||
group = "Socks"
|
||||
}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- [[ ACLs Settings ]]--
|
||||
s = m:section(NamedSection, arg[1], translate("ACLs"), translate("ACLs"))
|
||||
|
||||
s:tab("Main", translate("Main"))
|
||||
s:tab("Proxy", translate("Proxy"))
|
||||
s:tab("DNS", translate("DNS"))
|
||||
s:tab("Log", translate("Log"))
|
||||
|
||||
---- Enable
|
||||
o = s:taboption("Main", Flag, "enabled", translate("Enable"))
|
||||
o.default = 1
|
||||
o.rmempty = false
|
||||
|
||||
---- Remarks
|
||||
o = s:taboption("Main", Value, "remarks", translate("Remarks"))
|
||||
o.default = arg[1]
|
||||
o.rmempty = false
|
||||
|
||||
o = s:taboption("Main", Value, "interface", translate("Source Interface"))
|
||||
o:value("", translate("All"))
|
||||
local iface = api.get_network_devices()
|
||||
for _, d in ipairs(iface) do
|
||||
o:value(d.name, d.label)
|
||||
end
|
||||
o.validate = function(self, value, section)
|
||||
if value == "" or value:match("^[a-zA-Z0-9][a-zA-Z0-9%.%_%-]*$") then
|
||||
return value
|
||||
end
|
||||
return nil, translate("Invalid interface name")
|
||||
end
|
||||
|
||||
local mac_t = {}
|
||||
api.sys.net.mac_hints(function(e, t)
|
||||
mac_t[#mac_t + 1] = {
|
||||
ip = t,
|
||||
mac = e
|
||||
}
|
||||
end)
|
||||
table.sort(mac_t, function(a,b)
|
||||
if #a.ip < #b.ip then
|
||||
return true
|
||||
elseif #a.ip == #b.ip then
|
||||
if a.ip < b.ip then
|
||||
return true
|
||||
else
|
||||
return #a.ip < #b.ip
|
||||
end
|
||||
end
|
||||
return false
|
||||
end)
|
||||
|
||||
---- Source
|
||||
sources = s:taboption("Main", DynamicList, "sources", translate("Source"))
|
||||
sources.description = "<ul><li>" .. translate("Example:")
|
||||
.. "</li><li>" .. translate("MAC") .. ": 00:00:00:FF:FF:FF"
|
||||
.. "</li><li>" .. translate("IP") .. ": 192.168.1.100"
|
||||
.. "</li><li>" .. translate("IP CIDR") .. ": 192.168.1.0/24"
|
||||
.. "</li><li>" .. translate("IP range") .. ": 192.168.1.100-192.168.1.200"
|
||||
.. "</li><li>" .. translate("IPSet") .. ": ipset:lanlist"
|
||||
.. "</li></ul>"
|
||||
sources.cast = "string"
|
||||
for _, key in pairs(mac_t) do
|
||||
sources:value(key.mac, "%s (%s)" % {key.mac, key.ip})
|
||||
end
|
||||
sources.cfgvalue = function(self, section)
|
||||
local value
|
||||
if self.tag_error[section] then
|
||||
value = self:formvalue(section)
|
||||
else
|
||||
value = self.map:get(section, self.option)
|
||||
if type(value) == "string" then
|
||||
local value2 = {}
|
||||
string.gsub(value, '[^' .. " " .. ']+', function(w) table.insert(value2, w) end)
|
||||
value = value2
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
sources.validate = function(self, value, t)
|
||||
local err = {}
|
||||
for _, v in ipairs(value) do
|
||||
local flag = false
|
||||
if v:find("ipset:") and v:find("ipset:") == 1 then
|
||||
local ipset = v:gsub("ipset:", "")
|
||||
if ipset and ipset ~= "" then
|
||||
flag = true
|
||||
end
|
||||
end
|
||||
if flag == false and datatypes.macaddr(v) then
|
||||
flag = true
|
||||
end
|
||||
if flag == false and datatypes.ip4addr(v) then
|
||||
flag = true
|
||||
end
|
||||
if flag == false and api.iprange(v) then
|
||||
flag = true
|
||||
end
|
||||
if flag == false then
|
||||
err[#err + 1] = v
|
||||
end
|
||||
end
|
||||
if #err > 0 then
|
||||
self:add_error(t, "invalid", translate("Not true format, please re-enter!"))
|
||||
for _, v in ipairs(err) do
|
||||
self:add_error(t, "invalid", v)
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
local NODE = m:get("@global[0]", "node") or ""
|
||||
o = s:taboption("Main", ListValue, "mode", translate("Mode"))
|
||||
o:value("0", translate("No Proxy"))
|
||||
o:value("1", translate("Proxy"))
|
||||
|
||||
---- TCP No Redir Ports
|
||||
local TCP_NO_REDIR_PORTS = m:get("@global_forwarding[0]", "tcp_no_redir_ports")
|
||||
o = s:taboption("Proxy", Value, "tcp_no_redir_ports", translate("TCP No Redir Ports"))
|
||||
o:value("", translate("Use global config") .. "(" .. TCP_NO_REDIR_PORTS .. ")")
|
||||
o:value("disable", translate("No patterns are used"))
|
||||
o:value("1:65535", translate("All"))
|
||||
o:depends("mode", "1")
|
||||
o.validate = port_validate
|
||||
|
||||
---- UDP No Redir Ports
|
||||
local UDP_NO_REDIR_PORTS = m:get("@global_forwarding[0]", "udp_no_redir_ports")
|
||||
o = s:taboption("Proxy", Value, "udp_no_redir_ports", translate("UDP No Redir Ports"),
|
||||
"<font color='red'>" ..
|
||||
translate("Fill in the ports you don't want to be forwarded by the agent, with the highest priority.") ..
|
||||
"</font>")
|
||||
o:value("", translate("Use global config") .. "(" .. UDP_NO_REDIR_PORTS .. ")")
|
||||
o:value("disable", translate("No patterns are used"))
|
||||
o:value("1:65535", translate("All"))
|
||||
o:depends("mode", "1")
|
||||
o.validate = port_validate
|
||||
|
||||
o = s:taboption("Main", HideValue, "_hide_node_option", "")
|
||||
o:depends("mode", "0")
|
||||
o:depends({ tcp_no_redir_ports = "1:65535", udp_no_redir_ports = "1:65535" })
|
||||
if TCP_NO_REDIR_PORTS == "1:65535" and UDP_NO_REDIR_PORTS == "1:65535" then
|
||||
o:depends({ tcp_no_redir_ports = "", udp_no_redir_ports = "" })
|
||||
end
|
||||
|
||||
o = s:taboption("Main", Flag, "use_global_config", translatef("Use global config"))
|
||||
o.default = "0"
|
||||
o.rmempty = false
|
||||
o:depends({ _hide_node_option = "1", ['!reverse'] = true })
|
||||
|
||||
o = s:taboption("Main", ListValue, "node", "<a style='color: red'>" .. translate("Proxy Node") .. "</a>")
|
||||
o.group = {}
|
||||
o:depends({ _hide_node_option = false, use_global_config = false })
|
||||
o.template = m:template_path("/cbi/nodes_listvalue")
|
||||
o.description = translate("Use global configuration when using the global node.")
|
||||
|
||||
current_node_id = m:get(arg[1], "node")
|
||||
local node_value = s.fields["node"]:formvalue(s.section)
|
||||
if node_value then
|
||||
current_node_id = node_value
|
||||
end
|
||||
current_node = current_node_id and m:get(current_node_id) or {}
|
||||
|
||||
o = s:taboption("Main", HideValue, "node_save_before", "")
|
||||
o.value = current_node[".name"]
|
||||
o.cbid = function(self, section) return "node_save_before" end
|
||||
|
||||
o = s:taboption("Main", HideValue, "_acl_node_bool", "")
|
||||
o:depends({ node = "", ['!reverse'] = true })
|
||||
|
||||
o = s:taboption("Main", HideValue, "_diff_global_node", "")
|
||||
if NODE ~= "" then
|
||||
o:depends({ node = NODE, ['!reverse'] = true })
|
||||
else
|
||||
o:depends({ _acl_node_bool = "1" })
|
||||
end
|
||||
|
||||
---- TCP Proxy Drop Ports
|
||||
local TCP_PROXY_DROP_PORTS = m:get("@global_forwarding[0]", "tcp_proxy_drop_ports")
|
||||
o = s:taboption("Proxy", Value, "tcp_proxy_drop_ports", translate("TCP Proxy Drop Ports"))
|
||||
o:value("", translate("Use global config") .. "(" .. TCP_PROXY_DROP_PORTS .. ")")
|
||||
o:value("disable", translate("No patterns are used"))
|
||||
o.validate = port_validate
|
||||
o:depends({ use_global_config = true })
|
||||
o:depends({ _acl_node_bool = "1" })
|
||||
|
||||
---- UDP Proxy Drop Ports
|
||||
local UDP_PROXY_DROP_PORTS = m:get("@global_forwarding[0]", "udp_proxy_drop_ports")
|
||||
o = s:taboption("Proxy", Value, "udp_proxy_drop_ports", translate("UDP Proxy Drop Ports"))
|
||||
o:value("", translate("Use global config") .. "(" .. UDP_PROXY_DROP_PORTS .. ")")
|
||||
o:value("disable", translate("No patterns are used"))
|
||||
o:value("443", translate("QUIC"))
|
||||
o.validate = port_validate
|
||||
o:depends({ use_global_config = true })
|
||||
o:depends({ _acl_node_bool = "1" })
|
||||
|
||||
---- TCP Redir Ports
|
||||
local TCP_REDIR_PORTS = m:get("@global_forwarding[0]", "tcp_redir_ports")
|
||||
o = s:taboption("Proxy", Value, "tcp_redir_ports", translate("TCP Redir Ports"), translatef("Only work with using the %s node.", "TCP"))
|
||||
o:value("", translate("Use global config") .. "(" .. TCP_REDIR_PORTS .. ")")
|
||||
o:value("1:65535", translate("All"))
|
||||
o:value("80,443", "80,443")
|
||||
o:value("80:65535", "80 " .. translate("or more"))
|
||||
o:value("1:443", "443 " .. translate("or less"))
|
||||
o.validate = port_validate
|
||||
o:depends({ use_global_config = true })
|
||||
o:depends({ _acl_node_bool = "1" })
|
||||
|
||||
---- UDP Redir Ports
|
||||
local UDP_REDIR_PORTS = m:get("@global_forwarding[0]", "udp_redir_ports")
|
||||
o = s:taboption("Proxy", Value, "udp_redir_ports", translate("UDP Redir Ports"), translatef("Only work with using the %s node.", "UDP"))
|
||||
o:value("", translate("Use global config") .. "(" .. UDP_REDIR_PORTS .. ")")
|
||||
o:value("1:65535", translate("All"))
|
||||
o:value("53", "53")
|
||||
o.validate = port_validate
|
||||
o:depends({ use_global_config = true })
|
||||
o:depends({ _acl_node_bool = "1" })
|
||||
|
||||
o = s:taboption("Proxy", DummyValue, "tips", " ")
|
||||
o.rawhtml = true
|
||||
o.cfgvalue = function(t, n)
|
||||
return string.format('<font color="red">%s</font>',
|
||||
translate("The port settings support single ports and ranges.<br>Separate multiple ports with commas (,).<br>Example: 21,80,443,1000:2000."))
|
||||
end
|
||||
o:depends({ use_global_config = true })
|
||||
o:depends({ _acl_node_bool = "1" })
|
||||
|
||||
o = s:taboption("Proxy", Flag, "use_direct_list", translatef("Use %s", translate("Direct List")))
|
||||
o.default = "1"
|
||||
o:depends({ _acl_node_bool = "1", _diff_global_node = "1" })
|
||||
|
||||
o = s:taboption("Proxy", Flag, "use_proxy_list", translatef("Use %s", translate("Proxy List")))
|
||||
o.default = "1"
|
||||
o:depends({ _acl_node_bool = "1", _diff_global_node = "1" })
|
||||
|
||||
o = s:taboption("Proxy", Flag, "use_block_list", translatef("Use %s", translate("Block List")))
|
||||
o.default = "1"
|
||||
o:depends({ _acl_node_bool = "1", _diff_global_node = "1" })
|
||||
|
||||
if has_gfwlist then
|
||||
o = s:taboption("Proxy", Flag, "use_gfw_list", translatef("Use %s", translate("GFW List")))
|
||||
o.default = "1"
|
||||
o:depends({ _acl_node_bool = "1", _diff_global_node = "1" })
|
||||
end
|
||||
|
||||
if has_chnlist or has_chnroute then
|
||||
o = s:taboption("Proxy", ListValue, "chn_list", translate("China List"))
|
||||
o:value("0", translate("Close(Not use)"))
|
||||
o:value("direct", translate("Direct Connection"))
|
||||
o:value("proxy", translate("Proxy"))
|
||||
o.default = "direct"
|
||||
o:depends({ _acl_node_bool = "1", _diff_global_node = "1" })
|
||||
end
|
||||
|
||||
o = s:taboption("Proxy", ListValue, "tcp_proxy_mode", "TCP " .. translate("Proxy Mode"))
|
||||
o:value("disable", translate("No Proxy"))
|
||||
o:value("proxy", translate("Proxy"))
|
||||
o:depends({ _acl_node_bool = "1", _diff_global_node = "1" })
|
||||
|
||||
o = s:taboption("Proxy", ListValue, "udp_proxy_mode", "UDP " .. translate("Proxy Mode"))
|
||||
o:value("disable", translate("No Proxy"))
|
||||
o:value("proxy", translate("Proxy"))
|
||||
o:depends({ _acl_node_bool = "1", _diff_global_node = "1" })
|
||||
|
||||
o = s:taboption("Proxy", DummyValue, "switch_mode", "")
|
||||
o.template = m:template_path("/global/proxy")
|
||||
o:depends({ _acl_node_bool = "1", _diff_global_node = "1" })
|
||||
|
||||
-- Node → DNS Depends Settings
|
||||
o = s:taboption("Main", HideValue, "_node_sel_shunt", "")
|
||||
o:depends({ node = "__always__" })
|
||||
|
||||
o = s:taboption("Main", HideValue, "_node_sel_other", "")
|
||||
o:depends({ _node_sel_shunt = "1", ['!reverse'] = true })
|
||||
|
||||
---- DNS
|
||||
o = s:taboption("DNS", ListValue, "dns_shunt", "DNS " .. translate("Shunt"))
|
||||
o.default = "chinadns-ng"
|
||||
o:value("dnsmasq", "Dnsmasq")
|
||||
o:value("chinadns-ng", translate("ChinaDNS-NG (recommended)"))
|
||||
o:depends({ _acl_node_bool = "1", _diff_global_node = "1" })
|
||||
|
||||
o = s:taboption("DNS", Flag, "filter_proxy_ipv6", translate("Filter Proxy Host IPv6"), translate("Experimental feature."))
|
||||
o.default = "0"
|
||||
o:depends({ _acl_node_bool = "1", _diff_global_node = "1" })
|
||||
|
||||
---- DNS Forward Mode
|
||||
o = s:taboption("DNS", ListValue, "dns_mode", translate("Filter Mode"))
|
||||
if api.is_finded("dns2socks") then
|
||||
o:value("dns2socks", "dns2socks")
|
||||
end
|
||||
if has_singbox then
|
||||
o:value("sing-box", "Sing-Box")
|
||||
end
|
||||
if has_xray then
|
||||
o:value("xray", "Xray")
|
||||
end
|
||||
o:depends({ _acl_node_bool = "1", _node_sel_other = "1", _diff_global_node = "1" })
|
||||
o.write = function(self, section, value)
|
||||
if value == "dns2socks" then
|
||||
m:del(section, "v2ray_dns_mode")
|
||||
end
|
||||
return ListValue.write(self, section, value)
|
||||
end
|
||||
o.remove = function(self, section)
|
||||
local f = s.fields["node"]
|
||||
local id_val = f and f:formvalue(section) or ""
|
||||
if id_val == "" then
|
||||
return m:del(section, self.option)
|
||||
end
|
||||
for _, v in pairs(nodes_table) do
|
||||
if v.id == id_val then
|
||||
local new_val = (v.type == "Xray") and "xray" or "sing-box"
|
||||
m:set(section, self.option, new_val)
|
||||
|
||||
local dns_field = s.fields[v.type == "Xray" and "xray_dns_mode" or "singbox_dns_mode"]
|
||||
local v2ray_dns_mode = dns_field and dns_field:formvalue(section)
|
||||
if v2ray_dns_mode then
|
||||
m:set(section, "v2ray_dns_mode", v2ray_dns_mode)
|
||||
end
|
||||
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
o = s:taboption("DNS", ListValue, "xray_dns_mode", translate("Request protocol"))
|
||||
o.default = "tcp"
|
||||
o:value("tcp", "TCP")
|
||||
o:value("udp", "UDP")
|
||||
o:value("doh", "DoH")
|
||||
o:depends("dns_mode", "xray")
|
||||
o.cfgvalue = function(self, section)
|
||||
return m:get(section, "v2ray_dns_mode")
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
if s.fields["dns_mode"]:formvalue(section) == "xray" then
|
||||
return m:set(section, "v2ray_dns_mode", value)
|
||||
end
|
||||
end
|
||||
|
||||
o = s:taboption("DNS", ListValue, "singbox_dns_mode", translate("Request protocol"))
|
||||
o.default = "tcp"
|
||||
o:value("tcp", "TCP")
|
||||
o:value("udp", "UDP")
|
||||
o:value("doh", "DoH")
|
||||
o:value("http3", "HTTP3(DoH3)")
|
||||
o:value("tls", "TLS(DoT)")
|
||||
o:value("quic", "QUIC(DoQ)")
|
||||
o:depends("dns_mode", "sing-box")
|
||||
o.cfgvalue = function(self, section)
|
||||
return m:get(section, "v2ray_dns_mode")
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
if s.fields["dns_mode"]:formvalue(section) == "sing-box" then
|
||||
return m:set(section, "v2ray_dns_mode", value)
|
||||
end
|
||||
end
|
||||
|
||||
---- DNS Forward
|
||||
o = s:taboption("DNS", Value, "remote_dns", translate("Remote DNS"))
|
||||
o.datatype = "or(ipaddr,ipaddrport(1))"
|
||||
o.default = "1.1.1.1"
|
||||
o:value("1.1.1.1", "1.1.1.1 (CloudFlare)")
|
||||
o:value("1.1.1.2", "1.1.1.2 (CloudFlare-Security)")
|
||||
o:value("8.8.4.4", "8.8.4.4 (Google)")
|
||||
o:value("8.8.8.8", "8.8.8.8 (Google)")
|
||||
o:value("9.9.9.9", "9.9.9.9 (Quad9-Recommended)")
|
||||
o:value("149.112.112.112", "149.112.112.112 (Quad9-Recommended)")
|
||||
o:value("208.67.220.220", "208.67.220.220 (OpenDNS)")
|
||||
o:value("208.67.222.222", "208.67.222.222 (OpenDNS)")
|
||||
o:depends({dns_mode = "dns2socks"})
|
||||
o:depends({xray_dns_mode = "udp"})
|
||||
o:depends({xray_dns_mode = "tcp"})
|
||||
o:depends({singbox_dns_mode = "udp"})
|
||||
o:depends({singbox_dns_mode = "tcp"})
|
||||
o:depends({singbox_dns_mode = "tls"})
|
||||
o:depends({singbox_dns_mode = "quic"})
|
||||
|
||||
o = s:taboption("DNS", Value, "remote_dns_doh", translate("Remote DNS DoH"))
|
||||
o.description = translate("Format: URL[,IP] (optional IP to map the domain in the URL)")
|
||||
o:value("https://1.1.1.1/dns-query", "1.1.1.1 (CloudFlare)")
|
||||
o:value("https://1.1.1.2/dns-query", "1.1.1.2 (CloudFlare-Security)")
|
||||
o:value("https://8.8.4.4/dns-query", "8.8.4.4 (Google)")
|
||||
o:value("https://8.8.8.8/dns-query", "8.8.8.8 (Google)")
|
||||
o:value("https://9.9.9.9/dns-query", "9.9.9.9 (Quad9)")
|
||||
o:value("https://149.112.112.112/dns-query", "149.112.112.112 (Quad9)")
|
||||
o:value("https://208.67.222.222/dns-query", "208.67.222.222 (OpenDNS)")
|
||||
o:value("https://dns.adguard.com/dns-query,94.140.14.14", "94.140.14.14 (AdGuard)")
|
||||
o:value("https://doh.libredns.gr/dns-query,116.202.176.26", "116.202.176.26 (LibreDNS)")
|
||||
o:value("https://doh.libredns.gr/ads,116.202.176.26", "116.202.176.26 (LibreDNS-NoAds)")
|
||||
o.default = o.keylist[1]
|
||||
o.validate = function(self, value, t)
|
||||
if value ~= "" then
|
||||
value = api.trim(value)
|
||||
local flag = 0
|
||||
local util = require "luci.util"
|
||||
local val = util.split(value, ",")
|
||||
local url = val[1]
|
||||
val[1] = nil
|
||||
for i = 1, #val do
|
||||
local v = val[i]
|
||||
if v then
|
||||
if not api.datatypes.ipmask4(v) then
|
||||
flag = 1
|
||||
end
|
||||
end
|
||||
end
|
||||
if flag == 0 then
|
||||
return value
|
||||
end
|
||||
end
|
||||
return nil, translate("DoH request address") .. " " .. translate("Format must be:") .. " URL,IP"
|
||||
end
|
||||
o:depends({xray_dns_mode = "doh"})
|
||||
o:depends({singbox_dns_mode = "doh"})
|
||||
o:depends({singbox_dns_mode = "http3"})
|
||||
|
||||
o = s:taboption("DNS", Value, "remote_dns_client_ip", translate("EDNS Client Subnet"))
|
||||
o.description = translate("Notify the DNS server when the DNS query is notified, the location of the client (cannot be a private IP address).") .. "<br />" ..
|
||||
translate("This feature requires the DNS server to support the Edns Client Subnet (RFC7871).")
|
||||
o.datatype = "ipaddr"
|
||||
o:depends({dns_mode = "sing-box"})
|
||||
o:depends({dns_mode = "xray"})
|
||||
o:depends({_node_sel_shunt = "1", _diff_global_node = "1"})
|
||||
|
||||
o = s:taboption("DNS", Flag, "remote_fakedns", "FakeDNS", translate("Use FakeDNS work in the domain that proxy."))
|
||||
o.default = "0"
|
||||
o.rmempty = false
|
||||
o:depends({dns_mode = "sing-box"})
|
||||
o:depends({dns_mode = "xray"})
|
||||
o.validate = function(self, value, t)
|
||||
if value and value == "1" then
|
||||
local _dns_mode = s.fields["dns_mode"]:formvalue(t)
|
||||
local _node = s.fields["node"]:formvalue(t)
|
||||
if _dns_mode and _node then
|
||||
if (m:get(_node, "type") or ""):lower() ~= _dns_mode and not _node:find("socks_") then
|
||||
return nil, translatef("Node must be '%s' type to use FakeDNS.", _dns_mode)
|
||||
end
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
o = s:taboption("DNS", Value, "remote_rewrite_ttl", translate("Remote DNS") .. " TTL")
|
||||
o.datatype = "min(1)"
|
||||
o.default = "30"
|
||||
o:depends({dns_mode = "sing-box"})
|
||||
|
||||
o = s:taboption("DNS", ListValue, "chinadns_ng_default_tag", translate("Default DNS"))
|
||||
o.default = "none"
|
||||
o:value("gfw", translate("Remote DNS"))
|
||||
o:value("chn", translate("Direct DNS"))
|
||||
o:value("none", translate("Smart, Do not accept no-ip reply from Direct DNS"))
|
||||
o:value("none_noip", translate("Smart, Accept no-ip reply from Direct DNS"))
|
||||
local desc = "<ul>"
|
||||
.. "<li>" .. translate("When not matching any domain name list:") .. "</li>"
|
||||
.. "<li>" .. translate("Remote DNS: Can avoid more DNS leaks, but some domestic domain names maybe to proxy!") .. "</li>"
|
||||
.. "<li>" .. translate("Direct DNS: Internet experience may be better, but DNS will be leaked!") .. "</li>"
|
||||
o.description = desc
|
||||
.. "<li>" .. translate("Smart: Forward to both direct and remote DNS, if the direct DNS resolution result is a mainland China IP, then use the direct result, otherwise use the remote result.") .. "</li>"
|
||||
.. "<li>" .. translate("In smart mode, no-ip reply from Direct DNS:") .. "</li>"
|
||||
.. "<li>" .. translate("Do not accept: Wait and use Remote DNS Reply.") .. "</li>"
|
||||
.. "<li>" .. translate("Accept: Trust the Reply, using this option can improve DNS resolution speeds for some mainland IPv4-only sites.") .. "</li>"
|
||||
.. "</ul>"
|
||||
o:depends({dns_shunt = "chinadns-ng", tcp_proxy_mode = "proxy", chn_list = "direct"})
|
||||
|
||||
o = s:taboption("DNS", Flag, "force_https_soa", translate("Force HTTPS SOA"), translate("Force queries with qtype 65 to respond with an SOA record."))
|
||||
o.default = "0"
|
||||
o.rmempty = false
|
||||
o:depends({dns_shunt = "chinadns-ng"})
|
||||
|
||||
o = s:taboption("DNS", ListValue, "use_default_dns", translate("Default DNS"))
|
||||
o.default = "direct"
|
||||
o:value("remote", translate("Remote DNS"))
|
||||
o:value("direct", translate("Direct DNS"))
|
||||
o.description = desc .. "</ul>"
|
||||
o:depends({dns_shunt = "dnsmasq", tcp_proxy_mode = "proxy", chn_list = "direct"})
|
||||
|
||||
---- Log
|
||||
o = s:taboption("Log", Flag, "log", translate("Enable Node Log"))
|
||||
o.default = 0
|
||||
o.rmempty = false
|
||||
o:depends({ _acl_node_bool = "1", _diff_global_node = "1" })
|
||||
|
||||
o = s:taboption("Log", ListValue, "loglevel", translate("Log Level"))
|
||||
o.default = "warn"
|
||||
o:value("debug", "Debug")
|
||||
o:value("info", "Info")
|
||||
o:value("warn", "Warning")
|
||||
o:value("error", "Error")
|
||||
o:depends("log", "1")
|
||||
|
||||
o = s:taboption("Log", DummyValue, "_node_log", translate("Log File"))
|
||||
o.rawhtml = true
|
||||
o.cfgvalue = function(t, n)
|
||||
local log_file = api.TMP_PATH .. "/acl/" .. arg[1] .. "/node.log"
|
||||
local log_url = api.url("get_redir_log") .. "?id=" .. arg[1]
|
||||
local s = "<code>%s</code> " % log_file
|
||||
if api.fs.access(log_file) then
|
||||
local btn = string.format(
|
||||
'<input class="btn cbi-button cbi-button-apply" type="button" value="%s" onclick="window.open(\'%s\', \'_blank\')" />',
|
||||
translate("View Log"),
|
||||
log_url
|
||||
)
|
||||
s = s .. btn
|
||||
end
|
||||
return s
|
||||
end
|
||||
o:depends("log", "1")
|
||||
|
||||
o = s:taboption("Log", Flag, "log_chinadns_ng", translate("Enable") .. " ChinaDNS-NG " .. translate("Log"))
|
||||
o.default = "0"
|
||||
o.rmempty = false
|
||||
o:depends("dns_shunt", "chinadns-ng")
|
||||
|
||||
o = s:taboption("Log", DummyValue, "_chinadns_ng_log", translate("Log File"))
|
||||
o.rawhtml = true
|
||||
o.cfgvalue = function(t, n)
|
||||
local log_file = api.TMP_PATH .. "/acl/" .. arg[1] .. "/chinadns_ng.log"
|
||||
local log_url = api.url("get_chinadns_log") .. "?flag=" .. arg[1]
|
||||
local s = "<code>%s</code> " % log_file
|
||||
if api.fs.access(log_file) then
|
||||
local btn = string.format(
|
||||
'<input class="btn cbi-button cbi-button-apply" type="button" value="%s" onclick="window.open(\'%s\', \'_blank\')" />',
|
||||
translate("View Log"),
|
||||
log_url
|
||||
)
|
||||
s = s .. btn
|
||||
end
|
||||
return s
|
||||
end
|
||||
o:depends("log_chinadns_ng", "1")
|
||||
|
||||
local o_node = s.fields["node"]
|
||||
local shunt_list = {}
|
||||
|
||||
for k, v in pairs(socks_list) do
|
||||
o_node:value(v.id, v["remark"])
|
||||
o_node.group[#o_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
end
|
||||
for k, v in pairs(nodes_table) do
|
||||
if #normal_list == 0 then
|
||||
s.fields["dns_mode"]:depends({ _acl_node_bool = "1", _diff_global_node = "1" })
|
||||
break
|
||||
end
|
||||
if v.protocol and v.protocol == "_shunt" then
|
||||
if v.type == "Xray" and has_xray then
|
||||
o_node:value(v.id, v["remark"])
|
||||
o_node.group[#o_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
|
||||
s.fields["xray_dns_mode"]:depends({ _acl_node_bool = "1", _diff_global_node = "1", node = v.id })
|
||||
s.fields["_node_sel_shunt"]:depends({ node = v.id })
|
||||
end
|
||||
if v.type == "sing-box" and has_singbox then
|
||||
o_node:value(v.id, v["remark"])
|
||||
o_node.group[#o_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
|
||||
s.fields["singbox_dns_mode"]:depends({ _acl_node_bool = "1", _diff_global_node = "1", node = v.id })
|
||||
s.fields["_node_sel_shunt"]:depends({ node = v.id })
|
||||
s.fields["remote_rewrite_ttl"]:depends({ _acl_node_bool = "1", _diff_global_node = "1", node = v.id })
|
||||
end
|
||||
shunt_list[#shunt_list + 1] = v
|
||||
else
|
||||
o_node:value(v.id, v["remark"])
|
||||
o_node.group[#o_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
end
|
||||
end
|
||||
|
||||
-- Shunt
|
||||
if current_node.protocol == "_shunt" and current_node[".name"] ~= NODE then
|
||||
local shunt_lua = loadfile("/usr/lib/lua/luci/model/cbi/passwall/client/include/shunt_options.lua")
|
||||
setfenv(shunt_lua, getfenv(1))(m, s, {
|
||||
node = current_node,
|
||||
verify_option = s.fields["node"],
|
||||
tab = "Shunt",
|
||||
tab_desc = translate("Shunt Rule"),
|
||||
})
|
||||
end
|
||||
|
||||
m:appendTemplate("/include/node_change", { verify_option = s.fields["node"], shunt_list = api.jsonc.stringify(shunt_list) })
|
||||
|
||||
return api.return_map(m)
|
||||
@@ -1,36 +0,0 @@
|
||||
local api = require "luci.passwall.api"
|
||||
|
||||
api.set_default_cbi()
|
||||
|
||||
local com = require "luci.passwall.com"
|
||||
|
||||
m = Map()
|
||||
|
||||
-- [[ App Settings ]]--
|
||||
s = m:section(NamedSection, "@global_app[0]", "global_app", translate("App Update"))
|
||||
|
||||
s:appendTemplate("/app_update/app_version", {com = com})
|
||||
|
||||
o = s:option(Flag, "github_proxy", translate("GitHub Proxy"), translate("Use gh-proxy instead of proxy nodes for component updates."))
|
||||
o.default = 0
|
||||
|
||||
local k, v
|
||||
for _, k in ipairs(com.order) do
|
||||
v = com[k]
|
||||
if k ~= "chinadns-ng" then
|
||||
o = s:option(Value, k:gsub("%-","_") .. "_file", translatef("%s App Path", v.name))
|
||||
o.default = v.default_path or ("/usr/bin/" .. k)
|
||||
o.placeholder = o.default
|
||||
function o:remove(section)
|
||||
self:write(section, self.default)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
o = s:option(DummyValue, "tips", " ")
|
||||
o.rawhtml = true
|
||||
o.cfgvalue = function(t, n)
|
||||
return string.format('<font color="red">%s</font>', translate("if you want to run from memory, change the path, /tmp beginning then save the application and update it manually."))
|
||||
end
|
||||
|
||||
return api.return_map(m)
|
||||
@@ -1,813 +0,0 @@
|
||||
api = require "luci.passwall.api"
|
||||
datatypes = api.datatypes
|
||||
local fs = api.fs
|
||||
has_singbox = api.finded_com("sing-box")
|
||||
has_xray = api.finded_com("xray")
|
||||
local has_gfwlist = fs.access("/usr/share/passwall/rules/gfwlist")
|
||||
local has_chnlist = fs.access("/usr/share/passwall/rules/chnlist")
|
||||
local has_chnroute = fs.access("/usr/share/passwall/rules/chnroute")
|
||||
|
||||
api.set_default_cbi()
|
||||
|
||||
m = Map()
|
||||
|
||||
m:appendTemplate("/cbi/nodes_listvalue_com")
|
||||
|
||||
local nodes_table = {}
|
||||
for _, e in ipairs(api.get_valid_nodes()) do
|
||||
nodes_table[#nodes_table + 1] = e
|
||||
end
|
||||
|
||||
local normal_list = {}
|
||||
local balancing_list = {}
|
||||
local urltest_list = {}
|
||||
local shunt_list = {}
|
||||
local iface_list = {}
|
||||
for _, v in pairs(nodes_table) do
|
||||
if v.node_type == "normal" then
|
||||
normal_list[#normal_list + 1] = v
|
||||
end
|
||||
if v.protocol and v.protocol == "_balancing" then
|
||||
balancing_list[#balancing_list + 1] = v
|
||||
end
|
||||
if v.protocol and v.protocol == "_urltest" then
|
||||
urltest_list[#urltest_list + 1] = v
|
||||
end
|
||||
if v.protocol and v.protocol == "_shunt" then
|
||||
shunt_list[#shunt_list + 1] = v
|
||||
end
|
||||
if v.protocol and v.protocol == "_iface" then
|
||||
iface_list[#iface_list + 1] = v
|
||||
end
|
||||
end
|
||||
|
||||
local socks_list = {}
|
||||
|
||||
local socks_server = "127.0.0.1" .. ":" .. (m:get("@global[0]", "node_socks_port") or "1070")
|
||||
local socks_table = {}
|
||||
socks_table[#socks_table + 1] = {
|
||||
id = socks_server,
|
||||
remark = socks_server .. " - " .. translate("Proxy Node")
|
||||
}
|
||||
m:foreach("socks", function(s)
|
||||
if s.enabled == "1" and s.node then
|
||||
local id, remark
|
||||
for k, n in pairs(nodes_table) do
|
||||
if (s.node == n.id) then
|
||||
remark = n["remark"]; break
|
||||
end
|
||||
end
|
||||
id = "127.0.0.1" .. ":" .. s.port
|
||||
socks_table[#socks_table + 1] = {
|
||||
id = id,
|
||||
remark = id .. " - " .. (remark or translate("Misconfigured"))
|
||||
}
|
||||
if has_singbox or has_xray then
|
||||
socks_list[#socks_list + 1] = {
|
||||
id = s[".name"],
|
||||
remark = translate("Socks Config") .. " " .. string.format("[%s %s]", s.port, translate("Port")),
|
||||
group = "Socks"
|
||||
}
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
local doh_validate = function(self, value, t)
|
||||
value = value:gsub("%s+", "")
|
||||
if value ~= "" then
|
||||
local flag = 0
|
||||
local util = require "luci.util"
|
||||
local val = util.split(value, ",")
|
||||
local url = val[1]
|
||||
val[1] = nil
|
||||
for i = 1, #val do
|
||||
local v = val[i]
|
||||
if v then
|
||||
if not datatypes.ipmask4(v) and not datatypes.ipmask6(v) then
|
||||
flag = 1
|
||||
end
|
||||
end
|
||||
end
|
||||
if flag == 0 then
|
||||
return value
|
||||
end
|
||||
end
|
||||
return nil, translatef("%s request address","DoH") .. " " .. translate("Format must be:") .. " URL,IP"
|
||||
end
|
||||
|
||||
m:appendTemplate("/global/status")
|
||||
|
||||
s = m:section(NamedSection, "@global[0]", "global")
|
||||
|
||||
s:tab("Main", translate("Main"))
|
||||
|
||||
-- [[ Global Settings ]]--
|
||||
o = s:taboption("Main", Flag, "enabled", translate("Main switch"))
|
||||
o.rmempty = false
|
||||
|
||||
---- Node
|
||||
o = s:taboption("Main", ListValue, "node", "<a style='color: red'>" .. translate("Proxy Node") .. "</a>")
|
||||
o.template = m:template_path("/cbi/nodes_listvalue")
|
||||
o:value("", translate("Close"))
|
||||
o.group = {""}
|
||||
|
||||
current_node_id = m:get(s.section, "node")
|
||||
local node_value = s.fields["node"]:formvalue(s.section)
|
||||
if node_value then
|
||||
current_node_id = node_value
|
||||
end
|
||||
current_node = current_node_id and m:get(current_node_id) or {}
|
||||
|
||||
-- Shunt Start
|
||||
if (has_singbox or has_xray) and #nodes_table > 0 then
|
||||
if #normal_list > 0 or #iface_list > 0 then
|
||||
if current_node.protocol == "_shunt" then
|
||||
local shunt_lua = loadfile("/usr/lib/lua/luci/model/cbi/passwall/client/include/shunt_options.lua")
|
||||
setfenv(shunt_lua, getfenv(1))(m, s, {
|
||||
node = current_node,
|
||||
verify_option = s.fields["node"],
|
||||
tab = "Shunt",
|
||||
tab_desc = translate("Shunt Rule"),
|
||||
})
|
||||
end
|
||||
else
|
||||
local tips = s:taboption("Main", DummyValue, "tips", " ")
|
||||
tips.rawhtml = true
|
||||
tips.cfgvalue = function(t, n)
|
||||
return string.format('<a style="color: red">%s</a>', translate("There are no available nodes, please add or subscribe nodes first."))
|
||||
end
|
||||
tips:depends("_node", "1")
|
||||
for k, v in pairs(shunt_list) do
|
||||
tips:depends("node", v.id)
|
||||
end
|
||||
for k, v in pairs(balancing_list) do
|
||||
tips:depends("node", v.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
o = s:taboption("Main", Value, "node_socks_port", translate("Node") .. " Socks " .. translate("Listen Port"))
|
||||
o.default = 1070
|
||||
o.placeholder = 1070
|
||||
o.datatype = "range(1,65535)"
|
||||
o:depends("_node", "1")
|
||||
--[[
|
||||
if has_singbox or has_xray then
|
||||
o = s:taboption("Main", Value, "node_http_port", translate("Node") .. " HTTP " .. translate("Listen Port") .. " " .. translate("0 is not use"))
|
||||
o.default = 0
|
||||
o.datatype = "port"
|
||||
end
|
||||
]]--
|
||||
o = s:taboption("Main", Flag, "node_socks_bind_local", translate("Node") .. " Socks " .. translate("Bind Local"), translate("When selected, it can only be accessed localhost."))
|
||||
o.default = "1"
|
||||
o:depends("_node", "1")
|
||||
|
||||
o = s:taboption("Main", HideValue, "node_save_before", "")
|
||||
o.value = current_node[".name"]
|
||||
o.cbid = function(self, section) return "node_save_before" end
|
||||
|
||||
o = s:taboption("Main", HideValue, "_node", "")
|
||||
o:depends({ node = "", ['!reverse'] = true })
|
||||
|
||||
-- Node → DNS Depends Settings
|
||||
o = s:taboption("Main", HideValue, "_node_sel_shunt", "")
|
||||
o:depends({ node = "__always__" })
|
||||
|
||||
o = s:taboption("Main", HideValue, "_node_sel_other", "")
|
||||
o:depends({ _node_sel_shunt = "1", ['!reverse'] = true })
|
||||
|
||||
-- [[ DNS Settings ]]--
|
||||
s:tab("DNS", translate("DNS"))
|
||||
|
||||
o = s:taboption("DNS", ListValue, "dns_shunt", "DNS " .. translate("Shunt"))
|
||||
o:value("dnsmasq", "Dnsmasq")
|
||||
o:value("chinadns-ng", translate("ChinaDNS-NG (recommended)"))
|
||||
if api.is_finded("smartdns") then
|
||||
o:value("smartdns", "SmartDNS")
|
||||
o.write = function(self, section, value)
|
||||
if value ~= "smartdns" then
|
||||
m:del(section, "group_domestic")
|
||||
end
|
||||
return ListValue.write(self, section, value)
|
||||
end
|
||||
|
||||
o = s:taboption("DNS", Value, "group_domestic", translate("Domestic group name"))
|
||||
o.placeholder = "local"
|
||||
o.rmempty = false
|
||||
o:depends("dns_shunt", "smartdns")
|
||||
o.description = translate("You only need to configure domestic DNS packets in SmartDNS, and fill in the domestic DNS group name here.")
|
||||
o.validate = function(self, value, section)
|
||||
value = api.trim(value)
|
||||
if value == "" then
|
||||
return nil, translatef("%s cannot be empty.", "SmartDNS " .. translate("Domestic group name"))
|
||||
end
|
||||
return value
|
||||
end
|
||||
end
|
||||
|
||||
o = s:taboption("DNS", ListValue, "direct_dns_mode", translate("Direct DNS") .. " " .. translate("Request protocol"))
|
||||
o:value("", translate("Auto"))
|
||||
o:value("udp", translatef("Requery DNS By %s", "UDP"))
|
||||
o:value("tcp", translatef("Requery DNS By %s", "TCP"))
|
||||
o:depends({dns_shunt = "dnsmasq"})
|
||||
o:depends({dns_shunt = "chinadns-ng"})
|
||||
|
||||
o = s:taboption("DNS", Value, "direct_dns", translate("Direct DNS"))
|
||||
o.datatype = "or(ipaddr,ipaddrport(1))"
|
||||
o.default = "223.5.5.5"
|
||||
o:value("223.5.5.5")
|
||||
o:value("223.6.6.6")
|
||||
o:value("180.184.1.1")
|
||||
o:value("180.184.2.2")
|
||||
o:value("114.114.114.114")
|
||||
o:value("114.114.115.115")
|
||||
o:value("119.28.28.28")
|
||||
o:depends("direct_dns_mode", "udp")
|
||||
o:depends("direct_dns_mode", "tcp")
|
||||
|
||||
o = s:taboption("DNS", Flag, "filter_proxy_ipv6", translate("Filter Proxy Host IPv6"))
|
||||
o.default = "0"
|
||||
|
||||
-- 分流时dns过滤模式保存逻辑
|
||||
function dns_mode_save(section)
|
||||
local f = s.fields["node"]
|
||||
local id_val = f and f:formvalue(section) or ""
|
||||
if id_val == "" then
|
||||
return
|
||||
end
|
||||
for _, v in pairs(shunt_list) do
|
||||
if v.id == id_val then
|
||||
local type_val = v.type
|
||||
if type_val and (type_val == "Xray" or type_val == "sing-box") then
|
||||
local dns_shunt_val = s.fields["dns_shunt"]:formvalue(section)
|
||||
local dns_mode_val = (dns_shunt_val ~= "smartdns") and "dns_mode" or "smartdns_dns_mode"
|
||||
local current_val = m:get(section, dns_mode_val) or ""
|
||||
local new_val = (type_val == "Xray") and "xray" or "sing-box"
|
||||
|
||||
if current_val ~= new_val then
|
||||
m:set(section, dns_mode_val, new_val)
|
||||
m:del(section, (dns_mode_val == "dns_mode") and "smartdns_dns_mode" or "dns_mode")
|
||||
end
|
||||
|
||||
local dns_field = s.fields[type_val == "Xray" and "xray_dns_mode" or "singbox_dns_mode"]
|
||||
local v2ray_dns_mode = dns_field and dns_field:formvalue(section)
|
||||
if v2ray_dns_mode and m:get(section, "v2ray_dns_mode") ~= v2ray_dns_mode then
|
||||
m:set(section, "v2ray_dns_mode", v2ray_dns_mode)
|
||||
end
|
||||
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---- DNS Forward Mode
|
||||
o = s:taboption("DNS", ListValue, "dns_mode", translate("Filter Mode"))
|
||||
o.default = "tcp"
|
||||
o:value("udp", translatef("Requery DNS By %s", "UDP"))
|
||||
o:value("tcp", translatef("Requery DNS By %s", "TCP"))
|
||||
if api.is_finded("dns2socks") then
|
||||
o:value("dns2socks", "dns2socks")
|
||||
end
|
||||
if has_singbox then
|
||||
o:value("sing-box", "Sing-Box")
|
||||
end
|
||||
if has_xray then
|
||||
o:value("xray", "Xray")
|
||||
end
|
||||
o:depends({ dns_shunt = "chinadns-ng", _node_sel_other = "1" })
|
||||
o:depends({ dns_shunt = "dnsmasq", _node_sel_other = "1" })
|
||||
o.write = function(self, section, value)
|
||||
if value ~= "sing-box" and value ~= "xray" then
|
||||
m:del(section, "v2ray_dns_mode")
|
||||
end
|
||||
return ListValue.write(self, section, value)
|
||||
end
|
||||
o.remove = function(self, section)
|
||||
local f = s.fields["smartdns_dns_mode"]
|
||||
if f and f:formvalue(section) then
|
||||
return m:del(section, self.option)
|
||||
end
|
||||
dns_mode_save(section)
|
||||
end
|
||||
|
||||
---- SmartDNS Forward Mode
|
||||
if api.is_finded("smartdns") then
|
||||
o = s:taboption("DNS", ListValue, "smartdns_dns_mode", translate("Filter Mode"))
|
||||
o:value("socks", "Socks")
|
||||
if has_singbox then
|
||||
o:value("sing-box", "Sing-Box")
|
||||
end
|
||||
if has_xray then
|
||||
o:value("xray", "Xray")
|
||||
end
|
||||
o:depends({ dns_shunt = "smartdns", _node_sel_other = "1" })
|
||||
o.write = function(self, section, value)
|
||||
if value == "socks" then
|
||||
m:del(section, "v2ray_dns_mode")
|
||||
end
|
||||
return ListValue.write(self, section, value)
|
||||
end
|
||||
o.remove = function(self, section)
|
||||
local f = s.fields["dns_mode"]
|
||||
if f and f:formvalue(section) then
|
||||
return m:del(section, self.option)
|
||||
end
|
||||
dns_mode_save(section)
|
||||
end
|
||||
|
||||
o = s:taboption("DNS", DynamicList, "smartdns_remote_dns", translate("Remote DNS"))
|
||||
o:value("tcp://1.1.1.1")
|
||||
o:value("tcp://8.8.4.4")
|
||||
o:value("tcp://8.8.8.8")
|
||||
o:value("tcp://9.9.9.9")
|
||||
o:value("tcp://208.67.222.222")
|
||||
o:value("tls://1.1.1.1")
|
||||
o:value("tls://8.8.4.4")
|
||||
o:value("tls://8.8.8.8")
|
||||
o:value("tls://9.9.9.9")
|
||||
o:value("tls://208.67.222.222")
|
||||
o:value("https://1.1.1.1/dns-query")
|
||||
o:value("https://8.8.4.4/dns-query")
|
||||
o:value("https://8.8.8.8/dns-query")
|
||||
o:value("https://9.9.9.9/dns-query")
|
||||
o:value("https://208.67.222.222/dns-query")
|
||||
o:value("https://dns.adguard.com/dns-query,94.140.14.14")
|
||||
o:value("https://doh.libredns.gr/dns-query,116.202.176.26")
|
||||
o:value("https://doh.libredns.gr/ads,116.202.176.26")
|
||||
o:depends({ dns_shunt = "smartdns", smartdns_dns_mode = "socks" })
|
||||
o.cfgvalue = function(self, section)
|
||||
return m:get(section, self.option) or {"tcp://1.1.1.1"}
|
||||
end
|
||||
end
|
||||
|
||||
o = s:taboption("DNS", ListValue, "xray_dns_mode", translate("Remote DNS") .. " " .. translate("Request protocol"))
|
||||
o.default = "tcp"
|
||||
o:value("tcp", "TCP")
|
||||
o:value("udp", "UDP")
|
||||
o:value("doh", "DoH")
|
||||
o:depends("dns_mode", "xray")
|
||||
o:depends("smartdns_dns_mode", "xray")
|
||||
o.cfgvalue = function(self, section)
|
||||
return m:get(section, "v2ray_dns_mode")
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
local f = s.fields["smartdns_dns_mode"]
|
||||
if s.fields["dns_mode"]:formvalue(section) == "xray" or (f and f:formvalue(section) == "xray") then
|
||||
return m:set(section, "v2ray_dns_mode", value)
|
||||
end
|
||||
end
|
||||
|
||||
o = s:taboption("DNS", ListValue, "singbox_dns_mode", translate("Remote DNS") .. " " .. translate("Request protocol"))
|
||||
o.default = "tcp"
|
||||
o:value("tcp", "TCP")
|
||||
o:value("udp", "UDP")
|
||||
o:value("doh", "DoH")
|
||||
o:value("http3", "HTTP3(DoH3)")
|
||||
o:value("tls", "TLS(DoT)")
|
||||
o:value("quic", "QUIC(DoQ)")
|
||||
o:depends("dns_mode", "sing-box")
|
||||
o:depends("smartdns_dns_mode", "sing-box")
|
||||
o.cfgvalue = function(self, section)
|
||||
return m:get(section, "v2ray_dns_mode")
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
local f = s.fields["smartdns_dns_mode"]
|
||||
if s.fields["dns_mode"]:formvalue(section) == "sing-box" or (f and f:formvalue(section) == "sing-box") then
|
||||
return m:set(section, "v2ray_dns_mode", value)
|
||||
end
|
||||
end
|
||||
|
||||
o = s:taboption("DNS", Value, "socks_server", translate("Socks Server"), translate("Make sure socks service is available on this address."))
|
||||
for k, v in pairs(socks_table) do o:value(v.id, v.remark) end
|
||||
o.default = socks_table[1].id
|
||||
o.validate = function(self, value, t)
|
||||
if not datatypes.ipaddrport(value) then
|
||||
return nil, translate("Socks Server") .. " " .. translate("Not valid IP format, please re-enter!")
|
||||
end
|
||||
return value
|
||||
end
|
||||
o:depends({dns_mode = "dns2socks"})
|
||||
|
||||
---- DNS Forward
|
||||
o = s:taboption("DNS", Value, "remote_dns", translate("Remote DNS"))
|
||||
o.datatype = "or(ipaddr,ipaddrport(1))"
|
||||
o.default = "1.1.1.1"
|
||||
o:value("1.1.1.1", "1.1.1.1 (CloudFlare)")
|
||||
o:value("1.1.1.2", "1.1.1.2 (CloudFlare-Security)")
|
||||
o:value("8.8.4.4", "8.8.4.4 (Google)")
|
||||
o:value("8.8.8.8", "8.8.8.8 (Google)")
|
||||
o:value("9.9.9.9", "9.9.9.9 (Quad9)")
|
||||
o:value("149.112.112.112", "149.112.112.112 (Quad9)")
|
||||
o:value("208.67.220.220", "208.67.220.220 (OpenDNS)")
|
||||
o:value("208.67.222.222", "208.67.222.222 (OpenDNS)")
|
||||
o:depends({dns_mode = "dns2socks"})
|
||||
o:depends({dns_mode = "tcp"})
|
||||
o:depends({dns_mode = "udp"})
|
||||
o:depends({xray_dns_mode = "udp"})
|
||||
o:depends({xray_dns_mode = "tcp"})
|
||||
o:depends({singbox_dns_mode = "udp"})
|
||||
o:depends({singbox_dns_mode = "tcp"})
|
||||
o:depends({singbox_dns_mode = "tls"})
|
||||
o:depends({singbox_dns_mode = "quic"})
|
||||
|
||||
---- DoH
|
||||
o = s:taboption("DNS", Value, "remote_dns_doh", translate("Remote DNS DoH"))
|
||||
o.description = translate("Format: URL[,IP] (optional IP to map the domain in the URL)")
|
||||
o.default = o.keylist[1]
|
||||
o:value("https://1.1.1.1/dns-query", "1.1.1.1 (CloudFlare)")
|
||||
o:value("https://1.1.1.2/dns-query", "1.1.1.2 (CloudFlare-Security)")
|
||||
o:value("https://8.8.4.4/dns-query", "8.8.4.4 (Google)")
|
||||
o:value("https://8.8.8.8/dns-query", "8.8.8.8 (Google)")
|
||||
o:value("https://9.9.9.9/dns-query", "9.9.9.9 (Quad9)")
|
||||
o:value("https://149.112.112.112/dns-query", "149.112.112.112 (Quad9)")
|
||||
o:value("https://208.67.222.222/dns-query", "208.67.222.222 (OpenDNS)")
|
||||
o:value("https://dns.adguard.com/dns-query,94.140.14.14", "94.140.14.14 (AdGuard)")
|
||||
o:value("https://doh.libredns.gr/dns-query,116.202.176.26", "116.202.176.26 (LibreDNS)")
|
||||
o:value("https://doh.libredns.gr/ads,116.202.176.26", "116.202.176.26 (LibreDNS-NoAds)")
|
||||
o.validate = doh_validate
|
||||
o:depends({xray_dns_mode = "doh"})
|
||||
o:depends({singbox_dns_mode = "doh"})
|
||||
o:depends({singbox_dns_mode = "http3"})
|
||||
|
||||
o = s:taboption("DNS", Value, "remote_dns_client_ip", translate("EDNS Client Subnet"))
|
||||
o.description = translate("Notify the DNS server when the DNS query is notified, the location of the client (cannot be a private IP address).") .. "<br />" ..
|
||||
translate("This feature requires the DNS server to support the Edns Client Subnet (RFC7871).")
|
||||
o.datatype = "ipaddr"
|
||||
o:depends({dns_mode = "sing-box"})
|
||||
o:depends({dns_mode = "xray"})
|
||||
o:depends("dns_shunt", "smartdns")
|
||||
o:depends("_node_sel_shunt", "1")
|
||||
|
||||
o = s:taboption("DNS", Flag, "remote_fakedns", "FakeDNS", translate("Use FakeDNS work in the domain that proxy."))
|
||||
o.default = "0"
|
||||
o:depends({dns_mode = "sing-box", dns_shunt = "dnsmasq"})
|
||||
o:depends({dns_mode = "sing-box", dns_shunt = "chinadns-ng"})
|
||||
o:depends({smartdns_dns_mode = "sing-box", dns_shunt = "smartdns"})
|
||||
o:depends({dns_mode = "xray", dns_shunt = "dnsmasq"})
|
||||
o:depends({dns_mode = "xray", dns_shunt = "chinadns-ng"})
|
||||
o:depends({smartdns_dns_mode = "xray", dns_shunt = "smartdns"})
|
||||
--o:depends("_node_sel_shunt", "1")
|
||||
o.validate = function(self, value, t)
|
||||
if value and value == "1" then
|
||||
local _dns_mode = s.fields["dns_mode"]:formvalue(t)
|
||||
if not _dns_mode and s.fields["smartdns_dns_mode"] then
|
||||
_dns_mode = s.fields["smartdns_dns_mode"]:formvalue(t)
|
||||
end
|
||||
local _node = s.fields["node"]:formvalue(t)
|
||||
if _dns_mode and _node then
|
||||
if (m:get(_node, "type") or ""):lower() ~= _dns_mode and not _node:find("socks_") then
|
||||
return nil, translatef("Node must be '%s' type to use FakeDNS.", _dns_mode)
|
||||
end
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
o = s:taboption("DNS", Value, "remote_rewrite_ttl", translate("Remote DNS") .. " TTL")
|
||||
o.datatype = "min(1)"
|
||||
o.default = "30"
|
||||
o:depends({dns_mode = "sing-box", dns_shunt = "dnsmasq"})
|
||||
o:depends({dns_mode = "sing-box", dns_shunt = "chinadns-ng"})
|
||||
o:depends({smartdns_dns_mode = "sing-box", dns_shunt = "smartdns"})
|
||||
|
||||
o = s:taboption("DNS", ListValue, "chinadns_ng_default_tag", translate("Default DNS"))
|
||||
o.default = "none"
|
||||
o:value("gfw", translate("Remote DNS"))
|
||||
o:value("chn", translate("Direct DNS"))
|
||||
o:value("none", translate("Smart, Do not accept no-ip reply from Direct DNS"))
|
||||
o:value("none_noip", translate("Smart, Accept no-ip reply from Direct DNS"))
|
||||
local desc = "<ul>"
|
||||
.. "<li>" .. translate("When not matching any domain name list:") .. "</li>"
|
||||
.. "<li>" .. translate("Remote DNS: Can avoid more DNS leaks, but some domestic domain names maybe to proxy!") .. "</li>"
|
||||
.. "<li>" .. translate("Direct DNS: Internet experience may be better, but DNS will be leaked!") .. "</li>"
|
||||
o.description = desc
|
||||
.. "<li>" .. translate("Smart: Forward to both direct and remote DNS, if the direct DNS resolution result is a mainland China IP, then use the direct result, otherwise use the remote result.") .. "</li>"
|
||||
.. "<li>" .. translate("In smart mode, no-ip reply from Direct DNS:") .. "</li>"
|
||||
.. "<li>" .. translate("Do not accept: Wait and use Remote DNS Reply.") .. "</li>"
|
||||
.. "<li>" .. translate("Accept: Trust the Reply, using this option can improve DNS resolution speeds for some mainland IPv4-only sites.") .. "</li>"
|
||||
.. "</ul>"
|
||||
o:depends({dns_shunt = "chinadns-ng", tcp_proxy_mode = "proxy", chn_list = "direct"})
|
||||
|
||||
o = s:taboption("DNS", ListValue, "use_default_dns", translate("Default DNS"))
|
||||
o.default = "direct"
|
||||
o:value("remote", translate("Remote DNS"))
|
||||
o:value("direct", translate("Direct DNS"))
|
||||
o.description = desc .. "</ul>"
|
||||
o:depends({dns_shunt = "dnsmasq", tcp_proxy_mode = "proxy", chn_list = "direct"})
|
||||
if api.is_finded("smartdns") then
|
||||
o:depends({dns_shunt = "smartdns", tcp_proxy_mode = "proxy", chn_list = "direct"})
|
||||
end
|
||||
|
||||
o = s:taboption("DNS", Flag, "force_https_soa", translate("Force HTTPS SOA"), translate("Force queries with qtype 65 to respond with an SOA record."))
|
||||
o.default = "0"
|
||||
o:depends({dns_shunt = "chinadns-ng"})
|
||||
if api.is_finded("smartdns") then
|
||||
o:depends({dns_shunt = "smartdns"})
|
||||
end
|
||||
|
||||
o = s:taboption("DNS", Flag, "dns_redirect", translate("DNS Redirect"), translate("Force special DNS server to need proxy devices."))
|
||||
o.default = "1"
|
||||
o.rmempty = false
|
||||
|
||||
local prefer_nft = m:get("@global_forwarding[0]", "prefer_nft") == "1"
|
||||
local set_title = api.i18n.translate(prefer_nft and "Clear NFTSET on Reboot" or "Clear IPSET on Reboot")
|
||||
o = s:taboption("DNS", Flag, "flush_set_on_reboot", set_title, translate("Clear IPSET/NFTSET on service reboot. This may increase reboot time."))
|
||||
o.default = "0"
|
||||
|
||||
set_title = api.i18n.translate(prefer_nft and "Clear NFTSET" or "Clear IPSET")
|
||||
o = s:taboption("DNS", DummyValue, "clear_ipset", set_title, translate("Try this feature if the rule modification does not take effect."))
|
||||
o.rawhtml = true
|
||||
function o.cfgvalue(self, section)
|
||||
return string.format(
|
||||
[[<button type="button" class="cbi-button cbi-button-remove" onclick="location.href='%s'">%s</button>]],
|
||||
api.url("flush_set") .. "?redirect=1&reload=1", set_title)
|
||||
end
|
||||
|
||||
s:tab("Proxy", translate("Mode"))
|
||||
|
||||
o = s:taboption("Proxy", Flag, "use_direct_list", translatef("Use %s", translate("Direct List")))
|
||||
o.default = "1"
|
||||
|
||||
o = s:taboption("Proxy", Flag, "use_proxy_list", translatef("Use %s", translate("Proxy List")))
|
||||
o.default = "1"
|
||||
|
||||
o = s:taboption("Proxy", Flag, "use_block_list", translatef("Use %s", translate("Block List")))
|
||||
o.default = "1"
|
||||
|
||||
if has_gfwlist then
|
||||
o = s:taboption("Proxy", Flag, "use_gfw_list", translatef("Use %s", translate("GFW List")))
|
||||
o.default = "1"
|
||||
end
|
||||
|
||||
if has_chnlist or has_chnroute then
|
||||
o = s:taboption("Proxy", ListValue, "chn_list", translate("China List"))
|
||||
o:value("0", translate("Close(Not use)"))
|
||||
o:value("direct", translate("Direct Connection"))
|
||||
o:value("proxy", translate("Proxy"))
|
||||
o.default = "direct"
|
||||
end
|
||||
|
||||
---- TCP Default Proxy Mode
|
||||
o = s:taboption("Proxy", ListValue, "tcp_proxy_mode", "TCP " .. translate("Default Proxy Mode"))
|
||||
o:value("disable", translate("No Proxy"))
|
||||
o:value("proxy", translate("Proxy"))
|
||||
o.default = "proxy"
|
||||
|
||||
---- UDP Default Proxy Mode
|
||||
o = s:taboption("Proxy", ListValue, "udp_proxy_mode", "UDP " .. translate("Default Proxy Mode"))
|
||||
o:value("disable", translate("No Proxy"))
|
||||
o:value("proxy", translate("Proxy"))
|
||||
o.default = "proxy"
|
||||
|
||||
o = s:taboption("Proxy", DummyValue, "switch_mode", "")
|
||||
o.template = m:template_path("/global/proxy")
|
||||
|
||||
---- Check the transparent proxy component
|
||||
local handle = io.popen("lsmod")
|
||||
local mods = ""
|
||||
if handle then
|
||||
mods = handle:read("*a") or ""
|
||||
handle:close()
|
||||
end
|
||||
|
||||
if (mods:find("REDIRECT") and mods:find("TPROXY")) or (mods:find("nft_redir") and mods:find("nft_tproxy")) then
|
||||
o = s:taboption("Proxy", Flag, "localhost_proxy", translate("Localhost Proxy"), translate("When selected, localhost can transparent proxy."))
|
||||
o.default = "1"
|
||||
o.rmempty = false
|
||||
|
||||
o = s:taboption("Proxy", Flag, "client_proxy", translate("Client Proxy"), translate("When selected, devices in LAN can transparent proxy. Otherwise, it will not be proxy. But you can still use access control to allow the designated device to proxy."))
|
||||
o.default = "1"
|
||||
o.rmempty = false
|
||||
else
|
||||
local html = string.format([[<div class="cbi-checkbox"><input class="cbi-input-checkbox" type="checkbox" disabled></div><div class="cbi-value-description"><font color="red">%s</font></div>]], translate("Missing components, transparent proxy is unavailable."))
|
||||
o = s:taboption("Proxy", DummyValue, "localhost_proxy", translate("Localhost Proxy"))
|
||||
o.rawhtml = true
|
||||
function o.cfgvalue(self, section)
|
||||
return html
|
||||
end
|
||||
|
||||
o = s:taboption("Proxy", DummyValue, "client_proxy", translate("Client Proxy"))
|
||||
o.rawhtml = true
|
||||
function o.cfgvalue(self, section)
|
||||
return html
|
||||
end
|
||||
end
|
||||
|
||||
o = s:taboption("Proxy", DummyValue, "_proxy_tips", " ")
|
||||
o.rawhtml = true
|
||||
o.cfgvalue = function(t, n)
|
||||
return string.format('<a style="color: red" href="%s">%s</a>', api.url("acl"), translate("Want different devices to use different proxy modes/ports/nodes? Please use access control."))
|
||||
end
|
||||
|
||||
s:tab("log", translate("Log"))
|
||||
o = s:taboption("log", Flag, "log_node", translate("Enable Node Log"))
|
||||
o.default = "0"
|
||||
o.rmempty = false
|
||||
|
||||
o = s:taboption("log", ListValue, "loglevel", translate("Log Level"))
|
||||
o.default = "warn"
|
||||
o:value("debug", "Debug")
|
||||
o:value("info", "Info")
|
||||
o:value("warn", "Warning")
|
||||
o:value("error", "Error")
|
||||
|
||||
o = s:taboption("log", DummyValue, "_node_log", translate("Log File"))
|
||||
o.rawhtml = true
|
||||
o.cfgvalue = function(t, n)
|
||||
local log_file = api.TMP_PATH .. "/acl/default/global.log"
|
||||
local log_url = api.url("get_redir_log") .. "?id=default"
|
||||
local s = "<code>%s</code> " % log_file
|
||||
if api.fs.access(log_file) then
|
||||
local btn = string.format(
|
||||
'<input class="btn cbi-button cbi-button-apply" type="button" value="%s" onclick="window.open(\'%s\', \'_blank\')" />',
|
||||
translate("View Log"),
|
||||
log_url
|
||||
)
|
||||
s = s .. btn
|
||||
end
|
||||
return s
|
||||
end
|
||||
o:depends("log_node", "1")
|
||||
|
||||
o = s:taboption("log", Flag, "log_chinadns_ng", translate("Enable") .. " ChinaDNS-NG " .. translate("Log"))
|
||||
o.default = "0"
|
||||
o.rmempty = false
|
||||
o:depends("dns_shunt", "chinadns-ng")
|
||||
|
||||
o = s:taboption("log", DummyValue, "_chinadns_ng_log", translate("Log File"))
|
||||
o.rawhtml = true
|
||||
o.cfgvalue = function(t, n)
|
||||
local log_file = api.TMP_PATH .. "/acl/default/chinadns_ng.log"
|
||||
local log_url = api.url("get_chinadns_log") .. "?flag=default"
|
||||
local s = "<code>%s</code> " % log_file
|
||||
if api.fs.access(log_file) then
|
||||
local btn = string.format(
|
||||
'<input class="btn cbi-button cbi-button-apply" type="button" value="%s" onclick="window.open(\'%s\', \'_blank\')" />',
|
||||
translate("View Log"),
|
||||
log_url
|
||||
)
|
||||
s = s .. btn
|
||||
end
|
||||
return s
|
||||
end
|
||||
o:depends("log_chinadns_ng", "1")
|
||||
|
||||
o = s:taboption("log", DummyValue, "_log_tips", " ")
|
||||
o.rawhtml = true
|
||||
o.cfgvalue = function(t, n)
|
||||
return string.format('<font color="red">%s</font>', translate("It is recommended to disable logging during regular use to reduce system overhead."))
|
||||
end
|
||||
|
||||
s:tab("faq", "FAQ")
|
||||
o = s:taboption("faq", DummyValue, "")
|
||||
o.template = m:template_path("/global/faq")
|
||||
|
||||
s:tab("maintain", translate("Maintain"))
|
||||
o = s:taboption("maintain", DummyValue, "")
|
||||
o.template = m:template_path("/global/backup")
|
||||
|
||||
m:appendTemplate("/include/node_change", { verify_option = s.fields["node"], shunt_list = api.jsonc.stringify(shunt_list) })
|
||||
|
||||
-- [[ Socks Server ]]--
|
||||
o = s:taboption("Main", Flag, "socks_enabled", "Socks " .. translate("Main switch"))
|
||||
o.rmempty = false
|
||||
|
||||
s2 = m:section(TypedSection, "socks", translate("Socks Config"))
|
||||
s2.template = "cbi/tblsection"
|
||||
s2.sortable = true
|
||||
s2.anonymous = true
|
||||
s2.addremove = true
|
||||
s2.extedit = api.url("socks_config", "%s")
|
||||
function s2.create(e, t)
|
||||
local uid = "socks_" .. api.gen_random_char(5)
|
||||
local n = 1
|
||||
m:foreach("socks", function(s)
|
||||
if s[".name"] == section then
|
||||
return false
|
||||
end
|
||||
n = n + 1
|
||||
end)
|
||||
TypedSection.create(e, uid)
|
||||
m:set(uid, "port", 1080 + n)
|
||||
m:set(uid, "http_port", 0)
|
||||
luci.http.redirect(e.extedit:format(uid))
|
||||
end
|
||||
function s2.remove(e, t)
|
||||
local new_node = ""
|
||||
local node0 = m:get("@nodes[0]") or nil
|
||||
if node0 then
|
||||
new_node = node0[".name"]
|
||||
end
|
||||
if (m:get("@global[0]", "node") or "") == t then
|
||||
m:set('@global[0]', "node", new_node)
|
||||
end
|
||||
m:foreach("acl_rule", function(s)
|
||||
if s["node"] and s["node"] == t then
|
||||
m:set(s[".name"], "node", "default")
|
||||
end
|
||||
end)
|
||||
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(api.c_config, s[".name"], list_name)
|
||||
if nodes then
|
||||
local changed = false
|
||||
local new_nodes = {}
|
||||
for _, node in ipairs(nodes) do
|
||||
if node ~= t then
|
||||
table.insert(new_nodes, node)
|
||||
else
|
||||
changed = true
|
||||
end
|
||||
end
|
||||
if changed then
|
||||
api.uci_set_c(s[".name"], list_name, new_nodes)
|
||||
end
|
||||
end
|
||||
end
|
||||
if s["fallback_node"] == t then
|
||||
m:del(s[".name"], "fallback_node")
|
||||
end
|
||||
end)
|
||||
TypedSection.remove(e, t)
|
||||
end
|
||||
|
||||
---- Enable
|
||||
o = s2:option(Flag, "enabled", translate("Enable"))
|
||||
o.default = 1
|
||||
o.rmempty = false
|
||||
|
||||
o = s2:option(ListValue, "node", translate("Socks Node"))
|
||||
o.template = m:template_path("/cbi/nodes_listvalue")
|
||||
o.group = {}
|
||||
|
||||
o = s2:option(DummyValue, "now_node", translate("Current Node"))
|
||||
o.rawhtml = true
|
||||
o.cfgvalue = function(_, n)
|
||||
local current_node = api.get_cache_var(n)
|
||||
if current_node then
|
||||
local node = m:get(current_node)
|
||||
if node then
|
||||
return (api.get_node_remarks(node) or ""):gsub("(:)%[", "%1<br>[")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
o = s2:option(DummyValue, "port_status", translate("Port Status"))
|
||||
o.rawhtml = true
|
||||
o.cfgvalue = function(t, n)
|
||||
local socks = m:get(n, "port") or "0"
|
||||
local http = m:get(n, "http_port") or "0"
|
||||
return string.format([[
|
||||
<div class="_socks_status" socks_id="%s">
|
||||
<span class="_socks"></span> SOCKS: %s
|
||||
<br>
|
||||
<span class="_http"></span> HTTP: %s
|
||||
</div>
|
||||
]], n, socks, http)
|
||||
end
|
||||
|
||||
local o_node = s.fields["node"]
|
||||
local o_socks = s2.fields["node"]
|
||||
for k, v in pairs(socks_list) do
|
||||
o_node:value(v.id, v["remark"])
|
||||
o_node.group[#o_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
end
|
||||
for k, v in pairs(nodes_table) do
|
||||
if #normal_list == 0 and #iface_list == 0 then
|
||||
break
|
||||
end
|
||||
if v.protocol and v.protocol == "_shunt" then
|
||||
if has_singbox or has_xray then
|
||||
o_node:value(v.id, v["remark"])
|
||||
o_node.group[#o_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
|
||||
s.fields["_node_sel_shunt"]:depends({ node = v.id })
|
||||
if m:get(v.id, "type") == "Xray" then
|
||||
s.fields["xray_dns_mode"]:depends({ node = v.id })
|
||||
else
|
||||
s.fields["singbox_dns_mode"]:depends({ node = v.id })
|
||||
s.fields["remote_rewrite_ttl"]:depends({ node = v.id })
|
||||
end
|
||||
end
|
||||
else
|
||||
o_node:value(v.id, v["remark"])
|
||||
o_node.group[#o_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
end
|
||||
if v.type == "Socks" then
|
||||
if has_singbox or has_xray then
|
||||
o_socks:value(v.id, v["remark"])
|
||||
o_socks.group[#o_socks.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
end
|
||||
else
|
||||
o_socks:value(v.id, v["remark"])
|
||||
o_socks.group[#o_socks.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
end
|
||||
end
|
||||
|
||||
m:appendTemplate("/global/footer")
|
||||
|
||||
m:appendTemplate("/cbi/sortable", {sectiontype = s2.sectiontype})
|
||||
|
||||
return api.return_map(m)
|
||||
@@ -1,183 +0,0 @@
|
||||
local api = require "luci.passwall.api"
|
||||
local datatypes = api.datatypes
|
||||
api.set_default_cbi()
|
||||
|
||||
local nodes_table = {}
|
||||
for k, e in ipairs(api.get_valid_nodes()) do
|
||||
if e.node_type == "normal" then
|
||||
nodes_table[#nodes_table + 1] = {
|
||||
id = e[".name"],
|
||||
obj = e,
|
||||
remarks = e["remark"],
|
||||
group = e["group"]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
m = Map(api.c_config, "HAPROXY " .. translate("Load Balancing"))
|
||||
|
||||
m:appendTemplate("/cbi/nodes_value_com")
|
||||
|
||||
-- [[ Haproxy Settings ]]--
|
||||
s = m:section(NamedSection, "@global_haproxy[0]", "global_haproxy", translate("Basic Settings"))
|
||||
|
||||
s:appendTemplate("/haproxy/status")
|
||||
|
||||
---- Balancing Enable
|
||||
o = s:option(Flag, "balancing_enable", translate("Enable Load Balancing"))
|
||||
o.rmempty = false
|
||||
o.default = false
|
||||
|
||||
---- Console Login Auth
|
||||
o = s:option(Flag, "console_auth", translate("Console Login Auth"))
|
||||
o.default = false
|
||||
o:depends("balancing_enable", true)
|
||||
|
||||
---- Console Username
|
||||
o = s:option(Value, "console_user", translate("Console Username"))
|
||||
o.default = ""
|
||||
o:depends("console_auth", true)
|
||||
|
||||
---- Console Password
|
||||
o = s:option(Value, "console_password", translate("Console Password"))
|
||||
o.password = true
|
||||
o.default = ""
|
||||
o:depends("console_auth", true)
|
||||
|
||||
---- Console Port
|
||||
o = s:option(Value, "console_port", translate("Console Port"), translate("In the browser input routing IP plus port access, such as:192.168.1.1:1188"))
|
||||
o.default = 1188
|
||||
o.placeholder = 1188
|
||||
o.datatype = "range(1,65535)"
|
||||
o:depends("balancing_enable", true)
|
||||
|
||||
o = s:option(Flag, "bind_local", translate("Haproxy Port") .. " " .. translate("Bind Local"), translate("When selected, it can only be accessed localhost."))
|
||||
o.default = "0"
|
||||
o:depends("balancing_enable", true)
|
||||
|
||||
o = s:option(ListValue, "balancingStrategy", translate("Balancing Strategy"))
|
||||
o:value("roundrobin", "roundRobin")
|
||||
o:value("random", "random")
|
||||
o:value("leastconn", "leastConn")
|
||||
o:value("source", "source")
|
||||
o.default = "roundrobin"
|
||||
o:depends("balancing_enable", true)
|
||||
|
||||
---- Health Check Type
|
||||
o = s:option(ListValue, "health_check_type", translate("Health Check Type"))
|
||||
o.default = "script_logic"
|
||||
o:value("tcp", "TCP")
|
||||
o:value("script_logic", translate("URL Test") .. string.format("(passwall %s)", translate("Inner implement")))
|
||||
o:depends("balancing_enable", true)
|
||||
|
||||
---- Passwall Inner implement Probe URL
|
||||
o = s:option(Value, "health_probe_url", translate("Probe URL"))
|
||||
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")
|
||||
o:value("https://www.youtube.com/generate_204", "YouTube")
|
||||
o:value("https://connect.rom.miui.com/generate_204", "MIUI (CN)")
|
||||
o:value("https://connectivitycheck.platform.hicloud.com/generate_204", "HiCloud (CN)")
|
||||
o:value("https://wifi.vivo.com.cn/generate_204", "VIVO (CN)")
|
||||
o.default = o.keylist[3]
|
||||
o.description = translate("The URL used to detect the connection status.")
|
||||
o:depends("health_check_type", "script_logic")
|
||||
|
||||
---- Health Check Inter
|
||||
o = s:option(Value, "health_check_inter", translate("Health Check Inter"))
|
||||
o.datatype = "min(20)"
|
||||
o.default = 60
|
||||
o:depends("balancing_enable", true)
|
||||
o.description = translate("Units:seconds") .. " ( ≥ 20s )<br>" ..
|
||||
translate("Note: Frequent health checks increase CPU usage; an interval ≥ 60 seconds is recommended.")
|
||||
|
||||
o = s:option(DummyValue, "health_check_tips", " ")
|
||||
o.rawhtml = true
|
||||
o.cfgvalue = function(t, n)
|
||||
return string.format('<span style="color: red">%s</span>', translate("When the URL test is used, the load balancing node will be converted into a Socks node. when node list set customizing, must be a Socks node, otherwise the health check will be invalid."))
|
||||
end
|
||||
o:depends("health_check_type", "script_logic")
|
||||
|
||||
-- [[ Balancing Settings ]]--
|
||||
s = m:section(TypedSection, "haproxy_config", translate("Node List"))
|
||||
s.description = "<font color='red'>" ..
|
||||
translate("Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group.") .. "<br>" ..
|
||||
translate("Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!") .. "</font>"
|
||||
s.template = "cbi/tblsection"
|
||||
s.sortable = true
|
||||
s.anonymous = true
|
||||
s.addremove = true
|
||||
|
||||
s.create = function(e, t)
|
||||
local uid = "haproxy_" .. api.gen_random_char(5)
|
||||
TypedSection.create(e, uid)
|
||||
end
|
||||
|
||||
s.remove = function(self, section)
|
||||
for k, v in pairs(self.children) do
|
||||
v.rmempty = true
|
||||
v.validate = nil
|
||||
end
|
||||
TypedSection.remove(self, section)
|
||||
end
|
||||
|
||||
---- Enable
|
||||
o = s:option(Flag, "enabled", translate("Enable"))
|
||||
o.default = 1
|
||||
o.rmempty = false
|
||||
|
||||
---- Node Address
|
||||
o = s:option(Value, "lbss", translate("Node Address"))
|
||||
o.template = m:template_path("/cbi/nodes_value")
|
||||
o.group = {}
|
||||
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.rmempty = false
|
||||
o.validate = function(self, value)
|
||||
if not value then return nil, translate("Node address cannot be empty.") end
|
||||
local t = m:get(value) or nil
|
||||
if t and t[".type"] == "nodes" then
|
||||
return value
|
||||
end
|
||||
if datatypes.hostport(value) or datatypes.ip4addrport(value) then
|
||||
return value
|
||||
end
|
||||
if api.is_ipv6addrport(value) then
|
||||
return value
|
||||
end
|
||||
return nil, translate("Not valid IP format, please re-enter!") .. " (IP:Port)"
|
||||
end
|
||||
|
||||
---- Haproxy Port
|
||||
o = s:option(Value, "haproxy_port", translate("Haproxy Port"))
|
||||
o.default = 1181
|
||||
o.datatype = "range(1,65535)"
|
||||
o.rmempty = false
|
||||
|
||||
---- Node Weight
|
||||
o = s:option(Value, "lbweight", translate("Node Weight"))
|
||||
o.datatype = "min(1)"
|
||||
o.default = 5
|
||||
o.rmempty = false
|
||||
|
||||
---- Export
|
||||
o = s:option(ListValue, "export", translate("Export Of Multi WAN"))
|
||||
o:value(0, translate("Auto"))
|
||||
local wa = require "luci.tools.webadmin"
|
||||
wa.cbi_add_networks(o)
|
||||
o.default = 0
|
||||
o.rmempty = false
|
||||
|
||||
---- Mode
|
||||
o = s:option(ListValue, "backup", translate("Mode"))
|
||||
o:value(0, translate("Primary"))
|
||||
o:value(1, translate("Standby"))
|
||||
o.rmempty = false
|
||||
|
||||
m:appendTemplate("/cbi/sortable", {sectiontype = s.sectiontype})
|
||||
|
||||
m:appendTemplate("/haproxy/js")
|
||||
|
||||
return api.return_map(m)
|
||||
@@ -1,244 +0,0 @@
|
||||
local m, s, data = ...
|
||||
|
||||
if not data.node or not data.node[".name"] then
|
||||
return
|
||||
end
|
||||
|
||||
local api = m.api
|
||||
local section_id = s.section
|
||||
local current_node = data.node
|
||||
local current_node_id = current_node[".name"]
|
||||
local verify_option = data.verify_option
|
||||
local node_list = data.node_list or api.get_node_list()
|
||||
local shunt_section_type = "shunt_option_list"
|
||||
|
||||
if data.tab then
|
||||
if #s.tab_names > 0 then
|
||||
table.insert(s.tab_names, 2, data.tab)
|
||||
s.tabs[data.tab] = {
|
||||
title = data.tab_desc,
|
||||
childs = { }
|
||||
}
|
||||
else
|
||||
s:tab(data.tab, data.tab_desc)
|
||||
end
|
||||
end
|
||||
|
||||
local groups = {}
|
||||
m:foreach("shunt_rules", function(s)
|
||||
if s.group and s.group ~= "" then
|
||||
groups[s.group] = true
|
||||
end
|
||||
end)
|
||||
|
||||
local node_save_before = luci.http.formvalue("node_save_before")
|
||||
local load_shunt = luci.http.formvalue("load_shunt")
|
||||
|
||||
local function get_cfgvalue()
|
||||
return function(self, section)
|
||||
return m:get(current_node_id, self.option)
|
||||
end
|
||||
end
|
||||
local function get_write()
|
||||
return function(self, section, value)
|
||||
if node_save_before and node_save_before ~= current_node_id then return end
|
||||
if load_shunt == "1" then return end
|
||||
if verify_option then
|
||||
if verify_option:formvalue(section) == current_node_id then
|
||||
m:set(current_node_id, self.option, value)
|
||||
end
|
||||
else
|
||||
m:set(current_node_id, self.option, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
local function get_remove()
|
||||
return function(self, section)
|
||||
if node_save_before and node_save_before ~= current_node_id then return end
|
||||
if load_shunt == "1" then return end
|
||||
if verify_option then
|
||||
if verify_option:formvalue(section) == current_node_id then
|
||||
m:del(current_node_id, self.option)
|
||||
end
|
||||
else
|
||||
m:del(current_node_id, self.option)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function add_option(class, option_name, option_title, option_desc)
|
||||
local a
|
||||
if data.tab then
|
||||
a = s:taboption(data.tab, class, option_name, option_title)
|
||||
else
|
||||
a = s:option(class, option_name, option_title)
|
||||
end
|
||||
if a then
|
||||
if option_desc then
|
||||
a.description = option_desc
|
||||
end
|
||||
a.cfgvalue = get_cfgvalue()
|
||||
a.write = get_write()
|
||||
a.remove = get_remove()
|
||||
end
|
||||
if verify_option then
|
||||
a:depends(verify_option.option, current_node_id)
|
||||
end
|
||||
return a
|
||||
end
|
||||
|
||||
local function add_depends(o, deps)
|
||||
if #o.deps > 0 then
|
||||
for index, value in ipairs(o.deps) do
|
||||
for k, v in pairs(deps) do
|
||||
o.deps[index][k] = v
|
||||
end
|
||||
end
|
||||
else
|
||||
o:depends(deps)
|
||||
end
|
||||
end
|
||||
|
||||
if current_node.type == "Xray" then
|
||||
o = add_option(ListValue, "domainStrategy", translate("Domain Strategy"))
|
||||
o:value("AsIs")
|
||||
o:value("IPIfNonMatch")
|
||||
o:value("IPOnDemand")
|
||||
o.default = "IPOnDemand"
|
||||
o.description = "<br /><ul><li>" .. translate("'AsIs': Only use domain for routing. Default value.")
|
||||
.. "</li><li>" .. translate("'IPIfNonMatch': When no rule matches current domain, resolves it into IP addresses (A or AAAA records) and try all rules again.")
|
||||
.. "</li><li>" .. translate("'IPOnDemand': As long as there is a IP-based rule, resolves the domain into IP immediately.")
|
||||
.. "</li></ul>"
|
||||
|
||||
o = add_option(ListValue, "domainMatcher", translate("Domain matcher"))
|
||||
o:value("hybrid")
|
||||
o:value("linear")
|
||||
end
|
||||
|
||||
o = add_option(Flag, "fakedns", '<a style="color:#FF8C00">FakeDNS</a>' .. " " .. translate("Main switch"), translate("Use FakeDNS work in the domain that proxy.") .. "<br>" ..
|
||||
translate("Suitable scenarios for let the node servers get the target domain names.") .. "<br>" ..
|
||||
translate("Such as: DNS unlocking of streaming media, reducing DNS query latency, etc."))
|
||||
|
||||
shunt_group = add_option(ListValue, "shunt_group", translate("Shunt Rule Group"))
|
||||
shunt_group:value("", translate("default"))
|
||||
for k, v in pairs(groups) do
|
||||
shunt_group:value(k)
|
||||
end
|
||||
|
||||
local shunt_group_val = m:get(current_node_id, "shunt_group") or ""
|
||||
shunt_group_val = shunt_group_val:lower()
|
||||
local shunt_rules = {}
|
||||
m:foreach("shunt_rules", function(e)
|
||||
local group = e.group or ""
|
||||
group = group:lower()
|
||||
if group == shunt_group_val then
|
||||
e.id = e[".name"]
|
||||
e.remarks = e.remarks or e[".name"]
|
||||
e["_node_option"] = e[".name"]
|
||||
e["_node_default"] = ""
|
||||
e["_fakedns_option"] = e[".name"] .. "_fakedns"
|
||||
e["_proxy_tag_option"] = e[".name"] .. "_proxy_tag"
|
||||
table.insert(shunt_rules, e)
|
||||
end
|
||||
end)
|
||||
table.insert(shunt_rules, {
|
||||
id = ".default",
|
||||
remarks = translate("Default"),
|
||||
_node_option = "default_node",
|
||||
_node_default = "_direct",
|
||||
_fakedns_option = "default_fakedns",
|
||||
_proxy_tag_option = "default_proxy_tag",
|
||||
})
|
||||
|
||||
s2 = m:section(Table, shunt_rules, " ")
|
||||
s2.config = m.config
|
||||
s2.sectiontype = shunt_section_type
|
||||
|
||||
o = s2:option(DummyValue, "remarks", translate("Rule"))
|
||||
o.rawhtml = true
|
||||
o.cfgvalue = function(self, section)
|
||||
if shunt_rules[section].id == ".default" then
|
||||
return string.format('<font style="color: red">%s</font>', shunt_rules[section].remarks)
|
||||
else
|
||||
return string.format('<a href="%s" target="_blank">%s</a>', api.url("shunt_rules", shunt_rules[section].id), shunt_rules[section].remarks)
|
||||
end
|
||||
end
|
||||
|
||||
_node = s2:option(Value, "_node", translate("Node"))
|
||||
_node.template = m:template_path("/cbi/nodes_listvalue")
|
||||
_node.group = {"","","",""}
|
||||
_node:value("", translate("Close (Not use)"))
|
||||
_node:value("_default", translate("Use default node"))
|
||||
_node:value("_direct", translate("Direct Connection"))
|
||||
_node:value("_blackhole", translate("Blackhole (Block)"))
|
||||
_node.cfgvalue = function(self, section)
|
||||
return m:get(current_node_id, shunt_rules[section]["_node_option"]) or shunt_rules[section]["_node_default"]
|
||||
end
|
||||
_node.write = function(self, section, value)
|
||||
if node_save_before and node_save_before ~= current_node_id then return end
|
||||
if load_shunt == "1" then return end
|
||||
return m:set(current_node_id, shunt_rules[section]["_node_option"], value)
|
||||
end
|
||||
_node.remove = function(self, section)
|
||||
if node_save_before and node_save_before ~= current_node_id then return end
|
||||
if load_shunt == "1" then return end
|
||||
return m:del(current_node_id, shunt_rules[section]["_node_option"])
|
||||
end
|
||||
|
||||
o = s2:option(Flag, "_fakedns", string.format('<a style="color:#FF8C00" title="%s">FakeDNS</a>', translate("Use FakeDNS work in the domain that proxy.") .. "\n" ..
|
||||
translate("Suitable scenarios for let the node servers get the target domain names.") .. "\n" ..
|
||||
translate("Such as: DNS unlocking of streaming media, reducing DNS query latency, etc.")))
|
||||
o.cfgvalue = function(self, section)
|
||||
return m:get(current_node_id, shunt_rules[section]["_fakedns_option"])
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
if node_save_before and node_save_before ~= current_node_id then return end
|
||||
if load_shunt == "1" then return end
|
||||
return m:set(current_node_id, shunt_rules[section]["_fakedns_option"], value)
|
||||
end
|
||||
o.remove = function(self, section)
|
||||
if node_save_before and node_save_before ~= current_node_id then return end
|
||||
if load_shunt == "1" then return end
|
||||
return m:del(current_node_id, shunt_rules[section]["_fakedns_option"])
|
||||
end
|
||||
|
||||
proxy_tag_node = s2:option(ListValue, "_proxy_tag", string.format('<a style="color:red" title="%s">%s</a>',
|
||||
translate("Set the node to be used as a pre-proxy.") .. "\n" .. translate("Each rule has a separate switch that controls whether this rule uses the pre-proxy or not."),
|
||||
translate("Preproxy")))
|
||||
proxy_tag_node.template = m:template_path("/cbi/nodes_listvalue")
|
||||
proxy_tag_node.group = {""}
|
||||
proxy_tag_node:value("", translate("Close (Not use)"))
|
||||
proxy_tag_node.cfgvalue = function(self, section)
|
||||
return m:get(current_node_id, shunt_rules[section]["_proxy_tag_option"])
|
||||
end
|
||||
proxy_tag_node.write = function(self, section, value)
|
||||
if node_save_before and node_save_before ~= current_node_id then return end
|
||||
if load_shunt == "1" then return end
|
||||
return m:set(current_node_id, shunt_rules[section]["_proxy_tag_option"], value)
|
||||
end
|
||||
proxy_tag_node.remove = function(self, section)
|
||||
if node_save_before and node_save_before ~= current_node_id then return end
|
||||
if load_shunt == "1" then return end
|
||||
return m:del(current_node_id, shunt_rules[section]["_proxy_tag_option"])
|
||||
end
|
||||
|
||||
for k1, v1 in pairs(node_list) do
|
||||
if k1 ~= "shunt_list" then
|
||||
for i, v in ipairs(v1) do
|
||||
_node:value(v.id, v.remark)
|
||||
_node.group[#_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
|
||||
proxy_tag_node:value(v.id, v.remark)
|
||||
proxy_tag_node.group[#proxy_tag_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
m:appendTemplate("/include/shunt_options", {
|
||||
node_id = current_node_id,
|
||||
section_id = section_id or current_node_id,
|
||||
section_type = shunt_section_type,
|
||||
tab_name = data.tab,
|
||||
normal_list = api.jsonc.stringify(node_list.normal_list),
|
||||
shunt_list = api.jsonc.stringify(node_list.shunt_list)
|
||||
})
|
||||
@@ -1,87 +0,0 @@
|
||||
api = require "luci.passwall.api"
|
||||
api.set_default_cbi()
|
||||
|
||||
m = Map()
|
||||
m.redirect = api.url("node_list")
|
||||
|
||||
if not arg[1] or not m:get(arg[1]) then
|
||||
luci.http.redirect(m.redirect)
|
||||
end
|
||||
|
||||
formvalue_key = "cbid." .. m.config .. "." .. arg[1] .. "."
|
||||
|
||||
m:appendTemplate("/node_config/header", {section = arg[1]})
|
||||
m:appendTemplate("/cbi/nodes_multivalue_com")
|
||||
m:appendTemplate("/cbi/nodes_listvalue_com")
|
||||
|
||||
groups = {}
|
||||
m:foreach("nodes", function(s)
|
||||
if s[".name"] ~= arg[1] then
|
||||
if s.group and s.group ~= "" then
|
||||
groups[s.group] = true
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
local s = m:section(NamedSection, arg[1], "nodes", translate("Node Config"))
|
||||
s.addremove = false
|
||||
s.dynamic = false
|
||||
|
||||
o = s:option(DummyValue, "passwall", " ")
|
||||
o.rawhtml = true
|
||||
o.template = m:template_path("/node_config/link_share_man")
|
||||
o.value = arg[1]
|
||||
|
||||
o = s:option(Value, "remarks", translate("Node Remarks"))
|
||||
o.default = translate("Remarks")
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "group", translate("Group Name"))
|
||||
o.default = ""
|
||||
o:value("", translate("default"))
|
||||
for k, v in pairs(groups) do
|
||||
o:value(k)
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
value = api.trim(value)
|
||||
local lower = value:lower()
|
||||
|
||||
if lower == "" or lower == "default" then
|
||||
return m:del(section, self.option)
|
||||
end
|
||||
|
||||
for _, v in ipairs(self.keylist or {}) do
|
||||
if v:lower() == lower then
|
||||
return m:set(section, self.option, v)
|
||||
end
|
||||
end
|
||||
m:set(section, self.option, value)
|
||||
end
|
||||
|
||||
local types_dir = "/usr/lib/lua/luci/model/cbi/" .. api.appname .. "/client/type/"
|
||||
s.val = {}
|
||||
s.val["type"] = m:get(arg[1], "type")
|
||||
|
||||
if luci.http.formvalue("cbi.submit") == "1" then
|
||||
local formvalue_type = luci.http.formvalue(formvalue_key .. "type")
|
||||
if formvalue_type then
|
||||
s.val["type"] = formvalue_type
|
||||
end
|
||||
end
|
||||
|
||||
o = s:option(ListValue, "type", translate("Type"))
|
||||
|
||||
local type_table = {}
|
||||
for filename in api.fs.dir(types_dir) do
|
||||
table.insert(type_table, filename)
|
||||
end
|
||||
table.sort(type_table)
|
||||
|
||||
for index, value in ipairs(type_table) do
|
||||
local p_func = loadfile(types_dir .. value)
|
||||
setfenv(p_func, getfenv(1))(m, s)
|
||||
end
|
||||
|
||||
m:appendTemplate("/node_config/footer", {section = arg[1]})
|
||||
|
||||
return api.return_map(m)
|
||||
@@ -1,49 +0,0 @@
|
||||
local api = require "luci.passwall.api"
|
||||
api.set_default_cbi()
|
||||
|
||||
m = Map()
|
||||
|
||||
-- [[ Other Settings ]]--
|
||||
s = m:section(NamedSection, "@global_other[0]", "global_other")
|
||||
|
||||
o = s:option(ListValue, "auto_detection_time", translate("Automatic detection delay"))
|
||||
o:value("0", translate("Close"))
|
||||
o:value("icmp", "Ping")
|
||||
o:value("tcping", "TCP Ping")
|
||||
|
||||
o = s:option(Flag, "show_node_info", translate("Show server address and port"))
|
||||
o.default = "0"
|
||||
|
||||
o = s:option(Value, "url_test_url", translate("URL Test Address"))
|
||||
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")
|
||||
o:value("https://www.youtube.com/generate_204", "YouTube")
|
||||
o:value("https://connect.rom.miui.com/generate_204", "MIUI (CN)")
|
||||
o:value("https://connectivitycheck.platform.hicloud.com/generate_204", "HiCloud (CN)")
|
||||
o:value("https://wifi.vivo.com.cn/generate_204", "VIVO (CN)")
|
||||
o.default = o.keylist[3]
|
||||
|
||||
if true then
|
||||
m:appendTemplate("/node_list/node_list")
|
||||
|
||||
if luci.http.formvalue("cbi.submit") == "1" then
|
||||
local group_order = {}
|
||||
group_order = luci.http.formvaluetable("group.order")
|
||||
if group_order then
|
||||
for k, v in pairs(group_order) do
|
||||
if v and v~= "" then
|
||||
local new_order = {}
|
||||
string.gsub(v, "[^" .. " " .. "]+", function(w)
|
||||
new_order[#new_order + 1] = w
|
||||
end)
|
||||
for idx, name in ipairs(new_order) do
|
||||
m.uci:reorder(m.config, name, idx - 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return api.return_map(m)
|
||||
@@ -1,227 +0,0 @@
|
||||
local api = require "luci.passwall.api"
|
||||
api.set_default_cbi()
|
||||
|
||||
local has_ss_rust = api.is_finded("sslocal")
|
||||
local has_singbox = api.finded_com("sing-box")
|
||||
local has_xray = api.finded_com("xray")
|
||||
local has_hysteria2 = api.finded_com("hysteria")
|
||||
local ss_type = {}
|
||||
local trojan_type = {}
|
||||
local vmess_type = {}
|
||||
local vless_type = {}
|
||||
local hysteria2_type = {}
|
||||
local xray_version = api.get_app_version("xray")
|
||||
if has_ss_rust then
|
||||
local s = "shadowsocks-rust"
|
||||
table.insert(ss_type, s)
|
||||
end
|
||||
if has_singbox then
|
||||
local s = "sing-box"
|
||||
table.insert(trojan_type, s)
|
||||
table.insert(ss_type, s)
|
||||
table.insert(vmess_type, s)
|
||||
table.insert(vless_type, s)
|
||||
table.insert(hysteria2_type, s)
|
||||
end
|
||||
if has_xray then
|
||||
local s = "xray"
|
||||
table.insert(trojan_type, s)
|
||||
table.insert(ss_type, s)
|
||||
table.insert(vmess_type, s)
|
||||
table.insert(vless_type, s)
|
||||
if api.compare_versions(xray_version, ">=", "26.1.13") then
|
||||
table.insert(hysteria2_type, s)
|
||||
end
|
||||
end
|
||||
if has_hysteria2 then
|
||||
local s = "hysteria2"
|
||||
table.insert(hysteria2_type, s)
|
||||
end
|
||||
|
||||
m = Map()
|
||||
|
||||
function m.on_before_save(self)
|
||||
self:foreach("subscribe_list", function(e)
|
||||
self:del(e[".name"], "md5")
|
||||
end)
|
||||
end
|
||||
|
||||
-- [[ Subscribe Settings ]]--
|
||||
s = m:section(NamedSection, "@global_subscribe[0]", "global_subscribe")
|
||||
|
||||
o = s:option(ListValue, "filter_keyword_mode", translate("Filter keyword Mode"))
|
||||
o:value("0", translate("Close"))
|
||||
o:value("1", translate("Discard List"))
|
||||
o:value("2", translate("Keep List"))
|
||||
o:value("3", translate("Discard List,But Keep List First"))
|
||||
o:value("4", translate("Keep List,But Discard List First"))
|
||||
|
||||
o = s:option(DynamicList, "filter_discard_list", translate("Discard List"))
|
||||
|
||||
o = s:option(DynamicList, "filter_keep_list", translate("Keep List"))
|
||||
|
||||
if #ss_type > 0 then
|
||||
o = s:option(ListValue, "ss_type", translatef("%s Node Use Type", "Shadowsocks"))
|
||||
o:value("", translate("Auto"))
|
||||
for key, value in pairs(ss_type) do
|
||||
o:value(value)
|
||||
end
|
||||
end
|
||||
|
||||
if #trojan_type > 0 then
|
||||
o = s:option(ListValue, "trojan_type", translatef("%s Node Use Type", "Trojan"))
|
||||
o:value("", translate("Auto"))
|
||||
for key, value in pairs(trojan_type) do
|
||||
o:value(value)
|
||||
end
|
||||
end
|
||||
|
||||
if #vmess_type > 0 then
|
||||
o = s:option(ListValue, "vmess_type", translatef("%s Node Use Type", "VMess"))
|
||||
o:value("", translate("Auto"))
|
||||
for key, value in pairs(vmess_type) do
|
||||
o:value(value)
|
||||
end
|
||||
end
|
||||
|
||||
if #vless_type > 0 then
|
||||
o = s:option(ListValue, "vless_type", translatef("%s Node Use Type", "VLESS"))
|
||||
o:value("", translate("Auto"))
|
||||
for key, value in pairs(vless_type) do
|
||||
o:value(value)
|
||||
end
|
||||
end
|
||||
|
||||
if #hysteria2_type > 0 then
|
||||
o = s:option(ListValue, "hysteria2_type", translatef("%s Node Use Type", "Hysteria2"))
|
||||
o:value("", translate("Auto"))
|
||||
for key, value in pairs(hysteria2_type) do
|
||||
o:value(value)
|
||||
end
|
||||
end
|
||||
|
||||
if #ss_type > 0 or #trojan_type > 0 or #vmess_type > 0 or #vless_type > 0 or #hysteria2_type > 0 then
|
||||
o.description = string.format("<font color='red'>%s</font>",
|
||||
translate("The configured type also applies to the core specified when manually importing nodes."))
|
||||
end
|
||||
|
||||
---- Subscribe Delete All
|
||||
o = s:option(DummyValue, "_stop", translate("Delete All Subscribe Node"))
|
||||
o.rawhtml = true
|
||||
function o.cfgvalue(self, section)
|
||||
return string.format(
|
||||
[[<input type="button" class="btn cbi-button cbi-button-remove" onclick="return confirmDeleteAll()" value="%s" />]],
|
||||
translate("Delete All Subscribe Node"))
|
||||
end
|
||||
|
||||
o = s:option(DummyValue, "_update", translate("Manual subscription All"))
|
||||
o.rawhtml = true
|
||||
o.cfgvalue = function(self, section)
|
||||
return string.format([[
|
||||
<input type="button" class="btn cbi-button cbi-button-apply" onclick="ManualSubscribeAll()" value="%s" />]],
|
||||
translate("Manual subscription All"))
|
||||
end
|
||||
|
||||
s = m:section(TypedSection, "subscribe_list", "", "<font color='red'>" .. translate("When adding a new subscription, please save and apply before manually subscribing. If you only change the subscription URL, you can subscribe manually, and the system will save it automatically.") .. "</font>")
|
||||
s.addremove = true
|
||||
s.anonymous = true
|
||||
s.sortable = true
|
||||
s.template = "cbi/tblsection"
|
||||
s.extedit = api.url("node_subscribe_config", "%s")
|
||||
function s.create(e, t)
|
||||
local uid = "sub_" .. api.gen_random_char(5)
|
||||
TypedSection.create(e, uid)
|
||||
m:set(uid, "hysteria_up_mbps", "100")
|
||||
m:set(uid, "hysteria_down_mbps", "100")
|
||||
luci.http.redirect(e.extedit:format(uid))
|
||||
end
|
||||
|
||||
o = s:option(Value, "remark", translate("Remarks"))
|
||||
o.width = "auto"
|
||||
o.rmempty = false
|
||||
o.validate = function(self, value, section)
|
||||
value = api.trim(value)
|
||||
if value == "" then
|
||||
return nil, translate("Remark cannot be empty.")
|
||||
end
|
||||
local duplicate = false
|
||||
m:foreach(s.sectiontype, function(e)
|
||||
if e[".name"] ~= section and e["remark"] and e["remark"]:lower() == value:lower() then
|
||||
duplicate = true
|
||||
return false
|
||||
end
|
||||
end)
|
||||
if duplicate or value:lower() == "default" then
|
||||
return nil, translate("This remark already exists, please change a new remark.")
|
||||
end
|
||||
return value
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
local old = m:get(section, self.option) or ""
|
||||
if old ~= value then
|
||||
m:foreach("nodes", function(e)
|
||||
if e["group"] and e["group"]:lower() == old:lower() then
|
||||
m:set(e[".name"], "group", value)
|
||||
end
|
||||
if e["protocol"] and (e["protocol"] == "_balancing" or e["protocol"] == "_urltest") and e["node_group"] then
|
||||
local gs = ""
|
||||
for g in e["node_group"]:gmatch("%S+") do
|
||||
if api.UrlEncode(old) == g then
|
||||
gs = gs .. " " .. api.UrlEncode(value)
|
||||
else
|
||||
gs = gs .. " " .. g
|
||||
end
|
||||
end
|
||||
gs = api.trim(gs)
|
||||
m:set(e[".name"], "node_group", gs)
|
||||
end
|
||||
end)
|
||||
end
|
||||
return Value.write(self, section, value)
|
||||
end
|
||||
|
||||
o = s:option(DummyValue, "_node_count", translate("Subscribe Info"))
|
||||
o.rawhtml = true
|
||||
o.cfgvalue = function(t, n)
|
||||
local remark = m:get(n, "remark") or ""
|
||||
local str = m:get(n, "rem_traffic") or ""
|
||||
local expired_date = m:get(n, "expired_date") or ""
|
||||
if expired_date ~= "" then
|
||||
str = str .. (str ~= "" and "/" or "") .. expired_date
|
||||
end
|
||||
str = str ~= "" and "<br>" .. str or ""
|
||||
local num = 0
|
||||
m:foreach("nodes", function(s)
|
||||
if s["group"] and s["group"]:lower() == remark:lower() then
|
||||
num = num + 1
|
||||
end
|
||||
end)
|
||||
return string.format("%s%s", translate("Node num") .. ": " .. num, str)
|
||||
end
|
||||
|
||||
o = s:option(Value, "url", translate("Subscribe URL"))
|
||||
o.width = "auto"
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(DummyValue, "_remove", translate("Delete the subscribed node"))
|
||||
o.rawhtml = true
|
||||
function o.cfgvalue(self, section)
|
||||
local remark = m:get(section, "remark") or ""
|
||||
return string.format(
|
||||
[[<input type="button" class="btn cbi-button cbi-button-remove" onclick="return confirmDeleteNode('%s')" value="%s" />]],
|
||||
remark, translate("Delete the subscribed node"))
|
||||
end
|
||||
|
||||
o = s:option(DummyValue, "_update", translate("Manual subscription"))
|
||||
o.rawhtml = true
|
||||
o.cfgvalue = function(self, section)
|
||||
return string.format([[
|
||||
<input type="button" class="btn cbi-button cbi-button-apply" onclick="ManualSubscribe('%s')" value="%s" />]],
|
||||
section, translate("Manual subscription"))
|
||||
end
|
||||
|
||||
m:appendTemplate("/cbi/sortable", {sectiontype = s.sectiontype})
|
||||
|
||||
m:appendTemplate("/node_subscribe/js")
|
||||
|
||||
return api.return_map(m)
|
||||
@@ -1,319 +0,0 @@
|
||||
local api = require "luci.passwall.api"
|
||||
api.set_default_cbi()
|
||||
|
||||
m = Map()
|
||||
m.redirect = api.url("node_subscribe")
|
||||
|
||||
if not arg[1] or not m:get(arg[1]) then
|
||||
luci.http.redirect(m.redirect)
|
||||
end
|
||||
|
||||
function m.on_before_save(self)
|
||||
self:del(arg[1], "md5")
|
||||
end
|
||||
|
||||
m:appendTemplate("/cbi/nodes_listvalue_com")
|
||||
|
||||
local has_ss_rust = api.is_finded("sslocal")
|
||||
local has_singbox = api.finded_com("sing-box")
|
||||
local has_xray = api.finded_com("xray")
|
||||
local has_hysteria2 = api.finded_com("hysteria")
|
||||
local ss_type = {}
|
||||
local trojan_type = {}
|
||||
local vmess_type = {}
|
||||
local vless_type = {}
|
||||
local hysteria2_type = {}
|
||||
local xray_version = api.get_app_version("xray")
|
||||
if has_ss_rust then
|
||||
local s = "shadowsocks-rust"
|
||||
table.insert(ss_type, s)
|
||||
end
|
||||
if has_singbox then
|
||||
local s = "sing-box"
|
||||
table.insert(trojan_type, s)
|
||||
table.insert(ss_type, s)
|
||||
table.insert(vmess_type, s)
|
||||
table.insert(vless_type, s)
|
||||
table.insert(hysteria2_type, s)
|
||||
end
|
||||
if has_xray then
|
||||
local s = "xray"
|
||||
table.insert(trojan_type, s)
|
||||
table.insert(ss_type, s)
|
||||
table.insert(vmess_type, s)
|
||||
table.insert(vless_type, s)
|
||||
if api.compare_versions(xray_version, ">=", "26.1.13") then
|
||||
table.insert(hysteria2_type, s)
|
||||
end
|
||||
end
|
||||
if has_hysteria2 then
|
||||
local s = "hysteria2"
|
||||
table.insert(hysteria2_type, s)
|
||||
end
|
||||
local nodes_table = {}
|
||||
for k, e in ipairs(api.get_valid_nodes()) do
|
||||
if e.node_type == "normal" then
|
||||
nodes_table[#nodes_table + 1] = {
|
||||
id = e[".name"],
|
||||
remark = e["remark"],
|
||||
type = e["type"],
|
||||
add_mode = e["add_mode"],
|
||||
chain_proxy = e["chain_proxy"],
|
||||
group = e["group"]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
s = m:section(NamedSection, arg[1])
|
||||
s.addremove = false
|
||||
s.dynamic = false
|
||||
|
||||
o = s:option(Value, "remark", translate("Subscribe Remark"))
|
||||
o.rmempty = false
|
||||
o.validate = function(self, value, section)
|
||||
value = api.trim(value)
|
||||
if value == "" then
|
||||
return nil, translate("Remark cannot be empty.")
|
||||
end
|
||||
local duplicate = false
|
||||
m:foreach("subscribe_list", function(e)
|
||||
if e[".name"] ~= section and e["remark"] and e["remark"]:lower() == value:lower() then
|
||||
duplicate = true
|
||||
return false
|
||||
end
|
||||
end)
|
||||
if duplicate or value:lower() == "default" then
|
||||
return nil, translate("This remark already exists, please change a new remark.")
|
||||
end
|
||||
return value
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
local old = m:get(section, self.option) or ""
|
||||
if old ~= value then
|
||||
m:foreach("nodes", function(e)
|
||||
if e["group"] and e["group"]:lower() == old:lower() then
|
||||
self.map:set(e[".name"], "group", value)
|
||||
end
|
||||
if e["protocol"] and (e["protocol"] == "_balancing" or e["protocol"] == "_urltest") and e["node_group"] then
|
||||
local gs = ""
|
||||
for g in e["node_group"]:gmatch("%S+") do
|
||||
if api.UrlEncode(old) == g then
|
||||
gs = gs .. " " .. api.UrlEncode(value)
|
||||
else
|
||||
gs = gs .. " " .. g
|
||||
end
|
||||
end
|
||||
gs = api.trim(gs)
|
||||
self.map:set(e[".name"], "node_group", gs)
|
||||
end
|
||||
end)
|
||||
end
|
||||
return Value.write(self, section, value)
|
||||
end
|
||||
|
||||
o = s:option(TextValue, "url", translate("Subscribe URL"))
|
||||
o.rows = 5
|
||||
o.rmempty = false
|
||||
o.validate = function(self, value)
|
||||
if not value or value == "" then
|
||||
return nil, translate("URL cannot be empty.")
|
||||
end
|
||||
return value:gsub("%s+", ""):gsub("%z", "")
|
||||
end
|
||||
|
||||
o = s:option(ListValue, "domain_resolver", translate("Domain DNS Resolve"))
|
||||
o.description = translate("If the node address is a domain name, this DNS will be used for resolution.") .. "<br>" ..
|
||||
translate("Supports only Xray or Sing-box node types.") .. "<br>" .. string.format('<font color="red">%s</font>',
|
||||
translate("Note: For node-specific DNS only. Keep Auto to avoid extra overhead."))
|
||||
o:value("", translate("Auto"))
|
||||
o:value("tcp", "TCP")
|
||||
o:value("udp", "UDP")
|
||||
o:value("https", "DoH")
|
||||
|
||||
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({ domain_resolver = "tcp" })
|
||||
o:depends({ domain_resolver = "udp" })
|
||||
|
||||
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({ domain_resolver = "https" })
|
||||
|
||||
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("UseIPv4v6", translate("Prefer IPv4"))
|
||||
o:value("UseIPv6v4", translate("Prefer IPv6"))
|
||||
o:value("UseIPv4", translate("IPv4 Only"))
|
||||
o:value("UseIPv6", translate("IPv6 Only"))
|
||||
|
||||
o = s:option(Flag, "allowInsecure", translate("allowInsecure"))
|
||||
o.default = "0"
|
||||
o.rmempty = false
|
||||
o.description = translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped.") .. "<br>" ..
|
||||
translate("Used when the node link does not include this parameter.")
|
||||
|
||||
o = s:option(ListValue, "filter_keyword_mode", translate("Filter keyword Mode"))
|
||||
o.default = "5"
|
||||
o:value("0", translate("Close"))
|
||||
o:value("1", translate("Discard List"))
|
||||
o:value("2", translate("Keep List"))
|
||||
o:value("3", translate("Discard List,But Keep List First"))
|
||||
o:value("4", translate("Keep List,But Discard List First"))
|
||||
o:value("5", translate("Use global config"))
|
||||
|
||||
o = s:option(DynamicList, "filter_discard_list", translate("Discard List"))
|
||||
o:depends("filter_keyword_mode", "1")
|
||||
o:depends("filter_keyword_mode", "3")
|
||||
o:depends("filter_keyword_mode", "4")
|
||||
|
||||
o = s:option(DynamicList, "filter_keep_list", translate("Keep List"))
|
||||
o:depends("filter_keyword_mode", "2")
|
||||
o:depends("filter_keyword_mode", "3")
|
||||
o:depends("filter_keyword_mode", "4")
|
||||
|
||||
if #ss_type > 0 then
|
||||
o = s:option(ListValue, "ss_type", translatef("%s Node Use Type", "Shadowsocks"))
|
||||
o.default = "global"
|
||||
o:value("global", translate("Use global config"))
|
||||
for key, value in pairs(ss_type) do
|
||||
o:value(value)
|
||||
end
|
||||
end
|
||||
|
||||
if #trojan_type > 0 then
|
||||
o = s:option(ListValue, "trojan_type", translatef("%s Node Use Type", "Trojan"))
|
||||
o.default = "global"
|
||||
o:value("global", translate("Use global config"))
|
||||
for key, value in pairs(trojan_type) do
|
||||
o:value(value)
|
||||
end
|
||||
end
|
||||
|
||||
if #vmess_type > 0 then
|
||||
o = s:option(ListValue, "vmess_type", translatef("%s Node Use Type", "VMess"))
|
||||
o.default = "global"
|
||||
o:value("global", translate("Use global config"))
|
||||
for key, value in pairs(vmess_type) do
|
||||
o:value(value)
|
||||
end
|
||||
end
|
||||
|
||||
if #vless_type > 0 then
|
||||
o = s:option(ListValue, "vless_type", translatef("%s Node Use Type", "VLESS"))
|
||||
o.default = "global"
|
||||
o:value("global", translate("Use global config"))
|
||||
for key, value in pairs(vless_type) do
|
||||
o:value(value)
|
||||
end
|
||||
end
|
||||
|
||||
if #hysteria2_type > 0 then
|
||||
o = s:option(ListValue, "hysteria2_type", translatef("%s Node Use Type", "Hysteria2"))
|
||||
o.default = "global"
|
||||
o:value("global", translate("Use global config"))
|
||||
for key, value in pairs(hysteria2_type) do
|
||||
o:value(value)
|
||||
end
|
||||
|
||||
o = s:option(Value, "hysteria_up_mbps", "Hy/Hy2 " .. translate("Max upload Mbps"))
|
||||
o.datatype = "uinteger"
|
||||
|
||||
o = s:option(Value, "hysteria_down_mbps", "Hy/Hy2 " .. translate("Max download Mbps"))
|
||||
o.datatype = "uinteger"
|
||||
end
|
||||
|
||||
o = s:option(Flag, "boot_update", translate("Update Once on Boot"), translate("Updates the subscription the first time PassWall runs automatically after each system boot."))
|
||||
o.default = 0
|
||||
|
||||
o = s:option(ListValue, "update_week_mode", translate("Auto Update Mode"))
|
||||
o:value("", translate("Disable"))
|
||||
o:value(8, translate("Loop Mode"))
|
||||
o:value(7, translate("Every day"))
|
||||
o:value(1, translate("Every Monday"))
|
||||
o:value(2, translate("Every Tuesday"))
|
||||
o:value(3, translate("Every Wednesday"))
|
||||
o:value(4, translate("Every Thursday"))
|
||||
o:value(5, translate("Every Friday"))
|
||||
o:value(6, translate("Every Saturday"))
|
||||
o:value(0, translate("Every Sunday"))
|
||||
|
||||
o = s:option(Value, "update_time_mode", translate("Update Time"))
|
||||
for t = 0, 23 do o:value(t .. ":00") end
|
||||
o.default = "0:00"
|
||||
o.datatype = "timehhmm"
|
||||
o:depends("update_week_mode", "0")
|
||||
o:depends("update_week_mode", "1")
|
||||
o:depends("update_week_mode", "2")
|
||||
o:depends("update_week_mode", "3")
|
||||
o:depends("update_week_mode", "4")
|
||||
o:depends("update_week_mode", "5")
|
||||
o:depends("update_week_mode", "6")
|
||||
o:depends("update_week_mode", "7")
|
||||
|
||||
o = s:option(ListValue, "update_interval_mode", translate("Update Interval(hour)"))
|
||||
for t = 1, 24 do o:value(t, t .. " " .. translate("hour")) end
|
||||
o.default = 2
|
||||
o:depends("update_week_mode", "8")
|
||||
o.rmempty = true
|
||||
|
||||
o = s:option(ListValue, "access_mode", translate("Subscribe URL Access Method"))
|
||||
o.default = ""
|
||||
o:value("", translate("Auto"))
|
||||
o:value("direct", translate("Direct Connection"))
|
||||
o:value("proxy", translate("Proxy"))
|
||||
|
||||
o = s:option(Value, "user_agent", translate("User-Agent"))
|
||||
o.default = "passwall"
|
||||
o:value("passwall", "PassWall")
|
||||
o:value("v2rayN/9.99", "v2rayN")
|
||||
o:value("clash.meta/v9.99.99", "Clash.Meta")
|
||||
o:value("Shadowrocket/9999 CFNetwork/3896.100.1.2.1 Darwin/27.0.0 iPhone19,7", "Shadowrocket")
|
||||
o:value("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0", "Edge for Linux")
|
||||
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0", "Edge for Windows")
|
||||
|
||||
o = s:option(ListValue, "chain_proxy", translate("Chain Proxy"))
|
||||
o:value("", translate("Close(Not use)"))
|
||||
o:value("1", translate("Preproxy Node"))
|
||||
o:value("2", translate("Landing Node"))
|
||||
o:value("3", translate("Outbound Interface"))
|
||||
|
||||
local descrStr = "Chained proxy works only with Xray or Sing-box nodes.<br>"
|
||||
descrStr = descrStr .. "You can only use manual or imported nodes as chained nodes."
|
||||
descrStr = translate(descrStr) .. "<br>" .. translate("Only support a layer of proxy.")
|
||||
|
||||
o1 = s:option(ListValue, "preproxy_node", translate("Preproxy Node"))
|
||||
o1:depends({ ["chain_proxy"] = "1" })
|
||||
o1.description = descrStr
|
||||
o1.template = m:template_path("/cbi/nodes_listvalue")
|
||||
o1.group = {}
|
||||
|
||||
o2 = s:option(ListValue, "to_node", translate("Landing Node"))
|
||||
o2:depends({ ["chain_proxy"] = "2" })
|
||||
o2.description = descrStr
|
||||
o2.template = m:template_path("/cbi/nodes_listvalue")
|
||||
o2.group = {}
|
||||
|
||||
o3 = s:option(Value, "outbound_iface", translate("Outbound Interface"))
|
||||
o3:depends({ ["chain_proxy"] = "3" })
|
||||
o3:value("", translate("All"))
|
||||
local iface = api.get_network_devices()
|
||||
for _, d in ipairs(iface) do
|
||||
o3:value(d.name, d.label)
|
||||
end
|
||||
|
||||
for k, v in pairs(nodes_table) do
|
||||
if (v.type == "Xray" or v.type == "sing-box") and (not v.chain_proxy or v.chain_proxy == "") and v.add_mode ~= "2" then
|
||||
o1:value(v.id, v.remark)
|
||||
o1.group[#o1.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
o2:value(v.id, v.remark)
|
||||
o2.group[#o2.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
end
|
||||
end
|
||||
|
||||
return api.return_map(m)
|
||||
@@ -1,261 +0,0 @@
|
||||
local api = require "luci.passwall.api"
|
||||
api.set_default_cbi()
|
||||
|
||||
local fs = api.fs
|
||||
local has_singbox = api.finded_com("sing-box")
|
||||
local has_xray = api.finded_com("xray")
|
||||
|
||||
local port_validate = function(self, value, t)
|
||||
return value:gsub("-", ":")
|
||||
end
|
||||
|
||||
m = Map()
|
||||
|
||||
-- [[ Delay Settings ]]--
|
||||
s = m:section(NamedSection, "@global_delay[0]", "global_delay", translate("Delay Settings"))
|
||||
|
||||
---- Open and close Daemon
|
||||
o = s:option(Flag, "start_daemon", translate("Open and close Daemon"))
|
||||
o.default = 1
|
||||
o.rmempty = false
|
||||
|
||||
---- Delay Start
|
||||
o = s:option(Value, "start_delay", translate("Delay Start"), translate("Units:seconds"))
|
||||
o.default = "1"
|
||||
o.rmempty = true
|
||||
|
||||
for index, value in ipairs({"stop", "start", "restart"}) do
|
||||
o = s:option(ListValue, value .. "_week_mode", translate(value .. " automatically mode"))
|
||||
o:value("", translate("Disable"))
|
||||
if value == "restart" then
|
||||
o:value(8, translate("Loop Mode"))
|
||||
end
|
||||
o:value(7, translate("Every day"))
|
||||
o:value(1, translate("Every Monday"))
|
||||
o:value(2, translate("Every Tuesday"))
|
||||
o:value(3, translate("Every Wednesday"))
|
||||
o:value(4, translate("Every Thursday"))
|
||||
o:value(5, translate("Every Friday"))
|
||||
o:value(6, translate("Every Saturday"))
|
||||
o:value(0, translate("Every Sunday"))
|
||||
o = s:option(Value, value .. "_time_mode", translate(value .. " Time"))
|
||||
for t = 0, 23 do o:value(t .. ":00") end
|
||||
o.default = "0:00"
|
||||
o.datatype = "timehhmm"
|
||||
o:depends(value .. "_week_mode", "0")
|
||||
o:depends(value .. "_week_mode", "1")
|
||||
o:depends(value .. "_week_mode", "2")
|
||||
o:depends(value .. "_week_mode", "3")
|
||||
o:depends(value .. "_week_mode", "4")
|
||||
o:depends(value .. "_week_mode", "5")
|
||||
o:depends(value .. "_week_mode", "6")
|
||||
o:depends(value .. "_week_mode", "7")
|
||||
o = s:option(ListValue, value .. "_interval_mode", translate(value .. " Interval(Hour)"))
|
||||
for t = 1, 24 do o:value(t, t .. " " .. translate("Hour")) end
|
||||
o.default = 2
|
||||
o:depends(value .. "_week_mode", "8")
|
||||
end
|
||||
|
||||
-- [[ Forwarding Settings ]]--
|
||||
s = m:section(NamedSection, "@global_forwarding[0]", "global_forwarding", translate("Forwarding Settings"))
|
||||
|
||||
---- TCP No Redir Ports
|
||||
o = s:option(Value, "tcp_no_redir_ports", translate("TCP No Redir Ports"))
|
||||
o.default = "disable"
|
||||
o:value("disable", translate("No patterns are used"))
|
||||
o:value("1:65535", translate("All"))
|
||||
o.validate = port_validate
|
||||
|
||||
---- UDP No Redir Ports
|
||||
o = s:option(Value, "udp_no_redir_ports", translate("UDP No Redir Ports"),
|
||||
"<font color='red'>" .. translate(
|
||||
"Fill in the ports you don't want to be forwarded by the agent, with the highest priority.") ..
|
||||
"</font>")
|
||||
o.default = "disable"
|
||||
o:value("disable", translate("No patterns are used"))
|
||||
o:value("1:65535", translate("All"))
|
||||
o.validate = port_validate
|
||||
|
||||
---- TCP Proxy Drop Ports
|
||||
o = s:option(Value, "tcp_proxy_drop_ports", translate("TCP Proxy Drop Ports"))
|
||||
o.default = "disable"
|
||||
o:value("disable", translate("No patterns are used"))
|
||||
o.validate = port_validate
|
||||
|
||||
---- UDP Proxy Drop Ports
|
||||
o = s:option(Value, "udp_proxy_drop_ports", translate("UDP Proxy Drop Ports"))
|
||||
o.default = "443"
|
||||
o:value("disable", translate("No patterns are used"))
|
||||
o:value("443", translate("QUIC"))
|
||||
o.validate = port_validate
|
||||
|
||||
---- TCP Redir Ports
|
||||
o = s:option(Value, "tcp_redir_ports", translate("TCP Redir Ports"))
|
||||
o:value("1:65535", translate("All"))
|
||||
o:value("22,25,53,80,143,443,465,587,853,873,993,995,5222,8080,8443,9418", translate("Common Use"))
|
||||
o:value("80,443", translate("Only Web"))
|
||||
o.default = o.keylist[2]
|
||||
o.validate = port_validate
|
||||
|
||||
---- UDP Redir Ports
|
||||
o = s:option(Value, "udp_redir_ports", translate("UDP Redir Ports"))
|
||||
o.default = "1:65535"
|
||||
o:value("1:65535", translate("All"))
|
||||
o:value("53", "DNS")
|
||||
o.validate = port_validate
|
||||
|
||||
o = s:option(DummyValue, "tips", " ")
|
||||
o.rawhtml = true
|
||||
o.cfgvalue = function(t, n)
|
||||
return string.format('<font color="red">%s</font>',
|
||||
translate("The port settings support single ports and ranges.<br>Separate multiple ports with commas (,).<br>Example: 21,80,443,1000:2000."))
|
||||
end
|
||||
|
||||
---- Use nftables
|
||||
o = s:option(ListValue, "prefer_nft", translate("Prefer firewall tools"))
|
||||
o.default = "1"
|
||||
o:value("0", "Iptables")
|
||||
o:value("1", "Nftables")
|
||||
|
||||
---- Check the transparent proxy component
|
||||
local handle = io.popen("lsmod")
|
||||
local mods = handle and handle:read("*a") or ""
|
||||
if handle then handle:close() end
|
||||
|
||||
if (mods:find("REDIRECT") and mods:find("TPROXY")) or (mods:find("nft_redir") and mods:find("nft_tproxy")) then
|
||||
o = s:option(ListValue, "tcp_proxy_way", translate("TCP Proxy Way"))
|
||||
o.default = "redirect"
|
||||
o:value("redirect", "REDIRECT")
|
||||
o:value("tproxy", "TPROXY")
|
||||
o:depends("ipv6_tproxy", false)
|
||||
o.remove = function(self, section)
|
||||
-- 禁止在隐藏时删除
|
||||
end
|
||||
|
||||
o = s:option(ListValue, "_tcp_proxy_way", translate("TCP Proxy Way"))
|
||||
o.default = "tproxy"
|
||||
o:value("tproxy", "TPROXY")
|
||||
o:depends("ipv6_tproxy", true)
|
||||
o.write = function(self, section, value)
|
||||
self.map:set(section, "tcp_proxy_way", value)
|
||||
end
|
||||
|
||||
if mods:find("ip6table_mangle") or mods:find("nft_tproxy") then
|
||||
---- IPv6 TProxy
|
||||
o = s:option(Flag, "ipv6_tproxy", translate("IPv6 TProxy"),
|
||||
"<font color='red'>" .. translate(
|
||||
"Experimental feature. Make sure that your node supports IPv6.") ..
|
||||
"</font>")
|
||||
o.default = 0
|
||||
o.rmempty = false
|
||||
end
|
||||
end
|
||||
|
||||
o = s:option(Flag, "accept_icmp", translate("Hijacking ICMP (PING)"))
|
||||
o.default = 0
|
||||
|
||||
o = s:option(Flag, "accept_icmpv6", translate("Hijacking ICMPv6 (IPv6 PING)"))
|
||||
o:depends("ipv6_tproxy", true)
|
||||
o.default = 0
|
||||
|
||||
if has_xray then
|
||||
s_xray = m:section(NamedSection, "@global_xray[0]", "global_xray", "Xray " .. translate("Settings"))
|
||||
|
||||
o = s_xray:option(Flag, "fragment", translate("Fragment"), translate("TCP fragments, which can deceive the censorship system in some cases, such as bypassing SNI blacklists."))
|
||||
o.default = 0
|
||||
|
||||
o = s_xray:option(ListValue, "fragment_packets", translate("Fragment Packets"), translate("\"1-3\" is for segmentation at TCP layer, applying to the beginning 1 to 3 data writes by the client. \"tlshello\" is for TLS client hello packet fragmentation."))
|
||||
o.default = "tlshello"
|
||||
o:value("tlshello", "tlshello")
|
||||
o:value("1-1", "1-1")
|
||||
o:value("1-2", "1-2")
|
||||
o:value("1-3", "1-3")
|
||||
o:value("1-5", "1-5")
|
||||
o:depends("fragment", true)
|
||||
|
||||
o = s_xray:option(Value, "fragment_lengths", translate("Fragment Length"), translate("Fragmented packet length (byte)"))
|
||||
o.default = "3-5,6-8,10-20"
|
||||
o:depends("fragment", true)
|
||||
|
||||
o = s_xray:option(Value, "fragment_delays", translate("Fragment Delay"), translate("Fragmentation interval (ms)"))
|
||||
o.default = "10-20"
|
||||
o:depends("fragment", true)
|
||||
|
||||
o = s_xray:option(Value, "fragment_maxSplit", translate("Max Split"), translate("Limit the maximum number of splits."))
|
||||
o.datatype = "or(uinteger,portrange)"
|
||||
o.default = "3-6"
|
||||
o:depends("fragment", true)
|
||||
|
||||
o = s_xray:option(Flag, "noise", translate("Noise"), translate("UDP noise, Under some circumstances it can bypass some UDP based protocol restrictions."))
|
||||
o.default = 0
|
||||
|
||||
o = s_xray:option(Flag, "sniffing_override_dest", translate("Override the connection destination address"))
|
||||
o.default = 0
|
||||
o.description = translate("Override the connection destination address with the sniffed domain.<br />Otherwise use sniffed domain for routing only.<br />If using shunt nodes, configure the domain shunt rules correctly.")
|
||||
|
||||
local domains_excluded = string.format("/usr/share/%s/rules/domains_excluded", m.config)
|
||||
o = s_xray:option(TextValue, "excluded_domains", translate("Excluded Domains"), translate("If the traffic sniffing result is in this list, the destination address will not be overridden."))
|
||||
o.rows = 15
|
||||
o.wrap = "off"
|
||||
o.cfgvalue = function(self, section) return fs.readfile(domains_excluded) or "" end
|
||||
o.write = function(self, section, value) fs.writefile(domains_excluded, value:gsub("\r\n", "\n")) end
|
||||
o:depends({sniffing_override_dest = true})
|
||||
|
||||
o = s_xray:option(Value, "buffer_size", translate("Buffer Size"), translate("Buffer size for every connection (kB)"))
|
||||
o.datatype = "uinteger"
|
||||
|
||||
s_xray_noise = m:section(TypedSection, "xray_noise_packets", translate("Xray Noise Packets"),"<font color='red'>" .. translate("To send noise packets, select \"Noise\" in Xray Settings.") .. "</font>")
|
||||
s_xray_noise.template = "cbi/tblsection"
|
||||
s_xray_noise.sortable = true
|
||||
s_xray_noise.anonymous = true
|
||||
s_xray_noise.addremove = true
|
||||
|
||||
s_xray_noise.create = function(e, t)
|
||||
local uid = "xray_noise_" .. api.gen_random_char(5)
|
||||
TypedSection.create(e, uid)
|
||||
end
|
||||
|
||||
s_xray_noise.remove = function(self, section)
|
||||
local o = m:get(section) or {}
|
||||
if o[".type"] == self.sectiontype then
|
||||
for k, v in pairs(self.children) do
|
||||
v.rmempty = true
|
||||
v.validate = nil
|
||||
end
|
||||
TypedSection.remove(self, section)
|
||||
end
|
||||
end
|
||||
|
||||
o = s_xray_noise:option(Flag, "enabled", translate("Enable"))
|
||||
o.default = 1
|
||||
o.rmempty = false
|
||||
|
||||
o = s_xray_noise:option(ListValue, "type", translate("Type"))
|
||||
o:value("rand", "rand")
|
||||
o:value("array", "array")
|
||||
o:value("str", "str")
|
||||
o:value("hex", "hex")
|
||||
o:value("base64", "base64")
|
||||
|
||||
o = s_xray_noise:option(Value, "packet", translate("Packet | Rand Length"))
|
||||
o.datatype = "minlength(1)"
|
||||
o.rmempty = false
|
||||
|
||||
o = s_xray_noise:option(Value, "delay", translate("Delay (ms)"))
|
||||
o.datatype = "or(uinteger,portrange)"
|
||||
o.rmempty = false
|
||||
end
|
||||
|
||||
if has_singbox then
|
||||
s = m:section(NamedSection, "@global_singbox[0]", "global_singbox", "Sing-Box " .. translate("Settings"))
|
||||
|
||||
o = s:option(Flag, "record_fragment", "TLS Record " .. translate("Fragment"),
|
||||
translate("Split handshake data into multiple TLS records for better censorship evasion. Low overhead. Recommended to enable first."))
|
||||
o.default = 0
|
||||
|
||||
o = s:option(Flag, "fragment", "TLS TCP " .. translate("Fragment"),
|
||||
translate("Split handshake into multiple TCP segments. Enhances obfuscation. May increase delay. Use only if needed."))
|
||||
o.default = 0
|
||||
end
|
||||
|
||||
return api.return_map(m)
|
||||
@@ -1,165 +0,0 @@
|
||||
local api = require "luci.passwall.api"
|
||||
local has_xray = api.finded_com("xray")
|
||||
local has_singbox = api.finded_com("sing-box")
|
||||
api.set_default_cbi()
|
||||
|
||||
m = Map()
|
||||
|
||||
-- [[ Rule Settings ]]--
|
||||
s = m:section(NamedSection, "@global_rules[0]", "global_rules", translate("Rule status"))
|
||||
|
||||
--[[
|
||||
o = s:option(Flag, "adblock", translate("Enable adblock"))
|
||||
o.rmempty = false
|
||||
]]--
|
||||
|
||||
---- gfwlist URL
|
||||
o = s:option(DynamicList, "gfwlist_url", translate("GFW domains(gfwlist) Update URL"))
|
||||
o:depends("geo2rule", false)
|
||||
o:value("https://cdn.jsdelivr.net/gh/YW5vbnltb3Vz/domain-list-community@release/gfwlist.txt", translate("v2fly/domain-list-community"))
|
||||
o:value("https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/gfw.txt", translate("Loyalsoldier/v2ray-rules-dat"))
|
||||
o:value("https://cdn.jsdelivr.net/gh/Loukky/gfwlist-by-loukky/gfwlist.txt", translate("Loukky/gfwlist-by-loukky"))
|
||||
o:value("https://cdn.jsdelivr.net/gh/gfwlist/gfwlist/gfwlist.txt", translate("gfwlist/gfwlist"))
|
||||
o:value("https://cdn.jsdelivr.net/gh/pexcn/daily@gh-pages/gfwlist/gfwlist.txt", translate("pexcn/gfwlist"))
|
||||
o.default = o.keylist[2]
|
||||
|
||||
----chnroute URL
|
||||
o = s:option(DynamicList, "chnroute_url", translate("China IPs(chnroute) Update URL"))
|
||||
o:depends("geo2rule", false)
|
||||
o:value("https://ispip.clang.cn/all_cn.txt", translate("Clang.CN"))
|
||||
o:value("https://cdn.jsdelivr.net/gh/gaoyifan/china-operator-ip@ip-lists/china.txt", translate("gaoyifan/china-operator-ip/china"))
|
||||
o:value("https://cdn.jsdelivr.net/gh/soffchen/GeoIP2-CN@release/CN-ip-cidr.txt", translate("soffchen/GeoIP2-CN"))
|
||||
o:value("https://cdn.jsdelivr.net/gh/Hackl0us/GeoIP2-CN@release/CN-ip-cidr.txt", translate("Hackl0us/GeoIP2-CN"))
|
||||
o:value("https://cdn.jsdelivr.net/gh/blackmatrix7/ios_rule_script@master/rule/Clash/ChinaMax/ChinaMax_IP_No_IPv6.txt", translate("ios_rule_script/ChinaMax_IP_No_IPv6"))
|
||||
o:value("https://cdn.jsdelivr.net/gh/pexcn/daily@gh-pages/chnroute/chnroute.txt", translate("pexcn/chnroute"))
|
||||
|
||||
----chnroute6 URL
|
||||
o = s:option(DynamicList, "chnroute6_url", translate("China IPv6s(chnroute6) Update URL"))
|
||||
o:depends("geo2rule", false)
|
||||
o:value("https://ispip.clang.cn/all_cn_ipv6.txt", translate("Clang.CN.IPv6"))
|
||||
o:value("https://cdn.jsdelivr.net/gh/gaoyifan/china-operator-ip@ip-lists/china6.txt", translate("gaoyifan/china-operator-ip/china6"))
|
||||
o:value("https://cdn.jsdelivr.net/gh/blackmatrix7/ios_rule_script@master/rule/Clash/ChinaMax/ChinaMax_IP.txt", translate("ios_rule_script/ChinaMax_IP"))
|
||||
o:value("https://cdn.jsdelivr.net/gh/pexcn/daily@gh-pages/chnroute/chnroute6.txt", translate("pexcn/chnroute6"))
|
||||
|
||||
----chnlist URL
|
||||
o = s:option(DynamicList, "chnlist_url", translate("China List(Chnlist) Update URL"))
|
||||
o:depends("geo2rule", false)
|
||||
o:value("https://cdn.jsdelivr.net/gh/felixonmars/dnsmasq-china-list/accelerated-domains.china.conf", translate("felixonmars/domains.china"))
|
||||
o:value("https://cdn.jsdelivr.net/gh/felixonmars/dnsmasq-china-list/apple.china.conf", translate("felixonmars/apple.china"))
|
||||
o:value("https://cdn.jsdelivr.net/gh/felixonmars/dnsmasq-china-list/google.china.conf", translate("felixonmars/google.china"))
|
||||
o:value("https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/china-list.txt", translate("Loyalsoldier/china-list"))
|
||||
o:value("https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/apple-cn.txt", translate("Loyalsoldier/apple-cn"))
|
||||
o:value("https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/google-cn.txt", translate("Loyalsoldier/google-cn"))
|
||||
o:value("https://cdn.jsdelivr.net/gh/blackmatrix7/ios_rule_script@master/rule/Clash/ChinaMax/ChinaMax_Domain.txt", translate("ios_rule_script/ChinaMax_Domain"))
|
||||
o:value("https://cdn.jsdelivr.net/gh/pexcn/daily@gh-pages/chinalist/chinalist.txt", translate("pexcn/chinalist"))
|
||||
|
||||
if has_xray or has_singbox then
|
||||
o = s:option(Value, "geoip_url", translate("GeoIP Update URL"))
|
||||
o:value("https://github.com/Loyalsoldier/geoip/releases/latest/download/geoip.dat", translate("Loyalsoldier/geoip"))
|
||||
o:value("https://github.com/MetaCubeX/meta-rules-dat/releases/latest/download/geoip.dat", translate("MetaCubeX/geoip"))
|
||||
o:value("https://cdn.jsdelivr.net/gh/Loyalsoldier/geoip@release/geoip.dat", translate("Loyalsoldier/geoip (CDN)"))
|
||||
o:value("https://cdn.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat", translate("MetaCubeX/geoip (CDN)"))
|
||||
o.default = o.keylist[1]
|
||||
|
||||
o = s:option(Value, "geosite_url", translate("Geosite Update URL"))
|
||||
o:value("https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat", translate("Loyalsoldier/geosite"))
|
||||
o:value("https://github.com/MetaCubeX/meta-rules-dat/releases/latest/download/geosite.dat", translate("MetaCubeX/geosite"))
|
||||
o:value("https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat", translate("Loyalsoldier/geosite (CDN)"))
|
||||
o:value("https://cdn.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat", translate("MetaCubeX/geosite (CDN)"))
|
||||
o.default = o.keylist[1]
|
||||
|
||||
o = s:option(Value, "v2ray_location_asset", translate("Location of Geo rule files"), translate("This variable specifies a directory where geoip.dat and geosite.dat files are."))
|
||||
o.default = "/usr/share/v2ray/"
|
||||
o.placeholder = "/usr/share/v2ray/"
|
||||
o.rmempty = false
|
||||
|
||||
if api.is_finded("geoview") then
|
||||
o = s:option(Flag, "geo2rule", translate("Generate Rule List from Geo"))
|
||||
o.default = 0
|
||||
o.rmempty = false
|
||||
o.description = translate("Generate rule lists such as GFW, China domains, and China IP ranges based on Geo files.") .. "<br><font color='red'>" ..
|
||||
translate("When manually updating with this option enabled, rules will be regenerated from existing Geo files even if no new version is available.") .. "</font>"
|
||||
|
||||
o = s:option(Flag, "enable_geoview", translate("Enable Geo Data Parsing"))
|
||||
o.default = 0
|
||||
o.rmempty = false
|
||||
o.description = "<ul>"
|
||||
.. "<li>" .. translate("Experimental feature.") .. "</li>"
|
||||
.. "<li>" .. "1." .. translate("Parses and preloads GeoIP/Geosite data to improve Sing-box/Xray routing performance.") .. "</li>"
|
||||
.. "<li>" .. "2." .. translate("Once enabled, the rule list can support GeoIP/Geosite rules.") .. "</li>"
|
||||
.. "</ul>"
|
||||
function o.write(self, section, value)
|
||||
local old = m:get(section, self.option) or "0"
|
||||
if old ~= value then
|
||||
m:set("@global[0]", "flush_set", "1")
|
||||
end
|
||||
return Flag.write(self, section, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
o = s:option(ListValue, "update_week_mode", translate("Auto Update Mode"))
|
||||
o:value("", translate("Disable"))
|
||||
o:value(8, translate("Loop Mode"))
|
||||
o:value(7, translate("Every day"))
|
||||
o:value(1, translate("Every Monday"))
|
||||
o:value(2, translate("Every Tuesday"))
|
||||
o:value(3, translate("Every Wednesday"))
|
||||
o:value(4, translate("Every Thursday"))
|
||||
o:value(5, translate("Every Friday"))
|
||||
o:value(6, translate("Every Saturday"))
|
||||
o:value(0, translate("Every Sunday"))
|
||||
|
||||
o = s:option(Value, "update_time_mode", translate("Update Time"))
|
||||
for t = 0, 23 do o:value(t .. ":00") end
|
||||
o.default = "0:00"
|
||||
o.datatype = "timehhmm"
|
||||
o:depends("update_week_mode", "0")
|
||||
o:depends("update_week_mode", "1")
|
||||
o:depends("update_week_mode", "2")
|
||||
o:depends("update_week_mode", "3")
|
||||
o:depends("update_week_mode", "4")
|
||||
o:depends("update_week_mode", "5")
|
||||
o:depends("update_week_mode", "6")
|
||||
o:depends("update_week_mode", "7")
|
||||
|
||||
o = s:option(ListValue, "update_interval_mode", translate("Update Interval(hour)"))
|
||||
for t = 1, 24 do o:value(t, t .. " " .. translate("hour")) end
|
||||
o.default = 2
|
||||
o:depends("update_week_mode", "8")
|
||||
o.rmempty = true
|
||||
|
||||
---- 更新选项,始终被js隐藏
|
||||
local flags = {
|
||||
"gfwlist_update", "chnroute_update", "chnroute6_update",
|
||||
"chnlist_update", "geoip_update", "geosite_update"
|
||||
}
|
||||
for _, f in ipairs(flags) do
|
||||
o = s:option(Flag, f)
|
||||
o.rmempty = false
|
||||
end
|
||||
|
||||
s:appendTemplate("/rule/rule_version")
|
||||
|
||||
if has_xray or has_singbox then
|
||||
m:appendTemplate("/rule/shunt_rule_list")
|
||||
|
||||
if luci.http.formvalue("cbi.submit") == "1" then
|
||||
local group_order = luci.http.formvaluetable("group.order")
|
||||
if group_order then
|
||||
for k, v in pairs(group_order) do
|
||||
if v and v~= "" then
|
||||
local new_order = {}
|
||||
string.gsub(v, "[^" .. " " .. "]+", function(w)
|
||||
new_order[#new_order + 1] = w
|
||||
end)
|
||||
for idx, name in ipairs(new_order) do
|
||||
m.uci:reorder(m.config, name, idx - 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return api.return_map(m)
|
||||
@@ -1,338 +0,0 @@
|
||||
local api = require "luci.passwall.api"
|
||||
local fs = api.fs
|
||||
local sys = api.sys
|
||||
local datatypes = api.datatypes
|
||||
local path = string.format("/usr/share/%s/rules/", api.appname)
|
||||
local gfwlist_path = path .. "gfwlist"
|
||||
local chnlist_path = path .. "chnlist"
|
||||
local chnroute_path = path .. "chnroute"
|
||||
|
||||
api.set_default_cbi()
|
||||
|
||||
m = Map()
|
||||
m.apply_on_parse = true
|
||||
|
||||
function clean_text(text)
|
||||
local nbsp = string.char(0xC2, 0xA0) -- 不间断空格(U+00A0)
|
||||
local fullwidth_space = string.char(0xE3, 0x80, 0x80) -- 全角空格(U+3000)
|
||||
return text
|
||||
:gsub("\t", " ")
|
||||
:gsub(nbsp, " ")
|
||||
:gsub(fullwidth_space, " ")
|
||||
:gsub("^%s+", "")
|
||||
:gsub("%s+$", "\n")
|
||||
:gsub("\r\n", "\n")
|
||||
:gsub("[ \t]*\n[ \t]*", "\n")
|
||||
end
|
||||
|
||||
-- [[ Rule List Settings ]]--
|
||||
s = m:section(TypedSection, "global_rules")
|
||||
s.anonymous = true
|
||||
|
||||
s:tab("direct_list", translate("Direct List"))
|
||||
s:tab("proxy_list", translate("Proxy List"))
|
||||
s:tab("block_list", translate("Block List"))
|
||||
s:tab("lan_ip_list", translate("Lan IP List"))
|
||||
s:tab("route_hosts", translate("Route Hosts"))
|
||||
|
||||
---- Direct Hosts
|
||||
local direct_host = path .. "direct_host"
|
||||
o = s:taboption("direct_list", TextValue, "direct_host", "", "<font color='red'>" .. translate("Join the direct hosts list of domain names will not proxy.") .. "</font>")
|
||||
o.rows = 15
|
||||
o.wrap = "off"
|
||||
o.cfgvalue = function(self, section)
|
||||
return fs.readfile(direct_host) or ""
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
fs.writefile(direct_host, value:gsub("\r\n", "\n"))
|
||||
sys.call("rm -rf /tmp/etc/passwall_tmp/dns_*")
|
||||
end
|
||||
o.remove = function(self, section, value)
|
||||
fs.writefile(direct_host, "")
|
||||
sys.call("rm -rf /tmp/etc/passwall_tmp/dns_*")
|
||||
end
|
||||
o.validate = function(self, value)
|
||||
local hosts= {}
|
||||
value = clean_text(value)
|
||||
string.gsub(value, '[^' .. "\r\n" .. ']+', function(w) table.insert(hosts, api.trim(w)) end)
|
||||
for index, host in ipairs(hosts) do
|
||||
if host ~= "" and not host:find("^#") and not host:find("^geosite:") then
|
||||
if not datatypes.hostname(host) then
|
||||
return nil, host .. " " .. translate("Not valid domain name, please re-enter!")
|
||||
end
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
---- Direct IP
|
||||
local direct_ip = path .. "direct_ip"
|
||||
o = s:taboption("direct_list", TextValue, "direct_ip", "", "<font color='red'>" .. translate("These had been joined ip addresses will not proxy. Please input the ip address or ip address segment,every line can input only one ip address. For example: 192.168.0.0/24 or 223.5.5.5.") .. "</font>")
|
||||
o.rows = 15
|
||||
o.wrap = "off"
|
||||
o.cfgvalue = function(self, section)
|
||||
return fs.readfile(direct_ip) or ""
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
fs.writefile(direct_ip, value:gsub("\r\n", "\n"))
|
||||
end
|
||||
o.remove = function(self, section, value)
|
||||
fs.writefile(direct_ip, "")
|
||||
end
|
||||
o.validate = function(self, value)
|
||||
local ipmasks= {}
|
||||
value = clean_text(value)
|
||||
string.gsub(value, '[^' .. "\r\n" .. ']+', function(w) table.insert(ipmasks, api.trim(w)) end)
|
||||
for index, ipmask in ipairs(ipmasks) do
|
||||
if ipmask ~= "" and not ipmask:find("^#") and not ipmask:find("^geoip:") then
|
||||
if not ( datatypes.ipmask4(ipmask) or datatypes.ipmask6(ipmask) ) then
|
||||
return nil, ipmask .. " " .. translate("Not valid IP format, please re-enter!")
|
||||
end
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
---- Proxy Hosts
|
||||
local proxy_host = path .. "proxy_host"
|
||||
o = s:taboption("proxy_list", TextValue, "proxy_host", "", "<font color='red'>" .. translate("These had been joined websites will use proxy. Please input the domain names of websites, every line can input only one website domain. For example: google.com.") .. "</font>")
|
||||
o.rows = 15
|
||||
o.wrap = "off"
|
||||
o.cfgvalue = function(self, section)
|
||||
return fs.readfile(proxy_host) or ""
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
fs.writefile(proxy_host, value:gsub("\r\n", "\n"))
|
||||
sys.call("rm -rf /tmp/etc/passwall_tmp/dns_*")
|
||||
end
|
||||
o.remove = function(self, section, value)
|
||||
fs.writefile(proxy_host, "")
|
||||
sys.call("rm -rf /tmp/etc/passwall_tmp/dns_*")
|
||||
end
|
||||
o.validate = function(self, value)
|
||||
local hosts= {}
|
||||
value = clean_text(value)
|
||||
string.gsub(value, '[^' .. "\r\n" .. ']+', function(w) table.insert(hosts, api.trim(w)) end)
|
||||
for index, host in ipairs(hosts) do
|
||||
if host ~= "" and not host:find("^#") and not host:find("^geosite:") then
|
||||
if not datatypes.hostname(host) then
|
||||
return nil, host .. " " .. translate("Not valid domain name, please re-enter!")
|
||||
end
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
---- Proxy IP
|
||||
local proxy_ip = path .. "proxy_ip"
|
||||
o = s:taboption("proxy_list", TextValue, "proxy_ip", "", "<font color='red'>" .. translate("These had been joined ip addresses will use proxy. Please input the ip address or ip address segment, every line can input only one ip address. For example: 35.24.0.0/24 or 8.8.4.4.") .. "</font>")
|
||||
o.rows = 15
|
||||
o.wrap = "off"
|
||||
o.cfgvalue = function(self, section)
|
||||
return fs.readfile(proxy_ip) or ""
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
fs.writefile(proxy_ip, value:gsub("\r\n", "\n"))
|
||||
end
|
||||
o.remove = function(self, section, value)
|
||||
fs.writefile(proxy_ip, "")
|
||||
end
|
||||
o.validate = function(self, value)
|
||||
local ipmasks= {}
|
||||
value = clean_text(value)
|
||||
string.gsub(value, '[^' .. "\r\n" .. ']+', function(w) table.insert(ipmasks, api.trim(w)) end)
|
||||
for index, ipmask in ipairs(ipmasks) do
|
||||
if ipmask ~= "" and not ipmask:find("^#") and not ipmask:find("^geoip:") then
|
||||
if not ( datatypes.ipmask4(ipmask) or datatypes.ipmask6(ipmask) ) then
|
||||
return nil, ipmask .. " " .. translate("Not valid IP format, please re-enter!")
|
||||
end
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
---- Block Hosts
|
||||
local block_host = path .. "block_host"
|
||||
o = s:taboption("block_list", TextValue, "block_host", "", "<font color='red'>" .. translate("These had been joined websites will be block. Please input the domain names of websites, every line can input only one website domain. For example: twitter.com.") .. "</font>")
|
||||
o.rows = 15
|
||||
o.wrap = "off"
|
||||
o.cfgvalue = function(self, section)
|
||||
return fs.readfile(block_host) or ""
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
fs.writefile(block_host, value:gsub("\r\n", "\n"))
|
||||
end
|
||||
o.remove = function(self, section, value)
|
||||
fs.writefile(block_host, "")
|
||||
end
|
||||
o.validate = function(self, value)
|
||||
local hosts= {}
|
||||
value = clean_text(value)
|
||||
string.gsub(value, '[^' .. "\r\n" .. ']+', function(w) table.insert(hosts, api.trim(w)) end)
|
||||
for index, host in ipairs(hosts) do
|
||||
if host ~= "" and not host:find("^#") and not host:find("^geosite:") then
|
||||
if not datatypes.hostname(host) then
|
||||
return nil, host .. " " .. translate("Not valid domain name, please re-enter!")
|
||||
end
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
---- Block IP
|
||||
local block_ip = path .. "block_ip"
|
||||
o = s:taboption("block_list", TextValue, "block_ip", "", "<font color='red'>" .. translate("These had been joined ip addresses will be block. Please input the ip address or ip address segment, every line can input only one ip address.") .. "</font>")
|
||||
o.rows = 15
|
||||
o.wrap = "off"
|
||||
o.cfgvalue = function(self, section)
|
||||
return fs.readfile(block_ip) or ""
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
fs.writefile(block_ip, value:gsub("\r\n", "\n"))
|
||||
end
|
||||
o.remove = function(self, section, value)
|
||||
fs.writefile(block_ip, "")
|
||||
end
|
||||
o.validate = function(self, value)
|
||||
local ipmasks= {}
|
||||
value = clean_text(value)
|
||||
string.gsub(value, '[^' .. "\r\n" .. ']+', function(w) table.insert(ipmasks, api.trim(w)) end)
|
||||
for index, ipmask in ipairs(ipmasks) do
|
||||
if ipmask ~= "" and not ipmask:find("^#") and not ipmask:find("^geoip:") then
|
||||
if not ( datatypes.ipmask4(ipmask) or datatypes.ipmask6(ipmask) ) then
|
||||
return nil, ipmask .. " " .. translate("Not valid IP format, please re-enter!")
|
||||
end
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
---- Lan IPv4
|
||||
local lanlist_ipv4 = path .. "lanlist_ipv4"
|
||||
o = s:taboption("lan_ip_list", TextValue, "lanlist_ipv4", "", "<font color='red'>" .. translate("The list is the IPv4 LAN IP list, which represents the direct connection IP of the LAN. If you need the LAN IP in the proxy list, please clear it from the list. Do not modify this list by default.") .. "</font>")
|
||||
o.rows = 15
|
||||
o.wrap = "off"
|
||||
o.cfgvalue = function(self, section)
|
||||
return fs.readfile(lanlist_ipv4) or ""
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
fs.writefile(lanlist_ipv4, value:gsub("\r\n", "\n"))
|
||||
end
|
||||
o.remove = function(self, section, value)
|
||||
fs.writefile(lanlist_ipv4, "")
|
||||
end
|
||||
o.validate = function(self, value)
|
||||
local ipmasks= {}
|
||||
value = clean_text(value)
|
||||
string.gsub(value, '[^' .. "\r\n" .. ']+', function(w) table.insert(ipmasks, api.trim(w)) end)
|
||||
for index, ipmask in ipairs(ipmasks) do
|
||||
if ipmask ~= "" and not ipmask:find("^#") then
|
||||
if not datatypes.ipmask4(ipmask) then
|
||||
return nil, ipmask .. " " .. translate("Not valid IPv4 format, please re-enter!")
|
||||
end
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
---- Lan IPv6
|
||||
local lanlist_ipv6 = path .. "lanlist_ipv6"
|
||||
o = s:taboption("lan_ip_list", TextValue, "lanlist_ipv6", "", "<font color='red'>" .. translate("The list is the IPv6 LAN IP list, which represents the direct connection IP of the LAN. If you need the LAN IP in the proxy list, please clear it from the list. Do not modify this list by default.") .. "</font>")
|
||||
o.rows = 15
|
||||
o.wrap = "off"
|
||||
o.cfgvalue = function(self, section)
|
||||
return fs.readfile(lanlist_ipv6) or ""
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
fs.writefile(lanlist_ipv6, value:gsub("\r\n", "\n"))
|
||||
end
|
||||
o.remove = function(self, section, value)
|
||||
fs.writefile(lanlist_ipv6, "")
|
||||
end
|
||||
o.validate = function(self, value)
|
||||
local ipmasks= {}
|
||||
value = clean_text(value)
|
||||
string.gsub(value, '[^' .. "\r\n" .. ']+', function(w) table.insert(ipmasks, api.trim(w)) end)
|
||||
for index, ipmask in ipairs(ipmasks) do
|
||||
if ipmask ~= "" and not ipmask:find("^#") then
|
||||
if not datatypes.ipmask6(ipmask) then
|
||||
return nil, ipmask .. " " .. translate("Not valid IPv6 format, please re-enter!")
|
||||
end
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
---- Route Hosts
|
||||
local hosts = "/etc/hosts"
|
||||
o = s:taboption("route_hosts", TextValue, "hosts", "", "<font color='red'>" .. translate("Configure routing etc/hosts file, if you don't know what you are doing, please don't change the content.") .. "</font>")
|
||||
o.rows = 15
|
||||
o.wrap = "off"
|
||||
o.cfgvalue = function(self, section)
|
||||
return fs.readfile(hosts) or ""
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
fs.writefile(hosts, clean_text(value))
|
||||
end
|
||||
o.remove = function(self, section, value)
|
||||
fs.writefile(hosts, "")
|
||||
end
|
||||
|
||||
if fs.access(gfwlist_path) then
|
||||
s:tab("gfw_list", translate("GFW List"))
|
||||
o = s:taboption("gfw_list", DummyValue, "_gfw_fieldset")
|
||||
o.rawhtml = true
|
||||
o.default = string.format([[
|
||||
<div style="display: flex; align-items: center;">
|
||||
<input class="btn cbi-button cbi-button-add" type="button" onclick="read_gfw()" value="%s" />
|
||||
<label id="gfw_total_lines" style="margin-left: auto; margin-right: 10px;"></label>
|
||||
</div>
|
||||
<textarea id="gfw_textarea" class="cbi-input-textarea" style="width: 100%%; margin-top: 10px;" rows="40" wrap="off" readonly="readonly"></textarea>
|
||||
]], translate("Read List"))
|
||||
end
|
||||
|
||||
if fs.access(chnlist_path) then
|
||||
s:tab("chn_list", translate("China List") .. "(" .. translate("Domain") .. ")")
|
||||
o = s:taboption("chn_list", DummyValue, "_chn_fieldset")
|
||||
o.rawhtml = true
|
||||
o.default = string.format([[
|
||||
<div style="display: flex; align-items: center;">
|
||||
<input class="btn cbi-button cbi-button-add" type="button" onclick="read_chn()" value="%s" />
|
||||
<label id="chn_total_lines" style="margin-left: auto; margin-right: 10px;"></label>
|
||||
</div>
|
||||
<textarea id="chn_textarea" class="cbi-input-textarea" style="width: 100%%; margin-top: 10px;" rows="40" wrap="off" readonly="readonly"></textarea>
|
||||
]], translate("Read List"))
|
||||
end
|
||||
|
||||
if fs.access(chnroute_path) then
|
||||
s:tab("chnroute_list", translate("China List") .. "(IP)")
|
||||
o = s:taboption("chnroute_list", DummyValue, "_chnroute_fieldset")
|
||||
o.rawhtml = true
|
||||
o.default = string.format([[
|
||||
<div style="display: flex; align-items: center;">
|
||||
<input class="btn cbi-button cbi-button-add" type="button" onclick="read_chnroute()" value="%s" />
|
||||
<label id="chnroute_total_lines" style="margin-left: auto; margin-right: 10px;"></label>
|
||||
</div>
|
||||
<textarea id="chnroute_textarea" class="cbi-input-textarea" style="width: 100%%; margin-top: 10px;" rows="40" wrap="off" readonly="readonly"></textarea>
|
||||
]], translate("Read List"))
|
||||
end
|
||||
|
||||
m:appendTemplate("/rule_list/js")
|
||||
|
||||
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
|
||||
if api.compare_versions(api.get_app_version("geoview"), ">=", "0.1.0") then
|
||||
s:tab("geoview", translate("Geo View"))
|
||||
o = s:taboption("geoview", DummyValue, "_geoview_fieldset")
|
||||
o.rawhtml = true
|
||||
o.template = m:template_path("/rule_list/geoview")
|
||||
end
|
||||
end
|
||||
|
||||
m.on_before_save = function(self)
|
||||
m:set("@global[0]", "flush_set", "1")
|
||||
end
|
||||
|
||||
return api.return_map(m)
|
||||
@@ -1,264 +0,0 @@
|
||||
local api = require "luci.passwall.api"
|
||||
api.set_default_cbi()
|
||||
|
||||
local datatypes = api.datatypes
|
||||
|
||||
m = Map()
|
||||
m.redirect = api.url("rule")
|
||||
|
||||
if not arg[1] or not m:get(arg[1]) then
|
||||
luci.http.redirect(m.redirect)
|
||||
end
|
||||
|
||||
function m.on_before_save(self)
|
||||
self:set("@global[0]", "flush_set", "1")
|
||||
end
|
||||
|
||||
-- Add inline CSS to map description
|
||||
m.description = (m.description or "") .. "\n" .. [[
|
||||
<style>
|
||||
div[id^="cbid.passwall."] .cbi-value-field {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1em;
|
||||
}
|
||||
div[id^="cbid.passwall."] .cbi-checkbox {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
]]
|
||||
|
||||
function clean_text(text)
|
||||
local nbsp = string.char(0xC2, 0xA0) -- 不间断空格(U+00A0)
|
||||
local fullwidth_space = string.char(0xE3, 0x80, 0x80) -- 全角空格(U+3000)
|
||||
return text
|
||||
:gsub("\t", " ")
|
||||
:gsub(nbsp, " ")
|
||||
:gsub(fullwidth_space, " ")
|
||||
:gsub("^%s+", "")
|
||||
:gsub("%s+$", "\n")
|
||||
:gsub("\r\n", "\n")
|
||||
:gsub("[ \t]*\n[ \t]*", "\n")
|
||||
end
|
||||
|
||||
local remarks_lookup = {}
|
||||
local groups = {}
|
||||
m:foreach("shunt_rules", function(s)
|
||||
if s[".name"] ~= arg[1] then
|
||||
if s.remarks then
|
||||
remarks_lookup[s.remarks] = s[".name"]
|
||||
end
|
||||
if s.group and s.group ~= "" then
|
||||
groups[s.group] = true
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
s = m:section(NamedSection, arg[1], "shunt_rules", "Sing-Box/Xray " .. translate("Shunt Rule"))
|
||||
s.addremove = false
|
||||
s.dynamic = false
|
||||
|
||||
remarks = s:option(Value, "remarks", translate("Remarks"))
|
||||
remarks.default = arg[1]
|
||||
remarks.rmempty = false
|
||||
remarks.validate = function(self, value, section)
|
||||
value = api.trim(value)
|
||||
if value == "" then
|
||||
return nil, translate("Remark cannot be empty.")
|
||||
end
|
||||
if remarks_lookup[value] then
|
||||
return nil, translate("This remark already exists, please change a new remark.")
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
o = s:option(Value, "group", translate("Shunt Rule Group"))
|
||||
o.default = ""
|
||||
o:value("", translate("default"))
|
||||
for k, v in pairs(groups) do
|
||||
o:value(k)
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
value = api.trim(value)
|
||||
local lower = value:lower()
|
||||
|
||||
if lower == "" or lower == "default" then
|
||||
return m:del(section, self.option)
|
||||
end
|
||||
|
||||
for _, v in ipairs(self.keylist or {}) do
|
||||
if v:lower() == lower then
|
||||
return m:set(section, self.option, v)
|
||||
end
|
||||
end
|
||||
m:set(section, self.option, value)
|
||||
end
|
||||
|
||||
protocol = s:option(MultiValue, "protocol", translate("Protocol"))
|
||||
protocol:value("http")
|
||||
protocol:value("tls")
|
||||
protocol:value("quic")
|
||||
protocol:value("bittorrent")
|
||||
protocol.widget = "checkbox"
|
||||
protocol.default = nil
|
||||
|
||||
o = s:option(MultiValue, "inbound", translate("Inbound Tag"))
|
||||
o:value("tproxy", translate("Transparent proxy"))
|
||||
o:value("socks", "Socks")
|
||||
o.widget = "checkbox"
|
||||
o.default = nil
|
||||
|
||||
network = s:option(ListValue, "network", translate("Network"))
|
||||
network:value("tcp,udp", "TCP UDP")
|
||||
network:value("tcp", "TCP")
|
||||
network:value("udp", "UDP")
|
||||
|
||||
source = s:option(DynamicList, "source", translate("Source"))
|
||||
source.description = "<ul><li>" .. translate("Example:")
|
||||
.. "</li><li>" .. translate("IP") .. ": 192.168.1.100"
|
||||
.. "</li><li>" .. translate("IP CIDR") .. ": 192.168.1.0/24"
|
||||
.. "</li><li>" .. translate("GeoIP") .. ": geoip:private"
|
||||
.. "</li></ul>"
|
||||
source.cast = "string"
|
||||
source.cfgvalue = function(self, section)
|
||||
local value
|
||||
if self.tag_error[section] then
|
||||
value = self:formvalue(section)
|
||||
else
|
||||
value = self.map:get(section, self.option)
|
||||
if type(value) == "string" then
|
||||
local value2 = {}
|
||||
string.gsub(value, '[^' .. " " .. ']+', function(w) table.insert(value2, w) end)
|
||||
value = value2
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
source.validate = function(self, value, t)
|
||||
local err = {}
|
||||
for _, v in ipairs(value) do
|
||||
local flag = false
|
||||
if datatypes.ip4addr(v) then
|
||||
flag = true
|
||||
end
|
||||
|
||||
if flag == false and v:find("geoip:") and v:find("geoip:") == 1 then
|
||||
flag = true
|
||||
end
|
||||
|
||||
if flag == false then
|
||||
err[#err + 1] = v
|
||||
end
|
||||
end
|
||||
|
||||
if #err > 0 then
|
||||
self:add_error(t, "invalid", translate("Not true format, please re-enter!"))
|
||||
for _, v in ipairs(err) do
|
||||
self:add_error(t, "invalid", v)
|
||||
end
|
||||
end
|
||||
|
||||
return value
|
||||
end
|
||||
|
||||
--[[
|
||||
-- Too low usage rate, hidden
|
||||
sourcePort = s:option(Value, "sourcePort", translate("Source port"))
|
||||
]]--
|
||||
|
||||
port = s:option(Value, "port", translate("port"))
|
||||
|
||||
domain_list = s:option(TextValue, "domain_list", translate("Domain"))
|
||||
domain_list.rows = 10
|
||||
domain_list.wrap = "off"
|
||||
domain_list.validate = function(self, value)
|
||||
local hosts= {}
|
||||
value = clean_text(value)
|
||||
string.gsub(value, "[^\r\n]+", function(w) table.insert(hosts, w) end)
|
||||
for index, host in ipairs(hosts) do
|
||||
local flag = 1
|
||||
local tmp_host = host
|
||||
if not host:find("#") and host:find("%s") then
|
||||
elseif host:find("regexp:") and host:find("regexp:") == 1 then
|
||||
flag = 0
|
||||
elseif host:find("domain:.") and host:find("domain:.") == 1 then
|
||||
tmp_host = host:gsub("domain:", "")
|
||||
elseif host:find("full:.") and host:find("full:.") == 1 then
|
||||
tmp_host = host:gsub("full:", "")
|
||||
elseif host:find("geosite:") and host:find("geosite:") == 1 then
|
||||
flag = 0
|
||||
-- elseif host:find("ext:") and host:find("ext:") == 1 then
|
||||
-- flag = 0
|
||||
elseif host:find("rule-set:", 1, true) == 1 or host:find("rs:") == 1 then
|
||||
local w = host:sub(host:find(":") + 1, #host)
|
||||
if w:find("local:") == 1 or w:find("remote:") == 1 then
|
||||
flag = 0
|
||||
end
|
||||
elseif host:find("#") and host:find("#") == 1 then
|
||||
flag = 0
|
||||
end
|
||||
if flag == 1 then
|
||||
if not datatypes.hostname(tmp_host) then
|
||||
return nil, tmp_host .. " " .. translate("Not valid domain name, please re-enter!")
|
||||
end
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
domain_list.description = "<br /><ul>"
|
||||
.. "<li>" .. translate("Plaintext: If this string matches any part of the targeting domain, this rule takes effet. Example: rule 'sina.com' matches targeting domain 'sina.com', 'sina.com.cn' and 'www.sina.com', but not 'sina.cn'.") .. "</li>"
|
||||
.. "<li>" .. translate("Regular expression: Begining with 'regexp:', the rest is a regular expression. When the regexp matches targeting domain, this rule takes effect. Example: rule 'regexp:\\.goo.*\\.com$' matches 'www.google.com' and 'fonts.googleapis.com', but not 'google.com'.") .. "</li>"
|
||||
.. "<li>" .. translate("Subdomain (recommended): Begining with 'domain:' and the rest is a domain. When the targeting domain is exactly the value, or is a subdomain of the value, this rule takes effect. Example: rule 'domain:v2ray.com' matches 'www.v2ray.com', 'v2ray.com', but not 'xv2ray.com'.") .. "</li>"
|
||||
.. "<li>" .. translate("Full domain: Begining with 'full:' and the rest is a domain. When the targeting domain is exactly the value, the rule takes effect. Example: rule 'domain:v2ray.com' matches 'v2ray.com', but not 'www.v2ray.com'.") .. "</li>"
|
||||
.. "<li>" .. translate("Pre-defined domain list: Begining with 'geosite:' and the rest is a name, such as geosite:google or geosite:cn.") .. "</li>"
|
||||
.. "<li>"
|
||||
.. translate("Sing-Box is compatible with Geo rules and rule-set. rule-set begin with 'rule-set:remote:' or 'rule-set:local:'.")
|
||||
.. "<ul>"
|
||||
.. "<li>" .. translate("Such as:") .. "'rule-set:remote:https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs'" .. "</li>"
|
||||
.. "<li>" .. translate("Such as:") .. "'rule-set:local:/usr/share/sing-box/geosite-cn.srs'" .. "</li>"
|
||||
.. "</ul>"
|
||||
.. "</li>"
|
||||
.. "<li>" .. translate("Annotation: Begining with #") .. "</li>"
|
||||
.. "</ul>"
|
||||
ip_list = s:option(TextValue, "ip_list", "IP")
|
||||
ip_list.rows = 10
|
||||
ip_list.wrap = "off"
|
||||
ip_list.validate = function(self, value)
|
||||
local ipmasks= {}
|
||||
value = clean_text(value)
|
||||
string.gsub(value, "[^\r\n]+", function(w) table.insert(ipmasks, w) end)
|
||||
for index, ipmask in ipairs(ipmasks) do
|
||||
if ipmask:find("geoip:") and ipmask:find("geoip:") == 1 and not ipmask:find("%s") then
|
||||
-- elseif ipmask:find("ext:") and ipmask:find("ext:") == 1 and not ipmask:find("%s") then
|
||||
elseif ipmask:find("rule-set:", 1, true) == 1 or ipmask:find("rs:") == 1 then
|
||||
local w = ipmask:sub(ipmask:find(":") + 1, #ipmask)
|
||||
if w:find("local:") == 1 or w:find("remote:") == 1 then
|
||||
flag = 0
|
||||
end
|
||||
elseif ipmask:find("#") and ipmask:find("#") == 1 then
|
||||
else
|
||||
if not (datatypes.ipmask4(ipmask) or datatypes.ipmask6(ipmask)) then
|
||||
return nil, ipmask .. " " .. translate("Not valid IP format, please re-enter!")
|
||||
end
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
ip_list.description = "<br /><ul>"
|
||||
.. "<li>" .. translate("IP: such as '127.0.0.1'.") .. "</li>"
|
||||
.. "<li>" .. translate("CIDR: such as '127.0.0.0/8'.") .. "</li>"
|
||||
.. "<li>" .. translate("GeoIP: such as 'geoip:cn'. It begins with geoip: (lower case) and followed by two letter of country code.") .. "</li>"
|
||||
.. "<li>"
|
||||
.. translate("Sing-Box is compatible with Geo rules and rule-set. rule-set begin with 'rule-set:remote:' or 'rule-set:local:'.")
|
||||
.. "<ul>"
|
||||
.. "<li>" .. translate("Such as:") .. "'rule-set:remote:https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs'" .. "</li>"
|
||||
.. "<li>" .. translate("Such as:") .. "'rule-set:local:/usr/share/sing-box/geoip-cn.srs'" .. "</li>"
|
||||
.. "</ul>"
|
||||
.. "</li>"
|
||||
.. "<li>" .. translate("Annotation: Begining with #") .. "</li>"
|
||||
.. "</ul>"
|
||||
|
||||
o = s:option(Flag, "invert", "Invert", translate("Invert match result.") .. " " .. translate("Only support Sing-Box."))
|
||||
|
||||
return api.return_map(m)
|
||||
@@ -1,182 +0,0 @@
|
||||
local api = require "luci.passwall.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(m.redirect)
|
||||
end
|
||||
|
||||
m:appendTemplate("/cbi/nodes_dynamiclist_com")
|
||||
m:appendTemplate("/cbi/nodes_listvalue_com")
|
||||
|
||||
local has_singbox = api.finded_com("sing-box")
|
||||
local has_xray = api.finded_com("xray")
|
||||
|
||||
local nodes_table = {}
|
||||
for k, e in ipairs(api.get_valid_nodes()) do
|
||||
nodes_table[#nodes_table + 1] = e
|
||||
end
|
||||
|
||||
s = m:section(NamedSection, arg[1], translate("Socks Config"), translate("Socks Config"))
|
||||
s.addremove = false
|
||||
s.dynamic = false
|
||||
|
||||
---- Enable
|
||||
o = s:option(Flag, "enabled", translate("Enable"))
|
||||
o.default = 1
|
||||
o.rmempty = false
|
||||
|
||||
local auto_switch_tip
|
||||
local current_node = api.get_cache_var(arg[1])
|
||||
if current_node then
|
||||
local n = m:get(current_node)
|
||||
if n then
|
||||
if tonumber(m:get(arg[1], "enable_autoswitch") or 0) == 1 then
|
||||
if n then
|
||||
local remarks = api.get_node_remarks(n)
|
||||
local url = api.url("node_config", n[".name"])
|
||||
auto_switch_tip = translatef("Current node: %s", string.format('<a href="%s">%s</a>', url, remarks)) .. "<br />"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
socks_node = s:option(ListValue, "node", translate("Node"))
|
||||
if auto_switch_tip then
|
||||
socks_node.description = auto_switch_tip
|
||||
end
|
||||
socks_node.template = m:template_path("/cbi/nodes_listvalue")
|
||||
socks_node.group = {}
|
||||
|
||||
o = s:option(Flag, "bind_local", translate("Bind Local"), translate("When selected, it can only be accessed localhost."))
|
||||
o.default = "0"
|
||||
|
||||
local n = 1
|
||||
m:foreach("socks", function(s)
|
||||
if s[".name"] == section then
|
||||
return false
|
||||
end
|
||||
n = n + 1
|
||||
end)
|
||||
|
||||
o = s:option(Value, "port", "Socks " .. translate("Listen Port"))
|
||||
o.default = n + 1080
|
||||
o.datatype = "range(1,65535)"
|
||||
o.rmempty = false
|
||||
|
||||
if has_singbox or has_xray then
|
||||
o = s:option(Value, "http_port", "HTTP " .. translate("Listen Port") .. " " .. translate("0 is not use"))
|
||||
o.default = 0
|
||||
o.datatype = "port"
|
||||
end
|
||||
|
||||
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(
|
||||
'<code>%s</code> <input class="btn cbi-button cbi-button-apply" type="button" value="%s" onclick="window.open(\'%s\', \'_blank\')" />',
|
||||
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
|
||||
|
||||
o = s:option(ListValue, "backup_node_add_mode", translate("Backup Node Addition Method"))
|
||||
o:depends("enable_autoswitch", true)
|
||||
o.default = "manual"
|
||||
o:value("manual", translate("Manual"))
|
||||
o:value("batch", translate("Batch"))
|
||||
|
||||
o = s:option(DynamicList, "autoswitch_backup_node", translate("List of backup nodes"))
|
||||
o:depends("backup_node_add_mode", "manual")
|
||||
o.template = m:template_path("/cbi/nodes_dynamiclist")
|
||||
o.group = {}
|
||||
o.write = function(self, section, value)
|
||||
local n = s.fields["node"]:formvalue(section)
|
||||
local v = type(value) == "table" and value or { value }
|
||||
local t = {}
|
||||
for _, x in ipairs(v) do
|
||||
if x and x ~= n then t[#t+1] = x end
|
||||
end
|
||||
return DynamicList.write(self, section, t)
|
||||
end
|
||||
for i, v in pairs(nodes_table) do
|
||||
if v.protocol ~= "_shunt" then
|
||||
o:value(v.id, v.remark)
|
||||
o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
s.fields["enable_autoswitch"]:depends({ node = v.id })
|
||||
end
|
||||
socks_node:value(v.id, v["remark"])
|
||||
socks_node.group[#socks_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
end
|
||||
|
||||
o = s:option(MultiValue, "backup_node_group", translate("Select Group"))
|
||||
o:depends("backup_node_add_mode", "batch")
|
||||
o.widget = "checkbox"
|
||||
o:value("default", translate("default"))
|
||||
local groups = {}
|
||||
m:foreach("nodes", function(s)
|
||||
if s.group and s.group ~= "" then
|
||||
groups[s.group] = true
|
||||
end
|
||||
end)
|
||||
for k, v in pairs(groups) do
|
||||
o:value(api.UrlEncode(k), k)
|
||||
end
|
||||
|
||||
o = s:option(Value, "backup_node_match_rule", translate("Node Matching Rules"))
|
||||
o:depends("backup_node_add_mode", "batch")
|
||||
local descrStr = "Example: <code>^A && B && !C && D$</code><br>"
|
||||
descrStr = descrStr .. "This means the node remark must start with A (^), include B, exclude C (!), and end with D ($).<br>"
|
||||
descrStr = descrStr .. "Conditions are joined by <code>&&</code> (AND), and their order does not affect the result.<br>"
|
||||
descrStr = descrStr .. "Multiple groups can be separated by <code>||</code> (OR), matching succeeds if any group matches.<br>"
|
||||
descrStr = descrStr .. "Example: <code>A && B || C && D</code> means (A AND B) OR (C AND D)."
|
||||
o.description = translate(descrStr)
|
||||
|
||||
o = s:option(Value, "autoswitch_testing_time", translate("How often to test"), translate("Units:seconds"))
|
||||
o.datatype = "min(10)"
|
||||
o.default = 30
|
||||
o:depends("enable_autoswitch", true)
|
||||
|
||||
o = s:option(Value, "autoswitch_connect_timeout", translate("Timeout seconds"), translate("Units:seconds"))
|
||||
o.datatype = "range(3,10)"
|
||||
o.default = 3
|
||||
o:depends("enable_autoswitch", true)
|
||||
|
||||
o = s:option(Value, "autoswitch_retry_num", translate("Timeout retry num"))
|
||||
o.datatype = "range(1,5)"
|
||||
o.default = 1
|
||||
o:depends("enable_autoswitch", true)
|
||||
|
||||
o = s:option(Flag, "autoswitch_restore_switch", translate("Restore Switch"), translate("Immediately switch back to the main node when it becomes available."))
|
||||
o:depends("enable_autoswitch", true)
|
||||
|
||||
o = s:option(Value, "autoswitch_probe_url", translate("Probe URL"), translate("The URL used to detect the connection status."))
|
||||
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")
|
||||
o:value("https://www.youtube.com/generate_204", "YouTube")
|
||||
o:value("https://connect.rom.miui.com/generate_204", "MIUI (CN)")
|
||||
o:value("https://connectivitycheck.platform.hicloud.com/generate_204", "HiCloud (CN)")
|
||||
o:value("https://wifi.vivo.com.cn/generate_204", "VIVO (CN)")
|
||||
o.default = o.keylist[3]
|
||||
o:depends("enable_autoswitch", true)
|
||||
|
||||
o = s:option(DummyValue, "btn")
|
||||
o.template = m:template_path("/socks_auto_switch/btn")
|
||||
o:depends("backup_node_add_mode", "manual")
|
||||
|
||||
return api.return_map(m)
|
||||
@@ -1,35 +0,0 @@
|
||||
if not api.is_finded("ipt2socks") then
|
||||
return
|
||||
end
|
||||
|
||||
-- [[ Socks ]]
|
||||
local m, s1 = ...
|
||||
local type_name = "Socks"
|
||||
|
||||
s1.fields["type"]:value(type_name, "Socks")
|
||||
|
||||
if s1.val["type"] and s1.val["type"] ~= type_name then
|
||||
return
|
||||
end
|
||||
|
||||
local s = NamedSection(m, arg[1], "tmp_" .. s1.sectiontype)
|
||||
s.parent = s1
|
||||
s.type_name = type_name
|
||||
s.option_prefix = "socks_"
|
||||
api.set_type_cbi(s)
|
||||
|
||||
o = s:option(ListValue, "del_protocol", "¡¡") --ʼÖÕÒþ²Ø£¬ÓÃÓÚɾ³ý protocol
|
||||
o:depends({ __hide = "1" })
|
||||
o.rewrite_option = "protocol"
|
||||
|
||||
o = s:option(Value, "address", translate("Address (Support Domain Name)"))
|
||||
|
||||
o = s:option(Value, "port", translate("Port"))
|
||||
o.datatype = "port"
|
||||
|
||||
o = s:option(Value, "username", translate("Username"))
|
||||
|
||||
o = s:option(Value, "password", translate("Password"))
|
||||
o.password = true
|
||||
|
||||
api.type_cbi_section(s1, s)
|
||||
@@ -1,937 +0,0 @@
|
||||
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"
|
||||
|
||||
s1.fields["type"]:value(type_name, "Sing-Box")
|
||||
if not s1.fields["type"].default then
|
||||
s1.fields["type"].default = type_name
|
||||
end
|
||||
|
||||
if s1.val["type"] ~= type_name then
|
||||
return
|
||||
end
|
||||
|
||||
local s = NamedSection(m, arg[1], "tmp_" .. s1.sectiontype)
|
||||
s.parent = s1
|
||||
s.type_name = type_name
|
||||
s.option_prefix = "singbox_"
|
||||
api.set_type_cbi(s)
|
||||
|
||||
local ss_method_new_list = {
|
||||
"none", "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "xchacha20-ietf-poly1305", "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"
|
||||
}
|
||||
|
||||
local ss_method_old_list = {
|
||||
"aes-128-ctr", "aes-192-ctr", "aes-256-ctr", "aes-128-cfb", "aes-192-cfb", "aes-256-cfb", "rc4-md5", "chacha20-ietf", "xchacha20",
|
||||
}
|
||||
|
||||
local security_list = { "none", "auto", "aes-128-gcm", "chacha20-poly1305", "zero" }
|
||||
|
||||
local singbox_tags = luci.sys.exec(singbox_bin .. " version | grep 'Tags:' | awk '{print $2}'")
|
||||
|
||||
local singbox_version = api.get_app_version("sing-box"):match("[^v]+")
|
||||
local version_ge_1_14_0 = api.compare_versions(singbox_version, ">=", "1.14.0")
|
||||
|
||||
o = s:option(ListValue, "protocol", translate("Protocol"))
|
||||
o:value("socks", "Socks")
|
||||
o:value("http", "HTTP")
|
||||
o:value("shadowsocks", "Shadowsocks")
|
||||
o:value("vmess", "Vmess")
|
||||
o:value("trojan", "Trojan")
|
||||
if singbox_tags:find("with_wireguard") then
|
||||
o:value("wireguard", "WireGuard")
|
||||
end
|
||||
if singbox_tags:find("with_quic") then
|
||||
o:value("hysteria", "Hysteria")
|
||||
end
|
||||
o:value("vless", "VLESS")
|
||||
if singbox_tags:find("with_quic") then
|
||||
o:value("tuic", "TUIC")
|
||||
end
|
||||
if singbox_tags:find("with_quic") then
|
||||
o:value("hysteria2", "Hysteria2")
|
||||
end
|
||||
o:value("anytls", "AnyTLS")
|
||||
o:value("ssh", "SSH")
|
||||
if singbox_tags:find("with_naive_outbound") then
|
||||
o:value("naive", "NaïveProxy")
|
||||
end
|
||||
if version_ge_1_14_0 then
|
||||
o:value("snell", "Snell")
|
||||
end
|
||||
o:value("_urltest", translate("URLTest"))
|
||||
o:value("_shunt", translate("Shunt"))
|
||||
o:value("_iface", translate("Custom Interface"))
|
||||
|
||||
local protocol_val = m:get(s.section, "protocol")
|
||||
local formvalue_proto = s.fields["protocol"]:formvalue(s.section)
|
||||
if formvalue_proto then
|
||||
protocol_val = formvalue_proto
|
||||
end
|
||||
|
||||
local load_urltest_options = protocol_val == "_urltest"
|
||||
local load_shunt_options = protocol_val == "_shunt"
|
||||
local load_iface_options = protocol_val == "_iface"
|
||||
local load_normal_options = true
|
||||
if load_urltest_options or load_shunt_options or load_iface_options then
|
||||
load_normal_options = nil
|
||||
end
|
||||
|
||||
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, "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, "urltest_node", translate("URLTest node list"), translate("List of nodes to test, <a target='_blank' href='https://sing-box.sagernet.org/configuration/outbound/urltest'>document</a>"))
|
||||
o:depends({ node_add_mode = "manual" })
|
||||
o.widget = "checkbox"
|
||||
o.cast = "table"
|
||||
o.template = m:template_path("/cbi/nodes_multivalue")
|
||||
o.group = {}
|
||||
for k1, v1 in pairs(node_list) do
|
||||
if k1 == "socks_list" or k1 == "normal_list" then
|
||||
for i, v in ipairs(v1) do
|
||||
o:value(v.id, v.remark)
|
||||
o.group[#o.group+1] = v.group or ""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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, "node_match_rule", translate("Node Matching Rules"))
|
||||
o:depends({ node_add_mode = "batch" })
|
||||
local descrStr = "Example: <code>^A && B && !C && D$</code><br>"
|
||||
descrStr = descrStr .. "This means the node remark must start with A (^), include B, exclude C (!), and end with D ($).<br>"
|
||||
descrStr = descrStr .. "Conditions are joined by <code>&&</code> (AND), and their order does not affect the result.<br>"
|
||||
descrStr = descrStr .. "Multiple groups can be separated by <code>||</code> (OR), matching succeeds if any group matches.<br>"
|
||||
descrStr = descrStr .. "Example: <code>A && B || C && D</code> means (A AND B) OR (C AND D)."
|
||||
o.description = translate(descrStr)
|
||||
|
||||
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")
|
||||
o:value("https://www.youtube.com/generate_204", "YouTube")
|
||||
o:value("https://connect.rom.miui.com/generate_204", "MIUI (CN)")
|
||||
o:value("https://connectivitycheck.platform.hicloud.com/generate_204", "HiCloud (CN)")
|
||||
o:value("https://wifi.vivo.com.cn/generate_204", "VIVO (CN)")
|
||||
o.default = o.keylist[3]
|
||||
o.description = translate("The URL used to detect the connection status.")
|
||||
|
||||
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.") .. "<br>" ..
|
||||
translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are <code>s</code>, <code>m</code>, <code>h</code>, which correspond to seconds, minutes, and hours, respectively.") .. "<br>" ..
|
||||
translate("When the unit is not filled in, it defaults to seconds.") .. "<br>" ..
|
||||
translate("Test interval must be less or equal than idle timeout.")
|
||||
|
||||
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, "urltest_idle_timeout", translate("Idle timeout"))
|
||||
o:depends({ protocol = "_urltest" })
|
||||
o.placeholder = "30m"
|
||||
o.default = "30m"
|
||||
o.description = translate("The idle timeout.") .. "<br>" ..
|
||||
translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are <code>s</code>, <code>m</code>, <code>h</code>, which correspond to seconds, minutes, and hours, respectively.") .. "<br>" ..
|
||||
translate("When the unit is not filled in, it defaults to seconds.")
|
||||
|
||||
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 -- [[ 自定义接口 Start ]]
|
||||
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
|
||||
end
|
||||
|
||||
|
||||
if load_normal_options then
|
||||
|
||||
o = s:option(Value, "address", translate("Address (Support Domain Name)"))
|
||||
|
||||
o = s:option(Value, "port", translate("Port"))
|
||||
o.datatype = "port"
|
||||
|
||||
o = s:option(Value, "uuid", translate("ID"))
|
||||
o.password = true
|
||||
o:depends({ protocol = "vmess" })
|
||||
o:depends({ protocol = "vless" })
|
||||
o:depends({ protocol = "tuic" })
|
||||
|
||||
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, "password", translate("Password"))
|
||||
o.password = true
|
||||
o:depends({ protocol = "http" })
|
||||
o:depends({ protocol = "socks" })
|
||||
o:depends({ protocol = "shadowsocks" })
|
||||
o:depends({ protocol = "trojan" })
|
||||
o:depends({ protocol = "tuic" })
|
||||
o:depends({ protocol = "anytls" })
|
||||
o:depends({ protocol = "ssh" })
|
||||
o:depends({ protocol = "naive" })
|
||||
|
||||
if version_ge_1_14_0 then
|
||||
-- snell
|
||||
s.fields["password"]:depends({ protocol = "snell" })
|
||||
|
||||
o = s:option(Value, "snell_psk", translate("Pre shared key"))
|
||||
o.rmempty = false
|
||||
o:depends({ protocol = "snell" })
|
||||
|
||||
o = s:option(ListValue, "snell_version", translate("Version"))
|
||||
o:value("4")
|
||||
o:value("6")
|
||||
o:depends({ protocol = "snell" })
|
||||
|
||||
o = s:option(Flag, "snell_reuse", translate("Connection Reuse"))
|
||||
o:depends({ protocol = "snell" })
|
||||
|
||||
o = s:option(ListValue, "snell_network", translate("Transport"))
|
||||
o:value("", "TCP UDP")
|
||||
o:value("tcp", "TCP")
|
||||
o:value("udp", "UDP")
|
||||
o:depends({ protocol = "snell" })
|
||||
|
||||
o = s:option(ListValue, "snell_obfs_mode", translate("Camouflage Type"))
|
||||
o:value("none")
|
||||
o:value("http")
|
||||
o:depends({ protocol = "snell", snell_version = "4" })
|
||||
|
||||
o = s:option(Value, "snell_obfs_host", translate("HTTP Host"))
|
||||
o:depends({ protocol = "snell", snell_version = "4", snell_obfs_mode = "http" })
|
||||
|
||||
o = s:option(ListValue, "snell_mode", translate("Mode"))
|
||||
o:value("default")
|
||||
o:value("unshaped")
|
||||
o:value("unsafe-raw")
|
||||
o:depends({ protocol = "snell", snell_version = "6" })
|
||||
end
|
||||
|
||||
o = s:option(ListValue, "security", translate("Encrypt Method"))
|
||||
for a, t in ipairs(security_list) do o:value(t) end
|
||||
o:depends({ protocol = "vmess" })
|
||||
|
||||
o = s:option(ListValue, "ss_method", translate("Encrypt Method"))
|
||||
o.rewrite_option = "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({ protocol = "shadowsocks" })
|
||||
|
||||
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, "alter_id", "Alter ID")
|
||||
o.datatype = "uinteger"
|
||||
o.default = "0"
|
||||
o:depends({ protocol = "vmess" })
|
||||
|
||||
o = s:option(Flag, "global_padding", "global_padding", translate("Protocol parameter. Will waste traffic randomly if enabled."))
|
||||
o.default = "0"
|
||||
o:depends({ protocol = "vmess" })
|
||||
|
||||
o = s:option(Flag, "authenticated_length", "authenticated_length", translate("Protocol parameter. Enable length block encryption."))
|
||||
o.default = "0"
|
||||
o:depends({ protocol = "vmess" })
|
||||
|
||||
o = s:option(ListValue, "flow", translate("flow"))
|
||||
o.default = ""
|
||||
o:value("", translate("Disable"))
|
||||
o:value("xtls-rprx-vision")
|
||||
o:depends({ protocol = "vless", tls = true })
|
||||
|
||||
if singbox_tags:find("with_quic") then
|
||||
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({ protocol = "hysteria" })
|
||||
|
||||
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({ protocol = "hysteria" })
|
||||
|
||||
o = s:option(Value, "hysteria_obfs", translate("Obfs Password"))
|
||||
o:depends({ protocol = "hysteria" })
|
||||
|
||||
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({ protocol = "hysteria" })
|
||||
|
||||
o = s:option(Value, "hysteria_auth_password", translate("Auth Password"))
|
||||
o.password = true
|
||||
o:depends({ protocol = "hysteria", hysteria_auth_type = "string"})
|
||||
o:depends({ protocol = "hysteria", hysteria_auth_type = "base64"})
|
||||
|
||||
o = s:option(Value, "hysteria_up_mbps", translate("Max upload Mbps"))
|
||||
o.default = "10"
|
||||
o:depends({ protocol = "hysteria" })
|
||||
|
||||
o = s:option(Value, "hysteria_down_mbps", translate("Max download Mbps"))
|
||||
o.default = "50"
|
||||
o:depends({ protocol = "hysteria" })
|
||||
|
||||
o = s:option(Value, "hysteria_recv_window_conn", translate("QUIC stream receive window"))
|
||||
o:depends({ protocol = "hysteria" })
|
||||
|
||||
o = s:option(Value, "hysteria_recv_window", translate("QUIC connection receive window"))
|
||||
o:depends({ 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, "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({ protocol = "tuic" })
|
||||
|
||||
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({ protocol = "tuic" })
|
||||
|
||||
--[[
|
||||
o = s:option(Flag, "tuic_udp_over_stream", translate("UDP over stream"))
|
||||
o:depends({ protocol = "tuic" })
|
||||
]]--
|
||||
|
||||
o = s:option(Flag, "tuic_zero_rtt_handshake", translate("Enable 0-RTT QUIC handshake"))
|
||||
o.default = 0
|
||||
o:depends({ protocol = "tuic" })
|
||||
|
||||
o = s:option(Value, "tuic_heartbeat", translate("Heartbeat interval(second)"))
|
||||
o.datatype = "uinteger"
|
||||
o.default = "3"
|
||||
o:depends({ protocol = "tuic" })
|
||||
|
||||
o = s:option(ListValue, "tuic_alpn", translate("QUIC TLS ALPN"))
|
||||
o.default = "default"
|
||||
o:value("default", translate("Default"))
|
||||
o:value("h3")
|
||||
o:value("h2")
|
||||
o:value("h3,h2")
|
||||
o:value("http/1.1")
|
||||
o:value("h2,http/1.1")
|
||||
o:value("h3,h2,http/1.1")
|
||||
o:value("spdy/3.1")
|
||||
o:value("h3,spdy/3.1")
|
||||
o:depends({ protocol = "tuic" })
|
||||
end
|
||||
|
||||
if singbox_tags:find("with_quic") then
|
||||
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({ protocol = "hysteria2", hysteria2_realms = false })
|
||||
|
||||
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({ protocol = "hysteria2", hysteria2_realms = false })
|
||||
|
||||
o = s:option(Flag, "hysteria2_realms", translate("Realms"))
|
||||
o.default = "0"
|
||||
if version_ge_1_14_0 then
|
||||
o:depends({ protocol = "hysteria2"})
|
||||
else
|
||||
o:depends({ protocol = "__hide"})
|
||||
end
|
||||
|
||||
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)
|
||||
if realm then return value end
|
||||
return nil, translate("Invalid Realm URL.")
|
||||
end
|
||||
|
||||
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({ hysteria2_realms = "1" })
|
||||
|
||||
o = s:option(Flag, "hysteria2_realm_upnp", translate("Enable") .. " UPnP/NAT-PMP", translate("Enable UPnP/NAT-PMP port mapping on your gateway to improve hole punching success."))
|
||||
o.default = "0"
|
||||
o:depends({ hysteria2_realms = "1" })
|
||||
|
||||
o = s:option(Value, "hysteria2_auth_password", translate("Auth Password"))
|
||||
o.password = true
|
||||
o:depends({ protocol = "hysteria2"})
|
||||
|
||||
o = s:option(ListValue, "hysteria2_obfs_type", translate("Obfs Type"))
|
||||
o:value("", translate("Disable"))
|
||||
o:value("salamander")
|
||||
o:value("gecko")
|
||||
o:depends({ protocol = "hysteria2" })
|
||||
|
||||
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, "hysteria2_obfs_MinPacketSize", translate("Gecko Packet Size (min)"))
|
||||
o.datatype = "uinteger"
|
||||
o.placeholder = "512"
|
||||
o.default = "512"
|
||||
o:depends({ hysteria2_obfs_type = "gecko" })
|
||||
|
||||
o = s:option(Value, "hysteria2_obfs_MaxPacketSize", translate("Gecko Packet Size (max)"))
|
||||
o.datatype = "uinteger"
|
||||
o.placeholder = "1200"
|
||||
o.default = "1200"
|
||||
o:depends({ hysteria2_obfs_type = "gecko" })
|
||||
|
||||
o = s:option(Value, "hysteria2_up_mbps", translate("Max upload Mbps"))
|
||||
o:depends({ protocol = "hysteria2" })
|
||||
|
||||
o = s:option(Value, "hysteria2_down_mbps", translate("Max download Mbps"))
|
||||
o:depends({ protocol = "hysteria2" })
|
||||
|
||||
o = s:option(Value, "hysteria2_idle_timeout", translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)")
|
||||
o.datatype = "range(4,120)"
|
||||
o:depends({ protocol = "hysteria2"})
|
||||
|
||||
o = s:option(Value, "hysteria2_keep_alive_period", translate("QUIC KeepAlive interval"), translate("Units:seconds") .. " (2~60)")
|
||||
o.datatype = "range(2,60)"
|
||||
o:depends({ protocol = "hysteria2"})
|
||||
|
||||
o = s:option(Flag, "hysteria2_disable_mtu_discovery", translate("Disable MTU detection"))
|
||||
o.default = "0"
|
||||
o:depends({ protocol = "hysteria2"})
|
||||
end
|
||||
|
||||
-- [[ SSH config start ]] --
|
||||
o = s:option(TextValue, "ssh_priv_key", translate("Private Key"))
|
||||
o.rows = 5
|
||||
o.wrap = "off"
|
||||
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, "ssh_priv_key_pp", translate("Private Key Passphrase"))
|
||||
o.password = true
|
||||
o:depends({ 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, "ssh_host_key_algo", translate("Host Key Algorithms"))
|
||||
o:depends({ 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, "naive_insecure_concurrency", translate("Concurrent Tunnels"))
|
||||
o.datatype = "uinteger"
|
||||
o.placeholder = "0"
|
||||
o.default = "0"
|
||||
o:depends({ protocol = "naive" })
|
||||
|
||||
o = s:option(Flag, "naive_quic", translate("QUIC"))
|
||||
o.default = 0
|
||||
o:depends({ protocol = "naive" })
|
||||
|
||||
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({ naive_quic = "1" })
|
||||
-- [[ naive end ]] --
|
||||
|
||||
o = s:option(Flag, "tls", translate("TLS"))
|
||||
o.default = 0
|
||||
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, "alpn", translate("ALPN"))
|
||||
o.default = "default"
|
||||
o:value("default", translate("Default"))
|
||||
o:value("h3")
|
||||
o:value("h2")
|
||||
o:value("h3,h2")
|
||||
o:value("http/1.1")
|
||||
o:value("h2,http/1.1")
|
||||
o:value("h3,h2,http/1.1")
|
||||
o:depends({ tls = true })
|
||||
o:depends({ protocol = "hysteria" })
|
||||
|
||||
o = s:option(Flag, "tls_disable_sni", translate("Disable SNI"), translate("Do not send server name in ClientHello."))
|
||||
o.default = "0"
|
||||
o:depends({ tls = true })
|
||||
o:depends({ protocol = "hysteria"})
|
||||
o:depends({ protocol = "tuic" })
|
||||
o:depends({ protocol = "hysteria2" })
|
||||
|
||||
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, "tls_allowInsecure", translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped."))
|
||||
o.default = "0"
|
||||
o:depends({ tls = true })
|
||||
o:depends({ protocol = "hysteria" })
|
||||
o:depends({ protocol = "tuic" })
|
||||
o:depends({ protocol = "hysteria2" })
|
||||
|
||||
o = s:option(Value, "tls_pinSHA256", translate("TLS Chain Fingerprint (SHA256)"))
|
||||
o:depends({ tls = true })
|
||||
o:depends({ protocol = "hysteria" })
|
||||
o:depends({ protocol = "tuic" })
|
||||
o:depends({ protocol = "hysteria2" })
|
||||
o.description = translate("Once set, connects only when the server’s chain fingerprint matches.") ..
|
||||
string.format("<a href='javascript:void(0)' onclick='javascript:fetchCertSha256(this)'>%s</a>", "→ " .. translate("Fetch Manually"))
|
||||
|
||||
o = s:option(Flag, "tls_certificate", translate("TLS Certificate (PEM)"))
|
||||
o.default = "0"
|
||||
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, "tls_certificate_pem", " ", translate("Full certificate (chain), PEM format."))
|
||||
o.default = ""
|
||||
o.rows = 5
|
||||
o.wrap = "off"
|
||||
o:depends({ tls_certificate = true })
|
||||
o.cfgvalue = function(self, section)
|
||||
return (m:get(section, "tls_certificate_pem") or ""):gsub("\\n", "\n")
|
||||
end
|
||||
o.validate = function(self, value)
|
||||
value = api.trim(value):gsub("\r\n", "\n"):gsub("\r", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
|
||||
return value:gsub("\n", "\\n")
|
||||
end
|
||||
|
||||
o = s:option(Value, "cipherSuites", translate("Cipher Suites"), '<a href="https://go.dev/src/crypto/tls/cipher_suites.go#L44" target="_blank">***</a>' .. " " .. translate("Configures the list of supported cipher suites, separated by :"))
|
||||
o:depends({ tls = true })
|
||||
|
||||
o = s:option(Flag, "ech", translate("ECH"))
|
||||
o.default = "0"
|
||||
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, "ech_config", translate("ECH Config"))
|
||||
o.default = ""
|
||||
o.rows = 5
|
||||
o.wrap = "off"
|
||||
o:depends({ ech = true })
|
||||
o.cfgvalue = function(self, section)
|
||||
return (m:get(section, "ech_config") or ""):gsub("\\n", "\n")
|
||||
end
|
||||
o.validate = function(self, value)
|
||||
value = api.trim(value):gsub("\r\n", "\n"):gsub("\r", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
|
||||
return value:gsub("\n", "\\n")
|
||||
end
|
||||
|
||||
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, "utls", translate("uTLS"))
|
||||
o.default = "0"
|
||||
o:depends({ tls = true })
|
||||
|
||||
o = s:option(ListValue, "fingerprint", translate("Finger Print"))
|
||||
o:value("chrome")
|
||||
o:value("firefox")
|
||||
o:value("edge")
|
||||
o:value("safari")
|
||||
o:value("360")
|
||||
o:value("qq")
|
||||
o:value("ios")
|
||||
o:value("android")
|
||||
o:value("random")
|
||||
o:value("randomized")
|
||||
o.default = "chrome"
|
||||
o:depends({ utls = true })
|
||||
|
||||
-- [[ REALITY部分 ]] --
|
||||
o = s:option(Flag, "reality", translate("REALITY"))
|
||||
o.default = 0
|
||||
o:depends({ protocol = "vless", tls = true })
|
||||
o:depends({ protocol = "vmess", tls = true })
|
||||
o:depends({ protocol = "shadowsocks", tls = true })
|
||||
o:depends({ protocol = "trojan", tls = true })
|
||||
o:depends({ protocol = "anytls", tls = true })
|
||||
o:depends({ protocol = "http", tls = true })
|
||||
|
||||
o = s:option(Value, "reality_publicKey", translate("Public Key"))
|
||||
o:depends({ reality = true })
|
||||
|
||||
o = s:option(Value, "reality_shortId", translate("Short Id"))
|
||||
o:depends({ reality = true })
|
||||
end
|
||||
|
||||
o = s:option(ListValue, "transport", translate("Transport"))
|
||||
o:value("tcp", "TCP")
|
||||
o:value("http", "HTTP")
|
||||
o:value("ws", "WebSocket")
|
||||
o:value("httpupgrade", "HTTPUpgrade")
|
||||
if singbox_tags:find("with_quic") then
|
||||
o:value("quic", "QUIC")
|
||||
end
|
||||
if singbox_tags:find("with_grpc") then
|
||||
o:value("grpc", "gRPC")
|
||||
else o:value("grpc", "gRPC-lite")
|
||||
end
|
||||
o:depends({ protocol = "vmess" })
|
||||
o:depends({ protocol = "vless" })
|
||||
o:depends({ protocol = "shadowsocks" })
|
||||
o:depends({ protocol = "trojan" })
|
||||
|
||||
if singbox_tags:find("with_wireguard") then
|
||||
o = s:option(Value, "wireguard_public_key", translate("Public Key"))
|
||||
o:depends({ protocol = "wireguard" })
|
||||
|
||||
o = s:option(Value, "wireguard_secret_key", translate("Private Key"))
|
||||
o:depends({ protocol = "wireguard" })
|
||||
|
||||
o = s:option(Value, "wireguard_preSharedKey", translate("Pre shared key"))
|
||||
o:depends({ protocol = "wireguard" })
|
||||
|
||||
o = s:option(DynamicList, "wireguard_local_address", translate("Local Address"))
|
||||
o:depends({ protocol = "wireguard" })
|
||||
|
||||
o = s:option(Value, "wireguard_mtu", translate("MTU"))
|
||||
o.default = "1420"
|
||||
o:depends({ 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, "tcp_guise", translate("Camouflage Type"))
|
||||
o:value("none", "none")
|
||||
o:value("http", "http")
|
||||
o:depends({ transport = "tcp" })
|
||||
|
||||
o = s:option(DynamicList, "tcp_guise_http_host", translate("HTTP Host"))
|
||||
o:depends({ tcp_guise = "http" })
|
||||
|
||||
o = s:option(DynamicList, "tcp_guise_http_path", translate("HTTP Path"))
|
||||
o.placeholder = "/"
|
||||
o:depends({ tcp_guise = "http" })
|
||||
|
||||
-- [[ HTTP部分 ]]--
|
||||
o = s:option(DynamicList, "http_host", translate("HTTP Host"))
|
||||
o:depends({ transport = "http" })
|
||||
|
||||
o = s:option(Value, "http_path", translate("HTTP Path"))
|
||||
o.placeholder = "/"
|
||||
o:depends({ transport = "http" })
|
||||
|
||||
o = s:option(Flag, "http_h2_health_check", translate("Health check"))
|
||||
o:depends({ tls = true, transport = "http" })
|
||||
|
||||
o = s:option(Value, "http_h2_read_idle_timeout", translate("Idle timeout"))
|
||||
o.default = "15"
|
||||
o:depends({ http_h2_health_check = true })
|
||||
|
||||
o = s:option(Value, "http_h2_health_check_timeout", translate("Health check timeout"))
|
||||
o.default = "15"
|
||||
o:depends({ http_h2_health_check = true })
|
||||
|
||||
-- [[ WebSocket部分 ]]--
|
||||
o = s:option(Value, "ws_host", translate("WebSocket Host"))
|
||||
o:depends({ transport = "ws" })
|
||||
|
||||
o = s:option(Value, "ws_path", translate("WebSocket Path"))
|
||||
o.placeholder = "/"
|
||||
o:depends({ transport = "ws" })
|
||||
|
||||
-- [[ HTTPUpgrade部分 ]]--
|
||||
o = s:option(Value, "httpupgrade_host", translate("HTTPUpgrade Host"))
|
||||
o:depends({ transport = "httpupgrade" })
|
||||
|
||||
o = s:option(Value, "httpupgrade_path", translate("HTTPUpgrade Path"))
|
||||
o.placeholder = "/"
|
||||
o:depends({ transport = "httpupgrade" })
|
||||
|
||||
-- [[ gRPC部分 ]]--
|
||||
o = s:option(Value, "grpc_serviceName", "ServiceName")
|
||||
o:depends({ transport = "grpc" })
|
||||
|
||||
o = s:option(Flag, "grpc_health_check", translate("Health check"))
|
||||
o:depends({ transport = "grpc" })
|
||||
|
||||
o = s:option(Value, "grpc_idle_timeout", translate("Idle timeout"))
|
||||
o.default = "15"
|
||||
o:depends({ grpc_health_check = true })
|
||||
|
||||
o = s:option(Value, "grpc_health_check_timeout", translate("Health check timeout"))
|
||||
o.default = "15"
|
||||
o:depends({ grpc_health_check = true })
|
||||
|
||||
o = s:option(Flag, "grpc_permit_without_stream", translate("Permit without stream"))
|
||||
o.default = "0"
|
||||
o:depends({ grpc_health_check = true })
|
||||
|
||||
-- [[ 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")
|
||||
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0", "firefox")
|
||||
o:value("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", "safari")
|
||||
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({ tcp_guise = "http" })
|
||||
o:depends({ transport = "http" })
|
||||
o:depends({ transport = "ws" })
|
||||
o:depends({ transport = "httpupgrade" })
|
||||
o:depends({ protocol = "naive" })
|
||||
|
||||
-- [[ Mux ]]--
|
||||
o = s:option(Flag, "mux", translate("Mux"))
|
||||
o.rmempty = false
|
||||
o:depends({ protocol = "vmess" })
|
||||
o:depends({ protocol = "vless", flow = "" })
|
||||
o:depends({ protocol = "shadowsocks", uot = "" })
|
||||
o:depends({ protocol = "trojan" })
|
||||
|
||||
o = s:option(ListValue, "mux_type", translate("Mux"))
|
||||
o:value("smux")
|
||||
o:value("yamux")
|
||||
o:value("h2mux")
|
||||
o:depends({ mux = true })
|
||||
|
||||
o = s:option(Value, "mux_concurrency", translate("Mux concurrency"))
|
||||
o.default = 4
|
||||
o:depends({ mux = true, tcpbrutal = false })
|
||||
|
||||
o = s:option(Flag, "mux_padding", translate("Padding"))
|
||||
o.default = 0
|
||||
o:depends({ mux = true })
|
||||
|
||||
-- [[ TCP Brutal ]]--
|
||||
o = s:option(Flag, "tcpbrutal", translate("TCP Brutal"))
|
||||
o.default = 0
|
||||
o:depends({ mux = true })
|
||||
|
||||
o = s:option(Value, "tcpbrutal_up_mbps", translate("Max upload Mbps"))
|
||||
o.default = "10"
|
||||
o:depends({ tcpbrutal = true })
|
||||
|
||||
o = s:option(Value, "tcpbrutal_down_mbps", translate("Max download Mbps"))
|
||||
o.default = "50"
|
||||
o:depends({ tcpbrutal = true })
|
||||
|
||||
o = s:option(Flag, "shadowtls", "ShadowTLS")
|
||||
o.default = 0
|
||||
o:depends({ protocol = "vmess", tls = false })
|
||||
o:depends({ protocol = "shadowsocks", tls = false })
|
||||
|
||||
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({ shadowtls = true })
|
||||
|
||||
o = s:option(Value, "shadowtls_password", "ShadowTLS " .. translate("Password"))
|
||||
o.password = true
|
||||
o:depends({ shadowtls = true, shadowtls_version = "2" })
|
||||
o:depends({ shadowtls = true, shadowtls_version = "3" })
|
||||
|
||||
o = s:option(Value, "shadowtls_serverName", "ShadowTLS " .. translate("Domain"))
|
||||
o:depends({ shadowtls = true })
|
||||
|
||||
if singbox_tags:find("with_utls") then
|
||||
o = s:option(Flag, "shadowtls_utls", "ShadowTLS " .. translate("uTLS"))
|
||||
o.default = "0"
|
||||
o:depends({ shadowtls = true })
|
||||
|
||||
o = s:option(ListValue, "shadowtls_fingerprint", "ShadowTLS " .. translate("Finger Print"))
|
||||
o:value("chrome")
|
||||
o:value("firefox")
|
||||
o:value("edge")
|
||||
o:value("safari")
|
||||
-- o:value("360")
|
||||
o:value("qq")
|
||||
o:value("ios")
|
||||
-- o:value("android")
|
||||
o:value("random")
|
||||
-- o:value("randomized")
|
||||
o.default = "chrome"
|
||||
o:depends({ shadowtls = true, shadowtls_utls = true })
|
||||
end
|
||||
|
||||
-- [[ SIP003 plugin ]]--
|
||||
o = s:option(Flag, "plugin_enabled", translate("plugin"))
|
||||
o.default = 0
|
||||
o:depends({ protocol = "shadowsocks" })
|
||||
|
||||
o = s:option(ListValue, "plugin", "SIP003 " .. translate("plugin"))
|
||||
o.default = "obfs-local"
|
||||
o:depends({ plugin_enabled = true })
|
||||
o:value("obfs-local")
|
||||
o:value("v2ray-plugin")
|
||||
|
||||
o = s:option(Value, "plugin_opts", translate("opts"))
|
||||
o:depends({ plugin_enabled = true })
|
||||
|
||||
o = s:option(ListValue, "domain_resolver", translate("Domain DNS Resolve"))
|
||||
o.description = translate("If the node address is a domain name, this DNS will be used for resolution.") .. "<br>" .. string.format('<font color="red">%s</font>',
|
||||
translate("Note: For node-specific DNS only. Keep Auto to avoid extra overhead."))
|
||||
o:value("", translate("Auto"))
|
||||
o:value("tcp", "TCP")
|
||||
o:value("udp", "UDP")
|
||||
o:value("https", "DoH")
|
||||
|
||||
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({ domain_resolver = "tcp" })
|
||||
o:depends({ domain_resolver = "udp" })
|
||||
|
||||
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({ domain_resolver = "https" })
|
||||
|
||||
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"))
|
||||
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["protocol"].keylist
|
||||
if #protocols > 0 then
|
||||
for i, v in ipairs(protocols) do
|
||||
if not v:find("^_") then
|
||||
local depends_condition = { protocol = v }
|
||||
if v == "hysteria2" then
|
||||
depends_condition["hysteria2_realms"] = false
|
||||
end
|
||||
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
|
||||
end
|
||||
-- [[ Normal single node End ]]
|
||||
|
||||
if not load_shunt_options then
|
||||
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.
|
||||
o:value("1", translate("Preproxy Node"))
|
||||
o:value("3", translate("Outbound Interface"))
|
||||
end
|
||||
o:value("2", translate("Landing Node"))
|
||||
|
||||
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, "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, "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 = {}
|
||||
|
||||
for k1, v1 in pairs(node_list) do
|
||||
if k1 ~= "shunt_list" and k1 ~= "iface_list" then
|
||||
for i, v in ipairs(v1) do
|
||||
if v.id ~= s.section then
|
||||
o1:value(v.id, v.remark)
|
||||
o1.group[#o1.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
if k1 == "normal_list" then
|
||||
-- Landing Node not support use special node.
|
||||
o2:value(v.id, v.remark)
|
||||
o2.group[#o2.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
api.type_cbi_section(s1, s)
|
||||
|
||||
if load_shunt_options then
|
||||
local shunt_lua = loadfile("/usr/lib/lua/luci/model/cbi/passwall/client/include/shunt_options.lua")
|
||||
setfenv(shunt_lua, getfenv(1))(m, s1, {
|
||||
node = {
|
||||
[".name"] = s.section,
|
||||
type = type_name,
|
||||
},
|
||||
node_list = node_list,
|
||||
})
|
||||
end
|
||||
@@ -1,874 +0,0 @@
|
||||
if not api.finded_com("xray") then
|
||||
return
|
||||
end
|
||||
|
||||
-- [[ Xray ]]
|
||||
local m, s1 = ...
|
||||
local type_name = "Xray"
|
||||
|
||||
s1.fields["type"]:value(type_name, "Xray")
|
||||
if not s1.fields["type"].default then
|
||||
s1.fields["type"].default = type_name
|
||||
end
|
||||
|
||||
if s1.val["type"] ~= type_name then
|
||||
return
|
||||
end
|
||||
|
||||
local s = NamedSection(m, arg[1], "tmp_" .. s1.sectiontype)
|
||||
s.parent = s1
|
||||
s.type_name = type_name
|
||||
s.option_prefix = "xray_"
|
||||
api.set_type_cbi(s)
|
||||
|
||||
local ss_method_list = {
|
||||
"aes-128-gcm", "aes-256-gcm", "chacha20-poly1305", "chacha20-ietf-poly1305", "xchacha20-poly1305", "xchacha20-ietf-poly1305", "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"
|
||||
}
|
||||
|
||||
local security_list = { "auto", "aes-128-gcm", "chacha20-poly1305" }
|
||||
|
||||
local header_type_list = {
|
||||
"none", "srtp", "utp", "wechat-video", "dtls", "wireguard", "dns"
|
||||
}
|
||||
|
||||
local xray_version = api.get_app_version("xray")
|
||||
|
||||
o = s:option(ListValue, "protocol", translate("Protocol"))
|
||||
o:value("socks", translate("Socks"))
|
||||
o:value("http", translate("HTTP"))
|
||||
o:value("vmess", translate("Vmess"))
|
||||
o:value("vless", translate("VLESS"))
|
||||
o:value("shadowsocks", translate("Shadowsocks"))
|
||||
o:value("trojan", translate("Trojan"))
|
||||
o:value("wireguard", translate("WireGuard"))
|
||||
if api.compare_versions(xray_version, ">=", "26.1.13") then
|
||||
o:value("hysteria2", translate("Hysteria2"))
|
||||
end
|
||||
if api.compare_versions(xray_version, ">=", "1.8.12") then
|
||||
o:value("_balancing", translate("Balancing"))
|
||||
end
|
||||
o:value("_shunt", translate("Shunt"))
|
||||
o:value("_iface", translate("Custom Interface"))
|
||||
local protocol_val = m:get(s.section, "protocol")
|
||||
local formvalue_proto = s.fields["protocol"]:formvalue(s.section)
|
||||
if formvalue_proto then
|
||||
protocol_val = formvalue_proto
|
||||
end
|
||||
|
||||
local load_balancing_options = protocol_val == "_balancing"
|
||||
local load_shunt_options = protocol_val == "_shunt"
|
||||
local load_iface_options = protocol_val == "_iface"
|
||||
local load_normal_options = true
|
||||
if load_balancing_options or load_shunt_options or load_iface_options then
|
||||
load_normal_options = nil
|
||||
end
|
||||
|
||||
local netdev_list = api.get_network_devices()
|
||||
local node_list = api.get_node_list()
|
||||
local fallback_list = {}
|
||||
local is_balancer = nil
|
||||
for k, e in ipairs(node_list.balancing_list or {}) do
|
||||
if e.id ~= s.section then
|
||||
fallback_list[#fallback_list + 1] = {
|
||||
id = e["id"],
|
||||
remark = e["remark"],
|
||||
group = e["group"],
|
||||
fallback = e.o["fallback_node"],
|
||||
}
|
||||
else
|
||||
is_balancer = true
|
||||
end
|
||||
end
|
||||
|
||||
if load_balancing_options then -- [[ Load balancing Start ]]
|
||||
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, "balancing_node", translate("Load balancing node list"), translate("Load balancing node list, <a target='_blank' href='https://xtls.github.io/config/routing.html#balancerobject'>document</a>"))
|
||||
o:depends({ node_add_mode = "manual" })
|
||||
o.widget = "checkbox"
|
||||
o.cast = "table"
|
||||
o.template = m:template_path("/cbi/nodes_multivalue")
|
||||
o.group = {}
|
||||
for k1, v1 in pairs(node_list) do
|
||||
if k1 == "socks_list" or k1 == "normal_list" then
|
||||
for i, v in ipairs(v1) do
|
||||
o:value(v.id, v.remark)
|
||||
o.group[#o.group+1] = v.group or ""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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, "node_match_rule", translate("Node Matching Rules"))
|
||||
o:depends({ node_add_mode = "batch" })
|
||||
local descrStr = "Example: <code>^A && B && !C && D$</code><br>"
|
||||
descrStr = descrStr .. "This means the node remark must start with A (^), include B, exclude C (!), and end with D ($).<br>"
|
||||
descrStr = descrStr .. "Conditions are joined by <code>&&</code> (AND), and their order does not affect the result.<br>"
|
||||
descrStr = descrStr .. "Multiple groups can be separated by <code>||</code> (OR), matching succeeds if any group matches.<br>"
|
||||
descrStr = descrStr .. "Example: <code>A && B || C && D</code> means (A AND B) OR (C AND D)."
|
||||
o.description = translate(descrStr)
|
||||
|
||||
o = s:option(ListValue, "balancingStrategy", translate("Balancing Strategy"))
|
||||
o:depends({ protocol = "_balancing" })
|
||||
o:value("random")
|
||||
o:value("roundRobin")
|
||||
o:value("leastPing")
|
||||
o:value("leastLoad")
|
||||
o.default = "random"
|
||||
|
||||
-- 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({ protocol = "_balancing" })
|
||||
o.template = m:template_path("/cbi/nodes_listvalue")
|
||||
-- 最大 fallback 套娃层数
|
||||
local MAX_FALLBACK_DEPTH = 3
|
||||
-- 检查是否会形成回环
|
||||
local function will_loop(start_id, target_id, depth)
|
||||
depth = depth or 0
|
||||
-- 超过最大深度后停止递归
|
||||
if depth >= MAX_FALLBACK_DEPTH then
|
||||
return false
|
||||
end
|
||||
for _, v in ipairs(fallback_list) do
|
||||
if v.id == target_id then
|
||||
local fb = v.fallback
|
||||
-- 没有 fallback
|
||||
if not fb or fb == "" or fb == "_direct" then
|
||||
return false
|
||||
end
|
||||
-- 检测到回环
|
||||
if fb == start_id then
|
||||
return true
|
||||
end
|
||||
-- 继续递归检查
|
||||
return will_loop(start_id, fb, depth + 1)
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
-- 获取 fallback 链深度
|
||||
local function get_fallback_depth(id, depth)
|
||||
depth = depth or 0
|
||||
if depth >= MAX_FALLBACK_DEPTH then
|
||||
return depth
|
||||
end
|
||||
for _, v in ipairs(fallback_list) do
|
||||
if v.id == id then
|
||||
local fb = v.fallback
|
||||
if not fb or fb == "" or fb == "_direct" then
|
||||
return depth
|
||||
end
|
||||
return get_fallback_depth(fb, depth + 1)
|
||||
end
|
||||
end
|
||||
return depth
|
||||
end
|
||||
for _, v in ipairs(fallback_list) do
|
||||
local depth = get_fallback_depth(v.id)
|
||||
-- 超过最大套娃层数后,不允许继续选择 balancer
|
||||
if depth < MAX_FALLBACK_DEPTH
|
||||
and not will_loop(s.section, v.id)
|
||||
then
|
||||
o:value(v.id, v.remark)
|
||||
o.group[#o.group + 1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
end
|
||||
end
|
||||
for k1, v1 in pairs(node_list) do
|
||||
if k1 == "socks_list" or k1 == "normal_list" or k1 == "urltest_list" then
|
||||
for i, v in ipairs(v1) do
|
||||
o:value(v.id, v.remark)
|
||||
o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 探测地址
|
||||
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, "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")
|
||||
o:value("https://www.youtube.com/generate_204", "YouTube")
|
||||
o:value("https://connect.rom.miui.com/generate_204", "MIUI (CN)")
|
||||
o:value("https://connectivitycheck.platform.hicloud.com/generate_204", "HiCloud (CN)")
|
||||
o:value("https://wifi.vivo.com.cn/generate_204", "VIVO (CN)")
|
||||
o.default = o.keylist[3]
|
||||
o.description = translate("The URL used to detect the connection status.")
|
||||
|
||||
-- 探测间隔
|
||||
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.") .. "<br>" ..
|
||||
translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are <code>s</code>, <code>m</code>, <code>h</code>, which correspond to seconds, minutes, and hours, respectively.") .. "<br>" ..
|
||||
translate("When the unit is not filled in, it defaults to seconds.")
|
||||
|
||||
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, "tolerance", translate("Failure Tolerance (%)"))
|
||||
o:depends({ balancingStrategy = "leastLoad" })
|
||||
o.datatype = "uinteger"
|
||||
o.default = "10"
|
||||
o.placeholder = "10"
|
||||
o.description = translate("The maximum acceptable speed test failure rate. For example, 1 means allowing a 1% failure rate.")
|
||||
end -- [[ 负载均衡 End ]]
|
||||
|
||||
if load_iface_options then -- [[ 自定义接口 Start ]]
|
||||
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
|
||||
end -- [[ 自定义接口 End ]]
|
||||
|
||||
|
||||
if load_normal_options then
|
||||
|
||||
o = s:option(Value, "address", translate("Address (Support Domain Name)"))
|
||||
|
||||
o = s:option(Value, "port", translate("Port"))
|
||||
o.datatype = "port"
|
||||
|
||||
o = s:option(Value, "uuid", translate("ID"))
|
||||
o.password = true
|
||||
o:depends({ protocol = "vmess" })
|
||||
o:depends({ protocol = "vless" })
|
||||
|
||||
o = s:option(Value, "username", translate("Username"))
|
||||
o:depends({ protocol = "http" })
|
||||
o:depends({ protocol = "socks" })
|
||||
|
||||
o = s:option(Value, "password", translate("Password"))
|
||||
o.password = true
|
||||
o:depends({ protocol = "http" })
|
||||
o:depends({ protocol = "socks" })
|
||||
o:depends({ protocol = "shadowsocks" })
|
||||
o:depends({ protocol = "trojan" })
|
||||
|
||||
o = s:option(ListValue, "security", translate("Encrypt Method"))
|
||||
for a, t in ipairs(security_list) do o:value(t) end
|
||||
o:depends({ protocol = "vmess" })
|
||||
|
||||
o = s:option(Value, "encryption", translate("Encrypt Method") .. " (encryption)")
|
||||
o.default = "none"
|
||||
o.placeholder = "none"
|
||||
o:depends({ protocol = "vless" })
|
||||
o.validate = function(self, value)
|
||||
value = api.trim(value)
|
||||
return (value == "" and "none" or value)
|
||||
end
|
||||
|
||||
o = s:option(ListValue, "ss_method", translate("Encrypt Method"))
|
||||
o.rewrite_option = "method"
|
||||
for a, t in ipairs(ss_method_list) do o:value(t) end
|
||||
o:depends({ protocol = "shadowsocks" })
|
||||
|
||||
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({ protocol = "vless" })
|
||||
|
||||
---- [[hysteria2]]
|
||||
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({ protocol = "hysteria2", hysteria2_realms = false })
|
||||
|
||||
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({ protocol = "hysteria2", hysteria2_realms = false })
|
||||
|
||||
o = s:option(Flag, "hysteria2_realms", translate("Realms"))
|
||||
o.default = "0"
|
||||
if api.compare_versions(xray_version, ">", "26.5.9") then
|
||||
o:depends({ protocol = "hysteria2"})
|
||||
else
|
||||
o:depends({ protocol = "__hide"})
|
||||
end
|
||||
|
||||
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)
|
||||
if realm then return value end
|
||||
return nil, translate("Invalid Realm URL.")
|
||||
end
|
||||
|
||||
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({ hysteria2_realms = "1" })
|
||||
|
||||
o = s:option(Flag, "hysteria2_realm_upnp", translate("Enable") .. " UPnP/NAT-PMP", translate("Enable UPnP/NAT-PMP port mapping on your gateway to improve hole punching success."))
|
||||
o.default = "0"
|
||||
o:depends({ hysteria2_realms = "1" })
|
||||
|
||||
o = s:option(Value, "hysteria2_auth_password", translate("Auth Password"))
|
||||
o.password = true
|
||||
o:depends({ protocol = "hysteria2"})
|
||||
|
||||
o = s:option(ListValue, "hysteria2_obfs_type", translate("Obfs Type"))
|
||||
o:value("", translate("Disable"))
|
||||
o:value("salamander")
|
||||
o:value("gecko")
|
||||
o:depends({ protocol = "hysteria2" })
|
||||
|
||||
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, "hysteria2_obfs_MinPacketSize", translate("Gecko Packet Size (min)"))
|
||||
o.datatype = "uinteger"
|
||||
o.placeholder = "512"
|
||||
o.default = "512"
|
||||
o:depends({ hysteria2_obfs_type = "gecko" })
|
||||
|
||||
o = s:option(Value, "hysteria2_obfs_MaxPacketSize", translate("Gecko Packet Size (max)"))
|
||||
o.datatype = "uinteger"
|
||||
o.placeholder = "1200"
|
||||
o.default = "1200"
|
||||
o:depends({ hysteria2_obfs_type = "gecko" })
|
||||
|
||||
o = s:option(Value, "hysteria2_up_mbps", translate("Max upload Mbps"))
|
||||
o:depends({ protocol = "hysteria2" })
|
||||
|
||||
o = s:option(Value, "hysteria2_down_mbps", translate("Max download Mbps"))
|
||||
o:depends({ protocol = "hysteria2" })
|
||||
|
||||
o = s:option(Value, "hysteria2_idle_timeout", translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)")
|
||||
o.datatype = "range(4,120)"
|
||||
o:depends({ protocol = "hysteria2"})
|
||||
|
||||
o = s:option(Value, "hysteria2_keep_alive_period", translate("QUIC KeepAlive interval"), translate("Units:seconds") .. " (2~60)")
|
||||
o.datatype = "range(2,60)"
|
||||
o:depends({ protocol = "hysteria2"})
|
||||
|
||||
o = s:option(Flag, "hysteria2_disable_mtu_discovery", translate("Disable MTU detection"))
|
||||
o.default = "0"
|
||||
o:depends({ protocol = "hysteria2"})
|
||||
---- [[hysteria2 end]]
|
||||
|
||||
o = s:option(Flag, "tls", translate("TLS"))
|
||||
o.default = 0
|
||||
o:depends({ protocol = "vmess" })
|
||||
o:depends({ protocol = "vless" })
|
||||
o:depends({ protocol = "http" })
|
||||
o:depends({ protocol = "trojan" })
|
||||
o:depends({ protocol = "shadowsocks" })
|
||||
|
||||
o = s:option(Flag, "reality", translate("REALITY"), translate("Only recommend to use with VLESS-TCP-XTLS-Vision."))
|
||||
o.default = 0
|
||||
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, "alpn", translate("alpn"))
|
||||
o.default = "default"
|
||||
o:value("default", translate("Default"))
|
||||
o:value("h3")
|
||||
o:value("h2")
|
||||
o:value("h3,h2")
|
||||
o:value("http/1.1")
|
||||
o:value("h2,http/1.1")
|
||||
o:value("h3,h2,http/1.1")
|
||||
o:depends({ tls = true, reality = false })
|
||||
|
||||
-- o = s:option(Value, "minversion", translate("minversion"))
|
||||
-- o.default = "1.3"
|
||||
-- o:value("1.3")
|
||||
-- o:depends({ tls = true })
|
||||
|
||||
o = s:option(Value, "tls_serverName", "SNI " .. translate("Domain"))
|
||||
o:depends({ tls = true })
|
||||
o:depends({ 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("<a href='javascript:void(0)' onclick='javascript:fetchCertSha256(this)'>%s</a>", "→ " .. translate("Fetch Manually"))
|
||||
|
||||
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, "tls_certificate", translate("TLS Certificate (PEM)"))
|
||||
o.default = "0"
|
||||
o:depends({ tls = true, reality = false })
|
||||
o:depends({ protocol = "hysteria2" })
|
||||
|
||||
o = s:option(TextValue, "tls_certificate_pem", " ", translate("Full certificate (chain), PEM format."))
|
||||
o.default = ""
|
||||
o.rows = 5
|
||||
o.wrap = "off"
|
||||
o:depends({ tls_certificate = true })
|
||||
o.cfgvalue = function(self, section)
|
||||
return (m:get(section, "tls_certificate_pem") or ""):gsub("\\n", "\n")
|
||||
end
|
||||
o.validate = function(self, value)
|
||||
value = api.trim(value):gsub("\r\n", "\n"):gsub("\r", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
|
||||
return value:gsub("\n", "\\n")
|
||||
end
|
||||
|
||||
o = s:option(Flag, "ech", translate("ECH"))
|
||||
o.default = "0"
|
||||
o:depends({ tls = true, reality = false })
|
||||
o:depends({ protocol = "hysteria2", hysteria2_realms = false })
|
||||
|
||||
o = s:option(TextValue, "ech_config", translate("ECH Config"))
|
||||
o.default = ""
|
||||
o.rows = 5
|
||||
o.wrap = "soft"
|
||||
o:depends({ ech = true })
|
||||
o.validate = function(self, value)
|
||||
return api.trim(value:gsub("[\r\n]", ""))
|
||||
end
|
||||
|
||||
-- [[ REALITY部分 ]] --
|
||||
o = s:option(Value, "reality_publicKey", translate("Public Key"))
|
||||
o:depends({ tls = true, reality = true })
|
||||
|
||||
o = s:option(Value, "reality_shortId", translate("Short Id"))
|
||||
o:depends({ tls = true, reality = true })
|
||||
|
||||
o = s:option(Value, "reality_spiderX", translate("Spider X"))
|
||||
o.placeholder = "/"
|
||||
o:depends({ tls = true, reality = true })
|
||||
|
||||
o = s:option(Flag, "utls", translate("uTLS"))
|
||||
o.default = "0"
|
||||
o:depends({ tls = true, reality = false })
|
||||
|
||||
o = s:option(ListValue, "fingerprint", translate("Finger Print"))
|
||||
o:value("chrome")
|
||||
o:value("firefox")
|
||||
o:value("edge")
|
||||
o:value("safari")
|
||||
o:value("360")
|
||||
o:value("qq")
|
||||
o:value("ios")
|
||||
o:value("android")
|
||||
o:value("random")
|
||||
o:value("randomized")
|
||||
o:value("unsafe")
|
||||
o.default = "chrome"
|
||||
o:depends({ tls = true, utls = true })
|
||||
o:depends({ tls = true, reality = true })
|
||||
|
||||
o = s:option(Value, "cipherSuites", translate("Cipher Suites"), '<a href="https://go.dev/src/crypto/tls/cipher_suites.go#L44" target="_blank">***</a>' .. " " .. translate("Configures the list of supported cipher suites, separated by :"))
|
||||
o:depends({ tls = true, reality = false })
|
||||
|
||||
o = s:option(Flag, "use_mldsa65Verify", translate("ML-DSA-65"))
|
||||
o.default = "0"
|
||||
o:depends({ tls = true, reality = true })
|
||||
|
||||
o = s:option(TextValue, "reality_mldsa65Verify", "ML-DSA-65 " .. translate("Public key"))
|
||||
o.default = ""
|
||||
o.rows = 5
|
||||
o.wrap = "soft"
|
||||
o:depends({ use_mldsa65Verify = true })
|
||||
o.validate = function(self, value)
|
||||
return api.trim(value:gsub("[\r\n]", ""))
|
||||
end
|
||||
|
||||
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({ protocol = "vmess" })
|
||||
o:depends({ protocol = "vless" })
|
||||
o:depends({ protocol = "shadowsocks" })
|
||||
o:depends({ protocol = "trojan" })
|
||||
|
||||
o = s:option(Value, "wireguard_public_key", translate("Public Key"))
|
||||
o:depends({ protocol = "wireguard" })
|
||||
|
||||
o = s:option(Value, "wireguard_secret_key", translate("Private Key"))
|
||||
o:depends({ protocol = "wireguard" })
|
||||
|
||||
o = s:option(Value, "wireguard_preSharedKey", translate("Pre shared key"))
|
||||
o:depends({ protocol = "wireguard" })
|
||||
|
||||
o = s:option(DynamicList, "wireguard_local_address", translate("Local Address"))
|
||||
o:depends({ protocol = "wireguard" })
|
||||
|
||||
o = s:option(Value, "wireguard_mtu", translate("MTU"))
|
||||
o.default = "1420"
|
||||
o:depends({ protocol = "wireguard" })
|
||||
|
||||
if api.compare_versions(xray_version, ">=", "1.8.0") then
|
||||
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, "wireguard_keepAlive", translate("Keep Alive"))
|
||||
o.default = "0"
|
||||
o:depends({ protocol = "wireguard" })
|
||||
|
||||
-- [[ RAW部分 ]]--
|
||||
|
||||
-- TCP伪装
|
||||
o = s:option(ListValue, "tcp_guise", translate("Camouflage Type"))
|
||||
o:value("none", "none")
|
||||
o:value("http", "http")
|
||||
o:depends({ transport = "raw" })
|
||||
|
||||
-- HTTP域名
|
||||
o = s:option(DynamicList, "tcp_guise_http_host", translate("HTTP Host"))
|
||||
o:depends({ tcp_guise = "http" })
|
||||
|
||||
-- HTTP路径
|
||||
o = s:option(DynamicList, "tcp_guise_http_path", translate("HTTP Path"))
|
||||
o.placeholder = "/"
|
||||
o:depends({ tcp_guise = "http" })
|
||||
|
||||
-- [[ mKCP部分 ]]--
|
||||
|
||||
o = s:option(ListValue, "mkcp_guise", translate("Camouflage Type"), translate('<br />none: default, no masquerade, data sent is packets with no characteristics.<br />srtp: disguised as an SRTP packet, it will be recognized as video call data (such as FaceTime).<br />utp: packets disguised as uTP will be recognized as bittorrent downloaded data.<br />wechat-video: packets disguised as WeChat video calls.<br />dtls: disguised as DTLS 1.2 packet.<br />wireguard: disguised as a WireGuard packet. (not really WireGuard protocol)<br />dns: Disguising traffic as DNS requests.'))
|
||||
for a, t in ipairs(header_type_list) do o:value(t) end
|
||||
o:depends({ transport = "mkcp" })
|
||||
|
||||
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, "mkcp_mtu", translate("KCP MTU"))
|
||||
o.datatype = "uinteger"
|
||||
o.default = 1350
|
||||
o:depends({ transport = "mkcp" })
|
||||
|
||||
o = s:option(Value, "mkcp_seed", translate("KCP Seed"))
|
||||
o:depends({ transport = "mkcp" })
|
||||
|
||||
-- [[ WebSocket部分 ]]--
|
||||
o = s:option(Value, "ws_host", translate("WebSocket Host"))
|
||||
o:depends({ transport = "ws" })
|
||||
|
||||
o = s:option(Value, "ws_path", translate("WebSocket Path"))
|
||||
o.placeholder = "/"
|
||||
o:depends({ transport = "ws" })
|
||||
|
||||
o = s:option(Value, "ws_heartbeatPeriod", translate("HeartbeatPeriod(second)"))
|
||||
o.datatype = "integer"
|
||||
o:depends({ transport = "ws" })
|
||||
|
||||
-- [[ gRPC部分 ]]--
|
||||
o = s:option(Value, "grpc_serviceName", "ServiceName")
|
||||
o:depends({ transport = "grpc" })
|
||||
|
||||
o = s:option(ListValue, "grpc_mode", "gRPC " .. translate("Transfer mode"))
|
||||
o:value("gun")
|
||||
o:value("multi")
|
||||
o:depends({ transport = "grpc" })
|
||||
|
||||
o = s:option(Flag, "grpc_health_check", translate("Health check"))
|
||||
o:depends({ transport = "grpc" })
|
||||
|
||||
o = s:option(Value, "grpc_idle_timeout", translate("Idle timeout"))
|
||||
o.default = "10"
|
||||
o:depends({ grpc_health_check = true })
|
||||
|
||||
o = s:option(Value, "grpc_health_check_timeout", translate("Health check timeout"))
|
||||
o.default = "20"
|
||||
o:depends({ grpc_health_check = true })
|
||||
|
||||
o = s:option(Flag, "grpc_permit_without_stream", translate("Permit without stream"))
|
||||
o.default = "0"
|
||||
o:depends({ grpc_health_check = true })
|
||||
|
||||
o = s:option(Value, "grpc_initial_windows_size", translate("Initial Windows Size"))
|
||||
o.default = "0"
|
||||
o:depends({ transport = "grpc" })
|
||||
|
||||
-- [[ HttpUpgrade部分 ]]--
|
||||
o = s:option(Value, "httpupgrade_host", translate("HttpUpgrade Host"))
|
||||
o:depends({ transport = "httpupgrade" })
|
||||
|
||||
o = s:option(Value, "httpupgrade_path", translate("HttpUpgrade Path"))
|
||||
o.placeholder = "/"
|
||||
o:depends({ transport = "httpupgrade" })
|
||||
|
||||
-- [[ 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, "xhttp_host", translate("XHTTP Host"))
|
||||
o:depends({ transport = "xhttp" })
|
||||
|
||||
o = s:option(Value, "xhttp_path", translate("XHTTP Path"))
|
||||
o.placeholder = "/"
|
||||
o:depends({ transport = "xhttp" })
|
||||
|
||||
o = s:option(Flag, "use_xhttp_extra", translate("XHTTP Extra"))
|
||||
o.default = "0"
|
||||
o:depends({ transport = "xhttp" })
|
||||
|
||||
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"
|
||||
local o_validate = o.validate
|
||||
o.validate = function(self, value)
|
||||
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
|
||||
local v = o_validate(self, value)
|
||||
if v then return v end
|
||||
return nil, "XHTTP Extra " .. translate("Must be JSON text!")
|
||||
end
|
||||
o.cfgvalue = function(self, section, value)
|
||||
local raw = m:get(section, "xhttp_extra")
|
||||
if raw then
|
||||
return api.base64Decode(raw)
|
||||
end
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
m:set(section, "xhttp_extra", api.base64Encode(value) or "")
|
||||
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)
|
||||
if address and address ~= "" then
|
||||
address = address:gsub("^%[", ""):gsub("%]$", "")
|
||||
m:set(section, "download_address", address)
|
||||
else
|
||||
m:del(section, "download_address")
|
||||
end
|
||||
else
|
||||
m:del(section, "download_address")
|
||||
end
|
||||
end
|
||||
o.remove = function(self, section, value)
|
||||
m:del(section, "xhttp_extra")
|
||||
m:del(section, "download_address")
|
||||
end
|
||||
|
||||
-- [[ 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")
|
||||
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0", "firefox")
|
||||
o:value("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", "safari")
|
||||
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({ 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, "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, "mux_concurrency", translate("Mux concurrency"))
|
||||
o.default = -1
|
||||
o:depends({ mux = true })
|
||||
|
||||
o = s:option(Value, "xudp_concurrency", translate("XUDP Mux concurrency"))
|
||||
o.default = 8
|
||||
o:depends({ mux = true })
|
||||
|
||||
--[[FinalMask]]
|
||||
o = s:option(Flag, "use_finalmask", "FinalMask")
|
||||
o.default = "0"
|
||||
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, "finalmask", " ")
|
||||
o:depends({ use_finalmask = true })
|
||||
o.rows = 10
|
||||
o.wrap = "off"
|
||||
o.description = translate("An FinalMaskObject in JSON format, used for sharing.") .. "<br>" ..
|
||||
translate("Custom finalmask overrides mkcp, hysteria2, fragment, noise, and related settings.")
|
||||
o.datatype = "json"
|
||||
local o_validate = o.validate
|
||||
o.validate = function(self, value)
|
||||
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
|
||||
local v = o_validate(self, value)
|
||||
if v then return v end
|
||||
return nil, "FinalMask " .. translate("Must be JSON text!")
|
||||
end
|
||||
o.cfgvalue = function(self, section, value)
|
||||
local raw = m:get(section, "finalmask")
|
||||
if raw then
|
||||
return api.base64Decode(raw)
|
||||
end
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
m:set(section, "finalmask", api.base64Encode(value) or "")
|
||||
end
|
||||
|
||||
--[[Fast Open]]
|
||||
o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open"), translate("Need node support required"))
|
||||
o.default = 0
|
||||
|
||||
--[[tcpMptcp]]
|
||||
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, "domain_resolver", translate("Domain DNS Resolve"))
|
||||
o.description = translate("If the node address is a domain name, this DNS will be used for resolution.") .. "<br>" .. string.format('<font color="red">%s</font>',
|
||||
translate("Note: For node-specific DNS only. Keep Auto to avoid extra overhead."))
|
||||
o:value("", translate("Auto"))
|
||||
o:value("tcp", "TCP")
|
||||
o:value("udp", "UDP")
|
||||
o:value("https", "DoH")
|
||||
|
||||
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({ domain_resolver = "tcp" })
|
||||
o:depends({ domain_resolver = "udp" })
|
||||
|
||||
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({ domain_resolver = "https" })
|
||||
|
||||
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("UseIPv4v6", translate("Prefer IPv4"))
|
||||
o:value("UseIPv6v4", translate("Prefer IPv6"))
|
||||
o:value("UseIPv4", translate("IPv4 Only"))
|
||||
o:value("UseIPv6", translate("IPv6 Only"))
|
||||
|
||||
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["protocol"].keylist
|
||||
if #protocols > 0 then
|
||||
for i, v in ipairs(protocols) do
|
||||
if not v:find("^_") then
|
||||
local depends_condition = { protocol = v }
|
||||
if v == "hysteria2" then
|
||||
depends_condition["hysteria2_realms"] = false
|
||||
end
|
||||
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["happy_eyeballs"] = false
|
||||
s.fields["domain_strategy"]:depends(strategy_depends)
|
||||
|
||||
if v ~= "hysteria2" then
|
||||
s.fields["tcp_fast_open"]:depends({ protocol = v })
|
||||
s.fields["tcpMptcp"]:depends({ protocol = v })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- [[ Normal single node End ]]
|
||||
|
||||
if not load_shunt_options then
|
||||
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.
|
||||
o:value("1", translate("Preproxy Node"))
|
||||
o:value("3", translate("Outbound Interface"))
|
||||
end
|
||||
o:value("2", translate("Landing Node"))
|
||||
|
||||
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, "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, "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 = {}
|
||||
|
||||
for k1, v1 in pairs(node_list) do
|
||||
if k1 ~= "shunt_list" and k1 ~= "iface_list" then
|
||||
for i, v in ipairs(v1) do
|
||||
if v.id ~= s.section then
|
||||
o1:value(v.id, v.remark)
|
||||
o1.group[#o1.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
if k1 == "normal_list" then
|
||||
-- Landing Node not support use special node.
|
||||
o2:value(v.id, v.remark)
|
||||
o2.group[#o2.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
api.type_cbi_section(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/passwall/client/include/shunt_options.lua")
|
||||
setfenv(shunt_lua, getfenv(1))(m, s1, {
|
||||
node = {
|
||||
[".name"] = s.section,
|
||||
type = type_name,
|
||||
},
|
||||
node_list = node_list,
|
||||
})
|
||||
end
|
||||
@@ -1,74 +0,0 @@
|
||||
if not api.is_finded("sslocal") then
|
||||
return
|
||||
end
|
||||
|
||||
-- [[ Shadowsocks Rust ]]
|
||||
local m, s1 = ...
|
||||
local type_name = "SS-Rust"
|
||||
|
||||
s1.fields["type"]:value(type_name, "Shadowsocks Rust")
|
||||
|
||||
if s1.val["type"] ~= type_name then
|
||||
return
|
||||
end
|
||||
|
||||
local s = NamedSection(m, arg[1], "tmp_" .. s1.sectiontype)
|
||||
s.parent = s1
|
||||
s.type_name = type_name
|
||||
s.option_prefix = "ssrust_"
|
||||
api.set_type_cbi(s)
|
||||
|
||||
local ssrust_encrypt_method_list = {
|
||||
"none", "plain",
|
||||
"aes-128-gcm", "aes-256-gcm", "chacha20-ietf-poly1305",
|
||||
"2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"
|
||||
}
|
||||
|
||||
o = s:option(ListValue, "del_protocol") --始终隐藏,用于删除 protocol
|
||||
o:depends({ __hide = "1" })
|
||||
o.rewrite_option = "protocol"
|
||||
|
||||
o = s:option(Value, "address", translate("Address (Support Domain Name)"))
|
||||
|
||||
o = s:option(Value, "port", translate("Port"))
|
||||
o.datatype = "port"
|
||||
|
||||
o = s:option(Value, "password", translate("Password"))
|
||||
o.password = true
|
||||
|
||||
o = s:option(Value, "ss_method", translate("Encrypt Method"))
|
||||
for a, t in ipairs(ssrust_encrypt_method_list) do o:value(t) end
|
||||
|
||||
o = s:option(Value, "timeout", translate("Connection Timeout"))
|
||||
o.datatype = "uinteger"
|
||||
o.default = 300
|
||||
|
||||
o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open"), translate("Need node support required"))
|
||||
o.default = 0
|
||||
|
||||
o = s:option(Flag, "plugin_enabled", translate("plugin"))
|
||||
o.default = 0
|
||||
|
||||
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({ plugin_enabled = true })
|
||||
o.validate = function(self, value, t)
|
||||
if value and value ~= "" and value ~= "none" then
|
||||
if not api.is_finded(value) then
|
||||
return nil, value .. ": " .. translate("Can't find this file!")
|
||||
else
|
||||
return value
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
o = s:option(Value, "plugin_opts", translate("opts"))
|
||||
o:depends({ plugin_enabled = true })
|
||||
|
||||
api.type_cbi_section(s1, s)
|
||||
@@ -1,68 +0,0 @@
|
||||
if not api.is_finded("ssr-local") and not api.is_finded("ssr-redir")then
|
||||
return
|
||||
end
|
||||
|
||||
-- [[ ShadowsocksR Libev ]]
|
||||
local m, s1 = ...
|
||||
local type_name = "SSR"
|
||||
|
||||
s1.fields["type"]:value(type_name, "ShadowsocksR Libev")
|
||||
|
||||
if s1.val["type"] ~= type_name then
|
||||
return
|
||||
end
|
||||
|
||||
local s = NamedSection(m, arg[1], "tmp_" .. s1.sectiontype)
|
||||
s.parent = s1
|
||||
s.type_name = type_name
|
||||
s.option_prefix = "ssr_"
|
||||
api.set_type_cbi(s)
|
||||
|
||||
local ssr_encrypt_method_list = {
|
||||
"none", "table", "rc2-cfb", "rc4", "rc4-md5", "rc4-md5-6", "aes-128-cfb",
|
||||
"aes-192-cfb", "aes-256-cfb", "aes-128-ctr", "aes-192-ctr", "aes-256-ctr",
|
||||
"bf-cfb", "camellia-128-cfb", "camellia-192-cfb", "camellia-256-cfb",
|
||||
"cast5-cfb", "des-cfb", "idea-cfb", "seed-cfb", "salsa20", "chacha20",
|
||||
"chacha20-ietf"
|
||||
}
|
||||
|
||||
local ssr_protocol_list = {
|
||||
"origin", "verify_simple", "verify_deflate", "verify_sha1", "auth_simple",
|
||||
"auth_sha1", "auth_sha1_v2", "auth_sha1_v4", "auth_aes128_md5",
|
||||
"auth_aes128_sha1", "auth_chain_a", "auth_chain_b", "auth_chain_c",
|
||||
"auth_chain_d", "auth_chain_e", "auth_chain_f"
|
||||
}
|
||||
local ssr_obfs_list = {
|
||||
"plain", "http_simple", "http_post", "random_head", "tls_simple",
|
||||
"tls1.0_session_auth", "tls1.2_ticket_auth"
|
||||
}
|
||||
|
||||
o = s:option(Value, "address", translate("Address (Support Domain Name)"))
|
||||
|
||||
o = s:option(Value, "port", translate("Port"))
|
||||
o.datatype = "port"
|
||||
|
||||
o = s:option(Value, "password", translate("Password"))
|
||||
o.password = true
|
||||
|
||||
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, "protocol", translate("Protocol"))
|
||||
for a, t in ipairs(ssr_protocol_list) do o:value(t) end
|
||||
|
||||
o = s:option(Value, "protocol_param", translate("Protocol_param"))
|
||||
|
||||
o = s:option(ListValue, "obfs", translate("Obfs"))
|
||||
for a, t in ipairs(ssr_obfs_list) do o:value(t) end
|
||||
|
||||
o = s:option(Value, "obfs_param", translate("Obfs_param"))
|
||||
|
||||
o = s:option(Value, "timeout", translate("Connection Timeout"))
|
||||
o.datatype = "uinteger"
|
||||
o.default = 300
|
||||
|
||||
o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open"), translate("Need node support required"))
|
||||
o.default = 0
|
||||
|
||||
api.type_cbi_section(s1, s)
|
||||
@@ -1,148 +0,0 @@
|
||||
if not api.finded_com("hysteria") then
|
||||
return
|
||||
end
|
||||
|
||||
-- [[ Hysteria2 ]]
|
||||
local m, s1 = ...
|
||||
local type_name = "Hysteria2"
|
||||
|
||||
s1.fields["type"]:value(type_name, "Hysteria2")
|
||||
|
||||
if s1.val["type"] ~= type_name then
|
||||
return
|
||||
end
|
||||
|
||||
local s = NamedSection(m, arg[1], "tmp_" .. s1.sectiontype)
|
||||
s.parent = s1
|
||||
s.type_name = type_name
|
||||
s.option_prefix = "hysteria2_"
|
||||
api.set_type_cbi(s)
|
||||
|
||||
o = s:option(ListValue, "protocol", translate("Protocol"))
|
||||
o:value("udp", "UDP")
|
||||
|
||||
o = s:option(Value, "address", translate("Address (Support Domain Name)"))
|
||||
o:depends({ realms = false })
|
||||
|
||||
o = s:option(Value, "port", translate("Port"))
|
||||
o.datatype = "port"
|
||||
o:depends({ realms = false })
|
||||
|
||||
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({ realms = false })
|
||||
|
||||
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({ realms = false })
|
||||
|
||||
o = s:option(Flag, "realms", translate("Realms"))
|
||||
o.default = "0"
|
||||
o.rewrite_option = o.option
|
||||
|
||||
o = s:option(Value, "realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name")
|
||||
o.rewrite_option = o.option
|
||||
o:depends({ 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, "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({ realms = "1" })
|
||||
|
||||
o = s:option(Flag, "realm_upnp", translate("Enable") .. " UPnP/NAT-PMP", translate("Enable UPnP/NAT-PMP port mapping on your gateway to improve hole punching success."))
|
||||
o.default = "0"
|
||||
o.rewrite_option = o.option
|
||||
o:depends({ realms = "1" })
|
||||
|
||||
o = s:option(Value, "auth_password", translate("Auth Password"))
|
||||
o.password = true
|
||||
o.rewrite_option = o.option
|
||||
|
||||
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, "obfs_password", translate("Obfs Password"))
|
||||
o.rewrite_option = o.option
|
||||
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.rewrite_option = o.option
|
||||
|
||||
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.rewrite_option = o.option
|
||||
|
||||
o = s:option(Flag, "fast_open", translate("Fast Open"))
|
||||
o.default = "0"
|
||||
|
||||
o = s:option(Value, "tls_serverName", translate("Domain"))
|
||||
|
||||
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, "tls_pinSHA256", translate("PinSHA256"),translate("Certificate fingerprint"))
|
||||
|
||||
o = s:option(Value, "up_mbps", translate("Max upload Mbps"))
|
||||
o.rewrite_option = o.option
|
||||
|
||||
o = s:option(Value, "down_mbps", translate("Max download Mbps"))
|
||||
o.rewrite_option = o.option
|
||||
|
||||
o = s:option(Value, "recv_window", translate("QUIC stream receive window"))
|
||||
o.rewrite_option = o.option
|
||||
|
||||
o = s:option(Value, "recv_window_conn", translate("QUIC connection receive window"))
|
||||
o.rewrite_option = o.option
|
||||
|
||||
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, "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, "disable_mtu_discovery", translate("Disable MTU detection"))
|
||||
o.default = "0"
|
||||
o.rewrite_option = o.option
|
||||
|
||||
|
||||
o = s:option(Flag, "ech", translate("ECH"))
|
||||
o.default = "0"
|
||||
o:depends({ realms = false })
|
||||
|
||||
o = s:option(TextValue, "ech_config", translate("ECH Config"))
|
||||
o.default = ""
|
||||
o.rows = 5
|
||||
o.wrap = "soft"
|
||||
o:depends({ ech = "1" })
|
||||
o.validate = function(self, value)
|
||||
return api.trim(value:gsub("[\r\n]", ""))
|
||||
end
|
||||
|
||||
o = s:option(Flag, "lazy_start", translate("Lazy Start"))
|
||||
o.default = "0"
|
||||
o.rewrite_option = o.option
|
||||
|
||||
api.type_cbi_section(s1, s)
|
||||
@@ -1,35 +0,0 @@
|
||||
if not api.is_finded("naive") then
|
||||
return
|
||||
end
|
||||
|
||||
-- [[ Naive ]]
|
||||
local m, s1 = ...
|
||||
local type_name = "Naiveproxy"
|
||||
|
||||
s1.fields["type"]:value(type_name, "NaïveProxy")
|
||||
|
||||
if s1.val["type"] ~= type_name then
|
||||
return
|
||||
end
|
||||
|
||||
local s = NamedSection(m, arg[1], "tmp_" .. s1.sectiontype)
|
||||
s.parent = s1
|
||||
s.type_name = type_name
|
||||
s.option_prefix = "naive_"
|
||||
api.set_type_cbi(s)
|
||||
|
||||
o = s:option(ListValue, "protocol", translate("Protocol"))
|
||||
o:value("https", translate("HTTPS"))
|
||||
o:value("quic", translate("QUIC"))
|
||||
|
||||
o = s:option(Value, "address", translate("Address (Support Domain Name)"))
|
||||
|
||||
o = s:option(Value, "port", translate("Port"))
|
||||
o.datatype = "port"
|
||||
|
||||
o = s:option(Value, "username", translate("Username"))
|
||||
|
||||
o = s:option(Value, "password", translate("Password"))
|
||||
o.password = true
|
||||
|
||||
api.type_cbi_section(s1, s)
|
||||
@@ -1,147 +0,0 @@
|
||||
local api = require "luci.passwall.api"
|
||||
api.set_default_cbi()
|
||||
|
||||
m = Map(api.s_config)
|
||||
m.redirect = api.url("server")
|
||||
|
||||
t = m:section(NamedSection, "global", "global", translate("Server-Side"))
|
||||
t.anonymous = true
|
||||
t.addremove = false
|
||||
|
||||
e = t:option(Flag, "enable", translate("Enable"))
|
||||
e.rmempty = false
|
||||
|
||||
e = t:option(Value, "start_delay", translate("Delay Start"), translate("Units:seconds"))
|
||||
e.default = "90"
|
||||
e.datatype = "range(0,600)"
|
||||
e.rmempty = false
|
||||
|
||||
|
||||
s_server = m:section(TypedSection, "server", translate("Servers Manager"))
|
||||
s_server.anonymous = true
|
||||
s_server.addremove = true
|
||||
s_server.sortable = true
|
||||
s_server.template = "cbi/tblsection"
|
||||
s_server.extedit = api.url("server_config", "%s")
|
||||
function s_server.create(self, section)
|
||||
local uid = api.gen_random_char()
|
||||
TypedSection.create(self, uid)
|
||||
luci.http.redirect(self.extedit:format(uid))
|
||||
end
|
||||
function s_server.remove(self, section)
|
||||
local o = m:get(section) or {}
|
||||
if o[".type"] == self.sectiontype then
|
||||
m:del(section)
|
||||
end
|
||||
end
|
||||
|
||||
e = s_server:option(Flag, "enable", translate("Enable"))
|
||||
e.width = "5%"
|
||||
e.rmempty = false
|
||||
|
||||
e = s_server:option(DummyValue, "status", translate("Status"))
|
||||
e.rawhtml = true
|
||||
e.cfgvalue = function(t, n)
|
||||
return string.format('<font class="_servers_status">%s</font>', translate("Collecting data..."))
|
||||
end
|
||||
|
||||
e = s_server:option(DummyValue, "remarks", translate("Remarks"))
|
||||
e.width = "15%"
|
||||
|
||||
e = s_server:option(DummyValue, "type", translate("Type"))
|
||||
e.width = "20%"
|
||||
e.rawhtml = true
|
||||
e.cfgvalue = function(t, n)
|
||||
local str = ""
|
||||
local type = m:get(n, "type") or ""
|
||||
if type == "sing-box" or type == "Xray" then
|
||||
local protocol = m:get(n, "protocol") or ""
|
||||
if protocol == "vmess" then
|
||||
protocol = "VMess"
|
||||
elseif protocol == "vless" then
|
||||
protocol = "VLESS"
|
||||
elseif protocol == "shadowsocks" then
|
||||
protocol = "SS"
|
||||
elseif protocol == "shadowsocksr" then
|
||||
protocol = "SSR"
|
||||
elseif protocol == "wireguard" then
|
||||
protocol = "WG"
|
||||
elseif protocol == "hysteria" then
|
||||
protocol = "HY"
|
||||
elseif protocol == "hysteria2" then
|
||||
protocol = "HY2"
|
||||
elseif protocol == "anytls" then
|
||||
protocol = "AnyTLS"
|
||||
else
|
||||
protocol = protocol:gsub("^%l",string.upper)
|
||||
local custom = m:get(n, "custom") or "0"
|
||||
if custom == "1" then
|
||||
protocol = translate("Custom Config")
|
||||
end
|
||||
end
|
||||
if type == "sing-box" then type = "Sing-Box" end
|
||||
type = type .. " " .. protocol
|
||||
end
|
||||
str = str .. translate(type)
|
||||
return str
|
||||
end
|
||||
|
||||
e = s_server:option(DummyValue, "port", translate("Port"))
|
||||
|
||||
e = s_server:option(Flag, "log", translate("Log"))
|
||||
e.default = "1"
|
||||
e.rmempty = false
|
||||
|
||||
m:appendTemplate("/cbi/sortable", {sectiontype = s_server.sectiontype})
|
||||
|
||||
m:appendTemplate("/server/server_list_status", {sectiontype = s_server.sectiontype})
|
||||
|
||||
s_user = m:section(TypedSection, "user", translate("Users Manager"))
|
||||
s_user.anonymous = true
|
||||
s_user.addremove = true
|
||||
s_user.sortable = true
|
||||
s_user.template = "cbi/tblsection"
|
||||
s_user.extedit = api.url("server_user_config", "%s")
|
||||
s_user.create = function(e, section)
|
||||
local uid = api.gen_random_char()
|
||||
TypedSection.create(e, uid)
|
||||
luci.http.redirect(e.extedit:format(uid))
|
||||
end
|
||||
s_user.remove = function(self, section)
|
||||
local o = m:get(section) or {}
|
||||
if o[".type"] == self.sectiontype then
|
||||
m:foreach("server", function(o)
|
||||
if o.user and section == o.user then
|
||||
m:set(o[".name"], "user", "")
|
||||
end
|
||||
local changed = false
|
||||
local users = o.users or {}
|
||||
for i = #users, 1, -1 do
|
||||
if section == users[i] then
|
||||
table.remove(users, i)
|
||||
changed = true
|
||||
end
|
||||
end
|
||||
if changed then
|
||||
m:set(o[".name"], "users", users)
|
||||
end
|
||||
end)
|
||||
m:del(section)
|
||||
luci.http.redirect(api.url("server"))
|
||||
end
|
||||
end
|
||||
|
||||
e = s_user:option(DummyValue, "username", translate("Username"))
|
||||
e.width = "25%"
|
||||
|
||||
e = s_user:option(DummyValue, "password", translate("Password"))
|
||||
e.width = "25%"
|
||||
|
||||
e = s_user:option(DummyValue, "uuid", "UUID")
|
||||
e.width = "25%"
|
||||
|
||||
m:appendTemplate("/cbi/sortable", {sectiontype = s_user.sectiontype})
|
||||
|
||||
m:appendTemplate("/server/log")
|
||||
|
||||
return api.return_map(m)
|
||||
@@ -1,52 +0,0 @@
|
||||
api = require "luci.passwall.api"
|
||||
api.set_default_cbi()
|
||||
|
||||
m = Map(api.s_config)
|
||||
m.redirect = api.url("server")
|
||||
|
||||
if not arg[1] or not m:get(arg[1]) then
|
||||
luci.http.redirect(m.redirect)
|
||||
end
|
||||
|
||||
m:appendTemplate("/server/config_header", {section = arg[1]})
|
||||
|
||||
m:appendTemplate("/cbi/nodes_listvalue_com")
|
||||
|
||||
user_list = {}
|
||||
m:foreach("user", function(s)
|
||||
user_list[#user_list + 1] = s
|
||||
end)
|
||||
|
||||
local s = m:section(NamedSection, arg[1], "server", translate("Server Config"))
|
||||
s.addremove = false
|
||||
s.dynamic = false
|
||||
|
||||
local types_dir = "/usr/lib/lua/luci/model/cbi/" .. api.appname .. "/server/type/"
|
||||
s.val = m:get(arg[1]) or {}
|
||||
|
||||
o = s:option(Flag, "enable", translate("Enable"))
|
||||
o.default = "1"
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "remarks", translate("Remarks"))
|
||||
o.default = translate("Remarks")
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(ListValue, "type", translate("Type"))
|
||||
|
||||
local type_table = {}
|
||||
for filename in api.fs.dir(types_dir) do
|
||||
table.insert(type_table, filename)
|
||||
end
|
||||
table.sort(type_table, function(a, b)
|
||||
return a < b
|
||||
end)
|
||||
|
||||
for index, value in ipairs(type_table) do
|
||||
local p_func = loadfile(types_dir .. value)
|
||||
setfenv(p_func, getfenv(1))(m, s)
|
||||
end
|
||||
|
||||
m:appendTemplate("/server/config_footer", {section = arg[1]})
|
||||
|
||||
return api.return_map(m)
|
||||
@@ -1,49 +0,0 @@
|
||||
if not api.is_finded("microsocks") then
|
||||
return
|
||||
end
|
||||
|
||||
-- [[ microsocks ]]
|
||||
local m, s1 = ...
|
||||
local type_name = "Socks"
|
||||
|
||||
s1.fields["type"]:value(type_name, "Socks")
|
||||
if not s1.fields["type"].default then
|
||||
s1.fields["type"].default = type_name
|
||||
end
|
||||
|
||||
if s1.val["type"] and s1.val["type"] ~= type_name then
|
||||
return
|
||||
end
|
||||
|
||||
local s = NamedSection(m, arg[1], "tmp_" .. s1.sectiontype)
|
||||
s.parent = s1
|
||||
s.type_name = type_name
|
||||
s.option_prefix = "socks_"
|
||||
api.set_type_cbi(s)
|
||||
|
||||
o = s:option(Value, "port", translate("Listen Port"))
|
||||
o.datatype = "port"
|
||||
|
||||
o = s:option(Flag, "auth", translate("Auth"))
|
||||
|
||||
o = s:option(ListValue, "user", translate("User"))
|
||||
for i, v in ipairs(user_list) do
|
||||
o:value(v[".name"], v.username)
|
||||
end
|
||||
o:depends({ auth = true })
|
||||
|
||||
o = s:option(Flag, "firewall_allow", translate("Firewall Allow"))
|
||||
o.default = "0"
|
||||
|
||||
o = s:option(Value, "firewall_allow_src", translate("Source zone"))
|
||||
o.rmempty = not m.is_js_luci
|
||||
o.nocreate = true
|
||||
o.allowany = true
|
||||
o.default = "wan"
|
||||
o.template = "cbi/firewall_zonelist"
|
||||
o:depends({ firewall_allow = true })
|
||||
|
||||
o = s:option(Flag, "log", translate("Log"))
|
||||
o.default = "1"
|
||||
|
||||
api.type_cbi_section(s1, s)
|
||||
@@ -1,611 +0,0 @@
|
||||
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"
|
||||
|
||||
s1.fields["type"]:value(type_name, "Sing-Box")
|
||||
if not s1.fields["type"].default then
|
||||
s1.fields["type"].default = type_name
|
||||
end
|
||||
|
||||
if not s1.val["type"] then
|
||||
s1.val["type"] = type_name
|
||||
end
|
||||
|
||||
if s1.val["type"] and s1.val["type"] ~= type_name then
|
||||
return
|
||||
end
|
||||
|
||||
local s = NamedSection(m, arg[1], "tmp_" .. s1.sectiontype)
|
||||
s.parent = s1
|
||||
s.type_name = type_name
|
||||
s.option_prefix = "singbox_"
|
||||
api.set_type_cbi(s)
|
||||
|
||||
local singbox_tags = luci.sys.exec(singbox_bin .. " version | grep 'Tags:' | awk '{print $2}'")
|
||||
|
||||
local local_version = api.get_app_version("sing-box"):match("[^v]+")
|
||||
local version_ge_1_14_0 = api.compare_versions(local_version, ">=", "1.14.0")
|
||||
|
||||
local ss_method_list = {
|
||||
"none", "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "xchacha20-ietf-poly1305",
|
||||
"2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"
|
||||
}
|
||||
|
||||
o = s:option(Flag, "custom", translate("Use Custom Config"))
|
||||
|
||||
o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)")
|
||||
o.rows = 10
|
||||
o.wrap = "off"
|
||||
o:depends({ custom = true })
|
||||
o.datatype = "json"
|
||||
local o_validate = o.validate
|
||||
o.validate = function(self, value)
|
||||
local v = o_validate(self, value)
|
||||
if v then return v end
|
||||
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
|
||||
end
|
||||
o.cfgvalue = function(self, section)
|
||||
local config_str = m:get(section, "config_str")
|
||||
if config_str then
|
||||
return api.base64Decode(config_str)
|
||||
end
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
m:set(section, "config_str", api.base64Encode(value) or "")
|
||||
end
|
||||
|
||||
o = s:option(ListValue, "protocol", translate("Protocol"))
|
||||
o:value("direct", "Direct")
|
||||
o:value("mixed", "Mixed")
|
||||
o:value("socks", "Socks")
|
||||
o:value("http", "HTTP")
|
||||
o:value("shadowsocks", "Shadowsocks")
|
||||
o:value("vmess", "Vmess")
|
||||
o:value("vless", "VLESS")
|
||||
o:value("trojan", "Trojan")
|
||||
if singbox_tags:find("with_naive_outbound") then
|
||||
o:value("naive", "Naive")
|
||||
end
|
||||
o:value("anytls", "AnyTLS")
|
||||
o:depends({ custom = false })
|
||||
|
||||
o = s:option(Value, "port", translate("Listen Port"))
|
||||
o.datatype = "port"
|
||||
o:depends({ custom = false })
|
||||
|
||||
o = s:option(MultiValue, "users", translate("User"))
|
||||
o.cast = "table"
|
||||
for i, v in ipairs(user_list) do
|
||||
o:value(v[".name"], v.username)
|
||||
end
|
||||
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 = "anytls" })
|
||||
|
||||
if singbox_tags:find("with_quic") then
|
||||
-- hysteria
|
||||
s.fields["protocol"]:value("hysteria", "Hysteria")
|
||||
s.fields["users"]:depends({ protocol = "hysteria" })
|
||||
|
||||
o = s:option(Value, "hysteria_obfs", translate("Obfs Password"))
|
||||
o:depends({ protocol = "hysteria" })
|
||||
|
||||
o = s:option(Value, "hysteria_up_mbps", translate("Max upload Mbps"))
|
||||
o.default = "100"
|
||||
o:depends({ protocol = "hysteria" })
|
||||
|
||||
o = s:option(Value, "hysteria_down_mbps", translate("Max download Mbps"))
|
||||
o.default = "100"
|
||||
o:depends({ protocol = "hysteria" })
|
||||
|
||||
o = s:option(Value, "hysteria_recv_window_conn", translate("QUIC stream receive window"))
|
||||
o:depends({ protocol = "hysteria" })
|
||||
|
||||
o = s:option(Value, "hysteria_recv_window_client", translate("QUIC connection receive window"))
|
||||
o:depends({ protocol = "hysteria" })
|
||||
|
||||
o = s:option(Value, "hysteria_max_conn_client", translate("QUIC concurrent bidirectional streams"))
|
||||
o.default = "1024"
|
||||
o:depends({ 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
|
||||
-- tuic
|
||||
s.fields["protocol"]:value("tuic", "TUIC")
|
||||
s.fields["users"]:depends({ protocol = "tuic" })
|
||||
|
||||
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({ protocol = "tuic" })
|
||||
|
||||
o = s:option(Flag, "tuic_zero_rtt_handshake", translate("Enable 0-RTT QUIC handshake"))
|
||||
o.default = 0
|
||||
o:depends({ protocol = "tuic" })
|
||||
|
||||
o = s:option(Value, "tuic_heartbeat", translate("Heartbeat interval(second)"))
|
||||
o.datatype = "uinteger"
|
||||
o.default = "3"
|
||||
o:depends({ protocol = "tuic" })
|
||||
|
||||
o = s:option(ListValue, "tuic_alpn", translate("QUIC TLS ALPN"))
|
||||
o.default = "default"
|
||||
o:value("default", translate("Default"))
|
||||
o:value("h3")
|
||||
o:value("h2")
|
||||
o:value("h3,h2")
|
||||
o:value("http/1.1")
|
||||
o:value("h2,http/1.1")
|
||||
o:value("h3,h2,http/1.1")
|
||||
o:value("spdy/3.1")
|
||||
o:value("h3,spdy/3.1")
|
||||
o:depends({ protocol = "tuic" })
|
||||
end
|
||||
|
||||
if singbox_tags:find("with_quic") then
|
||||
-- hysteria2
|
||||
s.fields["protocol"]:value("hysteria2", "Hysteria2")
|
||||
s.fields["users"]:depends({ protocol = "hysteria2" })
|
||||
|
||||
o = s:option(Flag, "hysteria2_realms", translate("Realms"))
|
||||
o.default = "0"
|
||||
o:depends({ protocol = "hysteria2"})
|
||||
|
||||
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)
|
||||
if realm then return value end
|
||||
return nil, translate("Invalid Realm URL.")
|
||||
end
|
||||
|
||||
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({ hysteria2_realms = "1" })
|
||||
|
||||
o = s:option(Flag, "hysteria2_realm_upnp", translate("Enable") .. " UPnP/NAT-PMP", translate("Enable UPnP/NAT-PMP port mapping on your gateway to improve hole punching success."))
|
||||
o.default = "0"
|
||||
o:depends({ hysteria2_realms = "1" })
|
||||
|
||||
o = s:option(ListValue, "hysteria2_obfs_type", translate("Obfs Type"))
|
||||
o:value("", translate("Disable"))
|
||||
o:value("salamander")
|
||||
o:value("gecko")
|
||||
o:depends({ protocol = "hysteria2" })
|
||||
|
||||
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, "hysteria2_obfs_MinPacketSize", translate("Gecko Packet Size (min)"))
|
||||
o.datatype = "uinteger"
|
||||
o.placeholder = "512"
|
||||
o.default = "512"
|
||||
o:depends({ hysteria2_obfs_type = "gecko" })
|
||||
|
||||
o = s:option(Value, "hysteria2_obfs_MaxPacketSize", translate("Gecko Packet Size (max)"))
|
||||
o.datatype = "uinteger"
|
||||
o.placeholder = "1200"
|
||||
o.default = "1200"
|
||||
o:depends({ hysteria2_obfs_type = "gecko" })
|
||||
|
||||
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({ protocol = "hysteria2" })
|
||||
|
||||
o = s:option(Value, "hysteria2_up_mbps", translate("Max upload Mbps"))
|
||||
o:depends({ protocol = "hysteria2", 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, "d_protocol", translate("Destination protocol"))
|
||||
o:value("tcp", "TCP")
|
||||
o:value("udp", "UDP")
|
||||
o:value("tcp,udp", "TCP,UDP")
|
||||
o:depends({ protocol = "direct" })
|
||||
|
||||
o = s:option(Value, "d_address", translate("Destination address"))
|
||||
o:depends({ protocol = "direct" })
|
||||
|
||||
o = s:option(Value, "d_port", translate("Destination port"))
|
||||
o.datatype = "port"
|
||||
o:depends({ protocol = "direct" })
|
||||
|
||||
o = s:option(Value, "decryption", translate("Encrypt Method"))
|
||||
o.default = "none"
|
||||
o:depends({ protocol = "vless" })
|
||||
|
||||
o = s:option(ListValue, "ss_method", translate("Encrypt Method"))
|
||||
o.rewrite_option = "method"
|
||||
for a, t in ipairs(ss_method_list) do o:value(t) end
|
||||
o:depends({ protocol = "shadowsocks" })
|
||||
|
||||
o = s:option(Value, "ss_password", translate("Password"))
|
||||
o:depends({ protocol = "shadowsocks" })
|
||||
|
||||
o = s:option(ListValue, "flow", translate("flow"))
|
||||
o.default = ""
|
||||
o:value("", translate("Disable"))
|
||||
o:value("xtls-rprx-vision")
|
||||
o:depends({ protocol = "vless" , tls = true })
|
||||
|
||||
o = s:option(Flag, "tls", translate("TLS"))
|
||||
o.default = 0
|
||||
o.validate = function(self, value, t)
|
||||
if value then
|
||||
local reality = s.fields["reality"] and s.fields["reality"]:formvalue(t) or nil
|
||||
local use_pem = s.fields["tls_use_pem"] and s.fields["tls_use_pem"]:formvalue(t) or nil
|
||||
if reality and reality == "1" then return value end
|
||||
if value == "1" and use_pem ~= "1" then
|
||||
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("Certificate and Private key path can not be empty!")
|
||||
end
|
||||
elseif value == "1" and use_pem == "1" then
|
||||
local ca = s.fields["tls_certificate"] and s.fields["tls_certificate"]:formvalue(t) or ""
|
||||
local key = s.fields["tls_key"] and s.fields["tls_key"]:formvalue(t) or ""
|
||||
if ca == "" or key == "" then
|
||||
return nil, translate("Certificate and Private key PEM can not be empty!")
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
end
|
||||
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, "reality", translate("REALITY"))
|
||||
o.default = 0
|
||||
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, "reality_private_key", translate("Private Key"))
|
||||
o:depends({ reality = true })
|
||||
|
||||
o = s:option(Value, "reality_shortId", translate("Short Id"))
|
||||
o:depends({ reality = true })
|
||||
|
||||
o = s:option(Value, "reality_handshake_server", translate("Handshake Server"))
|
||||
o.default = "google.com"
|
||||
o:depends({ reality = true })
|
||||
|
||||
o = s:option(Value, "reality_handshake_server_port", translate("Handshake Server Port"))
|
||||
o.datatype = "port"
|
||||
o.default = "443"
|
||||
o:depends({ reality = true })
|
||||
end
|
||||
|
||||
o = s:option(ListValue, "alpn", translate("ALPN"))
|
||||
o.default = "default"
|
||||
o:value("default", translate("Default"))
|
||||
o:value("h3")
|
||||
o:value("h2")
|
||||
o:value("h3,h2")
|
||||
o:value("http/1.1")
|
||||
o:value("h2,http/1.1")
|
||||
o:value("h3,h2,http/1.1")
|
||||
o:depends({ tls = true, reality = false })
|
||||
o:depends({ protocol = "hysteria" })
|
||||
|
||||
-- [[ TLS部分 ]] --
|
||||
|
||||
o = s:option(Flag, "tls_use_pem", translate("Use PEM"), translate("Use certificate and private key PEM content."))
|
||||
o:depends({ tls = true, reality = false })
|
||||
o:depends({ protocol = "naive" })
|
||||
o:depends({ protocol = "hysteria" })
|
||||
o:depends({ protocol = "tuic" })
|
||||
o:depends({ protocol = "hysteria2" })
|
||||
|
||||
o = s:option(FileUpload, "tls_certificateFile", translate("Path to the certificate file"), translate("as:") .. "/etc/ssl/fullchain.crt")
|
||||
o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. arg[1] .. ".crt"
|
||||
if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end
|
||||
o:depends({ tls = true, reality = false, tls_use_pem = false })
|
||||
o:depends({ protocol = "naive", tls_use_pem = false })
|
||||
o:depends({ protocol = "hysteria", tls_use_pem = false })
|
||||
o:depends({ protocol = "tuic", tls_use_pem = false })
|
||||
o:depends({ protocol = "hysteria2", tls_use_pem = false })
|
||||
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 = s:option(FileUpload, "tls_keyFile", translate("Path to the private key file"), 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({ tls = true, reality = false, tls_use_pem = false })
|
||||
o:depends({ protocol = "naive", tls_use_pem = false })
|
||||
o:depends({ protocol = "hysteria", tls_use_pem = false })
|
||||
o:depends({ protocol = "tuic", tls_use_pem = false })
|
||||
o:depends({ protocol = "hysteria2", tls_use_pem = false })
|
||||
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 = s:option(TextValue, "tls_certificate", "TLS Certificate (PEM)", translate("Full certificate (chain), PEM format."))
|
||||
o.default = ""
|
||||
o.rows = 5
|
||||
o.wrap = "off"
|
||||
o:depends({ tls_use_pem = true })
|
||||
o.cfgvalue = function(self, section)
|
||||
return (m:get(section, "tls_certificate") or ""):gsub("\\n", "\n")
|
||||
end
|
||||
o.validate = function(self, value)
|
||||
value = api.trim(value):gsub("\r\n", "\n"):gsub("\r", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
|
||||
return value:gsub("\n", "\\n")
|
||||
end
|
||||
|
||||
o = s:option(TextValue, "tls_key", "TLS Private Key (PEM)", translate("Private key in PEM format."))
|
||||
o.default = ""
|
||||
o.rows = 5
|
||||
o.wrap = "off"
|
||||
o:depends({ tls_use_pem = true })
|
||||
o.cfgvalue = function(self, section)
|
||||
return (m:get(section, "tls_key") or ""):gsub("\\n", "\n")
|
||||
end
|
||||
o.validate = function(self, value)
|
||||
value = api.trim(value):gsub("\r\n", "\n"):gsub("\r", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
|
||||
return value:gsub("\n", "\\n")
|
||||
end
|
||||
|
||||
o = s:option(Flag, "ech", translate("ECH"))
|
||||
o.default = "0"
|
||||
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, "ech_key", translate("ECH Key"))
|
||||
o.default = ""
|
||||
o.rows = 5
|
||||
o.wrap = "off"
|
||||
o:depends({ ech = true })
|
||||
o.cfgvalue = function(self, section)
|
||||
return (m:get(section, "ech_key") or ""):gsub("\\n", "\n")
|
||||
end
|
||||
o.validate = function(self, value)
|
||||
value = api.trim(value):gsub("\r\n", "\n"):gsub("\r", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
|
||||
return value:gsub("\n", "\\n")
|
||||
end
|
||||
|
||||
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({ protocol = "shadowsocks" })
|
||||
o:depends({ protocol = "vmess" })
|
||||
o:depends({ protocol = "vless" })
|
||||
o:depends({ protocol = "trojan" })
|
||||
|
||||
-- [[ HTTP部分 ]]--
|
||||
|
||||
o = s:option(DynamicList, "http_host", translate("HTTP Host"))
|
||||
o:depends({ transport = "http" })
|
||||
|
||||
o = s:option(Value, "http_path", translate("HTTP Path"))
|
||||
o:depends({ transport = "http" })
|
||||
|
||||
-- [[ WebSocket部分 ]]--
|
||||
|
||||
o = s:option(Value, "ws_host", translate("WebSocket Host"))
|
||||
o:depends({ transport = "ws" })
|
||||
|
||||
o = s:option(Value, "ws_path", translate("WebSocket Path"))
|
||||
o:depends({ transport = "ws" })
|
||||
|
||||
o = s:option(Value, "ws_earlyDataHeaderName", translate("Early data header name"), translate("Recommended value: Sec-WebSocket-Protocol"))
|
||||
o.placeholder = "Sec-WebSocket-Protocol"
|
||||
o:depends({ transport = "ws" })
|
||||
|
||||
-- [[ HTTPUpgrade部分 ]]--
|
||||
|
||||
o = s:option(Value, "httpupgrade_host", translate("HTTPUpgrade Host"))
|
||||
o:depends({ transport = "httpupgrade" })
|
||||
|
||||
o = s:option(Value, "httpupgrade_path", translate("HTTPUpgrade Path"))
|
||||
o:depends({ transport = "httpupgrade" })
|
||||
|
||||
-- [[ gRPC部分 ]]--
|
||||
o = s:option(Value, "grpc_serviceName", "ServiceName")
|
||||
o:depends({ transport = "grpc" })
|
||||
|
||||
-- [[ Mux ]]--
|
||||
o = s:option(Flag, "mux", translate("Mux"))
|
||||
o.rmempty = false
|
||||
o:depends({ protocol = "vmess" })
|
||||
o:depends({ protocol = "vless", flow = "" })
|
||||
o:depends({ protocol = "shadowsocks" })
|
||||
o:depends({ protocol = "trojan" })
|
||||
|
||||
-- [[ TCP Brutal ]]--
|
||||
o = s:option(Flag, "tcpbrutal", translate("TCP Brutal"))
|
||||
o.default = 0
|
||||
o:depends({ mux = true })
|
||||
|
||||
o = s:option(Value, "tcpbrutal_up_mbps", translate("Max upload Mbps"))
|
||||
o.default = "10"
|
||||
o:depends({ tcpbrutal = true })
|
||||
|
||||
o = s:option(Value, "tcpbrutal_down_mbps", translate("Max download Mbps"))
|
||||
o.default = "50"
|
||||
o:depends({ tcpbrutal = true })
|
||||
|
||||
if singbox_tags:find("with_wireguard") then
|
||||
-- wireguard
|
||||
s.fields["protocol"]:value("wireguard", "WireGuard")
|
||||
s.fields["users"]:depends({ protocol = "wireguard" })
|
||||
|
||||
o = s:option(Flag, "wireguard_system_interface", translate("System interface"))
|
||||
o.default = 0
|
||||
o:depends({ protocol = "wireguard" })
|
||||
|
||||
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.template = m:template_path("/server/gen_wireguard_key")
|
||||
o:depends({ protocol = "wireguard" })
|
||||
end
|
||||
|
||||
if version_ge_1_14_0 then
|
||||
-- snell
|
||||
s.fields["protocol"]:value("snell", "Snell")
|
||||
s.fields["users"]:depends({ protocol = "snell" })
|
||||
|
||||
o = s:option(ListValue, "snell_version", translate("Version"))
|
||||
o:value("5")
|
||||
o:value("6")
|
||||
o:depends({ protocol = "snell" })
|
||||
|
||||
o = s:option(Value, "snell_psk", translate("Pre shared key"))
|
||||
o.rmempty = false
|
||||
o:depends({ protocol = "snell" })
|
||||
|
||||
o = s:option(ListValue, "snell_obfs_mode", translate("Obfs"))
|
||||
o:value("none")
|
||||
o:value("http")
|
||||
o:depends({ protocol = "snell", snell_version = "5" })
|
||||
|
||||
o = s:option(ListValue, "snell_mode", translate("Mode"))
|
||||
o:value("default")
|
||||
o:value("unshaped")
|
||||
o:value("unsafe-raw")
|
||||
o:depends({ protocol = "snell", snell_version = "6" })
|
||||
end
|
||||
|
||||
o = s:option(Flag, "firewall_allow", translate("Firewall Allow"))
|
||||
o.default = "0"
|
||||
o:depends({ custom = false })
|
||||
|
||||
o = s:option(Value, "firewall_allow_src", translate("Source zone"))
|
||||
o.rmempty = not m.is_js_luci
|
||||
o.nocreate = true
|
||||
o.allowany = true
|
||||
o.default = "wan"
|
||||
o.template = "cbi/firewall_zonelist"
|
||||
o:depends({ custom = false, firewall_allow = true })
|
||||
|
||||
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
|
||||
if e.node_type == "normal" and e.type == type_name then
|
||||
nodes_table[#nodes_table + 1] = {
|
||||
id = e[".name"],
|
||||
remarks = e["remark"],
|
||||
group = e["group"]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
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"))
|
||||
o:value("_iface", translate("Custom Interface"))
|
||||
o.template = m:template_path("/cbi/nodes_listvalue")
|
||||
o.group = {"","","",""}
|
||||
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({ custom = false })
|
||||
|
||||
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, "outbound_node_port", translate("Port"))
|
||||
o.datatype = "port"
|
||||
o:depends({ outbound_node = "_socks" })
|
||||
o:depends({ 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, "outbound_node_password", translate("Password"))
|
||||
o.password = true
|
||||
o:depends({ outbound_node = "_socks" })
|
||||
o:depends({ outbound_node = "_http" })
|
||||
|
||||
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(Flag, "log", translate("Log"))
|
||||
o.default = "1"
|
||||
o.rmempty = false
|
||||
|
||||
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({ log = true })
|
||||
|
||||
api.type_cbi_section(s1, s)
|
||||
@@ -1,597 +0,0 @@
|
||||
if not api.finded_com("xray") then
|
||||
return
|
||||
end
|
||||
|
||||
-- [[ Xray ]]
|
||||
local m, s1 = ...
|
||||
local type_name = "Xray"
|
||||
|
||||
s1.fields["type"]:value(type_name, "Xray")
|
||||
if not s1.fields["type"].default then
|
||||
s1.fields["type"].default = type_name
|
||||
end
|
||||
|
||||
if not s1.val["type"] then
|
||||
s1.val["type"] = type_name
|
||||
end
|
||||
|
||||
if s1.val["type"] and s1.val["type"] ~= type_name then
|
||||
return
|
||||
end
|
||||
|
||||
local s = NamedSection(m, arg[1], "tmp_" .. s1.sectiontype)
|
||||
s.parent = s1
|
||||
s.type_name = type_name
|
||||
s.option_prefix = "xray_"
|
||||
api.set_type_cbi(s)
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
local header_type_list = {
|
||||
"none", "srtp", "utp", "wechat-video", "dtls", "wireguard", "dns"
|
||||
}
|
||||
|
||||
o = s:option(Flag, "custom", translate("Use Custom Config"))
|
||||
|
||||
o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)")
|
||||
o.rows = 10
|
||||
o.wrap = "off"
|
||||
o:depends({ custom = true })
|
||||
o.datatype = "json"
|
||||
local o_validate = o.validate
|
||||
o.validate = function(self, value)
|
||||
local v = o_validate(self, value)
|
||||
if v then return v end
|
||||
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
|
||||
end
|
||||
o.cfgvalue = function(self, section)
|
||||
local config_str = m:get(section, "config_str")
|
||||
if config_str then
|
||||
return api.base64Decode(config_str)
|
||||
end
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
m:set(section, "config_str", api.base64Encode(value) or "")
|
||||
end
|
||||
|
||||
o = s:option(ListValue, "protocol", translate("Protocol"))
|
||||
o:value("socks", "Socks")
|
||||
o:value("http", "HTTP")
|
||||
o:value("vmess", "Vmess")
|
||||
o:value("vless", "VLESS")
|
||||
o:value("shadowsocks", "Shadowsocks")
|
||||
o:value("trojan", "Trojan")
|
||||
o:value("hysteria2", "Hysteria2")
|
||||
o:value("wireguard", "WireGuard")
|
||||
o:value("tunnel", "Tunnel")
|
||||
o:depends({ custom = false })
|
||||
|
||||
o = s:option(Value, "port", translate("Listen Port"))
|
||||
o.datatype = "port"
|
||||
o:depends({ custom = false })
|
||||
|
||||
o = s:option(MultiValue, "users", translate("User"))
|
||||
o.cast = "table"
|
||||
for i, v in ipairs(user_list) do
|
||||
o:value(v[".name"], v.username)
|
||||
end
|
||||
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, "d_protocol", translate("Destination protocol"))
|
||||
o:value("tcp", "TCP")
|
||||
o:value("udp", "UDP")
|
||||
o:value("tcp,udp", "TCP,UDP")
|
||||
o:depends({ protocol = "tunnel" })
|
||||
|
||||
o = s:option(Value, "d_address", translate("Destination address"))
|
||||
o:depends({ protocol = "tunnel" })
|
||||
|
||||
o = s:option(Value, "d_port", translate("Destination port"))
|
||||
o.datatype = "port"
|
||||
o:depends({ protocol = "tunnel" })
|
||||
|
||||
o = s:option(Value, "decryption", translate("Encrypt Method") .. " (decryption)")
|
||||
o.default = "none"
|
||||
o.placeholder = "none"
|
||||
o:depends({ protocol = "vless" })
|
||||
o.validate = function(self, value)
|
||||
value = api.trim(value)
|
||||
return (value == "" and "none" or value)
|
||||
end
|
||||
|
||||
o = s:option(ListValue, "ss_method", translate("Encrypt Method"))
|
||||
o.rewrite_option = "method"
|
||||
for a, t in ipairs(ss_method_list) do o:value(t) end
|
||||
o:depends({ protocol = "shadowsocks" })
|
||||
|
||||
o = s:option(Value, "ss_password", translate("Password"))
|
||||
o:depends({ protocol = "shadowsocks" })
|
||||
|
||||
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({ protocol = "shadowsocks" })
|
||||
|
||||
o = s:option(Flag, "udp_forward", translate("UDP Forward"))
|
||||
o.default = "1"
|
||||
o.rmempty = false
|
||||
o:depends({ protocol = "socks" })
|
||||
|
||||
o = s:option(ListValue, "flow", translate("flow"))
|
||||
o.default = ""
|
||||
o:value("", translate("Disable"))
|
||||
o:value("xtls-rprx-vision")
|
||||
o:depends({ protocol = "vless" })
|
||||
|
||||
---- [[ hysteria2 ]]
|
||||
o = s:option(Flag, "hysteria2_realms", translate("Realms"))
|
||||
o.default = "0"
|
||||
o:depends({ protocol = "hysteria2"})
|
||||
|
||||
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)
|
||||
if realm then return value end
|
||||
return nil, translate("Invalid Realm URL.")
|
||||
end
|
||||
|
||||
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({ hysteria2_realms = "1" })
|
||||
|
||||
o = s:option(Flag, "hysteria2_realm_upnp", translate("Enable") .. " UPnP/NAT-PMP", translate("Enable UPnP/NAT-PMP port mapping on your gateway to improve hole punching success."))
|
||||
o.default = "0"
|
||||
o:depends({ hysteria2_realms = "1" })
|
||||
|
||||
o = s:option(ListValue, "hysteria2_obfs_type", translate("Obfs Type"))
|
||||
o:value("", translate("Disable"))
|
||||
o:value("salamander")
|
||||
o:value("gecko")
|
||||
o:depends({ protocol = "hysteria2" })
|
||||
|
||||
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, "hysteria2_obfs_MinPacketSize", translate("Gecko Packet Size (min)"))
|
||||
o.datatype = "uinteger"
|
||||
o.placeholder = "512"
|
||||
o.default = "512"
|
||||
o:depends({ hysteria2_obfs_type = "gecko" })
|
||||
|
||||
o = s:option(Value, "hysteria2_obfs_MaxPacketSize", translate("Gecko Packet Size (max)"))
|
||||
o.datatype = "uinteger"
|
||||
o.placeholder = "1200"
|
||||
o.default = "1200"
|
||||
o:depends({ hysteria2_obfs_type = "gecko" })
|
||||
|
||||
o = s:option(Flag, "hysteria2_ignore_client_bandwidth", translate("Client BBR Flow Control"))
|
||||
o.default = 0
|
||||
o:depends({ protocol = "hysteria2" })
|
||||
|
||||
o = s:option(Value, "hysteria2_up_mbps", translate("Max upload Mbps"))
|
||||
o:depends({ protocol = "hysteria2", 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, "tls", translate("TLS"))
|
||||
o.default = 0
|
||||
o.validate = function(self, value, t)
|
||||
if value then
|
||||
local reality = s.fields["reality"] and s.fields["reality"]:formvalue(t) or nil
|
||||
local use_pem = s.fields["tls_use_pem"] and s.fields["tls_use_pem"]:formvalue(t) or nil
|
||||
if reality and reality == "1" then return value end
|
||||
if value == "1" and use_pem ~= "1" then
|
||||
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("Certificate and Private key path can not be empty!")
|
||||
end
|
||||
elseif value == "1" and use_pem == "1" then
|
||||
local ca = s.fields["tls_certificate"] and s.fields["tls_certificate"]:formvalue(t) or ""
|
||||
local key = s.fields["tls_key"] and s.fields["tls_key"]:formvalue(t) or ""
|
||||
if ca == "" or key == "" then
|
||||
return nil, translate("Certificate and Private key PEM can not be empty!")
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
end
|
||||
o:depends({ protocol = "vmess" })
|
||||
o:depends({ protocol = "vless" })
|
||||
o:depends({ protocol = "http" })
|
||||
o:depends({ protocol = "shadowsocks" })
|
||||
o:depends({ protocol = "trojan" })
|
||||
|
||||
-- [[ REALITY部分 ]] --
|
||||
o = s:option(Flag, "reality", translate("REALITY"))
|
||||
o.default = 0
|
||||
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(Value, "reality_private_key", translate("Private Key"))
|
||||
o:depends({ reality = true })
|
||||
|
||||
o = s:option(DynamicList, "reality_shortId", translate("Short Id"))
|
||||
o:depends({ reality = true })
|
||||
|
||||
o = s:option(Value, "reality_dest", translate("Dest"))
|
||||
o.default = "google.com:443"
|
||||
o:depends({ reality = true })
|
||||
|
||||
o = s:option(DynamicList, "reality_serverNames", translate("serverNames"))
|
||||
o:depends({ reality = true })
|
||||
|
||||
o = s:option(ListValue, "alpn", translate("alpn"))
|
||||
o.default = "default"
|
||||
o:value("default", translate("Default"))
|
||||
o:value("h3")
|
||||
o:value("h2")
|
||||
o:value("h3,h2")
|
||||
o:value("http/1.1")
|
||||
o:value("h2,http/1.1")
|
||||
o:value("h3,h2,http/1.1")
|
||||
o:depends({ tls = true, reality = false })
|
||||
|
||||
o = s:option(Flag, "use_mldsa65Seed", translate("ML-DSA-65"))
|
||||
o.default = "0"
|
||||
o:depends({ reality = true })
|
||||
|
||||
o = s:option(TextValue, "reality_mldsa65Seed", "ML-DSA-65 " .. translate("Private Key"))
|
||||
o.default = ""
|
||||
o.rows = 5
|
||||
o.wrap = "soft"
|
||||
o:depends({ use_mldsa65Seed = true })
|
||||
o.validate = function(self, value)
|
||||
return api.trim(value:gsub("[\r\n]", ""))
|
||||
end
|
||||
|
||||
-- o = s:option(Value, "minversion", translate("minversion"))
|
||||
-- o.default = "1.3"
|
||||
-- o:value("1.3")
|
||||
--o:depends({ tls = true })
|
||||
|
||||
-- [[ TLS部分 ]] --
|
||||
|
||||
o = s:option(Flag, "tls_use_pem", translate("Use PEM"), translate("Use certificate and private key PEM content."))
|
||||
o:depends({ tls = true, reality = false })
|
||||
o:depends({ protocol = "hysteria2" })
|
||||
|
||||
o = s:option(FileUpload, "tls_certificateFile", translate("Path to the certificate file"), translate("as:") .. "/etc/ssl/fullchain.crt")
|
||||
o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. s.section .. ".crt"
|
||||
if o and o:formvalue(s.section) then o.default = o:formvalue(s.section) end
|
||||
o:depends({ tls = true, reality = false, tls_use_pem = false })
|
||||
o:depends({ protocol = "hysteria2", tls_use_pem = false })
|
||||
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 = s:option(FileUpload, "tls_keyFile", translate("Path to the private key file"), translate("as:") .. "/etc/ssl/private.key")
|
||||
o.default = m:get(s.section, "tls_keyFile") or "/etc/config/ssl/" .. s.section .. ".key"
|
||||
if o and o:formvalue(s.section) then o.default = o:formvalue(s.section) end
|
||||
o:depends({ tls = true, reality = false, tls_use_pem = false })
|
||||
o:depends({ protocol = "hysteria2", tls_use_pem = false })
|
||||
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 = s:option(TextValue, "tls_certificate", "TLS Certificate (PEM)", translate("Full certificate (chain), PEM format."))
|
||||
o.default = ""
|
||||
o.rows = 5
|
||||
o.wrap = "off"
|
||||
o:depends({ tls_use_pem = true })
|
||||
o.cfgvalue = function(self, section)
|
||||
return (m:get(section, "tls_certificate") or ""):gsub("\\n", "\n")
|
||||
end
|
||||
o.validate = function(self, value)
|
||||
value = api.trim(value):gsub("\r\n", "\n"):gsub("\r", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
|
||||
return value:gsub("\n", "\\n")
|
||||
end
|
||||
|
||||
o = s:option(TextValue, "tls_key", "TLS Private Key (PEM)", translate("Private key in PEM format."))
|
||||
o.default = ""
|
||||
o.rows = 5
|
||||
o.wrap = "off"
|
||||
o:depends({ tls_use_pem = true })
|
||||
o.cfgvalue = function(self, section)
|
||||
return (m:get(section, "tls_key") or ""):gsub("\\n", "\n")
|
||||
end
|
||||
o.validate = function(self, value)
|
||||
value = api.trim(value):gsub("\r\n", "\n"):gsub("\r", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
|
||||
return value:gsub("\n", "\\n")
|
||||
end
|
||||
|
||||
o = s:option(Flag, "ech", translate("ECH"))
|
||||
o.default = "0"
|
||||
o:depends({ tls = true, reality = false })
|
||||
|
||||
o = s:option(TextValue, "ech_key", translate("ECH Key"))
|
||||
o.default = ""
|
||||
o.rows = 5
|
||||
o.wrap = "soft"
|
||||
o:depends({ ech = true })
|
||||
o.validate = function(self, value)
|
||||
return api.trim(value:gsub("[\r\n]", ""))
|
||||
end
|
||||
|
||||
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({ protocol = "vmess" })
|
||||
o:depends({ protocol = "vless" })
|
||||
o:depends({ protocol = "shadowsocks" })
|
||||
o:depends({ protocol = "trojan" })
|
||||
|
||||
-- [[ WebSocket部分 ]]--
|
||||
|
||||
o = s:option(Value, "ws_host", translate("WebSocket Host"))
|
||||
o:depends({ transport = "ws" })
|
||||
|
||||
o = s:option(Value, "ws_path", translate("WebSocket Path"))
|
||||
o:depends({ transport = "ws" })
|
||||
|
||||
-- [[ HttpUpgrade部分 ]]--
|
||||
o = s:option(Value, "httpupgrade_host", translate("HttpUpgrade Host"))
|
||||
o:depends({ transport = "httpupgrade" })
|
||||
|
||||
o = s:option(Value, "httpupgrade_path", translate("HttpUpgrade Path"))
|
||||
o.placeholder = "/"
|
||||
o:depends({ transport = "httpupgrade" })
|
||||
|
||||
-- [[ XHTTP部分 ]]--
|
||||
o = s:option(Value, "xhttp_host", translate("XHTTP Host"))
|
||||
o:depends({ transport = "xhttp" })
|
||||
|
||||
o = s:option(Value, "xhttp_path", translate("XHTTP Path"))
|
||||
o.placeholder = "/"
|
||||
o:depends({ transport = "xhttp" })
|
||||
|
||||
o = s:option(Value, "xhttp_maxuploadsize", translate("maxUploadSize"))
|
||||
o.default = "1000000"
|
||||
o:depends({ transport = "xhttp" })
|
||||
|
||||
o = s:option(Value, "xhttp_maxconcurrentuploads", translate("maxConcurrentUploads"))
|
||||
o.default = "10"
|
||||
o:depends({ transport = "xhttp" })
|
||||
|
||||
-- [[ TCP部分 ]]--
|
||||
|
||||
-- TCP伪装
|
||||
o = s:option(ListValue, "tcp_guise", translate("Camouflage Type"))
|
||||
o:value("none", "none")
|
||||
o:value("http", "http")
|
||||
o:depends({ transport = "raw" })
|
||||
|
||||
-- HTTP域名
|
||||
o = s:option(DynamicList, "tcp_guise_http_host", translate("HTTP Host"))
|
||||
o:depends({ tcp_guise = "http" })
|
||||
|
||||
-- HTTP路径
|
||||
o = s:option(DynamicList, "tcp_guise_http_path", translate("HTTP Path"))
|
||||
o:depends({ tcp_guise = "http" })
|
||||
|
||||
-- [[ mKCP部分 ]]--
|
||||
|
||||
o = s:option(ListValue, "mkcp_guise", translate("Camouflage Type"), translate('<br />none: default, no masquerade, data sent is packets with no characteristics.<br />srtp: disguised as an SRTP packet, it will be recognized as video call data (such as FaceTime).<br />utp: packets disguised as uTP will be recognized as bittorrent downloaded data.<br />wechat-video: packets disguised as WeChat video calls.<br />dtls: disguised as DTLS 1.2 packet.<br />wireguard: disguised as a WireGuard packet. (not really WireGuard protocol)<br />dns: Disguising traffic as DNS requests.'))
|
||||
for a, t in ipairs(header_type_list) do o:value(t) end
|
||||
o:depends({ transport = "mkcp" })
|
||||
|
||||
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, "mkcp_mtu", translate("KCP MTU"))
|
||||
o.datatype = "uinteger"
|
||||
o.default = 1350
|
||||
o:depends({ transport = "mkcp" })
|
||||
|
||||
o = s:option(Value, "mkcp_seed", translate("KCP Seed"))
|
||||
o:depends({ transport = "mkcp" })
|
||||
|
||||
-- [[ gRPC部分 ]]--
|
||||
o = s:option(Value, "grpc_serviceName", "ServiceName")
|
||||
o:depends({ transport = "grpc" })
|
||||
|
||||
--[[FinalMask]]
|
||||
o = s:option(Flag, "use_finalmask", "FinalMask")
|
||||
o.default = "0"
|
||||
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, "finalmask", "FinalMask JSON")
|
||||
o:depends({ use_finalmask = true })
|
||||
o.rows = 10
|
||||
o.wrap = "off"
|
||||
o.datatype = "json"
|
||||
o.cfgvalue = function(self, section, value)
|
||||
local raw = m:get(section, "finalmask")
|
||||
if raw then
|
||||
return api.base64Decode(raw)
|
||||
end
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
m:set(section, "finalmask", api.base64Encode(value) or "")
|
||||
end
|
||||
|
||||
--[[acceptProxyProtocol]]
|
||||
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({ transport = "raw" })
|
||||
o:depends({ transport = "ws" })
|
||||
|
||||
--[[Fast Open]]
|
||||
o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open"))
|
||||
o.default = "0"
|
||||
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, "fallback", translate("Fallback"))
|
||||
o:depends({ protocol = "vless", transport = "raw" })
|
||||
o:depends({ protocol = "trojan", transport = "raw" })
|
||||
|
||||
--[[
|
||||
o = s:option(Value, "fallback_alpn", "Fallback alpn")
|
||||
o:depends({ fallback = true })
|
||||
|
||||
o = s:option(Value, "fallback_path", "Fallback path")
|
||||
o:depends({ fallback = true })
|
||||
|
||||
o = s:option(Value, "fallback_dest", "Fallback dest")
|
||||
o:depends({ fallback = true })
|
||||
|
||||
o = s:option(Value, "fallback_xver", "Fallback xver")
|
||||
o.default = 0
|
||||
o:depends({ fallback = true })
|
||||
]]--
|
||||
|
||||
o = s:option(DynamicList, "fallback_list", "Fallback", translate("format: dest,path,xver"))
|
||||
o:depends({ fallback = true })
|
||||
|
||||
-- Not supported at present
|
||||
--[[
|
||||
o = s:option(Flag, "wireguard_system_interface", translate("System interface"))
|
||||
o.default = 0
|
||||
o:depends({ protocol = "wireguard" })
|
||||
]]--
|
||||
|
||||
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.template = m:template_path("/server/gen_wireguard_key")
|
||||
o:depends({ protocol = "wireguard" })
|
||||
|
||||
o = s:option(Flag, "firewall_allow", translate("Firewall Allow"))
|
||||
o.default = "0"
|
||||
o:depends({ custom = false })
|
||||
|
||||
o = s:option(Value, "firewall_allow_src", translate("Source zone"))
|
||||
o.rmempty = not m.is_js_luci
|
||||
o.nocreate = true
|
||||
o.allowany = true
|
||||
o.default = "wan"
|
||||
o.template = "cbi/firewall_zonelist"
|
||||
o:depends({ custom = false, firewall_allow = true })
|
||||
|
||||
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
|
||||
if e.node_type == "normal" and e.type == type_name then
|
||||
nodes_table[#nodes_table + 1] = {
|
||||
id = e[".name"],
|
||||
remarks = e["remark"],
|
||||
group = e["group"]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
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"))
|
||||
o:value("_iface", translate("Custom Interface"))
|
||||
o.template = m:template_path("/cbi/nodes_listvalue")
|
||||
o.group = {"","","",""}
|
||||
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({ custom = false })
|
||||
|
||||
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, "outbound_node_port", translate("Port"))
|
||||
o.datatype = "port"
|
||||
o:depends({ outbound_node = "_socks"})
|
||||
o:depends({ 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, "outbound_node_password", translate("Password"))
|
||||
o.password = true
|
||||
o:depends({ outbound_node = "_socks"})
|
||||
o:depends({ outbound_node = "_http"})
|
||||
|
||||
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(Flag, "log", translate("Log"))
|
||||
o.default = "1"
|
||||
o.rmempty = false
|
||||
|
||||
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({ log = true })
|
||||
|
||||
api.type_cbi_section(s1, s)
|
||||
@@ -1,93 +0,0 @@
|
||||
if not api.is_finded("ssserver") then
|
||||
return
|
||||
end
|
||||
|
||||
-- [[ Shadowsocks Rust ]]
|
||||
local m, s1 = ...
|
||||
local type_name = "SS-Rust"
|
||||
|
||||
s1.fields["type"]:value(type_name, translate("Shadowsocks Rust"))
|
||||
|
||||
if not s1.val["type"] then
|
||||
s1.val["type"] = type_name
|
||||
end
|
||||
|
||||
if s1.val["type"] and s1.val["type"] ~= type_name then
|
||||
return
|
||||
end
|
||||
|
||||
local s = NamedSection(m, arg[1], "tmp_" .. s1.sectiontype)
|
||||
s.parent = s1
|
||||
s.type_name = type_name
|
||||
s.option_prefix = "ssrust_"
|
||||
api.set_type_cbi(s)
|
||||
|
||||
local ssrust_encrypt_method_list = {
|
||||
"plain", "none",
|
||||
"aes-128-gcm", "aes-256-gcm", "chacha20-ietf-poly1305",
|
||||
"2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"
|
||||
}
|
||||
|
||||
o = s:option(Flag, "custom", translate("Use Custom Config"))
|
||||
|
||||
o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)")
|
||||
o.rows = 10
|
||||
o.wrap = "off"
|
||||
o:depends({ custom = true })
|
||||
o.datatype = "json"
|
||||
local o_validate = o.validate
|
||||
o.validate = function(self, value)
|
||||
local v = o_validate(self, value)
|
||||
if v then return v end
|
||||
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
|
||||
end
|
||||
o.cfgvalue = function(self, section)
|
||||
local config_str = m:get(section, "config_str")
|
||||
if config_str then
|
||||
return api.base64Decode(config_str)
|
||||
end
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
m:set(section, "config_str", api.base64Encode(value) or "")
|
||||
end
|
||||
|
||||
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, "firewall_allow", translate("Firewall Allow"))
|
||||
o.default = "0"
|
||||
o:depends({ custom = false })
|
||||
|
||||
o = s:option(Value, "firewall_allow_src", translate("Source zone"))
|
||||
o.rmempty = not m.is_js_luci
|
||||
o.nocreate = true
|
||||
o.allowany = true
|
||||
o.default = "wan"
|
||||
o.template = "cbi/firewall_zonelist"
|
||||
o:depends({ custom = false, firewall_allow = true })
|
||||
|
||||
o = s:option(Flag, "log", translate("Log"))
|
||||
o.default = "1"
|
||||
o.rmempty = false
|
||||
|
||||
api.type_cbi_section(s1, s)
|
||||
@@ -1,124 +0,0 @@
|
||||
if not api.is_finded("ssr-server") then
|
||||
return
|
||||
end
|
||||
|
||||
-- [[ ShadowsocksR ]]
|
||||
local m, s1 = ...
|
||||
local type_name = "SSR"
|
||||
|
||||
s1.fields["type"]:value(type_name, translate("ShadowsocksR"))
|
||||
|
||||
if not s1.val["type"] then
|
||||
s1.val["type"] = type_name
|
||||
end
|
||||
|
||||
if s1.val["type"] and s1.val["type"] ~= type_name then
|
||||
return
|
||||
end
|
||||
|
||||
local s = NamedSection(m, arg[1], "tmp_" .. s1.sectiontype)
|
||||
s.parent = s1
|
||||
s.type_name = type_name
|
||||
s.option_prefix = "ssr_"
|
||||
api.set_type_cbi(s)
|
||||
|
||||
local ssr_encrypt_method_list = {
|
||||
"none", "table", "rc2-cfb", "rc4", "rc4-md5", "rc4-md5-6", "aes-128-cfb",
|
||||
"aes-192-cfb", "aes-256-cfb", "aes-128-ctr", "aes-192-ctr", "aes-256-ctr",
|
||||
"bf-cfb", "camellia-128-cfb", "camellia-192-cfb", "camellia-256-cfb",
|
||||
"cast5-cfb", "des-cfb", "idea-cfb", "seed-cfb", "salsa20", "chacha20",
|
||||
"chacha20-ietf"
|
||||
}
|
||||
|
||||
local ssr_protocol_list = {
|
||||
"origin", "verify_simple", "verify_deflate", "verify_sha1", "auth_simple",
|
||||
"auth_sha1", "auth_sha1_v2", "auth_sha1_v4", "auth_aes128_md5",
|
||||
"auth_aes128_sha1", "auth_chain_a", "auth_chain_b", "auth_chain_c",
|
||||
"auth_chain_d", "auth_chain_e", "auth_chain_f"
|
||||
}
|
||||
local ssr_obfs_list = {
|
||||
"plain", "http_simple", "http_post", "random_head", "tls_simple",
|
||||
"tls1.0_session_auth", "tls1.2_ticket_auth"
|
||||
}
|
||||
|
||||
o = s:option(Flag, "custom", translate("Use Custom Config"))
|
||||
|
||||
o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)")
|
||||
o.rows = 10
|
||||
o.wrap = "off"
|
||||
o:depends({ custom = true })
|
||||
o.datatype = "json"
|
||||
local o_validate = o.validate
|
||||
o.validate = function(self, value)
|
||||
local v = o_validate(self, value)
|
||||
if v then return v end
|
||||
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
|
||||
end
|
||||
o.cfgvalue = function(self, section)
|
||||
local config_str = m:get(section, "config_str")
|
||||
if config_str then
|
||||
return api.base64Decode(config_str)
|
||||
end
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
m:set(section, "config_str", api.base64Encode(value) or "")
|
||||
end
|
||||
|
||||
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, "firewall_allow", translate("Firewall Allow"))
|
||||
o.default = "0"
|
||||
o:depends({ custom = false })
|
||||
|
||||
o = s:option(Value, "firewall_allow_src", translate("Source zone"))
|
||||
o.rmempty = not m.is_js_luci
|
||||
o.nocreate = true
|
||||
o.allowany = true
|
||||
o.default = "wan"
|
||||
o.template = "cbi/firewall_zonelist"
|
||||
o:depends({ custom = false, firewall_allow = true })
|
||||
|
||||
o = s:option(Flag, "log", translate("Log"))
|
||||
o.default = "1"
|
||||
o.rmempty = false
|
||||
|
||||
api.type_cbi_section(s1, s)
|
||||
@@ -1,183 +0,0 @@
|
||||
if not api.finded_com("hysteria") then
|
||||
return
|
||||
end
|
||||
|
||||
-- [[ Hysteria2 ]]
|
||||
local m, s1 = ...
|
||||
local type_name = "Hysteria2"
|
||||
|
||||
s1.fields["type"]:value(type_name, "Hysteria2")
|
||||
|
||||
if s1.val["type"] and s1.val["type"] ~= type_name then
|
||||
return
|
||||
end
|
||||
|
||||
local s = NamedSection(m, arg[1], "tmp_" .. s1.sectiontype)
|
||||
s.parent = s1
|
||||
s.type_name = type_name
|
||||
s.option_prefix = "hysteria2_"
|
||||
api.set_type_cbi(s)
|
||||
|
||||
o = s:option(Flag, "custom", translate("Use Custom Config"))
|
||||
|
||||
o = s:option(TextValue, "custom_config", translate("Custom Config"))
|
||||
o.rows = 10
|
||||
o.wrap = "off"
|
||||
o:depends({ custom = true })
|
||||
o.datatype = "json"
|
||||
local o_validate = o.validate
|
||||
o.validate = function(self, value)
|
||||
local v = o_validate(self, value)
|
||||
if v then return v end
|
||||
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
|
||||
end
|
||||
o.cfgvalue = function(self, section)
|
||||
local config_str = m:get(section, "config_str")
|
||||
if config_str then
|
||||
return api.base64Decode(config_str)
|
||||
end
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
m:set(section, "config_str", api.base64Encode(value) or "")
|
||||
end
|
||||
|
||||
o = s:option(Value, "port", translate("Listen Port"))
|
||||
o.datatype = "port"
|
||||
o:depends({ custom = false })
|
||||
|
||||
o = s:option(MultiValue, "users", translate("User"))
|
||||
o.cast = "table"
|
||||
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.rewrite_option = o.option
|
||||
o:depends({ custom = false })
|
||||
|
||||
o = s:option(Value, "realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name")
|
||||
o.rewrite_option = o.option
|
||||
o:depends({ 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, "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({ realms = "1" })
|
||||
|
||||
o = s:option(Flag, "realm_upnp", translate("Enable") .. " UPnP/NAT-PMP", translate("Enable UPnP/NAT-PMP port mapping on your gateway to improve hole punching success."))
|
||||
o.default = "0"
|
||||
o.rewrite_option = o.option
|
||||
o:depends({ realms = "1" })
|
||||
|
||||
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:depends({ custom = false })
|
||||
|
||||
o = s:option(Value, "obfs_password", translate("Obfs Password"))
|
||||
o.rewrite_option = o.option
|
||||
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.rewrite_option = o.option
|
||||
|
||||
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.rewrite_option = o.option
|
||||
|
||||
o = s:option(Flag, "udp", translate("UDP"))
|
||||
o.default = "1"
|
||||
o.rewrite_option = o.option
|
||||
o:depends({ custom = false })
|
||||
|
||||
o = s:option(Value, "up_mbps", translate("Max upload Mbps"))
|
||||
o.rewrite_option = o.option
|
||||
o:depends({ custom = false })
|
||||
|
||||
o = s:option(Value, "down_mbps", translate("Max download Mbps"))
|
||||
o.rewrite_option = o.option
|
||||
o:depends({ custom = false })
|
||||
|
||||
o = s:option(Flag, "ignoreClientBandwidth", translate("ignoreClientBandwidth"))
|
||||
o.default = "0"
|
||||
o.rewrite_option = o.option
|
||||
o:depends({ custom = false })
|
||||
|
||||
o = s:option(FileUpload, "tls_certificateFile", translate("Path to the certificate file"), translate("as:") .. "/etc/ssl/fullchain.crt")
|
||||
o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. arg[1] .. ".crt"
|
||||
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("Path to the private key file"), 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(FileUpload, "ech_keyFile", translate("Path to the ECH key file"), translate("as:") .. "/etc/ssl/ech.pem")
|
||||
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, "firewall_allow", translate("Firewall Allow"))
|
||||
o.default = "0"
|
||||
o:depends({ custom = false })
|
||||
|
||||
o = s:option(Value, "firewall_allow_src", translate("Source zone"))
|
||||
o.rmempty = not m.is_js_luci
|
||||
o.nocreate = true
|
||||
o.allowany = true
|
||||
o.default = "wan"
|
||||
o.template = "cbi/firewall_zonelist"
|
||||
o:depends({ custom = false, firewall_allow = true })
|
||||
|
||||
o = s:option(Flag, "log", translate("Log"))
|
||||
o.default = "1"
|
||||
o.rmempty = false
|
||||
|
||||
api.type_cbi_section(s1, s)
|
||||
@@ -1,54 +0,0 @@
|
||||
api = require "luci.passwall.api"
|
||||
api.set_default_cbi()
|
||||
|
||||
m = Map(api.s_config)
|
||||
m.redirect = api.url("server")
|
||||
|
||||
if not arg[1] or not m:get(arg[1]) then
|
||||
luci.http.redirect(m.redirect)
|
||||
end
|
||||
|
||||
s = m:section(NamedSection, arg[1], "user", translate("User Config"))
|
||||
s.addremove = false
|
||||
s.dynamic = false
|
||||
|
||||
o = s:option(Value, "username", translate("Username"))
|
||||
o.datatype = "and(uciname,maxlength(24))"
|
||||
o.rmempty = false
|
||||
function o.validate(self, value, section)
|
||||
local exists = false
|
||||
m.uci:foreach("passwall_server", "user", function(s)
|
||||
if s[".name"] ~= section and s.username == value then
|
||||
exists = true
|
||||
return false
|
||||
end
|
||||
end)
|
||||
if exists then
|
||||
return nil, translate("This username already exists.")
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
o = s:option(Value, "password", translate("Password"))
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "uuid", "UUID")
|
||||
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/gen_wireguard_key")
|
||||
|
||||
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)
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,118 +0,0 @@
|
||||
local _M = {}
|
||||
|
||||
local function gh_release_url(self)
|
||||
--return "https://api.github.com/repos/" .. self.repo .. "/releases/latest"
|
||||
return "https://github.com/Openwrt-Passwall/openwrt-passwall-packages/releases/download/api-cache/" .. string.lower(self.name) .. "-release-api.json"
|
||||
end
|
||||
|
||||
local function gh_pre_release_url(self)
|
||||
--return "https://api.github.com/repos/" .. self.repo .. "/releases?per_page=1"
|
||||
return "https://github.com/Openwrt-Passwall/openwrt-passwall-packages/releases/download/api-cache/" .. string.lower(self.name) .. "-pre-release-api.json"
|
||||
end
|
||||
|
||||
-- 排序顺序定义
|
||||
_M.order = {
|
||||
"geoview",
|
||||
"chinadns-ng",
|
||||
"xray",
|
||||
"sing-box",
|
||||
"hysteria"
|
||||
}
|
||||
|
||||
_M.hysteria = {
|
||||
name = "Hysteria",
|
||||
repo = "HyNetwork/hysteria",
|
||||
get_url = gh_release_url,
|
||||
cmd_version = "version | awk '/^Version:/ {print $2}'",
|
||||
remote_version_str_replace = "app/",
|
||||
zipped = false,
|
||||
default_path = "/usr/bin/hysteria",
|
||||
match_fmt_str = "linux%%-%s$",
|
||||
file_tree = {
|
||||
armv6 = "arm",
|
||||
armv7 = "arm",
|
||||
mipsel = "mipsle"
|
||||
}
|
||||
}
|
||||
|
||||
_M["sing-box"] = {
|
||||
name = "Sing-Box",
|
||||
repo = "SagerNet/sing-box",
|
||||
get_url = gh_release_url,
|
||||
cmd_version = "version | awk '{print $3}' | sed -n 1P",
|
||||
zipped = true,
|
||||
zipped_suffix = "tar.gz",
|
||||
default_path = "/usr/bin/sing-box",
|
||||
match_fmt_str = "linux%%-%s",
|
||||
file_tree = {
|
||||
x86_64 = "amd64%-musl",
|
||||
x86 = "386%-musl",
|
||||
aarch64 = "arm64%-musl",
|
||||
rockchip = "arm64%-musl",
|
||||
mips = "mips%-softfloat",
|
||||
mips64 = "mips64%-softfloat",
|
||||
mipsel = "mipsle%-softfloat%-musl",
|
||||
mips64el = "mips64le%-softfloat",
|
||||
armv7 = "armv7%-musl",
|
||||
armv8 = "arm64%-musl",
|
||||
riscv64 = "riscv64%-musl"
|
||||
}
|
||||
}
|
||||
|
||||
_M.xray = {
|
||||
name = "Xray",
|
||||
repo = "XTLS/Xray-core",
|
||||
get_url = gh_pre_release_url,
|
||||
cmd_version = "version | awk '{print $2}' | sed -n 1P",
|
||||
zipped = true,
|
||||
default_path = "/usr/bin/xray",
|
||||
match_fmt_str = "linux%%-%s",
|
||||
file_tree = {
|
||||
x86_64 = "64",
|
||||
x86 = "32",
|
||||
mips = "mips32",
|
||||
mipsel = "mips32le",
|
||||
mips64el = "mips64le"
|
||||
}
|
||||
}
|
||||
|
||||
_M["chinadns-ng"] = {
|
||||
name = "ChinaDNS-NG",
|
||||
repo = "zfl9/chinadns-ng",
|
||||
get_url = gh_release_url,
|
||||
cmd_version = "-V | awk '{print $2}'",
|
||||
zipped = false,
|
||||
default_path = "/usr/bin/chinadns-ng",
|
||||
match_fmt_str = "%s",
|
||||
file_tree = {
|
||||
x86_64 = "wolfssl@x86_64.*x86_64@",
|
||||
x86 = "wolfssl@i386.*i686",
|
||||
mips = "wolfssl@mips%-.*mips32%+soft_float@",
|
||||
mips64 = "wolfssl@mips64%-.*mips64%+soft_float@",
|
||||
mipsel = "wolfssl@mipsel.*mips32%+soft_float@",
|
||||
mips64el = "wolfssl@mips64el%-.*mips64%+soft_float@",
|
||||
aarch64 = "wolfssl_noasm@aarch64.*v8a",
|
||||
rockchip = "wolfssl@aarch64.*v8a",
|
||||
armv5 = "wolfssl@arm.*v5te",
|
||||
armv6 = "wolfssl@arm.*v6t2",
|
||||
armv7 = "wolfssl@arm.*eabihf.*v7a",
|
||||
armv8 = "wolfssl_noasm@aarch64.*v8a",
|
||||
riscv64 = "wolfssl@riscv64.*"
|
||||
}
|
||||
}
|
||||
|
||||
_M.geoview = {
|
||||
name = "Geoview",
|
||||
repo = "snowie2000/geoview",
|
||||
get_url = gh_release_url,
|
||||
cmd_version = '-version 2>/dev/null | awk \'NR==1 && $1=="Geoview" {print $2}\'',
|
||||
zipped = false,
|
||||
default_path = "/usr/bin/geoview",
|
||||
match_fmt_str = "linux%%-%s",
|
||||
file_tree = {
|
||||
mipsel = "mipsle",
|
||||
mips64el = "mips64le"
|
||||
}
|
||||
}
|
||||
|
||||
return _M
|
||||
@@ -1,203 +0,0 @@
|
||||
#!/usr/bin/lua
|
||||
|
||||
local action = arg[1]
|
||||
local api = require "luci.passwall.api"
|
||||
local sys = api.sys
|
||||
local uci = api.uci
|
||||
local jsonc = api.jsonc
|
||||
|
||||
local CONFIG = api.s_config
|
||||
local CONFIG_PATH = api.S_TMP_PATH
|
||||
local LOG_APP_FILE = "/tmp/log/" .. CONFIG .. ".log"
|
||||
local TMP_BIN_PATH = CONFIG_PATH .. "/bin"
|
||||
local require_dir = "luci.passwall."
|
||||
|
||||
local function log(...)
|
||||
local f, err = io.open(LOG_APP_FILE, "a")
|
||||
if f and err == nil then
|
||||
local str = os.date("%Y-%m-%d %H:%M:%S: ") .. table.concat({...}, " ")
|
||||
f:write(str .. "\n")
|
||||
f:close()
|
||||
end
|
||||
end
|
||||
|
||||
local function cmd(cmd)
|
||||
sys.call(cmd)
|
||||
end
|
||||
|
||||
local function ln_run(s, d, command, output)
|
||||
if not output then
|
||||
output = "/dev/null"
|
||||
end
|
||||
d = TMP_BIN_PATH .. "/" .. d
|
||||
cmd(string.format('[ ! -f "%s" ] && ln -s %s %s 2>/dev/null', d, s, d))
|
||||
return string.format("%s >%s 2>&1 &", d .. " " .. command, output)
|
||||
end
|
||||
|
||||
local function remove_firewall_rules(defer_commit)
|
||||
local to_delete = {}
|
||||
uci:foreach("firewall", "rule", function(rule)
|
||||
local name = rule[".name"]
|
||||
if name and name:find(CONFIG, 1, true) == 1 then
|
||||
table.insert(to_delete, name)
|
||||
end
|
||||
end)
|
||||
if #to_delete > 0 then
|
||||
for _, name in ipairs(to_delete) do
|
||||
uci:delete("firewall", name)
|
||||
end
|
||||
if not defer_commit then
|
||||
api.uci_save(uci, "firewall", true, true)
|
||||
end
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function start()
|
||||
local enabled = tonumber(api.uci_get_s("@global[0]", "enable") or 0)
|
||||
if enabled == nil or enabled == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
-- 启动前清理所有旧防火墙规则
|
||||
local fw_changed = remove_firewall_rules(true)
|
||||
|
||||
cmd(string.format("mkdir -p %s %s", CONFIG_PATH, TMP_BIN_PATH))
|
||||
cmd(string.format("touch %s", LOG_APP_FILE))
|
||||
local firewall_num = 0
|
||||
api.uci_foreach_s("server", function(server)
|
||||
local id = server[".name"]
|
||||
local enable = server.enable
|
||||
if enable and tonumber(enable) == 1 then
|
||||
local enable_log = server.log
|
||||
local log_path = nil
|
||||
if enable_log and enable_log == "1" then
|
||||
log_path = CONFIG_PATH .. "/" .. id .. ".log"
|
||||
else
|
||||
log_path = nil
|
||||
end
|
||||
local remarks = server.remarks
|
||||
local port = tonumber(server.port)
|
||||
local bin
|
||||
local config = {}
|
||||
local config_file = CONFIG_PATH .. "/" .. id .. ".json"
|
||||
local udp_forward = 1
|
||||
local type = server.type or ""
|
||||
if type == "Socks" then
|
||||
local auth = ""
|
||||
if server.auth and server.auth == "1" then
|
||||
local user = nil
|
||||
if server.user then
|
||||
user = api.uci_get_s(server.user)
|
||||
end
|
||||
local username = user and user.username or ""
|
||||
local password = user and user.password or ""
|
||||
if username ~= "" and password ~= "" then
|
||||
username = "-u " .. username
|
||||
password = "-P " .. password
|
||||
auth = username .. " " .. password
|
||||
end
|
||||
end
|
||||
bin = ln_run("/usr/bin/microsocks", "microsocks_" .. id, string.format("-i :: -p %s %s", port, auth), log_path)
|
||||
elseif type == "SSR" then
|
||||
if server.custom == "1" and server.config_str then
|
||||
config = jsonc.parse(api.base64Decode(server.config_str))
|
||||
else
|
||||
config = require(require_dir .. "util_shadowsocks").gen_config_server(server)
|
||||
end
|
||||
local udp_param = ""
|
||||
udp_forward = tonumber(server.udp_forward) or 1
|
||||
if udp_forward == 1 then
|
||||
udp_param = "-u"
|
||||
end
|
||||
type = type:lower()
|
||||
bin = ln_run("/usr/bin/" .. type .. "-server", type .. "-server", "-c " .. config_file .. " " .. udp_param, log_path)
|
||||
elseif type == "SS-Rust" then
|
||||
if server.custom == "1" and server.config_str then
|
||||
config = jsonc.parse(api.base64Decode(server.config_str))
|
||||
else
|
||||
config = require(require_dir .. "util_shadowsocks").gen_config_server(server)
|
||||
end
|
||||
bin = ln_run("/usr/bin/ssserver", "ssserver", "-c " .. config_file, log_path)
|
||||
elseif type == "Xray" then
|
||||
if server.custom == "1" and server.config_str then
|
||||
config = jsonc.parse(api.base64Decode(server.config_str))
|
||||
if log_path then
|
||||
if not config.log then
|
||||
config.log = {}
|
||||
end
|
||||
config.log.loglevel = server.loglevel
|
||||
end
|
||||
else
|
||||
config = require(require_dir .. "util_xray").gen_config_server(server)
|
||||
end
|
||||
bin = ln_run(api.get_app_path("xray"), "xray", "run -c " .. config_file, log_path)
|
||||
elseif type == "sing-box" then
|
||||
if server.custom == "1" and server.config_str then
|
||||
config = jsonc.parse(api.base64Decode(server.config_str))
|
||||
if log_path then
|
||||
if not config.log then
|
||||
config.log = {}
|
||||
end
|
||||
config.log.timestamp = true
|
||||
config.log.disabled = false
|
||||
config.log.level = server.loglevel
|
||||
config.log.output = log_path
|
||||
end
|
||||
else
|
||||
config = require(require_dir .. "util_sing-box").gen_config_server(server)
|
||||
end
|
||||
bin = ln_run(api.get_app_path("sing-box"), "sing-box", "run -c " .. config_file, log_path)
|
||||
elseif type == "Hysteria2" then
|
||||
if server.custom == "1" and server.config_str then
|
||||
config = jsonc.parse(api.base64Decode(server.config_str))
|
||||
else
|
||||
config = require(require_dir .. "util_hysteria2").gen_config_server(server)
|
||||
end
|
||||
bin = ln_run(api.get_app_path("hysteria"), "hysteria", "-c " .. config_file .. " server", log_path)
|
||||
end
|
||||
|
||||
if next(config) then
|
||||
local f, err = io.open(config_file, "w")
|
||||
if f and err == nil then
|
||||
f:write(jsonc.stringify(config, 1))
|
||||
f:close()
|
||||
end
|
||||
log(string.format("%s 生成配置文件并运行 - %s", remarks, config_file))
|
||||
end
|
||||
|
||||
if bin then
|
||||
cmd(bin)
|
||||
end
|
||||
|
||||
local firewall_allow = server.firewall_allow
|
||||
if firewall_allow == "1" then
|
||||
firewall_num = firewall_num + 1
|
||||
local uid = CONFIG .. "_" .. id
|
||||
uci:section("firewall", "rule", uid)
|
||||
uci:set("firewall", uid, "name", uid)
|
||||
uci:set("firewall", uid, "src", server.firewall_allow_src or "wan")
|
||||
uci:set("firewall", uid, "dest_port", port)
|
||||
uci:set("firewall", uid, "target", "ACCEPT")
|
||||
end
|
||||
end
|
||||
end)
|
||||
if firewall_num > 0 or fw_changed then
|
||||
api.uci_save(uci, "firewall", true, true)
|
||||
end
|
||||
end
|
||||
|
||||
local function stop()
|
||||
cmd(string.format("/bin/busybox top -bn1 | grep -v 'grep' | grep '%s/' | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1", CONFIG_PATH))
|
||||
remove_firewall_rules()
|
||||
cmd(string.format("rm -rf %s %s", CONFIG_PATH, LOG_APP_FILE))
|
||||
end
|
||||
|
||||
if action then
|
||||
if action == "start" then
|
||||
start()
|
||||
elseif action == "stop" then
|
||||
stop()
|
||||
end
|
||||
end
|
||||
@@ -1,212 +0,0 @@
|
||||
module("luci.passwall.util_hysteria2", package.seeall)
|
||||
local api = require "luci.passwall.api"
|
||||
local jsonc = api.jsonc
|
||||
|
||||
function gen_config_server(node)
|
||||
local users = nil
|
||||
if node.users and #node.users > 0 then
|
||||
users = {}
|
||||
for i, v in ipairs(node.users) do
|
||||
local user = api.uci_get_s(v) or {}
|
||||
if user[".type"] == "user" then
|
||||
users[user.username] = user.password
|
||||
end
|
||||
end
|
||||
if not next(users) then
|
||||
users = nil
|
||||
end
|
||||
end
|
||||
local config = {
|
||||
listen = (function()
|
||||
if node.hysteria2_realms and node.hysteria2_realm_url then
|
||||
local url = node.hysteria2_realm_url:gsub("/+$", "")
|
||||
if node.port then
|
||||
url = url .. (url:find("?") and "&lport=" or "?lport=") .. node.port
|
||||
end
|
||||
return url
|
||||
end
|
||||
return ":" .. (node.port or "0")
|
||||
end)(),
|
||||
tls = {
|
||||
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
|
||||
}
|
||||
} 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
|
||||
} 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,
|
||||
portMapping = (node.hysteria2_realm_upnp == "1") and { enabled = true } or nil
|
||||
} or nil,
|
||||
ech = (node.ech_keyFile and node.ech_keyFile ~= "") and { keyPath = node.ech_keyFile } 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
|
||||
if min <= 0 or min > max or max > 2048 then
|
||||
min = 512
|
||||
max = 1200
|
||||
end
|
||||
config.obfs.gecko.minPacketSize = min
|
||||
config.obfs.gecko.maxPacketSize = max
|
||||
end
|
||||
|
||||
return config
|
||||
end
|
||||
|
||||
function gen_config(var)
|
||||
local node_id = var["node"]
|
||||
if not node_id then
|
||||
print("node 不能为空")
|
||||
return
|
||||
end
|
||||
local node = api.uci_get_c(node_id)
|
||||
local local_redir_port = var["local_redir_port"]
|
||||
local local_socks_address = var["local_socks_address"] or "0.0.0.0"
|
||||
local local_socks_port = var["local_socks_port"]
|
||||
local local_socks_username = var["local_socks_username"]
|
||||
local local_socks_password = var["local_socks_password"]
|
||||
local local_http_address = var["local_http_address"] or "0.0.0.0"
|
||||
local local_http_port = var["local_http_port"]
|
||||
local local_http_username = var["local_http_username"]
|
||||
local local_http_password = var["local_http_password"]
|
||||
local tcp_proxy_way = var["tcp_proxy_way"]
|
||||
local server_host = var["server_host"] or (node.address or ""):lower()
|
||||
local server_port = var["server_port"] or node.port
|
||||
|
||||
if api.is_ipv6(server_host) then
|
||||
server_host = api.get_ipv6_full(server_host)
|
||||
end
|
||||
|
||||
local port_hop = ((server_port or "") .. "," .. (node.hysteria2_hop or "")):gsub("^[%s,]+", ""):gsub("[%s,]+$", ""):gsub(":", "-")
|
||||
local server = server_host .. ":" .. (port_hop ~= "" and port_hop or "443")
|
||||
|
||||
local config = {
|
||||
server = (function()
|
||||
if node.hysteria2_realms and node.hysteria2_realm_url then
|
||||
return node.hysteria2_realm_url:gsub("/+$", "")
|
||||
end
|
||||
return server
|
||||
end)(),
|
||||
realm = (node.hysteria2_realms and node.hysteria2_realm_stun) and {
|
||||
stunServers = node.hysteria2_realm_stun,
|
||||
portMapping = (node.hysteria2_realm_upnp == "1") and { enabled = true } or nil
|
||||
} or nil,
|
||||
transport = {
|
||||
type = "udp",
|
||||
udp = node.hysteria2_hop and (function()
|
||||
local udp = {}
|
||||
local t = node.hysteria2_hop_interval
|
||||
if not t then return nil end
|
||||
if t:find("-", 1, true) then
|
||||
local min, max = t:match("^(%d+)%-(%d+)$")
|
||||
min = tonumber(min)
|
||||
max = tonumber(max)
|
||||
if min and max then
|
||||
min = (min >= 5) and min or 5
|
||||
max = (max >= min) and max or min
|
||||
udp.minHopInterval = min .. "s"
|
||||
udp.maxHopInterval = max .. "s"
|
||||
return udp
|
||||
end
|
||||
end
|
||||
t = tonumber((t or "30"):match("^%d+"))
|
||||
t = (t and t >= 5) and t or 30
|
||||
udp.hopInterval = t .. "s"
|
||||
return udp
|
||||
end)() or nil
|
||||
},
|
||||
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
|
||||
}
|
||||
} or nil,
|
||||
auth = node.hysteria2_auth_password,
|
||||
tls = {
|
||||
sni = node.tls_serverName,
|
||||
insecure = (node.tls_allowInsecure == "1") and true or false,
|
||||
pinSHA256 = (node.tls_pinSHA256) and node.tls_pinSHA256 or nil,
|
||||
ech = (node.ech == "1") and node.ech_config or nil
|
||||
},
|
||||
quic = {
|
||||
initStreamReceiveWindow = (node.hysteria2_recv_window) and tonumber(node.hysteria2_recv_window) or nil,
|
||||
initConnReceiveWindow = (node.hysteria2_recv_window_conn) and tonumber(node.hysteria2_recv_window_conn) or nil,
|
||||
maxIdleTimeout = (function(t)
|
||||
t = tonumber(tostring(t or "30"):match("^%d+"))
|
||||
return (t and t >= 4 and t <= 120) and t .. "s" or "30s"
|
||||
end)(node.hysteria2_idle_timeout),
|
||||
keepAlivePeriod = (function(t)
|
||||
t = tonumber(tostring(t or "0"):match("^%d+"))
|
||||
return (t and t >= 2 and t <= 60) and t .. "s" or nil
|
||||
end)(node.hysteria2_keep_alive_period),
|
||||
disablePathMTUDiscovery = (node.hysteria2_disable_mtu_discovery) and true or false,
|
||||
},
|
||||
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
|
||||
} or nil,
|
||||
fast_open = (node.fast_open == "1") and true or false,
|
||||
lazy = (node.hysteria2_lazy_start == "1") and true or false,
|
||||
socks5 = (local_socks_address and local_socks_port) and {
|
||||
listen = local_socks_address .. ":" .. local_socks_port,
|
||||
username = (local_socks_username and local_socks_password) and local_socks_username or nil,
|
||||
password = (local_socks_username and local_socks_password) and local_socks_password or nil,
|
||||
disableUDP = false,
|
||||
} or nil,
|
||||
http = (local_http_address and local_http_port) and {
|
||||
listen = local_http_address .. ":" .. local_http_port,
|
||||
username = (local_http_username and local_http_password) and local_http_username or nil,
|
||||
password = (local_http_username and local_http_password) and local_http_password or nil,
|
||||
} or nil,
|
||||
tcpRedirect = ("redirect" == tcp_proxy_way and local_redir_port) and {
|
||||
listen = "0.0.0.0:" .. local_redir_port
|
||||
} or nil,
|
||||
tcpTProxy = ("tproxy" == tcp_proxy_way and local_redir_port) and {
|
||||
listen = "0.0.0.0:" .. local_redir_port
|
||||
} or nil,
|
||||
udpTProxy = (local_redir_port) and {
|
||||
listen = "0.0.0.0:" .. local_redir_port
|
||||
} 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
|
||||
if min <= 0 or min > max or max > 2048 then
|
||||
min = 512
|
||||
max = 1200
|
||||
end
|
||||
config.obfs.gecko.minPacketSize = min
|
||||
config.obfs.gecko.maxPacketSize = max
|
||||
end
|
||||
|
||||
return jsonc.stringify(config, 1)
|
||||
end
|
||||
|
||||
_G.gen_config = gen_config
|
||||
|
||||
if arg[1] then
|
||||
local func =_G[arg[1]]
|
||||
if func then
|
||||
local var = nil
|
||||
if arg[2] then
|
||||
var = jsonc.parse(arg[2])
|
||||
end
|
||||
print(func(var))
|
||||
end
|
||||
end
|
||||
@@ -1,42 +0,0 @@
|
||||
module("luci.passwall.util_naiveproxy", package.seeall)
|
||||
local api = require "luci.passwall.api"
|
||||
local jsonc = api.jsonc
|
||||
|
||||
function gen_config(var)
|
||||
local node_id = var["node"]
|
||||
if not node_id then
|
||||
print("node 不能为空")
|
||||
return
|
||||
end
|
||||
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"]
|
||||
local server_host = var["server_host"] or (node.address or ""):lower()
|
||||
local server_port = var["server_port"] or node.port
|
||||
|
||||
if api.is_ipv6(server_host) then
|
||||
server_host = api.get_ipv6_full(server_host)
|
||||
end
|
||||
local server = server_host .. ":" .. server_port
|
||||
|
||||
local config = {
|
||||
listen = run_type .. "://" .. local_addr .. ":" .. local_port,
|
||||
proxy = node.protocol .. "://" .. node.username .. ":" .. node.password .. "@" .. server
|
||||
}
|
||||
|
||||
return jsonc.stringify(config, 1)
|
||||
end
|
||||
|
||||
_G.gen_config = gen_config
|
||||
|
||||
if arg[1] then
|
||||
local func =_G[arg[1]]
|
||||
if func then
|
||||
local var = nil
|
||||
if arg[2] then
|
||||
var = jsonc.parse(arg[2])
|
||||
end
|
||||
print(func(var))
|
||||
end
|
||||
end
|
||||
@@ -1,158 +0,0 @@
|
||||
module("luci.passwall.util_shadowsocks", package.seeall)
|
||||
local api = require "luci.passwall.api"
|
||||
local jsonc = api.jsonc
|
||||
|
||||
function gen_config_server(node)
|
||||
local user = nil
|
||||
if node.user then
|
||||
user = api.uci_get_s(node.user)
|
||||
end
|
||||
local config = {}
|
||||
config.server_port = tonumber(node.port)
|
||||
config.password = user and user.password or ""
|
||||
config.timeout = tonumber(node.timeout)
|
||||
config.fast_open = (node.tcp_fast_open and node.tcp_fast_open == "1") and true or false
|
||||
config.method = node.method
|
||||
|
||||
if node.type == "SS-Rust" then
|
||||
config.server = "::"
|
||||
config.mode = "tcp_and_udp"
|
||||
else
|
||||
config.server = {"[::0]", "0.0.0.0"}
|
||||
end
|
||||
|
||||
if node.type == "SSR" then
|
||||
config.protocol = node.protocol
|
||||
config.protocol_param = node.protocol_param
|
||||
config.obfs = node.obfs
|
||||
config.obfs_param = node.obfs_param
|
||||
end
|
||||
|
||||
return config
|
||||
end
|
||||
|
||||
local plugin_sh, plugin_bin
|
||||
|
||||
function gen_config(var)
|
||||
local node_id = var["node"]
|
||||
if not node_id then
|
||||
print("node 不能为空")
|
||||
return
|
||||
end
|
||||
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"]
|
||||
local local_port = var["local_port"]
|
||||
local mode = var["mode"]
|
||||
local local_socks_address = var["local_socks_address"] or "0.0.0.0"
|
||||
local local_socks_port = var["local_socks_port"]
|
||||
local local_socks_username = var["local_socks_username"]
|
||||
local local_socks_password = var["local_socks_password"]
|
||||
local local_http_address = var["local_http_address"] or "0.0.0.0"
|
||||
local local_http_port = var["local_http_port"]
|
||||
local local_http_username = var["local_http_username"]
|
||||
local local_http_password = var["local_http_password"]
|
||||
local local_redir_port = var["local_redir_port"]
|
||||
local local_redir_address = var["local_redir_address"] or "0.0.0.0"
|
||||
local loglevel = var["loglevel"]
|
||||
|
||||
if api.is_ipv6(server_host) then
|
||||
server_host = api.get_ipv6_only(server_host)
|
||||
end
|
||||
local server = server_host
|
||||
|
||||
local plugin_file
|
||||
if node.plugin and node.plugin ~= "" and node.plugin ~= "none" then
|
||||
plugin_sh = var["plugin_sh"] or ""
|
||||
plugin_file = (plugin_sh ~="") and plugin_sh or node.plugin
|
||||
plugin_bin = node.plugin
|
||||
end
|
||||
|
||||
local config = {
|
||||
server = server,
|
||||
server_port = tonumber(server_port),
|
||||
local_address = local_addr,
|
||||
local_port = tonumber(local_port),
|
||||
password = node.password,
|
||||
method = node.method,
|
||||
timeout = tonumber(node.timeout),
|
||||
fast_open = (node.tcp_fast_open and node.tcp_fast_open == "1") and true or false,
|
||||
reuse_port = true,
|
||||
tcp_tproxy = var["tcp_tproxy"] and true or nil
|
||||
}
|
||||
|
||||
if node.type == "SSR" then
|
||||
config.protocol = node.protocol
|
||||
config.protocol_param = node.protocol_param
|
||||
config.obfs = node.obfs
|
||||
config.obfs_param = node.obfs_param
|
||||
elseif node.type == "SS-Rust" then
|
||||
config = {
|
||||
servers = {
|
||||
{
|
||||
address = server,
|
||||
port = tonumber(server_port),
|
||||
method = node.method,
|
||||
password = node.password,
|
||||
timeout = tonumber(node.timeout),
|
||||
plugin = plugin_file or nil,
|
||||
plugin_opts = (plugin_file) and node.plugin_opts or nil
|
||||
}
|
||||
},
|
||||
locals = {},
|
||||
fast_open = (node.tcp_fast_open and node.tcp_fast_open == "1") and true or false,
|
||||
log = {
|
||||
level = (loglevel == "debug") and 1 or 0
|
||||
}
|
||||
}
|
||||
if local_socks_address and local_socks_port then
|
||||
table.insert(config.locals, {
|
||||
local_address = local_socks_address,
|
||||
local_port = tonumber(local_socks_port),
|
||||
mode = "tcp_and_udp"
|
||||
})
|
||||
end
|
||||
if local_http_address and local_http_port then
|
||||
table.insert(config.locals, {
|
||||
protocol = "http",
|
||||
local_address = local_http_address,
|
||||
local_port = tonumber(local_http_port)
|
||||
})
|
||||
end
|
||||
if local_redir_address and local_redir_port then
|
||||
table.insert(config.locals, {
|
||||
protocol = "redir",
|
||||
mode = "tcp_and_udp",
|
||||
tcp_redir = var["tcp_tproxy"] and "tproxy" or nil,
|
||||
local_address = local_redir_address,
|
||||
local_port = tonumber(local_redir_port)
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return jsonc.stringify(config, 1)
|
||||
end
|
||||
|
||||
_G.gen_config = gen_config
|
||||
|
||||
if arg[1] then
|
||||
local func =_G[arg[1]]
|
||||
if func then
|
||||
local var = nil
|
||||
if arg[2] then
|
||||
var = jsonc.parse(arg[2])
|
||||
end
|
||||
print(func(var))
|
||||
if plugin_sh and plugin_sh ~="" and plugin_bin then
|
||||
local f = io.open(plugin_sh, "w")
|
||||
f:write("#!/bin/sh\n")
|
||||
f:write("export PATH=/usr/sbin:/usr/bin:/sbin:/bin:/root/bin:$PATH\n")
|
||||
f:write(plugin_bin .. " $@ &\n")
|
||||
f:write("echo $! > " .. plugin_sh:gsub("%.sh$", ".pid") .. "\n")
|
||||
f:write("wait\n")
|
||||
f:close()
|
||||
luci.sys.call("chmod +x " .. plugin_sh)
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1,238 +0,0 @@
|
||||
<div id="app_version">
|
||||
<%
|
||||
local map = self.map
|
||||
local api = map.api
|
||||
local com = self.com
|
||||
-%>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
var appInfoList = new Array();
|
||||
var inProgressCount = 0;
|
||||
var tokenStr = '<%=token%>';
|
||||
var checkUpdateText = '<%:Check update%>';
|
||||
var forceUpdateText = '<%:Force update%>';
|
||||
var retryText = '<%:Retry%>';
|
||||
var noUpdateText = '<%:It is the latest version%>';
|
||||
var updateSuccessText = '<%:Update successful%>';
|
||||
var clickToUpdateText = '<%:Click to update%>';
|
||||
var inProgressText = '<%:Updating...%>';
|
||||
var unexpectedErrorText = '<%:Unexpected error%>';
|
||||
var updateInProgressNotice = '<%:Updating, are you sure to close?%>';
|
||||
var downloadingText = '<%:Downloading...%>';
|
||||
var decompressioningText = '<%:Unpacking...%>';
|
||||
var movingText = '<%:Moving...%>';
|
||||
|
||||
//window.onload = function () {};
|
||||
|
||||
function addPageNotice() {
|
||||
if (inProgressCount === 0) {
|
||||
window.onbeforeunload = function (e) {
|
||||
e.returnValue = updateInProgressNotice;
|
||||
return updateInProgressNotice;
|
||||
};
|
||||
}
|
||||
inProgressCount++;
|
||||
}
|
||||
|
||||
function removePageNotice() {
|
||||
inProgressCount--;
|
||||
if (inProgressCount === 0) {
|
||||
window.onbeforeunload = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function onUpdateSuccess(btn) {
|
||||
if (btn) {
|
||||
btn.value = updateSuccessText;
|
||||
btn.placeholder = updateSuccessText;
|
||||
btn.disabled = true;
|
||||
}
|
||||
|
||||
if (inProgressCount === 0) {
|
||||
window.setTimeout(function () {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function onRequestError(btn, errorMessage) {
|
||||
btn.disabled = false;
|
||||
btn.value = retryText;
|
||||
|
||||
var ckeckDetailElm = document.getElementById(btn.id + '-detail');
|
||||
if (errorMessage && ckeckDetailElm) {
|
||||
ckeckDetailElm.textContent = errorMessage
|
||||
}
|
||||
}
|
||||
|
||||
function onBtnClick(btn, app) {
|
||||
if (appInfoList[app] === undefined) {
|
||||
checkUpdate(btn, app);
|
||||
} else {
|
||||
doUpdate(btn, app);
|
||||
}
|
||||
}
|
||||
|
||||
function checkUpdate(btn, app) {
|
||||
btn.disabled = true;
|
||||
btn.value = inProgressText;
|
||||
|
||||
addPageNotice();
|
||||
|
||||
var ckeckDetailElm = document.getElementById(btn.id + '-detail');
|
||||
if (ckeckDetailElm) {
|
||||
ckeckDetailElm.textContent = "";
|
||||
}
|
||||
XHR.get('<%=api.url("check_")%>' + app, {
|
||||
token: tokenStr,
|
||||
arch: ''
|
||||
}, function (x, json) {
|
||||
removePageNotice();
|
||||
if (json.code) {
|
||||
appInfoList[app] = undefined;
|
||||
onRequestError(btn, json.error);
|
||||
} else {
|
||||
appInfoList[app] = json;
|
||||
if (json.has_update) {
|
||||
btn.disabled = false;
|
||||
btn.value = clickToUpdateText;
|
||||
btn.placeholder = clickToUpdateText;
|
||||
|
||||
if (ckeckDetailElm) {
|
||||
var urlNode = '';
|
||||
if (json.remote_version) {
|
||||
urlNode = '<em style="color:red;">' + json.remote_version + '</em>';
|
||||
if (json.html_url) {
|
||||
urlNode = '<a href="' + json.html_url + '" target="_blank">' + urlNode + '</a>';
|
||||
}
|
||||
}
|
||||
ckeckDetailElm.innerHTML = urlNode;
|
||||
}
|
||||
} else {
|
||||
btn.disabled = true;
|
||||
btn.value = noUpdateText;
|
||||
var forceBtn = document.getElementById('_' + app + '-force_btn');
|
||||
if (forceBtn) {
|
||||
forceBtn.style.display = "inline";
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function doUpdate(btn, app) {
|
||||
btn.disabled = true;
|
||||
btn.value = downloadingText;
|
||||
|
||||
addPageNotice();
|
||||
|
||||
var appUpdateUrl = '<%=api.url("update_")%>' + app;
|
||||
var appInfo = appInfoList[app];
|
||||
// Download file
|
||||
XHR.get(appUpdateUrl, {
|
||||
token: tokenStr,
|
||||
url: appInfo ? appInfo.data.browser_download_url : '',
|
||||
size: appInfo ? appInfo.data.size / 1024 : null
|
||||
}, function (x, json) {
|
||||
if (json.code) {
|
||||
removePageNotice();
|
||||
onRequestError(btn, json.error);
|
||||
} else if (json.zip) {
|
||||
btn.value = decompressioningText;
|
||||
|
||||
// Extract file
|
||||
XHR.get(appUpdateUrl, {
|
||||
token: tokenStr,
|
||||
task: 'extract',
|
||||
file: json.file,
|
||||
subfix: appInfo ? appInfo.type : ''
|
||||
}, function (x, json) {
|
||||
if (json.code) {
|
||||
removePageNotice();
|
||||
onRequestError(btn, json.error);
|
||||
} else {
|
||||
move(btn, appUpdateUrl, json.file);
|
||||
}
|
||||
}, 300)
|
||||
} else {
|
||||
move(btn, appUpdateUrl, json.file);
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function move(btn, url, file) {
|
||||
btn.value = movingText;
|
||||
|
||||
// Move file to target dir
|
||||
XHR.get(url, {
|
||||
token: tokenStr,
|
||||
task: 'move',
|
||||
file: file
|
||||
}, function (x, json) {
|
||||
removePageNotice();
|
||||
if (json.code) {
|
||||
onRequestError(btn, json.error);
|
||||
} else {
|
||||
onUpdateSuccess(btn);
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
//]]>
|
||||
</script>
|
||||
|
||||
<div class="cbi-value">
|
||||
<label class="cbi-value-title">Passwall <%:Version%></label>
|
||||
<div class="cbi-value-field">
|
||||
<!--div class="cbi-value-description"-->
|
||||
<span>【 <%=api.get_version()%> 】</span>
|
||||
<input class="btn cbi-button cbi-button-apply" type="button" id="passwall-check_btn"
|
||||
onclick="onBtnClick(this,'passwall');" value="<%:Check update%>" />
|
||||
<span id="passwall-check_btn-detail"></span>
|
||||
<!--/div-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%for _, k in ipairs(com.order) do
|
||||
local v = com[k]
|
||||
%>
|
||||
<div class="cbi-value">
|
||||
<label class="cbi-value-title"><%=v.name%>
|
||||
<%:Version%>
|
||||
</label>
|
||||
<div class="cbi-value-field">
|
||||
<!--div class="cbi-value-description"-->
|
||||
<span>【 <span class="com_version" key="<%=k%>"><%=translate("Null")%></span> 】</span>
|
||||
<input class="btn cbi-button cbi-button-apply" type="button" id="_<%=k%>-check_btn"
|
||||
onclick="onBtnClick(this,'<%=k%>');" value="<%:Check update%>" />
|
||||
<input class="btn cbi-button cbi-button-apply" type="button" id="_<%=k%>-force_btn"
|
||||
onclick="doUpdate(this,'<%=k%>');" value="<%:Force update%>" style="display:none"/>
|
||||
<span id="_<%=k%>-check_btn-detail"></span>
|
||||
<!--/div-->
|
||||
</div>
|
||||
</div>
|
||||
<%end%>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
let coms = document.getElementsByClassName("com_version");
|
||||
if (coms.length > 0) {
|
||||
for (let i = 0; i < coms.length; i++) {
|
||||
let key = coms[i].getAttribute("key");
|
||||
coms[i].innerHTML = "<%=translate("Collecting data...")%>";
|
||||
XHR.get('<%=api.url("version_")%>' + key, null, function(x, result) {
|
||||
if (x.status == 200 && result.code == 1) {
|
||||
let data = result.data;
|
||||
let t = "<%=translate("Null")%>";
|
||||
if (data && data != "")
|
||||
t = data;
|
||||
coms[i].innerHTML = t;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
//]]>
|
||||
</script>
|
||||
</div>
|
||||
@@ -1,65 +0,0 @@
|
||||
<%
|
||||
local map = self.map
|
||||
-%>
|
||||
<!-- map footer -->
|
||||
<script type="text/javascript">
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
setTimeout(updateElements, 200);
|
||||
function updateElements() {
|
||||
//删除value控件和描述之间多余换行
|
||||
document.querySelectorAll("div.cbi-value-description").forEach(function(descDiv) {
|
||||
var prev = descDiv.previousSibling;
|
||||
while (prev && prev.nodeType === Node.TEXT_NODE && prev.textContent.trim() === "") {
|
||||
prev = prev.previousSibling;
|
||||
}
|
||||
if (prev && prev.nodeType === Node.ELEMENT_NODE && prev.tagName === "BR") {
|
||||
prev.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<% if luci.http.formvalue("cbi.apply") == "1" and map.is_js_luci then -%>
|
||||
<script type="text/javascript">
|
||||
document.addEventListener("luci-loaded", function() {
|
||||
//L.env.apply_display = 2; //Write whatever you want, it's all just a fake progress bar anyway.
|
||||
//L.env.apply_holdoff = 1;
|
||||
|
||||
let tt;
|
||||
let flag = 2;
|
||||
if (flag == 1) {
|
||||
L.ui.changes.confirm(false, Date.now() + L.env.apply_rollback * 1000);
|
||||
} else {
|
||||
let ts = Date.now();
|
||||
const deadline = Date.now() + L.env.apply_rollback * 1000;
|
||||
const tick = () => {
|
||||
const now = Date.now();
|
||||
|
||||
L.ui.changes.displayStatus('notice spinning',
|
||||
E('p', _('Applying configuration changes… %ds')
|
||||
.format(Math.max(Math.floor((deadline - Date.now()) / 1000), 0))));
|
||||
|
||||
if (now >= deadline)
|
||||
return;
|
||||
|
||||
tt = window.setTimeout(tick, 1000 - (now - ts));
|
||||
ts = now;
|
||||
};
|
||||
tick();
|
||||
}
|
||||
|
||||
window.setTimeout(() => {
|
||||
if (tt)
|
||||
window.clearTimeout(tt);
|
||||
L.ui.changes.displayStatus(false);
|
||||
<% if not map.redirect then -%>
|
||||
//window.location = window.location.href.split('#')[0];
|
||||
window.location = "<%=map.api.url("log")%>";
|
||||
<%- else %>
|
||||
window.location.href = "<%=map.redirect%>";
|
||||
<%- end %>
|
||||
}, (L.env.apply_display + L.env.apply_holdoff) * 1000);
|
||||
});
|
||||
</script>
|
||||
<%- end %>
|
||||
@@ -1,28 +0,0 @@
|
||||
<%
|
||||
local map = self.map
|
||||
local api = map.api
|
||||
-%>
|
||||
<!-- map header -->
|
||||
<script src="<%=resource%>/view/<%=api.appname%>/cbi.js"></script>
|
||||
<script src="<%=resource%>/view/<%=api.appname%>/func.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
function update_config(cfgid, data, redirect_url) {
|
||||
XHR.get('<%=api.url("update_config")%>', {
|
||||
id: cfgid,
|
||||
data: JSON.stringify(data)
|
||||
},
|
||||
function(x, data) {
|
||||
if (x && x.status == 200 && data.code == 1) {
|
||||
if (redirect_url) {
|
||||
window.location.href = redirect_url;
|
||||
}
|
||||
}
|
||||
else {
|
||||
alert("<%:Error%>");
|
||||
}
|
||||
});
|
||||
}
|
||||
//]]>
|
||||
</script>
|
||||
@@ -1,3 +0,0 @@
|
||||
<div id="cbi-<%=self.config.."-"..section.."-"..self.option%>" data-index="<%=self.index%>" data-depends="<%=pcdata(self:deplist2json(section))%>" style="display: none !important">
|
||||
<input type="hidden" id="<%=cbid%>" name="<%=cbid%>" value="<%=pcdata(self:cfgvalue(section) or self.default or "")%>" />
|
||||
</div>
|
||||
@@ -1,149 +0,0 @@
|
||||
<%+cbi/valueheader%>
|
||||
<%
|
||||
-- Template Developers:
|
||||
-- - lwb1978
|
||||
-- - snowie2000
|
||||
-- Copyright: copyright(c)2025–2027
|
||||
-- Description: Passwall(2) UI template
|
||||
|
||||
local json = require "luci.jsonc"
|
||||
|
||||
-- 读取值(keylist/vallist/group)
|
||||
local values = {}
|
||||
for i, key in pairs(self.keylist) do
|
||||
values[#values + 1] = {
|
||||
key = key,
|
||||
label = self.vallist[i] or key,
|
||||
group = self.group and self.group[i] or nil
|
||||
}
|
||||
end
|
||||
|
||||
-- 获取当前配置值
|
||||
local current_key = {}
|
||||
local cval = self:cfgvalue(section)
|
||||
if type(cval) == "table" then
|
||||
for _, v in ipairs(cval) do
|
||||
for item in string.gmatch(v, "[^,]+") do
|
||||
current_key[#current_key+1] = item
|
||||
end
|
||||
end
|
||||
elseif type(cval) == "string" then
|
||||
for item in string.gmatch(cval, "[^,]+") do
|
||||
current_key[#current_key+1] = item
|
||||
end
|
||||
end
|
||||
|
||||
-- 分离无组节点(ungrouped)与有组节点(grouped),并保持原有顺序
|
||||
local ungrouped = {}
|
||||
local groups = {}
|
||||
local group_order = {}
|
||||
for _, item in ipairs(values) do
|
||||
if not item.group or item.group == "" then
|
||||
table.insert(ungrouped, item)
|
||||
else
|
||||
local g = item.group
|
||||
if not groups[g] then
|
||||
groups[g] = {}
|
||||
table.insert(group_order, g)
|
||||
end
|
||||
table.insert(groups[g], item)
|
||||
end
|
||||
end
|
||||
|
||||
-- 打包数据给 JS
|
||||
local dropdown_data = {
|
||||
current_key = current_key,
|
||||
ungrouped = ungrouped,
|
||||
groups = groups,
|
||||
group_order = group_order,
|
||||
cbid = cbid,
|
||||
}
|
||||
%>
|
||||
<script>
|
||||
window.dl_dropdown_data = window.dl_dropdown_data || {};
|
||||
window.dl_dropdown_data["<%=cbid%>"] = <%=json.stringify(dropdown_data)%>;
|
||||
</script>
|
||||
|
||||
<div id="<%=cbid%>.main" class="dl-dropdown-container">
|
||||
<div style="display:none !important;">
|
||||
<select id="<%=cbid%>.ref" class="cbi-input-select" style="display:none !important;">
|
||||
<option value>placeholder</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- 已选节点显示区 -->
|
||||
<div id="<%=cbid%>.selected" class="dl-selected-list"></div>
|
||||
<!-- 模拟 listvalue 控件外观 -->
|
||||
<div class="cbi-input-value cbi-input-select dl-dropdown-display" id="<%=cbid%>.display" tabindex="0">
|
||||
<span id="<%=cbid%>.label" class="dl-dropdown-label">
|
||||
<%=translate("-- Please choose --")%>
|
||||
</span>
|
||||
<span class="dl-arrow-down"></span>
|
||||
</div>
|
||||
<!-- 下拉面板 -->
|
||||
<div id="<%=cbid%>.panel" class="cbi-dynamiclist-panel dl-dropdown-panel" style="display:none;">
|
||||
<!-- 搜索框 -->
|
||||
<div style="padding:8px;border-bottom:1px solid #f0f0f0;">
|
||||
<input id="<%=cbid%>.search" class="cbi-input-text dl-dropdown-search" type="text" placeholder="🔍 <%:Search nodes...%>" inputmode="search" enterkeyhint="done" />
|
||||
</div>
|
||||
<!-- 列表容器 -->
|
||||
<div id="<%=cbid%>.list" style="padding:8px;">
|
||||
<!-- 首次点击 display 时由 JS 填充 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
(function(){
|
||||
const cbid = "<%=cbid%>";
|
||||
const panel = document.getElementById(cbid + ".panel");
|
||||
const display = document.getElementById(cbid + ".display");
|
||||
const labelSpan = document.getElementById(cbid + ".label");
|
||||
const searchInput = document.getElementById(cbid + ".search");
|
||||
const listContainer = document.getElementById(cbid + ".list");
|
||||
|
||||
function init(){
|
||||
const hidden = document.getElementById(cbid + ".hidden");
|
||||
const data = window.dl_dropdown_data[cbid];
|
||||
let arr = data.current_key;
|
||||
if (!arr || arr.length === 0) return;
|
||||
arr.forEach(key=>{
|
||||
let label = "";
|
||||
data.ungrouped.concat(...Object.values(data.groups)).forEach(v=>{
|
||||
if (v.key == key) label = v.label;
|
||||
});
|
||||
if (label)
|
||||
dl_add_item(cbid, key, label);
|
||||
});
|
||||
}
|
||||
|
||||
// 点击 display
|
||||
display.addEventListener("click", function(e){
|
||||
e.stopPropagation();
|
||||
dl_render_dropdown_list(cbid,panel,listContainer,labelSpan,searchInput,display);
|
||||
document.querySelectorAll(".cbi-dynamiclist-panel").forEach(p=>{
|
||||
if (p !== panel) p.style.display = "none";
|
||||
});
|
||||
if (panel.style.display !== "none") {
|
||||
dl_closePanel(cbid,panel,listContainer,searchInput);
|
||||
} else {
|
||||
dl_openPanel(cbid,display,panel,listContainer,searchInput);
|
||||
}
|
||||
});
|
||||
|
||||
dl_registerAdaptive(cbid);
|
||||
|
||||
init();
|
||||
|
||||
new Sortable(document.getElementById(cbid + ".selected"), {
|
||||
animation: 150,
|
||||
ghostClass: "dragging-row",
|
||||
onEnd: function(){
|
||||
dl_update_hidden(cbid);
|
||||
}
|
||||
});
|
||||
})();
|
||||
//]]>
|
||||
</script>
|
||||
|
||||
<%+cbi/valuefooter%>
|
||||
@@ -1,654 +0,0 @@
|
||||
<%
|
||||
-- Template Developers:
|
||||
-- - lwb1978
|
||||
-- - snowie2000
|
||||
-- Copyright: copyright(c)2025–2027
|
||||
-- Description: Passwall(2) UI template
|
||||
-- It is the common part of the template and cannot be used independently
|
||||
%>
|
||||
<script src="<%=resource%>/view/passwall/Sortable.min.js"></script>
|
||||
<style>
|
||||
/* 主下拉按钮的下箭头 */
|
||||
.dl-arrow-down {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 6px solid #666;
|
||||
margin-left: 6px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* 组标题的右箭头(折叠) */
|
||||
.dl-arrow-right {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-left: 5px solid #555;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* 组标题的下箭头(展开) */
|
||||
.dl-arrow-down-small {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 5px solid #555;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* 基础列表项样式 */
|
||||
.cbi-dynamiclist-panel li[data-key] {
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
/* 鼠标悬停效果 - 使用透明度避免覆盖问题 */
|
||||
.cbi-dynamiclist-panel li[data-key]:hover {
|
||||
background-color: rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
|
||||
.dl-dropdown-container {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
min-width: 220px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@media (max-width: 1152px) {
|
||||
.dl-dropdown-container {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.dl-dropdown-container {
|
||||
display: block;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.dl-dropdown-display {
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.dl-dropdown-label,
|
||||
.dl-selected-label {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
.dl-dropdown-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2147483647;
|
||||
border: 1px solid #dcdcdc;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 6px 18px rgba(0,0,0,0.08);
|
||||
max-height: 50vh;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.dl-dropdown-search {
|
||||
width: 100%;
|
||||
max-width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
box-sizing: border-box;
|
||||
padding: 6px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.dl-group-title {
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.dl-group-list {
|
||||
list-style: none;
|
||||
margin: 6px 0 0 8px;
|
||||
padding: 0;
|
||||
}
|
||||
.dl-group-item {
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: left !important;
|
||||
}
|
||||
.dl-selected-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
.dl-remove {
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
color: red;
|
||||
}
|
||||
.dragging-row {
|
||||
background-color: rgba(131, 191, 255, 0.7) !important;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
// css helper functions
|
||||
function dl_camelToKebab(str) {
|
||||
return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase()
|
||||
}
|
||||
|
||||
function dl_style2Css(styleDeclaration, properties) {
|
||||
const cssRules = properties.map(prop => {
|
||||
const kebabCaseProp = dl_camelToKebab(prop);[1, 5]
|
||||
const value = styleDeclaration[prop]
|
||||
if (value) {
|
||||
return `${kebabCaseProp}: ${value};`
|
||||
}
|
||||
return ''
|
||||
})
|
||||
// Filter out any empty strings and join the rules
|
||||
return cssRules.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
const dl_parseColorToRgba = (function() {
|
||||
// Create canvas and context once (Closure)
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
return function(colorStr) {
|
||||
if (!colorStr)
|
||||
return null;
|
||||
ctx.clearRect(0, 0, 1, 1);
|
||||
// 2. Apply the color
|
||||
ctx.fillStyle = colorStr;
|
||||
// 3. Fill a single pixel
|
||||
ctx.fillRect(0, 0, 1, 1);
|
||||
// 4. Extract pixel data [R, G, B, A]
|
||||
const data = ctx.getImageData(0, 0, 1, 1).data;
|
||||
return {
|
||||
r: data[0],
|
||||
g: data[1],
|
||||
b: data[2],
|
||||
// Convert alpha from 0-255 to 0-1 (rounded to 3 decimal places)
|
||||
a: Math.round((data[3] / 255) * 1000) / 1000
|
||||
};
|
||||
};
|
||||
})();
|
||||
|
||||
// Helper to convert back to Hex (for output consistency)
|
||||
function dl_rgbToHex(r, g, b) {
|
||||
const toHex = (n) => {
|
||||
const hex = Math.max(0, Math.min(255, n)).toString(16)
|
||||
return hex.length === 1 ? '0' + hex : hex
|
||||
}
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
||||
}
|
||||
|
||||
function dl_isTransparent(color) {
|
||||
const cleanColor = dl_parseColorToRgba(color);
|
||||
// check #RRGGBBAA for transparency
|
||||
return !cleanColor || (cleanColor.a !== undefined && !cleanColor.a);
|
||||
}
|
||||
|
||||
function dl_getColorSchema(color) {
|
||||
const rgb = dl_parseColorToRgba(color);
|
||||
if (!rgb) return 'unknown'; // Handle invalid colors
|
||||
// Calculate YIQ brightness (human eye perception)
|
||||
const brightness = ((rgb.r * 299) + (rgb.g * 587) + (rgb.b * 114)) / 1000;
|
||||
|
||||
return brightness > 128 ? 'light' : 'dark';
|
||||
}
|
||||
|
||||
function dl_lighter(color, amount) {
|
||||
const rgb = dl_parseColorToRgba(color);
|
||||
if (!rgb) return color;
|
||||
// Add amount to each channel
|
||||
const r = rgb.r + amount;
|
||||
const g = rgb.g + amount;
|
||||
const b = rgb.b + amount;
|
||||
// Convert back to Hex (clamping happens inside rgbToHex)
|
||||
return dl_rgbToHex(r, g, b);
|
||||
}
|
||||
|
||||
function dl_darker(color, amount) {
|
||||
const rgb = dl_parseColorToRgba(color);
|
||||
if (!rgb) return color;
|
||||
// Subtract amount from each channel
|
||||
const r = rgb.r - amount;
|
||||
const g = rgb.g - amount;
|
||||
const b = rgb.b - amount;
|
||||
|
||||
return dl_rgbToHex(r, g, b);
|
||||
}
|
||||
|
||||
// copy select styles
|
||||
function dl_adaptiveStyle(cbid) {
|
||||
const display = document.getElementById(cbid + ".display");
|
||||
const hiddenRef = document.getElementById(cbid + ".ref");
|
||||
const panel = document.getElementById(cbid + ".panel");
|
||||
if (hiddenRef && display) {
|
||||
const elOption = hiddenRef.getElementsByTagName("option")[0]
|
||||
const styleSelect = window.getComputedStyle(hiddenRef)
|
||||
const styleOption = window.getComputedStyle(elOption)
|
||||
const styleBody = window.getComputedStyle(document.body)
|
||||
|
||||
const styleNode = document.createElement('style')
|
||||
const styleNames = ["width", "color", "height", "padding", "margin", "lineHeight", "border", "borderRadius", "minWidth", "minHeight"]
|
||||
if (styleSelect.borderBottomStyle !== "none") {
|
||||
styleNames.push("borderBottomWidth", "borderBottomStyle", "borderBottomColor");
|
||||
}
|
||||
document.head.appendChild(styleNode)
|
||||
// trace back from option -> select -> body for background color
|
||||
const panelRadius = styleSelect.borderRadius;
|
||||
const optionColor = !dl_isTransparent(styleOption.backgroundColor) ? styleOption.backgroundColor : !dl_isTransparent(styleSelect.backgroundColor) ? styleSelect.backgroundColor : styleBody.backgroundColor
|
||||
const titleColor = dl_getColorSchema(optionColor) === "light" ? dl_darker(optionColor, 30) : dl_lighter(optionColor, 30)
|
||||
const selectStyleCSS = [`#${CSS.escape(cbid + ".display")}, .dl-selected-item {`, dl_style2Css(styleSelect, styleNames), dl_style2Css(styleSelect, ["backgroundColor"]), "}"]
|
||||
const optionStyleCSS = [`#${CSS.escape(cbid + ".panel")} {`, dl_style2Css(styleOption, styleNames), `background-color: ${optionColor};`, `border-radius: ${panelRadius};`, "}"]
|
||||
const titleStyleCSS = [`#${CSS.escape(cbid + ".panel")} .dl-group-title {`, `background-color: ${titleColor} !important;`, "}"]
|
||||
styleNode.textContent = [].concat(selectStyleCSS, optionStyleCSS, titleStyleCSS).join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
function dl_idSafe(id) {
|
||||
return id
|
||||
.trim()
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/[\x00-\x1F\x7F]/g, "");
|
||||
}
|
||||
|
||||
// 更新组内选中计数
|
||||
function dl_updateGroupCounts(cbid, listContainer, searchInput) {
|
||||
const groups = listContainer.querySelectorAll(".dl-group");
|
||||
const isSearching = searchInput.value.trim() !== "";
|
||||
groups.forEach(group => {
|
||||
const gname = group.getAttribute("data-group");
|
||||
const items = group.querySelectorAll("li[data-key]");
|
||||
const span = document.getElementById("group-count-" + cbid + "-" + gname);
|
||||
if (!span) return;
|
||||
if (isSearching) {
|
||||
// 搜索状态:显示匹配数量
|
||||
let matchCount = 0;
|
||||
items.forEach(li => {
|
||||
if (li.style.display !== "none") matchCount++;
|
||||
});
|
||||
span.textContent = "(" + matchCount + "/" + items.length + ")";
|
||||
if (matchCount > 0) {
|
||||
span.style.color = "#28a745";
|
||||
span.style.fontWeight = "600";
|
||||
} else {
|
||||
span.style.color = "#dc3545";
|
||||
span.style.fontWeight = "normal";
|
||||
}
|
||||
} else {
|
||||
// 默认状态
|
||||
span.textContent = "(" + items.length + ")";
|
||||
span.style.color = "";
|
||||
span.style.fontWeight = "normal";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//搜索过滤器:按 name 或 label 做模糊匹配,搜索时自动展开所有组并隐藏不匹配条目
|
||||
function dl_filterList(keyword, cbid, listContainer, searchInput) {
|
||||
keyword = (keyword || "").toLowerCase().trim();
|
||||
const topItems = listContainer.querySelectorAll("ul li[data-key]");
|
||||
topItems.forEach(li=>{
|
||||
const name = (li.getAttribute("data-node-name") || "").toLowerCase();
|
||||
if (!keyword || name.indexOf(keyword) !== -1) {
|
||||
li.style.display = "block";
|
||||
} else {
|
||||
li.style.display = "none";
|
||||
}
|
||||
});
|
||||
const groups = listContainer.querySelectorAll(".dl-group");
|
||||
groups.forEach(group=>{
|
||||
const items = group.querySelectorAll("li[data-key]");
|
||||
let matchCount = 0;
|
||||
items.forEach(li=>{
|
||||
const name = (li.getAttribute("data-node-name") || "").toLowerCase();
|
||||
if (!keyword || name.indexOf(keyword) !== -1) {
|
||||
li.style.display = "block";
|
||||
matchCount++;
|
||||
} else {
|
||||
li.style.display = "none";
|
||||
}
|
||||
});
|
||||
group.style.display = (matchCount === 0 && keyword !== "") ? "none" : "block";
|
||||
const ul = group.querySelector(".dl-group-list");
|
||||
const gname = group.getAttribute("data-group");
|
||||
const arrow = document.getElementById("arrow-" + cbid + "-" + gname);
|
||||
if (keyword) {
|
||||
if (ul) ul.style.display = (matchCount > 0 ? "block" : "none");
|
||||
if (arrow) arrow.className = (matchCount > 0 ? "dl-arrow-down-small" : "dl-arrow-right");
|
||||
} else {
|
||||
if (ul) ul.style.display = "none";
|
||||
if (arrow) arrow.className = "dl-arrow-right";
|
||||
}
|
||||
});
|
||||
dl_updateGroupCounts(cbid, listContainer, searchInput);
|
||||
}
|
||||
|
||||
// 切换单个组(点击组标题)
|
||||
function dl_toggleGroup(listContainer, cbid, g) {
|
||||
g = dl_idSafe(g);
|
||||
const group = listContainer.querySelector(".dl-group[data-group='" + g + "']");
|
||||
if (!group) return;
|
||||
const ul = group.querySelector(".dl-group-list");
|
||||
const arrow = document.getElementById("arrow-" + cbid + "-" + g);
|
||||
if (!ul) return;
|
||||
const searchInput = document.getElementById(cbid + ".search");
|
||||
const isSearching = searchInput?.value.trim() !== "";
|
||||
const isExpanded = ul.style.display !== "none";
|
||||
|
||||
if (isExpanded) {
|
||||
ul.style.display = "none";
|
||||
if (arrow) arrow.className = "dl-arrow-right";
|
||||
} else {
|
||||
ul.style.display = "block";
|
||||
if (arrow) arrow.className = "dl-arrow-down-small";
|
||||
|
||||
if (!isSearching) {
|
||||
const allGroups = listContainer.querySelectorAll(".dl-group");
|
||||
allGroups.forEach(otherGroup => {
|
||||
if (otherGroup !== group) {
|
||||
const otherUl = otherGroup.querySelector(".dl-group-list");
|
||||
const otherGname = otherGroup.getAttribute("data-group");
|
||||
const otherArrow = document.getElementById("arrow-" + cbid + "-" + otherGname);
|
||||
if (otherUl) otherUl.style.display = "none";
|
||||
if (otherArrow) otherArrow.className = "dl-arrow-right";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算panel位置
|
||||
function dl_repositionPanel(panel, display) {
|
||||
if (!panel || panel.style.display === "none") return;
|
||||
const rect = display.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
||||
panel.style.visibility = "hidden";
|
||||
panel.style.display = "block";
|
||||
panel.style.minHeight = "100px";
|
||||
panel.style.maxHeight = Math.min(0.5*viewportHeight, 550) + "px";
|
||||
const panelHeight = panel.offsetHeight;
|
||||
const spaceBelow = viewportHeight - rect.bottom;
|
||||
const spaceAbove = rect.top;
|
||||
let top, isUp = false;
|
||||
if (spaceBelow >= panelHeight) {
|
||||
top = rect.bottom + 2;
|
||||
isUp = false;
|
||||
} else if (spaceAbove >= panelHeight) {
|
||||
top = rect.top - panelHeight - 2;
|
||||
isUp = true;
|
||||
} else {
|
||||
if (spaceBelow >= spaceAbove) {
|
||||
top = Math.max(rect.bottom - 2, viewportHeight - panelHeight - 2);
|
||||
isUp = false;
|
||||
} else {
|
||||
top = Math.min(rect.top - panelHeight + 2, 2);
|
||||
isUp = true;
|
||||
}
|
||||
}
|
||||
panel.style.left = rect.left + "px";
|
||||
panel.style.top = top + "px";
|
||||
const panelRect = panel.getBoundingClientRect();
|
||||
const displayWidth = rect.width;
|
||||
const remainingWidth = window.innerWidth - panelRect.left - 12;
|
||||
const maxWidth = Math.max(displayWidth, Math.floor(remainingWidth));
|
||||
panel.style.maxWidth = maxWidth + "px";
|
||||
panel.style.minWidth = displayWidth + "px";
|
||||
panel.style.width = "auto";
|
||||
panel.style.visibility = "";
|
||||
}
|
||||
|
||||
// 打开/关闭面板
|
||||
function dl_openPanel(cbid, display, panel, listContainer, searchInput) {
|
||||
if (!panel._moved) {
|
||||
document.body.appendChild(panel);
|
||||
panel._moved = true;
|
||||
}
|
||||
panel.style.display = "block";
|
||||
dl_repositionPanel(panel, display);
|
||||
// 失焦监听
|
||||
const handler = function(e){
|
||||
const target = e.target;
|
||||
if (panel.style.display !== "none") {
|
||||
if (!panel.contains(target) && !display.contains(target)) {
|
||||
dl_closePanel(cbid, panel, listContainer, searchInput, display);
|
||||
}
|
||||
}
|
||||
}
|
||||
panel._docClickHandler = handler;
|
||||
document.addEventListener("click", handler);
|
||||
// 滚动 / resize 自动 reposition
|
||||
let ticking = false;
|
||||
const repositionHandler = function () {
|
||||
if (ticking) return;
|
||||
ticking = true;
|
||||
requestAnimationFrame(function () {
|
||||
ticking = false;
|
||||
dl_repositionPanel(panel, display);
|
||||
});
|
||||
};
|
||||
panel._repositionHandler = repositionHandler;
|
||||
window.addEventListener("scroll", repositionHandler, true);
|
||||
window.addEventListener("resize", repositionHandler);
|
||||
}
|
||||
function dl_closePanel(cbid, panel, listContainer, searchInput) {
|
||||
panel.style.display = "none";
|
||||
searchInput.value = "";
|
||||
dl_filterList("", cbid, listContainer, searchInput);
|
||||
// document click
|
||||
if (panel._docClickHandler) {
|
||||
document.removeEventListener("click", panel._docClickHandler);
|
||||
panel._docClickHandler = null;
|
||||
}
|
||||
// scroll / resize
|
||||
if (panel._repositionHandler) {
|
||||
window.removeEventListener("scroll", panel._repositionHandler, true);
|
||||
window.removeEventListener("resize", panel._repositionHandler);
|
||||
panel._repositionHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
function dl_add_item(cbid, key, label) {
|
||||
const container = document.getElementById(cbid + ".selected");
|
||||
const hidden = document.getElementById(cbid + ".hidden");
|
||||
if (container.querySelector(`[data-key="${key}"]`)) return;
|
||||
const item = document.createElement("div");
|
||||
item.className = "cbi-input-value cbi-input-select dl-dropdown-display dl-selected-item";
|
||||
item.setAttribute("data-key", key);
|
||||
item.innerHTML = `
|
||||
<span class="dl-selected-label" style="cursor:grab;" title="${label}">${label}</span>
|
||||
<span class="dl-remove">×</span>
|
||||
`;
|
||||
item.querySelector(".dl-remove").onclick = function(){
|
||||
item.remove();
|
||||
dl_update_hidden(cbid);
|
||||
};
|
||||
container.appendChild(item);
|
||||
dl_update_hidden(cbid);
|
||||
}
|
||||
|
||||
function dl_update_hidden(cbid) {
|
||||
const main = document.getElementById(cbid + ".main");
|
||||
const container = document.getElementById(cbid + ".selected");
|
||||
main.querySelectorAll(`input[type="hidden"][name="${cbid}"]`).forEach(el => el.remove());
|
||||
let items = container.querySelectorAll(".dl-selected-item");
|
||||
items.forEach(el => {
|
||||
let key = el.getAttribute("data-key");
|
||||
let input = document.createElement("input");
|
||||
input.type = "hidden";
|
||||
input.name = cbid;
|
||||
input.value = key;
|
||||
main.insertBefore(input, container);
|
||||
});
|
||||
}
|
||||
|
||||
// 动态生成下拉框
|
||||
window.dl_dropdown_rendered = {};
|
||||
function dl_escape_html(s) {
|
||||
return s.replace(/[&<>"']/g, c => ({
|
||||
"&":"&", "<":"<", ">":">", '"':""", "'":"'"
|
||||
}[c]));
|
||||
}
|
||||
|
||||
function dl_render_dropdown_list(cbid, panel, listContainer, labelSpan, searchInput, display) {
|
||||
if (window.dl_dropdown_rendered[cbid]) return;
|
||||
const data = window.dl_dropdown_data[cbid];
|
||||
if (!data) return;
|
||||
if (!listContainer) return;
|
||||
|
||||
let html = "";
|
||||
|
||||
// 无组项
|
||||
if (data.ungrouped && data.ungrouped.length > 0) {
|
||||
html += `<ul style="list-style:none;padding:0;margin:0 0 8px 0;">`;
|
||||
|
||||
data.ungrouped.forEach(item => {
|
||||
html += `
|
||||
<li data-key="${item.key}"
|
||||
data-node-name="${dl_escape_html(item.label.toLowerCase())}"
|
||||
class="list-item">
|
||||
<span class="dl-item-label" style="margin-left:12px;">
|
||||
${dl_escape_html(item.label)}
|
||||
</span>
|
||||
</li>`;
|
||||
});
|
||||
|
||||
html += `</ul>`;
|
||||
}
|
||||
|
||||
// 分组项
|
||||
data.group_order.forEach(gname => {
|
||||
const items = data.groups[gname];
|
||||
|
||||
html += `
|
||||
<div class="dl-group" data-group="${dl_idSafe(gname)}" style="margin-bottom:8px;">
|
||||
<div class="dl-group-title" data-group-name="${dl_idSafe(gname)}">
|
||||
<span class="dl-arrow-right" id="arrow-${cbid}-${dl_idSafe(gname)}"></span>
|
||||
<b style="margin-left:6px;">${dl_escape_html(gname)}</b>
|
||||
<span id="group-count-${cbid}-${dl_idSafe(gname)}"
|
||||
style="margin-left:8px;">(${items.length})</span>
|
||||
</div>
|
||||
<ul id="group-${cbid}-${dl_idSafe(gname)}" class="dl-group-list" style="display:none">
|
||||
`;
|
||||
|
||||
items.forEach(item => {
|
||||
html += `
|
||||
<li data-key="${item.key}"
|
||||
data-node-name="${dl_escape_html(item.label.toLowerCase())}"
|
||||
class="dl-group-item">
|
||||
<span class="dl-item-label" title="${dl_escape_html(item.label)}">
|
||||
${dl_escape_html(item.label)}
|
||||
</span>
|
||||
</li>`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
listContainer.innerHTML = html;
|
||||
|
||||
window.dl_dropdown_rendered[cbid] = true;
|
||||
|
||||
dl_adaptiveStyle(cbid);
|
||||
|
||||
// 点击项(无组与组内项都使用 li[data-key])
|
||||
listContainer.addEventListener("click", function(e){
|
||||
let li = e.target;
|
||||
while(li && li !== listContainer && !li.hasAttribute('data-key')) li = li.parentNode;
|
||||
if(!li || li === listContainer) return;
|
||||
const key = li.getAttribute('data-key') || "";
|
||||
const text = li.querySelector(".dl-item-label")?.textContent || li.textContent || key;
|
||||
//改值
|
||||
dl_add_item(cbid, key, text);
|
||||
labelSpan.innerText = "<%=translate("-- Please choose --")%>";
|
||||
dl_closePanel(cbid,panel,listContainer,searchInput);
|
||||
});
|
||||
|
||||
// 搜索功能
|
||||
searchInput.addEventListener("input", function() {
|
||||
dl_filterList(this.value, cbid, listContainer, searchInput);
|
||||
dl_repositionPanel(panel, display);
|
||||
});
|
||||
searchInput.addEventListener('keydown', function(e) {
|
||||
const isEnter = e.key === "Enter" || e.keyCode === 13;
|
||||
if (!isEnter) return;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
searchInput.blur();
|
||||
});
|
||||
|
||||
// 切换组
|
||||
listContainer.querySelectorAll(".dl-group-title").forEach(title => {
|
||||
title.addEventListener("click", function() {
|
||||
const g = this.closest(".dl-group")?.getAttribute("data-group");
|
||||
if (g) {
|
||||
dl_toggleGroup(listContainer, cbid, g);
|
||||
dl_repositionPanel(panel, display);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 防止 panel 惯性滚动穿透
|
||||
panel.addEventListener('wheel', function (e) {
|
||||
const deltaY = e.deltaY;
|
||||
const scrollTop = panel.scrollTop;
|
||||
const scrollHeight = panel.scrollHeight;
|
||||
const clientHeight = panel.clientHeight;
|
||||
const isAtTop = scrollTop === 0;
|
||||
const isAtBottom = scrollTop + clientHeight >= scrollHeight;
|
||||
if (deltaY < 0 && isAtTop) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (deltaY > 0 && isAtBottom) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
const dl_adaptiveControls = new Set();
|
||||
function dl_registerAdaptive(cbid) {
|
||||
dl_adaptiveControls.add(cbid);
|
||||
dl_adaptiveStyle(cbid);
|
||||
}
|
||||
let dl_adaptiveTicking = false;
|
||||
window.addEventListener("resize", () => {
|
||||
if (!dl_adaptiveTicking) {
|
||||
dl_adaptiveTicking = true;
|
||||
requestAnimationFrame(() => {
|
||||
dl_adaptiveControls.forEach(cbid => {
|
||||
dl_adaptiveStyle(cbid);
|
||||
});
|
||||
dl_adaptiveTicking = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
//]]>
|
||||
</script>
|
||||
@@ -1,150 +0,0 @@
|
||||
<%+cbi/valueheader%>
|
||||
<%
|
||||
-- Template Developers:
|
||||
-- - lwb1978
|
||||
-- - snowie2000
|
||||
-- Copyright: copyright(c)2025–2027
|
||||
-- Description: Passwall(2) UI template
|
||||
|
||||
local json = require "luci.jsonc"
|
||||
|
||||
-- 读取值(keylist/vallist/group)
|
||||
local values = {}
|
||||
for i, key in pairs(self.keylist) do
|
||||
values[#values + 1] = {
|
||||
key = key,
|
||||
label = self.vallist[i] or key,
|
||||
group = self.group and self.group[i] or nil
|
||||
}
|
||||
end
|
||||
|
||||
-- 获取当前配置值(单值)
|
||||
local current_key = nil
|
||||
local cval = self:cfgvalue(section)
|
||||
if type(cval) == "table" then
|
||||
-- 若意外为table,取第一个
|
||||
for k,_ in pairs(cval) do
|
||||
current_key = k
|
||||
break
|
||||
end
|
||||
elseif type(cval) == "string" then
|
||||
current_key = (cval:match("%S+")) -- 取第一个 token
|
||||
end
|
||||
|
||||
-- 分离无组节点(ungrouped)与有组节点(grouped),并保持原有顺序
|
||||
local ungrouped = {}
|
||||
local groups = {}
|
||||
local group_order = {}
|
||||
for _, item in ipairs(values) do
|
||||
if not item.group or item.group == "" then
|
||||
table.insert(ungrouped, item)
|
||||
else
|
||||
local g = item.group
|
||||
if not groups[g] then
|
||||
groups[g] = {}
|
||||
table.insert(group_order, g)
|
||||
end
|
||||
table.insert(groups[g], item)
|
||||
end
|
||||
end
|
||||
|
||||
-- 如果没有配置值,默认取第一个(优先无组第一项,否则第一组第一项)
|
||||
if not current_key then
|
||||
if #ungrouped > 0 then
|
||||
current_key = ungrouped[1].key
|
||||
else
|
||||
-- 找到 group_order 第一个有节点的项
|
||||
for _, g in ipairs(group_order) do
|
||||
if groups[g] and #groups[g] > 0 then
|
||||
current_key = groups[g][1].key
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 获取当前 label(用于主控件显示)
|
||||
local function find_label_by_key(k)
|
||||
if not k then return "" end
|
||||
for _, v in ipairs(values) do
|
||||
if v.key == k then return v.label end
|
||||
end
|
||||
return ""
|
||||
end
|
||||
local current_label = find_label_by_key(current_key) or ""
|
||||
|
||||
-- 打包数据给 JS
|
||||
local dropdown_data = {
|
||||
current_key = current_key,
|
||||
current_label = current_label,
|
||||
ungrouped = ungrouped,
|
||||
groups = groups,
|
||||
group_order = group_order,
|
||||
cbid = cbid,
|
||||
}
|
||||
%>
|
||||
<script>
|
||||
window.lv_dropdown_data = window.lv_dropdown_data || {};
|
||||
window.lv_dropdown_data["<%=cbid%>"] = <%=json.stringify(dropdown_data)%>;
|
||||
</script>
|
||||
|
||||
<div id="<%=cbid%>.main" class="lv-dropdown-container">
|
||||
<!-- 隐藏 select(保存实际配置值) -->
|
||||
<div style="display:none !important;">
|
||||
<select id="<%=cbid%>" name="<%=cbid%>" class="cbi-input-select" data-update="change" style="display:none !important;">
|
||||
<option value="<%=current_key%>" selected="selected">placeholder</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- 模拟 ListValue 控件外观 -->
|
||||
<div class="cbi-input-value cbi-input-select lv-dropdown-display" id="<%=cbid%>.display" tabindex="0">
|
||||
<span id="<%=cbid%>.label" class="lv-dropdown-label" title="<%=pcdata(current_label)%>">
|
||||
<%=pcdata("( "..translate("Not Set").." )")%>
|
||||
</span>
|
||||
<span class="lv-arrow-down"></span>
|
||||
</div>
|
||||
<!-- 下拉面板 -->
|
||||
<div id="<%=cbid%>.panel" class="cbi-listvalue-panel lv-dropdown-panel" style="display:none;">
|
||||
<!-- 搜索框 -->
|
||||
<div style="padding:8px;border-bottom:1px solid #f0f0f0;">
|
||||
<input id="<%=cbid%>.search" class="cbi-input-text lv-dropdown-search" type="text" placeholder="🔍 <%:Search nodes...%>" inputmode="search" enterkeyhint="done" />
|
||||
</div>
|
||||
<!-- 列表容器 -->
|
||||
<div id="<%=cbid%>.list" style="padding:8px;">
|
||||
<!-- 首次点击 display 时由 JS 填充 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
(function(){
|
||||
const cbid = "<%=cbid%>";
|
||||
const hiddenSelect = document.getElementById(cbid);
|
||||
const panel = document.getElementById(cbid + ".panel");
|
||||
const display = document.getElementById(cbid + ".display");
|
||||
const labelSpan = document.getElementById(cbid + ".label");
|
||||
const searchInput = document.getElementById(cbid + ".search");
|
||||
const listContainer = document.getElementById(cbid + ".list");
|
||||
|
||||
// 点击 display
|
||||
display.addEventListener("click", function(e){
|
||||
e.stopPropagation();
|
||||
lv_render_dropdown_list(cbid,panel,listContainer,hiddenSelect,labelSpan,searchInput,display);
|
||||
document.querySelectorAll(".cbi-listvalue-panel").forEach(p=>{
|
||||
if (p !== panel) p.style.display = "none";
|
||||
});
|
||||
if (panel.style.display !== "none") {
|
||||
lv_closePanel(cbid,panel,listContainer,hiddenSelect,searchInput);
|
||||
} else {
|
||||
lv_openPanel(cbid,display,panel,listContainer,hiddenSelect,searchInput);
|
||||
}
|
||||
});
|
||||
lv_registerAdaptive(cbid);
|
||||
setTimeout(() => {
|
||||
labelSpan.textContent = lv_ellipsisByWidth(cbid, '<%=pcdata(current_label ~= "" and current_label or ("( "..translate("Not Set").." )"))%>');
|
||||
}, 100);
|
||||
})();
|
||||
//]]>
|
||||
</script>
|
||||
|
||||
<%+cbi/valuefooter%>
|
||||
@@ -1,758 +0,0 @@
|
||||
<%
|
||||
-- Template Developers:
|
||||
-- - lwb1978
|
||||
-- - snowie2000
|
||||
-- Copyright: copyright(c)2025–2027
|
||||
-- Description: Passwall(2) UI template
|
||||
-- It is the common part of the template and cannot be used independently
|
||||
%>
|
||||
<style>
|
||||
/* 主下拉按钮的下箭头 */
|
||||
.lv-arrow-down {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 6px solid #666;
|
||||
margin-left: 6px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* 组标题的右箭头(折叠) */
|
||||
.lv-arrow-right {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-left: 5px solid #555;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* 组标题的下箭头(展开) */
|
||||
.lv-arrow-down-small {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 5px solid #555;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* 基础列表项样式 */
|
||||
.cbi-listvalue-panel li[data-key] {
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
/* 鼠标悬停效果 - 使用透明度避免覆盖问题 */
|
||||
.cbi-listvalue-panel li[data-key]:hover {
|
||||
background-color: rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
/* 选中项样式 - 使用更高优先级 */
|
||||
.cbi-listvalue-panel li[data-key].is-selected {
|
||||
background-color: #007bff !important;
|
||||
color: white !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
/* 选中项悬停时保持主色调 */
|
||||
.cbi-listvalue-panel li[data-key].is-selected:hover {
|
||||
background-color: #0056b3 !important;
|
||||
}
|
||||
|
||||
.lv-dropdown-container {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
min-width: 220px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@media (max-width: 1152px) {
|
||||
.lv-dropdown-container {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.lv-dropdown-container {
|
||||
display: block;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.lv-dropdown-display {
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.lv-dropdown-label {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
.lv-dropdown-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2147483647;
|
||||
border: 1px solid #dcdcdc;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 6px 18px rgba(0,0,0,0.08);
|
||||
max-height: 50vh;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.lv-dropdown-search {
|
||||
width: 100%;
|
||||
max-width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
box-sizing: border-box;
|
||||
padding: 6px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.lv-group-title {
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.lv-group-list {
|
||||
list-style: none;
|
||||
margin: 6px 0 0 8px;
|
||||
padding: 0;
|
||||
}
|
||||
.lv-group-item {
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: left !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
// css helper functions
|
||||
function lv_camelToKebab(str) {
|
||||
return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase()
|
||||
}
|
||||
|
||||
function lv_style2Css(styleDeclaration, properties) {
|
||||
const cssRules = properties.map(prop => {
|
||||
const kebabCaseProp = lv_camelToKebab(prop);[1, 5]
|
||||
const value = styleDeclaration[prop]
|
||||
if (value) {
|
||||
return `${kebabCaseProp}: ${value};`
|
||||
}
|
||||
return ''
|
||||
})
|
||||
// Filter out any empty strings and join the rules
|
||||
return cssRules.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
const lv_parseColorToRgba = (function() {
|
||||
// Create canvas and context once (Closure)
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
return function(colorStr) {
|
||||
if (!colorStr)
|
||||
return null;
|
||||
ctx.clearRect(0, 0, 1, 1);
|
||||
// 2. Apply the color
|
||||
ctx.fillStyle = colorStr;
|
||||
// 3. Fill a single pixel
|
||||
ctx.fillRect(0, 0, 1, 1);
|
||||
// 4. Extract pixel data [R, G, B, A]
|
||||
const data = ctx.getImageData(0, 0, 1, 1).data;
|
||||
return {
|
||||
r: data[0],
|
||||
g: data[1],
|
||||
b: data[2],
|
||||
// Convert alpha from 0-255 to 0-1 (rounded to 3 decimal places)
|
||||
a: Math.round((data[3] / 255) * 1000) / 1000
|
||||
};
|
||||
};
|
||||
})();
|
||||
|
||||
// Helper to convert back to Hex (for output consistency)
|
||||
function lv_rgbToHex(r, g, b) {
|
||||
const toHex = (n) => {
|
||||
const hex = Math.max(0, Math.min(255, n)).toString(16)
|
||||
return hex.length === 1 ? '0' + hex : hex
|
||||
}
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
||||
}
|
||||
|
||||
function lv_isTransparent(color) {
|
||||
const cleanColor = lv_parseColorToRgba(color);
|
||||
// check #RRGGBBAA for transparency
|
||||
return !cleanColor || (cleanColor.a !== undefined && !cleanColor.a);
|
||||
}
|
||||
|
||||
function lv_getColorSchema(color) {
|
||||
const rgb = lv_parseColorToRgba(color);
|
||||
if (!rgb) return 'unknown'; // Handle invalid colors
|
||||
// Calculate YIQ brightness (human eye perception)
|
||||
const brightness = ((rgb.r * 299) + (rgb.g * 587) + (rgb.b * 114)) / 1000;
|
||||
|
||||
return brightness > 128 ? 'light' : 'dark';
|
||||
}
|
||||
|
||||
function lv_lighter(color, amount) {
|
||||
const rgb = lv_parseColorToRgba(color);
|
||||
if (!rgb) return color;
|
||||
// Add amount to each channel
|
||||
const r = rgb.r + amount;
|
||||
const g = rgb.g + amount;
|
||||
const b = rgb.b + amount;
|
||||
// Convert back to Hex (clamping happens inside rgbToHex)
|
||||
return lv_rgbToHex(r, g, b);
|
||||
}
|
||||
|
||||
function lv_darker(color, amount) {
|
||||
const rgb = lv_parseColorToRgba(color);
|
||||
if (!rgb) return color;
|
||||
// Subtract amount from each channel
|
||||
const r = rgb.r - amount;
|
||||
const g = rgb.g - amount;
|
||||
const b = rgb.b - amount;
|
||||
|
||||
return lv_rgbToHex(r, g, b);
|
||||
}
|
||||
|
||||
// copy select styles
|
||||
function lv_adaptiveStyle(cbid) {
|
||||
const display = document.getElementById(cbid + ".display");
|
||||
const hiddenSelect = document.getElementById(cbid);
|
||||
const panel = document.getElementById(cbid + ".panel");
|
||||
if (hiddenSelect && display) {
|
||||
const elOption = hiddenSelect.getElementsByTagName("option")[0]
|
||||
const styleSelect = window.getComputedStyle(hiddenSelect)
|
||||
const styleOption = window.getComputedStyle(elOption)
|
||||
const styleBody = window.getComputedStyle(document.body)
|
||||
|
||||
const styleNode = document.createElement('style')
|
||||
const styleNames = ["width", "color", "height", "padding", "margin", "lineHeight", "border", "borderRadius", "minWidth", "minHeight"]
|
||||
if (styleSelect.borderBottomStyle !== "none") {
|
||||
styleNames.push("borderBottomWidth", "borderBottomStyle", "borderBottomColor");
|
||||
}
|
||||
document.head.appendChild(styleNode)
|
||||
// trace back from option -> select -> body for background color
|
||||
const panelRadius = styleSelect.borderRadius;
|
||||
const optionColor = !lv_isTransparent(styleOption.backgroundColor) ? styleOption.backgroundColor : !lv_isTransparent(styleSelect.backgroundColor) ? styleSelect.backgroundColor : styleBody.backgroundColor
|
||||
const titleColor = lv_getColorSchema(optionColor) === "light" ? lv_darker(optionColor, 30) : lv_lighter(optionColor, 30)
|
||||
const selectStyleCSS = [`#${CSS.escape(cbid + ".display")} {`, lv_style2Css(styleSelect, styleNames), lv_style2Css(styleSelect, ["backgroundColor"]), "}"]
|
||||
const optionStyleCSS = [`#${CSS.escape(cbid + ".panel")} {`, lv_style2Css(styleOption, styleNames), `background-color: ${optionColor};`, `border-radius: ${panelRadius};`, "}"]
|
||||
const titleStyleCSS = [`#${CSS.escape(cbid + ".panel")} .lv-group-title {`, `background-color: ${titleColor} !important;`, "}"]
|
||||
styleNode.textContent = [].concat(selectStyleCSS, optionStyleCSS, titleStyleCSS).join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
function lv_idSafe(id) {
|
||||
return id
|
||||
.trim()
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/[\x00-\x1F\x7F]/g, "");
|
||||
}
|
||||
|
||||
// 高亮当前选中的项
|
||||
function lv_highlightSelectedItem(listContainer, hiddenSelect) {
|
||||
const allItems = listContainer.querySelectorAll("li[data-key]");
|
||||
const currentKey = hiddenSelect.options[0].value;
|
||||
allItems.forEach(item => {
|
||||
item.classList.remove("is-selected");
|
||||
if (item.getAttribute('data-key') === currentKey) {
|
||||
item.classList.add("is-selected");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新组内选中计数
|
||||
function lv_updateGroupCounts(cbid, listContainer, hiddenSelect, searchInput) {
|
||||
const groups = listContainer.querySelectorAll(".lv-group");
|
||||
const currentKey = hiddenSelect.options[0].value;
|
||||
const isSearching = searchInput.value.trim() !== "";
|
||||
groups.forEach(group => {
|
||||
const gname = group.getAttribute("data-group");
|
||||
const items = group.querySelectorAll("li[data-key]");
|
||||
const span = document.getElementById("group-count-" + cbid + "-" + gname);
|
||||
if (!span) return;
|
||||
if (isSearching) {
|
||||
// 搜索状态:显示匹配数量
|
||||
let matchCount = 0;
|
||||
items.forEach(li => {
|
||||
if (li.style.display !== "none") matchCount++;
|
||||
});
|
||||
span.textContent = "(" + matchCount + "/" + items.length + ")";
|
||||
if (matchCount > 0) {
|
||||
span.style.color = "#28a745";
|
||||
span.style.fontWeight = "600";
|
||||
} else {
|
||||
span.style.color = "#dc3545";
|
||||
span.style.fontWeight = "normal";
|
||||
}
|
||||
} else {
|
||||
// 默认状态:显示选中项数量
|
||||
let selectedCount = 0;
|
||||
items.forEach(li => {
|
||||
if (li.getAttribute('data-key') === currentKey) {
|
||||
selectedCount = 1;
|
||||
}
|
||||
});
|
||||
span.textContent = "(" + selectedCount + "/" + items.length + ")";
|
||||
if (selectedCount > 0) {
|
||||
span.style.color = "#007bff";
|
||||
span.style.fontWeight = "600";
|
||||
} else {
|
||||
span.style.color = "";
|
||||
span.style.fontWeight = "normal";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//搜索过滤器:按 name 或 label 做模糊匹配,搜索时自动展开所有组并隐藏不匹配条目
|
||||
function lv_filterList(keyword, cbid, listContainer, hiddenSelect, searchInput) {
|
||||
keyword = (keyword || "").toLowerCase().trim();
|
||||
const topItems = listContainer.querySelectorAll("ul li[data-key]");
|
||||
topItems.forEach(li=>{
|
||||
const name = (li.getAttribute("data-node-name") || "").toLowerCase();
|
||||
if (!keyword || name.indexOf(keyword) !== -1) {
|
||||
li.style.display = "block";
|
||||
} else {
|
||||
li.style.display = "none";
|
||||
}
|
||||
});
|
||||
const groups = listContainer.querySelectorAll(".lv-group");
|
||||
groups.forEach(group=>{
|
||||
const items = group.querySelectorAll("li[data-key]");
|
||||
let matchCount = 0;
|
||||
items.forEach(li=>{
|
||||
const name = (li.getAttribute("data-node-name") || "").toLowerCase();
|
||||
if (!keyword || name.indexOf(keyword) !== -1) {
|
||||
li.style.display = "block";
|
||||
matchCount++;
|
||||
} else {
|
||||
li.style.display = "none";
|
||||
}
|
||||
});
|
||||
group.style.display = (matchCount === 0 && keyword !== "") ? "none" : "block";
|
||||
const ul = group.querySelector(".lv-group-list");
|
||||
const gname = group.getAttribute("data-group");
|
||||
const arrow = document.getElementById("arrow-" + cbid + "-" + gname);
|
||||
if (keyword) {
|
||||
if (ul) ul.style.display = (matchCount > 0 ? "block" : "none");
|
||||
if (arrow) arrow.className = (matchCount > 0 ? "lv-arrow-down-small" : "lv-arrow-right");
|
||||
} else {
|
||||
if (ul) ul.style.display = "none";
|
||||
if (arrow) arrow.className = "lv-arrow-right";
|
||||
}
|
||||
});
|
||||
lv_updateGroupCounts(cbid, listContainer, hiddenSelect, searchInput);
|
||||
lv_highlightSelectedItem(listContainer, hiddenSelect);
|
||||
}
|
||||
|
||||
// 切换单个组(点击组标题)
|
||||
function lv_toggleGroup(listContainer, cbid, g) {
|
||||
g = lv_idSafe(g);
|
||||
const group = listContainer.querySelector(".lv-group[data-group='" + g + "']");
|
||||
if (!group) return;
|
||||
const ul = group.querySelector(".lv-group-list");
|
||||
const arrow = document.getElementById("arrow-" + cbid + "-" + g);
|
||||
if (!ul) return;
|
||||
const searchInput = document.getElementById(cbid + ".search");
|
||||
const isSearching = searchInput?.value.trim() !== "";
|
||||
const isExpanded = ul.style.display !== "none";
|
||||
|
||||
if (isExpanded) {
|
||||
ul.style.display = "none";
|
||||
if (arrow) arrow.className = "lv-arrow-right";
|
||||
} else {
|
||||
ul.style.display = "block";
|
||||
if (arrow) arrow.className = "lv-arrow-down-small";
|
||||
|
||||
if (!isSearching) {
|
||||
const allGroups = listContainer.querySelectorAll(".lv-group");
|
||||
allGroups.forEach(otherGroup => {
|
||||
if (otherGroup !== group) {
|
||||
const otherUl = otherGroup.querySelector(".lv-group-list");
|
||||
const otherGname = otherGroup.getAttribute("data-group");
|
||||
const otherArrow = document.getElementById("arrow-" + cbid + "-" + otherGname);
|
||||
if (otherUl) otherUl.style.display = "none";
|
||||
if (otherArrow) otherArrow.className = "lv-arrow-right";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 展开包含当前 hiddenSelect 值的组(初始化或打开面板时使用)
|
||||
function lv_expandGroupOfCurrent(cbid, listContainer, hiddenSelect) {
|
||||
const key = hiddenSelect.options[0].value;
|
||||
if (!key) return;
|
||||
const targetLi = listContainer.querySelector(`li[data-key='${CSS.escape(key)}']`);
|
||||
if (!targetLi) return;
|
||||
let parentGroup = targetLi.closest(".lv-group");
|
||||
if (parentGroup) {
|
||||
const ul = parentGroup.querySelector(".lv-group-list");
|
||||
const gname = parentGroup.getAttribute("data-group");
|
||||
const arrow = document.getElementById("arrow-" + cbid + "-" + gname);
|
||||
if (ul) ul.style.display = "block";
|
||||
if (arrow) arrow.className = "lv-arrow-down-small";
|
||||
const allGroups = listContainer.querySelectorAll(".lv-group");
|
||||
allGroups.forEach(gp=>{
|
||||
if (gp !== parentGroup) {
|
||||
const gul = gp.querySelector(".lv-group-list");
|
||||
const otherGname = gp.getAttribute("data-group");
|
||||
const gar = document.getElementById("arrow-" + cbid + "-" + otherGname);
|
||||
if (gul) gul.style.display = "none";
|
||||
if (gar) gar.className = "lv-arrow-right";
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const allGroups = listContainer.querySelectorAll(".lv-group");
|
||||
allGroups.forEach(gp=>{
|
||||
const gul = gp.querySelector(".lv-group-list");
|
||||
const gname = gp.getAttribute("data-group");
|
||||
const gar = document.getElementById("arrow-" + cbid + "-" + gname);
|
||||
if (gul) gul.style.display = "none";
|
||||
if (gar) gar.className = "lv-arrow-right";
|
||||
});
|
||||
}
|
||||
if (targetLi) {
|
||||
requestAnimationFrame(() => {
|
||||
targetLi.scrollIntoView({ block: "nearest" });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 计算panel位置
|
||||
function lv_repositionPanel(panel, display) {
|
||||
if (!panel || panel.style.display === "none") return;
|
||||
const rect = display.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
||||
panel.style.visibility = "hidden";
|
||||
panel.style.display = "block";
|
||||
panel.style.minHeight = "100px";
|
||||
panel.style.maxHeight = Math.min(0.5*viewportHeight, 550) + "px";
|
||||
const panelHeight = panel.offsetHeight;
|
||||
const spaceBelow = viewportHeight - rect.bottom;
|
||||
const spaceAbove = rect.top;
|
||||
let top, isUp = false;
|
||||
if (spaceBelow >= panelHeight) {
|
||||
top = rect.bottom + 2;
|
||||
isUp = false;
|
||||
} else if (spaceAbove >= panelHeight) {
|
||||
top = rect.top - panelHeight - 2;
|
||||
isUp = true;
|
||||
} else {
|
||||
if (spaceBelow >= spaceAbove) {
|
||||
top = Math.max(rect.bottom - 2, viewportHeight - panelHeight - 2);
|
||||
isUp = false;
|
||||
} else {
|
||||
top = Math.min(rect.top - panelHeight + 2, 2);
|
||||
isUp = true;
|
||||
}
|
||||
}
|
||||
panel.style.left = rect.left + "px";
|
||||
panel.style.top = top + "px";
|
||||
const panelRect = panel.getBoundingClientRect();
|
||||
const displayWidth = rect.width;
|
||||
const remainingWidth = window.innerWidth - panelRect.left - 12;
|
||||
const maxWidth = Math.max(displayWidth, Math.floor(remainingWidth));
|
||||
panel.style.maxWidth = maxWidth + "px";
|
||||
panel.style.minWidth = displayWidth + "px";
|
||||
panel.style.width = "auto";
|
||||
panel.style.visibility = "";
|
||||
}
|
||||
|
||||
// 打开/关闭面板
|
||||
function lv_openPanel(cbid, display, panel, listContainer, hiddenSelect, searchInput) {
|
||||
if (!panel._moved) {
|
||||
document.body.appendChild(panel);
|
||||
panel._moved = true;
|
||||
}
|
||||
lv_expandGroupOfCurrent(cbid, listContainer, hiddenSelect);
|
||||
lv_highlightSelectedItem(listContainer, hiddenSelect);
|
||||
panel.style.display = "block";
|
||||
lv_repositionPanel(panel, display);
|
||||
// 失焦监听
|
||||
const handler = function(e){
|
||||
const target = e.target;
|
||||
if (panel.style.display !== "none") {
|
||||
if (!panel.contains(target) && !display.contains(target)) {
|
||||
lv_closePanel(cbid, panel, listContainer, hiddenSelect, searchInput, display);
|
||||
}
|
||||
}
|
||||
}
|
||||
panel._docClickHandler = handler;
|
||||
document.addEventListener("click", handler);
|
||||
// 滚动 / resize 自动 reposition
|
||||
let ticking = false;
|
||||
const repositionHandler = function () {
|
||||
if (ticking) return;
|
||||
ticking = true;
|
||||
requestAnimationFrame(function () {
|
||||
ticking = false;
|
||||
lv_repositionPanel(panel, display);
|
||||
});
|
||||
};
|
||||
panel._repositionHandler = repositionHandler;
|
||||
window.addEventListener("scroll", repositionHandler, true);
|
||||
window.addEventListener("resize", repositionHandler);
|
||||
}
|
||||
function lv_closePanel(cbid, panel, listContainer, hiddenSelect, searchInput) {
|
||||
panel.style.display = "none";
|
||||
searchInput.value = "";
|
||||
lv_filterList("", cbid, listContainer, hiddenSelect, searchInput);
|
||||
// document click
|
||||
if (panel._docClickHandler) {
|
||||
document.removeEventListener("click", panel._docClickHandler);
|
||||
panel._docClickHandler = null;
|
||||
}
|
||||
// scroll / resize
|
||||
if (panel._repositionHandler) {
|
||||
window.removeEventListener("scroll", panel._repositionHandler, true);
|
||||
window.removeEventListener("resize", panel._repositionHandler);
|
||||
panel._repositionHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 动态生成下拉框
|
||||
window.lv_dropdown_rendered = {};
|
||||
function lv_escape_html(s) {
|
||||
return s.replace(/[&<>"']/g, c => ({
|
||||
"&":"&", "<":"<", ">":">", '"':""", "'":"'"
|
||||
}[c]));
|
||||
}
|
||||
|
||||
function lv_change(cbid, listContainer, hiddenSelect, labelSpan, searchInput, new_key, new_text) {
|
||||
//改值
|
||||
hiddenSelect.options[0].value = new_key;
|
||||
hiddenSelect.options[0].text = new_key;
|
||||
hiddenSelect.value = new_key;
|
||||
labelSpan.textContent = lv_ellipsisByWidth(cbid, new_text);
|
||||
labelSpan.title = new_text;
|
||||
lv_highlightSelectedItem(listContainer, hiddenSelect);
|
||||
lv_updateGroupCounts(cbid, listContainer, hiddenSelect, searchInput);
|
||||
}
|
||||
|
||||
function lv_render_dropdown_list(cbid, panel, listContainer, hiddenSelect, labelSpan, searchInput, display) {
|
||||
if (window.lv_dropdown_rendered[cbid]) return;
|
||||
const data = window.lv_dropdown_data[cbid];
|
||||
if (!data) return;
|
||||
if (!listContainer) return;
|
||||
|
||||
let html = "";
|
||||
|
||||
// 无组项
|
||||
if (data.ungrouped && data.ungrouped.length > 0) {
|
||||
html += `<ul style="list-style:none;padding:0;margin:0 0 8px 0;">`;
|
||||
|
||||
data.ungrouped.forEach(item => {
|
||||
html += `
|
||||
<li data-key="${item.key}"
|
||||
data-node-name="${lv_escape_html(item.label.toLowerCase())}"
|
||||
class="list-item">
|
||||
<span class="lv-item-label" style="margin-left:12px;">
|
||||
${lv_escape_html(item.label)}
|
||||
</span>
|
||||
</li>`;
|
||||
});
|
||||
|
||||
html += `</ul>`;
|
||||
}
|
||||
|
||||
// 分组项
|
||||
data.group_order.forEach(gname => {
|
||||
const items = data.groups[gname];
|
||||
|
||||
html += `
|
||||
<div class="lv-group" data-group="${lv_idSafe(gname)}" style="margin-bottom:8px;">
|
||||
<div class="lv-group-title" data-group-name="${lv_idSafe(gname)}">
|
||||
<span class="lv-arrow-right" id="arrow-${cbid}-${lv_idSafe(gname)}"></span>
|
||||
<b style="margin-left:6px;">${lv_escape_html(gname)}</b>
|
||||
<span id="group-count-${cbid}-${lv_idSafe(gname)}"
|
||||
style="margin-left:8px;">(0/${items.length})</span>
|
||||
</div>
|
||||
<ul id="group-${cbid}-${lv_idSafe(gname)}" class="lv-group-list" style="display:none">
|
||||
`;
|
||||
|
||||
items.forEach(item => {
|
||||
html += `
|
||||
<li data-key="${item.key}"
|
||||
data-node-name="${lv_escape_html(item.label.toLowerCase())}"
|
||||
class="lv-group-item">
|
||||
<span class="lv-item-label" title="${lv_escape_html(item.label)}">
|
||||
${lv_escape_html(item.label)}
|
||||
</span>
|
||||
</li>`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
listContainer.innerHTML = html;
|
||||
|
||||
window.lv_dropdown_rendered[cbid] = true;
|
||||
|
||||
lv_adaptiveStyle(cbid);
|
||||
lv_updateGroupCounts(cbid, listContainer, hiddenSelect, searchInput);
|
||||
|
||||
// 点击项(无组与组内项都使用 li[data-key])
|
||||
listContainer.addEventListener("click", function(e){
|
||||
let li = e.target;
|
||||
while(li && li !== listContainer && !li.hasAttribute('data-key')) li = li.parentNode;
|
||||
if(!li || li === listContainer) return;
|
||||
const key = li.getAttribute('data-key') || "";
|
||||
const text = li.querySelector(".lv-item-label")?.textContent || li.textContent || key;
|
||||
|
||||
const changed = key !== hiddenSelect.value;
|
||||
if (changed) {
|
||||
//改值
|
||||
lv_change(cbid, listContainer, hiddenSelect, labelSpan, searchInput, key, text);
|
||||
}
|
||||
lv_closePanel(cbid,panel,listContainer,hiddenSelect,searchInput);
|
||||
if (changed) {
|
||||
setTimeout(() => {
|
||||
hiddenSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
// 搜索功能
|
||||
searchInput.addEventListener("input", function() {
|
||||
lv_filterList(this.value, cbid, listContainer, hiddenSelect, searchInput);
|
||||
lv_repositionPanel(panel, display);
|
||||
});
|
||||
searchInput.addEventListener('keydown', function(e) {
|
||||
const isEnter = e.key === "Enter" || e.keyCode === 13;
|
||||
if (!isEnter) return;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
searchInput.blur();
|
||||
});
|
||||
|
||||
// 切换组
|
||||
listContainer.querySelectorAll(".lv-group-title").forEach(title => {
|
||||
title.addEventListener("click", function() {
|
||||
const g = this.closest(".lv-group")?.getAttribute("data-group");
|
||||
if (g) {
|
||||
lv_toggleGroup(listContainer, cbid, g);
|
||||
lv_repositionPanel(panel, display);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 防止 panel 惯性滚动穿透
|
||||
panel.addEventListener('wheel', function (e) {
|
||||
const deltaY = e.deltaY;
|
||||
const scrollTop = panel.scrollTop;
|
||||
const scrollHeight = panel.scrollHeight;
|
||||
const clientHeight = panel.clientHeight;
|
||||
const isAtTop = scrollTop === 0;
|
||||
const isAtBottom = scrollTop + clientHeight >= scrollHeight;
|
||||
if (deltaY < 0 && isAtTop) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (deltaY > 0 && isAtBottom) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
//截断display字符长度
|
||||
window.lv_labelSpan_maxWidth = {};
|
||||
function lv_ellipsisByWidth(cbid, text) {
|
||||
const el = document.getElementById(cbid + ".label");
|
||||
if (!el || !text) return text;
|
||||
text = text.trim();
|
||||
const maxWidth = el.clientWidth;
|
||||
window.lv_labelSpan_maxWidth[cbid] = maxWidth;
|
||||
if (maxWidth <= 0) return text;
|
||||
const style = window.getComputedStyle(el);
|
||||
const font = [
|
||||
style.fontStyle,
|
||||
style.fontVariant,
|
||||
style.fontWeight,
|
||||
style.fontSize || '16px',
|
||||
style.fontFamily || 'sans-serif'
|
||||
].join(" ").replace(/\s+/g, ' ');
|
||||
const canvas = lv_ellipsisByWidth._canvas || (lv_ellipsisByWidth._canvas = document.createElement("canvas"));
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.font = font;
|
||||
if (ctx.measureText(text).width <= maxWidth) {
|
||||
return text;
|
||||
}
|
||||
const ellipsis = "...";
|
||||
const ellipsisWidth = ctx.measureText(ellipsis).width;
|
||||
const minChars = 15;
|
||||
const probe = text.slice(0, minChars);
|
||||
const probeWidth = ctx.measureText(probe).width;
|
||||
if (probeWidth + ellipsisWidth > maxWidth) {
|
||||
return text;
|
||||
}
|
||||
let left = 0;
|
||||
let right = text.length;
|
||||
let result = ellipsis;
|
||||
while (left <= right) {
|
||||
const mid = (left + right) >> 1;
|
||||
const substr = text.slice(0, mid);
|
||||
const w = ctx.measureText(substr).width;
|
||||
if (w + ellipsisWidth <= maxWidth) {
|
||||
result = substr + ellipsis;
|
||||
left = mid + 1;
|
||||
} else {
|
||||
right = mid - 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const lv_adaptiveControls = new Set();
|
||||
function lv_registerAdaptive(cbid) {
|
||||
lv_adaptiveControls.add(cbid);
|
||||
lv_adaptiveStyle(cbid);
|
||||
}
|
||||
function lv_labelSpanResize(cbid) {
|
||||
const el = document.getElementById(cbid + ".label");
|
||||
if (!el) return;
|
||||
const maxWidth = el.clientWidth;
|
||||
if (window.lv_labelSpan_maxWidth[cbid] == maxWidth) return;
|
||||
let text = el.title;
|
||||
el.textContent = lv_ellipsisByWidth(cbid, text);
|
||||
}
|
||||
let lv_adaptiveTicking = false;
|
||||
window.addEventListener("resize", () => {
|
||||
if (!lv_adaptiveTicking) {
|
||||
lv_adaptiveTicking = true;
|
||||
requestAnimationFrame(() => {
|
||||
lv_adaptiveControls.forEach(cbid => {
|
||||
lv_adaptiveStyle(cbid);
|
||||
lv_labelSpanResize(cbid);
|
||||
});
|
||||
lv_adaptiveTicking = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
//]]>
|
||||
</script>
|
||||
@@ -1,134 +0,0 @@
|
||||
<%+cbi/valueheader%>
|
||||
<%
|
||||
-- Template Developers:
|
||||
-- - lwb1978
|
||||
-- Copyright: copyright(c)2025–2027
|
||||
-- Description: Passwall(2) UI template
|
||||
local json = require "luci.jsonc"
|
||||
local cbid = "cbid." .. self.config .. "." .. section .. "." .. self.option
|
||||
|
||||
-- 读取 MultiValue
|
||||
local values = {}
|
||||
for i, key in pairs(self.keylist) do
|
||||
values[#values + 1] = {
|
||||
key = key,
|
||||
label = self.vallist[i] or key,
|
||||
group = self.group and self.group[i] or nil
|
||||
}
|
||||
end
|
||||
|
||||
-- 获取选中值
|
||||
local selected = {}
|
||||
local cval = self:cfgvalue(section)
|
||||
if type(cval) == "table" then
|
||||
for _, v in pairs(cval) do
|
||||
selected[v] = true
|
||||
end
|
||||
elseif type(cval) == "string" then
|
||||
for v in cval:gmatch("%S+") do
|
||||
selected[v] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- 按原顺序分组
|
||||
local groups = {}
|
||||
local group_order = {}
|
||||
for _, item in ipairs(values) do
|
||||
local g = item.group
|
||||
if not g or g == "" then
|
||||
g = translate("default")
|
||||
end
|
||||
if not groups[g] then
|
||||
groups[g] = {}
|
||||
table.insert(group_order, g)
|
||||
end
|
||||
table.insert(groups[g], item)
|
||||
end
|
||||
|
||||
local total_count = #values
|
||||
local selected_count = 0
|
||||
for _, item in ipairs(values) do
|
||||
if selected[item.key] then
|
||||
selected_count = selected_count + 1
|
||||
end
|
||||
end
|
||||
|
||||
local function idSafe(id)
|
||||
return (id
|
||||
:gsub("^%s+", "")
|
||||
:gsub("%s+$", "")
|
||||
:gsub("%s+", "-")
|
||||
:gsub("[%z\1-\31\127]", "")
|
||||
)
|
||||
end
|
||||
%>
|
||||
|
||||
<div id="<%=cbid%>" class="cbi-input-select" style="display:inline-block;">
|
||||
<div style="display:none !important;">
|
||||
<select id="<%=cbid%>.ref" class="cbi-input-select" style="display:none !important;">
|
||||
<option value>placeholder</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- 搜索框 -->
|
||||
<input type="text" id="<%=cbid%>.search"
|
||||
class="mv_search_input cbi-input-text"
|
||||
placeholder="🔍 <%:Search nodes...%>" inputmode="search" enterkeyhint="done" />
|
||||
<!-- 主容器 -->
|
||||
<div class="mv_list_container" id="<%=cbid%>.panel">
|
||||
<ul class="cbi-multi mv_node_list" id="<%=cbid%>.node_list">
|
||||
<% for _, gname in ipairs(group_order) do local items = groups[gname] %>
|
||||
<li class="group-block" data-group="<%=idSafe(gname)%>">
|
||||
<!-- 组标题 -->
|
||||
<div class="group-title">
|
||||
<span id="arrow-<%=self.option%>-<%=idSafe(gname)%>" class="mv-arrow-right"></span>
|
||||
<b style="margin-left:8px;"><%=pcdata(gname)%></b>
|
||||
<%
|
||||
local g_selected = 0
|
||||
for _, it in ipairs(items) do
|
||||
if selected[it.key] then
|
||||
g_selected = g_selected + 1
|
||||
end
|
||||
end
|
||||
%>
|
||||
<span id="group-count-<%=self.option%>-<%=idSafe(gname)%>" style="margin-left:8px;">
|
||||
(<%=g_selected%>/<%=#items%>)
|
||||
</span>
|
||||
</div>
|
||||
<!-- 组内容 -->
|
||||
<ul id="group-<%=self.option%>-<%=idSafe(gname)%>" class="group-items" style="display:none;"
|
||||
data-items='<%=json.stringify(items)%>'
|
||||
data-selected='<%=json.stringify(selected)%>'>
|
||||
</ul>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- 底部控制栏 -->
|
||||
<div class="mv-controls">
|
||||
<input class="btn cbi-button cbi-button-edit" type="button" onclick="mv_selectAll('<%=cbid%>','<%=self.option%>',true)" value="<%:Select all%>">
|
||||
<input class="btn cbi-button cbi-button-edit" type="button" onclick="mv_selectAll('<%=cbid%>','<%=self.option%>',false)" value="<%:DeSelect all%>">
|
||||
<span id="count-<%=self.option%>" style="color:#666;"><%:Selected:%> <span style='color:red;'><%=selected_count%>/<%=total_count%></span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
(function(){
|
||||
const cbid = "<%=cbid%>";
|
||||
const opt = "<%=self.option%>";
|
||||
const searchInput = document.getElementById(cbid + ".search");
|
||||
const nodeList = document.getElementById(cbid + ".node_list");
|
||||
|
||||
nodeList.querySelectorAll(".group-title").forEach(title => {
|
||||
title.addEventListener("click", function() {
|
||||
const g = this.closest(".group-block")?.getAttribute("data-group");
|
||||
if (g) mv_toggleGroup(opt, nodeList, searchInput, g);
|
||||
});
|
||||
});
|
||||
|
||||
mv_multivalue_init(cbid, opt, nodeList, searchInput)
|
||||
})();
|
||||
//]]>
|
||||
</script>
|
||||
|
||||
<%+cbi/valuefooter%>
|
||||
@@ -1,460 +0,0 @@
|
||||
<%
|
||||
-- Template Developers:
|
||||
-- - lwb1978
|
||||
-- Copyright: copyright(c)2025–2027
|
||||
-- Description: Passwall(2) UI template
|
||||
%>
|
||||
|
||||
<style>
|
||||
/* 组标题的右箭头 */
|
||||
.mv-arrow-right {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-left: 5px solid #555;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* 组标题的下箭头 */
|
||||
.mv-arrow-down-small {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 5px solid #555;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.mv_search_input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 6px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
max-height: 36px;
|
||||
}
|
||||
.mv_list_container {
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
margin-bottom: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mv_node_list {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
.mv_node_list li.group-block {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
.mv_node_list .group-title {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mv_node_list ul.group-items {
|
||||
margin: 0 0 8px 16px;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.mv_node_list ul.group-items li.node-item {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
}
|
||||
.mv-node-row {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.mv-node-checkbox {
|
||||
margin: 0;
|
||||
vertical-align: middle;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.mv-node-label {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
}
|
||||
.mv-controls {
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
// css helper functions
|
||||
function mv_camelToKebab(str) {
|
||||
return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase()
|
||||
}
|
||||
|
||||
function mv_style2Css(styleDeclaration, properties) {
|
||||
const cssRules = properties.map(prop => {
|
||||
const kebabCaseProp = mv_camelToKebab(prop);[1, 5]
|
||||
const value = styleDeclaration[prop]
|
||||
if (value) {
|
||||
return `${kebabCaseProp}: ${value};`
|
||||
}
|
||||
return ''
|
||||
})
|
||||
// Filter out any empty strings and join the rules
|
||||
return cssRules.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
const mv_parseColorToRgba = (function() {
|
||||
// Create canvas and context once (Closure)
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
return function(colorStr) {
|
||||
if (!colorStr)
|
||||
return null;
|
||||
ctx.clearRect(0, 0, 1, 1);
|
||||
// 2. Apply the color
|
||||
ctx.fillStyle = colorStr;
|
||||
// 3. Fill a single pixel
|
||||
ctx.fillRect(0, 0, 1, 1);
|
||||
// 4. Extract pixel data [R, G, B, A]
|
||||
const data = ctx.getImageData(0, 0, 1, 1).data;
|
||||
return {
|
||||
r: data[0],
|
||||
g: data[1],
|
||||
b: data[2],
|
||||
// Convert alpha from 0-255 to 0-1 (rounded to 3 decimal places)
|
||||
a: Math.round((data[3] / 255) * 1000) / 1000
|
||||
};
|
||||
};
|
||||
})();
|
||||
|
||||
// Helper to convert back to Hex (for output consistency)
|
||||
function mv_rgbToHex(r, g, b) {
|
||||
const toHex = (n) => {
|
||||
const hex = Math.max(0, Math.min(255, n)).toString(16)
|
||||
return hex.length === 1 ? '0' + hex : hex
|
||||
}
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
||||
}
|
||||
|
||||
function mv_isTransparent(color) {
|
||||
const cleanColor = mv_parseColorToRgba(color);
|
||||
// check #RRGGBBAA for transparency
|
||||
return !cleanColor || (cleanColor.a !== undefined && !cleanColor.a);
|
||||
}
|
||||
|
||||
function mv_getColorSchema(color) {
|
||||
const rgb = mv_parseColorToRgba(color);
|
||||
if (!rgb) return 'unknown'; // Handle invalid colors
|
||||
// Calculate YIQ brightness (human eye perception)
|
||||
const brightness = ((rgb.r * 299) + (rgb.g * 587) + (rgb.b * 114)) / 1000;
|
||||
|
||||
return brightness > 128 ? 'light' : 'dark';
|
||||
}
|
||||
|
||||
function mv_lighter(color, amount) {
|
||||
const rgb = mv_parseColorToRgba(color);
|
||||
if (!rgb) return color;
|
||||
// Add amount to each channel
|
||||
const r = rgb.r + amount;
|
||||
const g = rgb.g + amount;
|
||||
const b = rgb.b + amount;
|
||||
// Convert back to Hex (clamping happens inside rgbToHex)
|
||||
return mv_rgbToHex(r, g, b);
|
||||
}
|
||||
|
||||
function mv_darker(color, amount) {
|
||||
const rgb = mv_parseColorToRgba(color);
|
||||
if (!rgb) return color;
|
||||
// Subtract amount from each channel
|
||||
const r = rgb.r - amount;
|
||||
const g = rgb.g - amount;
|
||||
const b = rgb.b - amount;
|
||||
|
||||
return mv_rgbToHex(r, g, b);
|
||||
}
|
||||
|
||||
// copy select styles
|
||||
function mv_adaptiveStyle(cbid) {
|
||||
const mainDiv = document.getElementById(cbid);
|
||||
const hiddenRef = document.getElementById(cbid + ".ref");
|
||||
const panel = document.getElementById(cbid + ".panel");
|
||||
if (hiddenRef && mainDiv) {
|
||||
const elOption = hiddenRef.getElementsByTagName("option")[0]
|
||||
const styleSelect = window.getComputedStyle(hiddenRef)
|
||||
const styleOption = window.getComputedStyle(elOption)
|
||||
const styleBody = window.getComputedStyle(document.body)
|
||||
|
||||
const styleNode = document.createElement('style')
|
||||
const styleNames = ["width", "color", "height", "margin", "lineHeight", "borderRadius", "minWidth", "minHeight"]
|
||||
|
||||
document.head.appendChild(styleNode)
|
||||
// trace back from option -> select -> body for background color
|
||||
const panelRadius = styleSelect.borderRadius;
|
||||
const optionColor = !mv_isTransparent(styleOption.backgroundColor) ? styleOption.backgroundColor : !mv_isTransparent(styleSelect.backgroundColor) ? styleSelect.backgroundColor : styleBody.backgroundColor
|
||||
const titleColor = mv_getColorSchema(optionColor) === "light" ? mv_darker(optionColor, 30) : mv_lighter(optionColor, 30)
|
||||
const selectStyleCSS = [`#${CSS.escape(cbid)} {`, mv_style2Css(styleSelect, styleNames), "}"]
|
||||
const optionStyleCSS = [`#${CSS.escape(cbid + ".panel")} {`, mv_style2Css(styleOption, styleNames), `background-color: ${optionColor};`, `border-radius: ${panelRadius};`, "}"]
|
||||
const titleStyleCSS = [`#${CSS.escape(cbid + ".panel")} .group-title {`, `background-color: ${titleColor} !important;`, "}"]
|
||||
styleNode.textContent = [].concat(selectStyleCSS, optionStyleCSS, titleStyleCSS).join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
function mv_idSafe(id) {
|
||||
return id
|
||||
.trim()
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/[\x00-\x1F\x7F]/g, "");
|
||||
}
|
||||
|
||||
// 折叠组
|
||||
function mv_toggleGroup(opt, nodeList, searchInput, g) {
|
||||
g = mv_idSafe(g);
|
||||
const ul = document.getElementById("group-" + opt + "-" + g);
|
||||
const arrow = document.getElementById("arrow-" + opt + "-" + g);
|
||||
if (!ul) return;
|
||||
// 判断是否在搜索状态
|
||||
const keyword = searchInput.value.trim().toLowerCase();
|
||||
const isSearching = keyword.length > 0;
|
||||
// 搜索状态下,仅切换当前组,不处理其他组
|
||||
if (isSearching){
|
||||
ul.style.display = ul.style.display === "none" ? "block" : "none";
|
||||
if (arrow) arrow.className = ul.style.display === "none" ? "mv-arrow-right" : "mv-arrow-down-small";
|
||||
return;
|
||||
}
|
||||
// 非搜索模式:先折叠其他组
|
||||
nodeList.querySelectorAll(".group-block").forEach(group=>{
|
||||
const gname = group.getAttribute("data-group");
|
||||
const gul = document.getElementById("group-" + opt + "-" + gname);
|
||||
const garrow = document.getElementById("arrow-" + opt + "-" + gname);
|
||||
if (gname !== g) {
|
||||
if (gul) gul.style.display = "none";
|
||||
if (garrow) garrow.className = "mv-arrow-right";
|
||||
}
|
||||
});
|
||||
nodeList.parentNode.scrollTop = 0;
|
||||
// 切换当前组
|
||||
ul.style.display = ul.style.display === "none" ? "block" : "none";
|
||||
if (arrow) arrow.className = ul.style.display === "none" ? "mv-arrow-right" : "mv-arrow-down-small";
|
||||
};
|
||||
|
||||
// 计数
|
||||
function mv_updateCount(opt, nodeList, searchInput) {
|
||||
const keyword = searchInput.value.trim().toLowerCase();
|
||||
const isSearching = keyword.length > 0;
|
||||
// 当前实例下的所有 checkbox
|
||||
const cbs = isSearching
|
||||
? Array.from(nodeList.querySelectorAll("input[type=checkbox]")).filter(cb => cb.closest("li").style.display !== "none")
|
||||
: nodeList.querySelectorAll("input[type=checkbox]");
|
||||
let checked = 0;
|
||||
cbs.forEach(cb => { if(cb.checked) checked++; });
|
||||
// 更新总计
|
||||
const totalSpan = document.getElementById("count-" + opt);
|
||||
if (totalSpan) {
|
||||
totalSpan.innerHTML = "<%:Selected:%> <span style='color:red;'>" + checked + "/" + cbs.length + "</span>";
|
||||
}
|
||||
// 更新每个组计数
|
||||
nodeList.querySelectorAll(".group-block").forEach(group => {
|
||||
const gname = group.getAttribute("data-group");
|
||||
const groupCbs = group.querySelectorAll("li[data-node-name] input[type=checkbox]");
|
||||
let groupChecked = 0;
|
||||
let totalCount = 0;
|
||||
groupCbs.forEach(cb => {
|
||||
const li = cb.closest("li");
|
||||
// 搜索时只统计可见节点
|
||||
if (!isSearching || li.style.display !== "none") {
|
||||
totalCount++;
|
||||
if (cb.checked) groupChecked++;
|
||||
}
|
||||
});
|
||||
const span = document.getElementById("group-count-" + opt + "-" + gname);
|
||||
if(span) span.textContent = "(" + groupChecked + "/" + totalCount + ")";
|
||||
});
|
||||
}
|
||||
|
||||
// 搜索
|
||||
function mv_filterGroups(searchInput, opt, nodeList) {
|
||||
const keyword = searchInput.value.trim().toLowerCase();
|
||||
nodeList.querySelectorAll(".group-block").forEach(group => {
|
||||
const items = group.querySelectorAll("li[data-node-name]");
|
||||
let matchCount = 0;
|
||||
items.forEach(li => {
|
||||
const name = li.getAttribute("data-node-name");
|
||||
if (!keyword || name.indexOf(keyword) !== -1) {
|
||||
li.style.display = "";
|
||||
matchCount++;
|
||||
} else {
|
||||
li.style.display = "none";
|
||||
}
|
||||
});
|
||||
// 搜索时自动展开组
|
||||
const gname = group.getAttribute("data-group");
|
||||
const ul = document.getElementById("group-" + opt + "-" + gname);
|
||||
const arrow = document.getElementById("arrow-" + opt + "-" + gname);
|
||||
|
||||
if (matchCount === 0 && keyword !== "") {
|
||||
group.style.display = "none";
|
||||
} else {
|
||||
group.style.display = "";
|
||||
if (keyword && ul && arrow) {
|
||||
ul.style.display = "";
|
||||
arrow.className = "mv-arrow-down-small";
|
||||
}
|
||||
}
|
||||
});
|
||||
mv_updateCount(opt, nodeList, searchInput);
|
||||
// 清空搜索后恢复全部折叠
|
||||
if (!keyword) {
|
||||
mv_collapseAllGroups(opt, nodeList);
|
||||
}
|
||||
}
|
||||
|
||||
// 折叠所有组
|
||||
function mv_collapseAllGroups(opt, nodeList) {
|
||||
nodeList.querySelectorAll(".group-block").forEach(group => {
|
||||
const gname = group.getAttribute("data-group");
|
||||
const ul = document.getElementById("group-" + opt + "-" + gname);
|
||||
const arrow = document.getElementById("arrow-" + opt + "-" + gname);
|
||||
if (ul) ul.style.display = "none";
|
||||
if (arrow) arrow.className = "mv-arrow-right";
|
||||
});
|
||||
}
|
||||
|
||||
window.mv_nodeitem_rendered = {};
|
||||
function mv_render_multivalue_list(cbid, opt, nodeList, searchInput) {
|
||||
if (window.mv_nodeitem_rendered[cbid]) return;
|
||||
const root = document.getElementById(cbid);
|
||||
if (!root) return;
|
||||
// 遍历所有组
|
||||
root.querySelectorAll(".group-items").forEach(function(ul) {
|
||||
// 组名
|
||||
const gname = ul.id.replace("group-" + opt + "-", "");
|
||||
// 解析 Lua 注入的数据
|
||||
const items = JSON.parse(ul.dataset.items || "[]");
|
||||
const selected = JSON.parse(ul.dataset.selected || "{}");
|
||||
// 清空
|
||||
ul.innerHTML = "";
|
||||
// 列表渲染
|
||||
items.forEach(function(item) {
|
||||
// li
|
||||
let li = document.createElement("li");
|
||||
li.className = "node-item";
|
||||
li.setAttribute("data-node-name", item.label.toLowerCase());
|
||||
li.title = item.label;
|
||||
// row div
|
||||
let row = document.createElement("div");
|
||||
row.className = "mv-node-row";
|
||||
// checkbox
|
||||
let checkboxId = cbid + "." + item.key;
|
||||
let checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.className = "cbi-input-checkbox mv-node-checkbox";
|
||||
checkbox.id = checkboxId;
|
||||
checkbox.name = cbid;
|
||||
checkbox.value = item.key;
|
||||
if (selected[item.key]) checkbox.checked = true;
|
||||
// label
|
||||
let label = document.createElement("label");
|
||||
label.className = "mv-node-label";
|
||||
label.htmlFor = checkboxId;
|
||||
label.textContent = item.label;
|
||||
// 组装
|
||||
row.appendChild(checkbox);
|
||||
row.appendChild(label);
|
||||
li.appendChild(row);
|
||||
ul.appendChild(li);
|
||||
});
|
||||
});
|
||||
window.mv_nodeitem_rendered[cbid] = true;
|
||||
searchInput.addEventListener("input", function() {
|
||||
mv_filterGroups(searchInput, opt, nodeList);
|
||||
});
|
||||
searchInput.addEventListener('keydown', function(e) {
|
||||
const isEnter = e.key === "Enter" || e.keyCode === 13;
|
||||
if (!isEnter) return;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
searchInput.blur();
|
||||
});
|
||||
// checkbox 改变时更新计数
|
||||
nodeList.addEventListener("change", () => {
|
||||
mv_updateCount(opt, nodeList, searchInput);
|
||||
});
|
||||
}
|
||||
|
||||
// 全选 / 全不选
|
||||
function mv_selectAll(cbid, opt, flag) {
|
||||
if (!window.mv_nodeitem_rendered[cbid]) return;
|
||||
const nodeList = document.getElementById(cbid + ".node_list");
|
||||
const searchInput = document.getElementById(cbid + ".search");
|
||||
const cbs = nodeList.querySelectorAll("input[type=checkbox]");
|
||||
cbs.forEach(cb=>{
|
||||
if (cb.offsetParent !== null) cb.checked = flag;
|
||||
});
|
||||
mv_updateCount(opt, nodeList, searchInput);
|
||||
};
|
||||
|
||||
|
||||
function mv_onControlVisible(cbid, cb) {
|
||||
var root = document.getElementById(cbid);
|
||||
if (!root) return;
|
||||
var container = root.closest(".cbi-value");
|
||||
if (!container) return;
|
||||
if (container.offsetParent !== null) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
var observer = new MutationObserver(function () {
|
||||
if (container.offsetParent !== null) {
|
||||
observer.disconnect();
|
||||
cb();
|
||||
}
|
||||
});
|
||||
observer.observe(container, {
|
||||
attributes: true,
|
||||
attributeFilter: ["style", "class"]
|
||||
});
|
||||
}
|
||||
|
||||
function mv_multivalue_init(cbid, opt, nodeList, searchInput) {
|
||||
mv_onControlVisible(cbid, function () {
|
||||
var root = document.getElementById(cbid);
|
||||
if (!root || root.dataset.rendered) return;
|
||||
root.dataset.rendered = "1";
|
||||
mv_render_multivalue_list(cbid, opt, nodeList, searchInput)
|
||||
});
|
||||
mv_registerAdaptive(cbid);
|
||||
}
|
||||
|
||||
const mv_adaptiveControls = new Set();
|
||||
function mv_registerAdaptive(cbid) {
|
||||
mv_adaptiveControls.add(cbid);
|
||||
mv_adaptiveStyle(cbid);
|
||||
}
|
||||
let mv_adaptiveTicking = false;
|
||||
window.addEventListener("resize", () => {
|
||||
if (!mv_adaptiveTicking) {
|
||||
mv_adaptiveTicking = true;
|
||||
requestAnimationFrame(() => {
|
||||
mv_adaptiveControls.forEach(cbid => mv_adaptiveStyle(cbid));
|
||||
mv_adaptiveTicking = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
//]]>
|
||||
</script>
|
||||
@@ -1,164 +0,0 @@
|
||||
<%+cbi/valueheader%>
|
||||
<%
|
||||
-- Template Developers:
|
||||
-- - lwb1978
|
||||
-- Copyright: copyright(c)2025–2027
|
||||
-- Description: Passwall(2) UI template
|
||||
|
||||
local json = require "luci.jsonc"
|
||||
|
||||
-- 读取值(keylist/vallist/group)
|
||||
local values = {{key = "", label = translate("-- Please choose --")}}
|
||||
for i, key in pairs(self.keylist) do
|
||||
if key and key ~= "" then
|
||||
values[#values + 1] = {
|
||||
key = key,
|
||||
label = self.vallist[i] or key,
|
||||
group = self.group and self.group[i] or nil
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- 获取当前配置值(单值)
|
||||
local current_key = nil
|
||||
local cval = self:cfgvalue(section)
|
||||
if type(cval) == "table" then
|
||||
-- 若意外为table,取第一个
|
||||
for k,_ in pairs(cval) do
|
||||
current_key = k
|
||||
break
|
||||
end
|
||||
elseif type(cval) == "string" then
|
||||
current_key = (cval:match("%S+")) -- 取第一个 token
|
||||
end
|
||||
|
||||
-- 分离无组节点(ungrouped)与有组节点(grouped),并保持原有顺序
|
||||
local ungrouped = {}
|
||||
local groups = {}
|
||||
local group_order = {}
|
||||
local current_key_added = false
|
||||
for _, item in ipairs(values) do
|
||||
if not current_key_added and current_key and current_key ~= "" and item.key == current_key then
|
||||
current_key_added = true
|
||||
end
|
||||
if not item.group or item.group == "" then
|
||||
table.insert(ungrouped, item)
|
||||
else
|
||||
local g = item.group
|
||||
if not groups[g] then
|
||||
groups[g] = {}
|
||||
table.insert(group_order, g)
|
||||
end
|
||||
table.insert(groups[g], item)
|
||||
end
|
||||
end
|
||||
if not current_key_added and current_key and current_key ~= "" then
|
||||
table.insert(ungrouped, {key = current_key, label = current_key})
|
||||
end
|
||||
|
||||
-- 如果没有配置值,默认取第一个(优先无组第一项,否则第一组第一项)
|
||||
if not current_key then
|
||||
if #ungrouped > 0 then
|
||||
current_key = ungrouped[1].key
|
||||
else
|
||||
-- 找到 group_order 第一个有节点的项
|
||||
for _, g in ipairs(group_order) do
|
||||
if groups[g] and #groups[g] > 0 then
|
||||
current_key = groups[g][1].key
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 获取当前 label(用于主控件显示)
|
||||
local function find_label_by_key(k)
|
||||
if not k then return "" end
|
||||
for _, v in ipairs(values) do
|
||||
if v.key == k then return v.label end
|
||||
end
|
||||
return ""
|
||||
end
|
||||
local current_label = find_label_by_key(current_key) or ""
|
||||
|
||||
-- 打包数据给 JS
|
||||
local dropdown_data = {
|
||||
current_key = current_key,
|
||||
current_label = current_label,
|
||||
ungrouped = ungrouped,
|
||||
groups = groups,
|
||||
group_order = group_order,
|
||||
cbid = cbid,
|
||||
}
|
||||
%>
|
||||
<script>
|
||||
window.v_dropdown_data = window.v_dropdown_data || {};
|
||||
window.v_dropdown_data["<%=cbid%>"] = <%=json.stringify(dropdown_data)%>;
|
||||
</script>
|
||||
|
||||
<div id="<%=cbid%>.v-main" class="v-dropdown-container">
|
||||
<div style="display:none !important;">
|
||||
<select id="<%=cbid%>.ref" class="cbi-input-select" style="display:none !important;">
|
||||
<option value>placeholder</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- 隐藏 input(保存实际配置值) -->
|
||||
<input type="text" id="<%=cbid%>" name="<%=cbid%>" value="<%=current_key%>" data-update="change" style="display:none !important;" />
|
||||
<!-- 模拟 ListValue 控件外观 -->
|
||||
<div class="cbi-input-value cbi-input-select v-dropdown-display" id="<%=cbid%>.display" tabindex="0">
|
||||
<span id="<%=cbid%>.label" class="v-dropdown-label" title="<%=pcdata(current_label)%>">
|
||||
<%=pcdata(translate("-- Please choose --"))%>
|
||||
</span>
|
||||
<span class="v-arrow-down"></span>
|
||||
</div>
|
||||
<!-- 下拉面板 -->
|
||||
<div id="<%=cbid%>.panel" class="cbi-listvalue-panel v-dropdown-panel" style="display:none;">
|
||||
<!-- 搜索框 -->
|
||||
<div style="padding:8px;border-bottom:1px solid #f0f0f0;">
|
||||
<input id="<%=cbid%>.search" class="cbi-input-text v-dropdown-search" type="text" placeholder="🔍 <%:Search nodes...%>" inputmode="search" enterkeyhint="done" />
|
||||
</div>
|
||||
<!-- 列表容器 -->
|
||||
<div id="<%=cbid%>.list" style="padding:8px;padding-bottom:0;">
|
||||
<!-- 首次点击 display 时由 JS 填充 -->
|
||||
</div>
|
||||
<!-- 自定义 -->
|
||||
<div style="padding:8px;padding-top:0;margin-top:0;">
|
||||
<input id="<%=cbid%>.custom" class="cbi-input-text v-dropdown-custom" type="text" placeholder="<%=translate("-- custom --")%>" inputmode="text" enterkeyhint="done" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
(function(){
|
||||
const cbid = "<%=cbid%>";
|
||||
const hiddenInput = document.getElementById(cbid);
|
||||
const panel = document.getElementById(cbid + ".panel");
|
||||
const display = document.getElementById(cbid + ".display");
|
||||
const labelSpan = document.getElementById(cbid + ".label");
|
||||
const searchInput = document.getElementById(cbid + ".search");
|
||||
const listContainer = document.getElementById(cbid + ".list");
|
||||
const customInput = document.getElementById(cbid + ".custom");
|
||||
|
||||
// 点击 display
|
||||
display.addEventListener("click", function(e){
|
||||
e.stopPropagation();
|
||||
v_render_dropdown_list(cbid,panel,listContainer,hiddenInput,labelSpan,searchInput,display,customInput);
|
||||
document.querySelectorAll(".cbi-listvalue-panel").forEach(p=>{
|
||||
if (p !== panel) p.style.display = "none";
|
||||
});
|
||||
if (panel.style.display !== "none") {
|
||||
v_closePanel(cbid,panel,listContainer,hiddenInput,searchInput,customInput);
|
||||
} else {
|
||||
v_openPanel(cbid,display,panel,listContainer,hiddenInput,searchInput,customInput);
|
||||
}
|
||||
});
|
||||
v_registerAdaptive(cbid);
|
||||
setTimeout(() => {
|
||||
labelSpan.textContent = v_ellipsisByWidth(cbid, '<%=pcdata(current_label ~= "" and current_label or current_key)%>');
|
||||
}, 100);
|
||||
})();
|
||||
//]]>
|
||||
</script>
|
||||
|
||||
<%+cbi/valuefooter%>
|
||||
@@ -1,792 +0,0 @@
|
||||
<%
|
||||
-- Template Developers:
|
||||
-- - lwb1978
|
||||
-- Copyright: copyright(c)2025–2027
|
||||
-- Description: Passwall(2) UI template
|
||||
-- It is the common part of the template and cannot be used independently
|
||||
%>
|
||||
<style>
|
||||
/* 主下拉按钮的下箭头 */
|
||||
.v-arrow-down {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 6px solid #666;
|
||||
margin-left: 6px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* 组标题的右箭头(折叠) */
|
||||
.v-arrow-right {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-left: 5px solid #555;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* 组标题的下箭头(展开) */
|
||||
.v-arrow-down-small {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 5px solid #555;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* 基础列表项样式 */
|
||||
.cbi-listvalue-panel li[data-key] {
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
/* 鼠标悬停效果 - 使用透明度避免覆盖问题 */
|
||||
.cbi-listvalue-panel li[data-key]:hover {
|
||||
background-color: rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
/* 选中项样式 - 使用更高优先级 */
|
||||
.cbi-listvalue-panel li[data-key].is-selected {
|
||||
background-color: #007bff !important;
|
||||
color: white !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
/* 选中项悬停时保持主色调 */
|
||||
.cbi-listvalue-panel li[data-key].is-selected:hover {
|
||||
background-color: #0056b3 !important;
|
||||
}
|
||||
|
||||
.v-dropdown-container {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
min-width: 220px;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.v-dropdown-container {
|
||||
display: block;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.v-dropdown-display {
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
.v-dropdown-label {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
.v-dropdown-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2147483647;
|
||||
border: 1px solid #dcdcdc;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 6px 18px rgba(0,0,0,0.08);
|
||||
max-height: 50vh;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.v-dropdown-search, .v-dropdown-custom {
|
||||
width: 100%;
|
||||
max-width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
box-sizing: border-box;
|
||||
padding: 6px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.v-group-title {
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.v-group-list {
|
||||
list-style: none;
|
||||
margin: 6px 0 0 8px;
|
||||
padding: 0;
|
||||
}
|
||||
.v-group-item {
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: left !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
// css helper functions
|
||||
function v_camelToKebab(str) {
|
||||
return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase()
|
||||
}
|
||||
|
||||
function v_style2Css(styleDeclaration, properties) {
|
||||
const cssRules = properties.map(prop => {
|
||||
const kebabCaseProp = v_camelToKebab(prop);[1, 5]
|
||||
const value = styleDeclaration[prop]
|
||||
if (value) {
|
||||
return `${kebabCaseProp}: ${value};`
|
||||
}
|
||||
return ''
|
||||
})
|
||||
// Filter out any empty strings and join the rules
|
||||
return cssRules.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
const v_parseColorToRgba = (function() {
|
||||
// Create canvas and context once (Closure)
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
return function(colorStr) {
|
||||
if (!colorStr)
|
||||
return null;
|
||||
ctx.clearRect(0, 0, 1, 1);
|
||||
// 2. Apply the color
|
||||
ctx.fillStyle = colorStr;
|
||||
// 3. Fill a single pixel
|
||||
ctx.fillRect(0, 0, 1, 1);
|
||||
// 4. Extract pixel data [R, G, B, A]
|
||||
const data = ctx.getImageData(0, 0, 1, 1).data;
|
||||
return {
|
||||
r: data[0],
|
||||
g: data[1],
|
||||
b: data[2],
|
||||
// Convert alpha from 0-255 to 0-1 (rounded to 3 decimal places)
|
||||
a: Math.round((data[3] / 255) * 1000) / 1000
|
||||
};
|
||||
};
|
||||
})();
|
||||
|
||||
// Helper to convert back to Hex (for output consistency)
|
||||
function v_rgbToHex(r, g, b) {
|
||||
const toHex = (n) => {
|
||||
const hex = Math.max(0, Math.min(255, n)).toString(16)
|
||||
return hex.length === 1 ? '0' + hex : hex
|
||||
}
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
||||
}
|
||||
|
||||
function v_isTransparent(color) {
|
||||
const cleanColor = v_parseColorToRgba(color);
|
||||
// check #RRGGBBAA for transparency
|
||||
return !cleanColor || (cleanColor.a !== undefined && !cleanColor.a);
|
||||
}
|
||||
|
||||
function v_getColorSchema(color) {
|
||||
const rgb = v_parseColorToRgba(color);
|
||||
if (!rgb) return 'unknown'; // Handle invalid colors
|
||||
// Calculate YIQ brightness (human eye perception)
|
||||
const brightness = ((rgb.r * 299) + (rgb.g * 587) + (rgb.b * 114)) / 1000;
|
||||
|
||||
return brightness > 128 ? 'light' : 'dark';
|
||||
}
|
||||
|
||||
function v_lighter(color, amount) {
|
||||
const rgb = v_parseColorToRgba(color);
|
||||
if (!rgb) return color;
|
||||
// Add amount to each channel
|
||||
const r = rgb.r + amount;
|
||||
const g = rgb.g + amount;
|
||||
const b = rgb.b + amount;
|
||||
// Convert back to Hex (clamping happens inside rgbToHex)
|
||||
return v_rgbToHex(r, g, b);
|
||||
}
|
||||
|
||||
function v_darker(color, amount) {
|
||||
const rgb = v_parseColorToRgba(color);
|
||||
if (!rgb) return color;
|
||||
// Subtract amount from each channel
|
||||
const r = rgb.r - amount;
|
||||
const g = rgb.g - amount;
|
||||
const b = rgb.b - amount;
|
||||
|
||||
return v_rgbToHex(r, g, b);
|
||||
}
|
||||
|
||||
// copy select styles
|
||||
function v_adaptiveStyle(cbid) {
|
||||
const display = document.getElementById(cbid + ".display");
|
||||
const hiddenRef = document.getElementById(cbid + ".ref");
|
||||
const panel = document.getElementById(cbid + ".panel");
|
||||
if (hiddenRef && display) {
|
||||
const elOption = hiddenRef.getElementsByTagName("option")[0]
|
||||
const styleSelect = window.getComputedStyle(hiddenRef)
|
||||
const styleOption = window.getComputedStyle(elOption)
|
||||
const styleBody = window.getComputedStyle(document.body)
|
||||
|
||||
const styleNode = document.createElement('style')
|
||||
const styleNames = ["color", "height", "padding", "margin", "lineHeight", "border", "borderRadius", "minWidth","minHeight"]
|
||||
if (styleSelect.borderBottomStyle !== "none") {
|
||||
styleNames.push("borderBottomWidth", "borderBottomStyle", "borderBottomColor");
|
||||
}
|
||||
document.head.appendChild(styleNode)
|
||||
// trace back from option -> select -> body for background color
|
||||
const panelRadius = styleSelect.borderRadius;
|
||||
const optionColor = !v_isTransparent(styleOption.backgroundColor) ? styleOption.backgroundColor : !v_isTransparent(styleSelect.backgroundColor) ? styleSelect.backgroundColor : styleBody.backgroundColor
|
||||
const titleColor = v_getColorSchema(optionColor) === "light" ? v_darker(optionColor, 30) : v_lighter(optionColor, 30)
|
||||
const selectStyleCSS = [`#${CSS.escape(cbid + ".display")} {`, v_style2Css(styleSelect, styleNames), v_style2Css(styleSelect, ["backgroundColor"]), "}"]
|
||||
const optionStyleCSS = [`#${CSS.escape(cbid + ".panel")} {`, v_style2Css(styleOption, styleNames), `background-color: ${optionColor};`, `border-radius: ${panelRadius};`, "}"]
|
||||
const titleStyleCSS = [`#${CSS.escape(cbid + ".panel")} .v-group-title {`, `background-color: ${titleColor} !important;`, "}"]
|
||||
styleNode.textContent = [].concat(selectStyleCSS, optionStyleCSS, titleStyleCSS).join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
function v_idSafe(id) {
|
||||
return id
|
||||
.trim()
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/[\x00-\x1F\x7F]/g, "");
|
||||
}
|
||||
|
||||
// 高亮当前选中的项
|
||||
function v_highlightSelectedItem(listContainer, hiddenInput) {
|
||||
const allItems = listContainer.querySelectorAll("li[data-key]");
|
||||
const currentKey = hiddenInput.value;
|
||||
allItems.forEach(item => {
|
||||
item.classList.remove("is-selected");
|
||||
if (item.getAttribute('data-key') === currentKey) {
|
||||
item.classList.add("is-selected");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新组内选中计数
|
||||
function v_updateGroupCounts(cbid, listContainer, hiddenInput, searchInput) {
|
||||
const groups = listContainer.querySelectorAll(".v-group");
|
||||
const currentKey = hiddenInput.value;
|
||||
const isSearching = searchInput.value.trim() !== "";
|
||||
groups.forEach(group => {
|
||||
const gname = group.getAttribute("data-group");
|
||||
const items = group.querySelectorAll("li[data-key]");
|
||||
const span = document.getElementById("group-count-" + cbid + "-" + gname);
|
||||
if (!span) return;
|
||||
if (isSearching) {
|
||||
// 搜索状态:显示匹配数量
|
||||
let matchCount = 0;
|
||||
items.forEach(li => {
|
||||
if (li.style.display !== "none") matchCount++;
|
||||
});
|
||||
span.textContent = "(" + matchCount + "/" + items.length + ")";
|
||||
if (matchCount > 0) {
|
||||
span.style.color = "#28a745";
|
||||
span.style.fontWeight = "600";
|
||||
} else {
|
||||
span.style.color = "#dc3545";
|
||||
span.style.fontWeight = "normal";
|
||||
}
|
||||
} else {
|
||||
// 默认状态:显示选中项数量
|
||||
let selectedCount = 0;
|
||||
items.forEach(li => {
|
||||
if (li.getAttribute('data-key') === currentKey) {
|
||||
selectedCount = 1;
|
||||
}
|
||||
});
|
||||
span.textContent = "(" + selectedCount + "/" + items.length + ")";
|
||||
if (selectedCount > 0) {
|
||||
span.style.color = "#007bff";
|
||||
span.style.fontWeight = "600";
|
||||
} else {
|
||||
span.style.color = "";
|
||||
span.style.fontWeight = "normal";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//搜索过滤器:按 name 或 label 做模糊匹配,搜索时自动展开所有组并隐藏不匹配条目
|
||||
function v_filterList(keyword, cbid, listContainer, hiddenInput, searchInput) {
|
||||
keyword = (keyword || "").toLowerCase().trim();
|
||||
const topItems = listContainer.querySelectorAll("ul li[data-key]");
|
||||
topItems.forEach((li, index)=>{
|
||||
if (index === 0) {
|
||||
li.style.display = "block";
|
||||
return;
|
||||
}
|
||||
const name = (li.getAttribute("data-node-name") || "").toLowerCase();
|
||||
if (!keyword || name.indexOf(keyword) !== -1) {
|
||||
li.style.display = "block";
|
||||
} else {
|
||||
li.style.display = "none";
|
||||
}
|
||||
});
|
||||
const groups = listContainer.querySelectorAll(".v-group");
|
||||
groups.forEach(group=>{
|
||||
const items = group.querySelectorAll("li[data-key]");
|
||||
let matchCount = 0;
|
||||
items.forEach(li=>{
|
||||
const name = (li.getAttribute("data-node-name") || "").toLowerCase();
|
||||
if (!keyword || name.indexOf(keyword) !== -1) {
|
||||
li.style.display = "block";
|
||||
matchCount++;
|
||||
} else {
|
||||
li.style.display = "none";
|
||||
}
|
||||
});
|
||||
group.style.display = (matchCount === 0 && keyword !== "") ? "none" : "block";
|
||||
const ul = group.querySelector(".v-group-list");
|
||||
const gname = group.getAttribute("data-group");
|
||||
const arrow = document.getElementById("arrow-" + cbid + "-" + gname);
|
||||
if (keyword) {
|
||||
if (ul) ul.style.display = (matchCount > 0 ? "block" : "none");
|
||||
if (arrow) arrow.className = (matchCount > 0 ? "v-arrow-down-small" : "v-arrow-right");
|
||||
} else {
|
||||
if (ul) ul.style.display = "none";
|
||||
if (arrow) arrow.className = "v-arrow-right";
|
||||
}
|
||||
});
|
||||
v_updateGroupCounts(cbid, listContainer, hiddenInput, searchInput);
|
||||
v_highlightSelectedItem(listContainer, hiddenInput);
|
||||
}
|
||||
|
||||
// 切换单个组(点击组标题)
|
||||
function v_toggleGroup(listContainer, cbid, g) {
|
||||
g = v_idSafe(g);
|
||||
const group = listContainer.querySelector(".v-group[data-group='" + g + "']");
|
||||
if (!group) return;
|
||||
const ul = group.querySelector(".v-group-list");
|
||||
const arrow = document.getElementById("arrow-" + cbid + "-" + g);
|
||||
if (!ul) return;
|
||||
const searchInput = document.getElementById(cbid + ".search");
|
||||
const isSearching = searchInput?.value.trim() !== "";
|
||||
const isExpanded = ul.style.display !== "none";
|
||||
|
||||
if (isExpanded) {
|
||||
ul.style.display = "none";
|
||||
if (arrow) arrow.className = "v-arrow-right";
|
||||
} else {
|
||||
ul.style.display = "block";
|
||||
if (arrow) arrow.className = "v-arrow-down-small";
|
||||
|
||||
if (!isSearching) {
|
||||
const allGroups = listContainer.querySelectorAll(".v-group");
|
||||
allGroups.forEach(otherGroup => {
|
||||
if (otherGroup !== group) {
|
||||
const otherUl = otherGroup.querySelector(".v-group-list");
|
||||
const otherGname = otherGroup.getAttribute("data-group");
|
||||
const otherArrow = document.getElementById("arrow-" + cbid + "-" + otherGname);
|
||||
if (otherUl) otherUl.style.display = "none";
|
||||
if (otherArrow) otherArrow.className = "v-arrow-right";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 展开包含当前 hiddenInput 值的组(初始化或打开面板时使用)
|
||||
function v_expandGroupOfCurrent(cbid, listContainer, hiddenInput) {
|
||||
const key = hiddenInput.value;
|
||||
if (!key) return;
|
||||
const targetLi = listContainer.querySelector(`li[data-key='${CSS.escape(key)}']`);
|
||||
if (!targetLi) return;
|
||||
let parentGroup = targetLi.closest(".v-group");
|
||||
if (parentGroup) {
|
||||
const ul = parentGroup.querySelector(".v-group-list");
|
||||
const gname = parentGroup.getAttribute("data-group");
|
||||
const arrow = document.getElementById("arrow-" + cbid + "-" + gname);
|
||||
if (ul) ul.style.display = "block";
|
||||
if (arrow) arrow.className = "v-arrow-down-small";
|
||||
const allGroups = listContainer.querySelectorAll(".v-group");
|
||||
allGroups.forEach(gp=>{
|
||||
if (gp !== parentGroup) {
|
||||
const gul = gp.querySelector(".v-group-list");
|
||||
const otherGname = gp.getAttribute("data-group");
|
||||
const gar = document.getElementById("arrow-" + cbid + "-" + otherGname);
|
||||
if (gul) gul.style.display = "none";
|
||||
if (gar) gar.className = "v-arrow-right";
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const allGroups = listContainer.querySelectorAll(".v-group");
|
||||
allGroups.forEach(gp=>{
|
||||
const gul = gp.querySelector(".v-group-list");
|
||||
const gname = gp.getAttribute("data-group");
|
||||
const gar = document.getElementById("arrow-" + cbid + "-" + gname);
|
||||
if (gul) gul.style.display = "none";
|
||||
if (gar) gar.className = "v-arrow-right";
|
||||
});
|
||||
}
|
||||
if (targetLi) {
|
||||
requestAnimationFrame(() => {
|
||||
targetLi.scrollIntoView({ block: "nearest" });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 计算panel位置
|
||||
function v_repositionPanel(panel, display) {
|
||||
if (!panel || panel.style.display === "none") return;
|
||||
const rect = display.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
||||
panel.style.visibility = "hidden";
|
||||
panel.style.display = "block";
|
||||
panel.style.minHeight = "100px";
|
||||
panel.style.maxHeight = Math.min(0.48*viewportHeight, 500) + "px";
|
||||
const panelHeight = panel.offsetHeight;
|
||||
const spaceBelow = viewportHeight - rect.bottom;
|
||||
const spaceAbove = rect.top;
|
||||
let top, isUp = false;
|
||||
if (spaceBelow >= panelHeight) {
|
||||
top = rect.bottom + 2;
|
||||
isUp = false;
|
||||
} else if (spaceAbove >= panelHeight) {
|
||||
top = rect.top - panelHeight - 2;
|
||||
isUp = true;
|
||||
} else {
|
||||
if (spaceBelow >= spaceAbove) {
|
||||
top = Math.max(rect.bottom - 2, viewportHeight - panelHeight - 2);
|
||||
isUp = false;
|
||||
} else {
|
||||
top = Math.min(rect.top - panelHeight + 2, 2);
|
||||
isUp = true;
|
||||
}
|
||||
}
|
||||
panel.style.left = rect.left + "px";
|
||||
panel.style.top = top + "px";
|
||||
const panelRect = panel.getBoundingClientRect();
|
||||
const displayWidth = rect.width;
|
||||
const remainingWidth = window.innerWidth - panelRect.left - 12;
|
||||
const maxWidth = Math.max(displayWidth, Math.floor(remainingWidth));
|
||||
panel.style.maxWidth = maxWidth + "px";
|
||||
panel.style.minWidth = Math.max(displayWidth, 240) + "px";
|
||||
panel.style.width = "auto";
|
||||
panel.style.visibility = "";
|
||||
}
|
||||
|
||||
// 打开/关闭面板
|
||||
function v_openPanel(cbid, display, panel, listContainer, hiddenInput, searchInput, customInput) {
|
||||
if (!panel._moved) {
|
||||
document.body.appendChild(panel);
|
||||
panel._moved = true;
|
||||
}
|
||||
v_expandGroupOfCurrent(cbid, listContainer, hiddenInput);
|
||||
v_highlightSelectedItem(listContainer, hiddenInput);
|
||||
panel.style.display = "block";
|
||||
v_repositionPanel(panel, display);
|
||||
// 失焦监听
|
||||
const handler = function(e){
|
||||
const target = e.target;
|
||||
if (panel.style.display !== "none") {
|
||||
if (!panel.contains(target) && !display.contains(target)) {
|
||||
v_closePanel(cbid, panel, listContainer, hiddenInput, searchInput, display, customInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
panel._docClickHandler = handler;
|
||||
document.addEventListener("click", handler);
|
||||
// 滚动 / resize 自动 reposition
|
||||
let ticking = false;
|
||||
const repositionHandler = function () {
|
||||
if (ticking) return;
|
||||
ticking = true;
|
||||
requestAnimationFrame(function () {
|
||||
ticking = false;
|
||||
v_repositionPanel(panel, display);
|
||||
});
|
||||
};
|
||||
panel._repositionHandler = repositionHandler;
|
||||
window.addEventListener("scroll", repositionHandler, true);
|
||||
window.addEventListener("resize", repositionHandler);
|
||||
}
|
||||
function v_closePanel(cbid, panel, listContainer, hiddenInput, searchInput, customInput) {
|
||||
panel.style.display = "none";
|
||||
searchInput.value = "";
|
||||
customInput.value = "";
|
||||
v_filterList("", cbid, listContainer, hiddenInput, searchInput);
|
||||
// document click
|
||||
if (panel._docClickHandler) {
|
||||
document.removeEventListener("click", panel._docClickHandler);
|
||||
panel._docClickHandler = null;
|
||||
}
|
||||
// scroll / resize
|
||||
if (panel._repositionHandler) {
|
||||
window.removeEventListener("scroll", panel._repositionHandler, true);
|
||||
window.removeEventListener("resize", panel._repositionHandler);
|
||||
panel._repositionHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
//自定义框
|
||||
function v_customEnter(cbid, labelSpan, hiddenInput, searchInput, panel, listContainer, customInput) {
|
||||
let inputValue = customInput.value.trim();
|
||||
if (!inputValue) {
|
||||
return;
|
||||
}
|
||||
const existingItems = listContainer.querySelectorAll('li[data-key="' + inputValue + '"]');
|
||||
if (existingItems.length <= 0) {
|
||||
let newLi = document.createElement('li');
|
||||
newLi.setAttribute('data-key', inputValue);
|
||||
newLi.setAttribute('data-node-name', inputValue.toLowerCase());
|
||||
newLi.className = 'list-item';
|
||||
|
||||
let newSpan = document.createElement('span');
|
||||
newSpan.className = 'v-item-label';
|
||||
newSpan.style.marginLeft = '12px';
|
||||
newSpan.textContent = inputValue;
|
||||
newLi.appendChild(newSpan);
|
||||
|
||||
const ungroupedList = listContainer.querySelector('ul');
|
||||
if (ungroupedList) {
|
||||
ungroupedList.appendChild(newLi);
|
||||
}
|
||||
}
|
||||
const changed = inputValue !== hiddenInput.value;
|
||||
if (changed) {
|
||||
//改值
|
||||
hiddenInput.value = inputValue;
|
||||
labelSpan.textContent = v_ellipsisByWidth(cbid, inputValue);
|
||||
labelSpan.title = inputValue;
|
||||
v_highlightSelectedItem(listContainer, hiddenInput);
|
||||
v_updateGroupCounts(cbid, listContainer, hiddenInput, searchInput);
|
||||
}
|
||||
v_closePanel(cbid,panel,listContainer,hiddenInput,searchInput,customInput);
|
||||
}
|
||||
|
||||
// 动态生成下拉框
|
||||
window.v_dropdown_rendered = {};
|
||||
function v_escape_html(s) {
|
||||
return s.replace(/[&<>"']/g, c => ({
|
||||
"&":"&", "<":"<", ">":">", '"':""", "'":"'"
|
||||
}[c]));
|
||||
}
|
||||
function v_render_dropdown_list(cbid, panel, listContainer, hiddenInput, labelSpan, searchInput, display, customInput) {
|
||||
if (window.v_dropdown_rendered[cbid]) return;
|
||||
const data = window.v_dropdown_data[cbid];
|
||||
if (!data) return;
|
||||
if (!listContainer) return;
|
||||
|
||||
let html = "";
|
||||
|
||||
// 无组项
|
||||
if (data.ungrouped && data.ungrouped.length > 0) {
|
||||
html += `<ul style="list-style:none;padding:0;margin:0 0 8px 0;">`;
|
||||
|
||||
data.ungrouped.forEach((item, index) => {
|
||||
const liStyle = index === 0 ? 'text-align:center;' : '';
|
||||
const spanStyle = index === 0 ? 'margin-left:0px;' : 'margin-left:12px;';
|
||||
html += `
|
||||
<li data-key="${item.key}"
|
||||
data-node-name="${v_escape_html(item.label.toLowerCase())}"
|
||||
class="list-item" style="${liStyle}">
|
||||
<span class="v-item-label" style="${spanStyle}">
|
||||
${v_escape_html(item.label)}
|
||||
</span>
|
||||
</li>`;
|
||||
});
|
||||
|
||||
html += `</ul>`;
|
||||
}
|
||||
|
||||
// 分组项
|
||||
data.group_order.forEach(gname => {
|
||||
const items = data.groups[gname];
|
||||
|
||||
html += `
|
||||
<div class="v-group" data-group="${v_idSafe(gname)}" style="margin-bottom:8px;">
|
||||
<div class="v-group-title" data-group-name="${v_idSafe(gname)}">
|
||||
<span class="v-arrow-right" id="arrow-${cbid}-${v_idSafe(gname)}"></span>
|
||||
<b style="margin-left:6px;">${v_escape_html(gname)}</b>
|
||||
<span id="group-count-${cbid}-${v_idSafe(gname)}"
|
||||
style="margin-left:8px;">(0/${items.length})</span>
|
||||
</div>
|
||||
<ul id="group-${cbid}-${v_idSafe(gname)}" class="v-group-list" style="display:none">
|
||||
`;
|
||||
|
||||
items.forEach(item => {
|
||||
html += `
|
||||
<li data-key="${item.key}"
|
||||
data-node-name="${v_escape_html(item.label.toLowerCase())}"
|
||||
class="v-group-item">
|
||||
<span class="v-item-label" title="${v_escape_html(item.label)}">
|
||||
${v_escape_html(item.label)}
|
||||
</span>
|
||||
</li>`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
listContainer.innerHTML = html;
|
||||
|
||||
window.v_dropdown_rendered[cbid] = true;
|
||||
|
||||
v_adaptiveStyle(cbid);
|
||||
v_updateGroupCounts(cbid, listContainer, hiddenInput, searchInput);
|
||||
|
||||
// 点击项(无组与组内项都使用 li[data-key])
|
||||
listContainer.addEventListener("click", function(e){
|
||||
let li = e.target;
|
||||
while(li && li !== listContainer && !li.hasAttribute('data-key')) li = li.parentNode;
|
||||
if(!li || li === listContainer) return;
|
||||
const key = li.getAttribute('data-key') || "";
|
||||
const text = li.querySelector(".v-item-label")?.textContent || li.textContent || key;
|
||||
|
||||
const changed = key !== hiddenInput.value;
|
||||
if (changed) {
|
||||
//改值
|
||||
hiddenInput.value = key;
|
||||
labelSpan.textContent = v_ellipsisByWidth(cbid, text);
|
||||
labelSpan.title = text;
|
||||
v_highlightSelectedItem(listContainer, hiddenInput);
|
||||
v_updateGroupCounts(cbid, listContainer, hiddenInput, searchInput);
|
||||
}
|
||||
v_closePanel(cbid,panel,listContainer,hiddenInput,searchInput,customInput);
|
||||
});
|
||||
|
||||
// 搜索功能
|
||||
searchInput.addEventListener("input", function() {
|
||||
v_filterList(this.value, cbid, listContainer, hiddenInput, searchInput);
|
||||
v_repositionPanel(panel, display);
|
||||
});
|
||||
searchInput.addEventListener('keydown', function(e) {
|
||||
const isEnter = e.key === "Enter" || e.keyCode === 13;
|
||||
if (!isEnter) return;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
searchInput.blur();
|
||||
});
|
||||
|
||||
// 切换组
|
||||
listContainer.querySelectorAll(".v-group-title").forEach(title => {
|
||||
title.addEventListener("click", function() {
|
||||
const g = this.closest(".v-group")?.getAttribute("data-group");
|
||||
if (g) {
|
||||
v_toggleGroup(listContainer, cbid, g);
|
||||
v_repositionPanel(panel, display);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//自定义框
|
||||
customInput.addEventListener("keydown", function(e){
|
||||
const isEnter = e.key === "Enter" || e.keyCode === 13;
|
||||
if (!isEnter) return;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
v_customEnter(cbid, labelSpan, hiddenInput, searchInput, panel, listContainer, customInput);
|
||||
});
|
||||
|
||||
// 防止 panel 惯性滚动穿透
|
||||
panel.addEventListener('wheel', function (e) {
|
||||
const deltaY = e.deltaY;
|
||||
const scrollTop = panel.scrollTop;
|
||||
const scrollHeight = panel.scrollHeight;
|
||||
const clientHeight = panel.clientHeight;
|
||||
const isAtTop = scrollTop === 0;
|
||||
const isAtBottom = scrollTop + clientHeight >= scrollHeight;
|
||||
if (deltaY < 0 && isAtTop) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (deltaY > 0 && isAtBottom) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
//截断display字符长度
|
||||
window.v_labelSpan_maxWidth = {};
|
||||
function v_ellipsisByWidth(cbid, text) {
|
||||
const el = document.getElementById(cbid + ".label");
|
||||
if (!el || !text) return text;
|
||||
text = text.trim();
|
||||
const maxWidth = el.clientWidth;
|
||||
window.v_labelSpan_maxWidth[cbid] = maxWidth;
|
||||
if (maxWidth <= 0) return text;
|
||||
const style = window.getComputedStyle(el);
|
||||
const font = [
|
||||
style.fontStyle,
|
||||
style.fontVariant,
|
||||
style.fontWeight,
|
||||
style.fontSize || '16px',
|
||||
style.fontFamily || 'sans-serif'
|
||||
].join(" ").replace(/\s+/g, ' ');
|
||||
const canvas = v_ellipsisByWidth._canvas || (v_ellipsisByWidth._canvas = document.createElement("canvas"));
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.font = font;
|
||||
if (ctx.measureText(text).width <= maxWidth) {
|
||||
return text;
|
||||
}
|
||||
const ellipsis = "...";
|
||||
const ellipsisWidth = ctx.measureText(ellipsis).width;
|
||||
const minChars = 15;
|
||||
const probe = text.slice(0, minChars);
|
||||
const probeWidth = ctx.measureText(probe).width;
|
||||
if (probeWidth + ellipsisWidth > maxWidth) {
|
||||
return text;
|
||||
}
|
||||
let left = 0;
|
||||
let right = text.length;
|
||||
let result = ellipsis;
|
||||
while (left <= right) {
|
||||
const mid = (left + right) >> 1;
|
||||
const substr = text.slice(0, mid);
|
||||
const w = ctx.measureText(substr).width;
|
||||
if (w + ellipsisWidth <= maxWidth) {
|
||||
result = substr + ellipsis;
|
||||
left = mid + 1;
|
||||
} else {
|
||||
right = mid - 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const v_adaptiveControls = new Set();
|
||||
function v_registerAdaptive(cbid) {
|
||||
v_adaptiveControls.add(cbid);
|
||||
v_adaptiveStyle(cbid);
|
||||
}
|
||||
function v_labelSpanResize(cbid) {
|
||||
const el = document.getElementById(cbid + ".label");
|
||||
if (!el) return;
|
||||
const maxWidth = el.clientWidth;
|
||||
if (window.v_labelSpan_maxWidth[cbid] == maxWidth) return;
|
||||
let text = el.title;
|
||||
el.textContent = v_ellipsisByWidth(cbid, text);
|
||||
}
|
||||
let v_adaptiveTicking = false;
|
||||
window.addEventListener("resize", () => {
|
||||
if (!v_adaptiveTicking) {
|
||||
v_adaptiveTicking = true;
|
||||
requestAnimationFrame(() => {
|
||||
v_adaptiveControls.forEach(cbid => {
|
||||
v_adaptiveStyle(cbid);
|
||||
v_labelSpanResize(cbid);
|
||||
});
|
||||
v_adaptiveTicking = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
//]]>
|
||||
</script>
|
||||
@@ -1,160 +0,0 @@
|
||||
<%
|
||||
local map = self.map
|
||||
local sectiontype = self.sectiontype
|
||||
local api = map.api
|
||||
local appname = self.appname or map.config
|
||||
-%>
|
||||
<script src="<%=resource%>/view/<%=api.appname%>/Sortable.min.js"></script>
|
||||
<style>
|
||||
table .cbi-button-up,
|
||||
table .cbi-button-down,
|
||||
.td.cbi-section-actions .cbi-button-up,
|
||||
.td.cbi-section-actions .cbi-button-down {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
vertical-align: middle;
|
||||
cursor: grab !important;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
font-weight: 100;
|
||||
padding: 0 !important;
|
||||
line-height: inherit;
|
||||
user-select: none;
|
||||
align-self: stretch;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.drag-handle:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.dragging-row {
|
||||
background-color: rgba(131, 191, 255, 0.7) !important;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1152px) {
|
||||
#cbi-<%=appname%>-<%=sectiontype%> .cbi-section-table-titles {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
//<![CDATA[
|
||||
//Add drag-and-drop sorting
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
function initSortableForTable() {
|
||||
var section = document.getElementById("cbi-<%=appname%>-<%=sectiontype%>");
|
||||
if (!section) return;
|
||||
|
||||
// === insert drag handle ===
|
||||
var rows = section.querySelectorAll("tr");
|
||||
rows.forEach(function(row) {
|
||||
var btn = row.querySelector(".cbi-button-remove:last-of-type");
|
||||
if (!btn) return;
|
||||
if (btn.nextElementSibling && btn.nextElementSibling.classList.contains("drag-handle")) return;
|
||||
var handle = document.createElement("span");
|
||||
handle.className = "drag-handle center";
|
||||
handle.title = "<%:Drag to reorder%>";
|
||||
handle.innerHTML = "⠿";
|
||||
btn.after(handle);
|
||||
});
|
||||
|
||||
// === init Sortable ===
|
||||
var table = section.getElementsByTagName("table")[0];
|
||||
if (!table) return;
|
||||
var root = table.tBodies[0] || table;
|
||||
if (root._sortable_initialized) return root._sortable_instance;
|
||||
root._sortable_initialized = true;
|
||||
|
||||
// save origin order
|
||||
root._origOrder = getCurrentOrder(root);
|
||||
|
||||
try {
|
||||
root._sortable_instance = Sortable.create(root, {
|
||||
handle: ".drag-handle",
|
||||
draggable: "tr.cbi-section-table-row",
|
||||
animation: 150,
|
||||
ghostClass: "dragging-row",
|
||||
fallbackOnBody: true,
|
||||
forceFallback: false,
|
||||
swapThreshold: 0.65,
|
||||
onEnd: function (evt) {
|
||||
updateHiddenInput(root, section);
|
||||
}
|
||||
});
|
||||
return root._sortable_instance;
|
||||
} catch (e) {
|
||||
root._sortable_initialized = false;
|
||||
console.error("Sortable init failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// get table current line order
|
||||
function getCurrentOrder(root) {
|
||||
var order = [];
|
||||
var rows = root.querySelectorAll("tr.cbi-section-table-row");
|
||||
rows.forEach(function (tr) {
|
||||
var id = tr.id || "";
|
||||
if (id.startsWith("cbi-<%=appname%>-"))
|
||||
id = id.replace("cbi-<%=appname%>-", "");
|
||||
order.push(id);
|
||||
});
|
||||
return order;
|
||||
}
|
||||
|
||||
// update after drag and drop hidden input
|
||||
function updateHiddenInput(root, section) {
|
||||
var newOrder = getCurrentOrder(root);
|
||||
var changed = newOrder.join(" ") !== root._origOrder.join(" ");
|
||||
var hiddenInput = section.querySelector('input[type="hidden"][id^="cbi.sts."]');
|
||||
if (hiddenInput) {
|
||||
hiddenInput.value = changed ? newOrder.join(" ") : "";
|
||||
}
|
||||
}
|
||||
|
||||
// hide lua luci version up/down
|
||||
function hideSortColumn(section) {
|
||||
var table = section.getElementsByTagName("table")[0];
|
||||
if(!table) return;
|
||||
var rows = table.querySelectorAll("tr.cbi-section-table-row");
|
||||
if(!rows.length) return;
|
||||
var colCount = rows[0].children.length, sortCol = -1;
|
||||
for(var col=0; col<colCount; col++){
|
||||
var hasSort=false, invalid=false;
|
||||
rows.forEach(function(tr){
|
||||
var td=tr.children[col]; if(!td) return;
|
||||
if(td.querySelector(".cbi-button-edit, .cbi-button-remove")) invalid=true;
|
||||
if(td.querySelector(".cbi-button-up, .cbi-button-down")) hasSort=true;
|
||||
});
|
||||
if(!invalid && hasSort){ sortCol=col; break; }
|
||||
}
|
||||
if(sortCol===-1) return;
|
||||
table.querySelectorAll("tr").forEach(function(tr){
|
||||
var c=tr.children[sortCol]; if(c) c.style.display="none";
|
||||
});
|
||||
}
|
||||
|
||||
// === wait TypedSection ===
|
||||
(function waitStable() {
|
||||
var last = 0, stable = 0;
|
||||
var THRESHOLD = 5;
|
||||
function tick() {
|
||||
var count = document.querySelectorAll("tr.cbi-section-table-row").length;
|
||||
if (count && count === last) stable++;
|
||||
else stable = 0;
|
||||
|
||||
last = count;
|
||||
if (stable >= THRESHOLD)
|
||||
setTimeout(initSortableForTable, 200);
|
||||
else
|
||||
requestAnimationFrame(tick);
|
||||
}
|
||||
tick();
|
||||
})();
|
||||
});
|
||||
</script>
|
||||
@@ -1,13 +0,0 @@
|
||||
<%+cbi/valueheader%>
|
||||
<textarea class="cbi-input-textarea" <% if not self.size then %> style="width: 100%"<% else %> cols="<%=self.size%>"<% end %>
|
||||
data-update="change"<%=
|
||||
attr("name", cbid) ..
|
||||
attr("id", cbid) ..
|
||||
ifattr(self.rows, "rows") ..
|
||||
ifattr(self.wrap, "wrap") ..
|
||||
ifattr(self.readonly, "readonly") ..
|
||||
ifattr(self.datatype, "data-type", self.datatype)
|
||||
%>>
|
||||
<%-=pcdata(self:cfgvalue(section) or self.default)-%>
|
||||
</textarea>
|
||||
<%+cbi/valuefooter%>
|
||||
@@ -1,285 +0,0 @@
|
||||
<%
|
||||
local map = self.map
|
||||
local api = map.api
|
||||
-%>
|
||||
|
||||
<div class="cbi-section">
|
||||
<h3><%:Backup and Restore%></h3>
|
||||
<div class="cbi-section-descr">
|
||||
<%:Backup or Restore Client and Server Configurations.%>
|
||||
<br>
|
||||
<font color="red"><%:Note: Restoring configurations across different versions may cause compatibility issues.%></font>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cbi-value" id="_backup_div">
|
||||
<label class="cbi-value-title"><%:Create Backup File%></label>
|
||||
<div class="cbi-value-field">
|
||||
<input class="btn cbi-button cbi-button-save" type="button" onclick="dl_backup()" value="<%:DL Backup%>" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cbi-value" id="_upload_div">
|
||||
<label class="cbi-value-title"><%:Restore Backup File%></label>
|
||||
<div class="cbi-value-field">
|
||||
<input class="btn cbi-button cbi-button-apply" type="button" onclick="show_upload_win()" value="<%:RST Backup%>" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cbi-value">
|
||||
<label class="cbi-value-title"><%=translatef("Reset %s Configuration", translate("Client"))%></label>
|
||||
<div class="cbi-value-field">
|
||||
<input class="btn cbi-button cbi-button-reset" type="button" onclick="do_reset('client')" value="<%:Do Reset%>" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cbi-value">
|
||||
<label class="cbi-value-title"><%=translatef("Reset %s Configuration", translate("Server"))%></label>
|
||||
<div class="cbi-value-field">
|
||||
<input class="btn cbi-button cbi-button-reset" type="button" onclick="do_reset('server')" value="<%:Do Reset%>" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="upload-modal" class="up-modal" style="display:none;">
|
||||
<div class="up-modal-content">
|
||||
<h3><%:Restore Backup File%></h3>
|
||||
<div class="up-cbi-value-field">
|
||||
<input class="cbi-input-file" type="file" id="ulfile" accept=".tar.gz" />
|
||||
</div>
|
||||
<div class="up-cbi-checkbox">
|
||||
<label>
|
||||
<input class="cbi-input-checkbox" type="checkbox" name="client" value="1" checked />
|
||||
<%:Client%>
|
||||
</label>
|
||||
<label>
|
||||
<input class="cbi-input-checkbox" type="checkbox" name="server" value="1" />
|
||||
<%:Server%>
|
||||
</label>
|
||||
</div>
|
||||
<div class="up-button-container">
|
||||
<input class="btn cbi-button cbi-button-apply" type="button" id="upload-btn" onclick="do_upload()" value="<%:UL Restore%>" />
|
||||
<input class="btn cbi-button cbi-button-remove" type="button" onclick="close_upload_win()" value="<%:CLOSE WIN%>" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.up-modal {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border: 2px solid #ccc;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.5);
|
||||
z-index: 1000;
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.up-modal-content {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.up-button-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.up-cbi-value-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.up-modal h3 {
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
.up-cbi-checkbox {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
margin: 0 0 30px;
|
||||
}
|
||||
|
||||
.up-cbi-checkbox label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function getBg(el) {
|
||||
if (!el) return null;
|
||||
const style = getComputedStyle(el);
|
||||
const bgImage = style.backgroundImage;
|
||||
const bgColor = style.backgroundColor;
|
||||
return (bgImage !== 'none' || !/rgba\([^,]+,[^,]+,[^,]+,\s*0\)/.test(bgColor) && bgColor !== 'transparent')
|
||||
? style.background
|
||||
: null;
|
||||
};
|
||||
|
||||
function show_upload_win(btn) {
|
||||
const uploadDiv = document.getElementById("upload-modal");
|
||||
uploadDiv.style.background = getBg(document.querySelector('.cbi-section')) || getBg(document.body) || '';
|
||||
uploadDiv.style.display = "block";
|
||||
document.getElementById("ulfile").focus();
|
||||
}
|
||||
|
||||
function close_upload_win(btn) {
|
||||
document.getElementById("ulfile").value = "";
|
||||
document.getElementById("upload-modal").style.display = "none";
|
||||
}
|
||||
|
||||
function dl_backup(btn) {
|
||||
fetch('<%= api.url("create_backup") %>', {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error("备份失败!");
|
||||
}
|
||||
const filename = response.headers.get("X-Backup-Filename");
|
||||
if (!filename) {
|
||||
return;
|
||||
}
|
||||
return response.blob().then(blob => ({ blob, filename }));
|
||||
})
|
||||
.then(result => {
|
||||
if (!result) return;
|
||||
const { blob, filename } = result;
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
})
|
||||
.catch(error => alert(error.message));
|
||||
}
|
||||
|
||||
function do_reset(type) {
|
||||
if (type !== "client" && type !== "server") return;
|
||||
const name = type === "client" ? "<%:Client%>".toLowerCase() : "<%:Server%>".toLowerCase();
|
||||
const redirect = type === "client" ? '<%=api.url("log")%>' : '<%=api.url("server")%>';
|
||||
const message = "<%=translatef("Do you want to restore the %s to default settings?", "%s")%>".replace("%s", name);
|
||||
const confirm_message = "<%=translatef("Are you sure you want to restore the %s to default settings?", "%s")%>".replace("%s", name);
|
||||
|
||||
if (confirm(message)) {
|
||||
setTimeout(function () {
|
||||
if (confirm(confirm_message)) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", '<%=api.url("reset_config")%>?type=' + type, false);
|
||||
xhr.send();
|
||||
window.location.href = redirect;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function do_upload(btn) {
|
||||
const client = document.querySelector('input[name="client"]').checked;
|
||||
const server = document.querySelector('input[name="server"]').checked;
|
||||
if (!client && !server) return;
|
||||
|
||||
const type = client === server ? "all" : client ? "client" : "server";
|
||||
|
||||
const fileInput = document.getElementById("ulfile");
|
||||
const file = fileInput.files[0];
|
||||
if (!file) {
|
||||
alert("<%:Please select a file first.%>");
|
||||
return;
|
||||
}
|
||||
if (!file.name.endsWith(".tar.gz")) {
|
||||
alert("<%:Invalid file type. Please upload a .tar.gz file.%>");
|
||||
fileInput.value = "";
|
||||
return;
|
||||
}
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||
if (file.size > maxSize) {
|
||||
alert("<%:File size exceeds 10MB limit.%>");
|
||||
fileInput.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
const binaryString = e.target.result; // ArrayBuffer
|
||||
const binary = new Uint8Array(binaryString);
|
||||
let binaryText = "";
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
binaryText += String.fromCharCode(binary[i]);
|
||||
}
|
||||
|
||||
const base64Data = btoa(binaryText);
|
||||
|
||||
const targetByteSize = 64 * 1024; // 分片大小 64KB
|
||||
let chunkSize = Math.floor(targetByteSize * 4 / 3);
|
||||
chunkSize = chunkSize + (4 - (chunkSize % 4)) % 4;
|
||||
const totalChunks = Math.ceil(base64Data.length / chunkSize);
|
||||
let currentChunk = 0;
|
||||
|
||||
function sendNextChunk() {
|
||||
if (currentChunk < totalChunks) {
|
||||
const chunk = base64Data.substring(currentChunk * chunkSize, (currentChunk + 1) * chunkSize);
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", '<%=api.url("restore_backup")%>?type=' + type, true);
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4) {
|
||||
if (xhr.status === 200) {
|
||||
const resp = JSON.parse(xhr.responseText);
|
||||
if (resp.status === "success") {
|
||||
currentChunk++;
|
||||
document.getElementById("upload-btn").value = "Uploading... " + Math.floor((currentChunk / totalChunks) * 100) + "%";
|
||||
sendNextChunk();
|
||||
} else {
|
||||
alert("Upload error: " + resp.message);
|
||||
document.getElementById("upload-btn").value = "<%:UL Restore%>";
|
||||
}
|
||||
} else {
|
||||
alert("Upload failed with status " + xhr.status);
|
||||
document.getElementById("upload-btn").value = "<%:UL Restore%>";
|
||||
}
|
||||
}
|
||||
};
|
||||
const formData = new FormData();
|
||||
formData.append("filename", file.name);
|
||||
formData.append("chunk", chunk);
|
||||
formData.append("chunk_index", currentChunk);
|
||||
formData.append("total_chunks", totalChunks);
|
||||
xhr.send(formData);
|
||||
} else {
|
||||
//alert("Upload completed.");
|
||||
document.getElementById("upload-btn").value = "<%:UL Restore%>";
|
||||
if (type === "server") {
|
||||
window.location.href = '<%=api.url("server")%>';
|
||||
} else {
|
||||
window.location.href = '<%=api.url("log")%>';
|
||||
}
|
||||
}
|
||||
}
|
||||
sendNextChunk();
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -1,67 +0,0 @@
|
||||
<%
|
||||
local map = self.map
|
||||
local api = map.api
|
||||
-%>
|
||||
<style>
|
||||
.dns-con {
|
||||
padding: 1rem;
|
||||
}
|
||||
.faq-title {
|
||||
color: var(--primary);
|
||||
font-weight: bolder;
|
||||
margin-bottom: 0.5rem;
|
||||
display: inline-block;
|
||||
}
|
||||
.reset-title {
|
||||
color: var(--primary);
|
||||
font-weight: bolder;
|
||||
margin-bottom: 0.3rem;
|
||||
display: inline-block;
|
||||
margin-top: 1.2rem;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.dns-item {
|
||||
margin-bottom: 0.8rem;
|
||||
line-height:1.2rem;
|
||||
}
|
||||
.dns-list {
|
||||
text-indent:1rem;
|
||||
line-height: 1.2rem;
|
||||
}
|
||||
</style>
|
||||
<div class="dns-con">
|
||||
<div id="faq_dns">
|
||||
<ul>
|
||||
<b class="faq-title"><%:DNS related issues:%></b>
|
||||
<li class="dns-item">1. <span><%:Certain browsers such as Chrome have built-in DNS service, which may affect DNS resolution settings. You can go to 'Settings -> Privacy and security -> Use secure DNS' menu to turn it off.%></span></li>
|
||||
<li class="dns-item">2. <span><%:If you are unable to access the internet after reboot, please try clearing the cache of your terminal devices (make sure to close all open browser application windows first, this step is especially important):%></span>
|
||||
<ul><li class="dns-list"> ◦ <span><%:For Windows systems, open Command Prompt and run the command 'ipconfig /flushdns'.%></span></li>
|
||||
<li class="dns-list"> ◦ <span><%:For Mac systems, open Terminal and run the command 'sudo killall -HUP mDNSResponder'.%></span></li>
|
||||
<li class="dns-list"> ◦ <span><%:For mobile devices, you can clear it by reconnecting to the network, such as toggling Airplane Mode and reconnecting to WiFi.%></span></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dns-item">3. <span><%:Please make sure your device's network settings point both the DNS server and default gateway to this router, to ensure DNS queries are properly routed.%></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="faq_reset"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var origin = window.location.origin;
|
||||
var hide_url = origin + "<%=api.url("hide")%>";
|
||||
var show_url = origin + "<%=api.url("show")%>";
|
||||
|
||||
function hide(url) {
|
||||
if (confirm('<%:Are you sure to hide?%>') == true) {
|
||||
window.location.href = hide_url;
|
||||
}
|
||||
}
|
||||
|
||||
var dom = document.getElementById("faq_reset");
|
||||
if (dom) {
|
||||
var li = "";
|
||||
li += "<a href='#' class='reset-title' onclick='hide()'>" + "<%: Hide in main menu:%>"+ "</a>" + "<br />" + "<%: Browser access: %>" + "<a href='#' onclick='hide()'>" + hide_url + "</a>" + "<br />";
|
||||
li += "<a href='#' class='reset-title'>" + "<%: Show in main menu:%>"+ "</a>" + "<br />" +"<%: Browser access: %>" + "<a href='#'>" + show_url + "</a>" + "<br />";
|
||||
dom.innerHTML = li;
|
||||
}
|
||||
</script>
|
||||
@@ -1,233 +0,0 @@
|
||||
<%
|
||||
local map = self.map
|
||||
local api = map.api
|
||||
local appname = map.config
|
||||
local sid = "@global[0]"
|
||||
-%>
|
||||
<style>
|
||||
#cbi-<%=appname%>-socks .td.cbi-value-field > div {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@media (max-width: 1152px) {
|
||||
#cbi-<%=appname%>-socks .td.cbi-value-field > div {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1152px) {
|
||||
#cbi-<%=appname%>-socks .cbi-section-table-titles {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
let global_id = "<%=sid%>";
|
||||
|
||||
function to_edit_node(btn, stype) {
|
||||
if (!btn) return;
|
||||
if (!stype) return;
|
||||
let hidden_select = null;
|
||||
const stype_dom = document.getElementById("cbi-<%=appname%>-" + stype);
|
||||
if (!stype_dom) return;
|
||||
const selects = stype_dom.querySelectorAll('select[id^="cbid."][id*="."][id$="node"]');
|
||||
const idReg = /^cbid\..*\..*node$/;
|
||||
for (const sel of selects) {
|
||||
if ( idReg.test(sel.id) && getComputedStyle(sel).display === "none" && (sel.compareDocumentPosition(btn) & Node.DOCUMENT_POSITION_FOLLOWING)) {
|
||||
hidden_select = sel;
|
||||
}
|
||||
}
|
||||
if (!hidden_select) return;
|
||||
let node_select_value = hidden_select?.options[0]?.value;
|
||||
if (!node_select_value || node_select_value.indexOf("_default") === 0 || node_select_value.indexOf("_direct") === 0 || node_select_value.indexOf("_blackhole") === 0) {
|
||||
return;
|
||||
}
|
||||
let to_url = '<%=api.url("node_config")%>/' + node_select_value;
|
||||
if (node_select_value.indexOf("socks_") === 0) {
|
||||
to_url = '<%=api.url("socks_config")%>/' + node_select_value;
|
||||
}
|
||||
location.href = to_url;
|
||||
}
|
||||
|
||||
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
async function go() {
|
||||
const _status = document.getElementsByClassName('_socks_status');
|
||||
for (let i = 0; i < _status.length; i++) {
|
||||
const id = _status[i].getAttribute("socks_id");
|
||||
XHR.get('<%=api.url("socks_status")%>', {
|
||||
index: i,
|
||||
id: id
|
||||
},
|
||||
function(x, result) {
|
||||
const socks = _status[i].querySelector('._socks');
|
||||
const http = _status[i].querySelector('._http');
|
||||
if (socks) {
|
||||
socks.textContent = result.socks_status ? "✓" : "X";
|
||||
socks.style.fontWeight = "bold";
|
||||
socks.style.color = result.socks_status ? "green" : "red";
|
||||
}
|
||||
if (http) {
|
||||
if (result.use_http) {
|
||||
http.textContent = result.http_status ? "✓" : "X";
|
||||
http.style.fontWeight = "bold";
|
||||
http.style.color = result.http_status ? "green" : "red";
|
||||
} else {
|
||||
http.textContent = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const global = document.getElementById("cbi-<%=appname%>-" + global_id);
|
||||
if (global) {
|
||||
// 分流规则节点提前加入 “编辑” 占位符,以保证节点列表左对齐
|
||||
document.querySelectorAll('.td[data-widget="nodes_listvalue"][data-name="_node"] > div').forEach(container => {
|
||||
if (!container.querySelector('.shunt-node-placeholder')) {
|
||||
const placeholder = document.createElement('div');
|
||||
placeholder.className = 'shunt-node-placeholder';
|
||||
placeholder.style.cssText = 'display:inline-flex; width:40px; flex-shrink:0;';
|
||||
container.appendChild(placeholder);
|
||||
}
|
||||
});
|
||||
|
||||
await delay(100);
|
||||
|
||||
const all_node = global.querySelectorAll("[id]");
|
||||
const reg1 = /^cbid\..*node\.main$/;
|
||||
//const reg1 = /^cbid\..*\.(tcp_node|udp_node)\.main$/;
|
||||
|
||||
for (let i = 0; i < all_node.length; i++) {
|
||||
const el = all_node[i];
|
||||
if (!reg1.test(el.id)) continue;
|
||||
|
||||
const node_select = el;
|
||||
if (!node_select) continue;
|
||||
const cbid = el.id.replace(/\.main$/, "");
|
||||
const hidden_select = document.getElementById(cbid);
|
||||
const node_select_value = hidden_select?.options[0]?.value;
|
||||
if (!node_select_value || node_select_value.indexOf("tcp") === 0 || node_select_value.indexOf("_default") === 0 || node_select_value.indexOf("_direct") === 0 || node_select_value.indexOf("_blackhole") === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let html = '<a href="#" onclick="return to_edit_node(this, global_id);"><%:Edit%></a>';
|
||||
|
||||
const m = cbid.match(/\.node$/);
|
||||
if (m) {
|
||||
html += '<a href="#" onclick="window.open(\'' + '<%=api.url("get_redir_log")%>?id=default' + '\', \'_blank\')"><%:Log%></a>';
|
||||
}
|
||||
|
||||
html = '<div class="node-actions" style="display:inline-flex; align-items:center; gap:4px; flex-wrap:wrap; margin-left:4px;">' + html + '</div>'
|
||||
|
||||
const placeholder = node_select.parentElement.querySelector('.shunt-node-placeholder');
|
||||
if (placeholder) {
|
||||
placeholder.innerHTML = html;
|
||||
} else {
|
||||
node_select.insertAdjacentHTML("beforeend", html);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const socks = document.getElementById("cbi-<%=appname%>-socks");
|
||||
if (socks) {
|
||||
const socks_enabled_dom = document.getElementById("cbi-<%=appname%>-" + global_id + "-socks_enabled");
|
||||
const descr = socks.getElementsByClassName("cbi-section-descr")[0];
|
||||
descr.replaceWith(socks_enabled_dom);
|
||||
const rows = socks.getElementsByClassName("cbi-section-table-row");
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
try {
|
||||
const row = rows[i];
|
||||
const id = row.id;
|
||||
if (!id) continue;
|
||||
let dom_id = id + "-node";
|
||||
const cbid = dom_id.replace("cbi-", "cbid-").replace(new RegExp("-", 'g'), ".");
|
||||
dom_id = cbid + ".main";
|
||||
const node_select = document.getElementById(dom_id);
|
||||
if (!node_select) continue;
|
||||
|
||||
let html = '<a href="#" onclick="return to_edit_node(this, \'socks\');"><%:Edit%></a>';
|
||||
html += '<a href="#" onclick="window.open(\'' + '<%=api.url("get_socks_log")%>?name=' + id.replace("cbi-<%=appname%>-", "") + '\', \'_blank\')"><%:Log%></a>';
|
||||
|
||||
node_select.insertAdjacentHTML("afterend",
|
||||
'<div class="node-actions" style="display:inline-flex; align-items:center; gap:4px; flex-wrap:wrap; margin-left:4px;">'
|
||||
+ html + '</div>'
|
||||
);
|
||||
} catch(err) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(function () {
|
||||
let lastCount = 0;
|
||||
let stableTimes = 0;
|
||||
const STABLE_THRESHOLD = 5;
|
||||
function waitStable() {
|
||||
const list = document.querySelectorAll('[id^="cbid.<%=appname%>."][id$="node.main"]');
|
||||
const count = list.length;
|
||||
if (count === lastCount && count > 0) {
|
||||
stableTimes++;
|
||||
} else {
|
||||
stableTimes = 0;
|
||||
lastCount = count;
|
||||
}
|
||||
if (stableTimes >= STABLE_THRESHOLD) {
|
||||
go();
|
||||
return;
|
||||
}
|
||||
requestAnimationFrame(waitStable);
|
||||
}
|
||||
waitStable();
|
||||
})();
|
||||
|
||||
(function () {
|
||||
const startTime = Date.now();
|
||||
const TIMEOUT = 3000;
|
||||
|
||||
const waitForDnsSelect = () => {
|
||||
const dns_select = document.querySelector("select[id*='dns_shunt']");
|
||||
if (dns_select) {
|
||||
initDnsSelect(dns_select);
|
||||
return;
|
||||
}
|
||||
if (Date.now() - startTime >= TIMEOUT) {
|
||||
return;
|
||||
}
|
||||
requestAnimationFrame(waitForDnsSelect);
|
||||
};
|
||||
waitForDnsSelect();
|
||||
|
||||
const initDnsSelect = (dns_select) => {
|
||||
if (dns_select.value === "chinadns-ng") {
|
||||
addLogLink(dns_select);
|
||||
}
|
||||
if (dns_select._dnsLogBinded) return;
|
||||
dns_select._dnsLogBinded = true;
|
||||
|
||||
dns_select.addEventListener("change", () => {
|
||||
const existingLogLink = dns_select.parentElement.querySelector("a.dns-log-link");
|
||||
if (existingLogLink) {
|
||||
existingLogLink.remove();
|
||||
}
|
||||
if (dns_select.value === "chinadns-ng") {
|
||||
addLogLink(dns_select);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addLogLink = (select) => {
|
||||
if (select.parentElement.querySelector("a.dns-log-link")) {
|
||||
return;
|
||||
}
|
||||
const logLink = document.createElement("a");
|
||||
logLink.innerHTML = "<%:Log%>";
|
||||
logLink.href = "#";
|
||||
logLink.className = "dns-log-link";
|
||||
logLink.style.marginLeft = "10px";
|
||||
logLink.setAttribute("onclick", "window.open('" + '<%=api.url("get_chinadns_log")%>' + "?flag=default', '_blank')");
|
||||
select.insertAdjacentElement("afterend", logLink);
|
||||
};
|
||||
})();
|
||||
//]]>
|
||||
</script>
|
||||
@@ -1,111 +0,0 @@
|
||||
<%+cbi/valueheader%>
|
||||
|
||||
<label class="cbi-value-title">
|
||||
<%:Switch Mode%>
|
||||
</label>
|
||||
<div class="cbi-value-field">
|
||||
<div>
|
||||
<input class="btn cbi-button cbi-button-apply" type="button" onclick="switch_gfw_mode()" value="<%:GFW List%>" />
|
||||
<input class="btn cbi-button cbi-button-apply" type="button" onclick="switch_chnroute_mode()" value="<%:Not China List%>" />
|
||||
<input class="btn cbi-button cbi-button-apply" type="button" onclick="switch_returnhome_mode()" value="<%:China List%>" />
|
||||
<input class="btn cbi-button cbi-button-apply" type="button" onclick="switch_global_mode()" value="<%:Global Proxy%>" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var opt = {
|
||||
base: 'cbid.passwall.<%=self.cfgid or section%>',
|
||||
client: true,
|
||||
get: function (opt) {
|
||||
var obj;
|
||||
var id = this.base + '.' + opt;
|
||||
obj = document.getElementsByName(id)[0] || document.getElementById(id);
|
||||
if (obj) {
|
||||
var combobox = document.getElementById('cbi.combobox.' + id);
|
||||
if (combobox) {
|
||||
obj.combobox = combobox;
|
||||
}
|
||||
var div = document.getElementById(id);
|
||||
if (div && div.getElementsByTagName("li").length > 0) {
|
||||
obj = div;
|
||||
}
|
||||
return obj;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
set: function (opt, val) {
|
||||
var obj;
|
||||
obj = this.get(opt);
|
||||
if (obj) {
|
||||
var event = document.createEvent("HTMLEvents");
|
||||
event.initEvent("change", true, true);
|
||||
if (obj.type === 'checkbox') {
|
||||
obj.checked = val;
|
||||
} else {
|
||||
obj.value = val;
|
||||
if (obj.combobox) {
|
||||
obj.combobox.value = val;
|
||||
}
|
||||
|
||||
var list = obj.getElementsByTagName("li");
|
||||
if (list.length > 0) {
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var li = list[i];
|
||||
var data = li.getAttribute("data-value");
|
||||
li.removeAttribute("selected");
|
||||
li.removeAttribute("display");
|
||||
if (data && data == val) {
|
||||
li.setAttribute("selected", true);
|
||||
li.setAttribute("display", "0");
|
||||
}
|
||||
}
|
||||
var input = document.getElementsByName(obj.id)[0];
|
||||
if (input) {
|
||||
input.value = val;
|
||||
} else {
|
||||
var input = document.createElement("input");
|
||||
input.setAttribute("type", "hidden");
|
||||
input.setAttribute("name", obj.id);
|
||||
input.setAttribute("value", val);
|
||||
obj.appendChild(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
obj.dispatchEvent(event);
|
||||
} catch (err) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function switch_gfw_mode() {
|
||||
opt.set("use_gfw_list", true);
|
||||
opt.set("chn_list", "0");
|
||||
opt.set("tcp_proxy_mode", "disable");
|
||||
opt.set("udp_proxy_mode", "disable");
|
||||
}
|
||||
|
||||
function switch_chnroute_mode() {
|
||||
opt.set("use_gfw_list", true);
|
||||
opt.set("chn_list", "direct");
|
||||
opt.set("tcp_proxy_mode", "proxy");
|
||||
opt.set("udp_proxy_mode", "proxy");
|
||||
}
|
||||
|
||||
function switch_returnhome_mode() {
|
||||
opt.set("use_gfw_list", false);
|
||||
opt.set("chn_list", "proxy");
|
||||
opt.set("tcp_proxy_mode", "disable");
|
||||
opt.set("udp_proxy_mode", "disable");
|
||||
}
|
||||
|
||||
function switch_global_mode() {
|
||||
opt.set("use_gfw_list", false);
|
||||
opt.set("chn_list", "0");
|
||||
opt.set("tcp_proxy_mode", "proxy");
|
||||
opt.set("udp_proxy_mode", "proxy");
|
||||
}
|
||||
</script>
|
||||
<%+cbi/valuefooter%>
|
||||
@@ -1,282 +0,0 @@
|
||||
<%
|
||||
local map = self.map
|
||||
local api = map.api
|
||||
-%>
|
||||
|
||||
<style>
|
||||
/*!
|
||||
Pure v1.0.1
|
||||
Copyright 2013 Yahoo!
|
||||
Licensed under the BSD License.
|
||||
https://github.com/pure-css/pure/blob/master/LICENSE.md
|
||||
*/
|
||||
.pure-g{letter-spacing:-.31em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-align-content:flex-start;-ms-flex-line-pack:start;align-content:flex-start}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active) {table .pure-g{display:block}}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u{display:inline-block;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-g [class*=pure-u]{font-family:-apple-system,BlinkMacSystemFont,sans-serif}.pure-u-1,.pure-u-1-1,.pure-u-1-12,.pure-u-1-2,.pure-u-1-24,.pure-u-1-3,.pure-u-1-4,.pure-u-1-5,.pure-u-1-6,.pure-u-1-8,.pure-u-10-24,.pure-u-11-12,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-2-24,.pure-u-2-3,.pure-u-2-5,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24,.pure-u-3-24,.pure-u-3-4,.pure-u-3-5,.pure-u-3-8,.pure-u-4-24,.pure-u-4-5,.pure-u-5-12,.pure-u-5-24,.pure-u-5-5,.pure-u-5-6,.pure-u-5-8,.pure-u-6-24,.pure-u-7-12,.pure-u-7-24,.pure-u-7-8,.pure-u-8-24,.pure-u-9-24{display:inline-block;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-10-24,.pure-u-5-12{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-14-24,.pure-u-7-12{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-15-24,.pure-u-5-8{width:62.5%}.pure-u-16-24,.pure-u-2-3{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-18-24,.pure-u-3-4{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-20-24,.pure-u-5-6{width:83.3333%}.pure-u-21-24,.pure-u-7-8{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-24-24,.pure-u-5-5{width:100%}
|
||||
.block {
|
||||
margin: 0.5rem;
|
||||
padding: 0;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
line-height: 1;
|
||||
font-family: inherit;
|
||||
min-width: inherit;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
border: 1px solid rgba(0,0,0,.05);
|
||||
border-radius: .375rem;
|
||||
box-shadow: 0 0 2rem 0 rgba(136,152,170,.15);
|
||||
}
|
||||
.img-con {
|
||||
margin: 1rem;
|
||||
|
||||
}
|
||||
.green {
|
||||
font-size:.9rem;
|
||||
color: #2dce89;
|
||||
}
|
||||
.red {
|
||||
font-size:.9rem;
|
||||
color: #fb6340;
|
||||
}
|
||||
.yellow {
|
||||
font-size:.9rem;
|
||||
color: #fb9a05;
|
||||
}
|
||||
.block img {
|
||||
width: 48px;
|
||||
height: auto;
|
||||
/* float:right; */
|
||||
}
|
||||
.block h4 {
|
||||
font-size: .8125rem;
|
||||
font-weight: 600;
|
||||
margin: 1rem 0rem 1rem 1rem;
|
||||
color:#8898aa!important;
|
||||
line-height: 1.8em;
|
||||
min-height: 48px;
|
||||
border-radius: 12.375px;
|
||||
}
|
||||
|
||||
.check {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 720px) {
|
||||
.block {
|
||||
margin: 0.2rem;
|
||||
}
|
||||
.pure-u-1-4 {
|
||||
width: 50%;
|
||||
}
|
||||
.pure-u-1-2 {
|
||||
width: 100%;
|
||||
}
|
||||
.block h4 {
|
||||
margin: 1rem 0rem 1rem 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.img-con {
|
||||
margin: 1.5rem 0.5rem;
|
||||
}
|
||||
.block img {
|
||||
width: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<fieldset id="_passwall_status_fieldset" class="cbi-section">
|
||||
<legend>
|
||||
<%:Running Status%>
|
||||
</legend>
|
||||
<div class="pure-g status">
|
||||
<div class="pure-u-1-4">
|
||||
<div class="block pure-g">
|
||||
<div class="pure-u-1-3">
|
||||
<div class="img-con">
|
||||
<img src='data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPg0KCS5zdDB7ZmlsbDojMjBiOGQ0O30NCgkuc3Qxe2ZpbGw6I0ZGRkZGRjt9DQoJLnN0MntmaWxsOiNEMkU1RjE7fQ0KCS5zdDN7ZmlsbDojQjVDRkU0O30NCjwvc3R5bGU+DQo8Zz4NCgk8Y2lyY2xlIGNsYXNzPSJzdDAiIGN4PSIyNTYiIGN5PSIyNTYiIHI9IjI1NiIvPg0KCTxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik0zODAuNiwxNDcuM2wtNDUuNywyMzAuNWMwLDAtNi40LDE2LTI0LDguM2wtMTA1LjUtODAuOUwxNjcsMjg2LjdsLTY0LjYtMjEuN2MwLDAtOS45LTMuNS0xMC45LTExLjINCgkJYy0xLTcuNywxMS4yLTExLjgsMTEuMi0xMS44bDI1Ni44LTEwMC43QzM1OS41LDE0MS4yLDM4MC42LDEzMS45LDM4MC42LDE0Ny4zIi8+DQoJPHBhdGggY2xhc3M9InN0MiIgZD0iTTE5Ny4yLDM3NS4yYzAsMC0zLjEtMC4zLTYuOS0xMi40Yy0zLjgtMTIuMi0yMy4zLTc2LjEtMjMuMy03Ni4xbDE1NS4xLTk4LjVjMCwwLDktNS40LDguNiwwDQoJCWMwLDAsMS42LDEtMy4yLDUuNGMtNC44LDQuNS0xMjEuOCwxMDkuNy0xMjEuOCwxMDkuNyIvPg0KCTxwYXRoIGNsYXNzPSJzdDMiIGQ9Ik0yNDUuOCwzMzYuMmwtNDEuNywzOC4xYzAsMC0zLjMsMi41LTYuOCwwLjlsOC03MC43Ii8+DQo8L2c+DQo8L3N2Zz4NCg=='/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-2-3">
|
||||
<h4 id="status_tcp">TCP<br /><span class="red"><%:NOT RUNNING%></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1-4">
|
||||
<div class="block pure-g">
|
||||
<div class="pure-u-1-3">
|
||||
<div class="img-con">
|
||||
<img src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMy4wLjEsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0i5Zu+5bGCXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTAwIDEwMDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4NCgkuc3Qwe2ZpbGw6I0ZCQjE0MDt9DQoJLnN0MXtmaWxsOiNGRkZGRkY7fQ0KPC9zdHlsZT4NCjxjaXJjbGUgY2xhc3M9InN0MCIgY3g9IjUwIiBjeT0iNTAiIHI9IjUwIi8+DQo8cGF0aCBjbGFzcz0ic3QxIiBkPSJNODYuOCw0Ny42QzgzLjYsMzMsNzQuOCwyMi41LDY2LjUsMjNoLTAuM2MtMy4zLDAtNi41LDEuMi04LjksMy4zYy0xLjgsMi00LjQsMy4yLTcuMSwzLjINCgljLTIuNiwwLTUuMS0xLjEtNi45LTNjLTIuNS0yLjMtNS44LTMuNS05LjEtMy41aC0wLjRjLTguMi0wLjUtMTcuMSwxMC0yMC4zLDI0LjZjLTMuNSwxNS41LDAuOCwyOS44LDkuNSwzMS45DQoJYzYuMywxLjYsMTMuMy0zLjksMTcuOS0xM2gxOC43YzQuNyw5LjEsMTEuNiwxNC42LDE3LjksMTNDODYsNzcuNCw5MC4yLDYzLjEsODYuOCw0Ny42TDg2LjgsNDcuNnogTTQyLjksNDguNUgzNnY3LjJoLTUuNnYtNy4yDQoJaC02Ljl2LTUuOGg2Ljl2LTcuMkgzNnY3LjJoNi45TDQyLjksNDguNXogTTYyLjcsNDkuMmMtMiwwLjEtMy43LTEuNC0zLjgtMy40Yy0wLjEtMiwxLjQtMy43LDMuNC0zLjhjMi0wLjEsMy43LDEuNCwzLjgsMy40DQoJYzAsMC4xLDAsMC4xLDAsMC4yQzY2LjIsNDcuNSw2NC43LDQ5LjEsNjIuNyw0OS4yQzYyLjcsNDkuMiw2Mi43LDQ5LjIsNjIuNyw0OS4yeiBNNzMuNyw0OS4yYy0yLDAuMS0zLjctMS40LTMuOC0zLjQNCgljLTAuMS0yLDEuNC0zLjcsMy40LTMuOGMyLTAuMSwzLjcsMS40LDMuOCwzLjRjMCwwLjEsMCwwLjEsMCwwLjJjMC4xLDEuOS0xLjQsMy41LTMuMywzLjZINzMuN3oiLz4NCjwvc3ZnPg0K" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-2-3">
|
||||
<h4 id="status_udp">UDP<br /><span class="red"><%:NOT RUNNING%></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1-4">
|
||||
<div class="block pure-g">
|
||||
<div class="pure-u-1-3">
|
||||
<div class="img-con">
|
||||
<img src='data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAwIDEwMDAiPg0KICA8c3R5bGU+DQogICAgLnN0MXtmaWxsOiNmZmZ9DQogICAgLnN0MntmaWxsOiMwMTk1ZGV9DQogICAgLnN0NHtmaWxsOiNmMmYyZjJ9DQogIDwvc3R5bGU+DQogIDxjbGlwUGF0aCBpZD0iY2xpcENpcmNsZSI+DQogICAgPGNpcmNsZSBjeD0iNTAwIiBjeT0iNTAwIiByPSI0ODAiLz4NCiAgPC9jbGlwUGF0aD4NCiAgPGNpcmNsZSBjeD0iNTAwIiBjeT0iNTAwIiByPSI0ODAiIGZpbGw9IiMwMGEwZWYiLz4NCiAgPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXBDaXJjbGUpIj4NCiAgICA8cGF0aCBkPSJNMTI5LjUgMzQ1aDkwLjljMTkuOCAwIDM1LjYgMiA0Ny40IDRhNzcuNSA3Ny41IDAgMCAxIDM3LjYgMjMuNyAxMzEgMTMxIDAgMCAxIDIzLjcgMzkuNWM1LjkgMTUuOCA3LjkgMzUuNiA3LjkgNTkuMyAwIDE5LjgtMiAzNy42LTcuOSA1My40YTExNC45IDExNC45IDAgMCAxLTI1LjcgNDMuNSA3NyA3NyAwIDAgMS0zNS42IDE5LjhjLTExLjkgNC0yNS43IDUuOS00My41IDUuOWgtOTQuOVYzNDVoLjF6bTUxLjQgNDMuNXYxNjQuMWgzNy42YzEzLjggMCAyMy43IDAgMjkuNy0yIDcuOS0yIDE1LjgtNS45IDE5LjgtOS45IDUuOS00IDkuOS0xMy44IDEzLjgtMjMuNyA0LTExLjkgNS45LTI1LjcgNS45LTQ1LjVzLTItMzMuNi01LjktNDMuNWE2OSA2OSAwIDAgMC0xMy44LTIzLjcgNDUuMyA0NS4zIDAgMCAwLTIzLjctMTEuOWMtNy45LTItMTkuOC0yLTQxLjUtMmgtMjEuN3YtMS45aC0uMnpNMzgwLjYgNTk0LjFWMzQ1SDQzMGwxMDAuOCAxNjYuMVYzNDVoNDcuNHYyNDkuMWgtNDkuNEw0MjggNDMydjE2Mi4xaC00Ny40ek02MTcuOCA1MTMuMWw0OS40LTRjMiAxNS44IDcuOSAyNy43IDE3LjggMzUuNmE1NS4yIDU1LjIgMCAwIDAgMzUuNiAxMS45IDU5IDU5IDAgMCAwIDM1LjYtOS45IDMwLjkgMzAuOSAwIDAgMCAxMS45LTIzLjdjMC01LjktMi05LjktNS45LTE1LjgtNC00LTkuOS03LjktMTcuOC05LjktNS45LTItMTkuOC01LjktMzkuNS05LjlhMTU4LjcgMTU4LjcgMCAwIDEtNTcuMy0yMy43IDYyLjcgNjIuNyAwIDAgMS0yMy43LTQ5LjQgODEgODEgMCAwIDEgOS45LTM1LjYgNjcuNiA2Ny42IDAgMCAxIDMxLjYtMjUuNyAxMjQgMTI0IDAgMCAxIDQ5LjQtNy45YzMxLjYgMCA1NS40IDUuOSA3MS4yIDE5LjhzMjMuNyAzMS42IDI1LjcgNTUuNGwtNDkuNCAyYy0yLTExLjktNS45LTIxLjctMTMuOC0yNy43LTcuOS01LjktMTcuOC03LjktMzEuNi03LjlzLTI1LjcgNC0zMy42IDkuOWMtNS45IDQtNy45IDkuOS03LjkgMTUuOHMyIDExLjkgNy45IDE1LjhjNS45IDUuOSAyMS43IDkuOSA0NS41IDE1LjggMjMuNyA1LjkgNDEuNSAxMS45IDUzLjQgMTcuOHMyMS43IDEzLjggMjcuNyAyNS43YTc4LjEgNzguMSAwIDAgMSA5LjkgMzkuNWMwIDEzLjgtNCAyNy43LTExLjkgMzkuNWE3Ny42IDc3LjYgMCAwIDEtMzMuNiAyNy43IDEzNS42IDEzNS42IDAgMCAxLTUzLjQgOS45Yy0zMS42IDAtNTcuMS01LjktNzMuMi0yMS43LTE4LjItMTcuOS0yNS45LTQxLjctMjkuOS02OS4zeiIgY2xhc3M9InN0MSIvPg0KICAgIDxnPg0KICAgICAgPHBhdGggZD0iTTI0OC4xIDU1MC42YzcuOS0yIDE1LjgtNS45IDE5LjgtOS45IDUuOS00IDkuOS0xMy44IDEzLjgtMjMuNyA0LTExLjkgNS45LTI1LjcgNS45LTQ1LjVzLTItMzMuNi01LjktNDMuNWE2OSA2OSAwIDAgMC0xMy44LTIzLjcgNDUuMyA0NS4zIDAgMCAwLTIzLjctMTEuOWMtNy45LTItMTkuOC0yLTQxLjUtMkgxODF2MTYyLjFoMzcuNmMxMy43LjEgMjMuNi4xIDI5LjUtMS45eiIgY2xhc3M9InN0MiIvPg0KICAgICAgPHBhdGggZD0iTTQ3Ni42IDk0OUg4NzBhNzkuMyA3OS4zIDAgMCAwIDc5LjEtNzkuMVY1MTUuMUw3OTMuMSAzNzJhNzkuNSA3OS41IDAgMCAxIDE4LjUgNDguMWwtNDkuNCAyYy0yLTExLjktNS45LTIxLjctMTMuOC0yNy43LTcuOS01LjktMTcuOC03LjktMzEuNi03LjlzLTI1LjcgNC0zMy42IDkuOWMtNS45IDQtNy45IDkuOS03LjkgMTUuOHMyIDExLjkgNy45IDE1LjhjNS45IDUuOSAyMS43IDkuOSA0NS41IDE1LjhzNDEuNSAxMS45IDUzLjQgMTcuOCAyMS43IDEzLjggMjcuNyAyNS43YTc4LjEgNzguMSAwIDAgMSA5LjkgMzkuNWMwIDEzLjgtNCAyNy43LTExLjkgMzkuNWE3Ny42IDc3LjYgMCAwIDEtMzMuNiAyNy43IDEzNS42IDEzNS42IDAgMCAxLTUzLjQgOS45Yy0zMS42IDAtNTcuMS01LjktNzMuMi0yMS43LTE4LTE3LjgtMjUuNy00MS41LTI5LjctNjkuMmw0OS40LTRjMiAxNS44IDcuOSAyNy43IDE3LjggMzUuNmE1NS4yIDU1LjIgMCAwIDAgMzUuNiAxMS45IDU5IDU5IDAgMCAwIDM1LjYtOS45IDMwLjkgMzAuOSAwIDAgMCAxMS45LTIzLjdjMC01LjktMi05LjktNS45LTE1LjgtNC00LTkuOS03LjktMTcuOC05LjktNS45LTItMTkuOC01LjktMzkuNS05LjlhMTU4LjcgMTU4LjcgMCAwIDEtNTcuMy0yMy43IDYyLjcgNjIuNyAwIDAgMS0xOS4zLTcyLjJsLTQ5LjgtNDUuNnYyNDguNGgtNDkuNEw0MjggNDMydjE2Mi4xaC00Ny40VjQ1Mi4zbC01MS40LTQwLjFjNS45IDE1LjggNy45IDM1LjYgNy45IDU5LjMgMCAxOS44LTIgMzcuNi03LjkgNTMuNGExMTQuOSAxMTQuOSAwIDAgMS0yNS43IDQzLjUgNzcgNzcgMCAwIDEtMzUuNiAxOS44Yy0xMS45IDQtMjUuNyA1LjktNDMuNSA1LjloLTk0Ljl2LTYuOWwtLjQgNi45TDQ3Ni42IDk0OXoiIGNsYXNzPSJzdDIiLz4NCiAgICA8L2c+DQogIDwvZz4NCjwvc3ZnPg0K'/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-2-3">
|
||||
<h4 id="status_dns">DNS<br /><span class="red"><%:NOT RUNNING%></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1-4">
|
||||
<div class="block pure-g">
|
||||
<div class="pure-u-1-3">
|
||||
<div class="img-con">
|
||||
<img src='data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIGlkPSJMYXllcl8xIiB4PSIwIiB5PSIwIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48c3R5bGU+LnN0MCwuc3Qxe2ZpbGw6bm9uZTtzdHJva2U6IzI4NGE2YTtzdHJva2Utd2lkdGg6LjI2O3N0cm9rZS1taXRlcmxpbWl0OjEwfS5zdDF7c3Ryb2tlLXdpZHRoOi4xM30uc3Q0e2ZpbGw6IzMzNzhiY30uc3Q1e2ZpbGw6IzE2OWJkNn0uc3Q2e2ZpbGw6IzAwYThkYX08L3N0eWxlPjxwYXRoIGQ9Im0yNTUuOCAxODkuNS01My4yLTYwLjEiIGNsYXNzPSJzdDAiLz48cGF0aCBkPSJtODggMTQzLjMgNDEuNiA2MC44IiBjbGFzcz0ic3QxIi8+PHBhdGggZD0ibTI1NS44IDE4OS42IDUyLjYtNjAuOG0xMSAxMjcuOSA2Mi41LTUyLjZtLTYyLjUgNTIuNSA2Mi41IDU0LjlNMzA5IDM4MWwtNTMuMi01OW0tNTMuMiA1OSA1My4yLTU5bS0xMjYuMS0xMi44IDYwLjEtNTIuNk0xMjkuNyAyMDRsNjAuMSA1Mi42bTY2LTY3LjFMMTI5LjcgMjA0bTEyNi4xLTE0LjVMMzgxLjkgMjA0bS02Mi41IDUyLjYtMTEtMTI3LjhtMTEgMTI3LjhMMzA5IDM4MW03Mi44LTY5LjQtMTI2IDEwLjRtLTEyNi4xLTEyLjhMMjU1LjggMzIybS01My4yIDU5LTEyLjgtMTI0LjNtMTIuOC0xMjcuMy0xMi44IDEyNy4yIiBjbGFzcz0ic3QwIi8+PHBhdGggZD0ibTcxLjkgMjIwLjIgNTcuOC0xNi4ybTEyLjEtMTE4IDYwLjggNDMuNG0xMy45LTcwLjYtMTMuOSA3MC42bTEzLjktNzAuNiA5MS45IDcwbS0xMy45LTcwIDEzLjkgNzBNMzY5LjEgODZsLTYwLjggNDIuOG0xMTUuMiAxNC41LTExNS4xLTE0LjVtNzMuNCA3NS4yIDQxLjYtNjAuOE0zODEuOCAyMDRsNTkgMTYuMm0tNTktMTYuMiA1Ny44IDg3LjlNMjk0LjUgNTguOGwtOTIgNzAuNk0zODEuOCAyMDQgMzY5IDg2bTEyLjggMjI1LjYgNTcuOC0xOS43bS01Ny44IDE5LjcgNTktOTEuNG0tNTkgOTEuNCA0MS42IDU2LjFtLTQxLjYtNTYuMUwzNjkgNDI0LjlNMzA5IDM4MWw2MC4xIDQ0TTMwOSAzODFsMTE0LjUtMTMuM00zMDkgMzgxbC0xMy44IDcxbTEzLjgtNzEtOTIuNSA3MW0tMTMuOS03MSAxMy45IDcxLjJNMjAyLjYgMzgxbDkyLjYgNzFtLTkyLjUtNzEtNjAuOCA0NG02MC44LTQ0TDg4IDM2OC4ybTQxLjctNTktNDEuNyA1OW00MS43LTU5IDEyLjEgMTE1LjdtLTEyLjEtMTE1LjctNTguNC0xNy45bTU4LjQtODcuMy01OC40IDg3LjNtNTguNCAxNy45LTU3LjgtODkuMU0xNDEuOCA4NmwtMTIuMSAxMThNODggMTQzLjNsMTE0LjUtMTMuOSIgY2xhc3M9InN0MSIvPjxwYXRoIGQ9Ik0xNDEuOCA4NiA5Ny4zIDY1LjJNMTQxLjggODZsLTMuNS00OC42bTc4LjIgMjEuNC03OC4xLTIxLjRtNzguMSAyMS40TDE4MyAxNy43bTMzLjUgNDEuMSAxMy45LTQ5LjJNMTQxLjggODZsNDEuMS02OC4ybTExMS42IDQxTDIzMC4zIDkuNm02NC4yIDQ5LjJMMjgwIDcuOW0tNjMuNiA1MC45TDI4MCA3LjltMTQuNSA1MC45TDMyOCAxNy43TTM2OS4xIDg2IDMyOCAxNy44TTM2OS4xIDg2bDQtNDguNm0tNzguNiAyMS40IDc4LjYtMjEuNG0tNCA0OC42IDQ1LjYtMjMuMU0zNjkuMSA4Nmw3OS4yIDEyLjFtLTI0LjggNDUuMiAyNC45LTQ1LjFtLTI0LjkgNDUuMS04LjctODAuNG04LjcgODAuNCA1Mi00bS01MiA0IDcxLjIgNDAuNW0tNTMuOSAzNi40IDM0LjctODFtLTM0LjcgODEgNTMuOC0zNi40bS01My44IDM2LjQgNjAuMSAxMi4xbS02MS4yIDU5LjYgNjEuMy01OS42bS02MS4zIDU5LjYgNjMtMTEuNm0tNjEuOS02MC4xIDYxLjkgNjAuMW0tNjMgMTEuNiA1My44IDM1LjltLTcwIDM5LjggNzAtMzkuOW0tNzAgMzkuOSA1MS41IDUuMm0tMzUuMi04MC45IDM1LjIgODFtLTUxLjQtNS4zIDI0LjMgNDUuNm0tMjQuMy00NS42LTkuMyA3OS44bS0uMS4xLTQ1LjEtMjIuNm0wIDAgMy41IDUwLjRtLTMuNS01MC40IDc4LjYtMTEuNm0tNzguNiAxMS42TDMyOCA0OTQuM00yOTUuMiA0NTJsMzIuOCA0Mi4yTTI5NS4yIDQ1Mmw3Ny41IDIzLjFNMjk1LjIgNDUyIDI4MCA1MDRtMTUuMi01Mi02NC44IDUybS0xMy45LTUyIDEzLjkgNTJtLTEzLjktNTIgNjMuNSA1Mm0tNjMuNS01Mi0zMyA0Mi4ybS00MS43LTY5LjMgNDEuNiA2OS40bTMzLjEtNDIuMy03OC42IDI0LjNtMy45LTUxLjQtNCA1MS41bTQtNTEuNS00NS4xIDI0LjlNODggMzY4LjJsOC43IDgxLjVtNDUuMS0yNC44LTc5LjItMTFNODggMzY4LjJsLTI1LjQgNDUuNk04OCAzNjguMmwtNTIgNS4ybTUyLTUuMi03MS42LTM5LjltNTQuOS0zNy01NC45IDM3bTU0LjktMzctNjIuNS0xMW02Mi41IDExLTM1LjIgODIuMW0zNS44LTE1My4yLTYzIDYwLjFtNjIuNCAxMS02Mi41LTU5bTYzLjEtMTIuMS01My44LTM1LjltNTMuOCAzNS45LTYzIDEyLjFtNjMtMTIuMS0zNS45LTgxbTUyIDQuMS01Mi00bTUyIDQtNjkuOSA0MS4xTTg4IDE0My4zbC0yNC44LTQyLjhNODggMTQzLjNsOS4zLTc4LjFNMTQxLjggODZsLTc4LjcgMTQuNSIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2UtbWl0ZXJsaW1pdDoxMDtzdHJva2U6IzI0NDA1ZDtzdHJva2Utd2lkdGg6LjEwMSIvPjxwYXRoIGQ9Im0xNTguOSAyODYuNy40LTYwLjggNjAuOC40LS40IDYwLjh6bTY1LjgtNjcgLjQtNjAuOCA2MC44LjQtLjQgNjAuOHptMCAxMzMuNC40LTYwLjggNjAuOC40LS40IDYwLjh6bTY2LjQtNjcgLjQtNjAuOCA2MC44LjQtLjQgNjAuOHoiIHN0eWxlPSJmaWxsOiMyNTZlYTUiLz48cGF0aCBkPSJtMjg3LjYgMTUwIC4zLTQyLjIgNDIuNC4zLS4zIDQyLjJ6bS0xMDYtLjEuMy00Mi4yIDQyLjQuMy0uMyA0Mi4yem0tNzMuNCA3NS4yLjMtNDIuMiA0Mi40LjMtLjMgNDIuMnptMCAxMDYuMS40LTQyLjIgNDIuNC40LS40IDQyLjJ6bTI1Mi44LS44LjMtNDIuMiA0Mi40LjMtLjMgNDIuMnptLjgtMTA1IC40LTQyLjIgNDIuNC40LS40IDQyLjJ6IiBjbGFzcz0ic3Q0Ii8+PHBhdGggZD0ibTczLjkgMTU3IC4yLTI3LjggMjcuNy4yLS4yIDI3Ljh6bTU0LjEtNTYuNS4yLTI3LjggMjcuNy4yLS4yIDI3Ljh6bTc0LjYtMjcuOS4yLTI3LjggMjcuNy4yLS4yIDI3Ljh6TTU3LjUgMjMzLjlsLjItMjcuOCAyNy41LjItLjIgMjcuOHptMzUyLTEwNC4yIDI3LjgtLjIuMiAyNy43LTI3LjguMnptLTU0LjItNTcuNSAyNy44LS4yLjIgMjcuNy0yNy44LjJ6TTI4MC41IDQ1bDI3LjgtLjIuMiAyNy43LTI3LjguMnptMTQ1LjEgMTYxLjUgMjcuOC0uMi4yIDI3LjctMjcuOC4yeiIgY2xhc3M9InN0NSIvPjxwYXRoIGQ9Im0xODEgNDAzLjIuMy00Mi4yIDQyLjQuMy0uMyA0Mi4yem0xMDYgMCAuMy00Mi4yIDQyLjQuMy0uMyA0Mi4yeiIgY2xhc3M9InN0NCIvPjxwYXRoIGQ9Im00MDkuMyAzODEuOC4yLTI3LjggMjcuNy4yLS4yIDI3Ljh6bS01My42IDU3LjEuMi0yNy44IDI3LjcuMi0uMiAyNy44em0tNzQuNSAyNy40LjItMjcuOCAyNy43LjItLjIgMjcuOHpNNDI1LjYgMzA1bC4yLTI3LjggMjcuNy4yLS4yIDI3Ljh6TTczLjkgMzU0LjFsMjcuOC0uMi4yIDI3LjctMjcuOC4yem01NC4yIDU2LjggMjcuOC0uMi4yIDI3LjctMjcuOC4yem03NC4yIDI3LjMgMjcuOC0uMi4yIDI3LjctMjcuOC4yek01Ny44IDI3Ny4zbDI3LjgtLjIuMiAyNy43LTI3LjkuMnoiIGNsYXNzPSJzdDUiLz48cGF0aCBpZD0iQiIgZD0ibTQ2Ny41IDE0Ni43LjEtMTUgMTUuMy4xLS4xIDE1eiIgY2xhc3M9InN0NiIvPjxwYXRoIGlkPSJCXzAwMDAwMDk3NTAxNDU3OTIwNjU5ODgxODMwMDAwMLine truncated
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-2-3">
|
||||
<h4 id="status_haproxy">HAPROXY<br /><span class="red">N/A</span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1-4 check" onclick="check_connect('aliyun', 'https://www.aliyun.com')" title="<%:TLS handshake test, latency for reference only%>">
|
||||
<div class="block pure-g">
|
||||
<div class="pure-u-1-3">
|
||||
<div class="img-con">
|
||||
<img src="data:image/svg+xml;base64,PHN2ZyB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBoZWlnaHQ9IjQwMCIgd2lkdGg9IjQwMCIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMTY1MyAxNjUzIiBjbGFzcz0iaWNvbiIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQgbWVldCI+Cgk8cmVjdCB3aWR0aD0iMTY1MyIgaGVpZ2h0PSIxNjUzIiBmaWxsPSJ0cmFuc3BhcmVudCIvPgoJPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwgMzE0LjUpIj4KCQk8cGF0aCBmaWxsPSIjRjc2RTA1IiBkPSJNMjk1LjYzMzQ4OCA3ODMuNjMyODE3YTc0Ljg5ODIxNiA3NC44OTgyMTYgMCAwIDEtNTkuNzIwNjA0LTcyLjU4ODU4di0zOTkuMjM3MTg4YTc3LjUzNzgwMSA3Ny41Mzc4MDEgMCAwIDEgNTkuNzIwNjA0LTcyLjU4ODU4bDM3MC42OTY2NzktODAuMzQyMzYgMzguOTMzODc0LTE1OC44NzAwMDVIMjc0Ljg0Njc1OUEyNzIuODY3MDcgMjcyLjg2NzA3IDAgMCAwIDAuMTY0OTc0IDI3Mi44NzMxNzR2NDcxLjgyNTc2OGEyNzYuNjYxNDczIDI3Ni42NjE0NzMgMCAwIDAgMjc0LjY4MTc4NSAyNzQuODQ2NzU5aDQzMC40MTcyODJsLTM4LjkzMzg3NC0xNTguMjEwMTA5ek0xMzc0LjIzMzc5MyAwLjAwNjEwNEg5NDAuNTE3MDI5bDM5LjU5Mzc3MSAxNTguODcwMDA1IDM3MC42OTY2NzkgODAuMzQyMzZhNzUuMDYzMTkgNzUuMDYzMTkgMCAwIDEgNTkuNzIwNjA0IDcyLjU4ODU4djM5OS4yMzcxODhhNzcuODY3NzQ5IDc3Ljg2Nzc0OSAwIDAgMS01OS43MjA2MDQgNzIuNTg4NThsLTM3MC42OTY2NzkgODAuMzQyMzYtMzkuNTkzNzcxIDE1OC44NzAwMDVIMTM3NC4yMzM3OTNhMjc1LjAxMTczMyAyNzUuMDExNzMzIDAgMCAwIDI3NS42NzE2MjktMjc1LjUwNjY1NVYyNzIuODczMTc0QTI3Ni4xNjY1NTEgMjc2LjE2NjU1MSAwIDAgMCAxMzc0LjIzMzc5MyAwLjAwNjEwNHoiLz4KCQk8cGF0aCBmaWxsPSIjRjc2RTA1IiBkPSJNNjY2LjMzMDE2NyA0OTAuNjM4OTEzaDMxNi4yNTUyNDR2MzguOTMzODc1SDY2Ni4zMzAxNjd6Ii8+Cgk8L2c+Cjwvc3ZnPg==" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-2-3">
|
||||
<h4 id="status_aliyun"><%:AliCloud Conn.%><br /><span id="_aliyun_status" class="red"><%:Touch Check%></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1-4 check" onclick="check_connect('google', 'https://www.google.com/generate_204')" title="<%:TLS handshake test, latency for reference only%>">
|
||||
<div class="block pure-g">
|
||||
<div class="pure-u-1-3">
|
||||
<div class="img-con">
|
||||
<img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjFlbSIgc3R5bGU9ImZsZXg6bm9uZTtsaW5lLWhlaWdodDoxIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIxZW0iIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHRpdGxlPkdvb2dsZTwvdGl0bGU+PHBhdGggZD0iTTIzIDEyLjI0NWMwLS45MDUtLjA3NS0xLjU2NS0uMjM2LTIuMjVoLTEwLjU0djQuMDgzaDYuMTg2Yy0uMTI0IDEuMDE0LS43OTcgMi41NDItMi4yOTQgMy41NjlsLS4wMjEuMTM2IDMuMzMyIDIuNTMuMjMuMDIyQzIxLjc3OSAxOC40MTcgMjMgMTUuNTkzIDIzIDEyLjI0NXoiIGZpbGw9IiM0Mjg1RjQiPjwvcGF0aD48cGF0aCBkPSJNMTIuMjI1IDIzYzMuMDMgMCA1LjU3NC0uOTc4IDcuNDMzLTIuNjY1bC0zLjU0Mi0yLjY4OGMtLjk0OC42NDgtMi4yMiAxLjEtMy44OTEgMS4xYTYuNzQ1IDYuNzQ1IDAgMDEtNi4zODYtNC41NzJsLS4xMzIuMDExLTMuNDY1IDIuNjI4LS4wNDUuMTI0QzQuMDQzIDIwLjUzMSA3LjgzNSAyMyAxMi4yMjUgMjN6IiBmaWxsPSIjMzRBODUzIj48L3BhdGg+PHBhdGggZD0iTTUuODQgMTQuMTc1QTYuNjUgNi42NSAwIDAxNS40NjMgMTJjMC0uNzU4LjEzOC0xLjQ5MS4zNjEtMi4xNzVsLS4wMDYtLjE0Ny0zLjUwOC0yLjY3LS4xMTUuMDU0QTEwLjgzMSAxMC44MzEgMCAwMDEgMTJjMCAxLjc3Mi40MzYgMy40NDcgMS4xOTcgNC45MzhsMy42NDItMi43NjN6IiBmaWxsPSIjRkJCQzA1Ij48L3BhdGg+PHBhdGggZD0iTTEyLjIyNSA1LjI1M2MyLjEwOCAwIDMuNTI5Ljg5MiA0LjM0IDEuNjM4bDMuMTY3LTMuMDMxQzE3Ljc4NyAyLjA4OCAxNS4yNTUgMSAxMi4yMjUgMSA3LjgzNCAxIDQuMDQzIDMuNDY5IDIuMTk3IDcuMDYybDMuNjMgMi43NjNhNi43NyA2Ljc3IDAgMDE2LjM5OC00LjU3MnoiIGZpbGw9IiNFQjQzMzUiPjwvcGF0aD48L3N2Zz4=" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-2-3">
|
||||
<h4 id="status_google"><%:Google Conn.%><br /><span id="_google_status" class="red"><%:Touch Check%></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1-4 check" onclick="check_connect('github', 'https://github.com')" title="<%:TLS handshake test, latency for reference only%>">
|
||||
<div class="block pure-g">
|
||||
<div class="pure-u-1-3">
|
||||
<div class="img-con">
|
||||
<img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjI1MDAiIHZpZXdCb3g9IjAgLTMuMSAyNDkwLjMgMjQ5MyIgd2lkdGg9IjI0OTQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGVsbGlwc2UgY3g9IjEyNDUuMiIgY3k9IjEyNDMuNCIgZmlsbD0iI2ZmZiIgcng9IjEyMTcuNiIgcnk9IjEyNDYuNSIvPjxwYXRoIGQ9Im0xMjQ1LjIgMS42Yy02ODcuNiAwLTEyNDUuMiA1NTcuNC0xMjQ1LjIgMTI0NS4xIDAgNTUwLjIgMzU2LjggMTAxNi45IDg1MS41IDExODEuNSA2Mi4yIDExLjUgODUuMS0yNyA4NS4xLTU5LjkgMC0yOS43LTEuMi0xMjcuOC0xLjctMjMxLjgtMzQ2LjQgNzUuMy00MTkuNS0xNDYuOS00MTkuNS0xNDYuOS01Ni42LTE0My45LTEzOC4zLTE4Mi4yLTEzOC4zLTE4Mi4yLTExMy03Ny4zIDguNS03NS43IDguNS03NS43IDEyNSA4LjggMTkwLjkgMTI4LjMgMTkwLjkgMTI4LjMgMTExLjEgMTkwLjQgMjkxLjMgMTM1LjMgMzYyLjMgMTAzLjUgMTEuMi04MC41IDQzLjQtMTM1LjQgNzkuMS0xNjYuNS0yNzYuNi0zMS41LTU2Ny4zLTEzOC4zLTU2Ny4zLTYxNS40IDAtMTM1LjkgNDguNi0yNDcgMTI4LjMtMzM0LjItMTIuOS0zMS4zLTU1LjUtMTU3LjkgMTIuMS0zMjkuNCAwIDAgMTA0LjYtMzMuNSAzNDIuNSAxMjcuNiA5OS4zLTI3LjYgMjA1LjgtNDEuNCAzMTEuNy00MS45IDEwNS44LjUgMjEyLjQgMTQuMyAzMTEuOSA0MS45IDIzNy43LTE2MS4xIDM0Mi4xLTEyNy42IDM0Mi4xLTEyNy42IDY3LjggMTcxLjUgMjUuMSAyOTguMiAxMi4yIDMyOS41IDc5LjggODcuMiAxMjguMSAxOTguMyAxMjguMSAzMzQuMiAwIDQ3OC4yLTI5MS4zIDU4My42LTU2OC42IDYxNC40IDQ0LjcgMzguNiA4NC41IDExNC40IDg0LjUgMjMwLjYgMCAxNjYuNi0xLjQgMzAwLjctMS40IDM0MS43IDAgMzMuMSAyMi40IDcyIDg1LjUgNTkuNyA0OTQuNS0xNjQuOCA4NTAuOC02MzEuNCA4NTAuOC0xMTgxLjQgMC02ODcuNy01NTcuNS0xMjQ1LjEtMTI0NS4xLTEyNDUuMSIgZmlsbD0iIzVjNmJjMCIvPjwvc3ZnPg==" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-2-3">
|
||||
<h4 id="status_github"><%:GitHub Conn.%><br /><span id="_github_status" class="red"><%:Touch Check%></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1-4 check" onclick="check_connect('youtube', 'https://www.youtube.com')" title="<%:TLS handshake test, latency for reference only%>">
|
||||
<div class="block pure-g">
|
||||
<div class="pure-u-1-3">
|
||||
<div class="img-con">
|
||||
<img src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgd2lkdGg9IjMwMCIKICAgaGVpZ2h0PSIzMDAiCiAgIHZlcnNpb249IjEuMSIKICAgaWQ9InN2ZzYiCiAgIHNvZGlwb2RpOmRvY25hbWU9IllvdVR1YmVfc29jaWFsX3JlZF9jaXJjbGVfKDIwMjQpLnN2ZyIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMS4yLjIgKGIwYTg0ODY1NDEsIDIwMjItMTItMDEpIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxkZWZzCiAgIGlkPSJkZWZzMTAiIC8+PHNvZGlwb2RpOm5hbWVkdmlldwogICBpZD0ibmFtZWR2aWV3OCIKICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICBib3JkZXJjb2xvcj0iIzAwMDAwMCIKICAgYm9yZGVyb3BhY2l0eT0iMC4yNSIKICAgaW5rc2NhcGU6c2hvd3BhZ2VzaGFkb3c9IjIiCiAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgIGlua3NjYXBlOnBhZ2VjaGVja2VyYm9hcmQ9IjAiCiAgIGlua3NjYXBlOmRlc2tjb2xvcj0iI2QxZDFkMSIKICAgc2hvd2dyaWQ9ImZhbHNlIgogICBpbmtzY2FwZTp6b29tPSIxLjI1IgogICBpbmtzY2FwZTpjeD0iMTQyLjQiCiAgIGlua3NjYXBlOmN5PSIxNTAuOCIKICAgaW5rc2NhcGU6d2luZG93LXdpZHRoPSIxMTg3IgogICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSI1ODIiCiAgIGlua3NjYXBlOndpbmRvdy14PSIwIgogICBpbmtzY2FwZTp3aW5kb3cteT0iMCIKICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ic3ZnNiIgLz4KICA8cmVjdAogICB3aWR0aD0iMzAwIgogICBoZWlnaHQ9IjMwMCIKICAgZmlsbD0iI2ZmZiIKICAgcnk9IjE1MCIKICAgaWQ9InJlY3QyIgogICBzdHlsZT0iZmlsbDojZmYwMDMzO2ZpbGwtb3BhY2l0eToxIiAvPgogIDxwYXRoCiAgIGZpbGw9InJlZCIKICAgZD0iTTE0OS45Mzc1MDU4NyA3OS4yMjI2NzM2NHMtNjMuMjE4ODUuMDAwMDU5LTc5LjA4NTkzOTI2IDQuMTIzMDQ3MDdjLTguNDk1Nzg3MTQgMi4zNzM4MDIwNC0xNS40OTE0MDgyNSA5LjM2OTUwMDE1LTE3Ljg2NTIzNDI5IDE3Ljk5MDIzNzI5LTQuMTIyOTUzMDYgMTUuODY3MTAwMjUtNC4xMjMwNDcwNiA0OC43MjY1NjA3OC00LjEyMzA0NzA2IDQ4LjcyNjU2MDc4cy4wMDAwOTQgMzIuOTg0MjAwNTIgNC4xMjMwNDcwNiA0OC42MDE1NjA3N2MyLjM3MzgyNjA0IDguNjIwNjIwMTQgOS4yNDQ1MDYxNSAxNS40OTEzODAyNSAxNy44NjUyMzQyOSAxNy44NjUyNDAyOSAxNS45OTIwMzAyNSA0LjI0Nzg4MDA3IDc5LjA4NTkzOTI2IDQuMjQ4MDQwMDcgNzkuMDg1OTM5MjYgNC4yNDgwNDAwN3M2My4zNDQxODEwMS0uMDAwMDUgNzkuMjEwOTQxMjctNC4xMjMwNDAwN2M4LjYyMDc5MDE0LTIuMzczODEwMDQgMTUuNDkxMzMwMjUtOS4xMTk2NjAxNSAxNy43NDAyMzAyOC0xNy44NjUyNDAyOSA0LjI0NzkzMDA3LTE1Ljc0MjMyMDI1IDQuMjQ4MDUwMDctNDguNjAxNTYwNzcgNC4yNDgwNTAwNy00OC42MDE1NjA3N3MuMTI0ODQtMzIuOTg0NDYwNTMtNC4yNDgwNTAwNy00OC44NTE1NjA3OGMtMi4yNDg5MDAwMy04LjYyMDczNzE0LTkuMTE5NDQwMTQtMTUuNDkxMzM0MjUtMTcuNzQwMjMwMjgtMTcuNzQwMjM3MjktMTUuODY2NzYwMjYtNC4zNzI4NDcwNy03OS4yMTA5NDEyNy00LjM3MzA0NzA3LTc5LjIxMDk0MTI3LTQuMzczMDQ3MDd6bS0yMC4xMTUyMzAzMiA0MC40ODA0NjQ2NSA1Mi41OTk2MTA4NCAzMC4zNTkzODA0OS01Mi41OTk2MTA4NCAzMC4yMzQzODA0OHYtNjAuNTkzNzYwOTd6IgogICBpZD0icGF0aDQiCiAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjEiIC8+Cjwvc3ZnPg==" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-2-3">
|
||||
<h4 id="status_youtube"><%:YouTube Conn.%><br /><span id="_youtube_status" class="red"><%:Touch Check%></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
//<![CDATA[
|
||||
var imgs = document.getElementsByTagName('img');
|
||||
for (var i = 0 ; i < imgs.length; i++) {
|
||||
document.getElementsByTagName('img')[i].setAttribute("oncontextmenu","return false;");
|
||||
document.getElementsByTagName('img')[i].setAttribute("ondragstart","return false;");
|
||||
}
|
||||
XHR.poll(5, '<%=api.url("index_status")%>', null,
|
||||
function (x, data) {
|
||||
var status_dns = document.getElementById('status_dns');
|
||||
var status_haproxy = document.getElementById('status_haproxy');
|
||||
if (data) {
|
||||
if (true) {
|
||||
var status_tcp = document.getElementById('status_tcp');
|
||||
if (status_tcp) {
|
||||
var text = 'TCP<br />';
|
||||
if (data["tcp_status"])
|
||||
text += '<span class="green"><%:RUNNING%></span>';
|
||||
else
|
||||
text += '<span class="red"><%:NOT RUNNING%></span>';
|
||||
status_tcp.innerHTML = text;
|
||||
}
|
||||
}
|
||||
|
||||
if (true) {
|
||||
var status_udp = document.getElementById('status_udp');
|
||||
if (status_udp) {
|
||||
var text = 'UDP<br />';
|
||||
if (data["udp_status"])
|
||||
text += '<span class="green"><%:RUNNING%></span>';
|
||||
else
|
||||
text += '<span class="red"><%:NOT RUNNING%></span>';
|
||||
status_udp.innerHTML = text;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.dns_mode_status) {
|
||||
status_dns.innerHTML = 'DNS<br /><span class="green"><%:RUNNING%></span>';
|
||||
} else {
|
||||
status_dns.innerHTML = 'DNS<br /><span class="red"><%:NOT RUNNING%></span>';
|
||||
}
|
||||
if (status_haproxy) {
|
||||
if (data.haproxy_status === "-1") {
|
||||
status_haproxy.innerHTML = 'HAPROXY<br /><span class="red"><%:UNINSTALLED%></span>';
|
||||
} else if (data.haproxy_status === "0") {
|
||||
status_haproxy.innerHTML = 'HAPROXY<br /><span class="green"><%:RUNNING%></span>';
|
||||
} else {
|
||||
status_haproxy.innerHTML = 'HAPROXY<br /><span class="red"><%:NOT RUNNING%></span>';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function check_connect(type, url) {
|
||||
var s = document.getElementById('_' + type + '_status');
|
||||
if (s) {
|
||||
var div = s.parentNode.parentNode.parentNode.parentNode;
|
||||
div.removeAttribute('onclick');
|
||||
s.innerHTML = '<%:Check...%>';
|
||||
var sendDate = (new Date()).getTime();
|
||||
XHR.get('<%=api.url("connect_status")%>', {
|
||||
type: type,
|
||||
url : url
|
||||
},
|
||||
function(x, rv) {
|
||||
if (rv.ping_type && rv.ping_type == "curl") {
|
||||
var use_time = rv.use_time;
|
||||
if (use_time < 1000) {
|
||||
s.className="green";
|
||||
} else if (use_time < 2000) {
|
||||
s.className="yellow";
|
||||
} else {
|
||||
s.className="red";
|
||||
}
|
||||
s.innerHTML = use_time + " ms";
|
||||
} else if (rv.status) {
|
||||
s.className="green";
|
||||
s.innerHTML = "<%:Working...%>";
|
||||
}
|
||||
else {
|
||||
s.className="red";
|
||||
s.innerHTML = '<%:Problem detected!%>';
|
||||
}
|
||||
div.setAttribute('onclick','check_connect("' + type + '","' + url + '")');
|
||||
}
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
//]]>
|
||||
</script>
|
||||
</fieldset>
|
||||
@@ -1,76 +0,0 @@
|
||||
<%
|
||||
local map = self.map
|
||||
local appname = map.config
|
||||
-%>
|
||||
|
||||
<style>
|
||||
@media screen and (min-width: 1152px) {
|
||||
#cbi-<%=appname%>-haproxy_config input[id*=".<%=appname%>."][id$=".haproxy_port"],
|
||||
#cbi-<%=appname%>-haproxy_config input[id*=".<%=appname%>."][id$=".lbweight"] {
|
||||
width: 100px !important;
|
||||
min-width: unset !important;
|
||||
max-width: unset !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
#cbi-<%=appname%>-haproxy_config select[id*=".<%=appname%>."][id$=".export"] {
|
||||
width: 130px !important;
|
||||
min-width: unset !important;
|
||||
max-width: unset !important;
|
||||
}
|
||||
|
||||
#cbi-<%=appname%>-haproxy_config select[id*=".<%=appname%>."][id$=".backup"] {
|
||||
width: 100px !important;
|
||||
min-width: unset !important;
|
||||
max-width: unset !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
let monitorStartTime = Date.now();
|
||||
|
||||
const monitorInterval = setInterval(function () {
|
||||
if (Date.now() - monitorStartTime > 3000) {
|
||||
clearInterval(monitorInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = Array.from(document.querySelectorAll("tr.cbi-section-table-row"))
|
||||
.filter(row => !row.classList.contains("placeholder")); // 排除无配置行
|
||||
|
||||
if (rows.length <= 1) return;
|
||||
|
||||
const lastRow = rows[rows.length - 1];
|
||||
const secondLastRow = rows[rows.length - 2];
|
||||
|
||||
const lastInput = lastRow.querySelector("input[name$='.haproxy_port']");
|
||||
const secondLastInput = secondLastRow.querySelector("input[name$='.haproxy_port']");
|
||||
|
||||
if (!lastInput || !secondLastInput) return;
|
||||
|
||||
// 如果还没绑定 change 事件,绑定一次
|
||||
if (!lastInput.dataset.bindChange) {
|
||||
lastInput.dataset.bindChange = "1";
|
||||
lastInput.addEventListener("input", () => {
|
||||
lastInput.dataset.userModified = "1";
|
||||
});
|
||||
}
|
||||
|
||||
// 如果用户手动修改过,就不再自动设置
|
||||
if (lastInput.dataset.userModified === "1") return;
|
||||
|
||||
const lastVal = lastInput.value.trim();
|
||||
const secondLastVal = secondLastInput.value.trim();
|
||||
|
||||
const lbssHiddenInput = lastRow.querySelector("div.cbi-dropdown > div > input[type='hidden'][name$='.lbss']");
|
||||
if (!lbssHiddenInput) {
|
||||
if (lastVal !== secondLastVal && secondLastVal !== "" && secondLastVal !== "0") {
|
||||
lastInput.value = secondLastVal;
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
//]]>
|
||||
</script>
|
||||
@@ -1,31 +0,0 @@
|
||||
<%
|
||||
local map = self.map
|
||||
local api = map.api
|
||||
local haproxy_enable = map:get("@global_haproxy[0]", "balancing_enable") or "0"
|
||||
local console_port = map:get("@global_haproxy[0]", "console_port") or "1188"
|
||||
-%>
|
||||
<p id="_status"></p>
|
||||
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
const haproxy_enable = "<%=haproxy_enable%>";
|
||||
if (haproxy_enable == "1") {
|
||||
function openwebui(){
|
||||
var url = window.location.hostname + ":<%=console_port%>";
|
||||
window.open('http://' + url, 'target', '');
|
||||
}
|
||||
XHR.poll(5, '<%=api.url("haproxy_status")%>', null,
|
||||
function(x, result) {
|
||||
if (x && x.status == 200) {
|
||||
var _status = document.getElementById('_status');
|
||||
if (_status) {
|
||||
if (result.status) {
|
||||
_status.innerHTML = '<input type="button" class="btn cbi-button cbi-button-apply" value="<%:Enter interface%>" onclick="openwebui()" />';
|
||||
} else {
|
||||
_status.innerHTML = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
//]]></script>
|
||||
@@ -1,73 +0,0 @@
|
||||
<%
|
||||
local map = self.map
|
||||
local api = map.api
|
||||
local config = map.config
|
||||
local verify_option = self.verify_option
|
||||
local section_id = verify_option.section.section
|
||||
-%>
|
||||
<% if verify_option then -%>
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const shunt_list = JSON.parse('<%=self.shunt_list%>');
|
||||
const dom_node_id = "<%=verify_option:cbid(section_id)%>";
|
||||
let uci_node_val = "";
|
||||
const onChange = (e) => {
|
||||
const new_val = e.target.value;
|
||||
const to_shunt = shunt_list.some(element => element.id == new_val);
|
||||
if (to_shunt) {
|
||||
const map = document.getElementById("cbi-<%=config%>");
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = 'node_save_before';
|
||||
hiddenInput.value = uci_node_val;
|
||||
map.appendChild(hiddenInput);
|
||||
const saveBtn = document.getElementsByClassName('cbi-button-save');
|
||||
if (saveBtn && saveBtn.length > 0) {
|
||||
saveBtn[saveBtn.length - 1].click();
|
||||
}
|
||||
/*
|
||||
XHR.get('<%=api.url("update_config")%>', {
|
||||
id: "<%=section_id%>",
|
||||
data: JSON.stringify({
|
||||
node: new_val,
|
||||
mode: "1"
|
||||
})
|
||||
}, function(x, data) {
|
||||
if (x && x.status == 200 && data.code == 1) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert("<%:Error%>");
|
||||
}
|
||||
});
|
||||
*/
|
||||
} else {
|
||||
try { shunt.hide(); } catch (error) {}
|
||||
}
|
||||
};
|
||||
const check = () => {
|
||||
const el = document.getElementById(dom_node_id);
|
||||
if (!el) {
|
||||
try { shunt.hide(); } catch (error) {}
|
||||
return;
|
||||
}
|
||||
let is_shunt = shunt_list.some(element => element.id == el.value);
|
||||
if (is_shunt) {
|
||||
try { shunt.show(); } catch (error) {}
|
||||
}
|
||||
if (!el.hasEventListener) {
|
||||
uci_node_val = el.value;
|
||||
el.addEventListener("change", onChange);
|
||||
el.hasEventListener = true;
|
||||
}
|
||||
};
|
||||
const observer = new MutationObserver(check);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
check();
|
||||
});
|
||||
//]]>
|
||||
</script>
|
||||
<%- end %>
|
||||
@@ -1,147 +0,0 @@
|
||||
<%
|
||||
local map = self.map
|
||||
local api = map.api
|
||||
local config = map.config
|
||||
local node_id = self.node_id
|
||||
local verify_option = self.verify_option
|
||||
local section_id = self.section_id
|
||||
local section_type = self.section_type
|
||||
local tab_name = self.tab_name
|
||||
-%>
|
||||
<style>
|
||||
.shunt_hidden {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
var normal_list = JSON.parse('<%=self.normal_list%>');
|
||||
|
||||
function check_fakedns(id, val) {
|
||||
const dom_id = "cbid.table." + id + ".";
|
||||
const fakedns_dom = document.getElementById(dom_id + "_fakedns");
|
||||
if (!val) {
|
||||
const selectNode = document.getElementById(dom_id + "_node");
|
||||
val = selectNode.value;
|
||||
}
|
||||
if (fakedns_dom) {
|
||||
if (val == "" || val == "_direct") {
|
||||
fakedns_dom.value = 0;
|
||||
fakedns_dom.checked = false;
|
||||
fakedns_dom.classList.add('shunt_hidden');
|
||||
} else {
|
||||
fakedns_dom.classList.remove('shunt_hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
function check_proxy_tag(id, val) {
|
||||
const dom_id = "cbid.table." + id + ".";
|
||||
const cbid = dom_id + "_proxy_tag";
|
||||
const proxy_tag_dom = document.getElementById(cbid + ".main");
|
||||
if (!val) {
|
||||
const selectNode = document.getElementById(dom_id + "_node");
|
||||
val = selectNode.value;
|
||||
}
|
||||
if (val == "" || val.startsWith("_") || val.startsWith("socks_") || !normal_list.find(element => element.id == val)) {
|
||||
const hiddenSelect = document.getElementById(cbid);
|
||||
const panel = document.getElementById(cbid + ".panel");
|
||||
const display = document.getElementById(cbid + ".display");
|
||||
const labelSpan = document.getElementById(cbid + ".label");
|
||||
const searchInput = document.getElementById(cbid + ".search");
|
||||
const listContainer = document.getElementById(cbid + ".list");
|
||||
lv_change(cbid, listContainer, hiddenSelect, labelSpan, searchInput, "", "<%:Close (Not use)%>");
|
||||
proxy_tag_dom?.classList.add('shunt_hidden');
|
||||
} else {
|
||||
proxy_tag_dom?.classList.remove('shunt_hidden');
|
||||
}
|
||||
}
|
||||
function refresh_depends() {
|
||||
const table_dom = document.getElementById("cbi-<%=config%>-<%=section_type%>");
|
||||
if (table_dom) {
|
||||
const trs = table_dom.getElementsByTagName("tr");
|
||||
for (let i = 1; i < trs.length;i++) {
|
||||
check_fakedns(i);
|
||||
check_proxy_tag(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
const shunt = {
|
||||
tab() {
|
||||
// Move the shunt options to Tab
|
||||
<% if tab_name then -%>
|
||||
const shunt_taboption = document.getElementById("container.<%=config%>.<%=section_id%>.<%=tab_name%>");
|
||||
if (shunt_taboption) {
|
||||
const shunt_option_list = document.getElementById("cbi-<%=config%>-<%=section_type%>");
|
||||
if (shunt_option_list) {
|
||||
shunt_taboption.appendChild(shunt_option_list);
|
||||
}
|
||||
}
|
||||
<%- end %>
|
||||
},
|
||||
hide() {
|
||||
document.getElementById("cbi-<%=config%>-<%=section_type%>")?.classList.add('shunt_hidden');
|
||||
<% if tab_name then -%>
|
||||
try { document.getElementById("tab.<%=config%>.<%=section_id%>.<%=tab_name%>")?.classList.add('shunt_hidden'); } catch (error) {}
|
||||
try { document.querySelector('li[data-tab*="<%=tab_name%>"]')?.classList.add('shunt_hidden'); } catch (error) {}
|
||||
const shunt_taboption = document.getElementById("container.<%=config%>.<%=section_id%>.<%=tab_name%>");
|
||||
if (shunt_taboption) {
|
||||
[...shunt_taboption.querySelectorAll('[name]')].forEach(el => {
|
||||
const nameValue = el.getAttribute('name');
|
||||
el.setAttribute('o-name', nameValue);
|
||||
el.removeAttribute('name');
|
||||
});
|
||||
}
|
||||
<%- end %>
|
||||
},
|
||||
show() {
|
||||
document.getElementById("cbi-<%=config%>-<%=section_type%>")?.classList.remove('shunt_hidden');
|
||||
<% if tab_name then -%>
|
||||
try { document.getElementById("tab.<%=config%>.<%=section_id%>.<%=tab_name%>")?.classList.remove('shunt_hidden'); } catch (error) {}
|
||||
try { document.querySelector('li[data-tab*="<%=tab_name%>"]')?.classList.remove('shunt_hidden'); } catch (error) {}
|
||||
const shunt_taboption = document.getElementById("container.<%=config%>.<%=section_id%>.<%=tab_name%>");
|
||||
if (shunt_taboption) {
|
||||
[...shunt_taboption.querySelectorAll('[o-name]')].forEach(el => {
|
||||
const nameValue = el.getAttribute('o-name');
|
||||
el.setAttribute('name', nameValue);
|
||||
el.removeAttribute('o-name');
|
||||
});
|
||||
}
|
||||
<%- end %>
|
||||
}
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
shunt.tab();
|
||||
waitForElement('select[name*="<%=config%>"][name*="shunt_group"]', function(el) {
|
||||
let o_val = el.value;
|
||||
el.addEventListener("change", () => {
|
||||
el.blur();
|
||||
if (o_val != el.value) {
|
||||
update_config("<%=node_id%>", {
|
||||
shunt_group: getOption("<%=config%>", "<%=section_id%>", "shunt_group").value,
|
||||
}, get_current_url());
|
||||
}
|
||||
});
|
||||
});
|
||||
setTimeout(() => {
|
||||
refresh_depends();
|
||||
const table_dom = document.getElementById("cbi-<%=config%>-<%=section_type%>");
|
||||
if (table_dom) {
|
||||
const trs = table_dom.getElementsByTagName("tr");
|
||||
for (let i = 1; i < trs.length;i++) {
|
||||
const selectNode = document.getElementById("cbid.table." + i + "._node.main");
|
||||
selectNode.addEventListener("change", function(el){
|
||||
let new_val = el.target.value
|
||||
check_fakedns(i, new_val);
|
||||
check_proxy_tag(i, new_val);
|
||||
});
|
||||
if (i == trs.length - 1) {
|
||||
const newArray = window.lv_dropdown_data["cbid.table." + i + "._node"].ungrouped.filter(item => item.key !== "_default");
|
||||
window.lv_dropdown_data["cbid.table." + i + "._node"].ungrouped = newArray
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 200)
|
||||
});
|
||||
//]]>
|
||||
</script>
|
||||
@@ -1,48 +0,0 @@
|
||||
<%
|
||||
local api = require "luci.passwall.api"
|
||||
local section_tag = api.is_js_luci() and "div" or "fieldset"
|
||||
-%>
|
||||
<%+header%>
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
var log_textarea = null;
|
||||
var first_load_done = false;
|
||||
|
||||
function scrollToBottom() {
|
||||
log_textarea.scrollTop = log_textarea.scrollHeight;
|
||||
}
|
||||
|
||||
function clearlog(btn) {
|
||||
XHR.get('<%=api.url("clear_log")%>', null,
|
||||
function(x, data) {
|
||||
if(x && x.status == 200 && log_textarea) {
|
||||
log_textarea.innerHTML = "";
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
XHR.poll(5, '<%=api.url("get_log")%>', null,
|
||||
function(x, data) {
|
||||
if(x && x.status == 200) {
|
||||
if (!log_textarea) log_textarea = document.getElementById('log_textarea');
|
||||
var wasBottom = (log_textarea.scrollTop + log_textarea.clientHeight >= log_textarea.scrollHeight - 10);
|
||||
log_textarea.innerHTML = x.responseText;
|
||||
if (!first_load_done) {
|
||||
scrollToBottom();
|
||||
first_load_done = true;
|
||||
}
|
||||
else if (wasBottom) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
//]]>
|
||||
</script>
|
||||
<<%=section_tag%> class="cbi-section" id="_log_fieldset">
|
||||
<input class="btn cbi-button cbi-button-remove" type="button" onclick="clearlog()" value="<%:Clear logs%>" />
|
||||
<textarea id="log_textarea" class="cbi-input-textarea" style="width: 100%;margin-top: 10px;" data-update="change" rows="40" wrap="off" readonly="readonly"></textarea>
|
||||
</<%=section_tag%>>
|
||||
<%+footer%>
|
||||
@@ -1,73 +0,0 @@
|
||||
<%
|
||||
local map = self.map
|
||||
local api = map.api
|
||||
local sid = self.section
|
||||
local config = map.config
|
||||
-%>
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
let node_id = "<%=sid%>";
|
||||
let node_config_url = '<%=api.url("node_config")%>/' + node_id;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
waitForElement('select[name*="<%=config%>"][name*="type"]', function(el) {
|
||||
let o_val = el.value;
|
||||
el.addEventListener("change", () => {
|
||||
el.blur();
|
||||
if (o_val != el.value) {
|
||||
let save = true;
|
||||
if (save) {
|
||||
/*
|
||||
const saveBtn = document.getElementsByClassName('cbi-button-save');
|
||||
if (saveBtn && saveBtn.length > 0) {
|
||||
saveBtn[saveBtn.length - 1].click();
|
||||
}
|
||||
*/
|
||||
update_config(node_id, {
|
||||
remarks: getOption("<%=config%>", "<%=sid%>", "remarks").value,
|
||||
group: getOption("<%=config%>", "<%=sid%>", "group").value,
|
||||
type: el.value,
|
||||
//protocol: ''
|
||||
}, node_config_url);
|
||||
}
|
||||
}
|
||||
});
|
||||
const fromUrlCache = JSON.parse(sessionStorage.getItem("fromUrl"));
|
||||
if (fromUrlCache && fromUrlCache.savetime && (Date.now() - fromUrlCache.timestamp) < fromUrlCache.savetime) {
|
||||
fromUrl(fromUrlCache.link, true)
|
||||
} else {
|
||||
sessionStorage.removeItem("fromUrl");
|
||||
}
|
||||
});
|
||||
waitForElement('select[name*="<%=config%>"][name*="protocol"]', function(el) {
|
||||
let o_val = el.value;
|
||||
const name_split = el.name.split(".");
|
||||
const name = name_split[name_split.length - 1];
|
||||
el.addEventListener("change", () => {
|
||||
el.blur();
|
||||
if (el.value.startsWith("_") || (o_val.startsWith("_") && !el.value.startsWith("_"))) {
|
||||
if (el.value.startsWith("_")) {
|
||||
const map = document.getElementById("cbi-<%=config%>");
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = 'load' + el.value;
|
||||
hiddenInput.value = '1';
|
||||
map.appendChild(hiddenInput);
|
||||
}
|
||||
const saveBtn = document.getElementsByClassName('cbi-button-save');
|
||||
if (saveBtn && saveBtn.length > 0) {
|
||||
saveBtn[saveBtn.length - 1].click();
|
||||
}
|
||||
/*
|
||||
update_config(node_id, {
|
||||
remarks: getOption("<%=config%>", "<%=sid%>", "remarks").value,
|
||||
group: getOption("<%=config%>", "<%=sid%>", "group").value,
|
||||
protocol: el.value,
|
||||
}, node_config_url);
|
||||
*/
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
//]]>
|
||||
</script>
|
||||
@@ -1,35 +0,0 @@
|
||||
<%
|
||||
local map = self.map
|
||||
local api = map.api
|
||||
local sid = self.section
|
||||
-%>
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
function fetchCertSha256(el) {
|
||||
if (el.getAttribute("data-loading") === "1") return;
|
||||
el.setAttribute("data-loading", "1");
|
||||
el.style.pointerEvents = "none";
|
||||
el.style.color = "red";
|
||||
XHR.get('<%=api.url("fetch_certsha256")%>', { id: "<%=sid%>" }, function(x, res) {
|
||||
el.setAttribute("data-loading", "0");
|
||||
el.style.pointerEvents = "";
|
||||
el.style.color = "";
|
||||
if (!x || x.status !== 200) {
|
||||
alert("<%:Error%>");
|
||||
return;
|
||||
}
|
||||
if (!res || res.code !== 1 || !res.data) {
|
||||
alert(res && res.msg ? res.msg : "<%:Fetch Failed!%>\n" +
|
||||
"<%:Requires OpenSSL to retrieve the TLS certificate chain fingerprint.%>\n" +
|
||||
"<%:QUIC protocols such as HY2 also require curl with http3 support.%>"
|
||||
);
|
||||
return;
|
||||
}
|
||||
const inputs = document.querySelectorAll('input[id$="_tls_pinSHA256"]');
|
||||
inputs.forEach(function(input) {
|
||||
input.value = res.data;
|
||||
});
|
||||
});
|
||||
}
|
||||
//]]>
|
||||
</script>
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1,113 +0,0 @@
|
||||
<%
|
||||
local map = self.map
|
||||
local api = map.api
|
||||
-%>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
var appname = "<%= api.appname %>"
|
||||
|
||||
function confirmDeleteNode(remark) {
|
||||
if (!confirm("<%:Delete the subscribed node%>: " + remark + " ?"))
|
||||
return false;
|
||||
|
||||
fetch('<%= api.url("subscribe_del_node") %>?remark=' + encodeURIComponent(remark), {
|
||||
method: "GET"
|
||||
}).then(res => {
|
||||
if (res.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert("<%:Failed to delete.%>");
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
function confirmDeleteAll() {
|
||||
if (!confirm("<%:Are you sure you want to delete all subscribed nodes?%>"))
|
||||
return false;
|
||||
|
||||
fetch('<%= api.url("subscribe_del_all") %>', {
|
||||
method: "GET"
|
||||
}).then(res => {
|
||||
if (res.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert("<%:Failed to delete.%>");
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
function ManualSubscribe(sectionId) {
|
||||
var urlInput = document.querySelector("input[name='cbid." + appname + "." + sectionId + ".url']");
|
||||
var currentUrl = urlInput ? urlInput.value.trim() : "";
|
||||
if (!currentUrl) {
|
||||
alert("<%:Subscribe URL cannot be empty.%>");
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('<%= api.url("subscribe_manual") %>?section='
|
||||
+ encodeURIComponent(sectionId)
|
||||
+ '&url='
|
||||
+ encodeURIComponent(currentUrl))
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (!data.success) {
|
||||
alert(data.msg || "Operation failed");
|
||||
} else {
|
||||
window.location.href = '<%= api.url("log") %>'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function ManualSubscribeAll() {
|
||||
var sectionIds = [];
|
||||
var urls = [];
|
||||
|
||||
var table = document.getElementById("cbi-" + appname + "-subscribe_list");
|
||||
var editBtns = table ? table.getElementsByClassName("cbi-button cbi-button-edit") : [];
|
||||
|
||||
for (var i = 0; i < editBtns.length; i++) {
|
||||
var btn = editBtns[i];
|
||||
var onclickStr = btn.getAttribute("onclick");
|
||||
if (!onclickStr) continue;
|
||||
|
||||
var id = onclickStr.substring(onclickStr.lastIndexOf('/') + 1, onclickStr.length - 1);
|
||||
if (!id) continue;
|
||||
|
||||
var urlInput = document.querySelector("input[name='cbid." + appname + "." + id + ".url']");
|
||||
var currentUrl = urlInput ? urlInput.value.trim() : "";
|
||||
if (!currentUrl) {
|
||||
alert("<%:Subscribe URL cannot be empty.%>");
|
||||
return;
|
||||
}
|
||||
|
||||
sectionIds.push(id);
|
||||
urls.push(currentUrl);
|
||||
}
|
||||
|
||||
if (sectionIds.length === 0) {
|
||||
//alert("No subscriptions found.");
|
||||
return;
|
||||
}
|
||||
|
||||
var params = new URLSearchParams();
|
||||
params.append("sections", sectionIds.join(","));
|
||||
params.append("urls", urls.join(","));
|
||||
|
||||
fetch('<%= api.url("subscribe_manual_all") %>', {
|
||||
method: 'POST',
|
||||
body: params
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (!data.success) {
|
||||
alert(data.msg || "Operation failed");
|
||||
} else {
|
||||
window.location.href = '<%= api.url("log") %>'
|
||||
}
|
||||
});
|
||||
}
|
||||
//]]>
|
||||
</script>
|
||||
@@ -1,192 +0,0 @@
|
||||
<%
|
||||
local map = self.map
|
||||
local api = map.api
|
||||
local fs = api.fs
|
||||
local has_old_geoip = fs.access("/tmp/bak_v2ray/geoip.dat")
|
||||
local has_old_geosite = fs.access("/tmp/bak_v2ray/geosite.dat")
|
||||
-%>
|
||||
|
||||
<style>
|
||||
div.cbi-value[id$="-gfwlist_update"],
|
||||
div.cbi-value[id$="-chnroute_update"],
|
||||
div.cbi-value[id$="-chnroute6_update"],
|
||||
div.cbi-value[id$="-chnlist_update"],
|
||||
div.cbi-value[id$="-geoip_update"],
|
||||
div.cbi-value[id$="-geosite_update"] {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="cbi-value" id="_rule_div">
|
||||
<label class="cbi-value-title">
|
||||
<%:Update Options%>
|
||||
</label>
|
||||
<div class="cbi-value-field">
|
||||
<div>
|
||||
<label>
|
||||
<input class="cbi-input-checkbox" type="checkbox" name="gfwlist" value="1" />
|
||||
gfwlist
|
||||
</label>
|
||||
<label>
|
||||
<input class="cbi-input-checkbox" type="checkbox" name="chnroute" value="1" />
|
||||
chnroute
|
||||
</label>
|
||||
<label>
|
||||
<input class="cbi-input-checkbox" type="checkbox" name="chnroute6" value="1" />
|
||||
chnroute6
|
||||
</label>
|
||||
<label>
|
||||
<input class="cbi-input-checkbox" type="checkbox" name="chnlist" value="1" />
|
||||
chnlist
|
||||
</label>
|
||||
<label>
|
||||
<input class="cbi-input-checkbox" type="checkbox" name="geoip" value="1" />
|
||||
geoip
|
||||
</label>
|
||||
<label>
|
||||
<input class="cbi-input-checkbox" type="checkbox" name="geosite" value="1" />
|
||||
geosite
|
||||
</label>
|
||||
<br><br>
|
||||
<input class="btn cbi-button cbi-button-apply" type="button" id="update_rules_btn" onclick="update_rules(this)" value="<%:Manually update%>" />
|
||||
<% if has_old_geoip then %>
|
||||
<input class="btn cbi-button cbi-button-reset" type="button" onclick="rollback_rules(this, 'geoip')" value="<%:Rollback%> GeoIP" />
|
||||
<% end %>
|
||||
<% if has_old_geosite then %>
|
||||
<input class="btn cbi-button cbi-button-reset" type="button" onclick="rollback_rules(this, 'geosite')" value="<%:Rollback%> Geosite" />
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const flags = [
|
||||
"gfwlist_update","chnroute_update","chnroute6_update",
|
||||
"chnlist_update","geoip_update","geosite_update"
|
||||
];
|
||||
const bindFlags = () => {
|
||||
let allBound = true;
|
||||
flags.forEach(flag => {
|
||||
const orig = Array.from(document.querySelectorAll(`input[name$=".${flag}"]`)).find(i => i.type === 'checkbox');
|
||||
if (!orig) { allBound = false; return; }
|
||||
// 隐藏最外层 div
|
||||
const wrapper = orig.closest('.cbi-value');
|
||||
if (wrapper && wrapper.style.display !== 'none') {
|
||||
wrapper.style.display = 'none';
|
||||
}
|
||||
const custom = document.querySelector(`.cbi-input-checkbox[name="${flag.replace('_update','')}"]`);
|
||||
if (!custom) { allBound = false; return; }
|
||||
custom.checked = orig.checked;
|
||||
// 自定义选择框与原生Flag双向绑定
|
||||
if (!custom._binded) {
|
||||
custom._binded = true;
|
||||
orig.addEventListener('change', () => {
|
||||
custom.checked = orig.checked;
|
||||
});
|
||||
custom.addEventListener('change', () => {
|
||||
orig.checked = custom.checked;
|
||||
orig.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
});
|
||||
}
|
||||
});
|
||||
return allBound;
|
||||
};
|
||||
|
||||
const geo2ruleFlag = () => {
|
||||
const geoEl = document.querySelector('input[type="checkbox"][name*="passwall"][name*="geo2rule"]');
|
||||
if (!geoEl) return;
|
||||
const updateCheckboxes = (disabled) => {
|
||||
flags.filter(flag => !flag.includes('geo')).forEach(flag => {
|
||||
const custom = document.querySelector(`.cbi-input-checkbox[name="${flag.replace('_update','')}"]`);
|
||||
if (custom) {
|
||||
//if (disabled) custom.checked = true;
|
||||
custom.parentElement.style.display = disabled ? 'none' : '';
|
||||
}
|
||||
});
|
||||
};
|
||||
updateCheckboxes(geoEl.checked);
|
||||
geoEl.addEventListener("change", () => {
|
||||
geoEl.blur();
|
||||
updateCheckboxes(geoEl.checked);
|
||||
});
|
||||
};
|
||||
|
||||
const bindAll = () => {
|
||||
bindFlags();
|
||||
geo2ruleFlag();
|
||||
};
|
||||
|
||||
const target = document.querySelector('form') || document.body;
|
||||
const observer = new MutationObserver(() => bindAll() ? observer.disconnect() : 0);
|
||||
observer.observe(target, { childList: true, subtree: true });
|
||||
const timer = setInterval(() => bindAll() ? (clearInterval(timer), observer.disconnect()) : 0, 300);
|
||||
setTimeout(() => { clearInterval(timer); observer.disconnect(); }, 5000);
|
||||
});
|
||||
|
||||
function update_rules(btn) {
|
||||
btn.disabled = true;
|
||||
btn.value = '<%:Updating...%>';
|
||||
var div = document.getElementById('_rule_div');
|
||||
var domList = div.getElementsByTagName('input');
|
||||
var checkBoxList = [];
|
||||
var len = domList.length;
|
||||
while(len--) {
|
||||
var dom = domList[len];
|
||||
if(dom.type == 'checkbox' && dom.checked) {
|
||||
checkBoxList.push(dom.name);
|
||||
}
|
||||
}
|
||||
XHR.get('<%=api.url("update_rules")%>', {
|
||||
update: checkBoxList.join(",")
|
||||
},
|
||||
function(x, data) {
|
||||
if(x && x.status == 200) {
|
||||
window.location.href = '<%=api.url("log")%>';
|
||||
} else {
|
||||
alert("<%:Error%>");
|
||||
btn.disabled = false;
|
||||
btn.value = '<%:Manually update%>';
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function rollback_rules(btn, type) {
|
||||
let ori_val = btn.value;
|
||||
btn.disabled = true;
|
||||
btn.value = '<%:Rollbacking...%>';
|
||||
var div = document.getElementById('_rule_div');
|
||||
var domList = div.getElementsByTagName('input');
|
||||
var checkBoxList = [];
|
||||
var len = domList.length;
|
||||
while(len--) {
|
||||
var dom = domList[len];
|
||||
if(dom.type == 'checkbox' && dom.checked) {
|
||||
if(type == 'geoip' && (dom.name === 'chnroute' || dom.name === 'chnroute6')) {
|
||||
checkBoxList.push(dom.name);
|
||||
}
|
||||
if(type == 'geosite' && (dom.name === 'gfwlist' || dom.name === 'chnlist')) {
|
||||
checkBoxList.push(dom.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
XHR.get('<%=api.url("rollback_rules")%>', {
|
||||
type: type,
|
||||
rules: checkBoxList.join(",")
|
||||
},
|
||||
function(x, data) {
|
||||
if(x && x.status == 200 && data.code == 1) {
|
||||
alert("<%:Success%>");
|
||||
btn.style.display = "none";
|
||||
} else {
|
||||
alert("<%:Error%>");
|
||||
btn.disabled = false;
|
||||
btn.value = ori_val;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
//]]>
|
||||
</script>
|
||||
@@ -1,433 +0,0 @@
|
||||
<%
|
||||
local map = self.map
|
||||
local api = map.api
|
||||
local appname = map.config
|
||||
local is_js_luci = api.is_js_luci()
|
||||
local section_tag = is_js_luci and "div" or "fieldset"
|
||||
local title_tag = is_js_luci and "h3" or "legend"
|
||||
-%>
|
||||
<script src="<%=resource%>/view/<%=appname%>/Sortable.min.js"></script>
|
||||
|
||||
<style>
|
||||
table th, .table .th {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table td, .table .td {
|
||||
text-align: center;
|
||||
/* white-space: nowrap; */
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
.td.cbi-section-actions {
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
.shunt_rule-wrapper {
|
||||
align-items: center;
|
||||
display: inline-flex !important;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.cbi-tabmenu > li {
|
||||
margin-right: 2px !important;
|
||||
}
|
||||
|
||||
.cbi-tabmenu > li:last-child {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.shunt_rule-wrapper .drag-handle {
|
||||
cursor: grab !important;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
font-weight: 100;
|
||||
padding: 0 !important;
|
||||
line-height: inherit;
|
||||
user-select: none;
|
||||
align-self: stretch;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.shunt_rule-wrapper .drag-handle:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* enable flex for small screens*/
|
||||
@media screen and (max-width: 1152px) {
|
||||
.cbi-section-table-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.cbi-section-table-titles {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.td-actions {
|
||||
padding-top: 0 !important;
|
||||
border-top-width: 0 !important;
|
||||
flex: 1 1 350px;
|
||||
}
|
||||
}
|
||||
|
||||
/* shrink actionbar even further for mobile devices */
|
||||
@media screen and (max-width: 500px) {
|
||||
.shunt_rule-wrapper {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.cbi-button {
|
||||
margin-left: 0 !important;
|
||||
margin-right: 1px !important;
|
||||
}
|
||||
|
||||
.td-actions {
|
||||
padding-left: 5px!important;
|
||||
padding-right: 5px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sortable-chosen {
|
||||
background-color: rgba(220, 235, 245, 0.4) !important;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.sortable-ghost {
|
||||
background: #cce5ff !important;
|
||||
height: 3px !important;
|
||||
}
|
||||
|
||||
.dragging-row {
|
||||
background-color: rgba(131, 191, 255, 0.7) !important;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
<% if api.is_js_luci() then -%>
|
||||
<script type="text/javascript">
|
||||
var cbi_t = [];
|
||||
function cbi_t_add(section, tab) {
|
||||
var t = document.getElementById('tab.' + section + '.' + tab);
|
||||
var c = document.getElementById('container.' + section + '.' + tab);
|
||||
|
||||
if( t && c ) {
|
||||
cbi_t[section] = (cbi_t[section] || [ ]);
|
||||
cbi_t[section][tab] = { 'tab': t, 'container': c, 'cid': c.id };
|
||||
}
|
||||
}
|
||||
|
||||
function cbi_t_switch(section, tab) {
|
||||
if( cbi_t[section] && cbi_t[section][tab] ) {
|
||||
// Before switching tabs, first deselect all currently active tabs.
|
||||
var o = cbi_t[section][tab];
|
||||
var h = document.getElementById('tab.' + section);
|
||||
for( var tid in cbi_t[section] ) {
|
||||
var o2 = cbi_t[section][tid];
|
||||
if( o.tab.id != o2.tab.id ) {
|
||||
o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab( |$)/, " cbi-tab-disabled ");
|
||||
o2.container.style.display = 'none';
|
||||
}
|
||||
else {
|
||||
if(h) h.value = tab;
|
||||
o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab-disabled( |$)/, " cbi-tab ");
|
||||
o2.container.style.display = 'block';
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
</script>
|
||||
<%- end %>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
function toPage(action, cbi_id) {
|
||||
if (action == "add") {
|
||||
const master = document.getElementById("shunt_rule_list");
|
||||
const dom = master.getElementsByClassName("cbi-tab")[0];
|
||||
const current_group = dom ? (dom.getAttribute("group_name") || "default") : "default";
|
||||
const add_name_val = document.getElementById("add_name_" + current_group).value;
|
||||
if (add_name_val.length > 1) {
|
||||
XHR.get('<%=api.url("add_shunt_rule")%>', {
|
||||
add_name: add_name_val,
|
||||
group: current_group
|
||||
},
|
||||
function(x, result) {
|
||||
if (x && x.status === 200) {
|
||||
if (result.code == 1) {
|
||||
window.location.href = result.data.redirect_url;
|
||||
} else {
|
||||
alert(result.data.message);
|
||||
}
|
||||
} else {
|
||||
alert("<%:Save failed!%>");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
if (action == "edit") {
|
||||
window.location.href='<%=api.url("shunt_rules")%>/' + cbi_id;
|
||||
}
|
||||
if (action == "del") {
|
||||
window.location.href = '<%=api.url("delete_select_shunt_rules")%>' + "?redirect=1&ids=" + cbi_id;
|
||||
}
|
||||
}
|
||||
|
||||
function row_top(btn, group) {
|
||||
const row = btn.closest("tr");
|
||||
if (!row) return;
|
||||
const parent = row.parentNode;
|
||||
let firstDataRow = parent.querySelector("tr:not(.cbi-section-table-titles)");
|
||||
if (firstDataRow && firstDataRow !== row) {
|
||||
parent.insertBefore(row, firstDataRow);
|
||||
set_order(group);
|
||||
}
|
||||
}
|
||||
|
||||
function get_shunt_rule_order(group) {
|
||||
let table = document.getElementById("cbi-<%=appname%>-shunt_rules-" + group + "-table");
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
let rows = table.querySelectorAll("tr.cbi-section-table-row");
|
||||
if (!rows || rows.length === 0) {
|
||||
return;
|
||||
}
|
||||
var ids = [];
|
||||
rows.forEach(function(row) {
|
||||
var id = row.id.replace("cbi-<%=appname%>-", "");
|
||||
ids.push(id);
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
|
||||
function set_order(group) {
|
||||
let table = document.getElementById("cbi-<%=appname%>-shunt_rules-" + group + "-table");
|
||||
if (!table) {
|
||||
alert("<%:No table!%>");
|
||||
return;
|
||||
}
|
||||
let rows = table.querySelectorAll("tr.cbi-section-table-row");
|
||||
if (!rows || rows.length === 0) {
|
||||
alert("<%:No shunt_rules!%>");
|
||||
return;
|
||||
}
|
||||
let ids = [];
|
||||
rows.forEach(function(row) {
|
||||
let id = row.id.replace("cbi-<%=appname%>-", "");
|
||||
ids.push(id);
|
||||
});
|
||||
const input_order = document.getElementsByName(`group.order.${group}`)[0];
|
||||
input_order.value = ids.join(" ");
|
||||
}
|
||||
|
||||
// List drag and rearrange
|
||||
function initSortableForTable(table) {
|
||||
if (!table) return null;
|
||||
let group = table.id.replace("cbi-<%=appname%>-shunt_rules-", "").replace("-table", "")
|
||||
var root = table.querySelector('tbody') || table;
|
||||
if (root._sortable_initialized) return root._sortable_instance;
|
||||
root._sortable_initialized = true;
|
||||
var opts = {
|
||||
handle: ".drag-handle",
|
||||
draggable: "tr.cbi-section-table-row",
|
||||
animation: 150,
|
||||
ghostClass: "dragging-row",
|
||||
fallbackOnBody: true,
|
||||
forceFallback: false,
|
||||
swapThreshold: 0.65,
|
||||
onEnd: function (evt) {
|
||||
set_order(group);
|
||||
}
|
||||
};
|
||||
try {
|
||||
var instance = Sortable.create(root, opts);
|
||||
root._sortable_instance = instance;
|
||||
return instance;
|
||||
} catch (err) {
|
||||
root._sortable_initialized = false;
|
||||
console.error("Sortable init failed:", err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function initAllSortable(group_shunt_rules) {
|
||||
if (typeof Sortable === 'undefined') {
|
||||
var retries = 0;
|
||||
var maxRetries = 25;
|
||||
var t = setInterval(function () {
|
||||
retries++;
|
||||
if (typeof Sortable !== 'undefined') {
|
||||
clearInterval(t);
|
||||
for (var group in group_shunt_rules) {
|
||||
var table = document.getElementById("cbi-<%=appname%>-shunt_rules-" + group + "-table");
|
||||
initSortableForTable(table);
|
||||
}
|
||||
} else if (retries >= maxRetries) {
|
||||
clearInterval(t);
|
||||
}
|
||||
}, 200);
|
||||
} else {
|
||||
for (var group in group_shunt_rules) {
|
||||
var table = document.getElementById("cbi-<%=appname%>-shunt_rules-" + group + "-table");
|
||||
initSortableForTable(table);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function escape_html(s) {
|
||||
return s.replace(/[&<>"']/g, c => ({
|
||||
"&":"&", "<":"<", ">":">", '"':""", "'":"'"
|
||||
}[c]));
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<script type="text/template" id="shunt_rules-table-template">
|
||||
<<%=section_tag%> class="cbi-section cbi-tblsection" id="cbi-<%=appname%>-shunt_rules-{{group}}-fieldset">
|
||||
<table class="table cbi-section-table" id="cbi-<%=appname%>-shunt_rules-{{group}}-table" style="">
|
||||
<tr class="tr cbi-section-table-titles anonymous">
|
||||
<th class="th cbi-section-table-cell" style="width:30%">ID</th>
|
||||
<th class="th cbi-section-table-cell" style="width:30%"><%:Remarks%></th>
|
||||
<th class="th cbi-section-table-cell cbi-section-actions"></th>
|
||||
</tr>
|
||||
{{shunt_rule-tr}}
|
||||
</table>
|
||||
<input type="hidden" name="group.order.{{group}}">
|
||||
<div class="cbi-section-create cbi-tblsection-create">
|
||||
<input type="text" id="add_name_{{group}}">
|
||||
<input class="cbi-button cbi-button-add" type="button" value="<%:Add%>" onclick="toPage('add')">
|
||||
</div>
|
||||
</<%=section_tag%>>
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="shunt_rule-tr-template">
|
||||
<tr class="tr cbi-section-table-row" id="cbi-<%=appname%>-{{id}}">
|
||||
<td class="td cbi-value-field"><b>{{id}}</b></td>
|
||||
<td class="td cbi-value-field">{{remarks}}</td>
|
||||
<td class="td cbi-section-table-cell nowrap cbi-section-actions td-actions">
|
||||
<div class="shunt_rule-wrapper">
|
||||
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:To Top%>" onclick="row_top(this, '{{group}}')" title="<%:To Top%>"/>
|
||||
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:Edit%>" onclick="toPage('edit', '{{id}}')" alt="<%:Edit%>" title="<%:Edit%>">
|
||||
<input class="btn cbi-button cbi-button-remove" type="button" value="<%:Delete%>" onclick="toPage('del', '{{id}}')" alt="<%:Delete%>" title="<%:Delete%>">
|
||||
<span class="drag-handle center" title="<%:Drag to reorder%>">⠿</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</script>
|
||||
|
||||
<<%=section_tag%> class="cbi-section">
|
||||
<<%=title_tag%>>Sing-Box/Xray <%:Shunt Rule%></<%=title_tag%>>
|
||||
<div class="cbi-section-descr"><a style="color: red"><%:Please note attention to the priority, the higher the order, the higher the priority.%></a></div>
|
||||
<div id="shunt_rule_list"></div>
|
||||
</<%=section_tag%>>
|
||||
|
||||
<script type="text/javascript">
|
||||
function loadNodeList() {
|
||||
XHR.get('<%=api.url("get_shunt_rules")%>', null, function(x, result) {
|
||||
var shunt_rule_list = result
|
||||
|
||||
var group_shunt_rules = {}
|
||||
if (shunt_rule_list.length > 0) {
|
||||
for (let i = 0; i < shunt_rule_list.length; i++) {
|
||||
let _shunt_rule = shunt_rule_list[i]
|
||||
if (!_shunt_rule.group || _shunt_rule.group === "") {
|
||||
_shunt_rule.group = "default"
|
||||
}
|
||||
if (!group_shunt_rules[_shunt_rule.group]) {
|
||||
group_shunt_rules[_shunt_rule.group] = []
|
||||
}
|
||||
group_shunt_rules[_shunt_rule.group].push(_shunt_rule)
|
||||
}
|
||||
} else {
|
||||
group_shunt_rules["default"] = [];
|
||||
}
|
||||
|
||||
var tab_ul_li_html = '';
|
||||
var tab_content_html = '';
|
||||
var shunt_rules_table_template = document.getElementById("shunt_rules-table-template");
|
||||
var shunt_rule_template = document.getElementById("shunt_rule-tr-template");
|
||||
var default_group = null
|
||||
for (let group in group_shunt_rules) {
|
||||
if (default_group == null)
|
||||
default_group = group
|
||||
|
||||
var table_html = "";
|
||||
if (true) {
|
||||
var new_shunt_rules_table_dom = shunt_rules_table_template.cloneNode(true);
|
||||
var _html = new_shunt_rules_table_dom.innerHTML;
|
||||
_html = _html.split("{{group}}").join(group);
|
||||
var shunt_rule_tr_html = "";
|
||||
for (var i = 0; i < group_shunt_rules[group].length; i++) {
|
||||
let o = group_shunt_rules[group][i]
|
||||
var newDom = shunt_rule_template.cloneNode(true);
|
||||
newDom.classList.add("cbi-rowstyle-" + (i % 2 + 1));
|
||||
var innerHTML = newDom.innerHTML;
|
||||
innerHTML = innerHTML.split("{{id}}").join(o[".name"]);
|
||||
innerHTML = innerHTML.split("{{group}}").join(o["group"] || "");
|
||||
innerHTML = innerHTML.split("{{remarks}}").join(o["remarks"] || "");
|
||||
|
||||
shunt_rule_tr_html += innerHTML
|
||||
}
|
||||
_html = _html.split("{{shunt_rule-tr}}").join(shunt_rule_tr_html);
|
||||
table_html = _html;
|
||||
}
|
||||
|
||||
var group_name = group
|
||||
if (group === "default") {
|
||||
group_name = "<%:default%>"
|
||||
}
|
||||
|
||||
tab_ul_li_html +=
|
||||
'<li group_name="' + group + '" id="tab.<%=appname%>.shunt_rules.' + group + '" class="cbi-tab">' +
|
||||
'<a onclick="this.blur(); return cbi_t_switch(\'<%=appname%>.shunt_rules\', \'' + group + '\')" href="<%=REQUEST_URI%>?tab.<%=appname%>.shunt_rules=' + group + '">' + escape_html(group_name) + " | " + "<font style='color: red'>" + group_shunt_rules[group].length + '</font></a>' +
|
||||
'</li>'
|
||||
tab_content_html +=
|
||||
'<div class="cbi-tabcontainer" id="container.<%=appname%>.shunt_rules.' + group + '" style="display: none;">' +
|
||||
'' + table_html +
|
||||
'</div>'
|
||||
}
|
||||
|
||||
function addTabDOM(li, content) {
|
||||
let tab_ul_html = '<ul class="cbi-tabmenu">';
|
||||
let tab_content_html = '<<%=section_tag%> class="cbi-section-shunt_rule cbi-section-shunt_rule-tabbed" id="cbi-<%=appname%>-shunt_rules">';
|
||||
if (li) {
|
||||
tab_ul_html += li;
|
||||
}
|
||||
if (content) {
|
||||
tab_content_html += content;
|
||||
}
|
||||
tab_ul_html += '</ul>';
|
||||
tab_content_html += '</<%=section_tag%>>';
|
||||
return tab_ul_html + tab_content_html;
|
||||
}
|
||||
|
||||
document.getElementById("shunt_rule_list").innerHTML = addTabDOM(tab_ul_li_html, tab_content_html);
|
||||
|
||||
for (let group in group_shunt_rules) {
|
||||
cbi_t_add("<%=appname%>.shunt_rules", group)
|
||||
}
|
||||
|
||||
if (default_group) {
|
||||
cbi_t_switch("<%=appname%>.shunt_rules", default_group)
|
||||
}
|
||||
|
||||
origin_group_shunt_rule_order = {};
|
||||
for (let group in group_shunt_rules) {
|
||||
origin_group_shunt_rule_order[group] = get_shunt_rule_order(group);
|
||||
}
|
||||
|
||||
initAllSortable(group_shunt_rules);
|
||||
});
|
||||
}
|
||||
|
||||
loadNodeList();
|
||||
|
||||
//]]>
|
||||
</script>
|
||||
@@ -1,112 +0,0 @@
|
||||
<%
|
||||
local map = self.map
|
||||
local api = map.api
|
||||
-%>
|
||||
|
||||
<style>
|
||||
.faq-title {
|
||||
color: var(--primary);
|
||||
font-weight: bolder;
|
||||
margin-bottom: 0.5rem;
|
||||
display: inline-block;
|
||||
}
|
||||
.faq-item {
|
||||
margin-bottom: 0.8rem;
|
||||
line-height:1.2rem;
|
||||
}
|
||||
|
||||
.geoview-search {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="cbi-value">
|
||||
<ul>
|
||||
<b class="faq-title"><%:Tips:%></b>
|
||||
<li class="faq-item">1. <span><%:By entering a domain or IP, you can query the Geo rule list they belong to.%></span></li>
|
||||
<li class="faq-item">2. <span><%:By entering a GeoIP or Geosite, you can extract the domains/IPs they contain.%></span></li>
|
||||
<li class="faq-item">3. <span><%:Use the GeoIP/Geosite query function to verify if the entered Geo rules are correct.%></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="cbi-value" id="cbi-passwall-geoview-lookup">
|
||||
<label class="cbi-value-title" for="geoview.lookup"><%:Domain/IP Query%></label>
|
||||
<div class="cbi-value-field">
|
||||
<div class="geoview-search">
|
||||
<input type="text" class="password-input cbi-input-text" id="geoview.lookup" name="geoview.lookup" inputmode="search" enterkeyhint="search" />
|
||||
<input class="btn cbi-button cbi-button-apply" type="button" id="lookup-view_btn"
|
||||
onclick='do_geoview(this, "lookup", lookupInput.value)'
|
||||
value="<%:Query%>" />
|
||||
</div>
|
||||
<div class="cbi-value-description">
|
||||
<%:Enter a domain or IP to query the Geo rule list they belong to.%>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cbi-value" id="cbi-passwall-geoview-extract">
|
||||
<label class="cbi-value-title" for="geoview.extract"><%:GeoIP/Geosite Query%></label>
|
||||
<div class="cbi-value-field">
|
||||
<div class="geoview-search">
|
||||
<input type="text" class="password-input cbi-input-text" id="geoview.extract" name="geoview.extract" inputmode="search" enterkeyhint="search" />
|
||||
<input class="btn cbi-button cbi-button-apply" type="button" id="extract-view_btn"
|
||||
onclick='do_geoview(this, "extract", extractInput.value)'
|
||||
value="<%:Query%>" />
|
||||
</div>
|
||||
<div class="cbi-value-description">
|
||||
<%:Enter a GeoIP or Geosite to extract the domains/IPs they contain. Format: geoip:cn or geosite:gfw%>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cbi-value">
|
||||
<textarea id="geoview_textarea" class="cbi-input-textarea" style="width: 100%; margin-top: 10px;" rows="25" wrap="off" readonly="readonly"></textarea>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
const lookupInput = document.getElementById("geoview.lookup");
|
||||
const extractInput = document.getElementById("geoview.extract");
|
||||
const lookup_btn = document.getElementById("lookup-view_btn");
|
||||
const extract_btn = document.getElementById("extract-view_btn");
|
||||
|
||||
function do_geoview(btn,action,value) {
|
||||
value = value.trim();
|
||||
if (!value) {
|
||||
alert("<%:Please enter query content!%>");
|
||||
return;
|
||||
}
|
||||
lookup_btn.disabled = true;
|
||||
extract_btn.disabled = true;
|
||||
btn.value = '<%:Querying%>';
|
||||
const textarea = document.getElementById('geoview_textarea');
|
||||
textarea.textContent = "";
|
||||
fetch('<%= api.url("geo_view") %>?action=' + action + '&value=' + encodeURIComponent(value))
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
textarea.textContent = data;
|
||||
lookup_btn.disabled = false;
|
||||
extract_btn.disabled = false;
|
||||
btn.value = '<%:Query%>';
|
||||
})
|
||||
}
|
||||
|
||||
lookupInput.addEventListener("keydown", function(e) {
|
||||
const isEnter = e.key === "Enter" || e.keyCode === 13;
|
||||
if (!isEnter) return;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
lookupInput.blur();
|
||||
lookup_btn.click();
|
||||
});
|
||||
|
||||
extractInput.addEventListener("keydown", function(e) {
|
||||
const isEnter = e.key === "Enter" || e.keyCode === 13;
|
||||
if (!isEnter) return;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
extractInput.blur();
|
||||
extract_btn.click();
|
||||
});
|
||||
//]]>
|
||||
</script>
|
||||
@@ -1,46 +0,0 @@
|
||||
<%
|
||||
local map = self.map
|
||||
local api = map.api
|
||||
-%>
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
function read_gfw() {
|
||||
fetch('<%= api.url("read_rulelist") %>?type=gfw')
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
var total_lines = data.split("\n").filter(line => line.trim() !== "").length;
|
||||
var textarea = document.getElementById('gfw_textarea');
|
||||
textarea.innerHTML = data;
|
||||
//textarea.scrollTop = textarea.scrollHeight;
|
||||
var totalLinesLabel = document.getElementById('gfw_total_lines');
|
||||
totalLinesLabel.innerHTML = "<%:Total Lines%> " + total_lines;
|
||||
})
|
||||
}
|
||||
|
||||
function read_chn() {
|
||||
fetch('<%= api.url("read_rulelist") %>?type=chn')
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
var total_lines = data.split("\n").filter(line => line.trim() !== "").length;
|
||||
var textarea = document.getElementById('chn_textarea');
|
||||
textarea.innerHTML = data;
|
||||
//textarea.scrollTop = textarea.scrollHeight;
|
||||
var totalLinesLabel = document.getElementById('chn_total_lines');
|
||||
totalLinesLabel.innerHTML = "<%:Total Lines%> " + total_lines;
|
||||
})
|
||||
}
|
||||
|
||||
function read_chnroute() {
|
||||
fetch('<%= api.url("read_rulelist") %>?type=chnroute')
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
var total_lines = data.split("\n").filter(line => line.trim() !== "").length;
|
||||
var textarea = document.getElementById('chnroute_textarea');
|
||||
textarea.innerHTML = data;
|
||||
//textarea.scrollTop = textarea.scrollHeight;
|
||||
var totalLinesLabel = document.getElementById('chnroute_total_lines');
|
||||
totalLinesLabel.innerHTML = "<%:Total Lines%> " + total_lines;
|
||||
})
|
||||
}
|
||||
//]]>
|
||||
</script>
|
||||
@@ -1,33 +0,0 @@
|
||||
<%
|
||||
local map = self.map
|
||||
local api = map.api
|
||||
local config = map.config
|
||||
local section = self.section
|
||||
-%>
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
let node_id = "<%=section%>";
|
||||
let node_config_url = '<%=api.url("server_config")%>/' + node_id;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
waitForElement('select[name*="<%=config%>"][name*="type"]', function(el) {
|
||||
let o_val = el.value;
|
||||
el.addEventListener("change", () => {
|
||||
el.blur();
|
||||
if (o_val != el.value) {
|
||||
let save = true;
|
||||
if (save) {
|
||||
//document.getElementsByClassName('cbi-button-save')[0].click();
|
||||
server_update_config(node_id, {
|
||||
enable: getOption("<%=config%>", node_id, "enable").value,
|
||||
remarks: getOption("<%=config%>", node_id, "remarks").value,
|
||||
type: getOption("<%=config%>", node_id, "type").value,
|
||||
protocol: ''
|
||||
}, node_config_url);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
//]]>
|
||||
</script>
|
||||
@@ -1,25 +0,0 @@
|
||||
<%
|
||||
local map = self.map
|
||||
local api = map.api
|
||||
local section = self.section
|
||||
-%>
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
function server_update_config(cfgid, data, redirect_url) {
|
||||
XHR.get('<%=api.url("server_update_config")%>', {
|
||||
id: cfgid,
|
||||
data: JSON.stringify(data)
|
||||
},
|
||||
function(x, data) {
|
||||
if (x && x.status == 200 && data.code == 1) {
|
||||
if (redirect_url) {
|
||||
window.location.href = redirect_url;
|
||||
}
|
||||
}
|
||||
else {
|
||||
alert("<%:Error%>");
|
||||
}
|
||||
});
|
||||
}
|
||||
//]]>
|
||||
</script>
|
||||
@@ -1,28 +0,0 @@
|
||||
<%
|
||||
local map = self.map
|
||||
local api = map.api
|
||||
local prefix = self.option_prefix or ""
|
||||
-%>
|
||||
<div class="cbi-value" id="cbi-<%=self.config.."-"..section.."-"..self.option%>" data-index="<%=self.index%>" data-depends="<%=pcdata(self:deplist2json(section))%>">
|
||||
<label class="cbi-value-title"><%:Generate key%></label>
|
||||
<div class="cbi-value-field">
|
||||
<input class="btn cbi-button" type="button" onclick="gen_wireguard_key(this)" value="<%:Generate key%>" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function gen_wireguard_key() {
|
||||
XHR.get('<%=api.url("gen_wireguard_key")%>', {},
|
||||
function(x, data) {
|
||||
if (x && x.status == 200 && data.code == 1) {
|
||||
const private_key_dom = getOption("<%=self.config%>", "<%=section%>", "<%=prefix%>wireguard_private_key");
|
||||
private_key_dom.value = data.data.private_key;
|
||||
|
||||
const public_key_dom = getOption("<%=self.config%>", "<%=section%>", "<%=prefix%>wireguard_public_key");
|
||||
public_key_dom.value = data.data.public_key;
|
||||
} else {
|
||||
alert("<%:Error%>");
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -1,49 +0,0 @@
|
||||
<%
|
||||
local map = self.map
|
||||
local api = map.api
|
||||
-%>
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
var log_textarea = null;
|
||||
var first_load_done = false;
|
||||
|
||||
function scrollToBottom() {
|
||||
log_textarea.scrollTop = log_textarea.scrollHeight;
|
||||
}
|
||||
|
||||
function clear_log(btn) {
|
||||
XHR.get('<%=api.url("server_clear_log")%>', null,
|
||||
function(x, data) {
|
||||
if(x && x.status == 200 && log_textarea) {
|
||||
log_textarea.innerHTML = "";
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
XHR.poll(3, '<%=api.url("server_get_log")%>', null,
|
||||
function(x, data) {
|
||||
if(x && x.status == 200) {
|
||||
if (!log_textarea) log_textarea = document.getElementById('log_textarea');
|
||||
var wasBottom = (log_textarea.scrollTop + log_textarea.clientHeight >= log_textarea.scrollHeight - 10);
|
||||
log_textarea.innerHTML = x.responseText;
|
||||
if (!first_load_done) {
|
||||
scrollToBottom();
|
||||
first_load_done = true;
|
||||
}
|
||||
else if (wasBottom) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
//]]>
|
||||
</script>
|
||||
<fieldset class="cbi-section" id="_log_fieldset">
|
||||
<legend>
|
||||
<%:Logs%>
|
||||
</legend>
|
||||
<input class="btn cbi-button cbi-button-remove" type="button" onclick="clear_log()" value="<%:Clear logs%>" />
|
||||
<textarea id="log_textarea" class="cbi-input-textarea" style="width: 100%;margin-top: 10px;" data-update="change" rows="20" wrap="off" readonly="readonly"></textarea>
|
||||
</fieldset>
|
||||
@@ -1,41 +0,0 @@
|
||||
<%
|
||||
local map = self.map
|
||||
local api = map.api
|
||||
local config = map.config
|
||||
local sectiontype = self.sectiontype
|
||||
-%>
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
var _servers_status = document.getElementsByClassName('_servers_status');
|
||||
for(var i = 0; i < _servers_status.length; i++) {
|
||||
var id = _servers_status[i].parentElement.parentElement.parentElement.id;
|
||||
id = id.substr(id.lastIndexOf("-") + 1);
|
||||
XHR.get('<%=api.url("server_status")%>', {
|
||||
index: i,
|
||||
id: id
|
||||
},
|
||||
function(x, result) {
|
||||
_servers_status[result.index].setAttribute("style","font-weight:bold;");
|
||||
_servers_status[result.index].setAttribute("color",result.status ? "green":"red");
|
||||
_servers_status[result.index].innerHTML = (result.status ? '✓' : 'X');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
var edit_btn = document.getElementById("cbi-<%=config%>-<%=sectiontype%>").getElementsByClassName("cbi-button cbi-button-edit");
|
||||
for (var i = 0; i < edit_btn.length; i++) {
|
||||
try {
|
||||
var onclick_str = edit_btn[i].getAttribute("onclick");
|
||||
var id = onclick_str.substring(onclick_str.lastIndexOf('/') + 1, onclick_str.length - 1);
|
||||
var td = edit_btn[i].parentNode;
|
||||
var new_div = "";
|
||||
// Add `log` button
|
||||
new_div += '<input class="btn cbi-button cbi-button-add" type="button" value="<%:Log%>" onclick="window.open(\'' + '<%=api.url("server_log")%>' + '?id=' + id + '\', \'_blank\')"/> ';
|
||||
td.innerHTML = new_div + td.innerHTML;
|
||||
}
|
||||
catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
//]]>
|
||||
</script>
|
||||
@@ -1,46 +0,0 @@
|
||||
<%
|
||||
local map = self.map
|
||||
local api = map.api
|
||||
-%>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
let socks_id = window.location.pathname.substring(window.location.pathname.lastIndexOf("/") + 1)
|
||||
function add_node_by_key() {
|
||||
var key = prompt("<%:Please enter the node keyword, pay attention to distinguish between spaces, uppercase and lowercase.%>", "");
|
||||
if (key) {
|
||||
window.location.href = '<%=api.url("socks_autoswitch_add_node")%>' + "?id=" + socks_id + "&key=" + key;
|
||||
}
|
||||
}
|
||||
function remove_node_by_key() {
|
||||
var key = prompt("<%:Please enter the node keyword, pay attention to distinguish between spaces, uppercase and lowercase.%>", "");
|
||||
if (key) {
|
||||
window.location.href = '<%=api.url("socks_autoswitch_remove_node")%>' + "?id=" + socks_id + "&key=" + key;
|
||||
}
|
||||
}
|
||||
//]]>
|
||||
</script>
|
||||
<style>
|
||||
.key-btn-div {
|
||||
display: table-cell;
|
||||
padding: 5px 0 5px;
|
||||
}
|
||||
@media screen and (max-width: 600px) {
|
||||
.key-btn-div {
|
||||
display: block;
|
||||
}
|
||||
.key-label {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
.key-btn-div .cbi-button {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
</style>
|
||||
<div class="cbi-value" id="cbi-<%=self.config.."-"..section.."-"..self.option%>" data-index="<%=self.index%>" data-depends="<%=pcdata(self:deplist2json(section))%>">
|
||||
<label class="cbi-value-title key-label"> </label>
|
||||
<div class="key-btn-div">
|
||||
<input class="btn cbi-button cbi-button-add" type="button" onclick="add_node_by_key()" value="<%:Add nodes to the standby node list by keywords%>" />
|
||||
<input class="btn cbi-button cbi-button-remove" type="button" onclick="remove_node_by_key()" value="<%:Delete nodes in the standby node list by keywords%>" />
|
||||
</div>
|
||||
</div>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1 +0,0 @@
|
||||
zh-cn
|
||||
@@ -1,4 +0,0 @@
|
||||
|
||||
config global 'global'
|
||||
option enable '0'
|
||||
option start_delay '90'
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
. /usr/share/passwall/utils.sh
|
||||
|
||||
[ "$ACTION" = "ifup" ] || [ "$ACTION" = "ifupdate" ] || exit 0
|
||||
|
||||
([ "$(get_cache_var "ENABLED_DEFAULT_ACL")" = "1" ] || [ "$(get_cache_var "ENABLED_ACLS")" = "1" ]) && [ -f ${LOCK_PATH}/${CONFIG}_ready.lock ] && {
|
||||
|
||||
default_device=$(ip route show default 2>/dev/null | awk -F 'dev ' '{print $2}' | awk '{print $1}' | head -n1)
|
||||
default6_device=$(ip -6 route show default 2>/dev/null | awk -F 'dev ' '{print $2}' | awk '{print $1}' | head -n1)
|
||||
( [ "$default_device" = "$DEVICE" ] || [ "$default6_device" = "$DEVICE" ] ) && {
|
||||
|
||||
if [ "$ACTION" = "ifupdate" ]; then
|
||||
USE_TABLES=$(get_cache_var "USE_TABLES")
|
||||
[ -n "$USE_TABLES" ] && { ${APP_PATH}/${USE_TABLES}.sh update_wan_sets >/dev/null 2>&1 & }
|
||||
exit 0
|
||||
fi
|
||||
|
||||
[ ! -d ${LOCK_PATH} ] && mkdir -p ${LOCK_PATH}
|
||||
LOCK_FILE="${LOCK_PATH}/${CONFIG}_ifup.lock"
|
||||
if [ -s ${LOCK_FILE} ]; then
|
||||
SPID=$(cat ${LOCK_FILE})
|
||||
if [ -e /proc/${SPID}/status ]; then
|
||||
exit 1
|
||||
fi
|
||||
cat /dev/null > ${LOCK_FILE}
|
||||
fi
|
||||
echo $$ > ${LOCK_FILE}
|
||||
|
||||
/etc/init.d/${CONFIG} restart >/dev/null 2>&1 &
|
||||
logger -p notice -t network -s "${CONFIG}: restart when $INTERFACE ifup"
|
||||
|
||||
rm -rf ${LOCK_FILE}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
[ "$ACTION" = "step" ] || exit 0
|
||||
|
||||
offset=${offset#-}
|
||||
offset=${offset%.*}
|
||||
|
||||
# Skip service restart if the time adjustment is less than 120 seconds.
|
||||
[ "$offset" -lt 120 ] && exit 0
|
||||
|
||||
. /usr/share/passwall/utils.sh
|
||||
|
||||
NTP_LOCK_FILE="${LOCK_PATH}/${CONFIG}_ntp.lock"
|
||||
[ -f "${NTP_LOCK_FILE}" ] && exit 0
|
||||
|
||||
([ "$(get_cache_var "ENABLED_DEFAULT_ACL")" = "1" ] || [ "$(get_cache_var "ENABLED_ACLS")" = "1" ]) && [ -f ${LOCK_PATH}/${CONFIG}_ready.lock ] && {
|
||||
|
||||
echo $$ > ${NTP_LOCK_FILE}
|
||||
|
||||
/etc/init.d/${CONFIG} restart >/dev/null 2>&1 &
|
||||
logger -p notice -t network -s "${CONFIG}: restart after NTP time step (${offset}s)"
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
START=99
|
||||
STOP=15
|
||||
|
||||
ROOT="${IPKG_INSTROOT:-}"
|
||||
. "$ROOT/usr/share/passwall/utils.sh"
|
||||
APP_FILE=${APP_PATH}/app.sh
|
||||
LOCK_FILE_DIR=/var/lock
|
||||
LOCK_FILE=${LOCK_FILE_DIR}/${CONFIG}.lock
|
||||
|
||||
set_lock() {
|
||||
[ ! -d "$LOCK_FILE_DIR" ] && mkdir -p $LOCK_FILE_DIR
|
||||
exec 999>"$LOCK_FILE"
|
||||
flock -xn 999
|
||||
}
|
||||
|
||||
unset_lock() {
|
||||
flock -u 999
|
||||
rm -rf "$LOCK_FILE"
|
||||
}
|
||||
|
||||
unlock() {
|
||||
failcount=1
|
||||
while [ "$failcount" -le 10 ]; do
|
||||
if [ -f "$LOCK_FILE" ]; then
|
||||
let "failcount++"
|
||||
sleep 1s
|
||||
[ "$failcount" -ge 10 ] && unset_lock
|
||||
else
|
||||
break
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
boot_func() {
|
||||
local delay=$(uci -q get ${CONFIG}.@global_delay[0].start_delay || echo 1)
|
||||
if [ "$delay" -gt 0 ]; then
|
||||
echolog "执行启动延时 $delay 秒后再启动!"
|
||||
sleep $delay
|
||||
fi
|
||||
restart boot
|
||||
touch ${LOCK_FILE_DIR}/${CONFIG}_ready.lock
|
||||
}
|
||||
|
||||
boot() {
|
||||
boot_func >/dev/null 2>&1 &
|
||||
}
|
||||
|
||||
start() {
|
||||
set_lock
|
||||
[ $? == 1 ] && echolog "脚本已经在运行,不重复运行,退出." && exit 0
|
||||
$APP_FILE start
|
||||
unset_lock
|
||||
}
|
||||
|
||||
stop() {
|
||||
unlock
|
||||
set_lock
|
||||
[ $? == 1 ] && echolog "停止脚本等待超时,不重复运行,退出." && exit 0
|
||||
$APP_FILE stop
|
||||
unset_lock
|
||||
}
|
||||
|
||||
restart() {
|
||||
set_lock
|
||||
[ $? == 1 ] && echolog "脚本已经在运行,不重复运行,退出." && exit 0
|
||||
rule_lock=${LOCK_FILE_DIR}/${CONFIG}_rule_update.lock
|
||||
sub_lock=${LOCK_FILE_DIR}/${CONFIG}_subscribe.lock
|
||||
for i in $(seq 1 300); do
|
||||
if [ ! -f "$rule_lock" ] && [ ! -f "$sub_lock" ]; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
rm -f "$rule_lock" "$sub_lock"
|
||||
$APP_FILE stop
|
||||
$APP_FILE start $1
|
||||
unset_lock
|
||||
}
|
||||
|
||||
reload() {
|
||||
echo "Warning: This service does not support configuration reloading"
|
||||
echo "Performing full restart instead..."
|
||||
restart
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
START=99
|
||||
|
||||
boot_func() {
|
||||
config_load passwall_server
|
||||
config_get start_delay global start_delay 90
|
||||
case "$start_delay" in
|
||||
''|*[!0-9]*) start_delay=90 ;;
|
||||
*) [ "$start_delay" -le 600 ] 2>/dev/null || start_delay=90 ;;
|
||||
esac
|
||||
[ "$start_delay" -gt 0 ] && sleep "$start_delay"
|
||||
restart
|
||||
}
|
||||
|
||||
boot() {
|
||||
boot_func >/dev/null 2>&1 &
|
||||
}
|
||||
|
||||
start() {
|
||||
lua /usr/lib/lua/luci/passwall/server_app.lua start
|
||||
}
|
||||
|
||||
stop() {
|
||||
lua /usr/lib/lua/luci/passwall/server_app.lua stop
|
||||
}
|
||||
|
||||
restart() {
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
reload() {
|
||||
echo "Warning: This service does not support configuration reloading"
|
||||
echo "Performing full restart instead..."
|
||||
restart
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ -e "/etc/config/ucitrack" ]; then
|
||||
uci -q batch <<-EOF
|
||||
delete ucitrack.@passwall[-1]
|
||||
add ucitrack passwall
|
||||
set ucitrack.@passwall[-1].init=passwall
|
||||
commit ucitrack
|
||||
EOF
|
||||
fi
|
||||
|
||||
uci -q batch <<-EOF
|
||||
delete firewall.passwall
|
||||
set firewall.passwall=include
|
||||
set firewall.passwall.type='script'
|
||||
set firewall.passwall.path='/var/etc/passwall.include'
|
||||
|
||||
set dhcp.@dnsmasq[0].localuse=1
|
||||
commit dhcp
|
||||
set uhttpd.main.max_requests=50
|
||||
commit uhttpd
|
||||
EOF
|
||||
|
||||
if [ -x "/sbin/fw3" ]; then
|
||||
uci -q set firewall.passwall.reload='1'
|
||||
else
|
||||
uci -q delete firewall.passwall.reload
|
||||
uci -q delete firewall.passwall.fw4_compatible
|
||||
fi
|
||||
uci commit firewall
|
||||
|
||||
[ ! -s "/etc/config/passwall" ] && cp -f /usr/share/passwall/0_default_config /etc/config/passwall
|
||||
|
||||
chmod +x /usr/share/passwall/*.sh
|
||||
|
||||
[ "$(uci -q get passwall.@global_xray[0].sniffing)" == "1" ] && [ "$(uci -q get passwall.@global_xray[0].route_only)" != "1" ] && uci -q set passwall.@global_xray[0].sniffing_override_dest=1
|
||||
uci -q delete passwall.@global_xray[0].sniffing
|
||||
uci -q delete passwall.@global_xray[0].route_only
|
||||
|
||||
[ "$(uci -q get passwall.@global_haproxy[0].health_check_type)" = "passwall_logic" ] && uci -q set passwall.@global_haproxy[0].health_check_type='script_logic'
|
||||
|
||||
for sid in $(uci show passwall | grep "=socks" | awk -F '.' '{print $2}' | awk -F '=' '{print $1}'); do
|
||||
if [ -z "$(echo ${sid} | grep '^socks_')" ]; then
|
||||
new_id="socks_$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 5)"
|
||||
old_ref="Socks_${sid}"
|
||||
uci -q rename passwall.${sid}="${new_id}"
|
||||
for item in $(uci show passwall | grep "='${old_ref}'\$" | cut -d '=' -sf 1); do
|
||||
uci -q set ${item}="${new_id}"
|
||||
done
|
||||
unset sid new_id old_ref item
|
||||
fi
|
||||
done
|
||||
for sid in $(uci show passwall | grep "@acl_rule" | cut -d '.' -sf 2 | cut -d '=' -sf 1); do
|
||||
new_id="acl_$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 5)"
|
||||
uci -q rename passwall.${sid}="${new_id}"
|
||||
unset sid new_id
|
||||
done
|
||||
for sid in $(uci show passwall | grep "@subscribe_list" | cut -d '.' -sf 2 | cut -d '=' -sf 1); do
|
||||
new_id="sub_$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 5)"
|
||||
uci -q rename passwall.${sid}="${new_id}"
|
||||
unset sid new_id
|
||||
done
|
||||
|
||||
migrate_option() {
|
||||
local section="$1"
|
||||
local old="$2"
|
||||
local new="$3"
|
||||
local prefix="passwall.${section}"
|
||||
if uci -q show "${prefix}.${new}" >/dev/null; then
|
||||
uci -q delete "${prefix}.${old}"
|
||||
elif uci -q show "${prefix}.${old}" >/dev/null; then
|
||||
uci -q set "${prefix}.${new}=$(uci -q get "${prefix}.${old}")"
|
||||
uci -q delete "${prefix}.${old}"
|
||||
fi
|
||||
}
|
||||
|
||||
migrate_option "@global[0]" tcp_node node
|
||||
uci -q delete "passwall.@global[0].udp_node"
|
||||
migrate_option "@global[0]" tcp_node_socks_port node_socks_port
|
||||
migrate_option "@global[0]" tcp_node_socks_bind_local node_socks_bind_local
|
||||
migrate_option "@global[0]" log_tcp log_node
|
||||
uci -q delete "passwall.@global[0].log_udp"
|
||||
|
||||
for sid in $(uci show passwall | grep "=acl_rule" | cut -d '.' -sf 2 | cut -d '=' -sf 1); do
|
||||
migrate_option "${sid}" tcp_node node
|
||||
uci -q delete "passwall.${sid}.udp_node"
|
||||
done
|
||||
|
||||
uci -q commit passwall
|
||||
|
||||
sed -i "s#add_from#group#g" /etc/config/passwall 2>/dev/null
|
||||
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-indexcache.*
|
||||
rm -rf /tmp/luci-modulecache/
|
||||
killall -HUP rpcd 2>/dev/null
|
||||
|
||||
exit 0
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ -e "/etc/config/ucitrack" ]; then
|
||||
uci -q batch <<-EOF
|
||||
delete ucitrack.@passwall_server[-1]
|
||||
add ucitrack passwall_server
|
||||
set ucitrack.@passwall_server[-1].init=passwall_server
|
||||
commit ucitrack
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Migrate server sections from the old `user` section type. New user entries
|
||||
# also use `user`, so only sections with a server `type` option are renamed.
|
||||
for sid in $(uci -q show passwall_server | sed -n 's/^passwall_server\.\([^.=]*\)=user$/\1/p'); do
|
||||
if [ -n "$(uci -q get "passwall_server.${sid}.type")" ]; then
|
||||
uci -q set "passwall_server.${sid}=server"
|
||||
fi
|
||||
done
|
||||
for sid in $(uci -q show passwall_server | sed -n 's/^passwall_server\.\([^.=]*\)\.protocol=dokodemo-door$/\1/p'); do
|
||||
uci -q set "passwall_server.${sid}.protocol=tunnel"
|
||||
done
|
||||
|
||||
uci -q commit passwall_server
|
||||
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-indexcache.*
|
||||
rm -rf /tmp/luci-modulecache/
|
||||
killall -HUP rpcd 2>/dev/null
|
||||
|
||||
exit 0
|
||||
@@ -1,233 +0,0 @@
|
||||
|
||||
config global
|
||||
option enabled '0'
|
||||
option socks_enabled '0'
|
||||
option node_socks_port '1070'
|
||||
option filter_proxy_ipv6 '1'
|
||||
option dns_shunt 'chinadns-ng'
|
||||
option dns_mode 'tcp'
|
||||
option remote_dns '1.1.1.1'
|
||||
list smartdns_remote_dns 'https://1.1.1.1/dns-query'
|
||||
option use_default_dns 'direct'
|
||||
option chinadns_ng_default_tag 'none'
|
||||
option dns_redirect '1'
|
||||
option use_direct_list '1'
|
||||
option use_proxy_list '1'
|
||||
option use_block_list '1'
|
||||
option use_gfw_list '1'
|
||||
option chn_list 'direct'
|
||||
option tcp_proxy_mode 'proxy'
|
||||
option udp_proxy_mode 'proxy'
|
||||
option localhost_proxy '1'
|
||||
option client_proxy '1'
|
||||
option acl_enable '0'
|
||||
option log_node '0'
|
||||
option loglevel 'error'
|
||||
option log_chinadns_ng '0'
|
||||
|
||||
config global_haproxy
|
||||
option balancing_enable '0'
|
||||
|
||||
config global_delay
|
||||
option start_daemon '1'
|
||||
option start_delay '60'
|
||||
|
||||
config global_forwarding
|
||||
option tcp_no_redir_ports 'disable'
|
||||
option udp_no_redir_ports 'disable'
|
||||
option tcp_proxy_drop_ports 'disable'
|
||||
option udp_proxy_drop_ports '443'
|
||||
option tcp_redir_ports '22,25,53,80,143,443,465,587,853,873,993,995,5222,8080,8443,9418'
|
||||
option udp_redir_ports '1:65535'
|
||||
option accept_icmp '0'
|
||||
option prefer_nft '1'
|
||||
option tcp_proxy_way 'redirect'
|
||||
option ipv6_tproxy '0'
|
||||
|
||||
config global_xray
|
||||
option sniffing_override_dest '0'
|
||||
|
||||
config global_singbox
|
||||
|
||||
config global_other
|
||||
option auto_detection_time 'tcping'
|
||||
option show_node_info '0'
|
||||
|
||||
config global_rules
|
||||
option chnlist_update '1'
|
||||
option chnroute_update '1'
|
||||
option chnroute6_update '1'
|
||||
option gfwlist_update '1'
|
||||
option geosite_update '0'
|
||||
option geoip_update '0'
|
||||
list gfwlist_url 'https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/gfw.txt'
|
||||
list chnroute_url 'https://ispip.clang.cn/all_cn.txt'
|
||||
list chnroute_url 'https://cdn.jsdelivr.net/gh/gaoyifan/china-operator-ip@ip-lists/china.txt'
|
||||
list chnroute6_url 'https://ispip.clang.cn/all_cn_ipv6.txt'
|
||||
list chnroute6_url 'https://cdn.jsdelivr.net/gh/gaoyifan/china-operator-ip@ip-lists/china6.txt'
|
||||
list chnlist_url 'https://cdn.jsdelivr.net/gh/felixonmars/dnsmasq-china-list/accelerated-domains.china.conf'
|
||||
list chnlist_url 'https://cdn.jsdelivr.net/gh/felixonmars/dnsmasq-china-list/apple.china.conf'
|
||||
option v2ray_location_asset '/usr/share/v2ray/'
|
||||
option geoip_url 'https://github.com/Loyalsoldier/geoip/releases/latest/download/geoip.dat'
|
||||
option geosite_url 'https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat'
|
||||
|
||||
config global_app
|
||||
option geoview_file '/usr/bin/geoview'
|
||||
option sing_box_file '/usr/bin/sing-box'
|
||||
option xray_file '/usr/bin/xray'
|
||||
option hysteria_file '/usr/bin/hysteria'
|
||||
|
||||
config global_subscribe
|
||||
option filter_keyword_mode '1'
|
||||
list filter_discard_list '距离下次重置剩余'
|
||||
list filter_discard_list '套餐到期'
|
||||
list filter_discard_list '过期时间'
|
||||
list filter_discard_list '剩余流量'
|
||||
list filter_discard_list 'QQ群'
|
||||
list filter_discard_list '官网'
|
||||
|
||||
config nodes 'myshunt'
|
||||
option remarks '分流总节点'
|
||||
option type 'Xray'
|
||||
option protocol '_shunt'
|
||||
option DirectGame '_direct'
|
||||
option ProxyGame '_default'
|
||||
option AIGC '_default'
|
||||
option Streaming '_default'
|
||||
option Proxy '_default'
|
||||
option Direct '_direct'
|
||||
option default_node '_direct'
|
||||
option domainStrategy 'IPOnDemand'
|
||||
|
||||
config shunt_rules 'DirectGame'
|
||||
option remarks 'DirectGame'
|
||||
option network 'tcp,udp'
|
||||
option domain_list '# 国内游戏geosite域名
|
||||
geosite:category-games@cn'
|
||||
option ip_list '# steam直连IP
|
||||
45.121.184.0/24
|
||||
103.10.124.0/23
|
||||
103.28.54.0/24
|
||||
146.66.152.0/24
|
||||
146.66.155.0/24
|
||||
153.254.86.0/24
|
||||
155.133.224.0/22
|
||||
155.133.230.0/24
|
||||
155.133.232.0/23
|
||||
155.133.234.0/24
|
||||
155.133.236.0/22
|
||||
155.133.240.0/23
|
||||
155.133.244.0/23
|
||||
155.133.246.0/24
|
||||
155.133.248.0/21
|
||||
162.254.192.0/21
|
||||
185.25.182.0/23
|
||||
190.217.32.0/22
|
||||
192.69.96.0/22
|
||||
205.196.6.0/24
|
||||
208.64.200.0/22
|
||||
208.78.164.0/22
|
||||
205.185.194.0/24'
|
||||
|
||||
config shunt_rules 'ProxyGame'
|
||||
option remarks 'ProxyGame'
|
||||
option network 'tcp,udp'
|
||||
option domain_list '# steam 商店/客服/聊天/网页布局/API/二维码/Google云同步 代理URL
|
||||
steamcommunity.com
|
||||
www.steamcommunity.com
|
||||
store.steampowered.com
|
||||
checkout.steampowered.com
|
||||
api.steampowered.com
|
||||
help.steampowered.com
|
||||
login.steampowered.com
|
||||
store.akamai.steamstatic.com
|
||||
steambroadcast.akamaized.net
|
||||
steamvideo-a.akamaihd.net
|
||||
steamusercontent-a.akamaihd.net
|
||||
steamstore-a.akamaihd.net
|
||||
steamcommunity-a.akamaihd.net
|
||||
steamcdn-a.akamaihd.net
|
||||
steamuserimages-a.akamaihd.net
|
||||
community.akamai.steamstatic.com
|
||||
avatars.akamai.steamstatic.com
|
||||
community.steamstatic.com
|
||||
cdn.akamai.steamstatic.com
|
||||
avatars.steamstatic.com
|
||||
shared.akamai.steamstatic.com
|
||||
clan.akamai.steamstatic.com
|
||||
cdn.cloudflare.steamstatic.com
|
||||
community.cloudflare.steamstatic.com
|
||||
store.cloudflare.steamstatic.com
|
||||
avatars.cloudflare.steamstatic.com
|
||||
clan.cloudflare.steamstatic.com
|
||||
shared.cloudflare.steamstatic.com
|
||||
steam-chat.com
|
||||
steamcloud-ugc.storage.googleapis.com
|
||||
steamcloud-eu-ams.storage.googleapis.com
|
||||
steamcloud-eu-fra.storage.googleapis.com
|
||||
steamcloud-finland.storage.googleapis.com
|
||||
steamcloud-saopaulo.storage.googleapis.com
|
||||
steamcloud-singapore.storage.googleapis.com
|
||||
steamcloud-sydney.storage.googleapis.com
|
||||
steamcloud-taiwan.storage.googleapis.com
|
||||
steamcloud-eu.storage.googleapis.com
|
||||
domain:blizzard.com
|
||||
#Steam支付
|
||||
domain:checkout.steampowered.com
|
||||
domain:globalapi.smart2pay.com
|
||||
geosite:category-games'
|
||||
|
||||
config shunt_rules 'AIGC'
|
||||
option remarks 'AIGC'
|
||||
option network 'tcp,udp'
|
||||
option domain_list 'geosite:category-ai-!cn
|
||||
geosite:apple-intelligence'
|
||||
|
||||
config shunt_rules 'Streaming'
|
||||
option remarks 'Streaming'
|
||||
option network 'tcp,udp'
|
||||
option domain_list 'geosite:netflix
|
||||
geosite:disney'
|
||||
|
||||
config shunt_rules 'Proxy'
|
||||
option remarks 'Proxy'
|
||||
option network 'tcp,udp'
|
||||
option domain_list 'geosite:geolocation-!cn'
|
||||
option ip_list '149.154.160.0/20
|
||||
91.108.4.0/22
|
||||
91.108.56.0/24
|
||||
109.239.140.0/24
|
||||
67.198.55.0/24
|
||||
8.8.4.4
|
||||
8.8.8.8
|
||||
208.67.222.222
|
||||
208.67.220.220
|
||||
1.1.1.1
|
||||
1.1.1.2
|
||||
1.0.0.1
|
||||
9.9.9.9
|
||||
149.112.112.112
|
||||
2001:67c:4e8::/48
|
||||
2001:b28:f23c::/48
|
||||
2001:b28:f23d::/48
|
||||
2001:b28:f23f::/48
|
||||
2001:b28:f242::/48
|
||||
2001:4860:4860::8888
|
||||
2001:4860:4860::8844
|
||||
2606:4700:4700::1111
|
||||
2606:4700:4700::1001'
|
||||
|
||||
config shunt_rules 'Direct'
|
||||
option remarks 'Direct'
|
||||
option network 'tcp,udp'
|
||||
option domain_list 'geosite:cn'
|
||||
option ip_list '223.5.5.5/32
|
||||
223.6.6.6/32
|
||||
119.29.29.29/32
|
||||
180.76.76.76/32
|
||||
114.114.114.114/32
|
||||
114.114.115.115/32
|
||||
1.12.12.12/32
|
||||
120.53.53.53/32
|
||||
geoip:cn
|
||||
geoip:private'
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,647 +0,0 @@
|
||||
#!/usr/bin/lua
|
||||
|
||||
-- Copyright(c): lwb1978 2025–2027
|
||||
|
||||
local api = require("luci.passwall.api")
|
||||
local urlencode = api.UrlEncode
|
||||
local base64 = api.base64Encode
|
||||
local json = api.jsonc
|
||||
|
||||
local domain_redir = {}
|
||||
|
||||
local function host_format(host)
|
||||
if not host then return "" end
|
||||
local str = host:match("%[(.-)%]") or host
|
||||
if api.datatypes.ip6addr(str) then
|
||||
return "[" .. str .. "]"
|
||||
end
|
||||
return host
|
||||
end
|
||||
|
||||
local function build_alpn(alpn) -- 排序+去重
|
||||
if not alpn then return nil end
|
||||
|
||||
local seen = {}
|
||||
local order = { "h3", "h2", "http/1.1" }
|
||||
|
||||
if type(alpn) == "table" then
|
||||
for _, v in ipairs(alpn) do
|
||||
if v then seen[v] = true end
|
||||
end
|
||||
else
|
||||
seen[tostring(alpn)] = true
|
||||
end
|
||||
|
||||
local t = {}
|
||||
for _, v in ipairs(order) do
|
||||
if seen[v] then table.insert(t, v) end
|
||||
end
|
||||
|
||||
if #t == 0 then return nil end
|
||||
return table.concat(t, ",")
|
||||
end
|
||||
|
||||
local function build_common(node)
|
||||
local o = {}
|
||||
|
||||
o.server = host_format(node.server)
|
||||
o.port = node.port
|
||||
o.name = node.name
|
||||
|
||||
-- ===== TLS =====
|
||||
o.tls = {}
|
||||
|
||||
if node["reality-opts"] then
|
||||
o.tls.security = "reality"
|
||||
o.tls.pbk = node["reality-opts"]["public-key"]
|
||||
o.tls.sid = node["reality-opts"]["short-id"]
|
||||
elseif node.tls then
|
||||
o.tls.security = "tls"
|
||||
end
|
||||
|
||||
o.tls.sni = node.servername or node.sni
|
||||
o.tls.fp = node["client-fingerprint"]
|
||||
o.tls.pcs = node.fingerprint
|
||||
o.tls.insecure = node["skip-cert-verify"] == true
|
||||
|
||||
o.tls.alpn = build_alpn(node.alpn)
|
||||
|
||||
local ech_opts = node["ech-opts"]
|
||||
if ech_opts and ech_opts.enable == true then
|
||||
if ech_opts["query-server-name"] then
|
||||
o.tls.ech = ech_opts["query-server-name"] .. "+https://223.5.5.5/dns-query"
|
||||
elseif ech_opts.config then
|
||||
o.tls.ech = ech_opts.config
|
||||
end
|
||||
end
|
||||
|
||||
-- ===== transport =====
|
||||
o.transport = {}
|
||||
local net = node.network or "tcp"
|
||||
o.transport.type = net
|
||||
|
||||
local function get_first(v)
|
||||
if type(v) == "table" then return v[1] end
|
||||
return v
|
||||
end
|
||||
|
||||
if net == "ws" then
|
||||
local opts = node["ws-opts"]
|
||||
if opts then
|
||||
local path = opts.path or "/"
|
||||
local ed = opts["max-early-data"]
|
||||
local eh = opts["early-data-header-name"]
|
||||
if ed then
|
||||
path = path .. "?ed=" .. ed
|
||||
end
|
||||
if eh then
|
||||
path = path .. (path:find("?", 1, true) and "&eh=" or "?eh=") .. eh
|
||||
end
|
||||
o.transport.path = path
|
||||
o.transport.host = opts.headers and opts.headers.Host
|
||||
end
|
||||
|
||||
elseif net == "grpc" then
|
||||
local opts = node["grpc-opts"]
|
||||
if opts then
|
||||
o.transport.serviceName = opts["grpc-service-name"]
|
||||
end
|
||||
|
||||
elseif net == "http" then
|
||||
local opts = node["http-opts"]
|
||||
if opts then
|
||||
-- Clash http-opts carries the camouflage Host under headers.Host
|
||||
-- (a list), like ws-opts; opts.host is not standard for http.
|
||||
o.transport.host = get_first(opts.host) or (opts.headers and get_first(opts.headers.Host))
|
||||
o.transport.path = get_first(opts.path)
|
||||
end
|
||||
|
||||
elseif net == "h2" then
|
||||
local opts = node["h2-opts"]
|
||||
if opts then
|
||||
o.transport.host = get_first(opts.host)
|
||||
o.transport.path = opts.path
|
||||
end
|
||||
|
||||
elseif net == "xhttp" then
|
||||
local opts = node["xhttp-opts"]
|
||||
if opts then
|
||||
o.transport.host = opts.host
|
||||
o.transport.path = opts.path
|
||||
o.transport.mode = opts.mode
|
||||
|
||||
local extra = {}
|
||||
|
||||
-- headers
|
||||
if opts.headers then
|
||||
extra.headers = opts.headers
|
||||
end
|
||||
|
||||
if opts["x-padding-bytes"] then
|
||||
extra.xPaddingBytes = opts["x-padding-bytes"]
|
||||
end
|
||||
|
||||
if opts["no-grpc-header"] ~= nil then
|
||||
extra.noGRPCHeader = opts["no-grpc-header"]
|
||||
end
|
||||
|
||||
if opts["sc-max-each-post-bytes"] then
|
||||
extra.scMaxEachPostBytes = opts["sc-max-each-post-bytes"]
|
||||
end
|
||||
|
||||
if opts["sc-min-posts-interval-ms"] then
|
||||
extra.scMinPostsIntervalMs = opts["sc-min-posts-interval-ms"]
|
||||
end
|
||||
|
||||
-- xmux
|
||||
if opts["reuse-settings"] then
|
||||
local r = opts["reuse-settings"]
|
||||
local xmux = {}
|
||||
|
||||
if r["max-concurrency"] then xmux.maxConcurrency = r["max-concurrency"] end
|
||||
if r["max-connections"] then xmux.maxConnections = tonumber(r["max-connections"]) end
|
||||
if r["c-max-reuse-times"] then xmux.cMaxReuseTimes = tonumber(r["c-max-reuse-times"]) end
|
||||
if r["h-max-request-times"] then xmux.hMaxRequestTimes = r["h-max-request-times"] end
|
||||
if r["h-max-reusable-secs"] then xmux.hMaxReusableSecs = r["h-max-reusable-secs"] end
|
||||
if r["h-keep-alive-period"] then xmux.hKeepAlivePeriod = tonumber(r["h-keep-alive-period"]) end
|
||||
|
||||
if next(xmux) then extra.xmux = xmux end
|
||||
end
|
||||
|
||||
-- download-settings
|
||||
if opts["download-settings"] then
|
||||
local d = opts["download-settings"]
|
||||
local ds = {}
|
||||
|
||||
if d.server then ds.address = domain_redir[d.server] or d.server end
|
||||
if d.port then ds.port = d.port end
|
||||
|
||||
ds.network = "xhttp"
|
||||
|
||||
-- TLS
|
||||
if d.tls then
|
||||
ds.security = "tls"
|
||||
ds.tlsSettings = {}
|
||||
|
||||
if d.servername then
|
||||
ds.tlsSettings.serverName = d.servername
|
||||
end
|
||||
if d["skip-cert-verify"] == true then
|
||||
ds.tlsSettings.allowInsecure = true
|
||||
end
|
||||
if d["client-fingerprint"] then
|
||||
ds.tlsSettings.fingerprint = d["client-fingerprint"]
|
||||
end
|
||||
if d.fingerprint then
|
||||
ds.tlsSettings.pinnedPeerCertSha256 = d.fingerprint
|
||||
end
|
||||
if d.alpn then
|
||||
ds.tlsSettings.alpn = d.alpn
|
||||
end
|
||||
end
|
||||
|
||||
-- xhttpSettings
|
||||
local xs = {}
|
||||
|
||||
if d.path then xs.path = d.path end
|
||||
if d.host then xs.host = d.host end
|
||||
|
||||
if next(xs) then
|
||||
ds.xhttpSettings = xs
|
||||
end
|
||||
|
||||
extra.downloadSettings = ds
|
||||
end
|
||||
|
||||
if next(extra) then
|
||||
o.transport.extra = json.stringify(extra)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
-- VLESS
|
||||
local function encode_vless(node)
|
||||
local o = build_common(node)
|
||||
|
||||
local link = "vless://" .. node.uuid .. "@" .. o.server .. ":" .. o.port
|
||||
local p = {}
|
||||
|
||||
if type(node.flow) == "string" and node.flow:sub(1, 5) == "xtls-" then table.insert(p, "flow=" .. urlencode(node.flow)) end
|
||||
if node.encryption then table.insert(p, "encryption=" .. urlencode(node.encryption)) end
|
||||
|
||||
-- TLS
|
||||
if o.tls.security then table.insert(p, "security=" .. o.tls.security) end
|
||||
if o.tls.pbk then table.insert(p, "pbk=" .. urlencode(o.tls.pbk)) end
|
||||
if o.tls.sid then table.insert(p, "sid=" .. urlencode(o.tls.sid)) end
|
||||
if o.tls.sni then table.insert(p, "sni=" .. urlencode(o.tls.sni)) end
|
||||
if o.tls.fp then table.insert(p, "fp=" .. urlencode(o.tls.fp)) end
|
||||
if o.tls.alpn then table.insert(p, "alpn=" .. urlencode(o.tls.alpn)) end
|
||||
if o.tls.ech then table.insert(p, "ech=" .. urlencode(o.tls.ech)) end
|
||||
if o.tls.pcs then table.insert(p, "pcs=" .. urlencode(o.tls.pcs)) end
|
||||
table.insert(p, "allowInsecure=" .. (o.tls.insecure and "1" or "0"))
|
||||
|
||||
-- transport
|
||||
table.insert(p, "type=" .. o.transport.type)
|
||||
if o.transport.host then table.insert(p, "host=" .. urlencode(o.transport.host)) end
|
||||
if o.transport.path then table.insert(p, "path=" .. urlencode(o.transport.path)) end
|
||||
if o.transport.serviceName then table.insert(p, "serviceName=" .. urlencode(o.transport.serviceName)) end
|
||||
if o.transport.mode then table.insert(p, "mode=" .. urlencode(o.transport.mode)) end
|
||||
if o.transport.extra then table.insert(p, "extra=" .. urlencode(o.transport.extra)) end
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return 0, link .. "#" .. urlencode(o.name or "")
|
||||
end
|
||||
|
||||
-- Trojan
|
||||
local function encode_trojan(node)
|
||||
local o = build_common(node)
|
||||
|
||||
local link = "trojan://" .. node.password .. "@" .. o.server .. ":" .. o.port
|
||||
local p = {}
|
||||
|
||||
if o.tls.security then table.insert(p, "security=" .. o.tls.security) end
|
||||
if o.tls.pbk then table.insert(p, "pbk=" .. urlencode(o.tls.pbk)) end
|
||||
if o.tls.sid then table.insert(p, "sid=" .. urlencode(o.tls.sid)) end
|
||||
if o.tls.sni then table.insert(p, "sni=" .. urlencode(o.tls.sni)) end
|
||||
if o.tls.fp then table.insert(p, "fp=" .. urlencode(o.tls.fp)) end
|
||||
if o.tls.alpn then table.insert(p, "alpn=" .. urlencode(o.tls.alpn)) end
|
||||
if o.tls.ech then table.insert(p, "ech=" .. urlencode(o.tls.ech)) end
|
||||
if o.tls.pcs then table.insert(p, "pcs=" .. urlencode(o.tls.pcs)) end
|
||||
table.insert(p, "allowInsecure=" .. (o.tls.insecure and "1" or "0"))
|
||||
|
||||
table.insert(p, "type=" .. o.transport.type)
|
||||
if o.transport.host then table.insert(p, "host=" .. urlencode(o.transport.host)) end
|
||||
if o.transport.path then table.insert(p, "path=" .. urlencode(o.transport.path)) end
|
||||
if o.transport.serviceName then table.insert(p, "serviceName=" .. urlencode(o.transport.serviceName)) end
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return 0, link .. "#" .. urlencode(o.name or "")
|
||||
end
|
||||
|
||||
-- VMess
|
||||
local function encode_vmess(node)
|
||||
local o = build_common(node)
|
||||
|
||||
local obj = {
|
||||
v = "2",
|
||||
ps = o.name,
|
||||
add = node.server,
|
||||
port = tostring(node.port),
|
||||
id = node.uuid,
|
||||
aid = tostring(node.alterId or 0),
|
||||
net = o.transport.type,
|
||||
security = node.cipher,
|
||||
scy = node.cipher,
|
||||
type = "none",
|
||||
host = o.transport.host or "",
|
||||
path = o.transport.path or "",
|
||||
tls = o.tls.security == "tls" and "tls" or "",
|
||||
sni = o.tls.sni,
|
||||
alpn = o.tls.alpn,
|
||||
fp = o.tls.fp,
|
||||
ech = o.tls.ech,
|
||||
pcs = o.tls.pcs,
|
||||
insecure = o.tls.insecure and "1" or "0",
|
||||
tfo = node.tfo and "1" or "0"
|
||||
}
|
||||
|
||||
if o.transport.type == "grpc" then
|
||||
obj.path = o.transport.serviceName or ""
|
||||
end
|
||||
|
||||
return 0, "vmess://" .. base64(json.stringify(obj))
|
||||
end
|
||||
|
||||
-- SS
|
||||
local function encode_ss(node)
|
||||
local ss_method = {
|
||||
["none"]=1, ["aes-128-gcm"]=1, ["aes-192-gcm"]=1, ["aes-256-gcm"]=1, ["chacha20-ietf-poly1305"]=1, ["xchacha20-ietf-poly1305"]=1, ["2022-blake3-aes-128-gcm"]=1, ["2022-blake3-aes-256-gcm"]=1, ["2022-blake3-chacha20-poly1305"]=1,
|
||||
["aes-128-ctr"]=1, ["aes-192-ctr"]=1, ["aes-256-ctr"]=1, ["aes-128-cfb"]=1, ["aes-192-cfb"]=1, ["aes-256-cfb"]=1, ["rc4-md5"]=1, ["chacha20-ietf"]=1, ["xchacha20"]=1,
|
||||
}
|
||||
if not ss_method[node.cipher or ""] then
|
||||
return 1, "订阅转换 → 丢弃 SS 节点:" .. (node.name or "") .. ",因 Core 不支持 " .. (node.cipher or "") .. " 加密方式"
|
||||
end
|
||||
|
||||
local userinfo = node.cipher .. ":" .. node.password
|
||||
local base = userinfo .. "@" .. host_format(node.server) .. ":" .. node.port
|
||||
local link = "ss://" .. base64(base)
|
||||
|
||||
local p = {}
|
||||
|
||||
if node.udp then table.insert(p, "udp=1") end
|
||||
if node["udp-over-tcp"] then table.insert(p, "uot=1") end
|
||||
|
||||
if node.plugin then
|
||||
local plugin = (node.plugin == "obfs") and "obfs-local" or node.plugin
|
||||
if plugin == "shadow-tls" then
|
||||
local shadow_tls = base64(json.stringify(node["plugin-opts"] or {}))
|
||||
table.insert(p, "shadow-tls=" .. urlencode(shadow_tls))
|
||||
elseif plugin == "obfs-local" or plugin == "v2ray-plugin" then
|
||||
local opts = {}
|
||||
for k, v in pairs(node["plugin-opts"] or {}) do
|
||||
if plugin == "obfs-local" then
|
||||
if k == "mode" then k = "obfs" end
|
||||
if k == "host" then k = "obfs-host" end
|
||||
elseif plugin == "v2ray-plugin" then
|
||||
if k == "mode" and v == "websocket" then
|
||||
v = nil
|
||||
elseif type(v) == "boolean" then
|
||||
if v == true then
|
||||
table.insert(opts, k)
|
||||
end
|
||||
v = nil
|
||||
end
|
||||
end
|
||||
if v ~= nil then
|
||||
if type(v) == "boolean" then
|
||||
v = v and "1" or "0"
|
||||
end
|
||||
table.insert(opts, k .. "=" .. v)
|
||||
end
|
||||
end
|
||||
if #opts > 0 then plugin = plugin .. ";" .. table.concat(opts, ";") end
|
||||
table.insert(p, "plugin=" .. urlencode(plugin))
|
||||
else
|
||||
return 1, "订阅转换 → 丢弃 SS 节点:" .. (node.name or "") .. ",因 Core 不支持 " .. plugin .. " 插件"
|
||||
end
|
||||
end
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return 0, link .. "#" .. urlencode(node.name or "")
|
||||
end
|
||||
|
||||
-- Hysteria
|
||||
local function encode_hysteria2(node)
|
||||
local link = "hysteria://" .. host_format(node.server) .. ":" .. (node.port or "")
|
||||
local p = {}
|
||||
|
||||
if node["auth-str"] then table.insert(p, "auth=" .. node["auth-str"]) end
|
||||
if node["ports"] then table.insert(p, "mport=" .. node["ports"]) end
|
||||
if node.obfs then table.insert(p, "obfsParam=" .. node.obfs) end
|
||||
if node.sni then table.insert(p, "sni=" .. node.sni) end
|
||||
if node.up then table.insert(p, "upmbps=" .. node.up) end
|
||||
if node.down then table.insert(p, "downmbps=" .. node.down) end
|
||||
if node["skip-cert-verify"] then table.insert(p, "insecure=1") end
|
||||
if node["fingerprint"] then table.insert(p, "pinSHA256=" .. urlencode(node["fingerprint"])) end
|
||||
|
||||
if node.alpn then
|
||||
table.insert(p, "alpn=" .. urlencode(build_alpn(node.alpn)))
|
||||
end
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return 0, link .. "#" .. urlencode(node.name or "")
|
||||
end
|
||||
|
||||
-- Hysteria2
|
||||
local function encode_hysteria2(node)
|
||||
local link = "hysteria2://" .. (node.password or "") .. "@" .. host_format(node.server) .. ":" .. (node.port or "")
|
||||
local p = {}
|
||||
|
||||
if node["ports"] then table.insert(p, "mport=" .. urlencode(node["ports"])) end
|
||||
if node.obfs then table.insert(p, "obfs=" .. node.obfs) end
|
||||
if node["obfs-password"] then table.insert(p, "obfs-password=" .. node["obfs-password"]) end
|
||||
if node["obfs-min-packet-size"] then table.insert(p, "minPacketSize=" .. node["obfs-min-packet-size"]) end
|
||||
if node["obfs-max-packet-size"] then table.insert(p, "maxPacketSize=" .. node["obfs-max-packet-size"]) end
|
||||
if node.up then table.insert(p, "upmbps=" .. node.up) end
|
||||
if node.down then table.insert(p, "downmbps=" .. node.down) end
|
||||
|
||||
if node.sni then table.insert(p, "sni=" .. urlencode(node.sni)) end
|
||||
if node["skip-cert-verify"] then table.insert(p, "insecure=1") end
|
||||
if node["fingerprint"] then table.insert(p, "pinSHA256=" .. urlencode(node["fingerprint"])) end
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return 0, link .. "#" .. urlencode(node.name or "")
|
||||
end
|
||||
|
||||
-- TUIC
|
||||
local function encode_tuic(node)
|
||||
local link = "tuic://" .. node.uuid .. ":" .. node.password .. "@" .. host_format(node.server) .. ":" .. node.port
|
||||
local p = {}
|
||||
|
||||
if node["congestion-controller"] then
|
||||
table.insert(p, "congestion_control=" .. node["congestion-controller"])
|
||||
end
|
||||
|
||||
if node.alpn then
|
||||
table.insert(p, "alpn=" .. urlencode(build_alpn(node.alpn)))
|
||||
end
|
||||
|
||||
if node.sni then table.insert(p, "sni=" .. urlencode(node.sni)) end
|
||||
if node["disable-sni"] then table.insert(p, "disable_sni=1") end
|
||||
if node["skip-cert-verify"] then table.insert(p, "allowInsecure=1") end
|
||||
if node["udp-relay-mode"] then table.insert(p, "udp_relay_mode=" .. node["udp-relay-mode"]) end
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return 0, link .. "#" .. urlencode(node.name or "")
|
||||
end
|
||||
|
||||
-- AnyTLS
|
||||
local function encode_anytls(node)
|
||||
local err_msg
|
||||
if node["shadow-tls-opts"] then
|
||||
err_msg = "ShadowTLS"
|
||||
elseif node["restls-opts"] then
|
||||
err_msg = "ResTLS"
|
||||
elseif node["jls-opts"] then
|
||||
err_msg = "JLS"
|
||||
end
|
||||
if err_msg then
|
||||
err_msg = "订阅转换 → 丢弃 AnyTLS 节点:" .. (node.name or "") .. ",因 Sing-Box 不支持 AnyTLS + " .. err_msg
|
||||
return 1, err_msg
|
||||
end
|
||||
|
||||
local o = build_common(node)
|
||||
|
||||
local link = "anytls://" .. (node.password or "") .. "@" .. host_format(node.server) .. ":" .. node.port
|
||||
local p = {}
|
||||
|
||||
if o.tls.security then table.insert(p, "security=" .. o.tls.security) end
|
||||
if o.tls.pbk then table.insert(p, "pbk=" .. urlencode(o.tls.pbk)) end
|
||||
if o.tls.sid then table.insert(p, "sid=" .. urlencode(o.tls.sid)) end
|
||||
if o.tls.sni then table.insert(p, "sni=" .. urlencode(o.tls.sni)) end
|
||||
if o.tls.alpn then table.insert(p, "alpn=" .. urlencode(o.tls.alpn)) end
|
||||
if o.tls.fp then table.insert(p, "fp=" .. urlencode(o.tls.fp)) end
|
||||
if o.tls.ech then table.insert(p, "ech=" .. urlencode(o.tls.ech)) end
|
||||
if o.tls.pcs then
|
||||
table.insert(p, "insecure=1")
|
||||
else
|
||||
table.insert(p, "insecure=" .. (o.tls.insecure and "1" or "0"))
|
||||
end
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return 0, link .. "#" .. urlencode(node.name or "")
|
||||
end
|
||||
|
||||
-- SSR
|
||||
local function encode_ssr(node)
|
||||
local link = host_format(node.server) .. ":" .. node.port .. ":" .. (node.protocol or "") .. ":" ..
|
||||
(node.cipher or "") .. ":" .. (node.obfs or "") .. ":" .. base64(node.password)
|
||||
local p = {}
|
||||
|
||||
if node["obfs-param"] then table.insert(p, "obfsparam=" .. base64(node["obfs-param"])) end
|
||||
if node["protocol-param"] then table.insert(p, "protoparam=" .. base64(node["protocol-param"])) end
|
||||
table.insert(p, "remarks=" .. base64(node.name))
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return 0, "ssr://" .. base64(link)
|
||||
end
|
||||
|
||||
-- snell
|
||||
local function encode_snell(node)
|
||||
local err_msg
|
||||
local obfs = node["obfs-opts"]
|
||||
if obfs and obfs.mode ~= "http" and obfs.mode ~= "none" then
|
||||
err_msg = obfs.mode
|
||||
end
|
||||
if err_msg then
|
||||
err_msg = "订阅转换 → 丢弃 Snell 节点:" .. (node.name or "") .. ",因 Sing-Box 不支持 Snell + " .. err_msg
|
||||
return 1, err_msg
|
||||
end
|
||||
local version = node.version and tonumber(node.version) or 4
|
||||
version = (version == 5) and 4 or version
|
||||
if version < 4 then
|
||||
err_msg = "订阅转换 → 丢弃 Snell 节点:" .. (node.name or "") .. ",因 Sing-Box 不支持 Snell 版本小于 4"
|
||||
return 1, err_msg
|
||||
end
|
||||
|
||||
local link = "snell://" .. host_format(node.server) .. ":" .. node.port
|
||||
local p = {}
|
||||
|
||||
if node.psk then table.insert(p, "psk=" .. urlencode(node.psk)) end
|
||||
table.insert(p, "version=" .. version)
|
||||
if obfs.mode == "http" then
|
||||
table.insert(p, "obfs=http")
|
||||
if obfs.host then table.insert(p, "obfs-host=" .. urlencode(obfs.host)) end
|
||||
end
|
||||
table.insert(p, "reuse=" .. (node.reuse and "1" or "0"))
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return 0, link .. "#" .. urlencode(node.name or "")
|
||||
end
|
||||
|
||||
local function encode_node(node)
|
||||
if (not node.type) or (not node.name) then return nil end
|
||||
|
||||
local t = node.type
|
||||
|
||||
node.server = domain_redir[node.server] or node.server
|
||||
|
||||
if t == "vless" then return encode_vless(node)
|
||||
elseif t == "trojan" then return encode_trojan(node)
|
||||
elseif t == "vmess" then return encode_vmess(node)
|
||||
elseif t == "ss" then return encode_ss(node)
|
||||
elseif t == "hysteria" then return encode_hysteria(node)
|
||||
elseif t == "hysteria2" then return encode_hysteria2(node)
|
||||
elseif t == "tuic" then return encode_tuic(node)
|
||||
elseif t == "anytls" then return encode_anytls(node)
|
||||
elseif t == "ssr" then return encode_ssr(node)
|
||||
elseif t == "snell" then return encode_snell(node)
|
||||
else api.log("订阅转换 → 丢弃不支持的节点:" .. node.name .. ",节点类型:" .. t)
|
||||
end
|
||||
end
|
||||
|
||||
function parseClashNode(raw, remark)
|
||||
if not raw then return "" end
|
||||
local ok, lyaml = pcall(require, "lyaml")
|
||||
if not ok then return raw end
|
||||
|
||||
local data = lyaml.load(raw)
|
||||
if not data or type(data) ~= "table" then return raw end
|
||||
if not data.proxies then return "" end
|
||||
|
||||
api.log('检测到 Clash 订阅,正在进行转换 ...')
|
||||
|
||||
-- Some airports use hosts domain redirect to disguise the real node domain name.
|
||||
for k, v in pairs(data.hosts or {}) do
|
||||
if type(k) == "string" and type(v) == "string" then
|
||||
if api.datatypes.hostname(k) and api.datatypes.hostname(v) then
|
||||
domain_redir[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local links = {}
|
||||
for _, node in ipairs(data.proxies) do
|
||||
local err, link = encode_node(node)
|
||||
if err == 1 and link then
|
||||
api.log(link)
|
||||
elseif link then
|
||||
table.insert(links, link)
|
||||
end
|
||||
end
|
||||
|
||||
return #links > 0 and table.concat(links, "\n") or ""
|
||||
end
|
||||
|
||||
function parse_clash_sub_info(headers)
|
||||
local userinfo = headers:match("[Ss]ubscription%-userinfo:%s*([^\r\n]+)")
|
||||
if not userinfo then return nil end
|
||||
|
||||
local upload = tonumber(userinfo:match("upload=(%d+)")) or 0
|
||||
local download = tonumber(userinfo:match("download=(%d+)")) or 0
|
||||
local total = tonumber(userinfo:match("total=(%d+)")) or 0
|
||||
local expire = tonumber(userinfo:match("expire=(%d+)"))
|
||||
local remain = total - (upload + download)
|
||||
if remain < 0 then remain = 0 end
|
||||
|
||||
local function format_size(bytes)
|
||||
local units = { "B", "KB", "MB", "GB", "TB", "PB" }
|
||||
local i = 1
|
||||
while bytes >= 1024 and i < #units do
|
||||
bytes = bytes / 1024
|
||||
i = i + 1
|
||||
end
|
||||
if bytes >= 100 then
|
||||
return string.format("%.0f%s", bytes, units[i])
|
||||
elseif bytes >= 10 then
|
||||
return string.format("%.1f%s", bytes, units[i])
|
||||
else
|
||||
return string.format("%.2f%s", bytes, units[i])
|
||||
end
|
||||
end
|
||||
|
||||
local rem_traffic = format_size(remain)
|
||||
|
||||
local expired_date
|
||||
if expire and expire > 0 then
|
||||
local t = os.date("*t", expire)
|
||||
expired_date = string.format("%d-%d-%d", t.year, t.month, t.day)
|
||||
else
|
||||
expired_date = "长期有效"
|
||||
end
|
||||
|
||||
return {
|
||||
rem_traffic = rem_traffic,
|
||||
expired_date = expired_date
|
||||
}
|
||||
end
|
||||
@@ -1,241 +0,0 @@
|
||||
#!/usr/bin/lua
|
||||
|
||||
local api = require ("luci.passwall.api")
|
||||
local appname = api.appname
|
||||
local fs = api.fs
|
||||
local uci = api.uci
|
||||
local sys = api.sys
|
||||
|
||||
local log = api.log
|
||||
|
||||
function get_ip_port_from(str)
|
||||
local result_port = sys.exec("echo -n " .. str .. " | sed -n 's/^.*[:#]\\([0-9]*\\)$/\\1/p'")
|
||||
local result_ip = sys.exec(string.format("__host=%s;__varport=%s;", str, result_port) .. "echo -n ${__host%%${__varport:+[:#]${__varport}*}}")
|
||||
return result_ip, result_port
|
||||
end
|
||||
|
||||
local var = api.get_args(arg)
|
||||
local haproxy_path = var["-path"]
|
||||
local haproxy_conf = var["-conf"]
|
||||
local haproxy_dns = "127.0.0.1"
|
||||
|
||||
local cpu_thread = sys.exec('echo -n $(cat /proc/cpuinfo | grep "processor" | wc -l)') or "1"
|
||||
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") or "1188"
|
||||
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
|
||||
|
||||
log("HAPROXY 负载均衡:")
|
||||
log(string.format(" * 控制台端口:%s", console_port))
|
||||
fs.mkdir(haproxy_path)
|
||||
local haproxy_file = haproxy_path .. "/" .. haproxy_conf
|
||||
|
||||
local f_out = io.open(haproxy_file, "a")
|
||||
|
||||
local haproxy_config = [[
|
||||
global
|
||||
daemon
|
||||
log 127.0.0.1 local2
|
||||
maxconn 60000
|
||||
stats socket {{path}}/haproxy.sock
|
||||
nbthread {{nbthread}}
|
||||
external-check
|
||||
insecure-fork-wanted
|
||||
spread-checks 50
|
||||
tune.max-checks-per-thread 5
|
||||
|
||||
defaults
|
||||
mode tcp
|
||||
log global
|
||||
option tcplog
|
||||
option dontlognull
|
||||
option abortonclose
|
||||
#option forwardfor except 127.0.0.0/8
|
||||
option redispatch
|
||||
retries 2
|
||||
timeout http-request 10s
|
||||
timeout queue 1m
|
||||
timeout connect 10s
|
||||
timeout client 1m
|
||||
timeout server 1m
|
||||
timeout http-keep-alive 10s
|
||||
timeout check 10s
|
||||
maxconn 3000
|
||||
|
||||
resolvers mydns
|
||||
resolve_retries 1
|
||||
timeout resolve 5s
|
||||
hold valid 600s
|
||||
{{dns}}
|
||||
]]
|
||||
|
||||
haproxy_config = haproxy_config:gsub("{{path}}", haproxy_path)
|
||||
haproxy_config = haproxy_config:gsub("{{nbthread}}", cpu_thread)
|
||||
|
||||
local mydns = ""
|
||||
local index = 0
|
||||
string.gsub(haproxy_dns, '[^' .. "," .. ']+', function(w)
|
||||
index = index + 1
|
||||
local s = w:gsub("#", ":")
|
||||
if not s:find(":") then
|
||||
s = s .. ":53"
|
||||
end
|
||||
mydns = mydns .. (index > 1 and "\n" or "") .. " " .. string.format("nameserver dns%s %s", index, s)
|
||||
end)
|
||||
haproxy_config = haproxy_config:gsub("{{dns}}", mydns)
|
||||
|
||||
f_out:write(haproxy_config)
|
||||
|
||||
local listens = {}
|
||||
|
||||
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 = 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
|
||||
server_remark = server_node.address .. ":" .. (server_node.port or hop)
|
||||
server_address = server_node.address
|
||||
server_port = server_node.port or hop
|
||||
t.origin_address = server_address
|
||||
t.origin_port = server_port
|
||||
if health_check_type == "script_logic" then
|
||||
if server_node.type ~= "Socks" then
|
||||
local new_port
|
||||
local cache = api.get_socks_port_by_cache(server_node[".name"])
|
||||
if cache then
|
||||
new_port = cache
|
||||
else
|
||||
new_port = api.get_new_port()
|
||||
local config_file = string.format("%s_%s.json", t[".name"], new_port)
|
||||
sys.call(string.format('/usr/share/%s/app.sh run_socks "%s"> /dev/null',
|
||||
appname,
|
||||
string.format("flag=%s node=%s bind=%s socks_port=%s config_file=%s",
|
||||
new_port, --flag
|
||||
server_node[".name"], --node
|
||||
"127.0.0.1", --bind
|
||||
new_port, --socks port
|
||||
config_file --config file
|
||||
)
|
||||
)
|
||||
)
|
||||
api.set_socks_port_to_cache(server_node[".name"], new_port)
|
||||
end
|
||||
server_address = "127.0.0.1"
|
||||
server_port = new_port
|
||||
end
|
||||
end
|
||||
else
|
||||
server_address, server_port = get_ip_port_from(lbss)
|
||||
server_remark = server_address .. ":" .. server_port
|
||||
t.origin_address = server_address
|
||||
t.origin_port = server_port
|
||||
end
|
||||
if server_address and server_port and listen_port > 0 then
|
||||
if not listens[listen_port] then
|
||||
listens[listen_port] = {}
|
||||
end
|
||||
t.server_remark = server_remark
|
||||
t.server_address = server_address
|
||||
t.server_port = server_port
|
||||
table.insert(listens[listen_port], t)
|
||||
else
|
||||
log(" - 丢弃1个明显无效的节点")
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
local inter = tonumber((health_check_inter or "20"):match("^%d+"))
|
||||
inter = (inter and inter >= 20) and inter or 20
|
||||
local inter_arg = "inter " .. inter .. "s fastinter " .. inter / 2 .. "s downinter " .. inter * 1.5 .. "s"
|
||||
|
||||
local sortTable = {}
|
||||
for i in pairs(listens) do
|
||||
if i ~= nil then
|
||||
table.insert(sortTable, i)
|
||||
end
|
||||
end
|
||||
table.sort(sortTable, function(a,b) return (a < b) end)
|
||||
|
||||
for i, port in pairs(sortTable) do
|
||||
log(" + 入口 %s:%s" % {bind_address, port})
|
||||
|
||||
f_out:write("\n" .. string.format([[
|
||||
listen %s
|
||||
bind %s:%s
|
||||
mode tcp
|
||||
balance %s
|
||||
]], port, bind_address, port, balancingStrategy))
|
||||
|
||||
if health_check_type == "script_logic" then
|
||||
f_out:write(string.format([[
|
||||
option external-check
|
||||
external-check command "/usr/share/%s/haproxy_check.sh"
|
||||
]], appname))
|
||||
end
|
||||
|
||||
local count_M, count_B = 1, 1
|
||||
for i, o in ipairs(listens[port]) do
|
||||
local remark = o.server_remark or ""
|
||||
-- 防止重名导致无法运行
|
||||
if tostring(o.backup) ~= "1" then
|
||||
remark = "M" .. count_M .. "-" .. remark
|
||||
count_M = count_M + 1
|
||||
else
|
||||
remark = "B" .. count_B .. "-" .. remark
|
||||
count_B = count_B + 1
|
||||
end
|
||||
local server = o.server_address .. ":" .. o.server_port
|
||||
local server_conf = "server {{remark}} {{server}} weight {{weight}} {{resolvers}} check {{inter_arg}} rise 2 fall 2 {{backup}}"
|
||||
server_conf = server_conf:gsub("{{remark}}", remark)
|
||||
server_conf = server_conf:gsub("{{server}}", server)
|
||||
server_conf = server_conf:gsub("{{weight}}", o.lbweight)
|
||||
local resolvers = "resolvers mydns"
|
||||
if api.is_ip(o.server_address) then
|
||||
resolvers = ""
|
||||
end
|
||||
server_conf = server_conf:gsub("{{resolvers}}", resolvers)
|
||||
server_conf = server_conf:gsub("{{inter_arg}}", inter_arg)
|
||||
server_conf = server_conf:gsub("{{backup}}", tostring(o.backup) == "1" and "backup" or "")
|
||||
|
||||
f_out:write(" " .. api.trim(server_conf) .. "\n")
|
||||
|
||||
if o.export ~= "0" then
|
||||
sys.call(string.format(". /usr/share/%s/utils.sh ; add_ip2route %s %s", appname, o.origin_address, o.export))
|
||||
end
|
||||
|
||||
log(string.format(" | - 出口节点:%s:%s,权重:%s", o.origin_address, o.origin_port, o.lbweight))
|
||||
end
|
||||
end
|
||||
|
||||
--控制台配置
|
||||
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
|
||||
mode http
|
||||
stats refresh 30s
|
||||
stats uri /
|
||||
stats admin if TRUE
|
||||
%s
|
||||
]]
|
||||
f_out:write("\n" .. string.format(str, console_port, (console_user and console_user ~= "" and console_password and console_password ~= "") and "stats auth " .. console_user .. ":" .. console_password or ""))
|
||||
|
||||
f_out:close()
|
||||
|
||||
--passwall内置健康检查URL
|
||||
if health_check_type == "script_logic" then
|
||||
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()
|
||||
end
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
export PATH=/usr/sbin:/usr/bin:/sbin:/bin:/root/bin
|
||||
CONFIG=passwall
|
||||
|
||||
listen_address=$1
|
||||
listen_port=$2
|
||||
server_address=$3
|
||||
server_port=$4
|
||||
|
||||
busybox pgrep -af "${CONFIG}/" | grep -E 'app\.sh.*(start|stop)|nftables\.sh|iptables\.sh|subscribe\.lua' >/dev/null && {
|
||||
# 特定任务执行中不检测
|
||||
exit 0
|
||||
}
|
||||
|
||||
probe_file="/tmp/etc/passwall/haproxy/Probe_URL"
|
||||
probeUrl="https://www.google.com/generate_204"
|
||||
if [ -f "$probe_file" ]; then
|
||||
firstLine=$(head -n 1 "$probe_file" | tr -d ' \t\n')
|
||||
[ -n "$firstLine" ] && probeUrl="$firstLine"
|
||||
fi
|
||||
|
||||
extra_params="-x socks5h://${server_address}:${server_port}"
|
||||
if /usr/bin/curl --help all | grep -q "\-\-retry-all-errors"; then
|
||||
extra_params="${extra_params} --retry-all-errors"
|
||||
fi
|
||||
|
||||
status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout 3 --retry 1 --max-time 6 -w "%{http_code}" "${probeUrl}")
|
||||
|
||||
case "$status" in
|
||||
200|204)
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -1,554 +0,0 @@
|
||||
local api = require "luci.passwall.api"
|
||||
|
||||
local var = api.get_args(arg)
|
||||
local FLAG = var["-FLAG"]
|
||||
local LISTEN_PORT = var["-LISTEN_PORT"]
|
||||
local DNS_LOCAL = var["-DNS_LOCAL"]
|
||||
local DNS_TRUST = var["-DNS_TRUST"]
|
||||
local USE_DIRECT_LIST = var["-USE_DIRECT_LIST"]
|
||||
local USE_PROXY_LIST = var["-USE_PROXY_LIST"]
|
||||
local USE_BLOCK_LIST = var["-USE_BLOCK_LIST"]
|
||||
local GFWLIST = var["-GFWLIST"]
|
||||
local CHNLIST = var["-CHNLIST"]
|
||||
local NO_IPV6_TRUST = var["-NO_IPV6_TRUST"]
|
||||
local DEFAULT_MODE = var["-DEFAULT_MODE"]
|
||||
local DEFAULT_TAG = var["-DEFAULT_TAG"]
|
||||
local NO_LOGIC_LOG = var["-NO_LOGIC_LOG"]
|
||||
local NODE = var["-NODE"]
|
||||
local NFTFLAG = var["-NFTFLAG"]
|
||||
local FILTER_HTTPS = var["-FILTER_HTTPS"]
|
||||
local LOG_FILE = var["-LOG_FILE"]
|
||||
|
||||
local sys = api.sys
|
||||
local fs = api.fs
|
||||
local datatypes = api.datatypes
|
||||
|
||||
local TMP_PATH = api.TMP_PATH
|
||||
local TMP_ACL_PATH = TMP_PATH .. "/acl"
|
||||
local RULES_PATH = "/usr/share/passwall/rules"
|
||||
local FLAG_PATH = TMP_ACL_PATH .. "/" .. FLAG
|
||||
local config_lines = {}
|
||||
local tmp_lines = {}
|
||||
local USE_GEOVIEW = api.uci_get_c("@global_rules[0]", "enable_geoview")
|
||||
local IS_SHUNT_NODE = api.uci_get_c(NODE, "protocol") == "_shunt"
|
||||
|
||||
if not api.is_finded("geoview") then
|
||||
USE_GEOVIEW = "0"
|
||||
end
|
||||
|
||||
local function log(...)
|
||||
if NO_LOGIC_LOG == "1" then
|
||||
return
|
||||
end
|
||||
api.log(...)
|
||||
end
|
||||
|
||||
local function is_file_nonzero(path)
|
||||
if path and #path > 1 then
|
||||
if sys.exec('[ -s "%s" ] && echo -n 1' % path) == "1" then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function insert_unique(dest_table, value, lookup_table)
|
||||
if not lookup_table[value] then
|
||||
table.insert(dest_table, value)
|
||||
lookup_table[value] = true
|
||||
end
|
||||
end
|
||||
|
||||
local function merge_array(array1, array2)
|
||||
for i, line in ipairs(array2) do
|
||||
table.insert(array1, #array1 + 1, line)
|
||||
end
|
||||
end
|
||||
|
||||
local function insert_array_before(array1, array2, target) --将array2插入到array1的target前面,target不存在则追加
|
||||
for i, line in ipairs(array1) do
|
||||
if line == target then
|
||||
for j = #array2, 1, -1 do
|
||||
table.insert(array1, i, array2[j])
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
merge_array(array1, array2)
|
||||
end
|
||||
|
||||
local function insert_array_after(array1, array2, target) --将array2插入到array1的target后面,target不存在则追加
|
||||
for i, line in ipairs(array1) do
|
||||
if line == target then
|
||||
for j = 1, #array2 do
|
||||
table.insert(array1, i + j, array2[j])
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
merge_array(array1, array2)
|
||||
end
|
||||
|
||||
local function get_geosite(list_arg, out_path)
|
||||
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")
|
||||
if bin and list_arg and out_path then
|
||||
local cmd = string.format("%q -type geosite -append=true -input %q -list %q -output %q -lowmem=true",
|
||||
bin, geosite_path, list_arg, out_path)
|
||||
sys.call(cmd)
|
||||
return 0
|
||||
end
|
||||
return 1
|
||||
end
|
||||
|
||||
if not fs.access(FLAG_PATH) then
|
||||
fs.mkdir(FLAG_PATH)
|
||||
end
|
||||
|
||||
local setflag = (NFTFLAG == "1") and "inet@passwall@" or ""
|
||||
|
||||
local only_global = (DEFAULT_MODE == "proxy" and CHNLIST == "0" and GFWLIST == "0") and 1
|
||||
|
||||
config_lines = {
|
||||
LOG_FILE ~= "/dev/null" and "verbose" or "",
|
||||
"bind-addr ::",
|
||||
"bind-port " .. LISTEN_PORT,
|
||||
"china-dns " .. DNS_LOCAL,
|
||||
"trust-dns " .. DNS_TRUST,
|
||||
tonumber(FILTER_HTTPS) == 1 and "filter-qtype 65" or ""
|
||||
}
|
||||
|
||||
for i = 1, 6 do
|
||||
table.insert(config_lines, "#--" .. i)
|
||||
end
|
||||
|
||||
--自定义规则组,后声明的组具有更高优先级
|
||||
--屏蔽列表
|
||||
local file_block_host = TMP_ACL_PATH .. "/block_host"
|
||||
if USE_BLOCK_LIST == "1" and not fs.access(file_block_host) then
|
||||
local block_domain, lookup_block_domain = {}, {}
|
||||
local geosite_arg = ""
|
||||
local f = io.open(RULES_PATH .. "/block_host")
|
||||
if f then
|
||||
for line in f:lines() do
|
||||
if not line:find("#") and line:find("geosite:") then
|
||||
line = string.match(line, ":([^:]+)$")
|
||||
geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line
|
||||
else
|
||||
line = api.get_std_domain(line)
|
||||
if line ~= "" and not line:find("#") then
|
||||
insert_unique(block_domain, line, lookup_block_domain)
|
||||
end
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
end
|
||||
if #block_domain > 0 then
|
||||
local f_out = io.open(file_block_host, "w")
|
||||
for i = 1, #block_domain do
|
||||
f_out:write(block_domain[i] .. "\n")
|
||||
end
|
||||
f_out:close()
|
||||
end
|
||||
if USE_GEOVIEW == "1" and geosite_arg ~= "" then
|
||||
if get_geosite(geosite_arg, file_block_host) == 0 then
|
||||
log(" - 解析[屏蔽列表] Geosite 到屏蔽域名表(blocklist)完成")
|
||||
else
|
||||
log(" - 解析[屏蔽列表] Geosite 到屏蔽域名表(blocklist)失败!")
|
||||
end
|
||||
end
|
||||
end
|
||||
if USE_BLOCK_LIST == "1" and is_file_nonzero(file_block_host) then
|
||||
tmp_lines = {
|
||||
"group null",
|
||||
"group-dnl " .. file_block_host
|
||||
}
|
||||
insert_array_after(config_lines, tmp_lines, "#--5")
|
||||
end
|
||||
|
||||
--始终用国内DNS解析节点域名
|
||||
local file_vpslist = TMP_ACL_PATH .. "/vpslist"
|
||||
if not is_file_nonzero(file_vpslist) then
|
||||
local f_out = io.open(file_vpslist, "w")
|
||||
local written_domains = {}
|
||||
local function process_address(address)
|
||||
address = (address or ""):lower()
|
||||
if api.vps_domain_exclude(address) then return end
|
||||
if datatypes.hostname(address) and not written_domains[address] then
|
||||
f_out:write(address .. "\n")
|
||||
written_domains[address] = true
|
||||
end
|
||||
end
|
||||
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 "")
|
||||
if dns and dns ~= "" then
|
||||
process_address(dns)
|
||||
end
|
||||
end)
|
||||
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)
|
||||
end
|
||||
end)
|
||||
f_out:close()
|
||||
end
|
||||
if is_file_nonzero(file_vpslist) then
|
||||
local sets = {
|
||||
setflag .. "psw_vps",
|
||||
setflag .. "psw_vps6"
|
||||
}
|
||||
tmp_lines = {
|
||||
"group vpslist",
|
||||
"group-dnl " .. file_vpslist,
|
||||
"group-upstream " .. DNS_LOCAL,
|
||||
"group-ipset " .. table.concat(sets, ",")
|
||||
}
|
||||
insert_array_after(config_lines, tmp_lines, "#--6")
|
||||
log(string.format(" - 节点列表中的域名(vpslist):%s", DNS_LOCAL or "默认"))
|
||||
end
|
||||
|
||||
--直连(白名单)列表
|
||||
local file_direct_host = TMP_ACL_PATH .. "/direct_host"
|
||||
if USE_DIRECT_LIST == "1" and not fs.access(file_direct_host) then
|
||||
local direct_domain, lookup_direct_domain = {}, {}
|
||||
local geosite_arg = ""
|
||||
local f = io.open(RULES_PATH .. "/direct_host")
|
||||
if f then
|
||||
for line in f:lines() do
|
||||
if not line:find("#") and line:find("geosite:") then
|
||||
line = string.match(line, ":([^:]+)$")
|
||||
geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line
|
||||
else
|
||||
line = api.get_std_domain(line)
|
||||
if line ~= "" and not line:find("#") and not line:find(":") then
|
||||
insert_unique(direct_domain, line, lookup_direct_domain)
|
||||
end
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
end
|
||||
if #direct_domain > 0 then
|
||||
local f_out = io.open(file_direct_host, "w")
|
||||
for i = 1, #direct_domain do
|
||||
f_out:write(direct_domain[i] .. "\n")
|
||||
end
|
||||
f_out:close()
|
||||
end
|
||||
if USE_GEOVIEW == "1" and geosite_arg ~= "" then
|
||||
if get_geosite(geosite_arg, file_direct_host) == 0 then
|
||||
log(" - 解析[直连列表] Geosite 到域名白名单(whitelist)完成")
|
||||
else
|
||||
log(" - 解析[直连列表] Geosite 到域名白名单(whitelist)失败!")
|
||||
end
|
||||
end
|
||||
end
|
||||
if USE_DIRECT_LIST == "1" and is_file_nonzero(file_direct_host) then
|
||||
local sets = {
|
||||
setflag .. "psw_white",
|
||||
setflag .. "psw_white6"
|
||||
}
|
||||
tmp_lines = {
|
||||
"group directlist",
|
||||
"group-dnl " .. file_direct_host,
|
||||
"group-upstream " .. DNS_LOCAL,
|
||||
"group-ipset " .. table.concat(sets, ",")
|
||||
}
|
||||
insert_array_after(config_lines, tmp_lines, "#--4")
|
||||
log(string.format(" - 域名白名单(whitelist):%s", DNS_LOCAL or "默认"))
|
||||
end
|
||||
|
||||
--代理(黑名单)列表
|
||||
local file_proxy_host = TMP_ACL_PATH .. "/proxy_host"
|
||||
if USE_PROXY_LIST == "1" and not fs.access(file_proxy_host) then
|
||||
local proxy_domain, lookup_proxy_domain = {}, {}
|
||||
local geosite_arg = ""
|
||||
local f = io.open(RULES_PATH .. "/proxy_host")
|
||||
if f then
|
||||
for line in f:lines() do
|
||||
if not line:find("#") and line:find("geosite:") then
|
||||
line = string.match(line, ":([^:]+)$")
|
||||
geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line
|
||||
else
|
||||
line = api.get_std_domain(line)
|
||||
if line ~= "" and not line:find("#") and not line:find(":") then
|
||||
insert_unique(proxy_domain, line, lookup_proxy_domain)
|
||||
end
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
end
|
||||
if #proxy_domain > 0 then
|
||||
local f_out = io.open(file_proxy_host, "w")
|
||||
for i = 1, #proxy_domain do
|
||||
f_out:write(proxy_domain[i] .. "\n")
|
||||
end
|
||||
f_out:close()
|
||||
end
|
||||
if USE_GEOVIEW == "1" and geosite_arg ~= "" then
|
||||
if get_geosite(geosite_arg, file_proxy_host) == 0 then
|
||||
log(" - 解析[代理列表] Geosite 到代理域名表(blacklist)完成")
|
||||
else
|
||||
log(" - 解析[代理列表] Geosite 到代理域名表(blacklist)失败!")
|
||||
end
|
||||
end
|
||||
end
|
||||
if USE_PROXY_LIST == "1" and is_file_nonzero(file_proxy_host) then
|
||||
local sets = {
|
||||
setflag .. "psw_black",
|
||||
setflag .. "psw_black6"
|
||||
}
|
||||
if FLAG ~= "default" then
|
||||
sets = {
|
||||
setflag .. "psw_" .. FLAG .. "_black",
|
||||
setflag .. "psw_" .. FLAG .. "_black6"
|
||||
}
|
||||
end
|
||||
tmp_lines = {
|
||||
"group proxylist",
|
||||
"group-dnl " .. file_proxy_host,
|
||||
"group-upstream " .. DNS_TRUST,
|
||||
"group-ipset " .. table.concat(sets, ",")
|
||||
}
|
||||
if NO_IPV6_TRUST == "1" then table.insert(tmp_lines, "no-ipv6 tag:proxylist") end
|
||||
insert_array_after(config_lines, tmp_lines, "#--3")
|
||||
log(string.format(" - 代理域名表(blacklist):%s", DNS_TRUST or "默认"))
|
||||
end
|
||||
|
||||
--内置组(chn/gfw)优先级在自定义组后
|
||||
--GFW列表
|
||||
if GFWLIST == "1" and is_file_nonzero(RULES_PATH .. "/gfwlist") then
|
||||
local sets = {
|
||||
setflag .. "psw_gfw",
|
||||
setflag .. "psw_gfw6"
|
||||
}
|
||||
if FLAG ~= "default" then
|
||||
sets = {
|
||||
setflag .. "psw_" .. FLAG .. "_gfw",
|
||||
setflag .. "psw_" .. FLAG .. "_gfw6"
|
||||
}
|
||||
end
|
||||
tmp_lines = {
|
||||
"gfwlist-file " .. RULES_PATH .. "/gfwlist",
|
||||
"add-taggfw-ip " .. table.concat(sets, ",")
|
||||
}
|
||||
if NO_IPV6_TRUST == "1" then table.insert(tmp_lines, "no-ipv6 tag:gfw") end
|
||||
merge_array(config_lines, tmp_lines)
|
||||
log(string.format(" - 防火墙域名表(gfwlist):%s", DNS_TRUST or "默认"))
|
||||
end
|
||||
|
||||
--中国列表
|
||||
if CHNLIST ~= "0" and is_file_nonzero(RULES_PATH .. "/chnlist") then
|
||||
if CHNLIST == "direct" then
|
||||
local sets = {
|
||||
setflag .. "psw_chn",
|
||||
setflag .. "psw_chn6"
|
||||
}
|
||||
local suffix = (NFTFLAG == "1") and "_static" or ""
|
||||
tmp_lines = {
|
||||
"chnlist-file " .. RULES_PATH .. "/chnlist",
|
||||
"ipset-name4 " .. setflag .. "psw_chn" .. suffix,
|
||||
"ipset-name6 " .. setflag .. "psw_chn6" .. suffix,
|
||||
"add-tagchn-ip" .. ((NFTFLAG == "1") and (" " .. table.concat(sets, ",")) or "")
|
||||
}
|
||||
merge_array(config_lines, tmp_lines)
|
||||
log(string.format(" - 中国域名表(chnroute):%s", DNS_LOCAL or "默认"))
|
||||
end
|
||||
|
||||
--回中国模式
|
||||
if CHNLIST == "proxy" then
|
||||
local sets = {
|
||||
setflag .. "psw_chn",
|
||||
setflag .. "psw_chn6"
|
||||
}
|
||||
tmp_lines = {
|
||||
"group chn_proxy",
|
||||
"group-dnl " .. RULES_PATH .. "/chnlist",
|
||||
"group-upstream " .. DNS_TRUST,
|
||||
"group-ipset " .. table.concat(sets, ",")
|
||||
}
|
||||
if NO_IPV6_TRUST == "1" then table.insert(tmp_lines, "no-ipv6 tag:chn_proxy") end
|
||||
insert_array_after(config_lines, tmp_lines, "#--1")
|
||||
log(string.format(" - 中国域名表(chnroute):%s", DNS_TRUST or "默认"))
|
||||
end
|
||||
end
|
||||
|
||||
--分流规则
|
||||
if IS_SHUNT_NODE then
|
||||
local white_domain, lookup_white_domain = {}, {}
|
||||
local shunt_domain, lookup_shunt_domain = {}, {}
|
||||
local file_white_host = FLAG_PATH .. "/shunt_direct_host"
|
||||
local file_shunt_host = FLAG_PATH .. "/shunt_proxy_host"
|
||||
local geosite_white_arg, geosite_shunt_arg = "", ""
|
||||
|
||||
local t = api.uci_get_c(NODE)
|
||||
local default_node_id = t["default_node"] or "_direct"
|
||||
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
|
||||
_node_id = default_node_id
|
||||
end
|
||||
|
||||
local domain_list = s.domain_list or ""
|
||||
for line in string.gmatch(domain_list, "[^\r\n]+") do
|
||||
if line ~= "" and not line:find("#") and not line:find("regexp:") and not line:find("ext:") and not line:find("rule-set:") and not line:find("rs:") then
|
||||
if line:find("geosite:") then
|
||||
line = string.match(line, ":([^:]+)$")
|
||||
if _node_id == "_direct" then
|
||||
geosite_white_arg = geosite_white_arg .. (geosite_white_arg ~= "" and "," or "") .. line
|
||||
else
|
||||
geosite_shunt_arg = geosite_shunt_arg .. (geosite_shunt_arg ~= "" and "," or "") .. line
|
||||
end
|
||||
else
|
||||
if line:find("domain:") or line:find("full:") then
|
||||
line = string.match(line, ":([^:]+)$")
|
||||
end
|
||||
line = api.get_std_domain(line)
|
||||
if line ~= "" and not line:find("#") then
|
||||
if _node_id == "_direct" then
|
||||
insert_unique(white_domain, line, lookup_white_domain)
|
||||
else
|
||||
insert_unique(shunt_domain, line, lookup_shunt_domain)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if _node_id ~= "_direct" then
|
||||
log(string.format(" - Sing-Box/Xray分流规则(%s):%s", s.remarks, DNS_TRUST or "默认"))
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
if is_file_nonzero(file_white_host) == nil then
|
||||
if #white_domain > 0 then
|
||||
local f_out = io.open(file_white_host, "w")
|
||||
for i = 1, #white_domain do
|
||||
f_out:write(white_domain[i] .. "\n")
|
||||
end
|
||||
f_out:close()
|
||||
end
|
||||
end
|
||||
|
||||
if is_file_nonzero(file_shunt_host) == nil then
|
||||
if #shunt_domain > 0 then
|
||||
local f_out = io.open(file_shunt_host, "w")
|
||||
for i = 1, #shunt_domain do
|
||||
f_out:write(shunt_domain[i] .. "\n")
|
||||
end
|
||||
f_out:close()
|
||||
end
|
||||
end
|
||||
|
||||
if GFWLIST == "1" and CHNLIST == "0" and USE_GEOVIEW == "1" then --仅GFW模式解析geosite
|
||||
local return_white, return_shunt
|
||||
if geosite_white_arg ~= "" then
|
||||
return_white = get_geosite(geosite_white_arg, file_white_host)
|
||||
end
|
||||
if geosite_shunt_arg ~= "" then
|
||||
return_shunt = get_geosite(geosite_shunt_arg, file_shunt_host)
|
||||
end
|
||||
if (return_white == nil or return_white == 0) and (return_shunt == nil or return_shunt == 0) then
|
||||
log(" - 解析[分流节点] Geosite 完成")
|
||||
else
|
||||
log(" - 解析[分流节点] Geosite 失败!")
|
||||
end
|
||||
end
|
||||
|
||||
local sets = {
|
||||
setflag .. "psw_shunt",
|
||||
setflag .. "psw_shunt6"
|
||||
}
|
||||
if FLAG ~= "default" then
|
||||
sets = {
|
||||
setflag .. "psw_" .. FLAG .. "_shunt",
|
||||
setflag .. "psw_" .. FLAG .. "_shunt6"
|
||||
}
|
||||
end
|
||||
|
||||
if is_file_nonzero(file_white_host) then
|
||||
if USE_DIRECT_LIST == "1" then
|
||||
--当白名单启用时,添加到白名单组一同处理
|
||||
for i, v in ipairs(config_lines) do
|
||||
if v == "group-dnl " .. file_direct_host then
|
||||
config_lines[i] = "group-dnl " .. file_direct_host .. "," .. file_white_host
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
--当白名单不启用时,创建新组,ipset到shuntlist
|
||||
tmp_lines = {
|
||||
"group whitelist",
|
||||
"group-dnl " .. file_white_host,
|
||||
"group-upstream " .. DNS_LOCAL,
|
||||
"group-ipset " .. table.concat(sets, ",")
|
||||
}
|
||||
insert_array_after(config_lines, tmp_lines, "#--4")
|
||||
end
|
||||
end
|
||||
|
||||
if is_file_nonzero(file_shunt_host) then
|
||||
tmp_lines = {
|
||||
"group shuntlist",
|
||||
"group-dnl " .. file_shunt_host,
|
||||
"group-upstream " .. DNS_TRUST,
|
||||
"group-ipset " .. table.concat(sets, ",")
|
||||
}
|
||||
if NO_IPV6_TRUST == "1" then table.insert(tmp_lines, "no-ipv6 tag:shuntlist") end
|
||||
insert_array_after(config_lines, tmp_lines, "#--2")
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
--只使用gfwlist模式,GFW列表以外的域名及默认使用本地DNS
|
||||
if GFWLIST == "1" and CHNLIST == "0" then DEFAULT_TAG = "chn" end
|
||||
|
||||
--回中国模式,中国列表以外的域名及默认使用本地DNS
|
||||
if CHNLIST == "proxy" then DEFAULT_TAG = "chn" end
|
||||
|
||||
--全局模式,默认使用远程DNS
|
||||
if only_global then
|
||||
DEFAULT_TAG = "gfw"
|
||||
if NO_IPV6_TRUST == "1" and not IS_SHUNT_NODE then
|
||||
table.insert(config_lines, "no-ipv6")
|
||||
end
|
||||
end
|
||||
|
||||
--是否接受直连 DNS 空响应
|
||||
if DEFAULT_TAG == "none_noip" then table.insert(config_lines, "noip-as-chnip") end
|
||||
|
||||
if DEFAULT_TAG == nil or DEFAULT_TAG == "smart" or DEFAULT_TAG == "none_noip" then DEFAULT_TAG = "none" end
|
||||
|
||||
table.insert(config_lines, "default-tag " .. DEFAULT_TAG)
|
||||
|
||||
if DEFAULT_TAG == "none" then
|
||||
table.insert(config_lines, "verdict-cache 5000")
|
||||
end
|
||||
|
||||
table.insert(config_lines, "hosts")
|
||||
|
||||
if DEFAULT_TAG == "chn" then
|
||||
log(string.format(" - 默认 DNS :%s", DNS_LOCAL))
|
||||
elseif DEFAULT_TAG == "gfw" then
|
||||
log(string.format(" - 默认 DNS :%s", DNS_TRUST))
|
||||
else
|
||||
log(string.format(" - 默认 DNS :%s", "智能匹配"))
|
||||
end
|
||||
|
||||
--输出配置文件
|
||||
if #config_lines > 0 then
|
||||
for i = 1, #config_lines do
|
||||
line = config_lines[i]
|
||||
if line ~= "" and not line:find("^#--") then
|
||||
print(line)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
log(" - ChinaDNS-NG已作为Dnsmasq上游,如果你自行配置了错误的DNS流程,将会导致域名(直连/代理域名)分流失效!!!")
|
||||
@@ -1,828 +0,0 @@
|
||||
local api = require "luci.passwall.api"
|
||||
local sys = api.sys
|
||||
local fs = api.fs
|
||||
local datatypes = api.datatypes
|
||||
local TMP = {}
|
||||
|
||||
local function tinsert(table_name, val)
|
||||
if table_name and type(table_name) == "table" then
|
||||
if not TMP[table_name] then
|
||||
TMP[table_name] = {}
|
||||
end
|
||||
if TMP[table_name][val] then
|
||||
return false
|
||||
end
|
||||
table.insert(table_name, val)
|
||||
TMP[table_name][val] = true
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function backup_servers()
|
||||
local DNSMASQ_DNS = api.uci_get("dhcp", "@dnsmasq[0]", "server")
|
||||
if DNSMASQ_DNS and #DNSMASQ_DNS > 0 then
|
||||
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 = 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 = 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
|
||||
api.uci_del_c("@global[0]", "dnsmasq_servers")
|
||||
api.uci_save_c(true)
|
||||
end
|
||||
if dns_table and #dns_table > 0 then
|
||||
api.uci_set("dhcp", "@dnsmasq[0]", "server", dns_table)
|
||||
api.uci_save(nil, "dhcp", true)
|
||||
end
|
||||
end
|
||||
|
||||
function stretch()
|
||||
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
|
||||
if not v:find("/") then
|
||||
_flag = true
|
||||
end
|
||||
end
|
||||
end
|
||||
if not _flag and dnsmasq_noresolv == "1" then
|
||||
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
|
||||
RESOLVFILE = "/tmp/resolv.conf.auto"
|
||||
else
|
||||
local size = file:seek("end")
|
||||
file:close()
|
||||
if size == 0 then
|
||||
RESOLVFILE = "/tmp/resolv.conf.auto"
|
||||
end
|
||||
end
|
||||
api.uci_set("dhcp", "@dnsmasq[0]", "resolvfile", RESOLVFILE)
|
||||
api.uci_save(nil, "dhcp", true)
|
||||
end
|
||||
end
|
||||
|
||||
function restart(var)
|
||||
local LOG = var["-LOG"]
|
||||
sys.call("/etc/init.d/dnsmasq restart >/dev/null 2>&1")
|
||||
if LOG == "1" then
|
||||
api.log("重启 dnsmasq 服务")
|
||||
end
|
||||
end
|
||||
|
||||
function logic_restart(var)
|
||||
local LOG = var["-LOG"]
|
||||
local DEFAULT_DNS = api.get_cache_var("DEFAULT_DNS")
|
||||
if DEFAULT_DNS then
|
||||
backup_servers()
|
||||
--sys.call("sed -i '/list server/d' /etc/config/dhcp >/dev/null 2>&1")
|
||||
local dns_table = {}
|
||||
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
|
||||
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()
|
||||
else
|
||||
sys.call("/etc/init.d/dnsmasq restart >/dev/null 2>&1")
|
||||
end
|
||||
if LOG == "1" then
|
||||
api.log("重启 dnsmasq 服务")
|
||||
end
|
||||
end
|
||||
|
||||
function copy_instance(var)
|
||||
local LISTEN_PORT = var["-LISTEN_PORT"]
|
||||
local TMP_DNSMASQ_PATH = var["-TMP_DNSMASQ_PATH"]
|
||||
local conf_lines = {}
|
||||
local DEFAULT_DNSMASQ_CFGID = sys.exec("echo -n $(uci -q show dhcp.@dnsmasq[0] | awk 'NR==1 {split($0, conf, /[.=]/); print conf[2]}')")
|
||||
local conf_file = "/var/etc/dnsmasq.conf." .. DEFAULT_DNSMASQ_CFGID
|
||||
|
||||
local retry = 5
|
||||
while not fs.access(conf_file) and retry > 0 do
|
||||
api.nixio.nanosleep(1, 0)
|
||||
retry = retry - 1
|
||||
end
|
||||
|
||||
if fs.access(conf_file) then
|
||||
for line in io.lines(conf_file) do
|
||||
local filter
|
||||
if line:find("passwall") then filter = true end
|
||||
if line:find("ubus") then filter = true end
|
||||
if line:find("dhcp") then filter = true end
|
||||
if line:find("server=") == 1 then filter = true end
|
||||
if line:find("port=") == 1 then filter = true end
|
||||
if line:find("min%-cache%-ttl") == 1 then filter = true end
|
||||
if line:find("conf%-dir=") == 1 then
|
||||
filter = true
|
||||
if TMP_DNSMASQ_PATH then
|
||||
local tmp_path = line:sub(1 + #"conf-dir=")
|
||||
sys.call(string.format("cp -r %s/* %s/ 2>/dev/null", tmp_path, TMP_DNSMASQ_PATH))
|
||||
end
|
||||
end
|
||||
if line:find("address=") == 1 or (line:find("server=") == 1 and line:find("/")) then filter = nil end
|
||||
if not filter then
|
||||
tinsert(conf_lines, line)
|
||||
end
|
||||
end
|
||||
else
|
||||
sys.call("logger -t passwall 'ERROR: dnsmasq config " .. conf_file .. " not found after 5s wait! DNS hijacking will fail.'")
|
||||
end
|
||||
|
||||
tinsert(conf_lines, "port=" .. LISTEN_PORT)
|
||||
if TMP_DNSMASQ_PATH then
|
||||
sys.call("rm -rf " .. TMP_DNSMASQ_PATH .. "/*passwall*")
|
||||
end
|
||||
if var["-return"] == "1" then
|
||||
return conf_lines
|
||||
end
|
||||
if #conf_lines > 0 then
|
||||
local DNSMASQ_CONF = var["-DNSMASQ_CONF"]
|
||||
local conf_out = io.open(DNSMASQ_CONF, "a")
|
||||
conf_out:write(table.concat(conf_lines, "\n"))
|
||||
conf_out:write("\n")
|
||||
conf_out:close()
|
||||
end
|
||||
end
|
||||
|
||||
function add_rule(var)
|
||||
local FLAG = var["-FLAG"]
|
||||
local TMP_DNSMASQ_PATH = var["-TMP_DNSMASQ_PATH"]
|
||||
local DNSMASQ_CONF_FILE = var["-DNSMASQ_CONF_FILE"]
|
||||
local LISTEN_PORT = var["-LISTEN_PORT"]
|
||||
local DEFAULT_DNS = var["-DEFAULT_DNS"]
|
||||
local LOCAL_DNS = var["-LOCAL_DNS"]
|
||||
local TUN_DNS = var["-TUN_DNS"]
|
||||
local USE_DEFAULT_DNS = var["-USE_DEFAULT_DNS"]
|
||||
local CHINADNS_DNS = var["-CHINADNS_DNS"]
|
||||
local NODE = var["-NODE"]
|
||||
local USE_DIRECT_LIST = var["-USE_DIRECT_LIST"]
|
||||
local USE_PROXY_LIST = var["-USE_PROXY_LIST"]
|
||||
local USE_BLOCK_LIST = var["-USE_BLOCK_LIST"]
|
||||
local USE_GFW_LIST = var["-USE_GFW_LIST"]
|
||||
local CHN_LIST = var["-CHN_LIST"]
|
||||
local DEFAULT_PROXY_MODE = var["-DEFAULT_PROXY_MODE"]
|
||||
local NO_PROXY_IPV6 = var["-NO_PROXY_IPV6"]
|
||||
local NO_LOGIC_LOG = var["-NO_LOGIC_LOG"]
|
||||
local NFTFLAG = var["-NFTFLAG"]
|
||||
local CACHE_PATH = api.CACHE_PATH
|
||||
local CACHE_FLAG = "dnsmasq_" .. FLAG
|
||||
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 = api.uci_get_c(NODE, "protocol") == "_shunt"
|
||||
local USE_GEOVIEW = api.uci_get_c("@global_rules[0]", "enable_geoview")
|
||||
|
||||
local list1 = {}
|
||||
local excluded_domain = {}
|
||||
|
||||
if not api.is_finded("geoview") then
|
||||
USE_GEOVIEW = "0"
|
||||
end
|
||||
|
||||
local function log(...)
|
||||
if NO_LOGIC_LOG == "1" then
|
||||
return
|
||||
end
|
||||
api.log(...)
|
||||
end
|
||||
|
||||
local function check_dns(domain, dns)
|
||||
if domain == "" or domain:find("#") then
|
||||
return false
|
||||
end
|
||||
if not dns then
|
||||
return
|
||||
end
|
||||
for k,v in ipairs(list1[domain].dns) do
|
||||
if dns == v then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function check_ipset(domain, ipset)
|
||||
if domain == "" or domain:find("#") then
|
||||
return false
|
||||
end
|
||||
if not ipset then
|
||||
return
|
||||
end
|
||||
for k,v in ipairs(list1[domain].ipsets) do
|
||||
if ipset == v then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function set_domain_address(domain, address)
|
||||
if domain == "" or domain:find("#") then
|
||||
return
|
||||
end
|
||||
if not list1[domain] then
|
||||
list1[domain] = {
|
||||
dns = {},
|
||||
ipsets = {}
|
||||
}
|
||||
end
|
||||
if not list1[domain].address then
|
||||
list1[domain].address = address
|
||||
end
|
||||
end
|
||||
|
||||
local function set_domain_dns(domain, dns)
|
||||
if domain == "" or domain:find("#") then
|
||||
return
|
||||
end
|
||||
if not dns then
|
||||
return
|
||||
end
|
||||
if not list1[domain] then
|
||||
list1[domain] = {
|
||||
dns = {},
|
||||
ipsets = {}
|
||||
}
|
||||
end
|
||||
for line in string.gmatch(dns, '[^' .. "," .. ']+') do
|
||||
if not check_dns(domain, line) then
|
||||
table.insert(list1[domain].dns, line)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function set_domain_ipset(domain, ipset)
|
||||
if domain == "" or domain:find("#") then
|
||||
return
|
||||
end
|
||||
if not ipset then
|
||||
return
|
||||
end
|
||||
if not list1[domain] then
|
||||
list1[domain] = {
|
||||
dns = {},
|
||||
ipsets = {}
|
||||
}
|
||||
end
|
||||
for line in string.gmatch(ipset, '[^' .. "," .. ']+') do
|
||||
if not check_ipset(domain, line) then
|
||||
table.insert(list1[domain].ipsets, line)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function add_excluded_domain(domain)
|
||||
if domain == "" or domain:find("#") then
|
||||
return
|
||||
end
|
||||
excluded_domain[domain] = true
|
||||
end
|
||||
|
||||
local function check_excluded_domain(domain)
|
||||
if domain == "" or domain:find("#") then
|
||||
return false
|
||||
end
|
||||
if excluded_domain[domain] then
|
||||
return true
|
||||
end
|
||||
local pos = domain:find(".", 1, true)
|
||||
while pos do
|
||||
if excluded_domain[domain:sub(pos + 1)] then
|
||||
return true
|
||||
end
|
||||
pos = domain:find(".", pos + 1, true)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function foreach_geosite(list_arg, callback)
|
||||
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")
|
||||
if not (bin and list_arg) then return end
|
||||
local cmd = string.format("%q -type geosite -action extract -input %q -list %q -lowmem=true", bin, geosite_path, list_arg)
|
||||
local pipe = io.popen(cmd)
|
||||
if not pipe then return end
|
||||
for line in pipe:lines() do
|
||||
if line ~= "" then callback(line) end
|
||||
end
|
||||
pipe:close()
|
||||
end
|
||||
|
||||
local cache_text = ""
|
||||
local nodes_address_md5 = sys.exec("echo -n $(uci show passwall | grep '\\.address') | md5sum")
|
||||
local new_rules = sys.exec("echo -n $(find /usr/share/passwall/rules -type f | xargs md5sum)")
|
||||
local new_text = TMP_DNSMASQ_PATH .. DNSMASQ_CONF_FILE .. DEFAULT_DNS .. LOCAL_DNS .. TUN_DNS .. USE_DEFAULT_DNS .. CHINADNS_DNS .. USE_DIRECT_LIST .. USE_PROXY_LIST .. USE_BLOCK_LIST .. USE_GFW_LIST .. CHN_LIST .. DEFAULT_PROXY_MODE .. NO_PROXY_IPV6 .. nodes_address_md5 .. new_rules .. NFTFLAG
|
||||
if fs.access(CACHE_TEXT_FILE) then
|
||||
for line in io.lines(CACHE_TEXT_FILE) do
|
||||
cache_text = line
|
||||
end
|
||||
end
|
||||
|
||||
if cache_text ~= new_text then
|
||||
api.remove(CACHE_DNS_PATH .. "*")
|
||||
end
|
||||
|
||||
local dnsmasq_default_dns
|
||||
if USE_DEFAULT_DNS ~= "nil" then
|
||||
if USE_DEFAULT_DNS == "direct" then
|
||||
dnsmasq_default_dns = LOCAL_DNS
|
||||
end
|
||||
if USE_DEFAULT_DNS == "remote" then
|
||||
dnsmasq_default_dns = TUN_DNS
|
||||
end
|
||||
if USE_DEFAULT_DNS == "remote" and CHN_LIST == "direct" then
|
||||
dnsmasq_default_dns = TUN_DNS
|
||||
end
|
||||
end
|
||||
|
||||
local only_global
|
||||
if DEFAULT_PROXY_MODE == "proxy" and CHN_LIST == "0" and USE_GFW_LIST == "0" then
|
||||
--没有启用中国列表和GFW列表时
|
||||
dnsmasq_default_dns = TUN_DNS
|
||||
only_global = 1
|
||||
end
|
||||
if USE_DEFAULT_DNS == "chinadns_ng" and CHINADNS_DNS ~= "0" then
|
||||
dnsmasq_default_dns = CHINADNS_DNS
|
||||
USE_CHINADNS_NG = "1"
|
||||
end
|
||||
|
||||
local setflag_4= (NFTFLAG == "1") and "4#inet#passwall#" or ""
|
||||
local setflag_6= (NFTFLAG == "1") and "6#inet#passwall#" or ""
|
||||
|
||||
if not fs.access(CACHE_DNS_PATH) then
|
||||
fs.mkdir(CACHE_DNS_PATH)
|
||||
|
||||
--屏蔽列表
|
||||
if USE_CHINADNS_NG == "0" and USE_BLOCK_LIST == "1" then
|
||||
local geosite_arg = ""
|
||||
local f = io.open("/usr/share/passwall/rules/block_host")
|
||||
if f then
|
||||
for line in f:lines() do
|
||||
if not line:find("#") and line:find("geosite:") then
|
||||
line = string.match(line, ":([^:]+)$")
|
||||
geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line
|
||||
else
|
||||
line = api.get_std_domain(line)
|
||||
if line ~= "" and not line:find("#") and not line:find(":") then
|
||||
set_domain_address(line, "")
|
||||
end
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
end
|
||||
if USE_GEOVIEW == "1" and geosite_arg ~= "" then
|
||||
foreach_geosite(geosite_arg, function(line)
|
||||
set_domain_address(line, "")
|
||||
end)
|
||||
log(" - 解析[屏蔽列表] Geosite 到屏蔽域名表(blocklist)完成")
|
||||
end
|
||||
end
|
||||
|
||||
local fwd_dns
|
||||
local no_ipv6
|
||||
|
||||
--始终用国内DNS解析节点域名
|
||||
if true then
|
||||
fwd_dns = LOCAL_DNS
|
||||
if USE_CHINADNS_NG == "1" then
|
||||
fwd_dns = nil
|
||||
else
|
||||
local sets = {
|
||||
setflag_4 .. "psw_vps",
|
||||
setflag_6 .. "psw_vps6"
|
||||
}
|
||||
local function process_address(address)
|
||||
address = (address or ""):lower()
|
||||
if api.vps_domain_exclude(address) then return end
|
||||
if datatypes.hostname(address) then
|
||||
set_domain_dns(address, fwd_dns)
|
||||
set_domain_ipset(address, table.concat(sets, ","))
|
||||
end
|
||||
end
|
||||
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 "")
|
||||
if dns and dns ~= "" then
|
||||
process_address(dns)
|
||||
end
|
||||
end)
|
||||
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)
|
||||
end
|
||||
end)
|
||||
log(string.format(" - 节点列表中的域名(vpslist):%s", fwd_dns or "默认"))
|
||||
end
|
||||
end
|
||||
|
||||
--直连(白名单)列表
|
||||
if USE_DIRECT_LIST == "1" then
|
||||
fwd_dns = LOCAL_DNS
|
||||
if USE_CHINADNS_NG == "1" then
|
||||
fwd_dns = nil
|
||||
end
|
||||
if fwd_dns then
|
||||
local sets = {
|
||||
setflag_4 .. "psw_white",
|
||||
setflag_6 .. "psw_white6"
|
||||
}
|
||||
--始终用国内DNS解析直连(白名单)列表
|
||||
local geosite_arg = ""
|
||||
local f = io.open("/usr/share/passwall/rules/direct_host")
|
||||
if f then
|
||||
for line in f:lines() do
|
||||
if not line:find("#") and line:find("geosite:") then
|
||||
line = string.match(line, ":([^:]+)$")
|
||||
geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line
|
||||
else
|
||||
line = api.get_std_domain(line)
|
||||
if line ~= "" and not line:find("#") and not line:find(":") then
|
||||
add_excluded_domain(line)
|
||||
set_domain_dns(line, fwd_dns)
|
||||
set_domain_ipset(line, table.concat(sets, ","))
|
||||
end
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
log(string.format(" - 域名白名单(whitelist):%s", fwd_dns or "默认"))
|
||||
end
|
||||
if USE_GEOVIEW == "1" and geosite_arg ~= "" then
|
||||
foreach_geosite(geosite_arg, function(line)
|
||||
add_excluded_domain(line)
|
||||
set_domain_dns(line, fwd_dns)
|
||||
set_domain_ipset(line, table.concat(sets, ","))
|
||||
end)
|
||||
log(" - 解析[直连列表] Geosite 到域名白名单(whitelist)完成")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--代理(黑名单)列表
|
||||
if USE_PROXY_LIST == "1" then
|
||||
fwd_dns = TUN_DNS
|
||||
if USE_CHINADNS_NG == "1" then
|
||||
fwd_dns = nil
|
||||
end
|
||||
if fwd_dns then
|
||||
local set_name = "psw_black"
|
||||
local set6_name = "psw_black6"
|
||||
if FLAG ~= "default" then
|
||||
set_name = "psw_" .. FLAG .. "_black"
|
||||
set6_name = "psw_" .. FLAG .. "_black6"
|
||||
end
|
||||
local sets = {
|
||||
setflag_4 .. set_name
|
||||
}
|
||||
if NO_PROXY_IPV6 ~= "1" then
|
||||
table.insert(sets, setflag_6 .. set6_name)
|
||||
end
|
||||
--始终使用远程DNS解析代理(黑名单)列表
|
||||
local geosite_arg = ""
|
||||
local f = io.open("/usr/share/passwall/rules/proxy_host")
|
||||
if f then
|
||||
for line in f:lines() do
|
||||
if not line:find("#") and line:find("geosite:") then
|
||||
line = string.match(line, ":([^:]+)$")
|
||||
geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line
|
||||
else
|
||||
line = api.get_std_domain(line)
|
||||
if line ~= "" and not line:find("#") and not line:find(":") then
|
||||
add_excluded_domain(line)
|
||||
if NO_PROXY_IPV6 == "1" then
|
||||
set_domain_address(line, "::")
|
||||
end
|
||||
set_domain_dns(line, fwd_dns)
|
||||
set_domain_ipset(line, table.concat(sets, ","))
|
||||
end
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
log(string.format(" - 代理域名表(blacklist):%s", fwd_dns or "默认"))
|
||||
end
|
||||
if USE_GEOVIEW == "1" and geosite_arg ~= "" then
|
||||
foreach_geosite(geosite_arg, function(line)
|
||||
add_excluded_domain(line)
|
||||
if NO_PROXY_IPV6 == "1" then
|
||||
set_domain_address(line, "::")
|
||||
end
|
||||
set_domain_dns(line, fwd_dns)
|
||||
set_domain_ipset(line, table.concat(sets, ","))
|
||||
end)
|
||||
log(" - 解析[代理列表] Geosite 到代理域名表(blacklist)完成")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--GFW列表
|
||||
if USE_GFW_LIST == "1" then
|
||||
fwd_dns = TUN_DNS
|
||||
if USE_CHINADNS_NG == "1" then
|
||||
fwd_dns = nil
|
||||
end
|
||||
if fwd_dns then
|
||||
local set_name = "psw_gfw"
|
||||
local set6_name = "psw_gfw6"
|
||||
if FLAG ~= "default" then
|
||||
set_name = "psw_" .. FLAG .. "_gfw"
|
||||
set6_name = "psw_" .. FLAG .. "_gfw6"
|
||||
end
|
||||
local sets = {
|
||||
setflag_4 .. set_name
|
||||
}
|
||||
if NO_PROXY_IPV6 ~= "1" then
|
||||
table.insert(sets, setflag_6 .. set6_name)
|
||||
end
|
||||
local f = io.open("/usr/share/passwall/rules/gfwlist")
|
||||
if f then
|
||||
for line in f:lines() do
|
||||
if line ~= "" and not line:find("#") and not check_excluded_domain(line) then
|
||||
if NO_PROXY_IPV6 == "1" then
|
||||
set_domain_address(line, "::")
|
||||
end
|
||||
if dnsmasq_default_dns == fwd_dns then
|
||||
fwd_dns = nil
|
||||
else
|
||||
set_domain_dns(line, fwd_dns)
|
||||
end
|
||||
set_domain_ipset(line, table.concat(sets, ","))
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
log(string.format(" - 防火墙域名表(gfwlist):%s", fwd_dns or "默认"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--中国列表
|
||||
if CHN_LIST ~= "0" then
|
||||
fwd_dns = nil
|
||||
if CHN_LIST == "direct" then
|
||||
fwd_dns = LOCAL_DNS
|
||||
end
|
||||
if CHN_LIST == "proxy" then
|
||||
fwd_dns = TUN_DNS
|
||||
end
|
||||
if USE_CHINADNS_NG == "1" then
|
||||
fwd_dns = nil
|
||||
end
|
||||
if fwd_dns then
|
||||
local sets = {
|
||||
setflag_4 .. "psw_chn",
|
||||
setflag_6 .. "psw_chn6"
|
||||
}
|
||||
if CHN_LIST == "proxy" then
|
||||
if NO_PROXY_IPV6 == "1" then
|
||||
sets = {
|
||||
setflag_4 .. "psw_chn"
|
||||
}
|
||||
end
|
||||
end
|
||||
local f = io.open("/usr/share/passwall/rules/chnlist")
|
||||
if f then
|
||||
for line in f:lines() do
|
||||
if line ~= "" and not line:find("#") and not check_excluded_domain(line) then
|
||||
if CHN_LIST == "proxy" and NO_PROXY_IPV6 == "1" then
|
||||
set_domain_address(line, "::")
|
||||
end
|
||||
if dnsmasq_default_dns == fwd_dns then
|
||||
fwd_dns = nil
|
||||
else
|
||||
set_domain_dns(line, fwd_dns)
|
||||
end
|
||||
set_domain_ipset(line, table.concat(sets, ","))
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
log(string.format(" - 中国域名表(chnroute):%s", fwd_dns or "默认"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--分流规则
|
||||
if IS_SHUNT_NODE and USE_CHINADNS_NG == "0" then
|
||||
local t = api.uci_get_c(NODE)
|
||||
local default_node_id = t["default_node"] or "_direct"
|
||||
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
|
||||
_node_id = default_node_id
|
||||
end
|
||||
|
||||
fwd_dns = nil
|
||||
no_ipv6 = nil
|
||||
|
||||
local sets = {}
|
||||
|
||||
if _node_id == "_direct" then
|
||||
fwd_dns = LOCAL_DNS
|
||||
if USE_DIRECT_LIST == "1" then
|
||||
table.insert(sets, setflag_4 .. "psw_white")
|
||||
table.insert(sets, setflag_6 .. "psw_white6")
|
||||
else
|
||||
local set_name = "psw_shunt"
|
||||
local set6_name = "psw_shunt6"
|
||||
if FLAG ~= "default" then
|
||||
set_name = "psw_" .. FLAG .. "_shunt"
|
||||
set6_name = "psw_" .. FLAG .. "_shunt6"
|
||||
end
|
||||
table.insert(sets, setflag_4 .. set_name)
|
||||
table.insert(sets, setflag_6 .. set6_name)
|
||||
end
|
||||
else
|
||||
local set_name = "psw_shunt"
|
||||
local set6_name = "psw_shunt6"
|
||||
if FLAG ~= "default" then
|
||||
set_name = "psw_" .. FLAG .. "_shunt"
|
||||
set6_name = "psw_" .. FLAG .. "_shunt6"
|
||||
end
|
||||
fwd_dns = TUN_DNS
|
||||
table.insert(sets, setflag_4 .. set_name)
|
||||
if NO_PROXY_IPV6 ~= "1" then
|
||||
table.insert(sets, setflag_6 .. set6_name)
|
||||
else
|
||||
no_ipv6 = true
|
||||
end
|
||||
end
|
||||
|
||||
local domain_list = s.domain_list or ""
|
||||
local geosite_arg = ""
|
||||
for line in string.gmatch(domain_list, "[^\r\n]+") do
|
||||
if line ~= "" and not line:find("#") and not line:find("regexp:") and not line:find("ext:") and not line:find("rule-set:") and not line:find("rs:") then
|
||||
if line:find("geosite:") then
|
||||
line = string.match(line, ":([^:]+)$")
|
||||
geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line
|
||||
else
|
||||
if line:find("domain:") or line:find("full:") then
|
||||
line = string.match(line, ":([^:]+)$")
|
||||
end
|
||||
line = api.get_std_domain(line)
|
||||
add_excluded_domain(line)
|
||||
|
||||
if no_ipv6 then
|
||||
set_domain_address(line, "::")
|
||||
end
|
||||
set_domain_dns(line, fwd_dns)
|
||||
set_domain_ipset(line, table.concat(sets, ","))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if USE_GFW_LIST == "1" and CHN_LIST == "0" and USE_GEOVIEW == "1" and geosite_arg ~= "" then --仅GFW模式解析geosite
|
||||
foreach_geosite(geosite_arg, function(line)
|
||||
add_excluded_domain(line)
|
||||
if no_ipv6 then
|
||||
set_domain_address(line, "::")
|
||||
end
|
||||
set_domain_dns(line, fwd_dns)
|
||||
set_domain_ipset(line, table.concat(sets, ","))
|
||||
end)
|
||||
log(string.format(" - 解析分流规则(%s) Geosite 完成", s.remarks))
|
||||
end
|
||||
|
||||
if _node_id ~= "_direct" then
|
||||
log(string.format(" - Sing-Box/Xray分流规则(%s):%s", s.remarks, fwd_dns or "默认"))
|
||||
end
|
||||
end
|
||||
end)
|
||||
elseif only_global == 1 and NO_PROXY_IPV6 == "1" then
|
||||
--节点:固定节点
|
||||
--代理模式:全局模式
|
||||
--过滤代理域名 IPv6:启用
|
||||
--禁止解析所有IPv6记录
|
||||
list1["#"] = {
|
||||
dns = {},
|
||||
ipsets = {},
|
||||
address = "::"
|
||||
}
|
||||
end
|
||||
|
||||
if list1 and next(list1) then
|
||||
local address_out = io.open(CACHE_DNS_PATH .. "/000-address.conf", "a")
|
||||
local server_out = io.open(CACHE_DNS_PATH .. "/001-server.conf", "a")
|
||||
local ipset_out = io.open(CACHE_DNS_PATH .. "/ipset.conf", "a")
|
||||
local set_name = "ipset"
|
||||
if NFTFLAG == "1" then
|
||||
set_name = "nftset"
|
||||
end
|
||||
for key, value in pairs(list1) do
|
||||
if value.address then
|
||||
local domain = "." .. key
|
||||
if key == "#" then
|
||||
domain = key
|
||||
end
|
||||
address_out:write(string.format("address=/%s/%s", domain, value.address) .. "\n")
|
||||
end
|
||||
if value.dns and #value.dns > 0 then
|
||||
for i, dns in ipairs(value.dns) do
|
||||
server_out:write(string.format("server=/.%s/%s", key, dns) .. "\n")
|
||||
end
|
||||
end
|
||||
if value.ipsets and #value.ipsets > 0 then
|
||||
local ipsets_str = ""
|
||||
for i, ipset in ipairs(value.ipsets) do
|
||||
ipsets_str = ipsets_str .. ipset .. ","
|
||||
end
|
||||
ipsets_str = ipsets_str:sub(1, #ipsets_str - 1)
|
||||
ipset_out:write(string.format("%s=/.%s/%s", set_name, key, ipsets_str) .. "\n")
|
||||
end
|
||||
end
|
||||
address_out:close()
|
||||
server_out:close()
|
||||
ipset_out:close()
|
||||
end
|
||||
|
||||
local f_out = io.open(CACHE_TEXT_FILE, "a")
|
||||
f_out:write(new_text)
|
||||
f_out:close()
|
||||
end
|
||||
|
||||
if USE_CHINADNS_NG == "0" then
|
||||
api.remove(TMP_DNSMASQ_PATH)
|
||||
fs.symlink(CACHE_DNS_PATH, TMP_DNSMASQ_PATH)
|
||||
end
|
||||
|
||||
if DNSMASQ_CONF_FILE ~= "nil" then
|
||||
local conf_lines = {}
|
||||
if LISTEN_PORT then
|
||||
--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 = api.CACHE_PATH .. "/dhcp-hosts"
|
||||
sys.call("touch " .. hosts)
|
||||
tinsert(conf_lines, "addn-hosts=" .. hosts)
|
||||
else
|
||||
--Modify the default dnsmasq service
|
||||
end
|
||||
if USE_CHINADNS_NG == "0" then
|
||||
tinsert(conf_lines, string.format("conf-dir=%s", TMP_DNSMASQ_PATH))
|
||||
end
|
||||
if dnsmasq_default_dns then
|
||||
for s in string.gmatch(dnsmasq_default_dns, '[^' .. "," .. ']+') do
|
||||
tinsert(conf_lines, string.format("server=%s", s))
|
||||
end
|
||||
tinsert(conf_lines, "all-servers")
|
||||
tinsert(conf_lines, "no-poll")
|
||||
tinsert(conf_lines, "no-resolv")
|
||||
if USE_CHINADNS_NG == "0" then
|
||||
log(string.format(" - 默认:%s", dnsmasq_default_dns))
|
||||
end
|
||||
|
||||
if FLAG == "default" then
|
||||
api.set_cache_var("DEFAULT_DNS", DEFAULT_DNS)
|
||||
end
|
||||
end
|
||||
if #conf_lines > 0 then
|
||||
local conf_out = io.open(DNSMASQ_CONF_FILE, "a")
|
||||
if conf_out then
|
||||
conf_out:write(table.concat(conf_lines, "\n"))
|
||||
conf_out:write("\n")
|
||||
conf_out:close()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if USE_CHINADNS_NG == "0" then
|
||||
log(" - PassWall必须依赖于Dnsmasq,如果你自行配置了错误的DNS流程,将会导致域名(直连/代理域名)分流失效!!!")
|
||||
end
|
||||
end
|
||||
|
||||
_G.stretch = stretch
|
||||
_G.restart = restart
|
||||
_G.logic_restart = logic_restart
|
||||
_G.copy_instance = copy_instance
|
||||
_G.add_rule = add_rule
|
||||
|
||||
if arg[1] then
|
||||
local func =_G[arg[1]]
|
||||
if func then
|
||||
func(api.get_function_args(arg))
|
||||
end
|
||||
end
|
||||
Loaded 100 of 4898 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user