update 2025-08-26 09:20:45

This commit is contained in:
actions-user 2025-08-26 09:20:45 +08:00
parent e4e07d79b6
commit 893377a19f
14 changed files with 227 additions and 37 deletions

View File

@ -39,6 +39,13 @@ const callNikkiUpdateSubscription = rpc.declare({
expect: { '': {} } expect: { '': {} }
}); });
const callNikkiAPI = rpc.declare({
object: 'luci.nikki',
method: 'api',
params: ['method', 'path', 'query', 'body'],
expect: { '': {} }
});
const callNikkiGetIdentifiers = rpc.declare({ const callNikkiGetIdentifiers = rpc.declare({
object: 'luci.nikki', object: 'luci.nikki',
method: 'get_identifiers', method: 'get_identifiers',
@ -103,21 +110,8 @@ return baseclass.extend({
return callNikkiUpdateSubscription(section_id); return callNikkiUpdateSubscription(section_id);
}, },
api: async function (method, path, query, body) { updateDashboard: function () {
const profile = await callNikkiProfile({ 'external-controller': null, 'secret': null }); return callNikkiAPI('POST', '/upgrade/ui');
const apiListen = profile['external-controller'];
const apiSecret = profile['secret'] ?? '';
if (!apiListen) {
return Promise.reject('API has not been configured');
}
const apiPort = apiListen.substring(apiListen.lastIndexOf(':') + 1);
const url = `http://${window.location.hostname}:${apiPort}${path}`;
return request.request(url, {
method: method,
headers: { 'Authorization': `Bearer ${apiSecret}` },
query: query,
content: body
});
}, },
openDashboard: async function () { openDashboard: async function () {
@ -146,10 +140,6 @@ return baseclass.extend({
return Promise.resolve(); return Promise.resolve();
}, },
updateDashboard: function () {
return this.api('POST', '/upgrade/ui');
},
getIdentifiers: function () { getIdentifiers: function () {
return callNikkiGetIdentifiers(); return callNikkiGetIdentifiers();
}, },

View File

@ -3,7 +3,7 @@
'use strict'; 'use strict';
import { access, popen, writefile } from 'fs'; import { access, popen, writefile } from 'fs';
import { get_users, get_groups, get_cgroups } from '/etc/nikki/ucode/include.uc'; import { load_profile, get_users, get_groups, get_cgroups } from '/etc/nikki/ucode/include.uc';
const methods = { const methods = {
version: { version: {
@ -62,6 +62,35 @@ const methods = {
return { success: success }; return { success: success };
} }
}, },
api: {
args: { method: 'method', path: 'path', query: 'query', body: 'body' },
call: function(req) {
let result = {};
const method = req.args?.method;
const path = req.args?.path;
const query = req.args?.query;
const body = req.args?.body;
const profile = load_profile();
const api_listen = profile['external-controller'];
const api_secret = profile['secret'];
if (!api_listen) {
return result;
}
const url = api_listen + path;
const process = popen(`curl --request '${method}' --oauth2-bearer '${api_secret}' --url-query '${query}' --data '${body}' '${url}'`);
if (process) {
result = json(process);
process.close();
}
return result;
}
},
get_identifiers: { get_identifiers: {
call: function() { call: function() {
const users = filter(get_users(), (x) => x != ''); const users = filter(get_users(), (x) => x != '');

View File

@ -779,6 +779,10 @@ function gen_config(var)
table.insert(outbounds, outbound) table.insert(outbounds, outbound)
fallback_node_tag = outbound.tag fallback_node_tag = outbound.tag
end end
else
if gen_balancer(fallback_node) then
fallback_node_tag = fallback_node_id
end
end end
end end
end end

View File

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

View File

@ -376,6 +376,9 @@ function clear_all_nodes()
end) end)
uci:foreach(appname, "subscribe_list", function(t) uci:foreach(appname, "subscribe_list", function(t)
uci:delete(appname, t[".name"], "md5") uci:delete(appname, t[".name"], "md5")
uci:delete(appname, t[".name"], "chain_proxy")
uci:delete(appname, t[".name"], "preproxy_node")
uci:delete(appname, t[".name"], "to_node")
end) end)
api.uci_save(uci, appname, true, true) api.uci_save(uci, appname, true, true)
@ -440,6 +443,16 @@ function delete_select_nodes()
uci:delete(appname, t[".name"], "fallback_node") uci:delete(appname, t[".name"], "fallback_node")
end end
end) end)
uci:foreach(appname, "subscribe_list", function(t)
if t["preproxy_node"] == w then
uci:delete(appname, t[".name"], "preproxy_node")
uci:delete(appname, t[".name"], "chain_proxy")
end
if t["to_node"] == w then
uci:delete(appname, t[".name"], "to_node")
uci:delete(appname, t[".name"], "chain_proxy")
end
end)
if (uci:get(appname, w, "add_mode") or "0") == "2" then if (uci:get(appname, w, "add_mode") or "0") == "2" then
local add_from = uci:get(appname, w, "add_from") or "" local add_from = uci:get(appname, w, "add_from") or ""
if add_from ~= "" then if add_from ~= "" then
@ -531,20 +544,29 @@ function create_backup()
end end
function restore_backup() function restore_backup()
local result = { status = "error", message = "unknown error" }
local ok, err = pcall(function() local ok, err = pcall(function()
local filename = http.formvalue("filename") local filename = http.formvalue("filename")
local chunk = http.formvalue("chunk") local chunk = http.formvalue("chunk")
local chunk_index = tonumber(http.formvalue("chunk_index") or "-1") local chunk_index = tonumber(http.formvalue("chunk_index") or "-1")
local total_chunks = tonumber(http.formvalue("total_chunks") or "-1") local total_chunks = tonumber(http.formvalue("total_chunks") or "-1")
if not filename or not chunk then if not filename then
http_write_json({ status = "error", message = "Missing filename or chunk" }) result = { status = "error", message = "Missing filename" }
return
end
if not chunk then
result = { status = "error", message = "Missing chunk data" }
return return
end end
local file_path = "/tmp/" .. filename local file_path = "/tmp/" .. filename
local decoded = nixio.bin.b64decode(chunk) local decoded = nixio.bin.b64decode(chunk)
if not decoded then
result = { status = "error", message = "Base64 decode failed" }
return
end
local fp = io.open(file_path, "a+") local fp = io.open(file_path, "a+")
if not fp then if not fp then
http_write_json({ status = "error", message = "Failed to open file for writing: " .. file_path }) result = { status = "error", message = "Failed to open file: " .. file_path }
return return
end end
fp:write(decoded) fp:write(decoded)
@ -552,7 +574,7 @@ function restore_backup()
if chunk_index + 1 == total_chunks then if chunk_index + 1 == total_chunks then
api.sys.call("echo '' > /tmp/log/passwall2.log") api.sys.call("echo '' > /tmp/log/passwall2.log")
api.log(" * PassWall2 配置文件上传成功…") api.log(" * PassWall2 配置文件上传成功…")
local temp_dir = '/tmp/passwall_bak' local temp_dir = '/tmp/passwall2_bak'
api.sys.call("mkdir -p " .. temp_dir) api.sys.call("mkdir -p " .. temp_dir)
if api.sys.call("tar -xzf " .. file_path .. " -C " .. temp_dir) == 0 then if api.sys.call("tar -xzf " .. file_path .. " -C " .. temp_dir) == 0 then
for _, backup_file in ipairs(backup_files) do for _, backup_file in ipairs(backup_files) do
@ -563,21 +585,23 @@ function restore_backup()
end end
api.log(" * PassWall2 配置还原成功…") api.log(" * PassWall2 配置还原成功…")
api.log(" * 重启 PassWall2 服务中…\n") api.log(" * 重启 PassWall2 服务中…\n")
api.sys.call('/etc/init.d/passwall2 restart > /dev/null 2>&1 &') luci.sys.call('/etc/init.d/passwall2 restart > /dev/null 2>&1 &')
api.sys.call('/etc/init.d/passwall2_server restart > /dev/null 2>&1 &') luci.sys.call('/etc/init.d/passwall2_server restart > /dev/null 2>&1 &')
result = { status = "success", message = "Upload completed", path = file_path }
else else
api.log(" * PassWall2 配置文件解压失败,请重试!") api.log(" * PassWall2 配置文件解压失败,请重试!")
result = { status = "error", message = "Decompression failed" }
end end
api.sys.call("rm -rf " .. temp_dir) api.sys.call("rm -rf " .. temp_dir)
fs.remove(file_path) fs.remove(file_path)
http_write_json({ status = "success", message = "Upload completed", path = file_path })
else else
http_write_json({ status = "success", message = "Chunk received" }) result = { status = "success", message = "Chunk received" }
end end
end) end)
if not ok then if not ok then
http_write_json({ status = "error", message = tostring(err) }) result = { status = "error", message = tostring(err) }
end end
http_write_json(result)
end end
function geo_view() function geo_view()

View File

@ -84,6 +84,16 @@ function s.remove(e, t)
m:del(s[".name"], "fallback_node") m:del(s[".name"], "fallback_node")
end end
end) end)
m.uci:foreach(appname, "subscribe_list", function(s)
if s["preproxy_node"] == t then
m:del(s[".name"], "preproxy_node")
m:del(s[".name"], "chain_proxy")
end
if s["to_node"] == t then
m:del(s[".name"], "to_node")
m:del(s[".name"], "chain_proxy")
end
end)
if (m:get(t, "add_mode") or "0") == "2" then if (m:get(t, "add_mode") or "0") == "2" then
local add_from = m:get(t, "add_from") or "" local add_from = m:get(t, "add_from") or ""
if add_from ~= "" then if add_from ~= "" then
@ -157,6 +167,8 @@ o.cfgvalue = function(t, n)
protocol = "HY2" protocol = "HY2"
elseif protocol == "anytls" then elseif protocol == "anytls" then
protocol = "AnyTLS" protocol = "AnyTLS"
elseif protocol == "ssh" then
protocol = "SSH"
else else
protocol = protocol:gsub("^%l",string.upper) protocol = protocol:gsub("^%l",string.upper)
end end

View File

@ -51,6 +51,18 @@ if has_hysteria2 then
local s = "hysteria2" local s = "hysteria2"
table.insert(hysteria2_type, s) table.insert(hysteria2_type, s)
end end
local nodes_table = {}
for k, e in ipairs(api.get_valid_nodes()) do
if e.node_type == "normal" then
nodes_table[#nodes_table + 1] = {
id = e[".name"],
remark = e["remark"],
type = e["type"],
add_mode = e["add_mode"],
chain_proxy = e["chain_proxy"]
}
end
end
s = m:section(NamedSection, arg[1]) s = m:section(NamedSection, arg[1])
s.addremove = false s.addremove = false
@ -205,4 +217,29 @@ o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, li
o:value("Passwall2/OpenWrt", "PassWall2") o:value("Passwall2/OpenWrt", "PassWall2")
o:value("v2rayN/9.99", "v2rayN") o:value("v2rayN/9.99", "v2rayN")
o = s:option(ListValue, "chain_proxy", translate("Chain Proxy"))
o:value("", translate("Close(Not use)"))
o:value("1", translate("Preproxy Node"))
o:value("2", translate("Landing Node"))
local descrStr = "Chained proxy works only with Xray or Sing-box nodes.<br>"
descrStr = descrStr .. "The chained node must be the same type as your subscription node (Xray with Xray, Sing-box with Sing-box).<br>"
descrStr = descrStr .. "You can only use manual or imported nodes as chained nodes."
descrStr = translate(descrStr) .. "<br>" .. translate("Only support a layer of proxy.")
o = s:option(ListValue, "preproxy_node", translate("Preproxy Node"))
o:depends({ ["chain_proxy"] = "1" })
o.description = descrStr
o = s:option(ListValue, "to_node", translate("Landing Node"))
o:depends({ ["chain_proxy"] = "2" })
o.description = descrStr
for k, v in pairs(nodes_table) do
if (v.type == "Xray" or v.type == "sing-box") and (not v.chain_proxy or v.chain_proxy == "") and v.add_mode ~= "2" then
s.fields["preproxy_node"]:value(v.id, v.remark)
s.fields["to_node"]:value(v.id, v.remark)
end
end
return m return m

View File

@ -65,6 +65,7 @@ end
if version_ge_1_12_0 then if version_ge_1_12_0 then
o:value("anytls", "AnyTLS") o:value("anytls", "AnyTLS")
end end
o:value("ssh", "SSH")
o:value("_urltest", translate("URLTest")) o:value("_urltest", translate("URLTest"))
o:value("_shunt", translate("Shunt")) o:value("_shunt", translate("Shunt"))
o:value("_iface", translate("Custom Interface")) o:value("_iface", translate("Custom Interface"))
@ -258,6 +259,7 @@ end
o = s:option(Value, _n("username"), translate("Username")) o = s:option(Value, _n("username"), translate("Username"))
o:depends({ [_n("protocol")] = "http" }) o:depends({ [_n("protocol")] = "http" })
o:depends({ [_n("protocol")] = "socks" }) o:depends({ [_n("protocol")] = "socks" })
o:depends({ [_n("protocol")] = "ssh" })
o = s:option(Value, _n("password"), translate("Password")) o = s:option(Value, _n("password"), translate("Password"))
o.password = true o.password = true
@ -268,6 +270,7 @@ o:depends({ [_n("protocol")] = "shadowsocksr" })
o:depends({ [_n("protocol")] = "trojan" }) o:depends({ [_n("protocol")] = "trojan" })
o:depends({ [_n("protocol")] = "tuic" }) o:depends({ [_n("protocol")] = "tuic" })
o:depends({ [_n("protocol")] = "anytls" }) o:depends({ [_n("protocol")] = "anytls" })
o:depends({ [_n("protocol")] = "ssh" })
o = s:option(ListValue, _n("security"), translate("Encrypt Method")) o = s:option(ListValue, _n("security"), translate("Encrypt Method"))
for a, t in ipairs(security_list) do o:value(t) end for a, t in ipairs(security_list) do o:value(t) end
@ -456,6 +459,24 @@ if singbox_tags:find("with_quic") then
o:depends({ [_n("protocol")] = "hysteria2"}) o:depends({ [_n("protocol")] = "hysteria2"})
end end
-- [[ SSH config start ]] --
o = s:option(Value, _n("ssh_priv_key"), translate("Private Key"))
o:depends({ [_n("protocol")] = "ssh" })
o = s:option(Value, _n("ssh_priv_key_pp"), translate("Private Key Passphrase"))
o.password = true
o:depends({ [_n("protocol")] = "ssh" })
o = s:option(DynamicList, _n("ssh_host_key"), translate("Host Key"), translate("Accept any if empty."))
o:depends({ [_n("protocol")] = "ssh" })
o = s:option(DynamicList, _n("ssh_host_key_algo"), translate("Host Key Algorithms"))
o:depends({ [_n("protocol")] = "ssh" })
o = s:option(Value, _n("ssh_client_version"), translate("Client Version"), translate("Random version will be used if empty."))
o:depends({ [_n("protocol")] = "ssh" })
-- [[ SSH config end ]] --
o = s:option(Flag, _n("tls"), translate("TLS")) o = s:option(Flag, _n("tls"), translate("TLS"))
o.default = 0 o.default = 0
o:depends({ [_n("protocol")] = "vmess" }) o:depends({ [_n("protocol")] = "vmess" })

View File

@ -485,6 +485,8 @@ function get_valid_nodes()
protocol = "HY2" protocol = "HY2"
elseif protocol == "anytls" then elseif protocol == "anytls" then
protocol = "AnyTLS" protocol = "AnyTLS"
elseif protocol == "ssh" then
protocol = "SSH"
else else
protocol = protocol:gsub("^%l",string.upper) protocol = protocol:gsub("^%l",string.upper)
end end
@ -530,6 +532,8 @@ function get_node_remarks(n)
protocol = "HY2" protocol = "HY2"
elseif protocol == "anytls" then elseif protocol == "anytls" then
protocol = "AnyTLS" protocol = "AnyTLS"
elseif protocol == "ssh" then
protocol = "SSH"
else else
protocol = protocol:gsub("^%l",string.upper) protocol = protocol:gsub("^%l",string.upper)
end end

View File

@ -439,6 +439,18 @@ function gen_outbound(flag, node, tag, proxy_table)
} }
end end
if node.protocol == "ssh" then
protocol_table = {
user = (node.username and node.username ~= "") and node.username or "root",
password = (node.password and node.password ~= "") and node.password or "",
private_key = node.ssh_priv_key,
private_key_passphrase = node.ssh_priv_key_pp,
host_key = node.ssh_host_key,
host_key_algorithms = node.ssh_host_key_algo,
client_version = node.ssh_client_version
}
end
if protocol_table then if protocol_table then
for key, value in pairs(protocol_table) do for key, value in pairs(protocol_table) do
result[key] = value result[key] = value

View File

@ -772,6 +772,10 @@ function gen_config(var)
table.insert(outbounds, outbound) table.insert(outbounds, outbound)
fallback_node_tag = outbound.tag fallback_node_tag = outbound.tag
end end
else
if gen_balancer(fallback_node) then
fallback_node_tag = fallback_node_id
end
end end
end end
end end

View File

@ -184,7 +184,6 @@ local api = require "luci.passwall2.api"
const chunk = base64Data.substring(currentChunk * chunkSize, (currentChunk + 1) * chunkSize); const chunk = base64Data.substring(currentChunk * chunkSize, (currentChunk + 1) * chunkSize);
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open("POST", '<%= api.url("restore_backup") %>', true); xhr.open("POST", '<%= api.url("restore_backup") %>', true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.onreadystatechange = function () { xhr.onreadystatechange = function () {
if (xhr.readyState === 4) { if (xhr.readyState === 4) {
if (xhr.status === 200) { if (xhr.status === 200) {
@ -203,12 +202,12 @@ local api = require "luci.passwall2.api"
} }
} }
}; };
xhr.send( const formData = new FormData();
"filename=" + encodeURIComponent(file.name) + formData.append("filename", file.name);
"&chunk=" + encodeURIComponent(chunk) + formData.append("chunk", chunk);
"&chunk_index=" + currentChunk + formData.append("chunk_index", currentChunk);
"&total_chunks=" + totalChunks formData.append("total_chunks", totalChunks);
); xhr.send(formData);
} else { } else {
//alert("Upload completed."); //alert("Upload completed.");
document.getElementById("upload-btn").value = "<%:UL Restore%>"; document.getElementById("upload-btn").value = "<%:UL Restore%>";

View File

@ -1645,6 +1645,15 @@ msgstr "落地节点"
msgid "Only support a layer of proxy." msgid "Only support a layer of proxy."
msgstr "仅支持一层代理。" msgstr "仅支持一层代理。"
msgid ""
"Chained proxy works only with Xray or Sing-box nodes.<br>"
"The chained node must be the same type as your subscription node (Xray with Xray, Sing-box with Sing-box).<br>"
"You can only use manual or imported nodes as chained nodes."
msgstr ""
"链式代理仅支持 Xray 与 Sing-box 节点。<br>"
"链式节点需与订阅节点类型一致Xray 对应 XraySing-box 对应 Sing-box。<br>"
"仅支持手动添加或导入的节点用作链式节点。"
msgid "Set the default domain resolution strategy for the sing-box node." msgid "Set the default domain resolution strategy for the sing-box node."
msgstr "为 sing-box 节点设置默认的域名解析策略。" msgstr "为 sing-box 节点设置默认的域名解析策略。"
@ -1797,3 +1806,21 @@ msgstr "可以通过输入 GeoIP/Geosite提取它们所包含的域名/IP。"
msgid "Use the GeoIP/Geosite query function to verify if the entered Geo rules are correct." msgid "Use the GeoIP/Geosite query function to verify if the entered Geo rules are correct."
msgstr "利用 GeoIP/Geosite 查询功能,可以验证输入的 Geo 规则是否正确。" msgstr "利用 GeoIP/Geosite 查询功能,可以验证输入的 Geo 规则是否正确。"
msgid "Private Key Passphrase"
msgstr "私钥指纹"
msgid "Host Key"
msgstr "主机密钥"
msgid "Accept any if empty."
msgstr "留空则不校验。"
msgid "Host Key Algorithms"
msgstr "主机密钥算法"
msgid "Client Version"
msgstr "客户端版本"
msgid "Random version will be used if empty."
msgstr "如留空,则使用随机版本。"

View File

@ -35,6 +35,7 @@ local vless_type_default = uci:get(appname, "@global_subscribe[0]", "vless_type"
local hysteria2_type_default = uci:get(appname, "@global_subscribe[0]", "hysteria2_type") or "hysteria2" local hysteria2_type_default = uci:get(appname, "@global_subscribe[0]", "hysteria2_type") or "hysteria2"
local domain_strategy_default = uci:get(appname, "@global_subscribe[0]", "domain_strategy") or "" local domain_strategy_default = uci:get(appname, "@global_subscribe[0]", "domain_strategy") or ""
local domain_strategy_node = "" local domain_strategy_node = ""
local preproxy_node_group, to_node_group, chain_node_type = "", "", ""
-- 判断是否过滤节点关键字 -- 判断是否过滤节点关键字
local filter_keyword_mode_default = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0" local filter_keyword_mode_default = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0"
local filter_keyword_discard_list_default = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {} local filter_keyword_discard_list_default = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {}
@ -1717,6 +1718,16 @@ local function update_node(manual)
if kkk == "type" and vvv == "sing-box" then if kkk == "type" and vvv == "sing-box" then
uci:set(appname, cfgid, "domain_strategy", domain_strategy_node) uci:set(appname, cfgid, "domain_strategy", domain_strategy_node)
end end
-- 订阅组链式代理
if chain_node_type ~= "" and kkk == "type" and vvv == chain_node_type then
if preproxy_node_group ~="" then
uci:set(appname, cfgid, "chain_proxy", "1")
uci:set(appname, cfgid, "preproxy_node", preproxy_node_group)
elseif to_node_group ~= "" then
uci:set(appname, cfgid, "chain_proxy", "2")
uci:set(appname, cfgid, "to_node", to_node_group)
end
end
end end
end end
end end
@ -1919,6 +1930,22 @@ local execute = function()
else else
domain_strategy_node = domain_strategy_default domain_strategy_node = domain_strategy_default
end end
-- 订阅组链式代理
local function valid_chain_node(node)
if not node then return "" end
local cp = uci:get(appname, node, "chain_proxy") or ""
local am = uci:get(appname, node, "add_mode") or "0"
chain_node_type = (cp == "" and am ~= "2") and (uci:get(appname, node, "type") or "") or ""
if chain_node_type ~= "Xray" and chain_node_type ~= "sing-box" then
chain_node_type = ""
return ""
end
return node
end
preproxy_node_group = (value.chain_proxy == "1") and valid_chain_node(value.preproxy_node) or ""
to_node_group = (value.chain_proxy == "2") and valid_chain_node(value.to_node) or ""
local ua = value.user_agent local ua = value.user_agent
local access_mode = value.access_mode local access_mode = value.access_mode
local result = (not access_mode) and "自动" or (access_mode == "direct" and "直连访问" or (access_mode == "proxy" and "通过代理" or "自动")) local result = (not access_mode) and "自动" or (access_mode == "direct" and "直连访问" or (access_mode == "proxy" and "通过代理" or "自动"))