Update On Fri Jul 11 20:40:25 CEST 2025

This commit is contained in:
github-action[bot]
2025-07-11 20:40:26 +02:00
parent e116426a2d
commit 4e5d06b4cd
44 changed files with 1644 additions and 912 deletions

View File

@@ -5,7 +5,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2
PKG_VERSION:=25.6.21
PKG_VERSION:=25.7.11
PKG_RELEASE:=1
PKG_CONFIG_DEPENDS:= \

View File

@@ -8,6 +8,7 @@ local http = require "luci.http"
local util = require "luci.util"
local i18n = require "luci.i18n"
local fs = api.fs
local jsonStringify = luci.jsonc.stringify
function index()
if not nixio.fs.access("/etc/config/passwall2") then
@@ -40,13 +41,14 @@ function index()
end
entry({"admin", "services", appname, "app_update"}, cbi(appname .. "/client/app_update"), _("App Update"), 95).leaf = true
entry({"admin", "services", appname, "rule"}, cbi(appname .. "/client/rule"), _("Rule Manage"), 96).leaf = true
entry({"admin", "services", appname, "geoview"}, form(appname .. "/client/geoview"), _("Geo View"), 97).leaf = true
entry({"admin", "services", appname, "node_subscribe_config"}, cbi(appname .. "/client/node_subscribe_config")).leaf = true
entry({"admin", "services", appname, "node_config"}, cbi(appname .. "/client/node_config")).leaf = true
entry({"admin", "services", appname, "shunt_rules"}, cbi(appname .. "/client/shunt_rules")).leaf = true
entry({"admin", "services", appname, "socks_config"}, cbi(appname .. "/client/socks_config")).leaf = true
entry({"admin", "services", appname, "acl"}, cbi(appname .. "/client/acl"), _("Access control"), 98).leaf = true
entry({"admin", "services", appname, "acl_config"}, cbi(appname .. "/client/acl_config")).leaf = true
entry({"admin", "services", appname, "log"}, form(appname .. "/client/log"), _("Log Maint"), 999).leaf = true
entry({"admin", "services", appname, "log"}, form(appname .. "/client/log"), _("Watch Logs"), 999).leaf = true
--[[ Server ]]
entry({"admin", "services", appname, "server"}, cbi(appname .. "/server/index"), _("Server-Side"), 99).leaf = true
@@ -90,40 +92,44 @@ function index()
end
--[[Backup]]
entry({"admin", "services", appname, "backup"}, call("create_backup")).leaf = true
entry({"admin", "services", appname, "create_backup"}, call("create_backup")).leaf = true
entry({"admin", "services", appname, "restore_backup"}, call("restore_backup")).leaf = true
--[[geoview]]
entry({"admin", "services", appname, "geo_view"}, call("geo_view")).leaf = true
end
local function http_write_json(content)
http.prepare_content("application/json")
http.write_json(content or {code = 1})
http.write(jsonStringify(content or {code = 1}))
end
function reset_config()
luci.sys.call('/etc/init.d/passwall2 stop')
luci.sys.call('[ -f "/usr/share/passwall2/0_default_config" ] && cp -f /usr/share/passwall2/0_default_config /etc/config/passwall2')
luci.http.redirect(api.url())
http.redirect(api.url())
end
function show_menu()
api.sh_uci_del(appname, "@global[0]", "hide_from_luci", true)
luci.sys.call("rm -rf /tmp/luci-*")
luci.sys.call("/etc/init.d/rpcd restart >/dev/null")
luci.http.redirect(api.url())
http.redirect(api.url())
end
function hide_menu()
api.sh_uci_set(appname, "@global[0]", "hide_from_luci", "1", true)
luci.sys.call("rm -rf /tmp/luci-*")
luci.sys.call("/etc/init.d/rpcd restart >/dev/null")
luci.http.redirect(luci.dispatcher.build_url("admin", "status", "overview"))
http.redirect(luci.dispatcher.build_url("admin", "status", "overview"))
end
function link_add_node()
-- 分片接收以突破uhttpd的限制
local tmp_file = "/tmp/links.conf"
local chunk = luci.http.formvalue("chunk")
local chunk_index = tonumber(luci.http.formvalue("chunk_index"))
local total_chunks = tonumber(luci.http.formvalue("total_chunks"))
local chunk = http.formvalue("chunk")
local chunk_index = tonumber(http.formvalue("chunk_index"))
local total_chunks = tonumber(http.formvalue("total_chunks"))
if chunk and chunk_index ~= nil and total_chunks ~= nil then
-- 按顺序拼接到文件
@@ -144,8 +150,8 @@ function link_add_node()
end
function socks_autoswitch_add_node()
local id = luci.http.formvalue("id")
local key = luci.http.formvalue("key")
local id = http.formvalue("id")
local key = http.formvalue("key")
if id and id ~= "" and key and key ~= "" then
uci:set(appname, id, "enable_autoswitch", "1")
local new_list = uci:get(appname, id, "autoswitch_backup_node") or {}
@@ -162,12 +168,12 @@ function socks_autoswitch_add_node()
uci:set_list(appname, id, "autoswitch_backup_node", new_list)
api.uci_save(uci, appname)
end
luci.http.redirect(api.url("socks_config", id))
http.redirect(api.url("socks_config", id))
end
function socks_autoswitch_remove_node()
local id = luci.http.formvalue("id")
local key = luci.http.formvalue("key")
local id = http.formvalue("id")
local key = http.formvalue("key")
if id and id ~= "" and key and key ~= "" then
uci:set(appname, id, "enable_autoswitch", "1")
local new_list = uci:get(appname, id, "autoswitch_backup_node") or {}
@@ -179,19 +185,19 @@ function socks_autoswitch_remove_node()
uci:set_list(appname, id, "autoswitch_backup_node", new_list)
api.uci_save(uci, appname)
end
luci.http.redirect(api.url("socks_config", id))
http.redirect(api.url("socks_config", id))
end
function gen_client_config()
local id = luci.http.formvalue("id")
local id = http.formvalue("id")
local config_file = api.TMP_PATH .. "/config_" .. id
luci.sys.call(string.format("/usr/share/passwall2/app.sh run_socks flag=config_%s node=%s bind=127.0.0.1 socks_port=1080 config_file=%s no_run=1", id, id, config_file))
if nixio.fs.access(config_file) then
luci.http.prepare_content("application/json")
luci.http.write(luci.sys.exec("cat " .. config_file))
http.prepare_content("application/json")
http.write(luci.sys.exec("cat " .. config_file))
luci.sys.call("rm -f " .. config_file)
else
luci.http.redirect(api.url("node_list"))
http.redirect(api.url("node_list"))
end
end
@@ -201,38 +207,37 @@ function get_now_use_node()
if node then
e["global"] = node
end
luci.http.prepare_content("application/json")
luci.http.write_json(e)
http_write_json(e)
end
function get_redir_log()
local id = luci.http.formvalue("id")
local name = luci.http.formvalue("name")
local id = http.formvalue("id")
local name = http.formvalue("name")
local file_path = "/tmp/etc/passwall2/acl/" .. id .. "/" .. name .. ".log"
if nixio.fs.access(file_path) then
local content = luci.sys.exec("tail -n 19999 '" .. file_path .. "'")
content = content:gsub("\n", "<br />")
luci.http.write(content)
http.write(content)
else
luci.http.write(string.format("<script>alert('%s');window.close();</script>", i18n.translate("Not enabled log")))
http.write(string.format("<script>alert('%s');window.close();</script>", i18n.translate("Not enabled log")))
end
end
function get_socks_log()
local name = luci.http.formvalue("name")
local name = http.formvalue("name")
local path = "/tmp/etc/passwall2/SOCKS_" .. name .. ".log"
if nixio.fs.access(path) then
local content = luci.sys.exec("tail -n 5000 ".. path)
content = content:gsub("\n", "<br />")
luci.http.write(content)
http.write(content)
else
luci.http.write(string.format("<script>alert('%s');window.close();</script>", i18n.translate("Not enabled log")))
http.write(string.format("<script>alert('%s');window.close();</script>", i18n.translate("Not enabled log")))
end
end
function get_log()
-- luci.sys.exec("[ -f /tmp/log/passwall2.log ] && sed '1!G;h;$!d' /tmp/log/passwall2.log > /tmp/log/passwall2_show.log")
luci.http.write(luci.sys.exec("[ -f '/tmp/log/passwall2.log' ] && cat /tmp/log/passwall2.log"))
http.write(luci.sys.exec("[ -f '/tmp/log/passwall2.log' ] && cat /tmp/log/passwall2.log"))
end
function clear_log()
@@ -242,20 +247,18 @@ end
function index_status()
local e = {}
e["global_status"] = luci.sys.call("/bin/busybox top -bn1 | grep -v 'grep' | grep '/tmp/etc/passwall2/bin/' | grep 'default' | grep 'global' >/dev/null") == 0
luci.http.prepare_content("application/json")
luci.http.write_json(e)
http_write_json(e)
end
function haproxy_status()
local e = luci.sys.call(string.format("/bin/busybox top -bn1 | grep -v grep | grep '%s/bin/' | grep haproxy >/dev/null", appname)) == 0
luci.http.prepare_content("application/json")
luci.http.write_json(e)
http_write_json(e)
end
function socks_status()
local e = {}
local index = luci.http.formvalue("index")
local id = luci.http.formvalue("id")
local index = http.formvalue("index")
local id = http.formvalue("id")
e.index = index
e.socks_status = luci.sys.call(string.format("/bin/busybox top -bn1 | grep -v -E 'grep|acl/|acl_' | grep '%s/bin/' | grep '%s' | grep 'SOCKS_' > /dev/null", appname, id)) == 0
local use_http = uci:get(appname, id, "http_port") or 0
@@ -264,14 +267,13 @@ function socks_status()
e.use_http = 1
e.http_status = luci.sys.call(string.format("/bin/busybox top -bn1 | grep -v -E 'grep|acl/|acl_' | grep '%s/bin/' | grep '%s' | grep -E 'HTTP_|HTTP2SOCKS' > /dev/null", appname, id)) == 0
end
luci.http.prepare_content("application/json")
luci.http.write_json(e)
http_write_json(e)
end
function connect_status()
local e = {}
e.use_time = ""
local url = luci.http.formvalue("url")
local url = http.formvalue("url")
local result = luci.sys.exec('curl --connect-timeout 3 -o /dev/null -I -sk -w "%{http_code}:%{time_appconnect}" ' .. url)
local code = tonumber(luci.sys.exec("echo -n '" .. result .. "' | awk -F ':' '{print $1}'") or "0")
if code ~= 0 then
@@ -283,15 +285,14 @@ function connect_status()
end
e.ping_type = "curl"
end
luci.http.prepare_content("application/json")
luci.http.write_json(e)
http_write_json(e)
end
function ping_node()
local index = luci.http.formvalue("index")
local address = luci.http.formvalue("address")
local port = luci.http.formvalue("port")
local type = luci.http.formvalue("type") or "icmp"
local index = http.formvalue("index")
local address = http.formvalue("address")
local port = http.formvalue("port")
local type = http.formvalue("type") or "icmp"
local e = {}
e.index = index
if type == "tcping" and luci.sys.exec("echo -n $(command -v tcping)") ~= "" then
@@ -302,13 +303,12 @@ function ping_node()
else
e.ping = luci.sys.exec("echo -n $(ping -c 1 -W 1 %q 2>&1 | grep -o 'time=[0-9]*' | awk -F '=' '{print $2}') 2>/dev/null" % address)
end
luci.http.prepare_content("application/json")
luci.http.write_json(e)
http_write_json(e)
end
function urltest_node()
local index = luci.http.formvalue("index")
local id = luci.http.formvalue("id")
local index = http.formvalue("index")
local id = http.formvalue("id")
local e = {}
e.index = index
local result = luci.sys.exec(string.format("/usr/share/passwall2/test.sh url_test_node %s %s", id, "urltest_node"))
@@ -321,21 +321,20 @@ function urltest_node()
e.use_time = string.format("%.2f", use_time / 1000)
end
end
luci.http.prepare_content("application/json")
luci.http.write_json(e)
http_write_json(e)
end
function set_node()
local type = luci.http.formvalue("type")
local config = luci.http.formvalue("config")
local section = luci.http.formvalue("section")
local type = http.formvalue("type")
local config = http.formvalue("config")
local section = http.formvalue("section")
uci:set(appname, type, config, section)
api.uci_save(uci, appname, true, true)
luci.http.redirect(api.url("log"))
http.redirect(api.url("log"))
end
function copy_node()
local section = luci.http.formvalue("section")
local section = http.formvalue("section")
local uuid = api.gen_short_uuid()
uci:section(appname, "nodes", uuid)
for k, v in pairs(uci:get_all(appname, section)) do
@@ -352,11 +351,13 @@ function copy_node()
uci:delete(appname, uuid, "add_from")
uci:set(appname, uuid, "add_mode", 1)
api.uci_save(uci, appname)
luci.http.redirect(api.url("node_config", uuid))
http.redirect(api.url("node_config", uuid))
end
function clear_all_nodes()
uci:set(appname, '@global[0]', "enabled", "0")
uci:set(appname, '@global[0]', "socks_enabled", "0")
uci:set(appname, '@haproxy_config[0]', "balancing_enable", "0")
uci:delete(appname, '@global[0]', "node")
uci:foreach(appname, "socks", function(t)
uci:delete(appname, t[".name"])
@@ -371,12 +372,15 @@ function clear_all_nodes()
uci:foreach(appname, "nodes", function(node)
uci:delete(appname, node['.name'])
end)
api.uci_save(uci, appname, true)
luci.sys.call("/etc/init.d/" .. appname .. " stop")
uci:foreach(appname, "subscribe_list", function(t)
uci:delete(appname, t[".name"], "md5")
end)
api.uci_save(uci, appname, true, true)
end
function delete_select_nodes()
local ids = luci.http.formvalue("ids")
local ids = http.formvalue("ids")
string.gsub(ids, '[^' .. "," .. ']+', function(w)
if (uci:get(appname, "@global[0]", "node") or "") == w then
uci:delete(appname, '@global[0]', "node")
@@ -413,38 +417,47 @@ function delete_select_nodes()
uci:delete(appname, t[".name"], "chain_proxy")
end
end)
if (uci:get(appname, w, "add_mode") or "0") == "2" then
local add_from = uci:get(appname, w, "add_from") or ""
if add_from ~= "" then
uci:foreach(appname, "subscribe_list", function(t)
if t["remark"] == add_from then
uci:delete(appname, t[".name"], "md5")
end
end)
end
end
uci:delete(appname, w)
end)
api.uci_save(uci, appname, true)
luci.sys.call("/etc/init.d/" .. appname .. " restart > /dev/null 2>&1 &")
api.uci_save(uci, appname, true, true)
end
function update_rules()
local update = luci.http.formvalue("update")
local update = http.formvalue("update")
luci.sys.call("lua /usr/share/passwall2/rule_update.lua log '" .. update .. "' > /dev/null 2>&1 &")
http_write_json()
end
function server_user_status()
local e = {}
e.index = luci.http.formvalue("index")
e.status = luci.sys.call(string.format("/bin/busybox top -bn1 | grep -v 'grep' | grep '%s/bin/' | grep -i '%s' >/dev/null", appname .. "_server", luci.http.formvalue("id"))) == 0
e.index = http.formvalue("index")
e.status = luci.sys.call(string.format("/bin/busybox top -bn1 | grep -v 'grep' | grep '%s/bin/' | grep -i '%s' >/dev/null", appname .. "_server", http.formvalue("id"))) == 0
http_write_json(e)
end
function server_user_log()
local id = luci.http.formvalue("id")
local id = http.formvalue("id")
if nixio.fs.access("/tmp/etc/passwall2_server/" .. id .. ".log") then
local content = luci.sys.exec("cat /tmp/etc/passwall2_server/" .. id .. ".log")
content = content:gsub("\n", "<br />")
luci.http.write(content)
http.write(content)
else
luci.http.write(string.format("<script>alert('%s');window.close();</script>", i18n.translate("Not enabled log")))
http.write(string.format("<script>alert('%s');window.close();</script>", i18n.translate("Not enabled log")))
end
end
function server_get_log()
luci.http.write(luci.sys.exec("[ -f '/tmp/log/passwall2_server.log' ] && cat /tmp/log/passwall2_server.log"))
http.write(luci.sys.exec("[ -f '/tmp/log/passwall2_server.log' ] && cat /tmp/log/passwall2_server.log"))
end
function server_clear_log()
@@ -475,12 +488,13 @@ function com_update(comname)
http_write_json(json)
end
local backup_files = {
"/etc/config/passwall2",
"/etc/config/passwall2_server",
"/usr/share/passwall2/domains_excluded"
}
function create_backup()
local backup_files = {
"/etc/config/passwall2",
"/etc/config/passwall2_server",
"/usr/share/passwall2/domains_excluded"
}
local date = os.date("%y%m%d%H%M")
local tar_file = "/tmp/passwall2-" .. date .. "-backup.tar.gz"
fs.remove(tar_file)
@@ -493,15 +507,115 @@ function create_backup()
fs.remove(tar_file)
end
function restore_backup()
local ok, err = pcall(function()
local filename = http.formvalue("filename")
local chunk = http.formvalue("chunk")
local chunk_index = tonumber(http.formvalue("chunk_index") or "-1")
local total_chunks = tonumber(http.formvalue("total_chunks") or "-1")
if not filename or not chunk then
http_write_json({ status = "error", message = "Missing filename or chunk" })
return
end
local file_path = "/tmp/" .. filename
local decoded = nixio.bin.b64decode(chunk)
local fp = io.open(file_path, "a+")
if not fp then
http_write_json({ status = "error", message = "Failed to open file for writing: " .. file_path })
return
end
fp:write(decoded)
fp:close()
if chunk_index + 1 == total_chunks then
api.sys.call("echo '' > /tmp/log/passwall2.log")
api.log(" * PassWall2 配置文件上传成功…")
local temp_dir = '/tmp/passwall_bak'
api.sys.call("mkdir -p " .. temp_dir)
if api.sys.call("tar -xzf " .. file_path .. " -C " .. temp_dir) == 0 then
for _, backup_file in ipairs(backup_files) do
local temp_file = temp_dir .. backup_file
if fs.access(temp_file) then
api.sys.call("cp -f " .. temp_file .. " " .. backup_file)
end
end
api.log(" * PassWall2 配置还原成功…")
api.log(" * 重启 PassWall2 服务中…\n")
api.sys.call('/etc/init.d/passwall2 restart > /dev/null 2>&1 &')
api.sys.call('/etc/init.d/passwall2_server restart > /dev/null 2>&1 &')
else
api.log(" * PassWall2 配置文件解压失败,请重试!")
end
api.sys.call("rm -rf " .. temp_dir)
fs.remove(file_path)
http_write_json({ status = "success", message = "Upload completed", path = file_path })
else
http_write_json({ status = "success", message = "Chunk received" })
end
end)
if not ok then
http_write_json({ status = "error", message = tostring(err) })
end
end
function geo_view()
local action = luci.http.formvalue("action")
local value = luci.http.formvalue("value")
if not value or value == "" then
http.prepare_content("text/plain")
http.write(i18n.translate("Please enter query content!"))
return
end
local geo_dir = (uci:get(appname, "@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"):match("^(.*)/")
local geosite_path = geo_dir .. "/geosite.dat"
local geoip_path = geo_dir .. "/geoip.dat"
local geo_type, file_path, cmd
local geo_string = ""
if action == "lookup" then
if api.datatypes.ipaddr(value) or api.datatypes.ip6addr(value) then
geo_type, file_path = "geoip", geoip_path
else
geo_type, file_path = "geosite", geosite_path
end
cmd = string.format("geoview -type %s -action lookup -input '%s' -value '%s' -lowmem=true", geo_type, file_path, value)
geo_string = luci.sys.exec(cmd):lower()
if geo_string ~= "" then
local lines = {}
for line in geo_string:gmatch("([^\n]*)\n?") do
if line ~= "" then
table.insert(lines, geo_type .. ":" .. line)
end
end
geo_string = table.concat(lines, "\n")
end
elseif action == "extract" then
local prefix, list = value:match("^(geoip:)(.*)$")
if not prefix then
prefix, list = value:match("^(geosite:)(.*)$")
end
if prefix and list and list ~= "" then
geo_type = prefix:sub(1, -2)
file_path = (geo_type == "geoip") and geoip_path or geosite_path
cmd = string.format("geoview -type %s -action extract -input '%s' -list '%s' -lowmem=true", geo_type, file_path, list)
geo_string = luci.sys.exec(cmd)
end
end
http.prepare_content("text/plain")
if geo_string and geo_string ~="" then
http.write(geo_string)
else
http.write(i18n.translate("No results were found!"))
end
end
function subscribe_del_node()
local remark = luci.http.formvalue("remark")
local remark = http.formvalue("remark")
if remark and remark ~= "" then
luci.sys.call("lua /usr/share/" .. appname .. "/subscribe.lua truncate " .. luci.util.shellquote(remark) .. " > /dev/null 2>&1")
end
luci.http.status(200, "OK")
http.status(200, "OK")
end
function subscribe_del_all()
luci.sys.call("lua /usr/share/" .. appname .. "/subscribe.lua truncate > /dev/null 2>&1")
luci.http.status(200, "OK")
http.status(200, "OK")
end

View File

@@ -0,0 +1,16 @@
local api = require "luci.passwall2.api"
local appname = "passwall2"
local fs = api.fs
local uci = api.uci
local geo_dir = (uci:get(appname, "@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"):match("^(.*)/")
local geosite_path = geo_dir .. "/geosite.dat"
local geoip_path = geo_dir .. "/geoip.dat"
if fs.access(geosite_path) and fs.access(geoip_path) then
f = SimpleForm(appname)
f.reset = false
f.submit = false
f:append(Template(appname .. "/rule/geoview"))
end
return f

View File

@@ -386,6 +386,10 @@ s:tab("faq", "FAQ")
o = s:taboption("faq", DummyValue, "")
o.template = appname .. "/global/faq"
s:tab("maintain", translate("Maintain"))
o = s:taboption("maintain", DummyValue, "")
o.template = appname .. "/global/backup"
-- [[ Socks Server ]]--
o = s:taboption("Main", Flag, "socks_enabled", "Socks " .. translate("Main switch"))
o.rmempty = false

View File

@@ -1,70 +1,8 @@
local api = require "luci.passwall2.api"
local appname = "passwall2"
local http = require "luci.http"
local fs = api.fs
local sys = api.sys
f = SimpleForm(appname)
f.reset = false
f.submit = false
f:append(Template(appname .. "/log/log"))
fb = SimpleForm('backup-restore')
fb.reset = false
fb.submit = false
s = fb:section(SimpleSection, translate("Backup and Restore"), translate("Backup or Restore Client and Server Configurations.") ..
"<br><font color='red'>" ..
translate("Note: Restoring configurations across different versions may cause compatibility issues.") ..
"</font>")
s.anonymous = true
s:append(Template(appname .. "/log/backup_restore"))
local backup_files = {
"/etc/config/passwall2",
"/etc/config/passwall2_server",
"/usr/share/passwall2/domains_excluded"
}
local file_path = '/tmp/passwall2_upload.tar.gz'
local temp_dir = '/tmp/passwall2_bak'
local fd
http.setfilehandler(function(meta, chunk, eof)
if not fd and meta and meta.name == "ulfile" and chunk then
sys.call("rm -rf " .. temp_dir)
fs.remove(file_path)
fd = nixio.open(file_path, "w")
sys.call("echo '' > /tmp/log/passwall2.log")
end
if fd and chunk then
fd:write(chunk)
end
if eof and fd then
fd:close()
fd = nil
if fs.access(file_path) then
api.log(" * PassWall2 配置文件上传成功…")
sys.call("mkdir -p " .. temp_dir)
if sys.call("tar -xzf " .. file_path .. " -C " .. temp_dir) == 0 then
for _, backup_file in ipairs(backup_files) do
local temp_file = temp_dir .. backup_file
if fs.access(temp_file) then
sys.call("cp -f " .. temp_file .. " " .. backup_file)
end
end
api.log(" * PassWall2 配置还原成功…")
api.log(" * 重启 PassWall2 服务中…\n")
sys.call('/etc/init.d/passwall2 restart > /dev/null 2>&1 &')
sys.call('/etc/init.d/passwall2_server restart > /dev/null 2>&1 &')
else
api.log(" * PassWall2 配置文件解压失败,请重试!")
end
else
api.log(" * PassWall2 配置文件上传失败,请重试!")
end
sys.call("rm -rf " .. temp_dir)
fs.remove(file_path)
end
end)
return f, fb
return f

View File

@@ -53,6 +53,16 @@ function s.remove(e, t)
m:set(s[".name"], "node", "default")
end
end)
if (m:get(t, "add_mode") or "0") == "2" then
local add_from = m:get(t, "add_from") or ""
if add_from ~= "" then
m.uci:foreach(appname, "subscribe_list", function(s)
if s["remark"] == add_from then
m:del(s[".name"], "md5")
end
end)
end
end
TypedSection.remove(e, t)
local new_node
local node0 = m:get("@nodes[0]") or nil

View File

@@ -155,7 +155,10 @@ local pi = s:option(Value, _n("probeInterval"), translate("Probe Interval"))
pi:depends({ [_n("balancingStrategy")] = "leastPing" })
pi:depends({ [_n("balancingStrategy")] = "leastLoad" })
pi.default = "1m"
pi.description = translate("The interval between initiating probes. The time format is numbers + units, such as '10s', '2h45m', and the supported time units are <code>ns</code>, <code>us</code>, <code>ms</code>, <code>s</code>, <code>m</code>, <code>h</code>, which correspond to nanoseconds, microseconds, milliseconds, seconds, minutes, and hours, respectively.")
pi.placeholder = "1m"
pi.description = translate("The interval between initiating probes.") .. "<br>" ..
translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are <code>s</code>, <code>m</code>, <code>h</code>, which correspond to seconds, minutes, and hours, respectively.") .. "<br>" ..
translate("When the unit is not filled in, it defaults to seconds.")
if api.compare_versions(xray_version, ">=", "1.8.12") then
ucpu:depends({ [_n("protocol")] = "_balancing" })

View File

@@ -126,20 +126,26 @@ o.description = translate("The URL used to detect the connection status.")
o = s:option(Value, _n("urltest_interval"), translate("Test interval"))
o:depends({ [_n("protocol")] = "_urltest" })
o.datatype = "uinteger"
o.default = "180"
o.description = translate("The test interval in seconds.") .. "<br />" ..
o.default = "3m"
o.placeholder = "3m"
o.description = translate("The interval between initiating probes.") .. "<br>" ..
translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are <code>s</code>, <code>m</code>, <code>h</code>, which correspond to seconds, minutes, and hours, respectively.") .. "<br>" ..
translate("When the unit is not filled in, it defaults to seconds.") .. "<br>" ..
translate("Test interval must be less or equal than idle timeout.")
o = s:option(Value, _n("urltest_tolerance"), translate("Test tolerance"), translate("The test tolerance in milliseconds."))
o:depends({ [_n("protocol")] = "_urltest" })
o.datatype = "uinteger"
o.placeholder = "50"
o.default = "50"
o = s:option(Value, _n("urltest_idle_timeout"), translate("Idle timeout"), translate("The idle timeout in seconds."))
o = s:option(Value, _n("urltest_idle_timeout"), translate("Idle timeout"))
o:depends({ [_n("protocol")] = "_urltest" })
o.datatype = "uinteger"
o.default = "1800"
o.placeholder = "30m"
o.default = "30m"
o.description = translate("The idle timeout.") .. "<br>" ..
translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are <code>s</code>, <code>m</code>, <code>h</code>, which correspond to seconds, minutes, and hours, respectively.") .. "<br>" ..
translate("When the unit is not filled in, it defaults to seconds.")
o = s:option(Flag, _n("urltest_interrupt_exist_connections"), translate("Interrupt existing connections"))
o:depends({ [_n("protocol")] = "_urltest" })

View File

@@ -48,10 +48,12 @@ o:value("none", translate("none"))
if api.is_finded("xray-plugin") then o:value("xray-plugin") end
if api.is_finded("v2ray-plugin") then o:value("v2ray-plugin") end
if api.is_finded("obfs-local") then o:value("obfs-local") end
if api.is_finded("shadow-tls") then o:value("shadow-tls") end
o = s:option(Value, _n("plugin_opts"), translate("opts"))
o:depends({ [_n("plugin")] = "xray-plugin"})
o:depends({ [_n("plugin")] = "v2ray-plugin"})
o:depends({ [_n("plugin")] = "obfs-local"})
o:depends({ [_n("plugin")] = "shadow-tls"})
api.luci_types(arg[1], m, s, type_name, option_prefix)

View File

@@ -1285,3 +1285,32 @@ function luci_types(id, m, s, type_name, option_prefix)
end
end
end
function format_go_time(input)
input = input and trim(input)
local N = 0
if input and input:match("^%d+$") then
N = tonumber(input)
elseif input and input ~= "" then
for value, unit in input:gmatch("(%d+)%s*([hms])") do
value = tonumber(value)
if unit == "h" then
N = N + value * 3600
elseif unit == "m" then
N = N + value * 60
elseif unit == "s" then
N = N + value
end
end
end
if N <= 0 then
return "0s"
end
local result = ""
local h = math.floor(N / 3600)
local m = math.floor(N % 3600 / 60)
local s = N % 60
if h > 0 then result = result .. h .. "h" end
if m > 0 then result = result .. m .. "m" end
if s > 0 or result == "" then result = result .. s .. "s" end
return result
end

View File

@@ -442,6 +442,7 @@ function gen_config_server(node)
if node.tls == "1" and node.reality == "1" then
tls.certificate_path = nil
tls.key_path = nil
tls.server_name = node.reality_handshake_server
tls.reality = {
enabled = true,
private_key = node.reality_private_key,
@@ -991,9 +992,9 @@ function gen_config(var)
tag = urltest_tag,
outbounds = valid_nodes,
url = _node.urltest_url or "https://www.gstatic.com/generate_204",
interval = _node.urltest_interval and tonumber(_node.urltest_interval) and string.format("%dm", tonumber(_node.urltest_interval) / 60) or "3m",
tolerance = _node.urltest_tolerance and tonumber(_node.urltest_tolerance) and tonumber(_node.urltest_tolerance) or 50,
idle_timeout = _node.urltest_idle_timeout and tonumber(_node.urltest_idle_timeout) and string.format("%dm", tonumber(_node.urltest_idle_timeout) / 60) or "30m",
interval = (api.format_go_time(_node.urltest_interval) ~= "0s") and api.format_go_time(_node.urltest_interval) or "3m",
tolerance = (_node.urltest_tolerance and tonumber(_node.urltest_tolerance) > 0) and tonumber(_node.urltest_tolerance) or 50,
idle_timeout = (api.format_go_time(_node.urltest_idle_timeout) ~= "0s") and api.format_go_time(_node.urltest_idle_timeout) or "30m",
interrupt_exist_connections = (_node.urltest_interrupt_exist_connections == "true" or _node.urltest_interrupt_exist_connections == "1") and true or false
}
table.insert(outbounds, outbound)

View File

@@ -787,7 +787,7 @@ function gen_config(var)
subjectSelector = { "blc-" },
pingConfig = {
destination = _node.useCustomProbeUrl and _node.probeUrl or nil,
interval = _node.probeInterval or "1m",
interval = (api.format_go_time(_node.probeInterval) ~= "0s") and api.format_go_time(_node.probeInterval) or "1m",
sampling = 3,
timeout = "5s"
}

View File

@@ -0,0 +1,223 @@
<%
local api = require "luci.passwall2.api"
-%>
<div class="cbi-section">
<h3><%:Backup and Restore%></h3>
<div class="cbi-section-descr">
<%:Backup or Restore Client and Server Configurations.%>
<br>
<font color="red"><%:Note: Restoring configurations across different versions may cause compatibility issues.%></font>
</div>
</div>
<div class="cbi-value" id="_backup_div">
<label class="cbi-value-title"><%:Create Backup File%></label>
<div class="cbi-value-field">
<input class="btn cbi-button cbi-button-save" type="button" onclick="dl_backup()" value="<%:DL Backup%>" />
</div>
</div>
<div class="cbi-value" id="_upload_div">
<label class="cbi-value-title"><%:Restore Backup File%></label>
<div class="cbi-value-field">
<input class="btn cbi-button cbi-button-apply" type="button" onclick="show_upload_win()" value="<%:RST Backup%>" />
</div>
</div>
<div class="cbi-value" id="_reset_div">
<label class="cbi-value-title"><%:Restore to default configuration%></label>
<div class="cbi-value-field">
<input class="btn cbi-button cbi-button-reset" type="button" onclick="do_reset()" value="<%:Do Reset%>" />
</div>
</div>
<div class="cbi-value"></div>
<div id="upload-modal" class="up-modal" style="display:none;">
<div class="up-modal-content">
<h3><%:Restore Backup File%></h3>
<div class="cbi-value" id="_upload_div">
<div class="up-cbi-value-field">
<input class="cbi-input-file" type="file" id="ulfile" accept=".tar.gz" />
<br />
<div class="up-button-container">
<input class="btn cbi-button cbi-button-apply" type="button" id="upload-btn" onclick="do_upload()" value="<%:UL Restore%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="close_upload_win()" value="<%:CLOSE WIN%>" />
</div>
</div>
</div>
</div>
</div>
<style>
.up-modal {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border: 2px solid #ccc;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
z-index: 1000;
}
.up-modal-content {
width: 100%;
max-width: 400px;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.up-button-container {
display: flex;
justify-content: space-between;
width: 100%;
max-width: 250px;
}
.up-cbi-value-field {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
</style>
<script>
function show_upload_win(btn) {
document.getElementById("upload-modal").style.display = "block";
}
function close_upload_win(btn) {
document.getElementById("ulfile").value = "";
document.getElementById("upload-modal").style.display = "none";
}
function dl_backup(btn) {
fetch('<%= api.url("create_backup") %>', {
method: 'POST'
})
.then(response => {
if (!response.ok) {
throw new Error("备份失败!");
}
const filename = response.headers.get("X-Backup-Filename");
if (!filename) {
return;
}
return response.blob().then(blob => ({ blob, filename }));
})
.then(result => {
if (!result) return;
const { blob, filename } = result;
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
})
.catch(error => alert(error.message));
}
function do_reset(btn) {
if (confirm("<%: Do you want to restore the client to default settings?%>")) {
setTimeout(function () {
if (confirm("<%: Are you sure you want to restore the client to default settings?%>")) {
var xhr1 = new XMLHttpRequest();
xhr1.open("GET",'<%= api.url("clear_log") %>', true);
xhr1.send();
var xhr2 = new XMLHttpRequest();
xhr2.open("GET",'<%= api.url("reset_config") %>', true);
xhr2.send();
window.location.href = '<%= api.url("log") %>'
}
}, 1000);
}
}
function do_upload(btn) {
const fileInput = document.getElementById("ulfile");
const file = fileInput.files[0];
if (!file) {
alert("<%:Please select a file first.%>");
return;
}
if (!file.name.endsWith(".tar.gz")) {
alert("<%:Invalid file type. Please upload a .tar.gz file.%>");
fileInput.value = "";
return;
}
const maxSize = 10 * 1024 * 1024; // 10MB
if (file.size > maxSize) {
alert("<%:File size exceeds 10MB limit.%>");
fileInput.value = "";
return;
}
const reader = new FileReader();
reader.onload = function (e) {
const binaryString = e.target.result; // ArrayBuffer
const binary = new Uint8Array(binaryString);
let binaryText = "";
for (let i = 0; i < binary.length; i++) {
binaryText += String.fromCharCode(binary[i]);
}
const base64Data = btoa(binaryText);
const targetByteSize = 64 * 1024; // 分片大小 64KB
let chunkSize = Math.floor(targetByteSize * 4 / 3);
chunkSize = chunkSize + (4 - (chunkSize % 4)) % 4;
const totalChunks = Math.ceil(base64Data.length / chunkSize);
let currentChunk = 0;
function sendNextChunk() {
if (currentChunk < totalChunks) {
const chunk = base64Data.substring(currentChunk * chunkSize, (currentChunk + 1) * chunkSize);
const xhr = new XMLHttpRequest();
xhr.open("POST", '<%= api.url("restore_backup") %>', true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
const resp = JSON.parse(xhr.responseText);
if (resp.status === "success") {
currentChunk++;
document.getElementById("upload-btn").value = "Uploading... " + Math.floor((currentChunk / totalChunks) * 100) + "%";
sendNextChunk();
} else {
alert("Upload error: " + resp.message);
document.getElementById("upload-btn").value = "<%:UL Restore%>";
}
} else {
alert("Upload failed with status " + xhr.status);
document.getElementById("upload-btn").value = "<%:UL Restore%>";
}
}
};
xhr.send(
"filename=" + encodeURIComponent(file.name) +
"&chunk=" + encodeURIComponent(chunk) +
"&chunk_index=" + currentChunk +
"&total_chunks=" + totalChunks
);
} else {
//alert("Upload completed.");
document.getElementById("upload-btn").value = "<%:UL Restore%>";
window.location.href = '<%= api.url("log") %>'
}
}
sendNextChunk();
};
reader.readAsArrayBuffer(file);
}
</script>

View File

@@ -1,132 +0,0 @@
<%
local api = require "luci.passwall2.api"
-%>
<div class="cbi-value" id="_backup_div">
<label class="cbi-value-title"><%:Create Backup File%></label>
<div class="cbi-value-field">
<input class="btn cbi-button cbi-button-save" type="button" onclick="dl_backup()" value="<%:DL Backup%>" />
</div>
</div>
<div class="cbi-value" id="_upload_div">
<label class="cbi-value-title"><%:Restore Backup File%></label>
<div class="cbi-value-field">
<input class="btn cbi-button cbi-button-apply" type="button" id="upload-btn" value="<%:RST Backup%>" />
</div>
</div>
<div class="cbi-value" id="_reset_div">
<label class="cbi-value-title"><%:Restore to default configuration%></label>
<div class="cbi-value-field">
<input class="btn cbi-button cbi-button-reset" type="button" onclick="do_reset()" value="<%:Do Reset%>" />
</div>
</div>
<div id="upload-modal" class="up-modal" style="display:none;">
<div class="up-modal-content">
<h3><%:Restore Backup File%></h3>
<div class="cbi-value" id="_upload_div">
<div class="up-cbi-value-field">
<input class="cbi-input-file" type="file" id="ulfile" name="ulfile" accept=".tar.gz" required />
<br />
<div class="up-button-container">
<input class="btn cbi-button cbi-button-apply" type="submit" value="<%:UL Restore%>" />
<input class="btn cbi-button cbi-button-remove" type="button" id="upload-close" value="<%:CLOSE WIN%>" />
</div>
</div>
</div>
</div>
</div>
<style>
.up-modal {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border: 2px solid #ccc;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
z-index: 1000;
}
.up-modal-content {
width: 100%;
max-width: 400px;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.up-button-container {
display: flex;
justify-content: space-between;
width: 100%;
max-width: 250px;
}
.up-cbi-value-field {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
</style>
<script>
document.getElementById("upload-btn").addEventListener("click", function() {
document.getElementById("upload-modal").style.display = "block";
});
document.getElementById("upload-close").addEventListener("click", function() {
document.getElementById("upload-modal").style.display = "none";
});
function dl_backup(btn) {
fetch('<%= api.url("backup") %>', {
method: 'POST'
})
.then(response => {
if (!response.ok) {
throw new Error("备份失败!");
}
const filename = response.headers.get("X-Backup-Filename");
if (!filename) {
return;
}
return response.blob().then(blob => ({ blob, filename }));
})
.then(result => {
if (!result) return;
const { blob, filename } = result;
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
})
.catch(error => alert(error.message));
}
function do_reset(btn) {
if (confirm("<%: Do you want to restore the client to default settings?%>")) {
setTimeout(function () {
if (confirm("<%: Are you sure you want to restore the client to default settings?%>")) {
var xhr1 = new XMLHttpRequest();
xhr1.open("GET",'<%= api.url("clear_log") %>', true);
xhr1.send();
var xhr2 = new XMLHttpRequest();
xhr2.open("GET",'<%= api.url("reset_config") %>', true);
xhr2.send();
}
}, 1000);
}
}
</script>

View File

@@ -54,13 +54,15 @@ local hysteria2_type = map:get("@global_subscribe[0]", "hysteria2_type") or "sin
}
function b64decsafe(str) {
var l;
str = str.replace(/-/g, "+").replace(/_/g, "/");
l = str.length;
l = (4 - l % 4) % 4;
if (l)
str = padright(str, l, "=");
return atob(str);
const orig = str;
try {
str = str.replace(/-/g, "+").replace(/_/g, "/");
const pad = (4 - str.length % 4) % 4;
if (pad) str += "=".repeat(pad);
return atob(str);
} catch (e) {
return orig;
}
}
function dictvalue(d, key) {
@@ -106,24 +108,11 @@ local hysteria2_type = map:get("@global_subscribe[0]", "hysteria2_type") or "sin
get: function (opt) {
var id = this.base + "." + opt;
var obj = document.getElementsByName(id)[0] || document.getElementsByClassName(id)[0] || document.getElementById(id)
// 如果找不到,返回一个模拟 DOM 的空对象,带常用方法和属性(防报错)
if (!obj) {
return {
value: "",
checked: false,
focus: function () {},
blur: function () {},
addEventListener: function () {},
removeEventListener: function () {},
classList: {
add: function () {},
remove: function () {},
toggle: function () {},
contains: function () { return false; }
}
};
if (obj) {
return obj;
} else {
return null;
}
return obj;
},
getlist: function (opt) {
var id = this.base + "." + opt;
@@ -206,6 +195,28 @@ local hysteria2_type = map:get("@global_subscribe[0]", "hysteria2_type") or "sin
url = b64encsafe(v_method.value + ":" + v_password.value) + "@" +
_address + ":" +
v_port.value + "/?";
var shadow_tls;
//生成SS Shadow-TLS 插件参数
const generateShadowTLSBase64 = function(paramStr) {
try {
let obj = {};
let list = paramStr.split(";");
for (let i = 0; i < list.length; i++) {
let kv = list[i].split("=");
if (kv.length === 2) {
let k = kv[0].trim(), v = kv[1].trim();
let m = k.match(/^v(\d+)$/);
if (m && v === "1") obj.version = m[1];
else if (k === "passwd") obj.password = v;
else obj[k] = v;
}
}
return b64encsafe(JSON.stringify(obj));
} catch (e) {
return "";
}
}
var params = "";
var v_plugin_dom = opt.get(dom_prefix + "plugin");
@@ -220,8 +231,13 @@ local hysteria2_type = map:get("@global_subscribe[0]", "hysteria2_type") or "sin
v_plugin += ";" + v_plugin_opts;
}
params += "&plugin=" + encodeURIComponent(v_plugin);
if (v_plugin_dom.value == "shadow-tls" && v_plugin_opts && v_plugin_opts != "") {
params = "shadow-tls=" + generateShadowTLSBase64(v_plugin_opts);
shadow_tls = 1;
}
}
} else {
} else if (v_type === "sing-box" || v_type === "Xray") {
var v_transport = opt.get(dom_prefix + "transport").value;
if (v_transport === "ws") {
params += opt.query("host", dom_prefix + "ws_host");
@@ -274,8 +290,31 @@ local hysteria2_type = map:get("@global_subscribe[0]", "hysteria2_type") or "sin
params += opt.query("alpn", dom_prefix + "alpn");
params += opt.query("sni", dom_prefix + "tls_serverName");
}
if (opt.get(dom_prefix + "shadowtls")?.checked) {
let st_plugin_str = "";
let st_version = opt.get(dom_prefix + "shadowtls_version")?.value;
if (st_version) st_plugin_str += "v" + st_version + "=1;";
let st_password = opt.get(dom_prefix + "shadowtls_password")?.value;
if (st_password) st_plugin_str += "passwd=" + st_password +";";
let st_host = opt.get(dom_prefix + "shadowtls_serverName")?.value;
if (st_host) st_plugin_str += "host=" + st_host +";";
if (opt.get(dom_prefix + "shadowtls_utls").checked) {
let st_fingerprint = opt.get(dom_prefix + "shadowtls_fingerprint")?.value;
if (st_fingerprint) st_plugin_str += "fingerprint=" + st_fingerprint;
}
params = "shadow-tls=" + generateShadowTLSBase64(st_plugin_str);
shadow_tls = 1;
}
}
if (shadow_tls) {
url = b64encsafe(v_method.value + ":" + v_password.value + "@" +
_address + ":" +
v_port.value) + "?";
} else {
params += "&group="
}
params += "&group="
params += "#" + encodeURIComponent(v_alias.value);
if (params[0] == "&") {
params = params.substring(1);
@@ -330,9 +369,9 @@ local hysteria2_type = map:get("@global_subscribe[0]", "hysteria2_type") or "sin
v_transport = "kcp";
info.type = opt.get(dom_prefix + "mkcp_guise").value;
} else if (v_transport === "quic") {
info.type = opt.get(dom_prefix + "quic_guise").value;
info.key = opt.get(dom_prefix + "quic_key").value;
info.securty = opt.get(dom_prefix + "quic_security").value;
info.type = opt.get(dom_prefix + "quic_guise")?.value;
info.key = opt.get(dom_prefix + "quic_key")?.value;
info.securty = opt.get(dom_prefix + "quic_security")?.value;
} else if (v_transport === "grpc") {
info.path = opt.get(dom_prefix + "grpc_serviceName").value;
}
@@ -772,232 +811,268 @@ local hysteria2_type = map:get("@global_subscribe[0]", "hysteria2_type") or "sin
opt.set('remarks', b64decutf8safe(rem));
}
if (ssu[0] === "ss") {
dom_prefix = "ss_"
var url0 = "", param = "";
var sipIndex = ssu[1].indexOf("@");
var ploc = ssu[1].indexOf("#");
if (ploc > 0) {
url0 = ssu[1].substr(0, ploc);
param = ssu[1].substr(ploc + 1);
} else {
url0 = ssu[1];
var url0 = ssu[1] || "";
param = "";
var ploc = url0.indexOf("#");
if (ploc >= 0) {
param = url0.substr(ploc + 1);
url0 = url0.substr(0, ploc);
}
var queryIndex = (url0 = url0.replace('/?', '?')).indexOf("?");
var queryStr = "";
if (queryIndex >= 0) {
queryStr = url0.substr(queryIndex + 1);
url0 = url0.substr(0, queryIndex);
}
var queryParam = {};
queryParam = Object.fromEntries(new URLSearchParams(queryStr));
var server, port, method, password, plugin, pluginOpts;
var sipIndex = url0.indexOf("@");
if (sipIndex !== -1) {
// SIP002
// SIP002 base64(method:pass)@host:port
var userInfo = b64decsafe(decodeURIComponent(url0.substr(0, sipIndex)));
var temp = url0.substr(sipIndex + 1).replace('/?', '?').split('?');
var serverInfo = temp[0].split(":");
var server = serverInfo[0];
var port = serverInfo[1];
var method, password, plugin, pluginOpts;
var queryParam = {};
if (temp[1]) {
var queryArray = temp[1].split('&');
var params;
for (var i = 0; i < queryArray.length; i++) {
params = queryArray[i].split('=');
queryParam[decodeURIComponent(params[0])] = decodeURIComponent(params[1] || '');
}
if (queryParam.plugin) {
var pluginInfo = decodeURIComponent(temp[1]);
var pluginIndex = pluginInfo.indexOf(";");
var pluginNameInfo = pluginInfo.substr(0, pluginIndex);
plugin = pluginNameInfo.substr(pluginNameInfo.indexOf("=") + 1)
pluginOpts = pluginInfo.substr(pluginIndex + 1).split("&")[0];
}
}
var temp = url0.substr(sipIndex + 1);
var serverInfo = temp.split(":");
server = serverInfo[0];
port = serverInfo[1];
var userInfoSplitIndex = userInfo.indexOf(":");
if (userInfoSplitIndex !== -1) {
method = userInfo.substr(0, userInfoSplitIndex);
password = userInfo.substr(userInfoSplitIndex + 1);
}
if (ss_type == "sing-box" && has_singbox) {
dom_prefix = "singbox_"
opt.set('type', "sing-box");
opt.set(dom_prefix + 'protocol', "shadowsocks");
} else if (ss_type == "xray" && has_xray) {
dom_prefix = "xray_"
opt.set('type', "Xray");
opt.set(dom_prefix + 'protocol', "shadowsocks");
} else if (ss_type == "shadowsocks-rust") {
} else {
// base64(method:pass@host:port)
var sstr = b64decsafe(decodeURIComponent(url0));
var m2022 = sstr.match(/^([^:]+):([^:]+):([^@]+)@([^:]+):(\d+)$/);
var mNormal = sstr.match(/^([^:]+):([^@]+)@([^:]+):(\d+)$/);
if (m2022) {
method = m2022[1];
password = m2022[2] + ":" + m2022[3];
server = m2022[4];
port = m2022[5];
} else if (mNormal) {
method = mNormal[1];
password = mNormal[2];
server = mNormal[3];
port = mNormal[4];
}
}
// 判断密码是否经过url编码
const isURLEncodedPassword = function(pwd) {
if (!/%[0-9A-Fa-f]{2}/.test(pwd)) return false;
try {
const decoded = decodeURIComponent(pwd.replace(/\+/g, "%20"));
const reencoded = encodeURIComponent(decoded);
return reencoded === pwd;
} catch (e) {
return false;
}
}
password = isURLEncodedPassword(password) ? decodeURIComponent(password) : password;
if (queryParam.plugin) {
var pluginParams = decodeURIComponent(queryParam.plugin).split(";");
plugin = pluginParams.shift();
pluginOpts = pluginParams.join(";");
}
if (ss_type == "sing-box" && has_singbox) {
dom_prefix = "singbox_"
opt.set('type', "sing-box");
opt.set(dom_prefix + 'protocol', "shadowsocks");
} else if (ss_type == "xray" && has_xray) {
dom_prefix = "xray_"
opt.set('type', "Xray");
opt.set(dom_prefix + 'protocol', "shadowsocks");
} else if (ss_type == "shadowsocks-rust") {
dom_prefix = "ssrust_"
opt.set('type', "SS-Rust");
} else {
if (["2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"].includes(method)) {
dom_prefix = "ssrust_"
opt.set('type', "SS-Rust");
} else {
if (["2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"].includes(method)) {
dom_prefix = "ssrust_"
opt.set('type', "SS-Rust");
} else {
dom_prefix = "ss_"
opt.set('type', "SS");
}
}
if (ss_type !== "xray") {
method = method.toLowerCase() === "chacha20-poly1305" ? "chacha20-ietf-poly1305" : method;
method = method.toLowerCase() === "xchacha20-poly1305" ? "xchacha20-ietf-poly1305" : method;
}
opt.set(dom_prefix + 'address', server);
opt.set(dom_prefix + 'port', port);
opt.set(dom_prefix + 'password', password || "");
opt.set(dom_prefix + 'method', method || "");
opt.set(dom_prefix + 'ss_method', method || "");
if (plugin && plugin != "none") {
plugin = (plugin === "simple-obfs") ? "obfs-local" : plugin;
opt.set(dom_prefix + 'plugin_enabled', true);
opt.set(dom_prefix + 'plugin', plugin || "none");
opt.set(dom_prefix + 'plugin_opts', pluginOpts || "");
//obfs-local插件转换成xray支持的格式
if (plugin == "obfs-local" && dom_prefix == "xray_") {
var obfs = pluginOpts.match(/obfs=([^;]+)/);
var obfs_host = pluginOpts.match(/obfs-host=([^;]+)/);
obfs = obfs ? obfs[1] : "";
obfs_host = obfs_host ? obfs_host[1] : "";
if (obfs === "http") {
opt.set(dom_prefix + 'transport', "raw");
opt.set(dom_prefix + 'tcp_guise', "http");
opt.set(dom_prefix + 'tcp_guise_http_host', obfs_host || '');
} else if (obfs === "tls") {
opt.set(dom_prefix + 'tls', true);
opt.set(dom_prefix + 'tls_serverName', obfs_host || '');
opt.set(dom_prefix + 'tls_allowInsecure', true);
}
}
}
if (param !== undefined) {
opt.set('remarks', decodeURIComponent(param));
}
if (!queryParam.plugin && (dom_prefix == "singbox_" || dom_prefix == "xray_")) {
opt.set(dom_prefix + 'encryption', queryParam.encryption);
if (queryParam.security) {
if (queryParam.security == "tls") {
opt.set(dom_prefix + 'tls', true);
opt.set(dom_prefix + 'reality', false);
opt.set(dom_prefix + 'flow', queryParam.flow || '');
opt.set(dom_prefix + 'alpn', queryParam.alpn || 'default');
opt.set(dom_prefix + 'tls_serverName', queryParam.sni || '');
opt.set(dom_prefix + 'tls_allowInsecure', true);
if (queryParam.allowinsecure === '0' || queryParam.insecure === '0') {
opt.set(dom_prefix + 'tls_allowInsecure', false);
}
if (queryParam.fp && queryParam.fp.trim() != "") {
opt.set(dom_prefix + 'utls', true);
opt.set(dom_prefix + 'fingerprint', queryParam.fp);
}
}
if (queryParam.security == "reality") {
opt.set(dom_prefix + 'tls', true);
opt.set(dom_prefix + 'reality', true);
opt.set(dom_prefix + 'flow', queryParam.flow || '');
opt.set(dom_prefix + 'alpn', queryParam.alpn || 'default');
opt.set(dom_prefix + 'tls_serverName', queryParam.sni || '');
if (queryParam.fp && queryParam.fp.trim() != "") {
opt.set(dom_prefix + 'utls', true);
opt.set(dom_prefix + 'fingerprint', queryParam.fp);
}
opt.set(dom_prefix + 'reality_publicKey', queryParam.pbk || '');
opt.set(dom_prefix + 'reality_shortId', queryParam.sid || '');
opt.set(dom_prefix + 'reality_spiderX', queryParam.spx || '');
}
}
queryParam.type = queryParam.type.toLowerCase();
if (queryParam.type === "kcp" || queryParam.type === "mkcp") {
queryParam.type = "mkcp";
}
if (queryParam.type === "h2" || queryParam.type === "http") {
queryParam.type = "http";
}
if (dom_prefix == "singbox_" && queryParam.type === "raw") {
queryParam.type = "tcp";
} else if (dom_prefix == "xray_" && queryParam.type === "tcp") {
queryParam.type = "raw";
}
if (dom_prefix == "xray_" && queryParam.type === "http") {
opt.set(dom_prefix + 'transport', "xhttp");
} else {
opt.set(dom_prefix + 'transport', queryParam.type);
}
if (queryParam.type === "raw" || queryParam.type === "tcp") {
opt.set(dom_prefix + 'tcp_guise', queryParam.headerType || "none");
if (queryParam.headerType && queryParam.headerType != "none") {
opt.set(dom_prefix + 'tcp_guise_http_host', queryParam.host || "");
opt.set(dom_prefix + 'tcp_guise_http_path', queryParam.path || "");
}
} else if (queryParam.type === "ws") {
opt.set(dom_prefix + 'ws_host', queryParam.host || "");
opt.set(dom_prefix + 'ws_path', queryParam.path || "");
if (dom_prefix == "singbox_" && queryParam.path && queryParam.path.length > 1) {
var ws_path_params = {};
var ws_path_dat = queryParam.path.split('?');
var ws_path = ws_path_dat[0];
var ws_path_params = {};
var ws_path_params_array = (ws_path_dat[1] || '').split('&');
for (i = 0; i < ws_path_params_array.length; i++) {
var kv = ws_path_params_array[i].split('=');
ws_path_params[decodeURIComponent(kv[0]).toLowerCase()] = decodeURIComponent(kv[1] || '');
}
if (ws_path_params.ed) {
opt.set(dom_prefix + 'ws_path', ws_path);
opt.set(dom_prefix + 'ws_enableEarlyData', true);
opt.set(dom_prefix + 'ws_maxEarlyData', ws_path_params.ed);
opt.set(dom_prefix + 'ws_earlyDataHeaderName', 'Sec-WebSocket-Protocol');
}
}
} else if (queryParam.type === "h2" || queryParam.type === "http") {
if (dom_prefix == "xray_") {
opt.set(dom_prefix + 'xhttp_mode', "stream-one");
opt.set(dom_prefix + 'xhttp_host', queryParam.host || "");
opt.set(dom_prefix + 'xhttp_path', queryParam.path || "");
} else {
opt.set(dom_prefix + 'http_host', queryParam.host || "");
opt.set(dom_prefix + 'http_path', queryParam.path || "");
}
} else if (queryParam.type === "quic") {
opt.set(dom_prefix + 'quic_guise', queryParam.headerType || "none");
opt.set(dom_prefix + 'quic_security', queryParam.quicSecurity);
opt.set(dom_prefix + 'quic_key', queryParam.key);
} else if (queryParam.type === "kcp" || queryParam.type === "mkcp") {
opt.set(dom_prefix + 'mkcp_guise', queryParam.headerType || "none");
} else if (queryParam.type === "grpc") {
opt.set(dom_prefix + 'grpc_serviceName', (queryParam.serviceName || queryParam.path) || "");
opt.set(dom_prefix + 'grpc_mode', queryParam.mode || "gun");
}
}
} else {
var sstr = b64decsafe(url0);
var team = sstr.split('@');
var part1 = team[0].split(':');
var part2 = team[1].split(':');
var method = part1[0]
if (ss_type == "sing-box" && has_singbox) {
dom_prefix = "singbox_"
opt.set('type', "sing-box");
opt.set(dom_prefix + 'protocol', "shadowsocks");
} else if (ss_type == "xray" && has_xray) {
dom_prefix = "xray_"
opt.set('type', "Xray");
opt.set(dom_prefix + 'protocol', "shadowsocks");
} else {
dom_prefix = "ss_"
opt.set('type', "SS");
}
if (ss_type !== "xray") {
method = method.toLowerCase() === "chacha20-poly1305" ? "chacha20-ietf-poly1305" : method;
method = method.toLowerCase() === "xchacha20-poly1305" ? "xchacha20-ietf-poly1305" : method;
}
if (ss_type !== "xray") {
method = method.toLowerCase() === "chacha20-poly1305" ? "chacha20-ietf-poly1305" : method;
method = method.toLowerCase() === "xchacha20-poly1305" ? "xchacha20-ietf-poly1305" : method;
}
opt.set(dom_prefix + 'address', server);
opt.set(dom_prefix + 'port', port);
opt.set(dom_prefix + 'password', password || "");
opt.set(dom_prefix + 'method', method || "");
opt.set(dom_prefix + 'ss_method', method || "");
if (plugin && plugin != "none") {
plugin = (plugin === "simple-obfs") ? "obfs-local" : plugin;
opt.set(dom_prefix + 'plugin_enabled', true);
opt.set(dom_prefix + 'plugin', plugin || "none");
opt.set(dom_prefix + 'plugin_opts', pluginOpts || "");
//obfs-local插件转换成xray支持的格式
if (plugin == "obfs-local" && dom_prefix == "xray_") {
var obfs = pluginOpts.match(/obfs=([^;]+)/);
var obfs_host = pluginOpts.match(/obfs-host=([^;]+)/);
obfs = obfs ? obfs[1] : "";
obfs_host = obfs_host ? obfs_host[1] : "";
if (obfs === "http") {
opt.set(dom_prefix + 'transport', "raw");
opt.set(dom_prefix + 'tcp_guise', "http");
opt.set(dom_prefix + 'tcp_guise_http_host', obfs_host || '');
} else if (obfs === "tls") {
opt.set(dom_prefix + 'tls', true);
opt.set(dom_prefix + 'tls_serverName', obfs_host || '');
opt.set(dom_prefix + 'tls_allowInsecure', true);
}
}
opt.set(dom_prefix + 'address', part2[0]);
opt.set(dom_prefix + 'port', part2[1]);
opt.set(dom_prefix + 'password', part1[1]);
opt.set(dom_prefix + 'method', method);
opt.set(dom_prefix + 'ss_method', method);
} else {
opt.set(dom_prefix + 'plugin', "none");
//opt.set(dom_prefix + 'plugin_opts', "");
if (param !== undefined) {
opt.set('remarks', decodeURIComponent(param));
}
if (param !== undefined) {
opt.set('remarks', decodeURIComponent(param));
}
if (Object.keys(queryParam).length > 0 && !queryParam.plugin) {
opt.set(dom_prefix + 'encryption', queryParam.encryption);
if (queryParam.security) {
if (queryParam.security == "tls") {
opt.set(dom_prefix + 'tls', true);
opt.set(dom_prefix + 'reality', false);
opt.set(dom_prefix + 'flow', queryParam.flow || '');
opt.set(dom_prefix + 'alpn', queryParam.alpn || 'default');
opt.set(dom_prefix + 'tls_serverName', queryParam.sni || '');
opt.set(dom_prefix + 'tls_allowInsecure', true);
if (queryParam.allowinsecure === '0' || queryParam.insecure === '0') {
opt.set(dom_prefix + 'tls_allowInsecure', false);
}
if (queryParam.fp && queryParam.fp.trim() != "") {
opt.set(dom_prefix + 'utls', true);
opt.set(dom_prefix + 'fingerprint', queryParam.fp);
}
}
if (queryParam.security == "reality") {
opt.set(dom_prefix + 'tls', true);
opt.set(dom_prefix + 'reality', true);
opt.set(dom_prefix + 'flow', queryParam.flow || '');
opt.set(dom_prefix + 'alpn', queryParam.alpn || 'default');
opt.set(dom_prefix + 'tls_serverName', queryParam.sni || '');
if (queryParam.fp && queryParam.fp.trim() != "") {
opt.set(dom_prefix + 'utls', true);
opt.set(dom_prefix + 'fingerprint', queryParam.fp);
}
opt.set(dom_prefix + 'reality_publicKey', queryParam.pbk || '');
opt.set(dom_prefix + 'reality_shortId', queryParam.sid || '');
opt.set(dom_prefix + 'reality_spiderX', queryParam.spx || '');
}
}
queryParam.type = queryParam.type?.toLowerCase();
if (queryParam.type === "kcp") {
queryParam.type = "mkcp";
}
if (queryParam.type === "h2") {
queryParam.type = "http";
}
if (dom_prefix == "singbox_" && queryParam.type === "raw") {
queryParam.type = "tcp";
} else if (dom_prefix == "xray_" && queryParam.type === "tcp") {
queryParam.type = "raw";
}
if (dom_prefix == "xray_" && queryParam.type === "http") {
opt.set(dom_prefix + 'transport', "xhttp");
} else {
opt.set(dom_prefix + 'transport', queryParam.type);
}
if (queryParam.type === "raw" || queryParam.type === "tcp") {
opt.set(dom_prefix + 'tcp_guise', queryParam.headerType || "none");
if (queryParam.headerType && queryParam.headerType != "none") {
opt.set(dom_prefix + 'tcp_guise_http_host', queryParam.host || "");
opt.set(dom_prefix + 'tcp_guise_http_path', queryParam.path || "");
}
} else if (queryParam.type === "ws") {
opt.set(dom_prefix + 'ws_host', queryParam.host || "");
opt.set(dom_prefix + 'ws_path', queryParam.path || "");
if (dom_prefix == "singbox_" && queryParam.path && queryParam.path.length > 1) {
var ws_path_params = {};
var ws_path_dat = queryParam.path.split('?');
var ws_path = ws_path_dat[0];
var ws_path_params = {};
var ws_path_params_array = (ws_path_dat[1] || '').split('&');
for (i = 0; i < ws_path_params_array.length; i++) {
var kv = ws_path_params_array[i].split('=');
ws_path_params[decodeURIComponent(kv[0]).toLowerCase()] = decodeURIComponent(kv[1] || '');
}
if (ws_path_params.ed) {
opt.set(dom_prefix + 'ws_path', ws_path);
opt.set(dom_prefix + 'ws_enableEarlyData', true);
opt.set(dom_prefix + 'ws_maxEarlyData', ws_path_params.ed);
opt.set(dom_prefix + 'ws_earlyDataHeaderName', 'Sec-WebSocket-Protocol');
}
}
} else if (queryParam.type === "http") {
if (dom_prefix == "xray_") {
opt.set(dom_prefix + 'xhttp_mode', "stream-one");
opt.set(dom_prefix + 'xhttp_host', queryParam.host || "");
opt.set(dom_prefix + 'xhttp_path', queryParam.path || "");
} else {
opt.set(dom_prefix + 'http_host', queryParam.host || "");
opt.set(dom_prefix + 'http_path', queryParam.path || "");
}
} else if (queryParam.type === "quic") {
opt.set(dom_prefix + 'quic_guise', queryParam.headerType || "none");
opt.set(dom_prefix + 'quic_security', queryParam.quicSecurity);
opt.set(dom_prefix + 'quic_key', queryParam.key);
} else if (queryParam.type === "mkcp") {
opt.set(dom_prefix + 'mkcp_guise', queryParam.headerType || "none");
} else if (queryParam.type === "grpc") {
opt.set(dom_prefix + 'grpc_serviceName', (queryParam.serviceName || queryParam.path) || "");
opt.set(dom_prefix + 'grpc_mode', queryParam.mode || "gun");
}
if (queryParam["shadow-tls"]) {
//解析SS Shadow-TLS 插件参数
const parseShadowTLSParams = function(base64Str, outObj) {
try {
let obj = JSON.parse(b64decsafe(base64Str));
if (outObj && typeof outObj === "object") {
for (let k in obj) outObj[k] = obj[k];
}
let out = [];
if (obj.version) out.push("v" + obj.version + "=1");
if (obj.password) out.push("passwd=" + obj.password);
for (let k in obj)
if (k !== "version" && k !== "password")
out.push(k + "=" + obj[k]);
return out.join(";");
} catch (e) {
return "";
}
}
if (dom_prefix === "ssrust_") {
opt.set(dom_prefix + 'plugin', "shadow-tls");
let shadowtlsOpt = parseShadowTLSParams(queryParam["shadow-tls"]);
opt.set(dom_prefix + 'plugin_opts', shadowtlsOpt || "");
} else if (dom_prefix === "singbox_") {
let shadowtlsOpt = {};
parseShadowTLSParams(queryParam["shadow-tls"], shadowtlsOpt);
if (Object.keys(shadowtlsOpt).length > 0) {
opt.set(dom_prefix + 'shadowtls', true);
opt.set(dom_prefix + 'shadowtls_version', shadowtlsOpt.version || "1");
opt.set(dom_prefix + 'shadowtls_password', shadowtlsOpt.password || "");
opt.set(dom_prefix + 'shadowtls_serverName', shadowtlsOpt.host || "");
if (shadowtlsOpt.fingerprint) {
opt.set(dom_prefix + 'shadowtls_utls', true);
opt.set(dom_prefix + 'shadowtls_fingerprint', shadowtlsOpt.fingerprint || "chrome");
}
}
}
}
}
}
@@ -1372,17 +1447,17 @@ local hysteria2_type = map:get("@global_subscribe[0]", "hysteria2_type") or "sin
dom_prefix = "singbox_"
opt.set(dom_prefix + 'protocol', "hysteria2");
opt.set(dom_prefix + 'hysteria2_auth_password', decodeURIComponent(password));
if (queryParam["obfs-password"]) {
if (queryParam["obfs-password"] || queryParam["obfs_password"]) {
opt.set(dom_prefix + 'hysteria2_obfs_type', "salamander");
opt.set(dom_prefix + 'hysteria2_obfs_password', queryParam["obfs-password"]);
opt.set(dom_prefix + 'hysteria2_obfs_password', queryParam["obfs-password"] || queryParam["obfs_password"]);
}
opt.set(dom_prefix + 'hysteria2_hop', queryParam.mport || "");
} else if (has_hysteria2) {
opt.set('type', "Hysteria2");
dom_prefix = "hysteria2_"
opt.set(dom_prefix + 'auth_password', decodeURIComponent(password));
if (queryParam["obfs-password"]) {
opt.set(dom_prefix + 'obfs', queryParam["obfs-password"]);
if (queryParam["obfs-password"] || queryParam["obfs_password"]) {
opt.set(dom_prefix + 'obfs', queryParam["obfs-password"] || queryParam["obfs_password"]);
}
if (queryParam.pinSHA256) {
opt.set(dom_prefix + 'tls_pinSHA256', queryParam.pinSHA256);

View File

@@ -0,0 +1,96 @@
<%
local api = require "luci.passwall2.api"
-%>
<style>
.faq-title {
color: var(--primary);
font-weight: bolder;
margin-bottom: 0.5rem;
display: inline-block;
}
.faq-item {
margin-bottom: 0.8rem;
line-height:1.2rem;
}
</style>
<div class="cbi-value">
<ul>
<b class="faq-title"><%:Tips:%></b>
<li class="faq-item">1. <span><%:By entering a domain or IP, you can query the Geo rule list they belong to.%></span></li>
<li class="faq-item">2. <span><%:By entering a GeoIP or Geosite, you can extract the domains/IPs they contain.%></span></li>
<li class="faq-item">3. <span><%:Use the GeoIP/Geosite query function to verify if the entered Geo rules are correct.%></span></li>
</ul>
</div>
<div class="cbi-value" id="cbi-geoview-lookup"><label class="cbi-value-title" for="geoview.lookup"><%:Domain/IP Query%></label>
<div class="cbi-value-field">
<input type="text" class="cbi-textfield" id="geoview.lookup" name="geoview.lookup" />
<input class="btn cbi-button cbi-button-apply" type="button" id="lookup-view_btn"
onclick='do_geoview(this, "lookup", document.getElementById("geoview.lookup").value)'
value="<%:Query%>" />
<br />
<div class="cbi-value-description">
<%:Enter a domain or IP to query the Geo rule list they belong to.%>
</div>
</div>
</div>
<div class="cbi-value" id="cbi-geoview-extract"><label class="cbi-value-title" for="geoview.extract"><%:GeoIP/Geosite Query%></label>
<div class="cbi-value-field">
<input type="text" class="cbi-textfield" id="geoview.extract" name="geoview.extract" />
<input class="btn cbi-button cbi-button-apply" type="button" id="extract-view_btn"
onclick='do_geoview(this, "extract", document.getElementById("geoview.extract").value)'
value="<%:Query%>" />
<br />
<div class="cbi-value-description">
<%:Enter a GeoIP or Geosite to extract the domains/IPs they contain. Format: geoip:cn or geosite:gfw%>
</div>
</div>
</div>
<div class="cbi-value">
<textarea id="geoview_textarea" class="cbi-input-textarea" style="width: 100%; margin-top: 10px;" rows="25" wrap="off" readonly="readonly"></textarea>
</div>
<script type="text/javascript">
//<![CDATA[
var lookup_btn = document.getElementById("lookup-view_btn");
var extract_btn = document.getElementById("extract-view_btn");
var QueryText = '<%:Query%>';
var QueryingText = '<%:Querying%>';
function do_geoview(btn,action,value) {
value = value.trim();
if (!value) {
alert("<%:Please enter query content!%>");
return;
}
lookup_btn.disabled = true;
extract_btn.disabled = true;
btn.value = QueryingText;
var textarea = document.getElementById('geoview_textarea');
textarea.textContent = "";
fetch('<%= api.url("geo_view") %>?action=' + action + '&value=' + encodeURIComponent(value))
.then(response => response.text())
.then(data => {
textarea.textContent = data;
lookup_btn.disabled = false;
extract_btn.disabled = false;
btn.value = QueryText;
})
}
document.getElementById("geoview.lookup").addEventListener("keydown", function(event) {
if (event.key === "Enter") {
event.preventDefault();
lookup_btn.click();
}
});
document.getElementById("geoview.extract").addEventListener("keydown", function(event) {
if (event.key === "Enter") {
event.preventDefault();
extract_btn.click();
}
});
//]]>
</script>

View File

@@ -46,6 +46,9 @@ msgstr "规则列表"
msgid "Access control"
msgstr "访问控制"
msgid "Watch Logs"
msgstr "查看日志"
msgid "Node Config"
msgstr "节点配置"
@@ -355,8 +358,14 @@ msgstr "用于检测连接状态的网址。"
msgid "Probe Interval"
msgstr "探测间隔"
msgid "The interval between initiating probes. The time format is numbers + units, such as '10s', '2h45m', and the supported time units are <code>ns</code>, <code>us</code>, <code>ms</code>, <code>s</code>, <code>m</code>, <code>h</code>, which correspond to nanoseconds, microseconds, milliseconds, seconds, minutes, and hours, respectively."
msgstr "发起探测的间隔。时间格式为数字+单位,比如<code>&quot;10s&quot;</code>, <code>&quot;2h45m&quot;</code>,支持的时间单位有 <code>ns</code><code>us</code><code>ms</code><code>s</code><code>m</code><code>h</code>,分别对应纳秒、微秒、毫秒、秒、分、时。"
msgid "The interval between initiating probes."
msgstr "发起探测的间隔。"
msgid "The time format is numbers + units, such as '10s', '2h45m', and the supported time units are <code>s</code>, <code>m</code>, <code>h</code>, which correspond to seconds, minutes, and hours, respectively."
msgstr "时间格式为数字+单位,比如<code>&quot;10s&quot;</code>, <code>&quot;2h45m&quot;</code>,支持的时间单位有 <code>s</code><code>m</code><code>h</code>,分别对应秒、分、时。"
msgid "When the unit is not filled in, it defaults to seconds."
msgstr "未填写单位时,默认为秒。"
msgid "Preferred Node Count"
msgstr "优选节点数量"
@@ -1615,8 +1624,8 @@ msgstr "仅 IPv4"
msgid "IPv6 Only"
msgstr "仅 IPv6"
msgid "Log Maint"
msgstr "日志维护"
msgid "Maintain"
msgstr "维护"
msgid "Backup and Restore"
msgstr "备份还原"
@@ -1651,6 +1660,15 @@ msgstr "恢复默认配置"
msgid "Do Reset"
msgstr "执行重置"
msgid "Please select a file first."
msgstr "请先选择一个文件。"
msgid "Invalid file type. Please upload a .tar.gz file."
msgstr "文件类型无效,请上传一个 .tar.gz 文件。"
msgid "File size exceeds 10MB limit."
msgstr "文件大小超过 10MB 限制。"
msgid "Do you want to restore the client to default settings?"
msgstr "是否要恢复客户端默认配置?"
@@ -1669,9 +1687,6 @@ msgstr "要测试的节点列表,<a target='_blank' href='https://sing-box.sag
msgid "Test interval"
msgstr "测试间隔"
msgid "The test interval in seconds."
msgstr "测试间隔时间(单位:秒)。"
msgid "Test interval must be less or equal than idle timeout."
msgstr "测试间隔时间必须小于或等于空闲超时时间。"
@@ -1684,8 +1699,8 @@ msgstr "测试容差时间(单位:毫秒)。"
msgid "Idle timeout"
msgstr "空闲超时"
msgid "The idle timeout in seconds."
msgstr "空闲超时时间(单位:秒)。"
msgid "The idle timeout."
msgstr "空闲超时时间。"
msgid "Interrupt existing connections"
msgstr "中断现有连接"
@@ -1707,3 +1722,42 @@ msgstr "自定义配置"
msgid "Must be JSON text!"
msgstr "必须是 JSON 文本内容!"
msgid "Geo View"
msgstr "Geo 查询"
msgid "Query"
msgstr "查询"
msgid "Querying"
msgstr "查询中"
msgid "Please enter query content!"
msgstr "请输入查询内容!"
msgid "No results were found!"
msgstr "未找到任何结果!"
msgid "Domain/IP Query"
msgstr "域名/IP 查询"
msgid "GeoIP/Geosite Query"
msgstr "GeoIP/Geosite 查询"
msgid "Enter a domain or IP to query the Geo rule list they belong to."
msgstr "输入域名/IP查询它们所在的 Geo 规则列表。"
msgid "Enter a GeoIP or Geosite to extract the domains/IPs they contain. Format: geoip:cn or geosite:gfw"
msgstr "输入 GeoIP/Geosite提取它们所包含的域名/IP。格式geoip:cn 或 geosite:gfw"
msgid "Tips:"
msgstr "小贴士:"
msgid "By entering a domain or IP, you can query the Geo rule list they belong to."
msgstr "可以通过输入域名/IP查询它们所在的 Geo 规则列表。"
msgid "By entering a GeoIP or Geosite, you can extract the domains/IPs they contain."
msgstr "可以通过输入 GeoIP/Geosite提取它们所包含的域名/IP。"
msgid "Use the GeoIP/Geosite query function to verify if the entered Geo rules are correct."
msgstr "利用 GeoIP/Geosite 查询功能,可以验证输入的 Geo 规则是否正确。"

View File

@@ -1240,7 +1240,7 @@ start() {
#echolog "程序已启动,先停止再重新启动!"
stop
}
mkdir -p /tmp/etc $TMP_PATH $TMP_BIN_PATH $TMP_SCRIPT_FUNC_PATH $TMP_ROUTE_PATH $TMP_ACL_PATH $TMP_PATH2
mkdir -p /tmp/etc /tmp/log $TMP_PATH $TMP_BIN_PATH $TMP_SCRIPT_FUNC_PATH $TMP_ROUTE_PATH $TMP_ACL_PATH $TMP_PATH2
get_config
export V2RAY_LOCATION_ASSET=$(config_t_get global_rules v2ray_location_asset "/usr/share/v2ray/")
export XRAY_LOCATION_ASSET=$V2RAY_LOCATION_ASSET
@@ -1312,7 +1312,7 @@ stop() {
eval_cache_var
[ -n "$USE_TABLES" ] && source $APP_PATH/${USE_TABLES}.sh stop
delete_ip2route
kill_all v2ray-plugin obfs-local
kill_all xray-plugin v2ray-plugin obfs-local shadow-tls
pgrep -f "sleep.*(6s|9s|58s)" | xargs kill -9 >/dev/null 2>&1
pgrep -af "${CONFIG}/" | awk '! /app\.sh|subscribe\.lua|rule_update\.lua|tasks\.sh|ujail/{print $1}' | xargs kill -9 >/dev/null 2>&1
unset V2RAY_LOCATION_ASSET

View File

@@ -400,18 +400,16 @@ do
end
end
-- urlencode
-- local function get_urlencode(c) return sformat("%%%02X", sbyte(c)) end
local function UrlEncode(szText)
return szText:gsub("([^%w%-_%.%~])", function(c)
return string.format("%%%02X", string.byte(c))
end)
end
-- local function urlEncode(szText)
-- local str = szText:gsub("([^0-9a-zA-Z ])", get_urlencode)
-- str = str:gsub(" ", "+")
-- return str
-- end
local function get_urldecode(h) return schar(tonumber(h, 16)) end
local function UrlDecode(szText)
return (szText and szText:gsub("+", " "):gsub("%%(%x%x)", get_urldecode)) or nil
return szText and szText:gsub("+", " "):gsub("%%(%x%x)", function(h)
return string.char(tonumber(h, 16))
end) or nil
end
-- trim
@@ -611,10 +609,9 @@ local function processData(szType, content, add_mode, add_from)
--ss://2022-blake3-aes-256-gcm:YctPZ6U7xPPcU%2Bgp3u%2B0tx%2FtRizJN9K8y%2BuKlW2qjlI%3D@192.168.100.1:8888/?plugin=v2ray-plugin%3Bserver#Example3
--ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTp0ZXN0@xxxxxx.com:443?type=ws&path=%2Ftestpath&host=xxxxxx.com&security=tls&fp=&alpn=h3%2Ch2%2Chttp%2F1.1&sni=xxxxxx.com#test-1%40ss
local idx_sp = 0
local idx_sp = content:find("#") or 0
local alias = ""
if content:find("#") then
idx_sp = content:find("#")
if idx_sp > 0 then
alias = content:sub(idx_sp + 1, -1)
end
result.remarks = UrlDecode(alias)
@@ -625,7 +622,7 @@ local function processData(szType, content, add_mode, add_from)
local query = split(info, "%?")
for _, v in pairs(split(query[2], '&')) do
local t = split(v, '=')
params[t[1]] = UrlDecode(t[2])
if #t >= 2 then params[t[1]] = UrlDecode(t[2]) end
end
if params.plugin then
local plugin_info = params.plugin
@@ -671,9 +668,22 @@ local function processData(szType, content, add_mode, add_from)
else
userinfo = base64Decode(hostInfo[1])
end
local method = userinfo:sub(1, userinfo:find(":") - 1)
local password = userinfo:sub(userinfo:find(":") + 1, #userinfo)
-- 判断密码是否经过url编码
local function isURLEncodedPassword(pwd)
if not pwd:find("%%[0-9A-Fa-f][0-9A-Fa-f]") then
return false
end
local ok, decoded = pcall(UrlDecode, pwd)
return ok and UrlEncode(decoded) == pwd
end
local decoded = UrlDecode(password)
if isURLEncodedPassword(password) and decoded then
password = decoded
end
result.method = method
result.password = password
@@ -835,6 +845,48 @@ local function processData(szType, content, add_mode, add_from)
result.error_msg = "请更换Xray或Sing-Box来支持SS更多的传输方式."
end
end
if params["shadow-tls"] then
if result.type ~= "sing-box" and result.type ~= "SS-Rust" then
result.error_msg = ss_type_default .. " 不支持 shadow-tls 插件."
else
-- 解析SS Shadow-TLS 插件参数
local function parseShadowTLSParams(b64str, out)
local ok, data = pcall(jsonParse, base64Decode(b64str))
if not ok or type(data) ~= "table" then return "" end
if type(out) == "table" then
for k, v in pairs(data) do out[k] = v end
end
local t = {}
if data.version then t[#t+1] = "v" .. data.version .. "=1" end
if data.password then t[#t+1] = "passwd=" .. data.password end
for k, v in pairs(data) do
if k ~= "version" and k ~= "password" then
t[#t+1] = k .. "=" .. tostring(v)
end
end
return table.concat(t, ";")
end
if result.type == "SS-Rust" then
result.plugin = "shadow-tls"
result.plugin_opts = parseShadowTLSParams(params["shadow-tls"])
elseif result.type == "sing-box" then
local shadowtlsOpt = {}
parseShadowTLSParams(params["shadow-tls"], shadowtlsOpt)
if next(shadowtlsOpt) then
result.shadowtls = "1"
result.shadowtls_version = shadowtlsOpt.version or "1"
result.shadowtls_password = shadowtlsOpt.password
result.shadowtls_serverName = shadowtlsOpt.host
if shadowtlsOpt.fingerprint then
result.shadowtls_utls = "1"
result.shadowtls_fingerprint = shadowtlsOpt.fingerprint or "chrome"
end
end
end
end
end
end
elseif szType == "trojan" then
if trojan_type_default == "sing-box" and has_singbox then
@@ -1275,14 +1327,14 @@ local function processData(szType, content, add_mode, add_from)
if hysteria2_type_default == "sing-box" and has_singbox then
result.type = 'sing-box'
result.protocol = "hysteria2"
if params["obfs-password"] then
if params["obfs-password"] or params["obfs_password"] then
result.hysteria2_obfs_type = "salamander"
result.hysteria2_obfs_password = params["obfs-password"]
result.hysteria2_obfs_password = params["obfs-password"] or params["obfs_password"]
end
elseif has_hysteria2 then
result.type = "Hysteria2"
if params["obfs-password"] then
result.hysteria2_obfs = params["obfs-password"]
if params["obfs-password"] or params["obfs_password"] then
result.hysteria2_obfs = params["obfs-password"] or params["obfs_password"]
end
end
elseif szType == 'tuic' then