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

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

View File

@ -5,12 +5,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=cloudreve
PKG_VERSION:=4.17.0
PKG_RELEASE:=13
PKG_VERSION:=4.18.0
PKG_RELEASE:=14
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/cloudreve/Cloudreve.git
PKG_SOURCE_VERSION:=d4a145c3ac68f08b9071dec0f2ac025c6dc7362d
PKG_SOURCE_VERSION:=0bb0ab833571d380153edd3529e01a7957b8b4ce
PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-3.0-only

View File

@ -461,25 +461,26 @@ function renderListeners(s, uciconfig, isClient) {
o.depends('type', 'tuic');
o.modalonly = true;
/* Hysteria2 fields */
o = s.taboption('field_general', form.Value, 'hysteria_up_mbps', _('Max upload speed'),
/* Brutal fields */
o = s.taboption('field_general', form.Value, 'brutal_up_mbps', _('Max upload speed'),
_('In Mbps.'));
o.datatype = 'uinteger';
o.depends('type', 'hysteria2');
o.depends({type: /^(hysteria2|shadowquic)$/});
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.'));
o.datatype = 'uinteger';
o.depends('type', 'hysteria2');
o.depends({type: /^(hysteria2|shadowquic)$/});
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.'));
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;
/* Hysteria2 fields */
o = s.taboption('field_general', form.ListValue, 'hysteria_obfs_type', _('Obfuscate type'));
o.value('', _('Disable'));
o.value('salamander', _('Salamander'));
@ -638,6 +639,7 @@ function renderListeners(s, uciconfig, isClient) {
o = s.taboption('field_general', form.Flag, 'plugin', _('Plugin'));
o.default = o.disabled;
o.depends({type: /^(shadowsocks|snell)$/});
o.depends({type: /^(vmess|vless|trojan|anytls)$/});
o.modalonly = true;
o = s.taboption('field_plugin', form.ListValue, 'plugin_type', _('Plugin type'));
@ -651,9 +653,12 @@ function renderListeners(s, uciconfig, isClient) {
if (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'));
}
if (['vmess', 'vless', 'trojan', 'anytls'].includes(type) && !['jls'].includes(value)) {
return _('Expecting: Only support %s.').format(_('JLS'));
}
}
return true;
@ -1045,6 +1050,10 @@ function renderListeners(s, uciconfig, isClient) {
switch (type) {
case 'shadowsocks':
case 'vmess':
case 'vless':
case 'trojan':
case 'anytls':
def_alpn = ['h2', 'http/1.1']; // when plugin_type in ['jls']
break;
case 'tuic':
@ -1079,8 +1088,21 @@ function renderListeners(s, uciconfig, isClient) {
o = s.taboption('field_tls', form.Value, 'tls_cert_path', _('Certificate path'),
_('The %s public key, in PEM format.').format(_('Server')));
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.rmempty = false;
o.validate = function(section_id, value) {
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 = s.taboption('field_tls', form.Button, '_upload_cert', _('Upload certificate'),
@ -1181,6 +1203,15 @@ function renderListeners(s, uciconfig, isClient) {
// uTLS fields
o = s.taboption('field_tls', form.Flag, 'tls_reality', _('REALITY'));
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.modalonly = true;
@ -1243,14 +1274,14 @@ function renderListeners(s, uciconfig, isClient) {
switch (type) {
case 'vless':
if (!['grpc', 'ws', 'xhttp'].includes(value))
return _('Expecting: only support %s.').format(_('gRPC') +
return _('Expecting: Only support %s.').format(_('gRPC') +
' / ' + _('WebSocket') +
' / ' + _('XHTTP'));
break;
case 'vmess':
case 'trojan':
if (!['grpc', 'ws'].includes(value))
return _('Expecting: only support %s.').format(_('gRPC') +
return _('Expecting: Only support %s.').format(_('gRPC') +
' / ' + _('WebSocket'));
break;
default:

View File

@ -667,6 +667,19 @@ return view.extend({
so.depends('type', 'tuic');
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 */
so = ss.taboption('field_general', form.DynamicList, 'hysteria_ports', _('Ports pool'));
so.datatype = 'or(port, portrange)';
@ -679,18 +692,6 @@ return view.extend({
so.depends('type', 'hysteria2');
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.value('', _('Disable'));
so.value('salamander', _('Salamander'));
@ -716,17 +717,6 @@ return view.extend({
so.depends('hysteria_obfs_type', 'gecko');
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 */
so = ss.taboption('field_general', form.DynamicList, 'shadowquic_quic_versions', _('QUIC versions'),
_('Support %s, default %s.').format('v1/v2', 'v1'));
@ -752,6 +742,17 @@ return view.extend({
so.depends('type', 'shadowquic');
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 */
so = ss.taboption('field_general', form.Value, 'wireguard_ip', _('Local address'),
_('The %s address used by local machine in the Wireguard network.').format('IPv4'));
@ -982,7 +983,7 @@ return view.extend({
if (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'));
}
}
@ -1166,6 +1167,14 @@ return view.extend({
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.'));
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: /^(tuic)$/, tls_disable_sni: '0'});
so.modalonly = true;
@ -1300,9 +1309,24 @@ return view.extend({
so.depends({type: 'ss', plugin_type: 'jls'});
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.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 = ss.taboption('field_tls', form.Value, 'tls_reality_public_key', _('REALITY public key'));
@ -1344,14 +1368,14 @@ return view.extend({
switch (type) {
case 'vmess':
if (!['http', 'h2', 'grpc', 'ws'].includes(value))
return _('Expecting: only support %s.').format(_('HTTP') +
return _('Expecting: Only support %s.').format(_('HTTP') +
' / ' + _('HTTPUpgrade') +
' / ' + _('gRPC') +
' / ' + _('WebSocket'));
break;
case 'vless':
if (!['http', 'h2', 'grpc', 'ws', 'xhttp'].includes(value))
return _('Expecting: only support %s.').format(_('HTTP') +
return _('Expecting: Only support %s.').format(_('HTTP') +
' / ' + _('HTTPUpgrade') +
' / ' + _('gRPC') +
' / ' + _('WebSocket') +
@ -1359,7 +1383,7 @@ return view.extend({
break;
case 'trojan':
if (!['grpc', 'ws'].includes(value))
return _('Expecting: only support %s.').format(_('gRPC') +
return _('Expecting: Only support %s.').format(_('gRPC') +
' / ' + _('WebSocket'));
break;
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

View File

@ -25,6 +25,30 @@ migrate() {
uci_set "$CONF" "$1" max_idle_time "$tuic_max_idle_time"
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
if isDefined "$1" plugin; then
local plugin

View File

@ -59,6 +59,22 @@ migrate() {
uci_set "$CONF" "$1" max_open_streams "$tuic_max_open_streams"
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
if isDefined "$1" plugin; then
local plugin

View File

@ -33,6 +33,30 @@ migrate() {
uci_set "$CONF" "$1" max_idle_time "$tuic_max_idle_time"
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
if isDefined "$1" plugin; then
local plugin

View File

@ -573,11 +573,13 @@ uci.foreach(uciconf, ucinode, (cfg) => {
"reduce-rtt": strToBool(cfg.tuic_reduce_rtt),
"request-timeout": strToInt(cfg.tuic_request_timeout) || null,
/* Brutal */
up: strToInt(cfg.brutal_up_mbps),
down: strToInt(cfg.brutal_down_mbps),
/* Hysteria / Hysteria2 */
ports: isEmpty(cfg.hysteria_ports) ? null : join(',', cfg.hysteria_ports),
"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-password": cfg.hysteria_obfs_password,
"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,
"query-server-name": cfg.tls_ech_query_server_name
} : null,
"jls-opts": cfg.tls_jls === '1' ? {
"username": cfg.tls_jls_username,
"password": cfg.tls_jls_password
} : null,
"reality-opts": cfg.tls_reality === '1' ? {
"public-key": cfg.tls_reality_public_key,
"short-id": cfg.tls_reality_short_id,

View File

@ -292,10 +292,12 @@ export function parseListener(cfg) {
"authentication-timeout": durationToSecond(cfg.tuic_authentication_timeout),
"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 */
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-password": cfg.hysteria_obfs_password,
"obfs-min-packet-size": strToInt(cfg.hysteria_obfs_min_packet_size),
@ -417,7 +419,7 @@ export function parseListener(cfg) {
/* TLS fields */
...(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' ? {
"reality-config": {
dest: cfg.tls_reality_dest,

View File

@ -7,8 +7,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.7.1
PKG_RELEASE:=179
PKG_VERSION:=26.7.15
PKG_RELEASE:=180
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \

View File

@ -441,24 +441,15 @@ o = s:option(Value, _n("tls_serverName"), "SNI " .. translate("Domain"))
o:depends({ [_n("tls")] = true })
o:depends({ [_n("protocol")] = "hysteria2" })
if api.compare_versions(os.date("%Y.%m.%d"), "<", "3026.6.1") then
o = s:option(Flag, _n("tls_allowInsecure"), translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped."))
o.default = "0"
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria2" })
end
o = s:option(Value, _n("tls_pinSHA256"), translate("TLS Chain Fingerprint (SHA256)"))
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria2" })
o.description = translate("Once set, connects only when the servers chain fingerprint matches.") ..
string.format("<a href='javascript:void(0)' onclick='javascript:fetchCertSha256(this)'>%s</a>", "" .. translate("Fetch Manually"))
if api.compare_versions(xray_version, ">=", "26.1.31") then
o = s:option(Value, _n("tls_pinSHA256"), translate("TLS Chain Fingerprint (SHA256)"))
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria2" })
o.description = translate("Once set, connects only when the servers 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(Value, _n("tls_CertByName"), translate("TLS Certificate Name (CertName)"), translate("TLS is used to verify the leaf certificate name."))
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Flag, _n("tls_certificate"), translate("TLS Certificate (PEM)"))
o.default = "0"

View File

@ -152,21 +152,9 @@ function gen_outbound(flag, node, tag, proxy_table)
security = node.stream_security,
tlsSettings = (node.stream_security == "tls") and {
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,
pinnedPeerCertSha256 = (function()
if api.compare_versions(xray_version, "<", "26.1.31") then return nil end
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)(),
pinnedPeerCertSha256 = node.tls_pinSHA256,
verifyPeerCertByName = node.tls_CertByName,
echConfigList = (node.ech == "1") and node.ech_config or nil,
certificates = (node.tls_certificate == "1" and node.tls_certificate_pem ~= "") and {
certificate = api.split(node.tls_certificate_pem, "\n"),
@ -820,7 +808,8 @@ function gen_config_server(node)
serverNames = node.reality_serverNames or {},
privateKey = node.reality_private_key,
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
end
end

View File

@ -1313,6 +1313,8 @@ local current_node = map:get(section)
opt.set(dom_prefix + 'utls', true);
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_CertByName', queryParam.vcn || '');
opt.set(

View File

@ -1069,6 +1069,10 @@ local function processData(szType, content, add_mode, group, sub_cfg)
result.reality_publicKey = params.pbk or nil
result.reality_shortId = params.sid or nil
end
if params.ech and params.ech ~= "" then
result.ech = "1"
result.ech_config = params.ech
end
result.tls_pinSHA256 = params.pcs
result.tls_CertByName = params.vcn
result.tls_allowInsecure = params.allowinsecure or params.allowInsecure or params.insecure

View File

@ -6,8 +6,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.7.12
PKG_RELEASE:=62
PKG_VERSION:=26.7.16
PKG_RELEASE:=63
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \

View File

@ -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)'));
}
});
}

View File

@ -114,6 +114,8 @@ function index()
--[[geoview]]
entry({"admin", "services", appname, "geo_view"}, call("geo_view")).leaf = true
entry({"admin", "services", appname, "fetch_certsha256"}, call("fetch_certsha256")).leaf = true
end
function check_site(host, port)
@ -995,3 +997,17 @@ function flush_set()
http.redirect(api.url("log"))
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

View File

@ -2,6 +2,8 @@ local api = require "luci.passwall2.api"
local appname = api.appname
local sys = api.sys
api.set_default_cbi()
m = Map(appname)
api.set_apply_on_parse(m)
@ -13,7 +15,8 @@ o.rmempty = false
o.default = false
-- [[ ACLs Settings ]]--
s = m:section(TypedSection, "acl_rule")
local cfgname = "acl_rule"
s = m:section(TypedSection, cfgname)
s.template = "cbi/tblsection"
s.sortable = true
s.anonymous = true
@ -65,4 +68,9 @@ i.cfgvalue = function(t, n)
return v
end
return m
local sortable = Template(appname .. "/cbi/sortable")
sortable.api = api
sortable.target_cfgname = cfgname
m:append(sortable)
return api.return_map(m)

View File

@ -1,6 +1,8 @@
local api = require "luci.passwall2.api"
local appname = api.appname
api.set_default_cbi()
m = Map(appname)
api.set_apply_on_parse(m)
@ -379,4 +381,4 @@ for k, v in pairs(nodes_table) do
end
end
return m
return api.return_map(m)

View File

@ -2,6 +2,8 @@ local api = require "luci.passwall2.api"
local com = require "luci.passwall2.com"
local appname = api.appname
api.set_default_cbi()
m = Map(appname)
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."))
end
return m
return api.return_map(m)

View File

@ -1,5 +1,5 @@
local api = require "luci.passwall2.api"
local appname = "passwall2"
local appname = api.appname
local fs = api.fs
local uci = api.uci

View File

@ -4,6 +4,8 @@ datatypes = api.datatypes
has_singbox = api.finded_com("sing-box")
has_xray = api.finded_com("xray")
api.set_default_cbi()
m = Map(appname)
api.set_apply_on_parse(m)
@ -387,4 +389,4 @@ footer.shunt_list = api.jsonc.stringify(shunt_list)
m:append(footer)
m:append(Template(appname .. "/global/status_bottom"))
return m
return api.return_map(m)

View File

@ -15,6 +15,8 @@ for k, e in ipairs(api.get_valid_nodes()) do
end
end
api.set_default_cbi()
m = Map(appname)
api.set_apply_on_parse(m)
@ -100,7 +102,8 @@ end
o:depends("health_check_type", "passwall_logic")
-- [[ 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'>" ..
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>"
@ -176,6 +179,11 @@ o:value(0, translate("Primary"))
o:value(1, translate("Standby"))
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"))
return m
return api.return_map(m)

View File

@ -1,6 +1,8 @@
api = require "luci.passwall2.api"
appname = api.appname
api.set_default_cbi()
m = Map(appname, translate("Node Config"))
m.redirect = api.url()
api.set_apply_on_parse(m)
@ -94,7 +96,6 @@ local footer = Template(appname .. "/node_config/footer")
footer.api = api
footer.config = m.config
footer.section = arg[1]
m:append(footer)
return m
return api.return_map(m)

View File

@ -3,6 +3,8 @@ local appname = api.appname
local datatypes = api.datatypes
local sys = api.sys
api.set_default_cbi()
m = Map(appname)
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"))
return m
return api.return_map(m)

View File

@ -38,6 +38,8 @@ if has_hysteria2 then
table.insert(hysteria2_type, s)
end
api.set_default_cbi()
m = Map(appname)
api.set_apply_on_parse(m)
@ -124,7 +126,8 @@ o.cfgvalue = function(self, section)
translate("Manual subscription All"))
end
s = m:section(TypedSection, "subscribe_list", "", "<font color='red'>" .. translate("When adding a new subscription, please save and apply before manually subscribing. If you only change the subscription URL, you can subscribe manually, and the system will save it automatically.") .. "</font>")
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.anonymous = true
s.sortable = true
@ -144,7 +147,7 @@ o.validate = function(self, value, section)
return nil, translate("Remark cannot be empty.")
end
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
duplicate = true
return false
@ -219,6 +222,11 @@ o.cfgvalue = function(self, section)
section, translate("Manual subscription"))
end
local sortable = Template(appname .. "/cbi/sortable")
sortable.api = api
sortable.target_cfgname = cfgname
m:append(sortable)
m:append(Template(appname .. "/node_subscribe/js"))
return m
return api.return_map(m)

View File

@ -1,6 +1,8 @@
local api = require "luci.passwall2.api"
local appname = api.appname
api.set_default_cbi()
m = Map(appname)
m.redirect = api.url("node_subscribe")
api.set_apply_on_parse(m)
@ -255,13 +257,7 @@ for t = 0, 23 do
end
end
o.default = "0:00"
o.validate = function(self, value)
local b = api.is_timehhmm(value)
if b then
return value
end
return nil
end
o.datatype = "timehhmm"
o:depends("update_week_mode", "0")
o:depends("update_week_mode", "1")
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("This may reveal limited hardware information. Enable only if you trust your provider.")
return m
return api.return_map(m)

View File

@ -8,6 +8,8 @@ local port_validate = function(self, value, t)
return value:gsub("-", ":")
end
api.set_default_cbi()
m = Map(appname)
api.set_apply_on_parse(m)
@ -53,13 +55,7 @@ for index, value in ipairs({"stop", "start", "restart"}) do
end
end
o.default = "0:00"
o.validate = function(self, value)
local b = api.is_timehhmm(value)
if b then
return value
end
return nil
end
o.datatype = "timehhmm"
o:depends(value .. "_week_mode", "0")
o:depends(value .. "_week_mode", "1")
o:depends(value .. "_week_mode", "2")
@ -312,4 +308,4 @@ if has_singbox then
o.default = 0
end
return m
return api.return_map(m)

View File

@ -1,6 +1,8 @@
local api = require "luci.passwall2.api"
local appname = api.appname
api.set_default_cbi()
m = Map(appname)
api.set_apply_on_parse(m)
@ -59,13 +61,7 @@ for t = 0, 23 do
end
end
o.default = "0:00"
o.validate = function(self, value)
local b = api.is_timehhmm(value)
if b then
return value
end
return nil
end
o.datatype = "timehhmm"
o:depends("update_week_mode", "0")
o:depends("update_week_mode", "1")
o:depends("update_week_mode", "2")
@ -92,7 +88,8 @@ end
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.anonymous = false
s.addremove = true
@ -113,4 +110,9 @@ end
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)

View File

@ -2,6 +2,8 @@ local api = require "luci.passwall2.api"
local appname = api.appname
local datatypes = api.datatypes
api.set_default_cbi()
m = Map(appname, "Sing-Box/Xray " .. translate("Shunt Rule"))
m.redirect = api.url("rule")
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."))
return m
return api.return_map(m)

View File

@ -1,6 +1,8 @@
local api = require "luci.passwall2.api"
local appname = api.appname
api.set_default_cbi()
m = Map(appname)
api.set_apply_on_parse(m)
@ -80,23 +82,14 @@ o = s:option(Flag, "enable_autoswitch", translate("Auto Switch"))
o.default = 0
o.rmempty = false
o = s:option(Value, "autoswitch_testing_time", translate("How often to test"), translate("Units:seconds"))
o.datatype = "min(10)"
o.default = 30
o = s:option(ListValue, "backup_node_add_mode", translate("Backup Node Addition Method"))
o:depends("enable_autoswitch", true)
o.default = "manual"
o:value("manual", translate("Manual"))
o:value("batch", translate("Batch"))
o = s:option(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:depends("enable_autoswitch", true)
o:depends("backup_node_add_mode", "manual")
o.template = appname .. "/cbi/nodes_dynamiclist"
o.group = {}
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")
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:depends("enable_autoswitch", true)
@ -134,6 +165,6 @@ o:depends("enable_autoswitch", true)
o = s:option(DummyValue, "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)

View File

@ -442,22 +442,15 @@ o = s:option(Value, _n("tls_serverName"), "SNI " .. translate("Domain"))
o:depends({ [_n("tls")] = true })
o:depends({ [_n("protocol")] = "hysteria2" })
if api.compare_versions(os.date("%Y.%m.%d"), "<", "3026.6.1") then
o = s:option(Flag, _n("tls_allowInsecure"), translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped."))
o.default = "0"
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria2" })
end
o = s:option(Value, _n("tls_pinSHA256"), translate("TLS Chain Fingerprint (SHA256)"))
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria2" })
o.description = translate("Once set, connects only when the servers chain fingerprint matches.") ..
string.format("<a href='javascript:void(0)' onclick='javascript:fetchCertSha256(this)'>%s</a>", "" .. translate("Fetch Manually"))
if api.compare_versions(xray_version, ">=", "26.1.31") then
o = s:option(Value, _n("tls_pinSHA256"), translate("TLS Chain Fingerprint (SHA256)"))
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Value, _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(Value, _n("tls_CertByName"), translate("TLS Certificate Name (CertName)"), translate("TLS is used to verify the leaf certificate name."))
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Flag, _n("tls_certificate"), translate("TLS Certificate (PEM)"))
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.rows = 10
o.wrap = "off"
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
local v = o_validate(self, value)
if v then return v end
return nil, "XHTTP Extra " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local raw = m:get(section, "xhttp_extra")
if raw then
@ -693,14 +694,6 @@ o.custom_write = function(self, section, value)
m:del(section, "download_address")
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)
m:del(section, "xhttp_extra")
m:del(section, "download_address")
@ -758,6 +751,14 @@ o.rows = 10
o.wrap = "off"
o.description = translate("An FinalMaskObject in JSON format, used for sharing.") .. "<br>" ..
translate("Custom finalmask overrides mkcp, hysteria2, fragment, noise, and related settings.")
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
local v = o_validate(self, value)
if v then return v end
return nil, "FinalMask " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local raw = m:get(section, "finalmask")
if raw then
@ -767,14 +768,6 @@ end
o.custom_write = function(self, section, value)
m:set(section, "finalmask", api.base64Encode(value) or "")
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]]
o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required"))

View File

@ -1,4 +1,7 @@
local api = require "luci.passwall2.api"
local appname = api.appname
api.set_default_cbi()
m = Map("passwall2_server", translate("Server-Side"))
api.set_apply_on_parse(m)
@ -10,7 +13,8 @@ t.addremove = false
e = t:option(Flag, "enable", translate("Enable"))
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.addremove = true
t.sortable = true
@ -85,8 +89,14 @@ e = t:option(Flag, "log", translate("Log"))
e.default = "1"
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"))
return m
m:append(Template(appname .. "/server/log"))
m:append(Template(appname .. "/server/users_list_status"))
return api.return_map(m)

View File

@ -129,12 +129,12 @@ o = s:option(TextValue, _n("custom_config"), translate("Custom Config"))
o.rows = 10
o.wrap = "off"
o:depends({ [_n("custom")] = true })
o.validate = function(self, value, t)
if value and api.jsonc.parse(value) then
return value
else
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
local v = o_validate(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str")

View File

@ -412,6 +412,7 @@ o = s:option(TextValue, _n("finalmask"), "FinalMask JSON")
o:depends({ [_n("use_finalmask")] = true })
o.rows = 10
o.wrap = "off"
o.datatype = "json"
o.custom_cfgvalue = function(self, section, value)
local raw = m:get(section, "finalmask")
if raw then
@ -421,14 +422,6 @@ end
o.custom_write = function(self, section, value)
m:set(section, "finalmask", api.base64Encode(value) or "")
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]]
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)
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.wrap = "off"
o:depends({ [_n("custom")] = true })
o.validate = function(self, value, t)
if value and api.jsonc.parse(value) then
return value
else
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
local v = o_validate(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str")

View File

@ -489,16 +489,16 @@ for _, d in ipairs(netdev_list) do
o:value(d.name, d.label)
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.wrap = "off"
o:depends({ [_n("custom")] = true })
o.validate = function(self, value, t)
if value and api.jsonc.parse(value) then
return value
else
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
local v = o_validate(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str")

View File

@ -49,16 +49,16 @@ o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"))
o.default = "0"
o:depends({ [_n("custom")] = false })
o = s:option(TextValue, _n("custom_config"), translate("Custom Config"))
o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)")
o.rows = 10
o.wrap = "off"
o:depends({ [_n("custom")] = true })
o.validate = function(self, value, t)
if value and api.jsonc.parse(value) then
return value
else
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
local v = o_validate(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str")

View File

@ -76,16 +76,16 @@ o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"))
o.default = "0"
o:depends({ [_n("custom")] = false })
o = s:option(TextValue, _n("custom_config"), translate("Custom Config"))
o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)")
o.rows = 10
o.wrap = "off"
o:depends({ [_n("custom")] = true })
o.validate = function(self, value, t)
if value and api.jsonc.parse(value) then
return value
else
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
local v = o_validate(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str")

View File

@ -2,6 +2,8 @@ api = require "luci.passwall2.api"
appname = api.appname
fs = api.fs
api.set_default_cbi()
m = Map("passwall2_server", translate("Server Config"))
m.redirect = api.url("server")
api.set_apply_on_parse(m)
@ -54,4 +56,4 @@ footer.config = m.config
footer.section = arg[1]
m:append(footer)
return m
return api.return_map(m)

View File

@ -367,8 +367,16 @@ function strToTable(str)
return loadstring("return " .. str)()
end
function is_timehhmm(timeStr)
local hour, minute = string.match(timeStr, "^(%d?%d):(%d%d)$")
function is_json(str)
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
hour = tonumber(hour)
minute = tonumber(minute)
@ -378,6 +386,7 @@ function is_timehhmm(timeStr)
end
return false
end
datatypes.timehhmm = is_timehhmm
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
@ -1380,6 +1389,40 @@ function set_apply_on_parse(map)
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)
local fv_type
local field_type = s.fields["type"]
@ -1585,6 +1628,56 @@ function match_node_rule(name, rule)
return false
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)
local v = uci:get(appname, "@global_subscribe[0]", field)
if v and v ~= "" then
@ -1610,6 +1703,26 @@ function cleanEmptyTables(t)
return next(t) and t or nil
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()
local dnsmasq_server = uci:get("dhcp", "@dnsmasq[0]", "server")
local dnsmasq_server_t = {}

View File

@ -1329,33 +1329,6 @@ function gen_config(var)
return result
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)
if not node_id or node_id == "" or node_id == "nil" then return nil end
if node_id:find("Socks_") then
@ -1377,7 +1350,7 @@ function gen_config(var)
-- new urltest
local ut_nodes
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
ut_nodes = _node.urltest_node
end

View File

@ -176,21 +176,9 @@ function gen_outbound(flag, node, tag, proxy_table)
security = node.stream_security,
tlsSettings = (node.stream_security == "tls") and {
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,
pinnedPeerCertSha256 = (function()
if api.compare_versions(xray_version, "<", "26.1.31") then return nil end
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)(),
pinnedPeerCertSha256 = node.tls_pinSHA256,
verifyPeerCertByName = node.tls_CertByName,
echConfigList = (node.ech == "1") and node.ech_config or nil,
certificates = (node.tls_certificate == "1" and node.tls_certificate_pem ~= "") and {
certificate = api.split(node.tls_certificate_pem, "\n"),
@ -839,7 +827,8 @@ function gen_config_server(node)
serverNames = node.reality_serverNames or {},
privateKey = node.reality_private_key,
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
end
end
@ -1032,33 +1021,6 @@ function gen_config(var)
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)
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"
@ -1090,7 +1052,7 @@ function gen_config(var)
-- new balancer
local blc_nodes
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
blc_nodes = _node.balancing_node
end

View File

@ -0,0 +1 @@
<!-- map footer -->

View File

@ -0,0 +1,2 @@
<!-- map header -->
<script src="<%=resource%>/view/<%=self.api.appname%>/cbi.js"></script>

View File

@ -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>

View File

@ -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%>

View File

@ -2,46 +2,8 @@
local api = require "luci.passwall2.api"
local appname = api.appname
-%>
<script src="<%=resource%>/view/<%=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%>-haproxy_config .cbi-section-table-titles {
display: none !important;
}
}
@media screen and (min-width: 1152px) {
#cbi-<%=appname%>-haproxy_config input[id*=".<%=appname%>."][id$=".haproxy_port"],
#cbi-<%=appname%>-haproxy_config input[id*=".<%=appname%>."][id$=".lbweight"] {
@ -110,129 +72,5 @@ table .cbi-button-down,
}
}, 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>

View File

@ -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>

View File

@ -1314,6 +1314,8 @@ local current_node = map:get(section)
opt.set(dom_prefix + 'utls', true);
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_CertByName', queryParam.vcn || '');
opt.set(

View File

@ -635,11 +635,15 @@ table td, .table .td {
if (cacheData && cacheData.savetime && (now - cacheData.timestamp) < cacheData.savetime) {
const a = ping_value[i].firstElementChild;
let color = "red";
if (cacheData.value < 100)
color = "green";
else if (cacheData.value < 200)
color = "#fb9a05";
a.innerText = cacheData.value + " ms";
if (cacheData.value >= 0) {
a.innerText = cacheData.value + " ms";
if (cacheData.value < 100)
color = "green";
else if (cacheData.value < 200)
color = "#fb9a05";
} else {
a.innerText = "<%:Timeout%>";
}
a.style.color = color;
a.onclick = function () {
ping_node(cbi_id, this, auto_detection_time);
@ -679,15 +683,23 @@ table td, .table .td {
const cache_data = {
dom_id: strs[i],
timestamp: Date.now(),
savetime: 60 * 1000,
savetime: 150 * 1000,
value: ping
};
text = result.ping + " ms";
localStorage.setItem(auto_detection_time + ":" + dom.address + ":" + dom.port, JSON.stringify(cache_data));
text = result.ping + " ms";
if (ping < 100)
color = "green";
else if (ping < 200)
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.style.color = color;
@ -1101,6 +1113,15 @@ table td, .table .td {
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>

View File

@ -1,46 +1,6 @@
<%
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">
//<![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>

View File

@ -4,44 +4,12 @@ local fs = api.fs
local has_old_geoip = fs.access("/tmp/bak_v2ray/geoip.dat")
local has_old_geosite = fs.access("/tmp/bak_v2ray/geosite.dat")
-%>
<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;
}
div.cbi-value[id$="-geoip_update"],
div.cbi-value[id$="-geosite_update"] {
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>
<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>

View File

@ -379,6 +379,9 @@ msgstr "توزیع بار"
msgid "Node Addition Method"
msgstr "روش افزودن گره"
msgid "Backup Node Addition Method"
msgstr "روش افزودن گره پشتیبان"
msgid "Manual"
msgstr "دستی"
@ -1651,6 +1654,12 @@ msgstr "گواهی TLS (PEM)"
msgid "Full certificate (chain), PEM format."
msgstr "گواهی کامل (زنجیره‌ای)، فرمت PEM."
msgid "Fetch Manually"
msgstr "دریافت دستی"
msgid "Fetch Failed"
msgstr "واکشی ناموفق"
msgid "ECH Config"
msgstr "پیکربندی ECH"
@ -2400,4 +2409,7 @@ msgid "Gecko Packet Size (min)"
msgstr "اندازه بسته Gecko (دقیقه)"
msgid "Gecko Packet Size (max)"
msgstr "اندازه بسته Gecko (حداکثر)"
msgstr "اندازه بسته Gecko (حداکثر)"
msgid "valid time (hh:mm)"
msgstr "زمان معتبر (ساعت:میلی‌متر)"

View File

@ -376,6 +376,9 @@ msgstr "Балансировка"
msgid "Node Addition Method"
msgstr "Способ добавления узлов"
msgid "Backup Node Addition Method"
msgstr "Метод добавления резервного узла"
msgid "Manual"
msgstr "Вручную"
@ -1652,6 +1655,12 @@ msgstr "Сертификат TLS (PEM)"
msgid "Full certificate (chain), PEM format."
msgstr "Полный сертификат (цепочка), формат PEM."
msgid "Fetch Manually"
msgstr "Получить вручную"
msgid "Fetch Failed"
msgstr "Не удалось получить данные."
msgid "ECH Config"
msgstr "Конфигурация ECH"
@ -2399,3 +2408,6 @@ msgstr "Размер упаковки Gecko (мин)"
msgid "Gecko Packet Size (max)"
msgstr "Размер упаковки Gecko (макс)"
msgid "valid time (hh:mm)"
msgstr "Время действия (чч:мм)"

View File

@ -376,6 +376,9 @@ msgstr "负载均衡"
msgid "Node Addition Method"
msgstr "节点添加方式"
msgid "Backup Node Addition Method"
msgstr "后备节点添加方式"
msgid "Manual"
msgstr "手动"
@ -1643,6 +1646,12 @@ msgstr "TLS 证书PEM"
msgid "Full certificate (chain), PEM format."
msgstr "完整证书PEM 格式。"
msgid "Fetch Manually"
msgstr "手动获取"
msgid "Fetch Failed"
msgstr "获取失败"
msgid "ECH Config"
msgstr "ECH 配置"
@ -2387,3 +2396,6 @@ msgstr "Gecko 包大小(最小)"
msgid "Gecko Packet Size (max)"
msgstr "Gecko 包大小(最大)"
msgid "valid time (hh:mm)"
msgstr "有效时间hh:mm"

View File

@ -376,6 +376,9 @@ msgstr "負載均衡"
msgid "Node Addition Method"
msgstr "節點新增方式"
msgid "Backup Node Addition Method"
msgstr "備用節點新增方式"
msgid "Manual"
msgstr "手動"
@ -1643,6 +1646,12 @@ msgstr "TLS 憑證PEM"
msgid "Full certificate (chain), PEM format."
msgstr "完整憑證PEM 格式。"
msgid "Fetch Manually"
msgstr "手動獲取"
msgid "Fetch Failed"
msgstr "獲取失敗"
msgid "ECH Config"
msgstr "ECH 配置"
@ -2393,3 +2402,6 @@ msgstr "Gecko 包大小(最小)"
msgid "Gecko Packet Size (max)"
msgstr "Gecko 包大小(最大)"
msgid "valid time (hh:mm)"
msgstr "有效時間hh:mm"

View File

@ -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 delete passwall2.@global_xray[0].sniffing
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
sed -i "s#add_from#group#g" /etc/config/passwall2 2>/dev/null

View File

@ -166,9 +166,8 @@ start() {
retry_num=$(config_n_get $id autoswitch_retry_num 1)
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")
backup_node=$(config_n_get $id autoswitch_backup_node)
backup_node=$(lua_api "get_socks_backup_nodes(\"${id}\")")
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)
if [ "$backup_node_num" -eq 1 ]; then
[ "$main_node" = "$backup_node" ] && return

View File

@ -1368,6 +1368,10 @@ local function processData(szType, content, add_mode, group, sub_cfg)
result.reality_publicKey = params.pbk or nil
result.reality_shortId = params.sid or nil
end
if params.ech and params.ech ~= "" then
result.ech = "1"
result.ech_config = params.ech
end
result.tls_pinSHA256 = params.pcs
result.tls_CertByName = params.vcn
local insecure = params.allowinsecure or params.allowInsecure or params.insecure

View File

@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
LUCI_PKGARCH:=all
PKG_NAME:=luci-app-ssr-plus
PKG_VERSION:=196
PKG_RELEASE:=44
PKG_RELEASE:=45
PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \

View File

@ -499,7 +499,10 @@ Xray.outbounds = {
fingerprint = server.fingerprint,
allowInsecure = (function()
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)(),
serverName = server.tls_host,
certificates = server.certificate and {

View File

@ -8,8 +8,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=netdata
PKG_VERSION:=2.10.3
PKG_RELEASE:=13
PKG_VERSION:=2.10.4
PKG_RELEASE:=14
PKG_MAINTAINER:=Josef Schlehofer <pepe.schlehofer@gmail.com>, Daniel Engberg <daniel.engberg.lists@pyret.net>
PKG_LICENSE:=GPL-3.0-or-later

View File

@ -8,12 +8,12 @@ PKG_NAME:=alwaysonline
PKG_UPSTREAM_VERSION:=1.2.1
PKG_UPSTREAM_GITHASH:=206292ca68e4d6c81b215163a7bbd0cd2eb36860
PKG_VERSION:=$(PKG_UPSTREAM_VERSION)~$(call version_abbrev,$(PKG_UPSTREAM_GITHASH))
PKG_RELEASE:=8
PKG_RELEASE:=9
UCI_VERSION:=0.2025.01.25
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/Jamesits/alwaysonline.git
PKG_SOURCE_VERSION:=0b7050013224cea1e9d4843817f9baa12b0bfe19
PKG_SOURCE_VERSION:=e5f20a72c8cf6a9ce0c5901e84dff18e96df5812
PKG_MIRROR_HASH:=skip
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_UPSTREAM_VERSION)

View File

@ -10,12 +10,13 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=tcptraceroute
PKG_REALVERSION:=1.5beta7
PKG_VERSION:=1.5_beta7
PKG_RELEASE:=1
PKG_RELEASE:=2
PKG_SOURCE_PROTO:=git
PKG_SOURCE_VERSION:=tcptraceroute-$(PKG_REALVERSION)
PKG_SOURCE_URL:=https://github.com/mct/tcptraceroute.git
include $(INCLUDE_DIR)/package.mk
define Package/tcptraceroute

View File

@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=Xray-core
PKG_VERSION:=26.7.11
PKG_RELEASE:=19
PKG_RELEASE:=20
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/XTLS/Xray-core/tar.gz/v$(PKG_VERSION)?

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, ",") {

View File

@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=z8102
PKG_VERSION:=0.2
PKG_RELEASE:=1
PKG_RELEASE:=2
PKG_MAINTAINER:=Yannick Chabanois <contact@openmptcprouter.com>

View File

@ -12,13 +12,15 @@ log() {
modem_reset()
{
modemnb=$1
# sim
if [ -e /sys/class/gpio/sim${modemnb}/value ]; then
echo "0" > /sys/class/gpio/sim${modemnb}/value
elif [ -f /usr/bin/gpiofind ]; then
gpioset `gpiofind "sim=${modemnb}"`=0
else
gpioset -t0 sim${modemnb}=0
if [ -z "$(cat /etc/board.json | grep z8802)" ]; then
# sim
if [ -e /sys/class/gpio/sim${modemnb}/value ]; then
echo "0" > /sys/class/gpio/sim${modemnb}/value
elif [ -f /usr/bin/gpiofind ]; then
gpioset `gpiofind "sim=${modemnb}"`=0
else
gpioset -t0 sim${modemnb}=0
fi
fi
# stop modem