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: { '': {} }
});
const callNikkiAPI = rpc.declare({
object: 'luci.nikki',
method: 'api',
params: ['method', 'path', 'query', 'body'],
expect: { '': {} }
});
const callNikkiGetIdentifiers = rpc.declare({
object: 'luci.nikki',
method: 'get_identifiers',
@ -103,21 +110,8 @@ return baseclass.extend({
return callNikkiUpdateSubscription(section_id);
},
api: async function (method, path, query, body) {
const profile = await callNikkiProfile({ 'external-controller': null, 'secret': null });
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
});
updateDashboard: function () {
return callNikkiAPI('POST', '/upgrade/ui');
},
openDashboard: async function () {
@ -146,10 +140,6 @@ return baseclass.extend({
return Promise.resolve();
},
updateDashboard: function () {
return this.api('POST', '/upgrade/ui');
},
getIdentifiers: function () {
return callNikkiGetIdentifiers();
},

View File

@ -3,7 +3,7 @@
'use strict';
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 = {
version: {
@ -62,6 +62,35 @@ const methods = {
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: {
call: function() {
const users = filter(get_users(), (x) => x != '');

View File

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

View File

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

View File

@ -376,6 +376,9 @@ function clear_all_nodes()
end)
uci:foreach(appname, "subscribe_list", function(t)
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)
api.uci_save(uci, appname, true, true)
@ -440,6 +443,16 @@ function delete_select_nodes()
uci:delete(appname, t[".name"], "fallback_node")
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
local add_from = uci:get(appname, w, "add_from") or ""
if add_from ~= "" then
@ -531,20 +544,29 @@ function create_backup()
end
function restore_backup()
local result = { status = "error", message = "unknown error" }
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" })
if not filename then
result = { status = "error", message = "Missing filename" }
return
end
if not chunk then
result = { status = "error", message = "Missing chunk data" }
return
end
local file_path = "/tmp/" .. filename
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+")
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
end
fp:write(decoded)
@ -552,7 +574,7 @@ function restore_backup()
if chunk_index + 1 == total_chunks then
api.sys.call("echo '' > /tmp/log/passwall2.log")
api.log(" * PassWall2 配置文件上传成功…")
local temp_dir = '/tmp/passwall_bak'
local temp_dir = '/tmp/passwall2_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
@ -563,21 +585,23 @@ function restore_backup()
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 &')
luci.sys.call('/etc/init.d/passwall2 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
api.log(" * PassWall2 配置文件解压失败,请重试!")
result = { status = "error", message = "Decompression failed" }
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" })
result = { status = "success", message = "Chunk received" }
end
end)
if not ok then
http_write_json({ status = "error", message = tostring(err) })
result = { status = "error", message = tostring(err) }
end
http_write_json(result)
end
function geo_view()

View File

@ -84,6 +84,16 @@ function s.remove(e, t)
m:del(s[".name"], "fallback_node")
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
local add_from = m:get(t, "add_from") or ""
if add_from ~= "" then
@ -157,6 +167,8 @@ o.cfgvalue = function(t, n)
protocol = "HY2"
elseif protocol == "anytls" then
protocol = "AnyTLS"
elseif protocol == "ssh" then
protocol = "SSH"
else
protocol = protocol:gsub("^%l",string.upper)
end

View File

@ -51,6 +51,18 @@ if has_hysteria2 then
local s = "hysteria2"
table.insert(hysteria2_type, s)
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.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("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

View File

@ -65,6 +65,7 @@ end
if version_ge_1_12_0 then
o:value("anytls", "AnyTLS")
end
o:value("ssh", "SSH")
o:value("_urltest", translate("URLTest"))
o:value("_shunt", translate("Shunt"))
o:value("_iface", translate("Custom Interface"))
@ -258,6 +259,7 @@ end
o = s:option(Value, _n("username"), translate("Username"))
o:depends({ [_n("protocol")] = "http" })
o:depends({ [_n("protocol")] = "socks" })
o:depends({ [_n("protocol")] = "ssh" })
o = s:option(Value, _n("password"), translate("Password"))
o.password = true
@ -268,6 +270,7 @@ o:depends({ [_n("protocol")] = "shadowsocksr" })
o:depends({ [_n("protocol")] = "trojan" })
o:depends({ [_n("protocol")] = "tuic" })
o:depends({ [_n("protocol")] = "anytls" })
o:depends({ [_n("protocol")] = "ssh" })
o = s:option(ListValue, _n("security"), translate("Encrypt Method"))
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"})
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.default = 0
o:depends({ [_n("protocol")] = "vmess" })

View File

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

View File

@ -439,6 +439,18 @@ function gen_outbound(flag, node, tag, proxy_table)
}
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
for key, value in pairs(protocol_table) do
result[key] = value

View File

@ -772,6 +772,10 @@ function gen_config(var)
table.insert(outbounds, outbound)
fallback_node_tag = outbound.tag
end
else
if gen_balancer(fallback_node) then
fallback_node_tag = fallback_node_id
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 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) {
@ -203,12 +202,12 @@ local api = require "luci.passwall2.api"
}
}
};
xhr.send(
"filename=" + encodeURIComponent(file.name) +
"&chunk=" + encodeURIComponent(chunk) +
"&chunk_index=" + currentChunk +
"&total_chunks=" + totalChunks
);
const formData = new FormData();
formData.append("filename", file.name);
formData.append("chunk", chunk);
formData.append("chunk_index", currentChunk);
formData.append("total_chunks", totalChunks);
xhr.send(formData);
} else {
//alert("Upload completed.");
document.getElementById("upload-btn").value = "<%:UL Restore%>";

View File

@ -1645,6 +1645,15 @@ msgstr "落地节点"
msgid "Only support a layer of proxy."
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."
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."
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 domain_strategy_default = uci:get(appname, "@global_subscribe[0]", "domain_strategy") or ""
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_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
uci:set(appname, cfgid, "domain_strategy", domain_strategy_node)
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
@ -1919,6 +1930,22 @@ local execute = function()
else
domain_strategy_node = domain_strategy_default
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 access_mode = value.access_mode
local result = (not access_mode) and "自动" or (access_mode == "direct" and "直连访问" or (access_mode == "proxy" and "通过代理" or "自动"))