mirror of
https://github.com/bolucat/Archive.git
synced 2025-12-24 13:28:37 +08:00
Update On Fri Jul 11 20:40:25 CEST 2025
This commit is contained in:
@@ -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:= \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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" })
|
||||
|
||||
@@ -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" })
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
@@ -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>"10s"</code>, <code>"2h45m"</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>"10s"</code>, <code>"2h45m"</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 规则是否正确。"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user