🦄 Sync 2026-07-16 04:24:09

This commit is contained in:
github-actions[bot] committed 2026-07-16 04:24:09 +08:00
1 parent 58b9b357a3
commit ddc3d78236
70 files changed
+2113 -1956

No files matched your search

+3 -3
View File
@@ -5,12 +5,12 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=cloudreve PKG_NAME:=cloudreve
PKG_VERSION:=4.17.0 PKG_VERSION:=4.18.0
PKG_RELEASE:=13 PKG_RELEASE:=14
PKG_SOURCE_PROTO:=git PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/cloudreve/Cloudreve.git PKG_SOURCE_URL:=https://github.com/cloudreve/Cloudreve.git
PKG_SOURCE_VERSION:=d4a145c3ac68f08b9071dec0f2ac025c6dc7362d PKG_SOURCE_VERSION:=0bb0ab833571d380153edd3529e01a7957b8b4ce
PKG_MIRROR_HASH:=skip PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-3.0-only PKG_LICENSE:=GPL-3.0-only
@@ -461,25 +461,26 @@ function renderListeners(s, uciconfig, isClient) {
o.depends('type', 'tuic'); o.depends('type', 'tuic');
o.modalonly = true; o.modalonly = true;
/* Hysteria2 fields */ /* Brutal fields */
o = s.taboption('field_general', form.Value, 'hysteria_up_mbps', _('Max upload speed'), o = s.taboption('field_general', form.Value, 'brutal_up_mbps', _('Max upload speed'),
_('In Mbps.')); _('In Mbps.'));
o.datatype = 'uinteger'; o.datatype = 'uinteger';
o.depends('type', 'hysteria2'); o.depends({type: /^(hysteria2|shadowquic)$/});
o.modalonly = true; o.modalonly = true;
o = s.taboption('field_general', form.Value, 'hysteria_down_mbps', _('Max download speed'), o = s.taboption('field_general', form.Value, 'brutal_down_mbps', _('Max download speed'),
_('In Mbps.')); _('In Mbps.'));
o.datatype = 'uinteger'; o.datatype = 'uinteger';
o.depends('type', 'hysteria2'); o.depends({type: /^(hysteria2|shadowquic)$/});
o.modalonly = true; o.modalonly = true;
o = s.taboption('field_general', form.Flag, 'hysteria_ignore_client_bandwidth', _('Ignore client bandwidth'), o = s.taboption('field_general', form.Flag, 'brutal_ignore_client_bandwidth', _('Ignore client bandwidth'),
_('Tell the client to use the BBR flow control algorithm instead of Hysteria CC.')); _('Tell the client to use the BBR flow control algorithm instead of Hysteria CC.'));
o.default = o.disabled; o.default = o.disabled;
o.depends({type: 'hysteria2', hysteria_up_mbps: '', hysteria_down_mbps: ''}); o.depends({type: /^(hysteria2|shadowquic)$/, brutal_up_mbps: '', brutal_down_mbps: ''});
o.modalonly = true; o.modalonly = true;
/* Hysteria2 fields */
o = s.taboption('field_general', form.ListValue, 'hysteria_obfs_type', _('Obfuscate type')); o = s.taboption('field_general', form.ListValue, 'hysteria_obfs_type', _('Obfuscate type'));
o.value('', _('Disable')); o.value('', _('Disable'));
o.value('salamander', _('Salamander')); o.value('salamander', _('Salamander'));
@@ -638,6 +639,7 @@ function renderListeners(s, uciconfig, isClient) {
o = s.taboption('field_general', form.Flag, 'plugin', _('Plugin')); o = s.taboption('field_general', form.Flag, 'plugin', _('Plugin'));
o.default = o.disabled; o.default = o.disabled;
o.depends({type: /^(shadowsocks|snell)$/}); o.depends({type: /^(shadowsocks|snell)$/});
o.depends({type: /^(vmess|vless|trojan|anytls)$/});
o.modalonly = true; o.modalonly = true;
o = s.taboption('field_plugin', form.ListValue, 'plugin_type', _('Plugin type')); o = s.taboption('field_plugin', form.ListValue, 'plugin_type', _('Plugin type'));
@@ -651,9 +653,12 @@ function renderListeners(s, uciconfig, isClient) {
if (value) { if (value) {
if (type === 'snell' && !['obfs', 'shadow-tls'].includes(value)) { if (type === 'snell' && !['obfs', 'shadow-tls'].includes(value)) {
return _('Expecting: only support %s.').format(_('obfs-simple') + return _('Expecting: Only support %s.').format(_('obfs-simple') +
' / ' + _('ShadowTLS')); ' / ' + _('ShadowTLS'));
} }
if (['vmess', 'vless', 'trojan', 'anytls'].includes(type) && !['jls'].includes(value)) {
return _('Expecting: Only support %s.').format(_('JLS'));
}
} }
return true; return true;
@@ -1045,6 +1050,10 @@ function renderListeners(s, uciconfig, isClient) {
switch (type) { switch (type) {
case 'shadowsocks': case 'shadowsocks':
case 'vmess':
case 'vless':
case 'trojan':
case 'anytls':
def_alpn = ['h2', 'http/1.1']; // when plugin_type in ['jls'] def_alpn = ['h2', 'http/1.1']; // when plugin_type in ['jls']
break; break;
case 'tuic': case 'tuic':
@@ -1079,8 +1088,21 @@ function renderListeners(s, uciconfig, isClient) {
o = s.taboption('field_tls', form.Value, 'tls_cert_path', _('Certificate path'), o = s.taboption('field_tls', form.Value, 'tls_cert_path', _('Certificate path'),
_('The %s public key, in PEM format.').format(_('Server'))); _('The %s public key, in PEM format.').format(_('Server')));
o.value('/etc/fchomo/certs/server_publickey.pem'); o.value('/etc/fchomo/certs/server_publickey.pem');
o.depends({tls: '1', tls_reality: '0', type: /^(http|socks|mixed|vmess|vless|trojan|anytls|tuic|hysteria2|hysteria2-realm|trusttunnel)$/}); o.validate = function(section_id, value) {
o.rmempty = false; const plugin_type = this.section.getOption('plugin_type').formvalue(section_id);
const tls_reality = this.section.getOption('tls_reality').formvalue(section_id);
if (plugin_type === 'jls' || tls_reality == 1) {
if (value)
return _('Expecting: Keep empty when %s is enabled.').format(_('JLS') +
' / ' + _('REALITY'));
} else if (!value) {
return _('Expecting: Cannot be empty.');
}
return true;
}
o.depends({tls: '1', type: /^(http|socks|mixed|vmess|vless|trojan|anytls|tuic|hysteria2|hysteria2-realm|trusttunnel)$/});
o.modalonly = true; o.modalonly = true;
o = s.taboption('field_tls', form.Button, '_upload_cert', _('Upload certificate'), o = s.taboption('field_tls', form.Button, '_upload_cert', _('Upload certificate'),
@@ -1181,6 +1203,15 @@ function renderListeners(s, uciconfig, isClient) {
// uTLS fields // uTLS fields
o = s.taboption('field_tls', form.Flag, 'tls_reality', _('REALITY')); o = s.taboption('field_tls', form.Flag, 'tls_reality', _('REALITY'));
o.default = o.disabled; o.default = o.disabled;
o.validate = function(section_id, value) {
const plugin_type = this.section.getOption('plugin_type').formvalue(section_id);
value = this.formvalue(section_id);
if (value == 1 && plugin_type === 'jls')
return _('Expecting: Cannot be enabled when %s is enabled.').format(_('JLS'));
return true;
}
o.depends('tls', '1'); o.depends('tls', '1');
o.modalonly = true; o.modalonly = true;
@@ -1243,14 +1274,14 @@ function renderListeners(s, uciconfig, isClient) {
switch (type) { switch (type) {
case 'vless': case 'vless':
if (!['grpc', 'ws', 'xhttp'].includes(value)) if (!['grpc', 'ws', 'xhttp'].includes(value))
return _('Expecting: only support %s.').format(_('gRPC') + return _('Expecting: Only support %s.').format(_('gRPC') +
' / ' + _('WebSocket') + ' / ' + _('WebSocket') +
' / ' + _('XHTTP')); ' / ' + _('XHTTP'));
break; break;
case 'vmess': case 'vmess':
case 'trojan': case 'trojan':
if (!['grpc', 'ws'].includes(value)) if (!['grpc', 'ws'].includes(value))
return _('Expecting: only support %s.').format(_('gRPC') + return _('Expecting: Only support %s.').format(_('gRPC') +
' / ' + _('WebSocket')); ' / ' + _('WebSocket'));
break; break;
default: default:
@@ -667,6 +667,19 @@ return view.extend({
so.depends('type', 'tuic'); so.depends('type', 'tuic');
so.modalonly = true; so.modalonly = true;
/* Brutal fields */
so = ss.taboption('field_general', form.Value, 'brutal_up_mbps', _('Max upload speed'),
_('In Mbps.'));
so.datatype = 'uinteger';
so.depends({type: /^(hysteria|hysteria2|shadowquic)$/});
so.modalonly = true;
so = ss.taboption('field_general', form.Value, 'brutal_down_mbps', _('Max download speed'),
_('In Mbps.'));
so.datatype = 'uinteger';
so.depends({type: /^(hysteria|hysteria2|shadowquic)$/});
so.modalonly = true;
/* Hysteria / Hysteria2 fields */ /* Hysteria / Hysteria2 fields */
so = ss.taboption('field_general', form.DynamicList, 'hysteria_ports', _('Ports pool')); so = ss.taboption('field_general', form.DynamicList, 'hysteria_ports', _('Ports pool'));
so.datatype = 'or(port, portrange)'; so.datatype = 'or(port, portrange)';
@@ -679,18 +692,6 @@ return view.extend({
so.depends('type', 'hysteria2'); so.depends('type', 'hysteria2');
so.modalonly = true; so.modalonly = true;
so = ss.taboption('field_general', form.Value, 'hysteria_up_mbps', _('Max upload speed'),
_('In Mbps.'));
so.datatype = 'uinteger';
so.depends({type: /^(hysteria|hysteria2)$/});
so.modalonly = true;
so = ss.taboption('field_general', form.Value, 'hysteria_down_mbps', _('Max download speed'),
_('In Mbps.'));
so.datatype = 'uinteger';
so.depends({type: /^(hysteria|hysteria2)$/});
so.modalonly = true;
so = ss.taboption('field_general', form.ListValue, 'hysteria_obfs_type', _('Obfuscate type')); so = ss.taboption('field_general', form.ListValue, 'hysteria_obfs_type', _('Obfuscate type'));
so.value('', _('Disable')); so.value('', _('Disable'));
so.value('salamander', _('Salamander')); so.value('salamander', _('Salamander'));
@@ -716,17 +717,6 @@ return view.extend({
so.depends('hysteria_obfs_type', 'gecko'); so.depends('hysteria_obfs_type', 'gecko');
so.modalonly = true; so.modalonly = true;
/* TrustTunnel fields */
so = ss.taboption('field_general', form.Flag, 'trusttunnel_health_check', _('Health check'));
so.default = so.enabled;
so.depends('type', 'trusttunnel');
so.modalonly = true;
so = ss.taboption('field_general', form.Flag, 'trusttunnel_quic', _('QUIC'));
so.default = so.disabled;
so.depends('type', 'trusttunnel');
so.modalonly = true;
/* ShadowQUIC fields */ /* ShadowQUIC fields */
so = ss.taboption('field_general', form.DynamicList, 'shadowquic_quic_versions', _('QUIC versions'), so = ss.taboption('field_general', form.DynamicList, 'shadowquic_quic_versions', _('QUIC versions'),
_('Support %s, default %s.').format('v1/v2', 'v1')); _('Support %s, default %s.').format('v1/v2', 'v1'));
@@ -752,6 +742,17 @@ return view.extend({
so.depends('type', 'shadowquic'); so.depends('type', 'shadowquic');
so.modalonly = true; so.modalonly = true;
/* TrustTunnel fields */
so = ss.taboption('field_general', form.Flag, 'trusttunnel_health_check', _('Health check'));
so.default = so.enabled;
so.depends('type', 'trusttunnel');
so.modalonly = true;
so = ss.taboption('field_general', form.Flag, 'trusttunnel_quic', _('QUIC'));
so.default = so.disabled;
so.depends('type', 'trusttunnel');
so.modalonly = true;
/* WireGuard fields */ /* WireGuard fields */
so = ss.taboption('field_general', form.Value, 'wireguard_ip', _('Local address'), so = ss.taboption('field_general', form.Value, 'wireguard_ip', _('Local address'),
_('The %s address used by local machine in the Wireguard network.').format('IPv4')); _('The %s address used by local machine in the Wireguard network.').format('IPv4'));
@@ -982,7 +983,7 @@ return view.extend({
if (value) { if (value) {
if (type === 'snell' && !['obfs', 'shadow-tls'].includes(value)) { if (type === 'snell' && !['obfs', 'shadow-tls'].includes(value)) {
return _('Expecting: only support %s.').format(_('obfs-simple') + return _('Expecting: Only support %s.').format(_('obfs-simple') +
' / ' + _('ShadowTLS')); ' / ' + _('ShadowTLS'));
} }
} }
@@ -1166,6 +1167,14 @@ return view.extend({
so = ss.taboption('field_tls', form.Value, 'tls_sni', _('TLS SNI'), so = ss.taboption('field_tls', form.Value, 'tls_sni', _('TLS SNI'),
_('Hostname that the client attempts to connect to at the start of the TLS handshake process.')); _('Hostname that the client attempts to connect to at the start of the TLS handshake process.'));
so.validate = function(section_id, value) {
const tls_jls = this.section.getOption('tls_jls').formvalue(section_id);
if (tls_jls == 1 && !value)
return _('Expecting: %s cannot be empty when %s is enabled.').format(_('TLS SNI'), _('JLS'));
return true;
};
so.depends({tls: '1', type: /^(http|vmess|vless|trojan|anytls|hysteria|hysteria2|shadowquic|trusttunnel|masque)$/}); so.depends({tls: '1', type: /^(http|vmess|vless|trojan|anytls|hysteria|hysteria2|shadowquic|trusttunnel|masque)$/});
so.depends({tls: '1', type: /^(tuic)$/, tls_disable_sni: '0'}); so.depends({tls: '1', type: /^(tuic)$/, tls_disable_sni: '0'});
so.modalonly = true; so.modalonly = true;
@@ -1300,9 +1309,24 @@ return view.extend({
so.depends({type: 'ss', plugin_type: 'jls'}); so.depends({type: 'ss', plugin_type: 'jls'});
so.modalonly = true; so.modalonly = true;
so = ss.taboption('field_tls', form.Flag, 'tls_jls', _('JLS'));
so.default = so.disabled;
so.depends({tls: '1', type: /^(vmess|vless|trojan|anytls)$/});
so.modalonly = true;
so = ss.taboption('field_tls', form.Value, 'tls_jls_username', _('JLS username'));
so.rmempty = false;
so.depends('tls_jls', '1');
so.modalonly = true;
so = ss.taboption('field_tls', form.Value, 'tls_jls_password', _('JLS password'));
so.rmempty = false;
so.depends('tls_jls', '1');
so.modalonly = true;
so = ss.taboption('field_tls', form.Flag, 'tls_reality', _('REALITY')); so = ss.taboption('field_tls', form.Flag, 'tls_reality', _('REALITY'));
so.default = so.disabled; so.default = so.disabled;
so.depends({tls: '1', type: /^(vmess|vless|trojan)$/}); so.depends({tls: '1', tls_jls: '0', type: /^(vmess|vless|trojan)$/});
so.modalonly = true; so.modalonly = true;
so = ss.taboption('field_tls', form.Value, 'tls_reality_public_key', _('REALITY public key')); so = ss.taboption('field_tls', form.Value, 'tls_reality_public_key', _('REALITY public key'));
@@ -1344,14 +1368,14 @@ return view.extend({
switch (type) { switch (type) {
case 'vmess': case 'vmess':
if (!['http', 'h2', 'grpc', 'ws'].includes(value)) if (!['http', 'h2', 'grpc', 'ws'].includes(value))
return _('Expecting: only support %s.').format(_('HTTP') + return _('Expecting: Only support %s.').format(_('HTTP') +
' / ' + _('HTTPUpgrade') + ' / ' + _('HTTPUpgrade') +
' / ' + _('gRPC') + ' / ' + _('gRPC') +
' / ' + _('WebSocket')); ' / ' + _('WebSocket'));
break; break;
case 'vless': case 'vless':
if (!['http', 'h2', 'grpc', 'ws', 'xhttp'].includes(value)) if (!['http', 'h2', 'grpc', 'ws', 'xhttp'].includes(value))
return _('Expecting: only support %s.').format(_('HTTP') + return _('Expecting: Only support %s.').format(_('HTTP') +
' / ' + _('HTTPUpgrade') + ' / ' + _('HTTPUpgrade') +
' / ' + _('gRPC') + ' / ' + _('gRPC') +
' / ' + _('WebSocket') + ' / ' + _('WebSocket') +
@@ -1359,7 +1383,7 @@ return view.extend({
break; break;
case 'trojan': case 'trojan':
if (!['grpc', 'ws'].includes(value)) if (!['grpc', 'ws'].includes(value))
return _('Expecting: only support %s.').format(_('gRPC') + return _('Expecting: Only support %s.').format(_('gRPC') +
' / ' + _('WebSocket')); ' / ' + _('WebSocket'));
break; break;
default: default:
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -25,6 +25,30 @@ migrate() {
uci_set "$CONF" "$1" max_idle_time "$tuic_max_idle_time" uci_set "$CONF" "$1" max_idle_time "$tuic_max_idle_time"
fi fi
# hysteria_up_mbps -> brutal_up_mbps
if isDefined "$1" hysteria_up_mbps; then
local hysteria_up_mbps
config_get hysteria_up_mbps "$1" hysteria_up_mbps ""
uci_remove "$CONF" "$1" hysteria_up_mbps
uci_set "$CONF" "$1" brutal_up_mbps "$hysteria_up_mbps"
fi
# hysteria_down_mbps -> brutal_down_mbps
if isDefined "$1" hysteria_down_mbps; then
local hysteria_down_mbps
config_get hysteria_down_mbps "$1" hysteria_down_mbps ""
uci_remove "$CONF" "$1" hysteria_down_mbps
uci_set "$CONF" "$1" brutal_down_mbps "$hysteria_down_mbps"
fi
# hysteria_ignore_client_bandwidth -> brutal_ignore_client_bandwidth
if isDefined "$1" hysteria_ignore_client_bandwidth; then
local hysteria_ignore_client_bandwidth
config_get hysteria_ignore_client_bandwidth "$1" hysteria_ignore_client_bandwidth ""
uci_remove "$CONF" "$1" hysteria_ignore_client_bandwidth
uci_set "$CONF" "$1" brutal_ignore_client_bandwidth "$hysteria_ignore_client_bandwidth"
fi
# plugin -> plugin_type # plugin -> plugin_type
if isDefined "$1" plugin; then if isDefined "$1" plugin; then
local plugin local plugin
@@ -59,6 +59,22 @@ migrate() {
uci_set "$CONF" "$1" max_open_streams "$tuic_max_open_streams" uci_set "$CONF" "$1" max_open_streams "$tuic_max_open_streams"
fi fi
# hysteria_up_mbps -> brutal_up_mbps
if isDefined "$1" hysteria_up_mbps; then
local hysteria_up_mbps
config_get hysteria_up_mbps "$1" hysteria_up_mbps ""
uci_remove "$CONF" "$1" hysteria_up_mbps
uci_set "$CONF" "$1" brutal_up_mbps "$hysteria_up_mbps"
fi
# hysteria_down_mbps -> brutal_down_mbps
if isDefined "$1" hysteria_down_mbps; then
local hysteria_down_mbps
config_get hysteria_down_mbps "$1" hysteria_down_mbps ""
uci_remove "$CONF" "$1" hysteria_down_mbps
uci_set "$CONF" "$1" brutal_down_mbps "$hysteria_down_mbps"
fi
# plugin -> plugin_type # plugin -> plugin_type
if isDefined "$1" plugin; then if isDefined "$1" plugin; then
local plugin local plugin
@@ -33,6 +33,30 @@ migrate() {
uci_set "$CONF" "$1" max_idle_time "$tuic_max_idle_time" uci_set "$CONF" "$1" max_idle_time "$tuic_max_idle_time"
fi fi
# hysteria_up_mbps -> brutal_up_mbps
if isDefined "$1" hysteria_up_mbps; then
local hysteria_up_mbps
config_get hysteria_up_mbps "$1" hysteria_up_mbps ""
uci_remove "$CONF" "$1" hysteria_up_mbps
uci_set "$CONF" "$1" brutal_up_mbps "$hysteria_up_mbps"
fi
# hysteria_down_mbps -> brutal_down_mbps
if isDefined "$1" hysteria_down_mbps; then
local hysteria_down_mbps
config_get hysteria_down_mbps "$1" hysteria_down_mbps ""
uci_remove "$CONF" "$1" hysteria_down_mbps
uci_set "$CONF" "$1" brutal_down_mbps "$hysteria_down_mbps"
fi
# hysteria_ignore_client_bandwidth -> brutal_ignore_client_bandwidth
if isDefined "$1" hysteria_ignore_client_bandwidth; then
local hysteria_ignore_client_bandwidth
config_get hysteria_ignore_client_bandwidth "$1" hysteria_ignore_client_bandwidth ""
uci_remove "$CONF" "$1" hysteria_ignore_client_bandwidth
uci_set "$CONF" "$1" brutal_ignore_client_bandwidth "$hysteria_ignore_client_bandwidth"
fi
# plugin -> plugin_type # plugin -> plugin_type
if isDefined "$1" plugin; then if isDefined "$1" plugin; then
local plugin local plugin
@@ -573,11 +573,13 @@ uci.foreach(uciconf, ucinode, (cfg) => {
"reduce-rtt": strToBool(cfg.tuic_reduce_rtt), "reduce-rtt": strToBool(cfg.tuic_reduce_rtt),
"request-timeout": strToInt(cfg.tuic_request_timeout) || null, "request-timeout": strToInt(cfg.tuic_request_timeout) || null,
/* Brutal */
up: strToInt(cfg.brutal_up_mbps),
down: strToInt(cfg.brutal_down_mbps),
/* Hysteria / Hysteria2 */ /* Hysteria / Hysteria2 */
ports: isEmpty(cfg.hysteria_ports) ? null : join(',', cfg.hysteria_ports), ports: isEmpty(cfg.hysteria_ports) ? null : join(',', cfg.hysteria_ports),
"hop-interval": strToInt(cfg.hysteria_hop_interval), // @DEBUG ERROR data type *utils.IntRanges[uint16] "hop-interval": strToInt(cfg.hysteria_hop_interval), // @DEBUG ERROR data type *utils.IntRanges[uint16]
up: cfg.hysteria_up_mbps ? cfg.hysteria_up_mbps + ' Mbps' : null,
down: cfg.hysteria_down_mbps ? cfg.hysteria_down_mbps + ' Mbps' : null,
obfs: cfg.hysteria_obfs_type, obfs: cfg.hysteria_obfs_type,
"obfs-password": cfg.hysteria_obfs_password, "obfs-password": cfg.hysteria_obfs_password,
"obfs-min-packet-size": strToInt(cfg.hysteria_obfs_min_packet_size), "obfs-min-packet-size": strToInt(cfg.hysteria_obfs_min_packet_size),
@@ -682,6 +684,10 @@ uci.foreach(uciconf, ucinode, (cfg) => {
config: cfg.tls_ech_config, config: cfg.tls_ech_config,
"query-server-name": cfg.tls_ech_query_server_name "query-server-name": cfg.tls_ech_query_server_name
} : null, } : null,
"jls-opts": cfg.tls_jls === '1' ? {
"username": cfg.tls_jls_username,
"password": cfg.tls_jls_password
} : null,
"reality-opts": cfg.tls_reality === '1' ? { "reality-opts": cfg.tls_reality === '1' ? {
"public-key": cfg.tls_reality_public_key, "public-key": cfg.tls_reality_public_key,
"short-id": cfg.tls_reality_short_id, "short-id": cfg.tls_reality_short_id,
@@ -292,10 +292,12 @@ export function parseListener(cfg) {
"authentication-timeout": durationToSecond(cfg.tuic_authentication_timeout), "authentication-timeout": durationToSecond(cfg.tuic_authentication_timeout),
"max-udp-relay-packet-size": strToInt(cfg.tuic_max_udp_relay_packet_size), "max-udp-relay-packet-size": strToInt(cfg.tuic_max_udp_relay_packet_size),
/* Brutal */
up: strToInt(cfg.brutal_up_mbps),
down: strToInt(cfg.brutal_down_mbps),
"ignore-client-bandwidth": strToBool(cfg.brutal_ignore_client_bandwidth),
/* Hysteria2 */ /* Hysteria2 */
up: strToInt(cfg.hysteria_up_mbps),
down: strToInt(cfg.hysteria_down_mbps),
"ignore-client-bandwidth": strToBool(cfg.hysteria_ignore_client_bandwidth),
obfs: cfg.hysteria_obfs_type, obfs: cfg.hysteria_obfs_type,
"obfs-password": cfg.hysteria_obfs_password, "obfs-password": cfg.hysteria_obfs_password,
"obfs-min-packet-size": strToInt(cfg.hysteria_obfs_min_packet_size), "obfs-min-packet-size": strToInt(cfg.hysteria_obfs_min_packet_size),
@@ -417,7 +419,7 @@ export function parseListener(cfg) {
/* TLS fields */ /* TLS fields */
...(cfg.allow_insecure === '1' ? { "allow-insecure": true } : cfg.tls === '1' ? { ...(cfg.allow_insecure === '1' ? { "allow-insecure": true } : cfg.tls === '1' ? {
alpn: cfg.tls_alpn, alpn: cfg.plugin_type in ['jls'] ? null : cfg.tls_alpn,
...(cfg.tls_reality === '1' ? { ...(cfg.tls_reality === '1' ? {
"reality-config": { "reality-config": {
dest: cfg.tls_reality_dest, dest: cfg.tls_reality_dest,
+2 -2
View File
@@ -7,8 +7,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.7.1 PKG_VERSION:=26.7.15
PKG_RELEASE:=179 PKG_RELEASE:=180
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
@@ -441,24 +441,15 @@ o = s:option(Value, _n("tls_serverName"), "SNI " .. translate("Domain"))
o:depends({ [_n("tls")] = true }) o:depends({ [_n("tls")] = true })
o:depends({ [_n("protocol")] = "hysteria2" }) o:depends({ [_n("protocol")] = "hysteria2" })
if api.compare_versions(os.date("%Y.%m.%d"), "<", "3026.6.1") then o = s:option(Value, _n("tls_pinSHA256"), translate("TLS Chain Fingerprint (SHA256)"))
o = s:option(Flag, _n("tls_allowInsecure"), translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped.")) o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o.default = "0" o:depends({ [_n("protocol")] = "hysteria2" })
o:depends({ [_n("tls")] = true, [_n("reality")] = false }) o.description = translate("Once set, connects only when the server’s chain fingerprint matches.") ..
o:depends({ [_n("protocol")] = "hysteria2" }) string.format("<a href='javascript:void(0)' onclick='javascript:fetchCertSha256(this)'>%s</a>", "→ " .. translate("Fetch Manually"))
end
if api.compare_versions(xray_version, ">=", "26.1.31") then o = s:option(Value, _n("tls_CertByName"), translate("TLS Certificate Name (CertName)"), translate("TLS is used to verify the leaf certificate name."))
o = s:option(Value, _n("tls_pinSHA256"), translate("TLS Chain Fingerprint (SHA256)")) o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("tls")] = true, [_n("reality")] = false }) o:depends({ [_n("protocol")] = "hysteria2" })
o:depends({ [_n("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, _n("tls_CertByName"), translate("TLS Certificate Name (CertName)"), translate("TLS is used to verify the leaf certificate name."))
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria2" })
end
o = s:option(Flag, _n("tls_certificate"), translate("TLS Certificate (PEM)")) o = s:option(Flag, _n("tls_certificate"), translate("TLS Certificate (PEM)"))
o.default = "0" o.default = "0"
@@ -152,21 +152,9 @@ function gen_outbound(flag, node, tag, proxy_table)
security = node.stream_security, security = node.stream_security,
tlsSettings = (node.stream_security == "tls") and { tlsSettings = (node.stream_security == "tls") and {
serverName = node.tls_serverName, serverName = node.tls_serverName,
allowInsecure = (function()
if node.tls_pinSHA256 and node.tls_pinSHA256 ~= "" then return nil end
if node.tls_allowInsecure == "1" then return true end
end)(),
fingerprint = (node.type == "Xray" and node.utls == "1" and node.fingerprint and node.fingerprint ~= "") and node.fingerprint or nil, fingerprint = (node.type == "Xray" and node.utls == "1" and node.fingerprint and node.fingerprint ~= "") and node.fingerprint or nil,
pinnedPeerCertSha256 = (function() pinnedPeerCertSha256 = node.tls_pinSHA256,
if api.compare_versions(xray_version, "<", "26.1.31") then return nil end verifyPeerCertByName = node.tls_CertByName,
if not node.tls_pinSHA256 then return "" end
return node.tls_pinSHA256
end)(),
verifyPeerCertByName = (function()
if api.compare_versions(xray_version, "<", "26.1.31") then return nil end
if not node.tls_CertByName then return "" end
return node.tls_CertByName
end)(),
echConfigList = (node.ech == "1") and node.ech_config or nil, echConfigList = (node.ech == "1") and node.ech_config or nil,
certificates = (node.tls_certificate == "1" and node.tls_certificate_pem ~= "") and { certificates = (node.tls_certificate == "1" and node.tls_certificate_pem ~= "") and {
certificate = api.split(node.tls_certificate_pem, "\n"), certificate = api.split(node.tls_certificate_pem, "\n"),
@@ -820,7 +808,8 @@ function gen_config_server(node)
serverNames = node.reality_serverNames or {}, serverNames = node.reality_serverNames or {},
privateKey = node.reality_private_key, privateKey = node.reality_private_key,
shortIds = node.reality_shortId or "", shortIds = node.reality_shortId or "",
mldsa65Seed = (node.use_mldsa65Seed == "1") and node.reality_mldsa65Seed or nil mldsa65Seed = (node.use_mldsa65Seed == "1") and node.reality_mldsa65Seed or nil,
minClientVer = "1.0.0"
} or nil } or nil
end end
end end
@@ -1313,6 +1313,8 @@ local current_node = map:get(section)
opt.set(dom_prefix + 'utls', true); opt.set(dom_prefix + 'utls', true);
opt.set(dom_prefix + 'fingerprint', queryParam.fp); opt.set(dom_prefix + 'fingerprint', queryParam.fp);
} }
opt.set(dom_prefix + 'ech', !!queryParam.ech);
opt.set(dom_prefix + 'ech_config', queryParam.ech || '');
opt.set(dom_prefix + 'tls_pinSHA256', queryParam.pcs || ''); opt.set(dom_prefix + 'tls_pinSHA256', queryParam.pcs || '');
opt.set(dom_prefix + 'tls_CertByName', queryParam.vcn || ''); opt.set(dom_prefix + 'tls_CertByName', queryParam.vcn || '');
opt.set( opt.set(
@@ -1069,6 +1069,10 @@ local function processData(szType, content, add_mode, group, sub_cfg)
result.reality_publicKey = params.pbk or nil result.reality_publicKey = params.pbk or nil
result.reality_shortId = params.sid or nil result.reality_shortId = params.sid or nil
end end
if params.ech and params.ech ~= "" then
result.ech = "1"
result.ech_config = params.ech
end
result.tls_pinSHA256 = params.pcs result.tls_pinSHA256 = params.pcs
result.tls_CertByName = params.vcn result.tls_CertByName = params.vcn
result.tls_allowInsecure = params.allowinsecure or params.allowInsecure or params.insecure result.tls_allowInsecure = params.allowinsecure or params.allowInsecure or params.insecure
+2 -2
View File
@@ -6,8 +6,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2 PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.7.12 PKG_VERSION:=26.7.16
PKG_RELEASE:=62 PKG_RELEASE:=63
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
@@ -0,0 +1,38 @@
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['json'] = function() {
return is_json(this);
}
cbi_validators['timehhmm'] = function() {
return is_timehhmm(this);
}
} else {
L.require('validation').then(function(validation) {
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)'));
}
});
}
@@ -114,6 +114,8 @@ function index()
--[[geoview]] --[[geoview]]
entry({"admin", "services", appname, "geo_view"}, call("geo_view")).leaf = true entry({"admin", "services", appname, "geo_view"}, call("geo_view")).leaf = true
entry({"admin", "services", appname, "fetch_certsha256"}, call("fetch_certsha256")).leaf = true
end end
function check_site(host, port) function check_site(host, port)
@@ -995,3 +997,17 @@ function flush_set()
http.redirect(api.url("log")) http.redirect(api.url("log"))
end end
end end
function fetch_certsha256()
local id = http.formvalue("id") or ""
local address = (id ~= "") and uci:get(appname, id, "address") or ""
local port = (id ~= "") and uci:get(appname, id, "port") or 0
local sni = (id ~= "") and uci:get(appname, id, "tls_serverName") or ""
sni = (sni ~= "") and sni or address
if address == "" or port == 0 then
http_write_json_error()
return
end
local data = api.fetch_cert_sha256(address, port, sni, 5)
http_write_json(data ~= "" and { code = 1, data = data } or { code = 0 })
end
@@ -2,6 +2,8 @@ local api = require "luci.passwall2.api"
local appname = api.appname local appname = api.appname
local sys = api.sys local sys = api.sys
api.set_default_cbi()
m = Map(appname) m = Map(appname)
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -13,7 +15,8 @@ o.rmempty = false
o.default = false o.default = false
-- [[ ACLs Settings ]]-- -- [[ ACLs Settings ]]--
s = m:section(TypedSection, "acl_rule") local cfgname = "acl_rule"
s = m:section(TypedSection, cfgname)
s.template = "cbi/tblsection" s.template = "cbi/tblsection"
s.sortable = true s.sortable = true
s.anonymous = true s.anonymous = true
@@ -65,4 +68,9 @@ i.cfgvalue = function(t, n)
return v return v
end end
return m local sortable = Template(appname .. "/cbi/sortable")
sortable.api = api
sortable.target_cfgname = cfgname
m:append(sortable)
return api.return_map(m)
@@ -1,6 +1,8 @@
local api = require "luci.passwall2.api" local api = require "luci.passwall2.api"
local appname = api.appname local appname = api.appname
api.set_default_cbi()
m = Map(appname) m = Map(appname)
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -379,4 +381,4 @@ for k, v in pairs(nodes_table) do
end end
end end
return m return api.return_map(m)
@@ -2,6 +2,8 @@ local api = require "luci.passwall2.api"
local com = require "luci.passwall2.com" local com = require "luci.passwall2.com"
local appname = api.appname local appname = api.appname
api.set_default_cbi()
m = Map(appname) m = Map(appname)
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -28,4 +30,4 @@ 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.")) 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 end
return m return api.return_map(m)
@@ -1,5 +1,5 @@
local api = require "luci.passwall2.api" local api = require "luci.passwall2.api"
local appname = "passwall2" local appname = api.appname
local fs = api.fs local fs = api.fs
local uci = api.uci local uci = api.uci
@@ -4,6 +4,8 @@ datatypes = api.datatypes
has_singbox = api.finded_com("sing-box") has_singbox = api.finded_com("sing-box")
has_xray = api.finded_com("xray") has_xray = api.finded_com("xray")
api.set_default_cbi()
m = Map(appname) m = Map(appname)
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -387,4 +389,4 @@ footer.shunt_list = api.jsonc.stringify(shunt_list)
m:append(footer) m:append(footer)
m:append(Template(appname .. "/global/status_bottom")) m:append(Template(appname .. "/global/status_bottom"))
return m return api.return_map(m)
@@ -15,6 +15,8 @@ for k, e in ipairs(api.get_valid_nodes()) do
end end
end end
api.set_default_cbi()
m = Map(appname) m = Map(appname)
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -100,7 +102,8 @@ end
o:depends("health_check_type", "passwall_logic") o:depends("health_check_type", "passwall_logic")
-- [[ Balancing Settings ]]-- -- [[ Balancing Settings ]]--
s = m:section(TypedSection, "haproxy_config", translate("Node List")) local cfgname = "haproxy_config"
s = m:section(TypedSection, cfgname, translate("Node List"))
s.description = "<font color='red'>" .. 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("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>" 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>"
@@ -176,6 +179,11 @@ o:value(0, translate("Primary"))
o:value(1, translate("Standby")) o:value(1, translate("Standby"))
o.rmempty = false o.rmempty = false
local sortable = Template(appname .. "/cbi/sortable")
sortable.api = api
sortable.target_cfgname = cfgname
m:append(sortable)
m:append(Template(appname .. "/haproxy/js")) m:append(Template(appname .. "/haproxy/js"))
return m return api.return_map(m)
@@ -1,6 +1,8 @@
api = require "luci.passwall2.api" api = require "luci.passwall2.api"
appname = api.appname appname = api.appname
api.set_default_cbi()
m = Map(appname, translate("Node Config")) m = Map(appname, translate("Node Config"))
m.redirect = api.url() m.redirect = api.url()
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -94,7 +96,6 @@ local footer = Template(appname .. "/node_config/footer")
footer.api = api footer.api = api
footer.config = m.config footer.config = m.config
footer.section = arg[1] footer.section = arg[1]
m:append(footer) m:append(footer)
return m return api.return_map(m)
@@ -3,6 +3,8 @@ local appname = api.appname
local datatypes = api.datatypes local datatypes = api.datatypes
local sys = api.sys local sys = api.sys
api.set_default_cbi()
m = Map(appname) m = Map(appname)
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -33,4 +35,4 @@ s:append(Template(appname .. "/node_list/link_add_node"))
m:append(Template(appname .. "/node_list/node_list")) m:append(Template(appname .. "/node_list/node_list"))
return m return api.return_map(m)
@@ -38,6 +38,8 @@ if has_hysteria2 then
table.insert(hysteria2_type, s) table.insert(hysteria2_type, s)
end end
api.set_default_cbi()
m = Map(appname) m = Map(appname)
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -124,7 +126,8 @@ o.cfgvalue = function(self, section)
translate("Manual subscription All")) translate("Manual subscription All"))
end 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>") local cfgname = "subscribe_list"
s = m:section(TypedSection, cfgname, "", "<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.addremove = true
s.anonymous = true s.anonymous = true
s.sortable = true s.sortable = true
@@ -144,7 +147,7 @@ o.validate = function(self, value, section)
return nil, translate("Remark cannot be empty.") return nil, translate("Remark cannot be empty.")
end end
local duplicate = false local duplicate = false
m.uci:foreach(appname, "subscribe_list", function(e) m.uci:foreach(appname, cfgname, function(e)
if e[".name"] ~= section and e["remark"] and e["remark"]:lower() == value:lower() then if e[".name"] ~= section and e["remark"] and e["remark"]:lower() == value:lower() then
duplicate = true duplicate = true
return false return false
@@ -219,6 +222,11 @@ o.cfgvalue = function(self, section)
section, translate("Manual subscription")) section, translate("Manual subscription"))
end end
local sortable = Template(appname .. "/cbi/sortable")
sortable.api = api
sortable.target_cfgname = cfgname
m:append(sortable)
m:append(Template(appname .. "/node_subscribe/js")) m:append(Template(appname .. "/node_subscribe/js"))
return m return api.return_map(m)
@@ -1,6 +1,8 @@
local api = require "luci.passwall2.api" local api = require "luci.passwall2.api"
local appname = api.appname local appname = api.appname
api.set_default_cbi()
m = Map(appname) m = Map(appname)
m.redirect = api.url("node_subscribe") m.redirect = api.url("node_subscribe")
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -255,13 +257,7 @@ for t = 0, 23 do
end end
end end
o.default = "0:00" o.default = "0:00"
o.validate = function(self, value) o.datatype = "timehhmm"
local b = api.is_timehhmm(value)
if b then
return value
end
return nil
end
o:depends("update_week_mode", "0") o:depends("update_week_mode", "0")
o:depends("update_week_mode", "1") o:depends("update_week_mode", "1")
o:depends("update_week_mode", "2") o:depends("update_week_mode", "2")
@@ -336,4 +332,4 @@ o.description = translate("Some subscription providers may require a hardware id
translate("Enabling this option will send a hashed HWID when updating subscriptions.") .. "<br>" .. translate("Enabling this option will send a hashed HWID when updating subscriptions.") .. "<br>" ..
translate("This may reveal limited hardware information. Enable only if you trust your provider.") translate("This may reveal limited hardware information. Enable only if you trust your provider.")
return m return api.return_map(m)
@@ -8,6 +8,8 @@ local port_validate = function(self, value, t)
return value:gsub("-", ":") return value:gsub("-", ":")
end end
api.set_default_cbi()
m = Map(appname) m = Map(appname)
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -53,13 +55,7 @@ for index, value in ipairs({"stop", "start", "restart"}) do
end end
end end
o.default = "0:00" o.default = "0:00"
o.validate = function(self, value) o.datatype = "timehhmm"
local b = api.is_timehhmm(value)
if b then
return value
end
return nil
end
o:depends(value .. "_week_mode", "0") o:depends(value .. "_week_mode", "0")
o:depends(value .. "_week_mode", "1") o:depends(value .. "_week_mode", "1")
o:depends(value .. "_week_mode", "2") o:depends(value .. "_week_mode", "2")
@@ -312,4 +308,4 @@ if has_singbox then
o.default = 0 o.default = 0
end end
return m return api.return_map(m)
@@ -1,6 +1,8 @@
local api = require "luci.passwall2.api" local api = require "luci.passwall2.api"
local appname = api.appname local appname = api.appname
api.set_default_cbi()
m = Map(appname) m = Map(appname)
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -59,13 +61,7 @@ for t = 0, 23 do
end end
end end
o.default = "0:00" o.default = "0:00"
o.validate = function(self, value) o.datatype = "timehhmm"
local b = api.is_timehhmm(value)
if b then
return value
end
return nil
end
o:depends("update_week_mode", "0") o:depends("update_week_mode", "0")
o:depends("update_week_mode", "1") o:depends("update_week_mode", "1")
o:depends("update_week_mode", "2") o:depends("update_week_mode", "2")
@@ -92,7 +88,8 @@ end
s:append(Template(appname .. "/rule/rule_version")) s:append(Template(appname .. "/rule/rule_version"))
s = m:section(TypedSection, "shunt_rules", "Sing-Box/Xray " .. translate("Shunt Rule"), "<a style='color: red'>" .. translate("Please note attention to the priority, the higher the order, the higher the priority.") .. "</a>") local cfgname = "shunt_rules"
s = m:section(TypedSection, cfgname, "Sing-Box/Xray " .. translate("Shunt Rule"), "<a style='color: red'>" .. translate("Please note attention to the priority, the higher the order, the higher the priority.") .. "</a>")
s.template = "cbi/tblsection" s.template = "cbi/tblsection"
s.anonymous = false s.anonymous = false
s.addremove = true s.addremove = true
@@ -113,4 +110,9 @@ end
o = s:option(DummyValue, "remarks", translate("Remarks")) o = s:option(DummyValue, "remarks", translate("Remarks"))
return m local sortable = Template(appname .. "/cbi/sortable")
sortable.api = api
sortable.target_cfgname = cfgname
m:append(sortable)
return api.return_map(m)
@@ -2,6 +2,8 @@ local api = require "luci.passwall2.api"
local appname = api.appname local appname = api.appname
local datatypes = api.datatypes local datatypes = api.datatypes
api.set_default_cbi()
m = Map(appname, "Sing-Box/Xray " .. translate("Shunt Rule")) m = Map(appname, "Sing-Box/Xray " .. translate("Shunt Rule"))
m.redirect = api.url("rule") m.redirect = api.url("rule")
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -248,4 +250,4 @@ ip_list.description = "<br /><ul>"
o = s:option(Flag, "invert", "invert", translate("Invert match result.") .. " " .. translate("Only support Sing-Box.")) o = s:option(Flag, "invert", "invert", translate("Invert match result.") .. " " .. translate("Only support Sing-Box."))
return m return api.return_map(m)
@@ -1,6 +1,8 @@
local api = require "luci.passwall2.api" local api = require "luci.passwall2.api"
local appname = api.appname local appname = api.appname
api.set_default_cbi()
m = Map(appname) m = Map(appname)
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -80,23 +82,14 @@ o = s:option(Flag, "enable_autoswitch", translate("Auto Switch"))
o.default = 0 o.default = 0
o.rmempty = false o.rmempty = false
o = s:option(Value, "autoswitch_testing_time", translate("How often to test"), translate("Units:seconds")) o = s:option(ListValue, "backup_node_add_mode", translate("Backup Node Addition Method"))
o.datatype = "min(10)"
o.default = 30
o:depends("enable_autoswitch", true) o:depends("enable_autoswitch", true)
o.default = "manual"
o:value("manual", translate("Manual"))
o:value("batch", translate("Batch"))
o = s:option(Value, "autoswitch_connect_timeout", translate("Timeout seconds"), translate("Units:seconds"))
o.datatype = "min(1)"
o.default = 3
o:depends("enable_autoswitch", true)
o = s:option(Value, "autoswitch_retry_num", translate("Timeout retry num"))
o.datatype = "min(1)"
o.default = 1
o:depends("enable_autoswitch", true)
o = s:option(DynamicList, "autoswitch_backup_node", translate("List of backup nodes")) o = s:option(DynamicList, "autoswitch_backup_node", translate("List of backup nodes"))
o:depends("enable_autoswitch", true) o:depends("backup_node_add_mode", "manual")
o.template = appname .. "/cbi/nodes_dynamiclist" o.template = appname .. "/cbi/nodes_dynamiclist"
o.group = {} o.group = {}
o.write = function(self, section, value) o.write = function(self, section, value)
@@ -118,6 +111,44 @@ for i, v in pairs(nodes_table) do
socks_node.group[#socks_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default") socks_node.group[#socks_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
end 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.uci:foreach(appname, "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 = "min(1)"
o.default = 3
o:depends("enable_autoswitch", true)
o = s:option(Value, "autoswitch_retry_num", translate("Timeout retry num"))
o.datatype = "min(1)"
o.default = 1
o:depends("enable_autoswitch", true)
o = s:option(Flag, "autoswitch_restore_switch", translate("Restore Switch"), translate("When detects main node is available, switch back to the main node.")) o = s:option(Flag, "autoswitch_restore_switch", translate("Restore Switch"), translate("When detects main node is available, switch back to the main node."))
o:depends("enable_autoswitch", true) o:depends("enable_autoswitch", true)
@@ -134,6 +165,6 @@ o:depends("enable_autoswitch", true)
o = s:option(DummyValue, "btn") o = s:option(DummyValue, "btn")
o.template = appname .. "/socks_auto_switch/btn" o.template = appname .. "/socks_auto_switch/btn"
o:depends("enable_autoswitch", true) o:depends("backup_node_add_mode", "manual")
return m return api.return_map(m)
@@ -442,22 +442,15 @@ o = s:option(Value, _n("tls_serverName"), "SNI " .. translate("Domain"))
o:depends({ [_n("tls")] = true }) o:depends({ [_n("tls")] = true })
o:depends({ [_n("protocol")] = "hysteria2" }) o:depends({ [_n("protocol")] = "hysteria2" })
if api.compare_versions(os.date("%Y.%m.%d"), "<", "3026.6.1") then o = s:option(Value, _n("tls_pinSHA256"), translate("TLS Chain Fingerprint (SHA256)"))
o = s:option(Flag, _n("tls_allowInsecure"), translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped.")) o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o.default = "0" o:depends({ [_n("protocol")] = "hysteria2" })
o:depends({ [_n("tls")] = true, [_n("reality")] = false }) o.description = translate("Once set, connects only when the server’s chain fingerprint matches.") ..
o:depends({ [_n("protocol")] = "hysteria2" }) string.format("<a href='javascript:void(0)' onclick='javascript:fetchCertSha256(this)'>%s</a>", "→ " .. translate("Fetch Manually"))
end
if api.compare_versions(xray_version, ">=", "26.1.31") then o = s:option(Value, _n("tls_CertByName"), translate("TLS Certificate Name (CertName)"), translate("TLS is used to verify the leaf certificate name."))
o = s:option(Value, _n("tls_pinSHA256"), translate("TLS Chain Fingerprint (SHA256)")) o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("tls")] = true, [_n("reality")] = false }) o:depends({ [_n("protocol")] = "hysteria2" })
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Value, _n("tls_CertByName"), translate("TLS Certificate Name (CertName)"), translate("TLS is used to verify the leaf certificate name."))
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria2" })
end
o = s:option(Flag, _n("tls_certificate"), translate("TLS Certificate (PEM)")) o = s:option(Flag, _n("tls_certificate"), translate("TLS Certificate (PEM)"))
o.default = "0" o.default = "0"
@@ -671,6 +664,14 @@ o = s:option(TextValue, _n("xhttp_extra"), " ", translate("An XHttpObject in J
o:depends({ [_n("use_xhttp_extra")] = true }) o:depends({ [_n("use_xhttp_extra")] = true })
o.rows = 10 o.rows = 10
o.wrap = "off" 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.custom_cfgvalue = function(self, section, value) o.custom_cfgvalue = function(self, section, value)
local raw = m:get(section, "xhttp_extra") local raw = m:get(section, "xhttp_extra")
if raw then if raw then
@@ -693,14 +694,6 @@ o.custom_write = function(self, section, value)
m:del(section, "download_address") m:del(section, "download_address")
end end
end end
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
if api.jsonc.parse(value) then
return value
else
return nil, "XHTTP Extra " .. translate("Must be JSON text!")
end
end
o.custom_remove = function(self, section, value) o.custom_remove = function(self, section, value)
m:del(section, "xhttp_extra") m:del(section, "xhttp_extra")
m:del(section, "download_address") m:del(section, "download_address")
@@ -758,6 +751,14 @@ o.rows = 10
o.wrap = "off" o.wrap = "off"
o.description = translate("An FinalMaskObject in JSON format, used for sharing.") .. "<br>" .. o.description = translate("An FinalMaskObject in JSON format, used for sharing.") .. "<br>" ..
translate("Custom finalmask overrides mkcp, hysteria2, fragment, noise, and related settings.") 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.custom_cfgvalue = function(self, section, value) o.custom_cfgvalue = function(self, section, value)
local raw = m:get(section, "finalmask") local raw = m:get(section, "finalmask")
if raw then if raw then
@@ -767,14 +768,6 @@ end
o.custom_write = function(self, section, value) o.custom_write = function(self, section, value)
m:set(section, "finalmask", api.base64Encode(value) or "") m:set(section, "finalmask", api.base64Encode(value) or "")
end end
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
if api.jsonc.parse(value) then
return value
else
return nil, "FinalMask " .. translate("Must be JSON text!")
end
end
--[[Fast Open]] --[[Fast Open]]
o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required")) o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required"))
@@ -1,4 +1,7 @@
local api = require "luci.passwall2.api" local api = require "luci.passwall2.api"
local appname = api.appname
api.set_default_cbi()
m = Map("passwall2_server", translate("Server-Side")) m = Map("passwall2_server", translate("Server-Side"))
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -10,7 +13,8 @@ t.addremove = false
e = t:option(Flag, "enable", translate("Enable")) e = t:option(Flag, "enable", translate("Enable"))
e.rmempty = false e.rmempty = false
t = m:section(TypedSection, "user", translate("Users Manager")) local cfgname = "user"
t = m:section(TypedSection, cfgname, translate("Users Manager"))
t.anonymous = true t.anonymous = true
t.addremove = true t.addremove = true
t.sortable = true t.sortable = true
@@ -85,8 +89,14 @@ e = t:option(Flag, "log", translate("Log"))
e.default = "1" e.default = "1"
e.rmempty = false e.rmempty = false
m:append(Template("passwall2/server/log")) local sortable = Template(appname .. "/cbi/sortable")
sortable.api = api
sortable.appname = m.config
sortable.target_cfgname = cfgname
m:append(sortable)
m:append(Template("passwall2/server/users_list_status")) m:append(Template(appname .. "/server/log"))
return m
m:append(Template(appname .. "/server/users_list_status"))
return api.return_map(m)
@@ -129,12 +129,12 @@ o = s:option(TextValue, _n("custom_config"), translate("Custom Config"))
o.rows = 10 o.rows = 10
o.wrap = "off" o.wrap = "off"
o:depends({ [_n("custom")] = true }) o:depends({ [_n("custom")] = true })
o.validate = function(self, value, t) o.datatype = "json"
if value and api.jsonc.parse(value) then local o_validate = o.validate
return value o.validate = function(self, value)
else local v = o_validate(self, value)
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!") if v then return v end
end return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end end
o.custom_cfgvalue = function(self, section, value) o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str") local config_str = m:get(section, "config_str")
@@ -412,6 +412,7 @@ o = s:option(TextValue, _n("finalmask"), "FinalMask JSON")
o:depends({ [_n("use_finalmask")] = true }) o:depends({ [_n("use_finalmask")] = true })
o.rows = 10 o.rows = 10
o.wrap = "off" o.wrap = "off"
o.datatype = "json"
o.custom_cfgvalue = function(self, section, value) o.custom_cfgvalue = function(self, section, value)
local raw = m:get(section, "finalmask") local raw = m:get(section, "finalmask")
if raw then if raw then
@@ -421,14 +422,6 @@ end
o.custom_write = function(self, section, value) o.custom_write = function(self, section, value)
m:set(section, "finalmask", api.base64Encode(value) or "") m:set(section, "finalmask", api.base64Encode(value) or "")
end end
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
if api.jsonc.parse(value) then
return value
else
return nil, "FinalMask " .. translate("Must be JSON text!")
end
end
--[[acceptProxyProtocol]] --[[acceptProxyProtocol]]
o = s:option(Flag, _n("acceptProxyProtocol"), translate("acceptProxyProtocol"), translate("Whether to receive PROXY protocol, when this node want to be fallback or forwarded by proxy, it must be enable, otherwise it cannot be used.")) o = s:option(Flag, _n("acceptProxyProtocol"), translate("acceptProxyProtocol"), translate("Whether to receive PROXY protocol, when this node want to be fallback or forwarded by proxy, it must be enable, otherwise it cannot be used."))
@@ -523,16 +516,16 @@ for _, d in ipairs(netdev_list) do
o:value(d.name, d.label) o:value(d.name, d.label)
end end
o = s:option(TextValue, _n("custom_config"), translate("Custom Config")) o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)")
o.rows = 10 o.rows = 10
o.wrap = "off" o.wrap = "off"
o:depends({ [_n("custom")] = true }) o:depends({ [_n("custom")] = true })
o.validate = function(self, value, t) o.datatype = "json"
if value and api.jsonc.parse(value) then local o_validate = o.validate
return value o.validate = function(self, value)
else local v = o_validate(self, value)
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!") if v then return v end
end return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end end
o.custom_cfgvalue = function(self, section, value) o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str") local config_str = m:get(section, "config_str")
@@ -489,16 +489,16 @@ for _, d in ipairs(netdev_list) do
o:value(d.name, d.label) o:value(d.name, d.label)
end end
o = s:option(TextValue, _n("custom_config"), translate("Custom Config")) o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)")
o.rows = 10 o.rows = 10
o.wrap = "off" o.wrap = "off"
o:depends({ [_n("custom")] = true }) o:depends({ [_n("custom")] = true })
o.validate = function(self, value, t) o.datatype = "json"
if value and api.jsonc.parse(value) then local o_validate = o.validate
return value o.validate = function(self, value)
else local v = o_validate(self, value)
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!") if v then return v end
end return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end end
o.custom_cfgvalue = function(self, section, value) o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str") local config_str = m:get(section, "config_str")
@@ -49,16 +49,16 @@ o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"))
o.default = "0" o.default = "0"
o:depends({ [_n("custom")] = false }) o:depends({ [_n("custom")] = false })
o = s:option(TextValue, _n("custom_config"), translate("Custom Config")) o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)")
o.rows = 10 o.rows = 10
o.wrap = "off" o.wrap = "off"
o:depends({ [_n("custom")] = true }) o:depends({ [_n("custom")] = true })
o.validate = function(self, value, t) o.datatype = "json"
if value and api.jsonc.parse(value) then local o_validate = o.validate
return value o.validate = function(self, value)
else local v = o_validate(self, value)
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!") if v then return v end
end return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end end
o.custom_cfgvalue = function(self, section, value) o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str") local config_str = m:get(section, "config_str")
@@ -76,16 +76,16 @@ o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"))
o.default = "0" o.default = "0"
o:depends({ [_n("custom")] = false }) o:depends({ [_n("custom")] = false })
o = s:option(TextValue, _n("custom_config"), translate("Custom Config")) o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)")
o.rows = 10 o.rows = 10
o.wrap = "off" o.wrap = "off"
o:depends({ [_n("custom")] = true }) o:depends({ [_n("custom")] = true })
o.validate = function(self, value, t) o.datatype = "json"
if value and api.jsonc.parse(value) then local o_validate = o.validate
return value o.validate = function(self, value)
else local v = o_validate(self, value)
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!") if v then return v end
end return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end end
o.custom_cfgvalue = function(self, section, value) o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str") local config_str = m:get(section, "config_str")
@@ -2,6 +2,8 @@ api = require "luci.passwall2.api"
appname = api.appname appname = api.appname
fs = api.fs fs = api.fs
api.set_default_cbi()
m = Map("passwall2_server", translate("Server Config")) m = Map("passwall2_server", translate("Server Config"))
m.redirect = api.url("server") m.redirect = api.url("server")
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -54,4 +56,4 @@ footer.config = m.config
footer.section = arg[1] footer.section = arg[1]
m:append(footer) m:append(footer)
return m return api.return_map(m)
+115 -2
View File
@@ -367,8 +367,16 @@ function strToTable(str)
return loadstring("return " .. str)() return loadstring("return " .. str)()
end end
function is_timehhmm(timeStr) function is_json(str)
local hour, minute = string.match(timeStr, "^(%d?%d):(%d%d)$") if str and jsonc.parse(str) then
return true
end
return false
end
datatypes.json = is_json
function is_timehhmm(str)
local hour, minute = string.match(str, "^(%d?%d):(%d%d)$")
if hour and minute then if hour and minute then
hour = tonumber(hour) hour = tonumber(hour)
minute = tonumber(minute) minute = tonumber(minute)
@@ -378,6 +386,7 @@ function is_timehhmm(timeStr)
end end
return false return false
end end
datatypes.timehhmm = is_timehhmm
function is_normal_node(e) function is_normal_node(e)
if e and e.type and e.protocol and (e.protocol == "_balancing" or e.protocol == "_shunt" or e.protocol == "_iface" or e.protocol == "_urltest") then if e and e.type and e.protocol and (e.protocol == "_balancing" or e.protocol == "_shunt" or e.protocol == "_iface" or e.protocol == "_urltest") then
@@ -1380,6 +1389,40 @@ function set_apply_on_parse(map)
end end
end end
function set_default_cbi()
local cbi = require "luci.cbi"
if true then
--TextValue
local TextValue = cbi.TextValue
local original_init = TextValue.__init__
function TextValue.__init__(self, ...)
original_init(self, ...)
self.template = appname .. "/cbi/tvalue"
end
end
end
function return_map(map)
local cbi = require "luci.cbi"
local api = require "luci.passwall2.api"
if true then
-- header
local header = cbi.Template(appname .. "/cbi/header")
header.api = api
header.config = map.config
table.insert(map.children, 1, header)
end
if true then
-- footer
local footer = cbi.Template(appname .. "/cbi/footer")
footer.api = api
footer.config = map.config
map:append(footer)
end
return map
end
function luci_types(id, m, s, type_name, option_prefix) function luci_types(id, m, s, type_name, option_prefix)
local fv_type local fv_type
local field_type = s.fields["type"] local field_type = s.fields["type"]
@@ -1585,6 +1628,56 @@ function match_node_rule(name, rule)
return false return false
end end
local normal_nodes = {}
function get_batch_nodes(node)
if #normal_nodes == 0 then
for k, e in ipairs(get_valid_nodes()) do
if e.node_type == "normal" and (not e.chain_proxy or e.chain_proxy == "") then
normal_nodes[#normal_nodes + 1] = {
id = e[".name"],
remarks = e["remarks"],
group = e["group"]
}
end
end
end
if not node.node_group or node.node_group == "" then return {} end
local nodes = {}
for g in node.node_group:gmatch("%S+") do
g = UrlDecode(g)
for k, v in pairs(normal_nodes) do
local gn = (v.group and v.group ~= "") and v.group or "default"
if gn:lower() == g:lower() and match_node_rule(v.remarks, node.node_match_rule) then
nodes[#nodes + 1] = v.id
end
end
end
return nodes
end
function get_socks_backup_nodes(id)
id = trim(id)
if id == "" then return "" end
local socks = uci:get_all(appname, id)
local nodes
if socks.backup_node_add_mode and socks.backup_node_add_mode == "batch" then
local node = {}
node.node_group = socks.backup_node_group
node.node_match_rule = socks.backup_node_match_rule
nodes = get_batch_nodes(node)
else
nodes = socks.autoswitch_backup_node
end
local backup_nodes, seen = {}, {}
for _, v in ipairs(nodes or {}) do
if v ~= socks.node and not seen[v] then
seen[v] = true
table.insert(backup_nodes, v)
end
end
return table.concat(backup_nodes, " ")
end
function get_core(field, candidates) function get_core(field, candidates)
local v = uci:get(appname, "@global_subscribe[0]", field) local v = uci:get(appname, "@global_subscribe[0]", field)
if v and v ~= "" then if v and v ~= "" then
@@ -1610,6 +1703,26 @@ function cleanEmptyTables(t)
return next(t) and t or nil return next(t) and t or nil
end end
function fetch_cert_sha256(host, port, sni, timeout)
if not host then return "" end
port = tonumber(port) or 443
sni = sni or host
timeout = tonumber(timeout) or 5
local cmd = string.format(
"timeout %d openssl s_client -connect %s:%d -servername %s -showcerts </dev/null 2>/dev/null " ..
"| awk 'BEGIN{c=0}/BEGIN CERT/{c++} c==1{print} /END CERT/{if(c==1)exit}' " ..
"| openssl x509 -outform der 2>/dev/null " ..
"| sha256sum 2>/dev/null",
timeout, host, port, sni
)
local out = trim(sys.exec(cmd))
local fp = out:match("^([0-9a-fA-F]+)")
if not fp or fp:lower():match("^e3b0c44298fc1c149afbf4c8996fb924") then
return ""
end
return fp:upper()
end
function get_dnsmasq_server_domain() function get_dnsmasq_server_domain()
local dnsmasq_server = uci:get("dhcp", "@dnsmasq[0]", "server") local dnsmasq_server = uci:get("dhcp", "@dnsmasq[0]", "server")
local dnsmasq_server_t = {} local dnsmasq_server_t = {}
@@ -1329,33 +1329,6 @@ function gen_config(var)
return result return result
end end
local nodes_list = {}
function get_urltest_batch_nodes(_node)
if #nodes_list == 0 then
for k, e in ipairs(api.get_valid_nodes()) do
if e.node_type == "normal" and (not e.chain_proxy or e.chain_proxy == "") then
nodes_list[#nodes_list + 1] = {
id = e[".name"],
remarks = e["remarks"],
group = e["group"]
}
end
end
end
if not _node.node_group or _node.node_group == "" then return {} end
local nodes = {}
for g in _node.node_group:gmatch("%S+") do
g = api.UrlDecode(g)
for k, v in pairs(nodes_list) do
local gn = (v.group and v.group ~= "") and v.group or "default"
if gn:lower() == g:lower() and api.match_node_rule(v.remarks, _node.node_match_rule) then
nodes[#nodes + 1] = v.id
end
end
end
return nodes
end
function get_node_by_id(node_id) function get_node_by_id(node_id)
if not node_id or node_id == "" or node_id == "nil" then return nil end if not node_id or node_id == "" or node_id == "nil" then return nil end
if node_id:find("Socks_") then if node_id:find("Socks_") then
@@ -1377,7 +1350,7 @@ function gen_config(var)
-- new urltest -- new urltest
local ut_nodes local ut_nodes
if _node.node_add_mode and _node.node_add_mode == "batch" then if _node.node_add_mode and _node.node_add_mode == "batch" then
ut_nodes = get_urltest_batch_nodes(_node) ut_nodes = api.get_batch_nodes(_node)
else else
ut_nodes = _node.urltest_node ut_nodes = _node.urltest_node
end end
@@ -176,21 +176,9 @@ function gen_outbound(flag, node, tag, proxy_table)
security = node.stream_security, security = node.stream_security,
tlsSettings = (node.stream_security == "tls") and { tlsSettings = (node.stream_security == "tls") and {
serverName = node.tls_serverName, serverName = node.tls_serverName,
allowInsecure = (function()
if node.tls_pinSHA256 and node.tls_pinSHA256 ~= "" then return nil end
if node.tls_allowInsecure == "1" then return true end
end)(),
fingerprint = (node.type == "Xray" and node.utls == "1" and node.fingerprint and node.fingerprint ~= "") and node.fingerprint or nil, fingerprint = (node.type == "Xray" and node.utls == "1" and node.fingerprint and node.fingerprint ~= "") and node.fingerprint or nil,
pinnedPeerCertSha256 = (function() pinnedPeerCertSha256 = node.tls_pinSHA256,
if api.compare_versions(xray_version, "<", "26.1.31") then return nil end verifyPeerCertByName = node.tls_CertByName,
if not node.tls_pinSHA256 then return "" end
return node.tls_pinSHA256
end)(),
verifyPeerCertByName = (function()
if api.compare_versions(xray_version, "<", "26.1.31") then return nil end
if not node.tls_CertByName then return "" end
return node.tls_CertByName
end)(),
echConfigList = (node.ech == "1") and node.ech_config or nil, echConfigList = (node.ech == "1") and node.ech_config or nil,
certificates = (node.tls_certificate == "1" and node.tls_certificate_pem ~= "") and { certificates = (node.tls_certificate == "1" and node.tls_certificate_pem ~= "") and {
certificate = api.split(node.tls_certificate_pem, "\n"), certificate = api.split(node.tls_certificate_pem, "\n"),
@@ -839,7 +827,8 @@ function gen_config_server(node)
serverNames = node.reality_serverNames or {}, serverNames = node.reality_serverNames or {},
privateKey = node.reality_private_key, privateKey = node.reality_private_key,
shortIds = node.reality_shortId or "", shortIds = node.reality_shortId or "",
mldsa65Seed = (node.use_mldsa65Seed == "1") and node.reality_mldsa65Seed or nil mldsa65Seed = (node.use_mldsa65Seed == "1") and node.reality_mldsa65Seed or nil,
minClientVer = "1.0.0"
} or nil } or nil
end end
end end
@@ -1032,33 +1021,6 @@ function gen_config(var)
end end
end end
local nodes_list = {}
function get_balancer_batch_nodes(_node)
if #nodes_list == 0 then
for k, e in ipairs(api.get_valid_nodes()) do
if e.node_type == "normal" and (not e.chain_proxy or e.chain_proxy == "") then
nodes_list[#nodes_list + 1] = {
id = e[".name"],
remarks = e["remarks"],
group = e["group"]
}
end
end
end
if not _node.node_group or _node.node_group == "" then return {} end
local nodes = {}
for g in _node.node_group:gmatch("%S+") do
g = api.UrlDecode(g)
for k, v in pairs(nodes_list) do
local gn = (v.group and v.group ~= "") and v.group or "default"
if gn:lower() == g:lower() and api.match_node_rule(v.remarks, _node.node_match_rule) then
nodes[#nodes + 1] = v.id
end
end
end
return nodes
end
function gen_loopback(outbound_tag, loopback_dst) function gen_loopback(outbound_tag, loopback_dst)
if not outbound_tag or outbound_tag == "" then return nil end if not outbound_tag or outbound_tag == "" then return nil end
local inbound_tag = loopback_dst and "lo-to-" .. loopback_dst or outbound_tag .. "-lo" local inbound_tag = loopback_dst and "lo-to-" .. loopback_dst or outbound_tag .. "-lo"
@@ -1090,7 +1052,7 @@ function gen_config(var)
-- new balancer -- new balancer
local blc_nodes local blc_nodes
if _node.node_add_mode and _node.node_add_mode == "batch" then if _node.node_add_mode and _node.node_add_mode == "batch" then
blc_nodes = get_balancer_batch_nodes(_node) blc_nodes = api.get_batch_nodes(_node)
else else
blc_nodes = _node.balancing_node blc_nodes = _node.balancing_node
end end
@@ -0,0 +1 @@
<!-- map footer -->
@@ -0,0 +1,2 @@
<!-- map header -->
<script src="<%=resource%>/view/<%=self.api.appname%>/cbi.js"></script>
@@ -0,0 +1,159 @@
<%
local api = self.api
local appname = self.appname or api.appname
local target_cfgname = self.target_cfgname
-%>
<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%>-<%=target_cfgname%> .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%>-<%=target_cfgname%>");
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>
@@ -0,0 +1,13 @@
<%+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%>
@@ -2,46 +2,8 @@
local api = require "luci.passwall2.api" local api = require "luci.passwall2.api"
local appname = api.appname local appname = api.appname
-%> -%>
<script src="<%=resource%>/view/<%=appname%>/Sortable.min.js"></script>
<style> <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%>-haproxy_config .cbi-section-table-titles {
display: none !important;
}
}
@media screen and (min-width: 1152px) { @media screen and (min-width: 1152px) {
#cbi-<%=appname%>-haproxy_config input[id*=".<%=appname%>."][id$=".haproxy_port"], #cbi-<%=appname%>-haproxy_config input[id*=".<%=appname%>."][id$=".haproxy_port"],
#cbi-<%=appname%>-haproxy_config input[id*=".<%=appname%>."][id$=".lbweight"] { #cbi-<%=appname%>-haproxy_config input[id*=".<%=appname%>."][id$=".lbweight"] {
@@ -110,129 +72,5 @@ table .cbi-button-down,
} }
}, 300); }, 300);
}); });
//节点列表添加拖拽排序
document.addEventListener("DOMContentLoaded", function () {
function initSortableForTable() {
var section = document.getElementById("cbi-<%=appname%>-haproxy_config");
if (!section) return;
hideSortColumn(section);
// === 插入 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);
});
// === 初始化 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;
// 保存原始顺序
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);
}
}
// 获取 table 当前行顺序
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;
}
// 拖拽完成后更新 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(" ") : "";
}
}
// 隐藏18.06 up/down 列
function hideSortColumn(section) {
var table = section.querySelector("table");
if (!table) return;
var ths = Array.prototype.slice.call(table.querySelectorAll("tr.cbi-section-table-titles > th"));
var dataRows = table.querySelectorAll("tr.cbi-section-table-row");
if (!ths.length || !dataRows.length) return;
var sortCol = -1;
for (var i = 0; i < ths.length; i++) {
var hasSort = false, invalid = false;
dataRows.forEach(function(tr) {
var td = tr.querySelectorAll(":scope > td")[i];
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 = i; break; }
}
if (sortCol === -1) return;
var rows = [table.querySelector("tr.cbi-section-table-titles")].concat(
Array.prototype.slice.call(dataRows),
Array.prototype.slice.call(table.querySelectorAll("tr.cbi-section-table-descr"))
);
rows.forEach(function(tr) {
var cells = Array.prototype.filter.call(tr.children, function(el) {
return el.tagName === "TH" || el.tagName === "TD";
});
if (cells[sortCol]) cells[sortCol].style.display = "none";
});
}
// === 等待 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> </script>
@@ -52,5 +52,29 @@ local api = self.api
} }
}); });
} }
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: node_id }, 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%> !");
return;
}
const inputs = document.querySelectorAll('input[id$=".xray_tls_pinSHA256"]');
inputs.forEach(function(input) {
input.value = res.data;
});
});
}
//]]> //]]>
</script> </script>
@@ -1314,6 +1314,8 @@ local current_node = map:get(section)
opt.set(dom_prefix + 'utls', true); opt.set(dom_prefix + 'utls', true);
opt.set(dom_prefix + 'fingerprint', queryParam.fp); opt.set(dom_prefix + 'fingerprint', queryParam.fp);
} }
opt.set(dom_prefix + 'ech', !!queryParam.ech);
opt.set(dom_prefix + 'ech_config', queryParam.ech || '');
opt.set(dom_prefix + 'tls_pinSHA256', queryParam.pcs || ''); opt.set(dom_prefix + 'tls_pinSHA256', queryParam.pcs || '');
opt.set(dom_prefix + 'tls_CertByName', queryParam.vcn || ''); opt.set(dom_prefix + 'tls_CertByName', queryParam.vcn || '');
opt.set( opt.set(
@@ -635,11 +635,15 @@ table td, .table .td {
if (cacheData && cacheData.savetime && (now - cacheData.timestamp) < cacheData.savetime) { if (cacheData && cacheData.savetime && (now - cacheData.timestamp) < cacheData.savetime) {
const a = ping_value[i].firstElementChild; const a = ping_value[i].firstElementChild;
let color = "red"; let color = "red";
if (cacheData.value < 100) if (cacheData.value >= 0) {
color = "green"; a.innerText = cacheData.value + " ms";
else if (cacheData.value < 200) if (cacheData.value < 100)
color = "#fb9a05"; color = "green";
a.innerText = cacheData.value + " ms"; else if (cacheData.value < 200)
color = "#fb9a05";
} else {
a.innerText = "<%:Timeout%>";
}
a.style.color = color; a.style.color = color;
a.onclick = function () { a.onclick = function () {
ping_node(cbi_id, this, auto_detection_time); ping_node(cbi_id, this, auto_detection_time);
@@ -679,15 +683,23 @@ table td, .table .td {
const cache_data = { const cache_data = {
dom_id: strs[i], dom_id: strs[i],
timestamp: Date.now(), timestamp: Date.now(),
savetime: 60 * 1000, savetime: 150 * 1000,
value: ping value: ping
}; };
text = result.ping + " ms";
localStorage.setItem(auto_detection_time + ":" + dom.address + ":" + dom.port, JSON.stringify(cache_data)); localStorage.setItem(auto_detection_time + ":" + dom.address + ":" + dom.port, JSON.stringify(cache_data));
text = result.ping + " ms";
if (ping < 100) if (ping < 100)
color = "green"; color = "green";
else if (ping < 200) else if (ping < 200)
color = "#fb9a05"; color = "#fb9a05";
} else {
const cache_data = {
dom_id: strs[i],
timestamp: Date.now(),
savetime: 60 * 1000,
value: -1
};
localStorage.setItem(auto_detection_time + ":" + dom.address + ":" + dom.port, JSON.stringify(cache_data));
} }
a.innerText = text; a.innerText = text;
a.style.color = color; a.style.color = color;
@@ -1101,6 +1113,15 @@ table td, .table .td {
onChange("auto_detection_time", auto_detection_time); onChange("auto_detection_time", auto_detection_time);
}); });
}); });
const links = document.querySelectorAll('a');
links.forEach(link => {
link.addEventListener('click', (e) => {
if (link.getAttribute('target') !== '_blank') {
ajaxAbortAll();
}
});
});
}); });
//]]> //]]>
</script> </script>
@@ -1,46 +1,6 @@
<% <%
local api = require "luci.passwall2.api" local api = require "luci.passwall2.api"
-%> -%>
<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-<%=api.appname%>-subscribe_list .cbi-section-table-titles {
display: none !important;
}
}
</style>
<script type="text/javascript"> <script type="text/javascript">
//<![CDATA[ //<![CDATA[
@@ -148,129 +108,5 @@ table .cbi-button-down,
} }
}); });
} }
//订阅列表添加拖拽排序
document.addEventListener("DOMContentLoaded", function () {
function initSortableForTable() {
var section = document.getElementById("cbi-<%=api.appname%>-subscribe_list");
if (!section) return;
hideSortColumn(section);
// === 插入 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);
});
// === 初始化 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;
// 保存原始顺序
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);
}
}
// 获取 table 当前行顺序
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-<%=api.appname%>-"))
id = id.replace("cbi-<%=api.appname%>-", "");
order.push(id);
});
return order;
}
// 拖拽完成后更新 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(" ") : "";
}
}
// 隐藏18.06 up/down 列
function hideSortColumn(section) {
var table = section.querySelector("table");
if (!table) return;
var ths = Array.prototype.slice.call(table.querySelectorAll("tr.cbi-section-table-titles > th"));
var dataRows = table.querySelectorAll("tr.cbi-section-table-row");
if (!ths.length || !dataRows.length) return;
var sortCol = -1;
for (var i = 0; i < ths.length; i++) {
var hasSort = false, invalid = false;
dataRows.forEach(function(tr) {
var td = tr.querySelectorAll(":scope > td")[i];
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 = i; break; }
}
if (sortCol === -1) return;
var rows = [table.querySelector("tr.cbi-section-table-titles")].concat(
Array.prototype.slice.call(dataRows),
Array.prototype.slice.call(table.querySelectorAll("tr.cbi-section-table-descr"))
);
rows.forEach(function(tr) {
var cells = Array.prototype.filter.call(tr.children, function(el) {
return el.tagName === "TH" || el.tagName === "TD";
});
if (cells[sortCol]) cells[sortCol].style.display = "none";
});
}
// === 等待 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> </script>
@@ -4,44 +4,12 @@ local fs = api.fs
local has_old_geoip = fs.access("/tmp/bak_v2ray/geoip.dat") local has_old_geoip = fs.access("/tmp/bak_v2ray/geoip.dat")
local has_old_geosite = fs.access("/tmp/bak_v2ray/geosite.dat") local has_old_geosite = fs.access("/tmp/bak_v2ray/geosite.dat")
-%> -%>
<script src="<%=resource%>/view/<%=api.appname%>/Sortable.min.js"></script>
<style> <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;
}
div.cbi-value[id$="-geoip_update"], div.cbi-value[id$="-geoip_update"],
div.cbi-value[id$="-geosite_update"] { div.cbi-value[id$="-geosite_update"] {
display: none !important; 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);
}
</style> </style>
<div class="cbi-value" id="_rule_div"> <div class="cbi-value" id="_rule_div">
@@ -157,118 +125,5 @@ local has_old_geosite = fs.access("/tmp/bak_v2ray/geosite.dat")
} }
); );
} }
//分流规则添加拖拽排序
document.addEventListener("DOMContentLoaded", function () {
function initSortableForTable() {
var section = document.getElementById("cbi-<%=api.appname%>-shunt_rules");
if (!section) return;
// === 插入 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);
});
// === 初始化 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;
// 保存原始顺序
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);
}
}
// 获取 table 当前行顺序
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-<%=api.appname%>-"))
id = id.replace("cbi-<%=api.appname%>-", "");
order.push(id);
});
return order;
}
// 拖拽完成后更新 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(" ") : "";
}
}
// 隐藏18.06 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";
});
}
// === 等待 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> </script>
+13 -1
View File
@@ -379,6 +379,9 @@ msgstr "توزیع بار"
msgid "Node Addition Method" msgid "Node Addition Method"
msgstr "روش افزودن گره" msgstr "روش افزودن گره"
msgid "Backup Node Addition Method"
msgstr "روش افزودن گره پشتیبان"
msgid "Manual" msgid "Manual"
msgstr "دستی" msgstr "دستی"
@@ -1651,6 +1654,12 @@ msgstr "گواهی TLS (PEM)"
msgid "Full certificate (chain), PEM format." msgid "Full certificate (chain), PEM format."
msgstr "گواهی کامل (زنجیره‌ای)، فرمت PEM." msgstr "گواهی کامل (زنجیره‌ای)، فرمت PEM."
msgid "Fetch Manually"
msgstr "دریافت دستی"
msgid "Fetch Failed"
msgstr "واکشی ناموفق"
msgid "ECH Config" msgid "ECH Config"
msgstr "پیکربندی ECH" msgstr "پیکربندی ECH"
@@ -2400,4 +2409,7 @@ msgid "Gecko Packet Size (min)"
msgstr "اندازه بسته Gecko (دقیقه)" msgstr "اندازه بسته Gecko (دقیقه)"
msgid "Gecko Packet Size (max)" msgid "Gecko Packet Size (max)"
msgstr "اندازه بسته Gecko (حداکثر)" msgstr "اندازه بسته Gecko (حداکثر)"
msgid "valid time (hh:mm)"
msgstr "زمان معتبر (ساعت:میلی‌متر)"
+12
View File
@@ -376,6 +376,9 @@ msgstr "Балансировка"
msgid "Node Addition Method" msgid "Node Addition Method"
msgstr "Способ добавления узлов" msgstr "Способ добавления узлов"
msgid "Backup Node Addition Method"
msgstr "Метод добавления резервного узла"
msgid "Manual" msgid "Manual"
msgstr "Вручную" msgstr "Вручную"
@@ -1652,6 +1655,12 @@ msgstr "Сертификат TLS (PEM)"
msgid "Full certificate (chain), PEM format." msgid "Full certificate (chain), PEM format."
msgstr "Полный сертификат (цепочка), формат PEM." msgstr "Полный сертификат (цепочка), формат PEM."
msgid "Fetch Manually"
msgstr "Получить вручную"
msgid "Fetch Failed"
msgstr "Не удалось получить данные."
msgid "ECH Config" msgid "ECH Config"
msgstr "Конфигурация ECH" msgstr "Конфигурация ECH"
@@ -2399,3 +2408,6 @@ msgstr "Размер упаковки Gecko (мин)"
msgid "Gecko Packet Size (max)" msgid "Gecko Packet Size (max)"
msgstr "Размер упаковки Gecko (макс)" msgstr "Размер упаковки Gecko (макс)"
msgid "valid time (hh:mm)"
msgstr "Время действия (чч:мм)"
+12
View File
@@ -376,6 +376,9 @@ msgstr "负载均衡"
msgid "Node Addition Method" msgid "Node Addition Method"
msgstr "节点添加方式" msgstr "节点添加方式"
msgid "Backup Node Addition Method"
msgstr "后备节点添加方式"
msgid "Manual" msgid "Manual"
msgstr "手动" msgstr "手动"
@@ -1643,6 +1646,12 @@ msgstr "TLS 证书(PEM)"
msgid "Full certificate (chain), PEM format." msgid "Full certificate (chain), PEM format."
msgstr "完整证书(链),PEM 格式。" msgstr "完整证书(链),PEM 格式。"
msgid "Fetch Manually"
msgstr "手动获取"
msgid "Fetch Failed"
msgstr "获取失败"
msgid "ECH Config" msgid "ECH Config"
msgstr "ECH 配置" msgstr "ECH 配置"
@@ -2387,3 +2396,6 @@ msgstr "Gecko 包大小(最小)"
msgid "Gecko Packet Size (max)" msgid "Gecko Packet Size (max)"
msgstr "Gecko 包大小(最大)" msgstr "Gecko 包大小(最大)"
msgid "valid time (hh:mm)"
msgstr "有效时间(hh:mm)"
+12
View File
@@ -376,6 +376,9 @@ msgstr "負載均衡"
msgid "Node Addition Method" msgid "Node Addition Method"
msgstr "節點新增方式" msgstr "節點新增方式"
msgid "Backup Node Addition Method"
msgstr "備用節點新增方式"
msgid "Manual" msgid "Manual"
msgstr "手動" msgstr "手動"
@@ -1643,6 +1646,12 @@ msgstr "TLS 憑證(PEM)"
msgid "Full certificate (chain), PEM format." msgid "Full certificate (chain), PEM format."
msgstr "完整憑證(鏈),PEM 格式。" msgstr "完整憑證(鏈),PEM 格式。"
msgid "Fetch Manually"
msgstr "手動獲取"
msgid "Fetch Failed"
msgstr "獲取失敗"
msgid "ECH Config" msgid "ECH Config"
msgstr "ECH 配置" msgstr "ECH 配置"
@@ -2393,3 +2402,6 @@ msgstr "Gecko 包大小(最小)"
msgid "Gecko Packet Size (max)" msgid "Gecko Packet Size (max)"
msgstr "Gecko 包大小(最大)" msgstr "Gecko 包大小(最大)"
msgid "valid time (hh:mm)"
msgstr "有效時間(hh:mm)"
@@ -62,6 +62,10 @@ chmod +x /usr/share/passwall2/*.sh
[ "$(uci -q get passwall2.@global_xray[0].sniffing)" == "1" ] && [ "$(uci -q get passwall2.@global_xray[0].route_only)" != "1" ] && uci -q set passwall2.@global_xray[0].sniffing_override_dest=1 [ "$(uci -q get passwall2.@global_xray[0].sniffing)" == "1" ] && [ "$(uci -q get passwall2.@global_xray[0].route_only)" != "1" ] && uci -q set passwall2.@global_xray[0].sniffing_override_dest=1
uci -q delete passwall2.@global_xray[0].sniffing uci -q delete passwall2.@global_xray[0].sniffing
uci -q delete passwall2.@global_xray[0].route_only uci -q delete passwall2.@global_xray[0].route_only
frag_len=$(uci -q get passwall2.@global_xray[0].fragment_length)
[ -n "$frag_len" ] && uci -q set passwall2.@global_xray[0].fragment_lengths="$frag_len" && uci -q delete passwall2.@global_xray[0].fragment_length
frag_dly=$(uci -q get passwall2.@global_xray[0].fragment_delay)
[ -n "$frag_dly" ] && uci -q set passwall2.@global_xray[0].fragment_delays="$frag_dly" && uci -q delete passwall2.@global_xray[0].fragment_delay
uci -q commit passwall2 uci -q commit passwall2
sed -i "s#add_from#group#g" /etc/config/passwall2 2>/dev/null sed -i "s#add_from#group#g" /etc/config/passwall2 2>/dev/null
@@ -166,9 +166,8 @@ start() {
retry_num=$(config_n_get $id autoswitch_retry_num 1) retry_num=$(config_n_get $id autoswitch_retry_num 1)
restore_switch=$(config_n_get $id autoswitch_restore_switch 0) restore_switch=$(config_n_get $id autoswitch_restore_switch 0)
probe_url=$(config_n_get $id autoswitch_probe_url "https://www.google.com/generate_204") probe_url=$(config_n_get $id autoswitch_probe_url "https://www.google.com/generate_204")
backup_node=$(config_n_get $id autoswitch_backup_node) backup_node=$(lua_api "get_socks_backup_nodes(\"${id}\")")
if [ -n "$backup_node" ]; then if [ -n "$backup_node" ]; then
backup_node=$(echo "$backup_node" | tr -s ' ' '\n' | uniq | tr -s '\n' ' ')
backup_node_num=$(printf "%s\n" "$backup_node" | wc -w) backup_node_num=$(printf "%s\n" "$backup_node" | wc -w)
if [ "$backup_node_num" -eq 1 ]; then if [ "$backup_node_num" -eq 1 ]; then
[ "$main_node" = "$backup_node" ] && return [ "$main_node" = "$backup_node" ] && return
@@ -1368,6 +1368,10 @@ local function processData(szType, content, add_mode, group, sub_cfg)
result.reality_publicKey = params.pbk or nil result.reality_publicKey = params.pbk or nil
result.reality_shortId = params.sid or nil result.reality_shortId = params.sid or nil
end end
if params.ech and params.ech ~= "" then
result.ech = "1"
result.ech_config = params.ech
end
result.tls_pinSHA256 = params.pcs result.tls_pinSHA256 = params.pcs
result.tls_CertByName = params.vcn result.tls_CertByName = params.vcn
local insecure = params.allowinsecure or params.allowInsecure or params.insecure local insecure = params.allowinsecure or params.allowInsecure or params.insecure
+1 -1
View File
@@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
PKG_NAME:=luci-app-ssr-plus PKG_NAME:=luci-app-ssr-plus
PKG_VERSION:=196 PKG_VERSION:=196
PKG_RELEASE:=44 PKG_RELEASE:=45
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \ CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
@@ -499,7 +499,10 @@ Xray.outbounds = {
fingerprint = server.fingerprint, fingerprint = server.fingerprint,
allowInsecure = (function() allowInsecure = (function()
if server.tls_CertSha and server.tls_CertSha ~= "" then return nil end if server.tls_CertSha and server.tls_CertSha ~= "" then return nil end
return server.insecure == "1" if os.date("%Y.%m.%d") < "2026.06.01" then
return server.insecure == "1"
end
return nil
end)(), end)(),
serverName = server.tls_host, serverName = server.tls_host,
certificates = server.certificate and { certificates = server.certificate and {
+2 -2
View File
@@ -8,8 +8,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=netdata PKG_NAME:=netdata
PKG_VERSION:=2.10.3 PKG_VERSION:=2.10.4
PKG_RELEASE:=13 PKG_RELEASE:=14
PKG_MAINTAINER:=Josef Schlehofer <pepe.schlehofer@gmail.com>, Daniel Engberg <daniel.engberg.lists@pyret.net> PKG_MAINTAINER:=Josef Schlehofer <pepe.schlehofer@gmail.com>, Daniel Engberg <daniel.engberg.lists@pyret.net>
PKG_LICENSE:=GPL-3.0-or-later PKG_LICENSE:=GPL-3.0-or-later
+2 -2
View File
@@ -8,12 +8,12 @@ PKG_NAME:=alwaysonline
PKG_UPSTREAM_VERSION:=1.2.1 PKG_UPSTREAM_VERSION:=1.2.1
PKG_UPSTREAM_GITHASH:=206292ca68e4d6c81b215163a7bbd0cd2eb36860 PKG_UPSTREAM_GITHASH:=206292ca68e4d6c81b215163a7bbd0cd2eb36860
PKG_VERSION:=$(PKG_UPSTREAM_VERSION)~$(call version_abbrev,$(PKG_UPSTREAM_GITHASH)) PKG_VERSION:=$(PKG_UPSTREAM_VERSION)~$(call version_abbrev,$(PKG_UPSTREAM_GITHASH))
PKG_RELEASE:=8 PKG_RELEASE:=9
UCI_VERSION:=0.2025.01.25 UCI_VERSION:=0.2025.01.25
PKG_SOURCE_PROTO:=git PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/Jamesits/alwaysonline.git PKG_SOURCE_URL:=https://github.com/Jamesits/alwaysonline.git
PKG_SOURCE_VERSION:=0b7050013224cea1e9d4843817f9baa12b0bfe19 PKG_SOURCE_VERSION:=e5f20a72c8cf6a9ce0c5901e84dff18e96df5812
PKG_MIRROR_HASH:=skip PKG_MIRROR_HASH:=skip
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_UPSTREAM_VERSION) PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_UPSTREAM_VERSION)
+2 -1
View File
@@ -10,12 +10,13 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=tcptraceroute PKG_NAME:=tcptraceroute
PKG_REALVERSION:=1.5beta7 PKG_REALVERSION:=1.5beta7
PKG_VERSION:=1.5_beta7 PKG_VERSION:=1.5_beta7
PKG_RELEASE:=1 PKG_RELEASE:=2
PKG_SOURCE_PROTO:=git PKG_SOURCE_PROTO:=git
PKG_SOURCE_VERSION:=tcptraceroute-$(PKG_REALVERSION) PKG_SOURCE_VERSION:=tcptraceroute-$(PKG_REALVERSION)
PKG_SOURCE_URL:=https://github.com/mct/tcptraceroute.git PKG_SOURCE_URL:=https://github.com/mct/tcptraceroute.git
include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/package.mk
define Package/tcptraceroute define Package/tcptraceroute
+1 -1
View File
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=Xray-core PKG_NAME:=Xray-core
PKG_VERSION:=26.7.11 PKG_VERSION:=26.7.11
PKG_RELEASE:=19 PKG_RELEASE:=20
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/XTLS/Xray-core/tar.gz/v$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/XTLS/Xray-core/tar.gz/v$(PKG_VERSION)?
-24
View File
@@ -1,24 +0,0 @@
--- a/infra/conf/transport_internet.go
+++ b/infra/conf/transport_internet.go
@@ -14,7 +14,6 @@ import (
"strconv"
"strings"
"syscall"
- "time"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
@@ -699,12 +698,7 @@ func (c *TLSConfig) Build() (proto.Message, error) {
config.MasterKeyLog = c.MasterKeyLog
if c.AllowInsecure {
- if time.Now().After(time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)) {
- return nil, errors.PrintRemovedFeatureError(`"allowInsecure"`, `"pinnedPeerCertSha256"`)
- } else {
- errors.LogWarning(context.Background(), `"allowInsecure" will be removed automatically after 2026-06-01, please use "pinnedPeerCertSha256"(pcs) and "verifyPeerCertByName"(vcn) instead, PLEASE CONTACT YOUR SERVICE PROVIDER (AIRPORT)`)
- config.AllowInsecure = true
- }
+ config.AllowInsecure = true
}
if c.PinnedPeerCertSha256 != "" {
for v := range strings.SplitSeq(c.PinnedPeerCertSha256, ",") {
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=z8102 PKG_NAME:=z8102
PKG_VERSION:=0.2 PKG_VERSION:=0.2
PKG_RELEASE:=1 PKG_RELEASE:=2
PKG_MAINTAINER:=Yannick Chabanois <contact@openmptcprouter.com> PKG_MAINTAINER:=Yannick Chabanois <contact@openmptcprouter.com>
+9 -7
View File
@@ -12,13 +12,15 @@ log() {
modem_reset() modem_reset()
{ {
modemnb=$1 modemnb=$1
# sim if [ -z "$(cat /etc/board.json | grep z8802)" ]; then
if [ -e /sys/class/gpio/sim${modemnb}/value ]; then # sim
echo "0" > /sys/class/gpio/sim${modemnb}/value if [ -e /sys/class/gpio/sim${modemnb}/value ]; then
elif [ -f /usr/bin/gpiofind ]; then echo "0" > /sys/class/gpio/sim${modemnb}/value
gpioset `gpiofind "sim=${modemnb}"`=0 elif [ -f /usr/bin/gpiofind ]; then
else gpioset `gpiofind "sim=${modemnb}"`=0
gpioset -t0 sim${modemnb}=0 else
gpioset -t0 sim${modemnb}=0
fi
fi fi
# stop modem # stop modem