diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile
index 79fdd5ee..6a80b3bb 100644
--- a/luci-app-passwall2/Makefile
+++ b/luci-app-passwall2/Makefile
@@ -6,7 +6,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2
-PKG_VERSION:=26.7.12
+PKG_VERSION:=26.7.16
PKG_RELEASE:=1
PKG_PO_VERSION:=$(PKG_VERSION)
diff --git a/luci-app-passwall2/htdocs/luci-static/resources/view/passwall2/cbi.js b/luci-app-passwall2/htdocs/luci-static/resources/view/passwall2/cbi.js
new file mode 100644
index 00000000..3f9d6470
--- /dev/null
+++ b/luci-app-passwall2/htdocs/luci-static/resources/view/passwall2/cbi.js
@@ -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)'));
+ }
+ });
+}
diff --git a/luci-app-passwall2/luasrc/controller/passwall2.lua b/luci-app-passwall2/luasrc/controller/passwall2.lua
index a2bc949a..1fac463a 100644
--- a/luci-app-passwall2/luasrc/controller/passwall2.lua
+++ b/luci-app-passwall2/luasrc/controller/passwall2.lua
@@ -113,6 +113,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
local function http_write_json(content)
@@ -939,3 +941,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
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl.lua
index 3df0c9f7..0b815a51 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl.lua
@@ -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)
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl_config.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl_config.lua
index b8b77501..b366d623 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl_config.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl_config.lua
@@ -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)
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/app_update.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/app_update.lua
index afdef1b9..462046e3 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/app_update.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/app_update.lua
@@ -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('%s', 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)
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/geoview.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/geoview.lua
index 071d8e80..abd9c1f1 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/geoview.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/geoview.lua
@@ -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
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/global.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/global.lua
index 6195b0bd..7e0e4a82 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/global.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/global.lua
@@ -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)
@@ -386,4 +388,4 @@ footer.shunt_list = api.jsonc.stringify(shunt_list)
m:append(footer)
-return m
+return api.return_map(m)
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/haproxy.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/haproxy.lua
index 6a8732ee..06dad374 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/haproxy.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/haproxy.lua
@@ -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 = "" ..
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.") .. "
" ..
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!") .. ""
@@ -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)
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_config.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_config.lua
index 5051bd7b..ead7043f 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_config.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_config.lua
@@ -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)
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_list.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_list.lua
index 745188de..37fb1867 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_list.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_list.lua
@@ -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)
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe.lua
index 7140ac5c..6ffdd44a 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe.lua
@@ -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", "", "" .. 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.") .. "")
+local cfgname = "subscribe_list"
+s = m:section(TypedSection, cfgname, "", "" .. 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.") .. "")
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)
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe_config.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe_config.lua
index b5068ccc..198e19bc 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe_config.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe_config.lua
@@ -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.") .. "
" ..
translate("This may reveal limited hardware information. Enable only if you trust your provider.")
-return m
+return api.return_map(m)
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/other.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/other.lua
index a715756c..b46db196 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/other.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/other.lua
@@ -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)
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/rule.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/rule.lua
index 2fba7319..e0b65f42 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/rule.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/rule.lua
@@ -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"), "" .. translate("Please note attention to the priority, the higher the order, the higher the priority.") .. "")
+local cfgname = "shunt_rules"
+s = m:section(TypedSection, cfgname, "Sing-Box/Xray " .. translate("Shunt Rule"), "" .. translate("Please note attention to the priority, the higher the order, the higher the priority.") .. "")
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)
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/shunt_rules.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/shunt_rules.lua
index e154fa84..f7035984 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/shunt_rules.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/shunt_rules.lua
@@ -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 = "
"
o = s:option(Flag, "invert", "invert", translate("Invert match result.") .. " " .. translate("Only support Sing-Box."))
-return m
+return api.return_map(m)
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/socks_config.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/socks_config.lua
index 59723a56..9d9ac64a 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/socks_config.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/socks_config.lua
@@ -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: ^A && B && !C && D$
"
+descrStr = descrStr .. "This means the node remark must start with A (^), include B, exclude C (!), and end with D ($).
"
+descrStr = descrStr .. "Conditions are joined by && (AND), and their order does not affect the result.
"
+descrStr = descrStr .. "Multiple groups can be separated by || (OR), matching succeeds if any group matches.
"
+descrStr = descrStr .. "Example: A && B || C && D 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)
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua
index 49308c64..12e44eb6 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua
@@ -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"), "<", "2026.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 server’s chain fingerprint matches.") ..
+ string.format("%s", "→ " .. 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.") .. "
" ..
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"))
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/index.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/index.lua
index 70bec3f8..33f24596 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/index.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/index.lua
@@ -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)
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/hysteria2.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/hysteria2.lua
index 0b0dd1e2..5473fc30 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/hysteria2.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/hysteria2.lua
@@ -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")
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua
index cf944c3f..09dd948d 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua
@@ -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")
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/sing-box.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/sing-box.lua
index d45dc4ad..8f906243 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/sing-box.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/sing-box.lua
@@ -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")
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ss-rust.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ss-rust.lua
index d0939430..a6bca4f9 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ss-rust.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ss-rust.lua
@@ -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")
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ssr.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ssr.lua
index 02a94f61..d77cf74c 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ssr.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ssr.lua
@@ -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")
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/user.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/user.lua
index c50d5ca9..b3da0a04 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/user.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/user.lua
@@ -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)
diff --git a/luci-app-passwall2/luasrc/passwall2/api.lua b/luci-app-passwall2/luasrc/passwall2/api.lua
index 7da2d87a..18a13dec 100644
--- a/luci-app-passwall2/luasrc/passwall2/api.lua
+++ b/luci-app-passwall2/luasrc/passwall2/api.lua
@@ -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 " ..
+ "| 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 = {}
diff --git a/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua b/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua
index bbb5e872..f25afa17 100644
--- a/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua
+++ b/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua
@@ -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
diff --git a/luci-app-passwall2/luasrc/passwall2/util_xray.lua b/luci-app-passwall2/luasrc/passwall2/util_xray.lua
index 7e2b196f..914320be 100644
--- a/luci-app-passwall2/luasrc/passwall2/util_xray.lua
+++ b/luci-app-passwall2/luasrc/passwall2/util_xray.lua
@@ -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 api.compare_versions(os.date("%Y.%m.%d"), "<", "2026.6.1") and 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
diff --git a/luci-app-passwall2/luasrc/view/passwall2/cbi/footer.htm b/luci-app-passwall2/luasrc/view/passwall2/cbi/footer.htm
new file mode 100644
index 00000000..46a25d3e
--- /dev/null
+++ b/luci-app-passwall2/luasrc/view/passwall2/cbi/footer.htm
@@ -0,0 +1 @@
+
diff --git a/luci-app-passwall2/luasrc/view/passwall2/cbi/header.htm b/luci-app-passwall2/luasrc/view/passwall2/cbi/header.htm
new file mode 100644
index 00000000..5d284d90
--- /dev/null
+++ b/luci-app-passwall2/luasrc/view/passwall2/cbi/header.htm
@@ -0,0 +1,2 @@
+
+
diff --git a/luci-app-passwall2/luasrc/view/passwall2/cbi/sortable.htm b/luci-app-passwall2/luasrc/view/passwall2/cbi/sortable.htm
new file mode 100644
index 00000000..2f508eaf
--- /dev/null
+++ b/luci-app-passwall2/luasrc/view/passwall2/cbi/sortable.htm
@@ -0,0 +1,159 @@
+<%
+local api = self.api
+local appname = self.appname or api.appname
+local target_cfgname = self.target_cfgname
+-%>
+
+
+
\ No newline at end of file
diff --git a/luci-app-passwall2/luasrc/view/passwall2/cbi/tvalue.htm b/luci-app-passwall2/luasrc/view/passwall2/cbi/tvalue.htm
new file mode 100644
index 00000000..8037b3d2
--- /dev/null
+++ b/luci-app-passwall2/luasrc/view/passwall2/cbi/tvalue.htm
@@ -0,0 +1,13 @@
+<%+cbi/valueheader%>
+
+<%+cbi/valuefooter%>
diff --git a/luci-app-passwall2/luasrc/view/passwall2/haproxy/js.htm b/luci-app-passwall2/luasrc/view/passwall2/haproxy/js.htm
index aca4d853..f993843d 100644
--- a/luci-app-passwall2/luasrc/view/passwall2/haproxy/js.htm
+++ b/luci-app-passwall2/luasrc/view/passwall2/haproxy/js.htm
@@ -2,46 +2,8 @@
local api = require "luci.passwall2.api"
local appname = api.appname
-%>
-
diff --git a/luci-app-passwall2/luasrc/view/passwall2/rule/rule_version.htm b/luci-app-passwall2/luasrc/view/passwall2/rule/rule_version.htm
index f5fbce49..8dbed277 100644
--- a/luci-app-passwall2/luasrc/view/passwall2/rule/rule_version.htm
+++ b/luci-app-passwall2/luasrc/view/passwall2/rule/rule_version.htm
@@ -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")
-%>
-
@@ -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= THRESHOLD)
- setTimeout(initSortableForTable, 200);
- else
- requestAnimationFrame(tick);
- }
- tick();
- })();
- });
//]]>
diff --git a/luci-app-passwall2/po/fa/passwall2.po b/luci-app-passwall2/po/fa/passwall2.po
index 3c185094..cd8e2625 100644
--- a/luci-app-passwall2/po/fa/passwall2.po
+++ b/luci-app-passwall2/po/fa/passwall2.po
@@ -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 (حداکثر)"
\ No newline at end of file
+msgstr "اندازه بسته Gecko (حداکثر)"
+
+msgid "valid time (hh:mm)"
+msgstr "زمان معتبر (ساعت:میلیمتر)"
diff --git a/luci-app-passwall2/po/ru/passwall2.po b/luci-app-passwall2/po/ru/passwall2.po
index 4bea8828..04797312 100644
--- a/luci-app-passwall2/po/ru/passwall2.po
+++ b/luci-app-passwall2/po/ru/passwall2.po
@@ -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 "Время действия (чч:мм)"
diff --git a/luci-app-passwall2/po/zh-cn/passwall2.po b/luci-app-passwall2/po/zh-cn/passwall2.po
index da052b98..baad1ec2 100644
--- a/luci-app-passwall2/po/zh-cn/passwall2.po
+++ b/luci-app-passwall2/po/zh-cn/passwall2.po
@@ -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)"
diff --git a/luci-app-passwall2/po/zh-tw/passwall2.po b/luci-app-passwall2/po/zh-tw/passwall2.po
index 9245e80e..c7b79250 100644
--- a/luci-app-passwall2/po/zh-tw/passwall2.po
+++ b/luci-app-passwall2/po/zh-tw/passwall2.po
@@ -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)"
diff --git a/luci-app-passwall2/root/usr/share/passwall2/socks_auto_switch.sh b/luci-app-passwall2/root/usr/share/passwall2/socks_auto_switch.sh
index 310a2070..54b8b23f 100755
--- a/luci-app-passwall2/root/usr/share/passwall2/socks_auto_switch.sh
+++ b/luci-app-passwall2/root/usr/share/passwall2/socks_auto_switch.sh
@@ -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