🎁 Sync 2026-07-16 21:58:33

This commit is contained in:
github-actions[bot] committed 2026-07-16 21:58:33 +08:00
1 parent 257116fcb5
commit b623db8914
46 files changed
+490 -615

No files matched your search

+2 -2
View File
@@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=ddns-go PKG_NAME:=ddns-go
PKG_VERSION:=6.17.1 PKG_VERSION:=6.17.2
PKG_RELEASE:=18 PKG_RELEASE:=19
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/jeessy2/ddns-go/tar.gz/v$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/jeessy2/ddns-go/tar.gz/v$(PKG_VERSION)?
+2 -2
View File
@@ -7,8 +7,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.7.15 PKG_VERSION:=26.7.16
PKG_RELEASE:=180 PKG_RELEASE:=181
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
@@ -0,0 +1,38 @@
function is_json(str) {
try {
const result = JSON.parse(str);
return typeof result === 'object' && result !== null;
} catch (e) {
return false;
}
}
function is_timehhmm(str) {
const match = String(str).match(/^(\d?\d):(\d\d)$/);
if (match) {
const hour = parseInt(match[1], 10);
const minute = parseInt(match[2], 10);
if (hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59) {
return true;
}
}
return false;
}
if (typeof cbi_validators !== 'undefined' && cbi_validators !== null) {
cbi_validators['json'] = function() {
return is_json(this);
}
cbi_validators['timehhmm'] = function() {
return is_timehhmm(this);
}
} else {
L.require('validation').then(function(validation) {
validation.types['json'] = function() {
return this.assert(is_json(this.value), _('Must be JSON text!'));
}
validation.types['timehhmm'] = function() {
return this.assert(is_timehhmm(this.value), _('valid time (hh:mm)'));
}
});
}
@@ -1152,10 +1152,20 @@ function fetch_certsha256()
local port = (id ~= "") and uci:get(appname, id, "port") or 0 local port = (id ~= "") and uci:get(appname, id, "port") or 0
local sni = (id ~= "") and uci:get(appname, id, "tls_serverName") or "" local sni = (id ~= "") and uci:get(appname, id, "tls_serverName") or ""
sni = (sni ~= "") and sni or address sni = (sni ~= "") and sni or address
local protocol = uci:get(appname, id, "protocol")
local h3, timeout = false, 10
if protocol == "hysteria2" then
h3 = true
timeout = 60
if port == 0 then
local hop = uci:get(appname, id, "hysteria2_hop") or "0"
port = tonumber(hop:match("^%s*(%d+)"))
end
end
if address == "" or port == 0 then if address == "" or port == 0 then
http_write_json_error() http_write_json_error()
return return
end end
local data = api.fetch_cert_sha256(address, port, sni, 5) local data = api.fetch_cert_sha256(address, port, sni, timeout, h3)
http_write_json(data ~= "" and { code = 1, data = data } or { code = 0 }) http_write_json(data ~= "" and { code = 1, data = data } or { code = 0 })
end end
@@ -2,6 +2,8 @@ local api = require "luci.passwall.api"
local appname = "passwall" local appname = "passwall"
local sys = api.sys local sys = api.sys
api.set_default_cbi()
m = Map(appname) m = Map(appname)
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -13,7 +15,8 @@ o.rmempty = false
o.default = false o.default = false
-- [[ ACLs Settings ]]-- -- [[ ACLs Settings ]]--
s = m:section(TypedSection, "acl_rule") local cfgname = "acl_rule"
s = m:section(TypedSection, cfgname)
s.template = "cbi/tblsection" s.template = "cbi/tblsection"
s.sortable = true s.sortable = true
s.anonymous = true s.anonymous = true
@@ -101,4 +104,9 @@ o:value("1:65535", translate("All"))
o:value("53", "53") o:value("53", "53")
]]-- ]]--
return m local sortable = Template(appname .. "/cbi/sortable")
sortable.api = api
sortable.target_cfgname = cfgname
m:append(sortable)
return api.return_map(m)
@@ -1,6 +1,8 @@
local api = require "luci.passwall.api" local api = require "luci.passwall.api"
local appname = "passwall" local appname = "passwall"
api.set_default_cbi()
m = Map(appname) m = Map(appname)
m.redirect = api.url("acl") m.redirect = api.url("acl")
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -601,4 +603,4 @@ for k, v in pairs(nodes_table) do
end end
end end
return m return api.return_map(m)
@@ -2,6 +2,8 @@ local api = require "luci.passwall.api"
local com = require "luci.passwall.com" local com = require "luci.passwall.com"
local appname = "passwall" local appname = "passwall"
api.set_default_cbi()
m = Map(appname) m = Map(appname)
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -34,4 +36,4 @@ o.cfgvalue = function(t, n)
return string.format('<font color="red">%s</font>', translate("if you want to run from memory, change the path, /tmp beginning then save the application and update it manually.")) return string.format('<font color="red">%s</font>', translate("if you want to run from memory, change the path, /tmp beginning then save the application and update it manually."))
end end
return m return api.return_map(m)
@@ -8,6 +8,8 @@ local has_gfwlist = fs.access("/usr/share/passwall/rules/gfwlist")
local has_chnlist = fs.access("/usr/share/passwall/rules/chnlist") local has_chnlist = fs.access("/usr/share/passwall/rules/chnlist")
local has_chnroute = fs.access("/usr/share/passwall/rules/chnroute") local has_chnroute = fs.access("/usr/share/passwall/rules/chnroute")
api.set_default_cbi()
m = Map(appname) m = Map(appname)
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -879,4 +881,4 @@ footer.shunt_list = api.jsonc.stringify(shunt_list)
m:append(footer) m:append(footer)
m:append(Template(appname .. "/global/status_bottom")) m:append(Template(appname .. "/global/status_bottom"))
return m return api.return_map(m)
@@ -15,6 +15,8 @@ for k, e in ipairs(api.get_valid_nodes()) do
end end
end end
api.set_default_cbi()
m = Map(appname, "HAPROXY " .. translate("Load Balancing")) m = Map(appname, "HAPROXY " .. translate("Load Balancing"))
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -101,7 +103,8 @@ end
o:depends("health_check_type", "passwall_logic") o:depends("health_check_type", "passwall_logic")
-- [[ Balancing Settings ]]-- -- [[ Balancing Settings ]]--
s = m:section(TypedSection, "haproxy_config", translate("Node List")) local cfgname = "haproxy_config"
s = m:section(TypedSection, cfgname, translate("Node List"))
s.description = "<font color='red'>" .. s.description = "<font color='red'>" ..
translate("Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group.") .. "<br>" .. translate("Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group.") .. "<br>" ..
translate("Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!") .. "</font>" translate("Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!") .. "</font>"
@@ -177,6 +180,11 @@ o:value(0, translate("Primary"))
o:value(1, translate("Standby")) o:value(1, translate("Standby"))
o.rmempty = false o.rmempty = false
local sortable = Template(appname .. "/cbi/sortable")
sortable.api = api
sortable.target_cfgname = cfgname
m:append(sortable)
m:append(Template(appname .. "/haproxy/js")) m:append(Template(appname .. "/haproxy/js"))
return m return api.return_map(m)
@@ -1,6 +1,8 @@
api = require "luci.passwall.api" api = require "luci.passwall.api"
appname = "passwall" appname = "passwall"
api.set_default_cbi()
m = Map(appname, translate("Node Config")) m = Map(appname, translate("Node Config"))
m.redirect = api.url("node_list") m.redirect = api.url("node_list")
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -120,7 +122,6 @@ local footer = Template(appname .. "/node_config/footer")
footer.api = api footer.api = api
footer.config = m.config footer.config = m.config
footer.section = arg[1] footer.section = arg[1]
m:append(footer) m:append(footer)
return m return api.return_map(m)
@@ -3,6 +3,8 @@ local appname = "passwall"
local sys = api.sys local sys = api.sys
local datatypes = api.datatypes local datatypes = api.datatypes
api.set_default_cbi()
m = Map(appname) m = Map(appname)
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -33,4 +35,4 @@ s:append(Template(appname .. "/node_list/link_add_node"))
m:append(Template(appname .. "/node_list/node_list")) m:append(Template(appname .. "/node_list/node_list"))
return m return api.return_map(m)
@@ -38,6 +38,8 @@ if has_hysteria2 then
table.insert(hysteria2_type, s) table.insert(hysteria2_type, s)
end end
api.set_default_cbi()
m = Map(appname) m = Map(appname)
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -127,7 +129,8 @@ o.cfgvalue = function(self, section)
translate("Manual subscription All")) translate("Manual subscription All"))
end end
s = m:section(TypedSection, "subscribe_list", "", "<font color='red'>" .. translate("When adding a new subscription, please save and apply before manually subscribing. If you only change the subscription URL, you can subscribe manually, and the system will save it automatically.") .. "</font>") local cfgname = "subscribe_list"
s = m:section(TypedSection, cfgname, "", "<font color='red'>" .. translate("When adding a new subscription, please save and apply before manually subscribing. If you only change the subscription URL, you can subscribe manually, and the system will save it automatically.") .. "</font>")
s.addremove = true s.addremove = true
s.anonymous = true s.anonymous = true
s.sortable = true s.sortable = true
@@ -148,7 +151,7 @@ o.validate = function(self, value, section)
return nil, translate("Remark cannot be empty.") return nil, translate("Remark cannot be empty.")
end end
local duplicate = false local duplicate = false
m.uci:foreach(appname, "subscribe_list", function(e) m.uci:foreach(appname, cfgname, function(e)
if e[".name"] ~= section and e["remark"] and e["remark"]:lower() == value:lower() then if e[".name"] ~= section and e["remark"] and e["remark"]:lower() == value:lower() then
duplicate = true duplicate = true
return false return false
@@ -223,6 +226,11 @@ o.cfgvalue = function(self, section)
section, translate("Manual subscription")) section, translate("Manual subscription"))
end end
local sortable = Template(appname .. "/cbi/sortable")
sortable.api = api
sortable.target_cfgname = cfgname
m:append(sortable)
m:append(Template(appname .. "/node_subscribe/js")) m:append(Template(appname .. "/node_subscribe/js"))
return m return api.return_map(m)
@@ -2,6 +2,8 @@ local api = require "luci.passwall.api"
local uci = api.uci local uci = api.uci
local appname = "passwall" local appname = "passwall"
api.set_default_cbi()
m = Map(appname) m = Map(appname)
m.redirect = api.url("node_subscribe") m.redirect = api.url("node_subscribe")
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -244,13 +246,7 @@ o:value(0, translate("Every Sunday"))
o = s:option(Value, "update_time_mode", translate("Update Time")) o = s:option(Value, "update_time_mode", translate("Update Time"))
for t = 0, 23 do o:value(t .. ":00") end for t = 0, 23 do o:value(t .. ":00") end
o.default = "0:00" o.default = "0:00"
o.validate = function(self, value) o.datatype = "timehhmm"
local b = api.is_timehhmm(value)
if b then
return value
end
return nil
end
o:depends("update_week_mode", "0") o:depends("update_week_mode", "0")
o:depends("update_week_mode", "1") o:depends("update_week_mode", "1")
o:depends("update_week_mode", "2") o:depends("update_week_mode", "2")
@@ -321,4 +317,4 @@ for k, v in pairs(nodes_table) do
end end
end end
return m return api.return_map(m)
@@ -8,6 +8,8 @@ local port_validate = function(self, value, t)
return value:gsub("-", ":") return value:gsub("-", ":")
end end
api.set_default_cbi()
m = Map(appname) m = Map(appname)
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -43,13 +45,7 @@ for index, value in ipairs({"stop", "start", "restart"}) do
o = s:option(Value, value .. "_time_mode", translate(value .. " Time")) o = s:option(Value, value .. "_time_mode", translate(value .. " Time"))
for t = 0, 23 do o:value(t .. ":00") end for t = 0, 23 do o:value(t .. ":00") end
o.default = "0:00" o.default = "0:00"
o.validate = function(self, value) o.datatype = "timehhmm"
local b = api.is_timehhmm(value)
if b then
return value
end
return nil
end
o:depends(value .. "_week_mode", "0") o:depends(value .. "_week_mode", "0")
o:depends(value .. "_week_mode", "1") o:depends(value .. "_week_mode", "1")
o:depends(value .. "_week_mode", "2") o:depends(value .. "_week_mode", "2")
@@ -271,4 +267,4 @@ if has_singbox then
o.default = 0 o.default = 0
end end
return m return api.return_map(m)
@@ -3,6 +3,8 @@ local appname = "passwall"
local has_xray = api.finded_com("xray") local has_xray = api.finded_com("xray")
local has_singbox = api.finded_com("sing-box") local has_singbox = api.finded_com("sing-box")
api.set_default_cbi()
m = Map(appname) m = Map(appname)
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -112,13 +114,7 @@ o:value(0, translate("Every Sunday"))
o = s:option(Value, "update_time_mode", translate("Update Time")) o = s:option(Value, "update_time_mode", translate("Update Time"))
for t = 0, 23 do o:value(t .. ":00") end for t = 0, 23 do o:value(t .. ":00") end
o.default = "0:00" o.default = "0:00"
o.validate = function(self, value) o.datatype = "timehhmm"
local b = api.is_timehhmm(value)
if b then
return value
end
return nil
end
o:depends("update_week_mode", "0") o:depends("update_week_mode", "0")
o:depends("update_week_mode", "1") o:depends("update_week_mode", "1")
o:depends("update_week_mode", "2") o:depends("update_week_mode", "2")
@@ -146,8 +142,10 @@ end
s:append(Template(appname .. "/rule/rule_version")) s:append(Template(appname .. "/rule/rule_version"))
local cfgname = "shunt_rules"
if has_xray or has_singbox then if has_xray or has_singbox then
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>") s = m:section(TypedSection, cfgname, "Sing-Box/Xray " .. translate("Shunt Rule"), "<a style='color: red'>" .. translate("Please note attention to the priority, the higher the order, the higher the priority.") .. "</a>")
s.template = "cbi/tblsection" s.template = "cbi/tblsection"
s.anonymous = false s.anonymous = false
s.addremove = true s.addremove = true
@@ -169,4 +167,9 @@ if has_xray or has_singbox then
o = s:option(DummyValue, "remarks", translate("Remarks")) o = s:option(DummyValue, "remarks", translate("Remarks"))
end end
return m local sortable = Template(appname .. "/cbi/sortable")
sortable.api = api
sortable.target_cfgname = cfgname
m:append(sortable)
return api.return_map(m)
@@ -1,5 +1,5 @@
local api = require "luci.passwall.api" local api = require "luci.passwall.api"
local appname = "passwall" local appname = api.appname
local fs = api.fs local fs = api.fs
local sys = api.sys local sys = api.sys
local uci = api.uci local uci = api.uci
@@ -2,6 +2,8 @@ local api = require "luci.passwall.api"
local appname = "passwall" local appname = "passwall"
local datatypes = api.datatypes local datatypes = api.datatypes
api.set_default_cbi()
m = Map(appname, "Sing-Box/Xray " .. translate("Shunt Rule")) m = Map(appname, "Sing-Box/Xray " .. translate("Shunt Rule"))
m.redirect = api.url("rule") m.redirect = api.url("rule")
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -245,4 +247,4 @@ ip_list.description = "<br /><ul>"
o = s:option(Flag, "invert", "Invert", translate("Invert match result.") .. " " .. translate("Only support Sing-Box.")) o = s:option(Flag, "invert", "Invert", translate("Invert match result.") .. " " .. translate("Only support Sing-Box."))
return m return api.return_map(m)
@@ -1,6 +1,8 @@
local api = require "luci.passwall.api" local api = require "luci.passwall.api"
local appname = "passwall" local appname = "passwall"
api.set_default_cbi()
m = Map(appname) m = Map(appname)
m.redirect = api.url() m.redirect = api.url()
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -166,4 +168,4 @@ o = s:option(DummyValue, "btn")
o.template = appname .. "/socks_auto_switch/btn" o.template = appname .. "/socks_auto_switch/btn"
o:depends("backup_node_add_mode", "manual") o:depends("backup_node_add_mode", "manual")
return m return api.return_map(m)
@@ -667,6 +667,14 @@ o = s:option(TextValue, _n("xhttp_extra"), " ", translate("An XHttpObject in J
o:depends({ [_n("use_xhttp_extra")] = true }) o:depends({ [_n("use_xhttp_extra")] = true })
o.rows = 10 o.rows = 10
o.wrap = "off" o.wrap = "off"
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
local v = o_validate(self, value)
if v then return v end
return nil, "XHTTP Extra " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value) o.custom_cfgvalue = function(self, section, value)
local raw = m:get(section, "xhttp_extra") local raw = m:get(section, "xhttp_extra")
if raw then if raw then
@@ -689,14 +697,6 @@ o.custom_write = function(self, section, value)
m:del(section, "download_address") m:del(section, "download_address")
end end
end end
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
if api.jsonc.parse(value) then
return value
else
return nil, "XHTTP Extra " .. translate("Must be JSON text!")
end
end
o.custom_remove = function(self, section, value) o.custom_remove = function(self, section, value)
m:del(section, "xhttp_extra") m:del(section, "xhttp_extra")
m:del(section, "download_address") m:del(section, "download_address")
@@ -752,6 +752,14 @@ o.rows = 10
o.wrap = "off" o.wrap = "off"
o.description = translate("An FinalMaskObject in JSON format, used for sharing.") .. "<br>" .. o.description = translate("An FinalMaskObject in JSON format, used for sharing.") .. "<br>" ..
translate("Custom finalmask overrides mkcp, hysteria2, fragment, noise, and related settings.") translate("Custom finalmask overrides mkcp, hysteria2, fragment, noise, and related settings.")
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
local v = o_validate(self, value)
if v then return v end
return nil, "FinalMask " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value) o.custom_cfgvalue = function(self, section, value)
local raw = m:get(section, "finalmask") local raw = m:get(section, "finalmask")
if raw then if raw then
@@ -761,14 +769,6 @@ end
o.custom_write = function(self, section, value) o.custom_write = function(self, section, value)
m:set(section, "finalmask", api.base64Encode(value) or "") m:set(section, "finalmask", api.base64Encode(value) or "")
end end
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
if api.jsonc.parse(value) then
return value
else
return nil, "FinalMask " .. translate("Must be JSON text!")
end
end
--[[Fast Open]] --[[Fast Open]]
o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required")) o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required"))
@@ -1,4 +1,7 @@
local api = require "luci.passwall.api" local api = require "luci.passwall.api"
local appname = api.appname
api.set_default_cbi()
m = Map("passwall_server", translate("Server-Side")) m = Map("passwall_server", translate("Server-Side"))
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -10,7 +13,8 @@ t.addremove = false
e = t:option(Flag, "enable", translate("Enable")) e = t:option(Flag, "enable", translate("Enable"))
e.rmempty = false e.rmempty = false
t = m:section(TypedSection, "user", translate("Users Manager")) local cfgname = "user"
t = m:section(TypedSection, cfgname, translate("Users Manager"))
t.anonymous = true t.anonymous = true
t.addremove = true t.addremove = true
t.sortable = true t.sortable = true
@@ -85,7 +89,14 @@ e = t:option(Flag, "log", translate("Log"))
e.default = "1" e.default = "1"
e.rmempty = false e.rmempty = false
m:append(Template("passwall/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("passwall/server/users_list_status")) m:append(Template(appname .. "/server/log"))
return m
m:append(Template(appname .. "/server/users_list_status"))
return api.return_map(m)
@@ -131,12 +131,12 @@ o = s:option(TextValue, _n("custom_config"), translate("Custom Config"))
o.rows = 10 o.rows = 10
o.wrap = "off" o.wrap = "off"
o:depends({ [_n("custom")] = true }) o:depends({ [_n("custom")] = true })
o.validate = function(self, value, t) o.datatype = "json"
if value and api.jsonc.parse(value) then local o_validate = o.validate
return value o.validate = function(self, value)
else local v = o_validate(self, value)
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!") if v then return v end
end return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end end
o.custom_cfgvalue = function(self, section, value) o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str") local config_str = m:get(section, "config_str")
@@ -406,6 +406,7 @@ o = s:option(TextValue, _n("finalmask"), "FinalMask JSON")
o:depends({ [_n("use_finalmask")] = true }) o:depends({ [_n("use_finalmask")] = true })
o.rows = 10 o.rows = 10
o.wrap = "off" o.wrap = "off"
o.datatype = "json"
o.custom_cfgvalue = function(self, section, value) o.custom_cfgvalue = function(self, section, value)
local raw = m:get(section, "finalmask") local raw = m:get(section, "finalmask")
if raw then if raw then
@@ -415,14 +416,6 @@ end
o.custom_write = function(self, section, value) o.custom_write = function(self, section, value)
m:set(section, "finalmask", api.base64Encode(value) or "") m:set(section, "finalmask", api.base64Encode(value) or "")
end end
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
if api.jsonc.parse(value) then
return value
else
return nil, "FinalMask " .. translate("Must be JSON text!")
end
end
--[[acceptProxyProtocol]] --[[acceptProxyProtocol]]
o = s:option(Flag, _n("acceptProxyProtocol"), translate("acceptProxyProtocol"), translate("Whether to receive PROXY protocol, when this node want to be fallback or forwarded by proxy, it must be enable, otherwise it cannot be used.")) o = s:option(Flag, _n("acceptProxyProtocol"), translate("acceptProxyProtocol"), translate("Whether to receive PROXY protocol, when this node want to be fallback or forwarded by proxy, it must be enable, otherwise it cannot be used."))
@@ -517,16 +510,16 @@ for _, d in ipairs(netdev_list) do
o:value(d.name, d.label) o:value(d.name, d.label)
end end
o = s:option(TextValue, _n("custom_config"), translate("Custom Config")) o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)")
o.rows = 10 o.rows = 10
o.wrap = "off" o.wrap = "off"
o:depends({ [_n("custom")] = true }) o:depends({ [_n("custom")] = true })
o.validate = function(self, value, t) o.datatype = "json"
if value and api.jsonc.parse(value) then local o_validate = o.validate
return value o.validate = function(self, value)
else local v = o_validate(self, value)
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!") if v then return v end
end return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end end
o.custom_cfgvalue = function(self, section, value) o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str") local config_str = m:get(section, "config_str")
@@ -489,16 +489,16 @@ for _, d in ipairs(netdev_list) do
o:value(d.name, d.label) o:value(d.name, d.label)
end end
o = s:option(TextValue, _n("custom_config"), translate("Custom Config")) o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)")
o.rows = 10 o.rows = 10
o.wrap = "off" o.wrap = "off"
o:depends({ [_n("custom")] = true }) o:depends({ [_n("custom")] = true })
o.validate = function(self, value, t) o.datatype = "json"
if value and api.jsonc.parse(value) then local o_validate = o.validate
return value o.validate = function(self, value)
else local v = o_validate(self, value)
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!") if v then return v end
end return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end end
o.custom_cfgvalue = function(self, section, value) o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str") local config_str = m:get(section, "config_str")
@@ -49,16 +49,16 @@ o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"))
o.default = "0" o.default = "0"
o:depends({ [_n("custom")] = false }) o:depends({ [_n("custom")] = false })
o = s:option(TextValue, _n("custom_config"), translate("Custom Config")) o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)")
o.rows = 10 o.rows = 10
o.wrap = "off" o.wrap = "off"
o:depends({ [_n("custom")] = true }) o:depends({ [_n("custom")] = true })
o.validate = function(self, value, t) o.datatype = "json"
if value and api.jsonc.parse(value) then local o_validate = o.validate
return value o.validate = function(self, value)
else local v = o_validate(self, value)
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!") if v then return v end
end return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end end
o.custom_cfgvalue = function(self, section, value) o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str") local config_str = m:get(section, "config_str")
@@ -76,16 +76,16 @@ o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"))
o.default = "0" o.default = "0"
o:depends({ [_n("custom")] = false }) o:depends({ [_n("custom")] = false })
o = s:option(TextValue, _n("custom_config"), translate("Custom Config")) o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)")
o.rows = 10 o.rows = 10
o.wrap = "off" o.wrap = "off"
o:depends({ [_n("custom")] = true }) o:depends({ [_n("custom")] = true })
o.validate = function(self, value, t) o.datatype = "json"
if value and api.jsonc.parse(value) then local o_validate = o.validate
return value o.validate = function(self, value)
else local v = o_validate(self, value)
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!") if v then return v end
end return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end end
o.custom_cfgvalue = function(self, section, value) o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str") local config_str = m:get(section, "config_str")
@@ -2,6 +2,8 @@ api = require "luci.passwall.api"
appname = api.appname appname = api.appname
fs = api.fs fs = api.fs
api.set_default_cbi()
m = Map("passwall_server", translate("Server Config")) m = Map("passwall_server", translate("Server Config"))
m.redirect = api.url("server") m.redirect = api.url("server")
api.set_apply_on_parse(m) api.set_apply_on_parse(m)
@@ -56,4 +58,4 @@ footer.config = m.config
footer.section = arg[1] footer.section = arg[1]
m:append(footer) m:append(footer)
return m return api.return_map(m)
+64 -10
View File
@@ -348,8 +348,16 @@ function strToTable(str)
return loadstring("return " .. str)() return loadstring("return " .. str)()
end end
function is_timehhmm(timeStr) function is_json(str)
local hour, minute = string.match(timeStr, "^(%d?%d):(%d%d)$") if str and jsonc.parse(str) then
return true
end
return false
end
datatypes.json = is_json
function is_timehhmm(str)
local hour, minute = string.match(str, "^(%d?%d):(%d%d)$")
if hour and minute then if hour and minute then
hour = tonumber(hour) hour = tonumber(hour)
minute = tonumber(minute) minute = tonumber(minute)
@@ -359,6 +367,7 @@ function is_timehhmm(timeStr)
end end
return false return false
end end
datatypes.timehhmm = is_timehhmm
function is_normal_node(e) function is_normal_node(e)
if e and e.type and e.protocol and (e.protocol == "_balancing" or e.protocol == "_shunt" or e.protocol == "_iface" or e.protocol == "_urltest") then if e and e.type and e.protocol and (e.protocol == "_balancing" or e.protocol == "_shunt" or e.protocol == "_iface" or e.protocol == "_urltest") then
@@ -1327,6 +1336,40 @@ function to_check_self()
} }
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.passwall.api"
if true then
-- header
local header = cbi.Template(appname .. "/cbi/header")
header.api = api
header.config = map.config
table.insert(map.children, 1, header)
end
if true then
-- footer
local footer = cbi.Template(appname .. "/cbi/footer")
footer.api = api
footer.config = map.config
map:append(footer)
end
return map
end
function luci_types(id, m, s, type_name, option_prefix) function luci_types(id, m, s, type_name, option_prefix)
local fv_type local fv_type
local field_type = s.fields["type"] local field_type = s.fields["type"]
@@ -1673,18 +1716,29 @@ function cleanEmptyTables(t)
return next(t) and t or nil return next(t) and t or nil
end end
function fetch_cert_sha256(host, port, sni, timeout) function fetch_cert_sha256(host, port, sni, timeout, http3)
if not host then return "" end if not host then return "" end
port = tonumber(port) or 443 port = tonumber(port) or 443
sni = sni or host sni = sni or host
timeout = tonumber(timeout) or 5 timeout = tonumber(timeout) or 5
local cmd = string.format( local cmd
"timeout %d openssl s_client -connect %s:%d -servername %s -showcerts </dev/null 2>/dev/null " .. if http3 then
"| awk 'BEGIN{c=0}/BEGIN CERT/{c++} c==1{print} /END CERT/{if(c==1)exit}' " .. cmd = string.format(
"| openssl x509 -outform der 2>/dev/null " .. "timeout %d curl --http3 -k -w '%%{certs}' -o /dev/null https://%s:%d 2>/dev/null " ..
"| sha256sum 2>/dev/null", "| awk 'BEGIN{c=0}/BEGIN CERT/{c++} c==1{print} /END CERT/{if(c==1)exit}' " ..
timeout, host, port, sni "| openssl x509 -outform der 2>/dev/null " ..
) "| sha256sum 2>/dev/null",
timeout, host, port
)
else
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
)
end
local out = trim(sys.exec(cmd)) local out = trim(sys.exec(cmd))
local fp = out:match("^([0-9a-fA-F]+)") local fp = out:match("^([0-9a-fA-F]+)")
if not fp or fp:lower():match("^e3b0c44298fc1c149afbf4c8996fb924") then if not fp or fp:lower():match("^e3b0c44298fc1c149afbf4c8996fb924") then
@@ -153,8 +153,8 @@ function gen_outbound(flag, node, tag, proxy_table)
tlsSettings = (node.stream_security == "tls") and { tlsSettings = (node.stream_security == "tls") and {
serverName = node.tls_serverName, serverName = node.tls_serverName,
fingerprint = (node.type == "Xray" and node.utls == "1" and node.fingerprint and node.fingerprint ~= "") and node.fingerprint or nil, fingerprint = (node.type == "Xray" and node.utls == "1" and node.fingerprint and node.fingerprint ~= "") and node.fingerprint or nil,
pinnedPeerCertSha256 = node.tls_pinSHA256, pinnedPeerCertSha256 = node.tls_pinSHA256 or "",
verifyPeerCertByName = node.tls_CertByName, verifyPeerCertByName = node.tls_CertByName or "",
echConfigList = (node.ech == "1") and node.ech_config or nil, echConfigList = (node.ech == "1") and node.ech_config or nil,
certificates = (node.tls_certificate == "1" and node.tls_certificate_pem ~= "") and { certificates = (node.tls_certificate == "1" and node.tls_certificate_pem ~= "") and {
certificate = api.split(node.tls_certificate_pem, "\n"), certificate = api.split(node.tls_certificate_pem, "\n"),
@@ -0,0 +1 @@
<!-- map footer -->
@@ -0,0 +1,2 @@
<!-- map header -->
<script src="<%=resource%>/view/<%=self.api.appname%>/cbi.js"></script>
@@ -403,7 +403,7 @@
function lv_expandGroupOfCurrent(cbid, listContainer, hiddenSelect) { function lv_expandGroupOfCurrent(cbid, listContainer, hiddenSelect) {
const key = hiddenSelect.options[0].value; const key = hiddenSelect.options[0].value;
if (!key) return; if (!key) return;
const targetLi = listContainer.querySelector("li[data-key='" + key.replace(/'/g,"\\'") + "']"); const targetLi = listContainer.querySelector(`li[data-key='${CSS.escape(key)}']`);
if (!targetLi) return; if (!targetLi) return;
let parentGroup = targetLi.closest(".lv-group"); let parentGroup = targetLi.closest(".lv-group");
if (parentGroup) { if (parentGroup) {
@@ -402,7 +402,7 @@
function v_expandGroupOfCurrent(cbid, listContainer, hiddenInput) { function v_expandGroupOfCurrent(cbid, listContainer, hiddenInput) {
const key = hiddenInput.value; const key = hiddenInput.value;
if (!key) return; if (!key) return;
const targetLi = listContainer.querySelector("li[data-key='" + key.replace(/'/g,"\\'") + "']"); const targetLi = listContainer.querySelector(`li[data-key='${CSS.escape(key)}']`);
if (!targetLi) return; if (!targetLi) return;
let parentGroup = targetLi.closest(".v-group"); let parentGroup = targetLi.closest(".v-group");
if (parentGroup) { if (parentGroup) {
@@ -0,0 +1,159 @@
<%
local api = self.api
local appname = self.appname or api.appname
local target_cfgname = self.target_cfgname
-%>
<script src="<%=resource%>/view/<%=api.appname%>/Sortable.min.js"></script>
<style>
table .cbi-button-up,
table .cbi-button-down,
.td.cbi-section-actions .cbi-button-up,
.td.cbi-section-actions .cbi-button-down {
display: none !important;
}
.drag-handle {
vertical-align: middle;
cursor: grab !important;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 100;
padding: 0 !important;
line-height: inherit;
user-select: none;
align-self: stretch;
background-color: transparent;
}
.drag-handle:hover {
background: transparent;
}
.dragging-row {
background-color: rgba(131, 191, 255, 0.7) !important;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
@media screen and (max-width: 1152px) {
#cbi-<%=appname%>-<%=target_cfgname%> .cbi-section-table-titles {
display: none !important;
}
}
</style>
<script>
//<![CDATA[
//Add drag-and-drop sorting
document.addEventListener("DOMContentLoaded", function () {
function initSortableForTable() {
var section = document.getElementById("cbi-<%=appname%>-<%=target_cfgname%>");
if (!section) return;
// === insert drag handle ===
var rows = section.querySelectorAll("tr");
rows.forEach(function(row) {
var btn = row.querySelector(".cbi-button-remove:last-of-type");
if (!btn) return;
if (btn.nextElementSibling && btn.nextElementSibling.classList.contains("drag-handle")) return;
var handle = document.createElement("span");
handle.className = "drag-handle center";
handle.title = "<%:Drag to reorder%>";
handle.innerHTML = "⠿";
btn.after(handle);
});
// === init Sortable ===
var table = section.getElementsByTagName("table")[0];
if (!table) return;
var root = table.tBodies[0] || table;
if (root._sortable_initialized) return root._sortable_instance;
root._sortable_initialized = true;
// save origin order
root._origOrder = getCurrentOrder(root);
try {
root._sortable_instance = Sortable.create(root, {
handle: ".drag-handle",
draggable: "tr.cbi-section-table-row",
animation: 150,
ghostClass: "dragging-row",
fallbackOnBody: true,
forceFallback: false,
swapThreshold: 0.65,
onEnd: function (evt) {
updateHiddenInput(root, section);
}
});
return root._sortable_instance;
} catch (e) {
root._sortable_initialized = false;
console.error("Sortable init failed:", e);
}
}
// get table current line order
function getCurrentOrder(root) {
var order = [];
var rows = root.querySelectorAll("tr.cbi-section-table-row");
rows.forEach(function (tr) {
var id = tr.id || "";
if (id.startsWith("cbi-<%=appname%>-"))
id = id.replace("cbi-<%=appname%>-", "");
order.push(id);
});
return order;
}
// update after drag and drop hidden input
function updateHiddenInput(root, section) {
var newOrder = getCurrentOrder(root);
var changed = newOrder.join(" ") !== root._origOrder.join(" ");
var hiddenInput = section.querySelector('input[type="hidden"][id^="cbi.sts."]');
if (hiddenInput) {
hiddenInput.value = changed ? newOrder.join(" ") : "";
}
}
// hide lua luci version up/down
function hideSortColumn(section) {
var table = section.getElementsByTagName("table")[0];
if(!table) return;
var rows = table.querySelectorAll("tr.cbi-section-table-row");
if(!rows.length) return;
var colCount = rows[0].children.length, sortCol = -1;
for(var col=0; col<colCount; col++){
var hasSort=false, invalid=false;
rows.forEach(function(tr){
var td=tr.children[col]; if(!td) return;
if(td.querySelector(".cbi-button-edit, .cbi-button-remove")) invalid=true;
if(td.querySelector(".cbi-button-up, .cbi-button-down")) hasSort=true;
});
if(!invalid && hasSort){ sortCol=col; break; }
}
if(sortCol===-1) return;
table.querySelectorAll("tr").forEach(function(tr){
var c=tr.children[sortCol]; if(c) c.style.display="none";
});
}
// === wait TypedSection ===
(function waitStable() {
var last = 0, stable = 0;
var THRESHOLD = 5;
function tick() {
var count = document.querySelectorAll("tr.cbi-section-table-row").length;
if (count && count === last) stable++;
else stable = 0;
last = count;
if (stable >= THRESHOLD)
setTimeout(initSortableForTable, 200);
else
requestAnimationFrame(tick);
}
tick();
})();
});
</script>
@@ -0,0 +1,13 @@
<%+cbi/valueheader%>
<textarea class="cbi-input-textarea" <% if not self.size then %> style="width: 100%"<% else %> cols="<%=self.size%>"<% end %>
data-update="change"<%=
attr("name", cbid) ..
attr("id", cbid) ..
ifattr(self.rows, "rows") ..
ifattr(self.wrap, "wrap") ..
ifattr(self.readonly, "readonly") ..
ifattr(self.datatype, "data-type", self.datatype)
%>>
<%-=pcdata(self:cfgvalue(section) or self.default)-%>
</textarea>
<%+cbi/valuefooter%>
@@ -2,46 +2,8 @@
local api = require "luci.passwall.api" local api = require "luci.passwall.api"
local appname = api.appname local appname = api.appname
-%> -%>
<script src="<%=resource%>/view/<%=appname%>/Sortable.min.js?v=1.15.7"></script>
<style> <style>
table .cbi-button-up,
table .cbi-button-down,
.td.cbi-section-actions .cbi-button-up,
.td.cbi-section-actions .cbi-button-down {
display: none !important;
}
.drag-handle {
vertical-align: middle;
cursor: grab !important;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 100;
padding: 0 !important;
line-height: inherit;
user-select: none;
align-self: stretch;
background-color: transparent;
}
.drag-handle:hover {
background: transparent;
}
.dragging-row {
background-color: rgba(131, 191, 255, 0.7) !important;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
@media screen and (max-width: 1152px) {
#cbi-<%=appname%>-haproxy_config .cbi-section-table-titles {
display: none !important;
}
}
@media screen and (min-width: 1152px) { @media screen and (min-width: 1152px) {
#cbi-<%=appname%>-haproxy_config input[id*=".<%=appname%>."][id$=".haproxy_port"], #cbi-<%=appname%>-haproxy_config input[id*=".<%=appname%>."][id$=".haproxy_port"],
#cbi-<%=appname%>-haproxy_config input[id*=".<%=appname%>."][id$=".lbweight"] { #cbi-<%=appname%>-haproxy_config input[id*=".<%=appname%>."][id$=".lbweight"] {
@@ -110,129 +72,5 @@ table .cbi-button-down,
} }
}, 300); }, 300);
}); });
//节点列表添加拖拽排序
document.addEventListener("DOMContentLoaded", function () {
function initSortableForTable() {
var section = document.getElementById("cbi-<%=appname%>-haproxy_config");
if (!section) return;
hideSortColumn(section);
// === 插入 drag handle ===
var rows = section.querySelectorAll("tr");
rows.forEach(function(row) {
var btn = row.querySelector(".cbi-button-remove:last-of-type");
if (!btn) return;
if (btn.nextElementSibling && btn.nextElementSibling.classList.contains("drag-handle")) return;
var handle = document.createElement("span");
handle.className = "drag-handle center";
handle.title = "<%:Drag to reorder%>";
handle.innerHTML = "⠿";
btn.after(handle);
});
// === 初始化 Sortable ===
var table = section.getElementsByTagName("table")[0];
if (!table) return;
var root = table.tBodies[0] || table;
if (root._sortable_initialized) return root._sortable_instance;
root._sortable_initialized = true;
// 保存原始顺序
root._origOrder = getCurrentOrder(root);
try {
root._sortable_instance = Sortable.create(root, {
handle: ".drag-handle",
draggable: "tr.cbi-section-table-row",
animation: 150,
ghostClass: "dragging-row",
fallbackOnBody: true,
forceFallback: false,
swapThreshold: 0.65,
onEnd: function (evt) {
updateHiddenInput(root, section);
}
});
return root._sortable_instance;
} catch (e) {
root._sortable_initialized = false;
console.error("Sortable init failed:", e);
}
}
// 获取 table 当前行顺序
function getCurrentOrder(root) {
var order = [];
var rows = root.querySelectorAll("tr.cbi-section-table-row");
rows.forEach(function (tr) {
var id = tr.id || "";
if (id.startsWith("cbi-<%=appname%>-"))
id = id.replace("cbi-<%=appname%>-", "");
order.push(id);
});
return order;
}
// 拖拽完成后更新 hidden input
function updateHiddenInput(root, section) {
var newOrder = getCurrentOrder(root);
var changed = newOrder.join(" ") !== root._origOrder.join(" ");
var hiddenInput = section.querySelector('input[type="hidden"][id^="cbi.sts."]');
if (hiddenInput) {
hiddenInput.value = changed ? newOrder.join(" ") : "";
}
}
// 隐藏18.06 up/down 列
function hideSortColumn(section) {
var table = section.querySelector("table");
if (!table) return;
var ths = Array.prototype.slice.call(table.querySelectorAll("tr.cbi-section-table-titles > th"));
var dataRows = table.querySelectorAll("tr.cbi-section-table-row");
if (!ths.length || !dataRows.length) return;
var sortCol = -1;
for (var i = 0; i < ths.length; i++) {
var hasSort = false, invalid = false;
dataRows.forEach(function(tr) {
var td = tr.querySelectorAll(":scope > td")[i];
if (!td) return;
if (td.querySelector(".cbi-button-edit, .cbi-button-remove")) invalid = true;
if (td.querySelector(".cbi-button-up, .cbi-button-down")) hasSort = true;
});
if (!invalid && hasSort) { sortCol = i; break; }
}
if (sortCol === -1) return;
var rows = [table.querySelector("tr.cbi-section-table-titles")].concat(
Array.prototype.slice.call(dataRows),
Array.prototype.slice.call(table.querySelectorAll("tr.cbi-section-table-descr"))
);
rows.forEach(function(tr) {
var cells = Array.prototype.filter.call(tr.children, function(el) {
return el.tagName === "TH" || el.tagName === "TD";
});
if (cells[sortCol]) cells[sortCol].style.display = "none";
});
}
// === 等待 TypedSection 行稳定 ===
(function waitStable() {
var last = 0, stable = 0;
var THRESHOLD = 5;
function tick() {
var count = document.querySelectorAll("tr.cbi-section-table-row").length;
if (count && count === last) stable++;
else stable = 0;
last = count;
if (stable >= THRESHOLD)
setTimeout(initSortableForTable, 200);
else
requestAnimationFrame(tick);
}
tick();
})();
});
//]]> //]]>
</script> </script>
@@ -67,7 +67,10 @@ local api = self.api
return; return;
} }
if (!res || res.code !== 1 || !res.data) { if (!res || res.code !== 1 || !res.data) {
alert(res && res.msg ? res.msg : "<%:Fetch Failed%> !"); alert(res && res.msg ? res.msg : "<%:Fetch Failed!%>\n" +
"<%:Requires OpenSSL to retrieve the TLS certificate chain fingerprint.%>\n" +
"<%:QUIC protocols such as HY2 also require curl with http3 support.%>"
);
return; return;
} }
const inputs = document.querySelectorAll('input[id$=".xray_tls_pinSHA256"]'); const inputs = document.querySelectorAll('input[id$=".xray_tls_pinSHA256"]');
@@ -635,11 +635,15 @@ table td, .table .td {
if (cacheData && cacheData.savetime && (now - cacheData.timestamp) < cacheData.savetime) { if (cacheData && cacheData.savetime && (now - cacheData.timestamp) < cacheData.savetime) {
const a = ping_value[i].firstElementChild; const a = ping_value[i].firstElementChild;
let color = "red"; let color = "red";
if (cacheData.value < 100) if (cacheData.value >= 0) {
color = "green"; a.innerText = cacheData.value + " ms";
else if (cacheData.value < 200) if (cacheData.value < 100)
color = "#fb9a05"; color = "green";
a.innerText = cacheData.value + " ms"; else if (cacheData.value < 200)
color = "#fb9a05";
} else {
a.innerText = "<%:Timeout%>";
}
a.style.color = color; a.style.color = color;
a.onclick = function () { a.onclick = function () {
ping_node(cbi_id, this, auto_detection_time); ping_node(cbi_id, this, auto_detection_time);
@@ -679,15 +683,23 @@ table td, .table .td {
const cache_data = { const cache_data = {
dom_id: strs[i], dom_id: strs[i],
timestamp: Date.now(), timestamp: Date.now(),
savetime: 60 * 1000, savetime: 150 * 1000,
value: ping value: ping
}; };
text = result.ping + " ms";
localStorage.setItem(auto_detection_time + ":" + dom.address + ":" + dom.port, JSON.stringify(cache_data)); localStorage.setItem(auto_detection_time + ":" + dom.address + ":" + dom.port, JSON.stringify(cache_data));
text = result.ping + " ms";
if (ping < 100) if (ping < 100)
color = "green"; color = "green";
else if (ping < 200) else if (ping < 200)
color = "#fb9a05"; color = "#fb9a05";
} else {
const cache_data = {
dom_id: strs[i],
timestamp: Date.now(),
savetime: 60 * 1000,
value: -1
};
localStorage.setItem(auto_detection_time + ":" + dom.address + ":" + dom.port, JSON.stringify(cache_data));
} }
a.innerText = text; a.innerText = text;
a.style.color = color; a.style.color = color;
@@ -1101,6 +1113,15 @@ table td, .table .td {
onChange("auto_detection_time", auto_detection_time); onChange("auto_detection_time", auto_detection_time);
}); });
}); });
const links = document.querySelectorAll('a');
links.forEach(link => {
link.addEventListener('click', (e) => {
if (link.getAttribute('target') !== '_blank') {
ajaxAbortAll();
}
});
});
}); });
//]]> //]]>
</script> </script>
@@ -1,46 +1,6 @@
<% <%
local api = require "luci.passwall.api" local api = require "luci.passwall.api"
-%> -%>
<script src="<%=resource%>/view/<%=api.appname%>/Sortable.min.js?v=1.15.7"></script>
<style>
table .cbi-button-up,
table .cbi-button-down,
.td.cbi-section-actions .cbi-button-up,
.td.cbi-section-actions .cbi-button-down {
display: none !important;
}
.drag-handle {
vertical-align: middle;
cursor: grab !important;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 100;
padding: 0 !important;
line-height: inherit;
user-select: none;
align-self: stretch;
background-color: transparent;
}
.drag-handle:hover {
background: transparent;
}
.dragging-row {
background-color: rgba(131, 191, 255, 0.7) !important;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
@media screen and (max-width: 1152px) {
#cbi-<%=api.appname%>-subscribe_list .cbi-section-table-titles {
display: none !important;
}
}
</style>
<script type="text/javascript"> <script type="text/javascript">
//<![CDATA[ //<![CDATA[
@@ -148,129 +108,5 @@ table .cbi-button-down,
} }
}); });
} }
//订阅列表添加拖拽排序
document.addEventListener("DOMContentLoaded", function () {
function initSortableForTable() {
var section = document.getElementById("cbi-<%=api.appname%>-subscribe_list");
if (!section) return;
hideSortColumn(section);
// === 插入 drag handle ===
var rows = section.querySelectorAll("tr");
rows.forEach(function(row) {
var btn = row.querySelector(".cbi-button-remove:last-of-type");
if (!btn) return;
if (btn.nextElementSibling && btn.nextElementSibling.classList.contains("drag-handle")) return;
var handle = document.createElement("span");
handle.className = "drag-handle center";
handle.title = "<%:Drag to reorder%>";
handle.innerHTML = "⠿";
btn.after(handle);
});
// === 初始化 Sortable ===
var table = section.getElementsByTagName("table")[0];
if (!table) return;
var root = table.tBodies[0] || table;
if (root._sortable_initialized) return root._sortable_instance;
root._sortable_initialized = true;
// 保存原始顺序
root._origOrder = getCurrentOrder(root);
try {
root._sortable_instance = Sortable.create(root, {
handle: ".drag-handle",
draggable: "tr.cbi-section-table-row",
animation: 150,
ghostClass: "dragging-row",
fallbackOnBody: true,
forceFallback: false,
swapThreshold: 0.65,
onEnd: function (evt) {
updateHiddenInput(root, section);
}
});
return root._sortable_instance;
} catch (e) {
root._sortable_initialized = false;
console.error("Sortable init failed:", e);
}
}
// 获取 table 当前行顺序
function getCurrentOrder(root) {
var order = [];
var rows = root.querySelectorAll("tr.cbi-section-table-row");
rows.forEach(function (tr) {
var id = tr.id || "";
if (id.startsWith("cbi-<%=api.appname%>-"))
id = id.replace("cbi-<%=api.appname%>-", "");
order.push(id);
});
return order;
}
// 拖拽完成后更新 hidden input
function updateHiddenInput(root, section) {
var newOrder = getCurrentOrder(root);
var changed = newOrder.join(" ") !== root._origOrder.join(" ");
var hiddenInput = section.querySelector('input[type="hidden"][id^="cbi.sts."]');
if (hiddenInput) {
hiddenInput.value = changed ? newOrder.join(" ") : "";
}
}
// 隐藏18.06 up/down 列
function hideSortColumn(section) {
var table = section.querySelector("table");
if (!table) return;
var ths = Array.prototype.slice.call(table.querySelectorAll("tr.cbi-section-table-titles > th"));
var dataRows = table.querySelectorAll("tr.cbi-section-table-row");
if (!ths.length || !dataRows.length) return;
var sortCol = -1;
for (var i = 0; i < ths.length; i++) {
var hasSort = false, invalid = false;
dataRows.forEach(function(tr) {
var td = tr.querySelectorAll(":scope > td")[i];
if (!td) return;
if (td.querySelector(".cbi-button-edit, .cbi-button-remove")) invalid = true;
if (td.querySelector(".cbi-button-up, .cbi-button-down")) hasSort = true;
});
if (!invalid && hasSort) { sortCol = i; break; }
}
if (sortCol === -1) return;
var rows = [table.querySelector("tr.cbi-section-table-titles")].concat(
Array.prototype.slice.call(dataRows),
Array.prototype.slice.call(table.querySelectorAll("tr.cbi-section-table-descr"))
);
rows.forEach(function(tr) {
var cells = Array.prototype.filter.call(tr.children, function(el) {
return el.tagName === "TH" || el.tagName === "TD";
});
if (cells[sortCol]) cells[sortCol].style.display = "none";
});
}
// === 等待 TypedSection 行稳定 ===
(function waitStable() {
var last = 0, stable = 0;
var THRESHOLD = 5;
function tick() {
var count = document.querySelectorAll("tr.cbi-section-table-row").length;
if (count && count === last) stable++;
else stable = 0;
last = count;
if (stable >= THRESHOLD)
setTimeout(initSortableForTable, 200);
else
requestAnimationFrame(tick);
}
tick();
})();
});
//]]> //]]>
</script> </script>
@@ -4,16 +4,8 @@ local fs = api.fs
local has_old_geoip = fs.access("/tmp/bak_v2ray/geoip.dat") local has_old_geoip = fs.access("/tmp/bak_v2ray/geoip.dat")
local has_old_geosite = fs.access("/tmp/bak_v2ray/geosite.dat") local has_old_geosite = fs.access("/tmp/bak_v2ray/geosite.dat")
-%> -%>
<script src="<%=resource%>/view/<%=api.appname%>/Sortable.min.js?v=1.15.7"></script>
<style> <style>
table .cbi-button-up,
table .cbi-button-down,
.td.cbi-section-actions .cbi-button-up,
.td.cbi-section-actions .cbi-button-down {
display: none !important;
}
div.cbi-value[id$="-gfwlist_update"], div.cbi-value[id$="-gfwlist_update"],
div.cbi-value[id$="-chnroute_update"], div.cbi-value[id$="-chnroute_update"],
div.cbi-value[id$="-chnroute6_update"], div.cbi-value[id$="-chnroute6_update"],
@@ -22,30 +14,6 @@ local has_old_geosite = fs.access("/tmp/bak_v2ray/geosite.dat")
div.cbi-value[id$="-geosite_update"] { div.cbi-value[id$="-geosite_update"] {
display: none !important; display: none !important;
} }
.drag-handle {
vertical-align: middle;
cursor: grab !important;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 100;
padding: 0 !important;
line-height: inherit;
user-select: none;
align-self: stretch;
background-color: transparent;
}
.drag-handle:hover {
background: transparent;
}
.dragging-row {
background-color: rgba(131, 191, 255, 0.7) !important;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
</style> </style>
<div class="cbi-value" id="_rule_div"> <div class="cbi-value" id="_rule_div">
@@ -219,120 +187,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;
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.getElementsByTagName("table")[0];
if(!table) return;
var rows = table.querySelectorAll("tr.cbi-section-table-row");
if(!rows.length) return;
var colCount = rows[0].children.length, sortCol = -1;
for(var col=0; col<colCount; col++){
var hasSort=false, invalid=false;
rows.forEach(function(tr){
var td=tr.children[col]; if(!td) return;
if(td.querySelector(".cbi-button-edit, .cbi-button-remove")) invalid=true;
if(td.querySelector(".cbi-button-up, .cbi-button-down")) hasSort=true;
});
if(!invalid && hasSort){ sortCol=col; break; }
}
if(sortCol===-1) return;
table.querySelectorAll("tr").forEach(function(tr){
var c=tr.children[sortCol]; if(c) c.style.display="none";
});
}
// === 等待 TypedSection 行稳定 ===
(function waitStable() {
var last = 0, stable = 0;
var THRESHOLD = 5;
function tick() {
var count = document.querySelectorAll("tr.cbi-section-table-row").length;
if (count && count === last) stable++;
else stable = 0;
last = count;
if (stable >= THRESHOLD)
setTimeout(initSortableForTable, 200);
else
requestAnimationFrame(tick);
}
tick();
})();
});
//]]> //]]>
</script> </script>
+11 -2
View File
@@ -1862,8 +1862,14 @@ msgstr "TLS 用于验证 leaf 证书的 name。"
msgid "Fetch Manually" msgid "Fetch Manually"
msgstr "手动获取" msgstr "手动获取"
msgid "Fetch Failed" msgid "Fetch Failed!"
msgstr "获取失败" msgstr "获取失败!"
msgid "Requires OpenSSL to retrieve the TLS certificate chain fingerprint."
msgstr "获取 TLS 证书链指纹需要 OpenSSL 支持。"
msgid "QUIC protocols such as HY2 also require curl with http3 support."
msgstr "HY2 等 QUIC 协议还需要 curl 支持 http3。"
msgid "TLS Certificate (PEM)" msgid "TLS Certificate (PEM)"
msgstr "TLS 证书(PEM)" msgstr "TLS 证书(PEM)"
@@ -2180,3 +2186,6 @@ msgstr "Gecko 包大小(最小)"
msgid "Gecko Packet Size (max)" msgid "Gecko Packet Size (max)"
msgstr "Gecko 包大小(最大)" msgstr "Gecko 包大小(最大)"
msgid "valid time (hh:mm)"
msgstr "有效时间(hh:mm)"
+1 -1
View File
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2 PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.7.16 PKG_VERSION:=26.7.16
PKG_RELEASE:=63 PKG_RELEASE:=64
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
@@ -177,8 +177,8 @@ function gen_outbound(flag, node, tag, proxy_table)
tlsSettings = (node.stream_security == "tls") and { tlsSettings = (node.stream_security == "tls") and {
serverName = node.tls_serverName, serverName = node.tls_serverName,
fingerprint = (node.type == "Xray" and node.utls == "1" and node.fingerprint and node.fingerprint ~= "") and node.fingerprint or nil, fingerprint = (node.type == "Xray" and node.utls == "1" and node.fingerprint and node.fingerprint ~= "") and node.fingerprint or nil,
pinnedPeerCertSha256 = node.tls_pinSHA256, pinnedPeerCertSha256 = node.tls_pinSHA256 or "",
verifyPeerCertByName = node.tls_CertByName, verifyPeerCertByName = node.tls_CertByName or "",
echConfigList = (node.ech == "1") and node.ech_config or nil, echConfigList = (node.ech == "1") and node.ech_config or nil,
certificates = (node.tls_certificate == "1" and node.tls_certificate_pem ~= "") and { certificates = (node.tls_certificate == "1" and node.tls_certificate_pem ~= "") and {
certificate = api.split(node.tls_certificate_pem, "\n"), certificate = api.split(node.tls_certificate_pem, "\n"),
@@ -403,7 +403,7 @@
function lv_expandGroupOfCurrent(cbid, listContainer, hiddenSelect) { function lv_expandGroupOfCurrent(cbid, listContainer, hiddenSelect) {
const key = hiddenSelect.options[0].value; const key = hiddenSelect.options[0].value;
if (!key) return; if (!key) return;
const targetLi = listContainer.querySelector("li[data-key='" + key.replace(/'/g,"\\'") + "']"); const targetLi = listContainer.querySelector(`li[data-key='${CSS.escape(key)}']`);
if (!targetLi) return; if (!targetLi) return;
let parentGroup = targetLi.closest(".lv-group"); let parentGroup = targetLi.closest(".lv-group");
if (parentGroup) { if (parentGroup) {
@@ -402,7 +402,7 @@
function v_expandGroupOfCurrent(cbid, listContainer, hiddenInput) { function v_expandGroupOfCurrent(cbid, listContainer, hiddenInput) {
const key = hiddenInput.value; const key = hiddenInput.value;
if (!key) return; if (!key) return;
const targetLi = listContainer.querySelector("li[data-key='" + key.replace(/'/g,"\\'") + "']"); const targetLi = listContainer.querySelector(`li[data-key='${CSS.escape(key)}']`);
if (!targetLi) return; if (!targetLi) return;
let parentGroup = targetLi.closest(".v-group"); let parentGroup = targetLi.closest(".v-group");
if (parentGroup) { if (parentGroup) {
+2 -2
View File
@@ -7,8 +7,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=virtualdog PKG_NAME:=virtualdog
PKG_VERSION:=0.0.2 PKG_VERSION:=0.0.3
PKG_RELEASE:=2 PKG_RELEASE:=3
# Point this at the released binary tarball when you publish builds. # Point this at the released binary tarball when you publish builds.
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://github.com/Carseason/openwrt-packages/releases/download/prebuilt/ PKG_SOURCE_URL:=https://github.com/Carseason/openwrt-packages/releases/download/prebuilt/
+2 -2
View File
@@ -5,13 +5,13 @@ include $(INCLUDE_DIR)/kernel.mk
# The name and version of your package are used to define the variable to point to the build directory of your package: $(PKG_BUILD_DIR) # The name and version of your package are used to define the variable to point to the build directory of your package: $(PKG_BUILD_DIR)
PKG_NAME:=xt_tls PKG_NAME:=xt_tls
PKG_VERSION:=0.3.5 PKG_VERSION:=0.3.5
PKG_RELEASE:=1 PKG_RELEASE:=2
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://github.com/Lochnair/xt_tls.git PKG_SOURCE_URL:=https://github.com/Lochnair/xt_tls.git
PKG_SOURCE_PROTO:=git PKG_SOURCE_PROTO:=git
PKG_SOURCE_SUBDIR=$(PKG_NAME)-$(PKG_VERSION) PKG_SOURCE_SUBDIR=$(PKG_NAME)-$(PKG_VERSION)
PKG_SOURCE_VERSION:=e393d3a343ad94b5b891b7dabc83fabf22742954 PKG_SOURCE_VERSION:=137644fe539dd910c63ce527f6456ab38f148e3f
PKG_MAINTAINER:=Nils Andreas Svee <me@lochnair.net> PKG_MAINTAINER:=Nils Andreas Svee <me@lochnair.net>
PKG_LICENSE=GPL-3.0 PKG_LICENSE=GPL-3.0