🎁 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
PKG_NAME:=ddns-go
PKG_VERSION:=6.17.1
PKG_RELEASE:=18
PKG_VERSION:=6.17.2
PKG_RELEASE:=19
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
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
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.7.15
PKG_RELEASE:=180
PKG_VERSION:=26.7.16
PKG_RELEASE:=181
PKG_PO_VERSION:=$(PKG_VERSION)
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 sni = (id ~= "") and uci:get(appname, id, "tls_serverName") or ""
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
http_write_json_error()
return
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 })
end
@@ -2,6 +2,8 @@ local api = require "luci.passwall.api"
local appname = "passwall"
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
@@ -101,4 +104,9 @@ o:value("1:65535", translate("All"))
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 appname = "passwall"
api.set_default_cbi()
m = Map(appname)
m.redirect = api.url("acl")
api.set_apply_on_parse(m)
@@ -601,4 +603,4 @@ for k, v in pairs(nodes_table) do
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 appname = "passwall"
api.set_default_cbi()
m = Map(appname)
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."))
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_chnroute = fs.access("/usr/share/passwall/rules/chnroute")
api.set_default_cbi()
m = Map(appname)
api.set_apply_on_parse(m)
@@ -879,4 +881,4 @@ footer.shunt_list = api.jsonc.stringify(shunt_list)
m:append(footer)
m:append(Template(appname .. "/global/status_bottom"))
return m
return api.return_map(m)
@@ -15,6 +15,8 @@ for k, e in ipairs(api.get_valid_nodes()) do
end
end
api.set_default_cbi()
m = Map(appname, "HAPROXY " .. translate("Load Balancing"))
api.set_apply_on_parse(m)
@@ -101,7 +103,8 @@ end
o:depends("health_check_type", "passwall_logic")
-- [[ Balancing Settings ]]--
s = m:section(TypedSection, "haproxy_config", translate("Node List"))
local cfgname = "haproxy_config"
s = m:section(TypedSection, cfgname, translate("Node List"))
s.description = "<font color='red'>" ..
translate("Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group.") .. "<br>" ..
translate("Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!") .. "</font>"
@@ -177,6 +180,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)
@@ -1,6 +1,8 @@
api = require "luci.passwall.api"
appname = "passwall"
api.set_default_cbi()
m = Map(appname, translate("Node Config"))
m.redirect = api.url("node_list")
api.set_apply_on_parse(m)
@@ -120,7 +122,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)
@@ -3,6 +3,8 @@ local appname = "passwall"
local sys = api.sys
local datatypes = api.datatypes
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)
@@ -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)
@@ -127,7 +129,8 @@ o.cfgvalue = function(self, section)
translate("Manual subscription All"))
end
s = m:section(TypedSection, "subscribe_list", "", "<font color='red'>" .. translate("When adding a new subscription, please save and apply before manually subscribing. If you only change the subscription URL, you can subscribe manually, and the system will save it automatically.") .. "</font>")
local cfgname = "subscribe_list"
s = m:section(TypedSection, cfgname, "", "<font color='red'>" .. translate("When adding a new subscription, please save and apply before manually subscribing. If you only change the subscription URL, you can subscribe manually, and the system will save it automatically.") .. "</font>")
s.addremove = true
s.anonymous = true
s.sortable = true
@@ -148,7 +151,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
@@ -223,6 +226,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)
@@ -2,6 +2,8 @@ local api = require "luci.passwall.api"
local uci = api.uci
local appname = "passwall"
api.set_default_cbi()
m = Map(appname)
m.redirect = api.url("node_subscribe")
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"))
for t = 0, 23 do o:value(t .. ":00") 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")
@@ -321,4 +317,4 @@ for k, v in pairs(nodes_table) do
end
end
return m
return api.return_map(m)
@@ -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)
@@ -43,13 +45,7 @@ for index, value in ipairs({"stop", "start", "restart"}) do
o = s:option(Value, value .. "_time_mode", translate(value .. " Time"))
for t = 0, 23 do o:value(t .. ":00") end
o.default = "0:00"
o.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")
@@ -271,4 +267,4 @@ if has_singbox then
o.default = 0
end
return m
return api.return_map(m)
@@ -3,6 +3,8 @@ local appname = "passwall"
local has_xray = api.finded_com("xray")
local has_singbox = api.finded_com("sing-box")
api.set_default_cbi()
m = Map(appname)
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"))
for t = 0, 23 do o:value(t .. ":00") 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")
@@ -146,8 +142,10 @@ end
s:append(Template(appname .. "/rule/rule_version"))
local cfgname = "shunt_rules"
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.anonymous = false
s.addremove = true
@@ -169,4 +167,9 @@ if has_xray or has_singbox then
o = s:option(DummyValue, "remarks", translate("Remarks"))
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 appname = "passwall"
local appname = api.appname
local fs = api.fs
local sys = api.sys
local uci = api.uci
@@ -2,6 +2,8 @@ local api = require "luci.passwall.api"
local appname = "passwall"
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)
@@ -245,4 +247,4 @@ ip_list.description = "<br /><ul>"
o = s:option(Flag, "invert", "Invert", translate("Invert match result.") .. " " .. translate("Only support Sing-Box."))
return m
return api.return_map(m)
@@ -1,6 +1,8 @@
local api = require "luci.passwall.api"
local appname = "passwall"
api.set_default_cbi()
m = Map(appname)
m.redirect = api.url()
api.set_apply_on_parse(m)
@@ -166,4 +168,4 @@ o = s:option(DummyValue, "btn")
o.template = appname .. "/socks_auto_switch/btn"
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.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
@@ -689,14 +697,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")
@@ -752,6 +752,14 @@ o.rows = 10
o.wrap = "off"
o.description = translate("An FinalMaskObject in JSON format, used for sharing.") .. "<br>" ..
translate("Custom finalmask overrides mkcp, hysteria2, fragment, noise, and related settings.")
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
local v = o_validate(self, value)
if v then return v end
return nil, "FinalMask " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local raw = m:get(section, "finalmask")
if raw then
@@ -761,14 +769,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"))
@@ -1,4 +1,7 @@
local api = require "luci.passwall.api"
local appname = api.appname
api.set_default_cbi()
m = Map("passwall_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,7 +89,14 @@ e = t:option(Flag, "log", translate("Log"))
e.default = "1"
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"))
return m
m:append(Template(appname .. "/server/log"))
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.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")
@@ -406,6 +406,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
@@ -415,14 +416,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."))
@@ -517,16 +510,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")
@@ -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")
@@ -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")
@@ -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")
@@ -2,6 +2,8 @@ api = require "luci.passwall.api"
appname = api.appname
fs = api.fs
api.set_default_cbi()
m = Map("passwall_server", translate("Server Config"))
m.redirect = api.url("server")
api.set_apply_on_parse(m)
@@ -56,4 +58,4 @@ footer.config = m.config
footer.section = arg[1]
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)()
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)
@@ -359,6 +367,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
@@ -1327,6 +1336,40 @@ function to_check_self()
}
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)
local fv_type
local field_type = s.fields["type"]
@@ -1673,18 +1716,29 @@ function cleanEmptyTables(t)
return next(t) and t or nil
end
function fetch_cert_sha256(host, port, sni, timeout)
function fetch_cert_sha256(host, port, sni, timeout, http3)
if not host then return "" end
port = tonumber(port) or 443
sni = sni or host
timeout = tonumber(timeout) or 5
local cmd = string.format(
"timeout %d openssl s_client -connect %s:%d -servername %s -showcerts </dev/null 2>/dev/null " ..
"| awk 'BEGIN{c=0}/BEGIN CERT/{c++} c==1{print} /END CERT/{if(c==1)exit}' " ..
"| openssl x509 -outform der 2>/dev/null " ..
"| sha256sum 2>/dev/null",
timeout, host, port, sni
)
local cmd
if http3 then
cmd = string.format(
"timeout %d curl --http3 -k -w '%%{certs}' -o /dev/null https://%s:%d 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
)
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 fp = out:match("^([0-9a-fA-F]+)")
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 {
serverName = node.tls_serverName,
fingerprint = (node.type == "Xray" and node.utls == "1" and node.fingerprint and node.fingerprint ~= "") and node.fingerprint or nil,
pinnedPeerCertSha256 = node.tls_pinSHA256,
verifyPeerCertByName = node.tls_CertByName,
pinnedPeerCertSha256 = node.tls_pinSHA256 or "",
verifyPeerCertByName = node.tls_CertByName or "",
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"),
@@ -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) {
const key = hiddenSelect.options[0].value;
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;
let parentGroup = targetLi.closest(".lv-group");
if (parentGroup) {
@@ -402,7 +402,7 @@
function v_expandGroupOfCurrent(cbid, listContainer, hiddenInput) {
const key = hiddenInput.value;
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;
let parentGroup = targetLi.closest(".v-group");
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 appname = api.appname
-%>
<script src="<%=resource%>/view/<%=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-<%=appname%>-haproxy_config .cbi-section-table-titles {
display: none !important;
}
}
@media screen and (min-width: 1152px) {
#cbi-<%=appname%>-haproxy_config input[id*=".<%=appname%>."][id$=".haproxy_port"],
#cbi-<%=appname%>-haproxy_config input[id*=".<%=appname%>."][id$=".lbweight"] {
@@ -110,129 +72,5 @@ table .cbi-button-down,
}
}, 300);
});
//节点列表添加拖拽排序
document.addEventListener("DOMContentLoaded", function () {
function initSortableForTable() {
var section = document.getElementById("cbi-<%=appname%>-haproxy_config");
if (!section) return;
hideSortColumn(section);
// === 插入 drag handle ===
var rows = section.querySelectorAll("tr");
rows.forEach(function(row) {
var btn = row.querySelector(".cbi-button-remove:last-of-type");
if (!btn) return;
if (btn.nextElementSibling && btn.nextElementSibling.classList.contains("drag-handle")) return;
var handle = document.createElement("span");
handle.className = "drag-handle center";
handle.title = "<%:Drag to reorder%>";
handle.innerHTML = "⠿";
btn.after(handle);
});
// === 初始化 Sortable ===
var table = section.getElementsByTagName("table")[0];
if (!table) return;
var root = table.tBodies[0] || table;
if (root._sortable_initialized) return root._sortable_instance;
root._sortable_initialized = true;
// 保存原始顺序
root._origOrder = getCurrentOrder(root);
try {
root._sortable_instance = Sortable.create(root, {
handle: ".drag-handle",
draggable: "tr.cbi-section-table-row",
animation: 150,
ghostClass: "dragging-row",
fallbackOnBody: true,
forceFallback: false,
swapThreshold: 0.65,
onEnd: function (evt) {
updateHiddenInput(root, section);
}
});
return root._sortable_instance;
} catch (e) {
root._sortable_initialized = false;
console.error("Sortable init failed:", e);
}
}
// 获取 table 当前行顺序
function getCurrentOrder(root) {
var order = [];
var rows = root.querySelectorAll("tr.cbi-section-table-row");
rows.forEach(function (tr) {
var id = tr.id || "";
if (id.startsWith("cbi-<%=appname%>-"))
id = id.replace("cbi-<%=appname%>-", "");
order.push(id);
});
return order;
}
// 拖拽完成后更新 hidden input
function updateHiddenInput(root, section) {
var newOrder = getCurrentOrder(root);
var changed = newOrder.join(" ") !== root._origOrder.join(" ");
var hiddenInput = section.querySelector('input[type="hidden"][id^="cbi.sts."]');
if (hiddenInput) {
hiddenInput.value = changed ? newOrder.join(" ") : "";
}
}
// 隐藏18.06 up/down 列
function hideSortColumn(section) {
var table = section.querySelector("table");
if (!table) return;
var ths = Array.prototype.slice.call(table.querySelectorAll("tr.cbi-section-table-titles > th"));
var dataRows = table.querySelectorAll("tr.cbi-section-table-row");
if (!ths.length || !dataRows.length) return;
var sortCol = -1;
for (var i = 0; i < ths.length; i++) {
var hasSort = false, invalid = false;
dataRows.forEach(function(tr) {
var td = tr.querySelectorAll(":scope > td")[i];
if (!td) return;
if (td.querySelector(".cbi-button-edit, .cbi-button-remove")) invalid = true;
if (td.querySelector(".cbi-button-up, .cbi-button-down")) hasSort = true;
});
if (!invalid && hasSort) { sortCol = i; break; }
}
if (sortCol === -1) return;
var rows = [table.querySelector("tr.cbi-section-table-titles")].concat(
Array.prototype.slice.call(dataRows),
Array.prototype.slice.call(table.querySelectorAll("tr.cbi-section-table-descr"))
);
rows.forEach(function(tr) {
var cells = Array.prototype.filter.call(tr.children, function(el) {
return el.tagName === "TH" || el.tagName === "TD";
});
if (cells[sortCol]) cells[sortCol].style.display = "none";
});
}
// === 等待 TypedSection 行稳定 ===
(function waitStable() {
var last = 0, stable = 0;
var THRESHOLD = 5;
function tick() {
var count = document.querySelectorAll("tr.cbi-section-table-row").length;
if (count && count === last) stable++;
else stable = 0;
last = count;
if (stable >= THRESHOLD)
setTimeout(initSortableForTable, 200);
else
requestAnimationFrame(tick);
}
tick();
})();
});
//]]>
</script>
@@ -67,7 +67,10 @@ local api = self.api
return;
}
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;
}
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) {
const a = ping_value[i].firstElementChild;
let color = "red";
if (cacheData.value < 100)
color = "green";
else if (cacheData.value < 200)
color = "#fb9a05";
a.innerText = cacheData.value + " ms";
if (cacheData.value >= 0) {
a.innerText = cacheData.value + " ms";
if (cacheData.value < 100)
color = "green";
else if (cacheData.value < 200)
color = "#fb9a05";
} else {
a.innerText = "<%:Timeout%>";
}
a.style.color = color;
a.onclick = function () {
ping_node(cbi_id, this, auto_detection_time);
@@ -679,15 +683,23 @@ table td, .table .td {
const cache_data = {
dom_id: strs[i],
timestamp: Date.now(),
savetime: 60 * 1000,
savetime: 150 * 1000,
value: ping
};
text = result.ping + " ms";
localStorage.setItem(auto_detection_time + ":" + dom.address + ":" + dom.port, JSON.stringify(cache_data));
text = result.ping + " ms";
if (ping < 100)
color = "green";
else if (ping < 200)
color = "#fb9a05";
} else {
const cache_data = {
dom_id: strs[i],
timestamp: Date.now(),
savetime: 60 * 1000,
value: -1
};
localStorage.setItem(auto_detection_time + ":" + dom.address + ":" + dom.port, JSON.stringify(cache_data));
}
a.innerText = text;
a.style.color = color;
@@ -1101,6 +1113,15 @@ table td, .table .td {
onChange("auto_detection_time", auto_detection_time);
});
});
const links = document.querySelectorAll('a');
links.forEach(link => {
link.addEventListener('click', (e) => {
if (link.getAttribute('target') !== '_blank') {
ajaxAbortAll();
}
});
});
});
//]]>
</script>
@@ -1,46 +1,6 @@
<%
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">
//<![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>
@@ -4,16 +4,8 @@ local fs = api.fs
local has_old_geoip = fs.access("/tmp/bak_v2ray/geoip.dat")
local has_old_geosite = fs.access("/tmp/bak_v2ray/geosite.dat")
-%>
<script src="<%=resource%>/view/<%=api.appname%>/Sortable.min.js?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;
}
div.cbi-value[id$="-gfwlist_update"],
div.cbi-value[id$="-chnroute_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"] {
display: none !important;
}
.drag-handle {
vertical-align: middle;
cursor: grab !important;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 100;
padding: 0 !important;
line-height: inherit;
user-select: none;
align-self: stretch;
background-color: transparent;
}
.drag-handle:hover {
background: transparent;
}
.dragging-row {
background-color: rgba(131, 191, 255, 0.7) !important;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
</style>
<div class="cbi-value" id="_rule_div">
@@ -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>
+11 -2
View File
@@ -1862,8 +1862,14 @@ msgstr "TLS 用于验证 leaf 证书的 name。"
msgid "Fetch Manually"
msgstr "手动获取"
msgid "Fetch Failed"
msgstr "获取失败"
msgid "Fetch Failed!"
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)"
msgstr "TLS 证书(PEM)"
@@ -2180,3 +2186,6 @@ msgstr "Gecko 包大小(最小)"
msgid "Gecko Packet Size (max)"
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_VERSION:=26.7.16
PKG_RELEASE:=63
PKG_RELEASE:=64
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -177,8 +177,8 @@ function gen_outbound(flag, node, tag, proxy_table)
tlsSettings = (node.stream_security == "tls") and {
serverName = node.tls_serverName,
fingerprint = (node.type == "Xray" and node.utls == "1" and node.fingerprint and node.fingerprint ~= "") and node.fingerprint or nil,
pinnedPeerCertSha256 = node.tls_pinSHA256,
verifyPeerCertByName = node.tls_CertByName,
pinnedPeerCertSha256 = node.tls_pinSHA256 or "",
verifyPeerCertByName = node.tls_CertByName or "",
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"),
@@ -403,7 +403,7 @@
function lv_expandGroupOfCurrent(cbid, listContainer, hiddenSelect) {
const key = hiddenSelect.options[0].value;
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;
let parentGroup = targetLi.closest(".lv-group");
if (parentGroup) {
@@ -402,7 +402,7 @@
function v_expandGroupOfCurrent(cbid, listContainer, hiddenInput) {
const key = hiddenInput.value;
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;
let parentGroup = targetLi.closest(".v-group");
if (parentGroup) {
+2 -2
View File
@@ -7,8 +7,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=virtualdog
PKG_VERSION:=0.0.2
PKG_RELEASE:=2
PKG_VERSION:=0.0.3
PKG_RELEASE:=3
# Point this at the released binary tarball when you publish builds.
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
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)
PKG_NAME:=xt_tls
PKG_VERSION:=0.3.5
PKG_RELEASE:=1
PKG_RELEASE:=2
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://github.com/Lochnair/xt_tls.git
PKG_SOURCE_PROTO:=git
PKG_SOURCE_SUBDIR=$(PKG_NAME)-$(PKG_VERSION)
PKG_SOURCE_VERSION:=e393d3a343ad94b5b891b7dabc83fabf22742954
PKG_SOURCE_VERSION:=137644fe539dd910c63ce527f6456ab38f148e3f
PKG_MAINTAINER:=Nils Andreas Svee <me@lochnair.net>
PKG_LICENSE=GPL-3.0