mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-30 07:35:13 +08:00
🎨 Sync 2026-09-15 11:19:33
This commit is contained in:
1 parent
a9e0557b71
commit
ac76f4cbc9
106 files changed
+17125
-19121
No files matched your search
@@ -34,14 +34,6 @@
|
||||
"path": "homeproxy/server"
|
||||
}
|
||||
},
|
||||
"admin/services/homeproxy/backup": {
|
||||
"title": "Backup / Restore",
|
||||
"order": 25,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "homeproxy/backup"
|
||||
}
|
||||
},
|
||||
"admin/services/homeproxy/status": {
|
||||
"title": "Service Status",
|
||||
"order": 30,
|
||||
|
||||
@@ -3,58 +3,25 @@
|
||||
"description": "Grant access to homeproxy configuration",
|
||||
"read": {
|
||||
"file": {
|
||||
"/etc/homeproxy/scripts/update_subscriptions.uc": [ "exec" ],
|
||||
"/var/run/homeproxy/homeproxy.log": [ "read" ],
|
||||
"/var/run/homeproxy/sing-box-c.log": [ "read" ],
|
||||
"/var/run/homeproxy/sing-box-s.log": [ "read" ]
|
||||
},
|
||||
"ubus": {
|
||||
"rc": [ "*" ],
|
||||
"service": [ "list" ],
|
||||
"luci.homeproxy": [
|
||||
"acllist_read",
|
||||
"connection_check",
|
||||
"resources_get_version",
|
||||
"singbox_get_features"
|
||||
],
|
||||
"luci.homeproxy_node_tools": [
|
||||
"config_diagnostics",
|
||||
"node_references",
|
||||
"preview_node_filter",
|
||||
"update_subscriptions_status",
|
||||
"validate_regex"
|
||||
]
|
||||
"luci.homeproxy": [ "*" ]
|
||||
},
|
||||
"uci": [ "homeproxy" ]
|
||||
},
|
||||
"write": {
|
||||
"file": {
|
||||
"/tmp/homeproxy_certificate.tmp": [ "write" ],
|
||||
"/tmp/homeproxy-backup-*.tar.gz": [ "read" ],
|
||||
"/tmp/homeproxy-restore-*.tar.gz": [ "write" ]
|
||||
"/tmp/homeproxy_panel.tmp": [ "write" ]
|
||||
},
|
||||
"ubus": {
|
||||
"luci.homeproxy": [
|
||||
"acllist_read",
|
||||
"acllist_write",
|
||||
"certificate_write",
|
||||
"log_clean",
|
||||
"resources_update",
|
||||
"singbox_generator"
|
||||
],
|
||||
"luci.homeproxy_backup": [
|
||||
"backup_cleanup",
|
||||
"backup_create",
|
||||
"backup_get_upload_path",
|
||||
"backup_restore",
|
||||
"backup_validate"
|
||||
],
|
||||
"luci.homeproxy_node_tools": [
|
||||
"remove_subscription_nodes",
|
||||
"update_subscriptions"
|
||||
],
|
||||
"luci.homeproxy_tcping": [
|
||||
"node_tcping",
|
||||
"nodes_tcping"
|
||||
]
|
||||
"rc": [ "*" ]
|
||||
},
|
||||
"uci": [ "homeproxy" ]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,553 +0,0 @@
|
||||
#!/usr/bin/ucode
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* HomeProxy 备份和恢复 RPC。
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { lstat, popen, readfile, stat, writefile } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
import {
|
||||
local_ipv4_addresses,
|
||||
normalize_local_controller_option
|
||||
} from '/etc/homeproxy/scripts/clash_api.uc';
|
||||
|
||||
function shellquote(s) {
|
||||
return `'${replace(s, "'", "'\\''")}'`;
|
||||
}
|
||||
|
||||
const HP_DIR = '/etc/homeproxy';
|
||||
const RUN_DIR = '/var/run/homeproxy';
|
||||
|
||||
function commandOutput(command) {
|
||||
const fd = popen(`${command} 2>&1`);
|
||||
let out = '';
|
||||
|
||||
if (fd) {
|
||||
for (let line = fd.read('line'); length(line); line = fd.read('line'))
|
||||
out += line;
|
||||
|
||||
fd.close();
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function generateTempSuffix() {
|
||||
let suffix = trim(commandOutput("hexdump -n 8 -e '1/1 \"%02x\"' /dev/urandom 2>/dev/null"));
|
||||
|
||||
if (!match(suffix, /^[a-f0-9]{16}$/))
|
||||
suffix = replace(trim(commandOutput('date +%s 2>/dev/null')), /[^a-f0-9]/g, '');
|
||||
|
||||
return suffix;
|
||||
}
|
||||
|
||||
function createWorkDir() {
|
||||
const work_dir = '/tmp/homeproxy-work-' + generateTempSuffix();
|
||||
system(`mkdir -p ${shellquote(work_dir)} && chmod 700 ${shellquote(work_dir)}`);
|
||||
return work_dir;
|
||||
}
|
||||
|
||||
let BACKUP_ARCHIVE = '/tmp/homeproxy-backup-' + generateTempSuffix() + '.tar.gz';
|
||||
|
||||
function isEmpty(value) {
|
||||
return !value || value === 'nil' || (type(value) in ['array', 'object'] && length(value) === 0);
|
||||
}
|
||||
|
||||
function normalizeRestoredLocalControllers() {
|
||||
let local_addresses = local_ipv4_addresses(commandOutput);
|
||||
let fallback_host = local_addresses[0];
|
||||
|
||||
if (isEmpty(fallback_host))
|
||||
return {};
|
||||
|
||||
const hpuci = cursor();
|
||||
let adjustments = {};
|
||||
|
||||
hpuci.load('homeproxy');
|
||||
|
||||
for (let item in [
|
||||
[ 'clash_api_proxy_external_controller', 9091, false ]
|
||||
]) {
|
||||
let next = normalize_local_controller_option(hpuci, 'homeproxy', 'config',
|
||||
local_addresses, fallback_host, item[0], item[1], item[2]);
|
||||
|
||||
if (next)
|
||||
adjustments[item[0]] = next;
|
||||
}
|
||||
|
||||
if (length(keys(adjustments)))
|
||||
hpuci.commit('homeproxy');
|
||||
|
||||
return adjustments;
|
||||
}
|
||||
|
||||
function restoredServiceExpectations() {
|
||||
const hpuci = cursor();
|
||||
hpuci.load('homeproxy');
|
||||
|
||||
const main_node = hpuci.get('homeproxy', 'config', 'main_node') || 'nil',
|
||||
default_outbound = hpuci.get('homeproxy', 'routing', 'default_outbound') || 'nil',
|
||||
outbound_node = isEmpty(main_node) ? default_outbound : main_node,
|
||||
server_enabled = hpuci.get('homeproxy', 'server', 'enabled') || '0';
|
||||
|
||||
return {
|
||||
client: !isEmpty(outbound_node),
|
||||
server: server_enabled === '1'
|
||||
};
|
||||
}
|
||||
|
||||
function checkSingBoxRuntimeConfig(path, label) {
|
||||
const config = stat(path);
|
||||
|
||||
if (!config || config.type !== 'file' || config.size <= 0)
|
||||
return `${label}运行配置未生成`;
|
||||
|
||||
if (system(`/usr/bin/sing-box check --config ${shellquote(path)} >/dev/null 2>&1`) !== 0)
|
||||
return `${label}运行配置校验失败`;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateRestoredService() {
|
||||
const expected = restoredServiceExpectations();
|
||||
|
||||
if (expected.client) {
|
||||
let error = checkSingBoxRuntimeConfig(`${RUN_DIR}/sing-box-c.json`, '客户端');
|
||||
if (error)
|
||||
return { result: false, error };
|
||||
}
|
||||
|
||||
if (expected.server) {
|
||||
let error = checkSingBoxRuntimeConfig(`${RUN_DIR}/sing-box-s.json`, '服务端');
|
||||
if (error)
|
||||
return { result: false, error };
|
||||
}
|
||||
|
||||
if (expected.client || expected.server) {
|
||||
const status = commandOutput('/etc/init.d/homeproxy status');
|
||||
|
||||
if (match(status, /active with no instances/))
|
||||
return { result: false, error: 'HomeProxy 服务未启动任何实例' };
|
||||
|
||||
if (system('/etc/init.d/homeproxy status >/dev/null 2>&1') !== 0)
|
||||
return { result: false, error: 'HomeProxy 服务状态异常' };
|
||||
}
|
||||
|
||||
return { result: true };
|
||||
}
|
||||
|
||||
function archiveMemberPath(line) {
|
||||
line = trim(line || '');
|
||||
if (!line)
|
||||
return null;
|
||||
|
||||
let matchPath = match(line, /^[^ \t]+[ \t]+[^ \t]+[ \t]+[^ \t]+[ \t]+[^ \t]+[ \t]+[^ \t]+[ \t]+(.+)$/);
|
||||
let path = matchPath ? matchPath[1] : line;
|
||||
|
||||
path = replace(path, /^\.\//, '');
|
||||
path = replace(path, /^\/+/, '');
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
function allowedBackupPath(path) {
|
||||
if (!path || match(path, /(^|\/)\.\.(\/|$)/) || match(path, /^etc\/homeproxy\/certs\/.*\//))
|
||||
return false;
|
||||
|
||||
return (
|
||||
path === 'etc/config/homeproxy' ||
|
||||
path === 'etc/homeproxy/resources/direct_list.txt' ||
|
||||
path === 'etc/homeproxy/resources/proxy_list.txt' ||
|
||||
match(path, /^etc\/homeproxy\/certs\/[^\/]+$/)
|
||||
);
|
||||
}
|
||||
|
||||
function validateBackupArchive(path) {
|
||||
const filestat = lstat(path);
|
||||
|
||||
if (!filestat || filestat.type !== 'file' || filestat.size <= 0)
|
||||
return { valid: false, error: '备份文件不存在或内容为空' };
|
||||
|
||||
const fd = popen(`/bin/tar -tvzf ${shellquote(path)} 2>&1`);
|
||||
let files = [], errors = [];
|
||||
|
||||
if (!fd)
|
||||
return { valid: false, error: '无法运行 tar 检查备份文件' };
|
||||
|
||||
for (let line = fd.read('line'); length(line); line = fd.read('line')) {
|
||||
let path = archiveMemberPath(line);
|
||||
if (!path)
|
||||
continue;
|
||||
|
||||
let mode = match(line, /^([^ \t]+)/);
|
||||
if (mode && substr(mode[1], 0, 1) !== '-') {
|
||||
errors = [ ...errors, `备份文件内包含不支持的条目类型: ${path}` ];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (path === 'homeproxy-backup-manifest.json')
|
||||
continue;
|
||||
|
||||
if (!allowedBackupPath(path))
|
||||
errors = [ ...errors, `备份文件内包含不支持的路径: ${path}` ];
|
||||
else
|
||||
files = [ ...files, path ];
|
||||
}
|
||||
|
||||
fd.close();
|
||||
|
||||
if (length(errors))
|
||||
return { valid: false, error: join('\n', errors), files };
|
||||
|
||||
if (index(files, 'etc/config/homeproxy') === -1)
|
||||
return { valid: false, error: '备份文件缺少 etc/config/homeproxy', files };
|
||||
|
||||
return { valid: true, files };
|
||||
}
|
||||
|
||||
function createBackupArchive(path) {
|
||||
let files = [];
|
||||
|
||||
if (stat('/etc/config/homeproxy')?.type === 'file')
|
||||
files = [ ...files, 'etc/config/homeproxy' ];
|
||||
|
||||
if (stat(`${HP_DIR}/resources/direct_list.txt`)?.type === 'file')
|
||||
files = [ ...files, 'etc/homeproxy/resources/direct_list.txt' ];
|
||||
|
||||
if (stat(`${HP_DIR}/resources/proxy_list.txt`)?.type === 'file')
|
||||
files = [ ...files, 'etc/homeproxy/resources/proxy_list.txt' ];
|
||||
|
||||
const certs = commandOutput(`[ -d /etc/homeproxy/certs ] && find /etc/homeproxy/certs -maxdepth 1 -type f -print || true`);
|
||||
for (let file in split(trim(certs), /\n/)) {
|
||||
file = archiveMemberPath(file);
|
||||
if (allowedBackupPath(file))
|
||||
files = [ ...files, file ];
|
||||
}
|
||||
|
||||
if (!length(files))
|
||||
return { result: false, error: '没有可备份的 HomeProxy 文件' };
|
||||
|
||||
const work_dir = createWorkDir();
|
||||
const staging_root = `${work_dir}/staging`;
|
||||
system(`mkdir -p ${shellquote(staging_root)}`);
|
||||
|
||||
for (let file in files) {
|
||||
let src = '/' + file;
|
||||
let dst = `${staging_root}/${file}`;
|
||||
let dst_dir = match(dst, /^(.*)\/[^\/]+$/);
|
||||
|
||||
if (dst_dir && dst_dir[1])
|
||||
system(`mkdir -p ${shellquote(dst_dir[1])}`);
|
||||
|
||||
let exit_code = system(`cp ${shellquote(src)} ${shellquote(dst)}`);
|
||||
if (exit_code !== 0) {
|
||||
system(`rm -rf ${shellquote(work_dir)}`);
|
||||
return { result: false, error: `复制文件失败: ${file}` };
|
||||
}
|
||||
}
|
||||
|
||||
let manifest = {};
|
||||
for (let file in files) {
|
||||
let fullpath = `${staging_root}/${file}`;
|
||||
let filestat = stat(fullpath);
|
||||
if (filestat?.type === 'file') {
|
||||
let hash = trim(commandOutput(`sha256sum ${shellquote(fullpath)} 2>/dev/null | awk '{print $1}'`));
|
||||
manifest[file] = {
|
||||
size: filestat.size,
|
||||
sha256: hash
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const manifest_file = `${staging_root}/homeproxy-backup-manifest.json`;
|
||||
writefile(manifest_file, sprintf('%J', manifest));
|
||||
|
||||
const backup_list = `${work_dir}/backup.list`;
|
||||
let all_files = [ ...files, 'homeproxy-backup-manifest.json' ];
|
||||
writefile(backup_list, join('\n', all_files) + '\n');
|
||||
|
||||
const temp_tar = `${work_dir}/archive.tar`;
|
||||
const temp_gz = `${work_dir}/archive.tar.gz`;
|
||||
let exit_code = system(`cd ${shellquote(staging_root)} && umask 077 && /bin/tar -cf ${shellquote(temp_tar)} -T ${shellquote(backup_list)}`);
|
||||
|
||||
if (exit_code !== 0) {
|
||||
system(`rm -rf ${shellquote(work_dir)}`);
|
||||
return { result: false, error: `tar 打包失败,退出码 ${exit_code}` };
|
||||
}
|
||||
|
||||
exit_code = system(`umask 077 && gzip -c ${shellquote(temp_tar)} > ${shellquote(temp_gz)}`);
|
||||
if (exit_code !== 0) {
|
||||
system(`rm -rf ${shellquote(work_dir)}`);
|
||||
return { result: false, error: `压缩失败,退出码 ${exit_code}` };
|
||||
}
|
||||
|
||||
system(`rm -f ${shellquote(path)}`);
|
||||
exit_code = system(`umask 077 && mv ${shellquote(temp_gz)} ${shellquote(path)} && chmod 600 ${shellquote(path)}`);
|
||||
system(`rm -rf ${shellquote(work_dir)}`);
|
||||
|
||||
if (exit_code !== 0) {
|
||||
system(`rm -f ${shellquote(path)}`);
|
||||
return { result: false, error: `生成备份文件失败,退出码 ${exit_code}` };
|
||||
}
|
||||
|
||||
const archive = lstat(path);
|
||||
if (!archive || archive.type !== 'file' || archive.size <= 0)
|
||||
return { result: false, error: '备份文件未成功生成' };
|
||||
|
||||
return {
|
||||
result: true,
|
||||
path,
|
||||
size: archive.size,
|
||||
files
|
||||
};
|
||||
}
|
||||
|
||||
function testExtractBackupArchive(path) {
|
||||
const restore_test_dir = createWorkDir();
|
||||
let exit_code = system(`cd ${shellquote(restore_test_dir)} && /bin/tar -xzf ${shellquote(path)} 2>/dev/null`);
|
||||
|
||||
if (exit_code !== 0) {
|
||||
system(`rm -rf ${shellquote(restore_test_dir)}`);
|
||||
return { result: false, error: `备份文件损坏,无法完整解压,退出码 ${exit_code}` };
|
||||
}
|
||||
|
||||
const manifest_path = `${restore_test_dir}/homeproxy-backup-manifest.json`;
|
||||
const manifest_content = readfile(manifest_path);
|
||||
|
||||
if (!manifest_content) {
|
||||
system(`rm -rf ${shellquote(restore_test_dir)}`);
|
||||
return { result: false, error: '备份文件缺少完整性清单(homeproxy-backup-manifest.json)' };
|
||||
}
|
||||
|
||||
let manifest = {};
|
||||
try {
|
||||
manifest = json(manifest_content);
|
||||
} catch (e) {
|
||||
system(`rm -rf ${shellquote(restore_test_dir)}`);
|
||||
return { result: false, error: '备份清单格式错误' };
|
||||
}
|
||||
|
||||
for (let file in manifest) {
|
||||
if (file === 'homeproxy-backup-manifest.json') {
|
||||
system(`rm -rf ${shellquote(restore_test_dir)}`);
|
||||
return { result: false, error: 'manifest 不能包含自身作为恢复目标' };
|
||||
}
|
||||
|
||||
if (!allowedBackupPath(file)) {
|
||||
system(`rm -rf ${shellquote(restore_test_dir)}`);
|
||||
return { result: false, error: `manifest 包含不允许的路径: ${file}` };
|
||||
}
|
||||
|
||||
let fullpath = `${restore_test_dir}/${file}`;
|
||||
let filestat = stat(fullpath);
|
||||
|
||||
if (!filestat || filestat.type !== 'file') {
|
||||
system(`rm -rf ${shellquote(restore_test_dir)}`);
|
||||
return { result: false, error: `备份文件缺失: ${file}` };
|
||||
}
|
||||
|
||||
if (filestat.size !== manifest[file].size) {
|
||||
system(`rm -rf ${shellquote(restore_test_dir)}`);
|
||||
return { result: false, error: `备份文件大小不匹配: ${file}` };
|
||||
}
|
||||
|
||||
let hash = trim(commandOutput(`sha256sum ${shellquote(fullpath)} 2>/dev/null | awk '{print $1}'`));
|
||||
if (hash !== manifest[file].sha256) {
|
||||
system(`rm -rf ${shellquote(restore_test_dir)}`);
|
||||
return { result: false, error: `备份文件校验和不匹配: ${file}` };
|
||||
}
|
||||
}
|
||||
|
||||
const fd = popen(`/bin/tar -tzf ${shellquote(path)} 2>&1`);
|
||||
if (fd) {
|
||||
for (let line = fd.read('line'); length(line); line = fd.read('line')) {
|
||||
let archive_path = archiveMemberPath(line);
|
||||
|
||||
if (!archive_path || archive_path === 'homeproxy-backup-manifest.json')
|
||||
continue;
|
||||
|
||||
if (allowedBackupPath(archive_path) && !manifest[archive_path]) {
|
||||
fd.close();
|
||||
system(`rm -rf ${shellquote(restore_test_dir)}`);
|
||||
return { result: false, error: `归档中存在未记录的可恢复文件: ${archive_path}` };
|
||||
}
|
||||
}
|
||||
fd.close();
|
||||
}
|
||||
|
||||
return { result: true, manifest: manifest, work_dir: restore_test_dir };
|
||||
}
|
||||
|
||||
function resetBackupTargetPaths() {
|
||||
system('rm -f /etc/config/homeproxy /etc/homeproxy/resources/direct_list.txt /etc/homeproxy/resources/proxy_list.txt');
|
||||
system('rm -rf /etc/homeproxy/certs && mkdir -p /etc/homeproxy/resources /etc/homeproxy/certs');
|
||||
}
|
||||
|
||||
function extractBackupArchive(work_dir, manifest) {
|
||||
resetBackupTargetPaths();
|
||||
|
||||
for (let file in manifest) {
|
||||
let src = `${work_dir}/${file}`;
|
||||
let dst = `/${file}`;
|
||||
let dst_dir = match(dst, /^(.*)\/[^\/]+$/);
|
||||
|
||||
if (dst_dir && dst_dir[1])
|
||||
system(`mkdir -p ${shellquote(dst_dir[1])}`);
|
||||
|
||||
let exit_code = system(`cp ${shellquote(src)} ${shellquote(dst)}`);
|
||||
if (exit_code !== 0)
|
||||
return exit_code;
|
||||
|
||||
if (match(file, /^etc\/homeproxy\/certs\/[^\/]+$/))
|
||||
system(`chmod 600 ${shellquote(dst)}`);
|
||||
else if (file === 'etc/config/homeproxy')
|
||||
system(`chmod 600 ${shellquote(dst)}`);
|
||||
else
|
||||
system(`chmod 644 ${shellquote(dst)}`);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const methods = {
|
||||
backup_create: {
|
||||
call: function() {
|
||||
BACKUP_ARCHIVE = '/tmp/homeproxy-backup-' + generateTempSuffix() + '.tar.gz';
|
||||
let result = createBackupArchive(BACKUP_ARCHIVE);
|
||||
if (result.result)
|
||||
result.download_path = BACKUP_ARCHIVE;
|
||||
return result;
|
||||
}
|
||||
},
|
||||
backup_get_upload_path: {
|
||||
call: function() {
|
||||
const upload_path = '/tmp/homeproxy-restore-' + generateTempSuffix() + '.tar.gz';
|
||||
return { upload_path: upload_path };
|
||||
}
|
||||
},
|
||||
backup_cleanup: {
|
||||
args: { path: 'path' },
|
||||
call: function(req) {
|
||||
const path = req.args?.path;
|
||||
|
||||
if (!path || !match(path, /^\/tmp\/homeproxy-(backup|restore)-[a-f0-9]+\.tar\.gz$/))
|
||||
return { result: false, error: '无效的临时文件路径' };
|
||||
|
||||
system(`rm -f ${shellquote(path)}`);
|
||||
return { result: true };
|
||||
}
|
||||
},
|
||||
backup_validate: {
|
||||
args: { path: 'path' },
|
||||
call: function(req) {
|
||||
const path = req.args?.path;
|
||||
|
||||
if (!path || !match(path, /^\/tmp\/homeproxy-restore-[a-f0-9]+\.tar\.gz$/))
|
||||
return { valid: false, error: '无效的备份文件路径' };
|
||||
|
||||
const check = validateBackupArchive(path);
|
||||
if (!check.valid)
|
||||
return check;
|
||||
|
||||
const integrity = testExtractBackupArchive(path);
|
||||
if (integrity.work_dir)
|
||||
system(`rm -rf ${shellquote(integrity.work_dir)}`);
|
||||
if (!integrity.result)
|
||||
return { valid: false, error: integrity.error, files: check.files };
|
||||
|
||||
return check;
|
||||
}
|
||||
},
|
||||
backup_restore: {
|
||||
args: { path: 'path' },
|
||||
call: function(req) {
|
||||
const restore_path = req.args?.path;
|
||||
|
||||
if (!restore_path)
|
||||
return { result: false, error: '缺少备份文件路径,请重新上传并验证备份文件' };
|
||||
|
||||
if (!match(restore_path, /^\/tmp\/homeproxy-restore-[a-f0-9]+\.tar\.gz$/))
|
||||
return { result: false, error: '无效的备份文件路径' };
|
||||
|
||||
const check = validateBackupArchive(restore_path);
|
||||
|
||||
if (!check.valid)
|
||||
return { result: false, error: check.error, files: check.files };
|
||||
|
||||
const extract_check = testExtractBackupArchive(restore_path);
|
||||
if (!extract_check.result)
|
||||
return { result: false, error: extract_check.error, files: check.files };
|
||||
|
||||
const manifest = extract_check.manifest;
|
||||
const work_dir = extract_check.work_dir;
|
||||
|
||||
const rollback_archive = '/tmp/homeproxy-rollback-' + generateTempSuffix() + '.tar.gz';
|
||||
const rollback = createBackupArchive(rollback_archive);
|
||||
if (!rollback.result) {
|
||||
system(`rm -rf ${shellquote(work_dir)}`);
|
||||
return { result: false, error: `创建回滚包失败: ${rollback.error}` };
|
||||
}
|
||||
|
||||
const rollback_extract_check = testExtractBackupArchive(rollback_archive);
|
||||
if (!rollback_extract_check.result) {
|
||||
system(`rm -rf ${shellquote(work_dir)}`);
|
||||
return { result: false, error: `回滚包校验失败: ${rollback_extract_check.error}` };
|
||||
}
|
||||
|
||||
const rollback_manifest = rollback_extract_check.manifest;
|
||||
const rollback_work_dir = rollback_extract_check.work_dir;
|
||||
|
||||
let exit_code = extractBackupArchive(work_dir, manifest);
|
||||
|
||||
if (exit_code !== 0) {
|
||||
system(`rm -rf ${shellquote(work_dir)}`);
|
||||
let rollback_code = extractBackupArchive(rollback_work_dir, rollback_manifest);
|
||||
system(`rm -rf ${shellquote(rollback_work_dir)}`);
|
||||
system('/sbin/uci commit homeproxy');
|
||||
system('/etc/init.d/homeproxy restart >/dev/null 2>&1');
|
||||
|
||||
if (rollback_code === 0)
|
||||
return { result: false, error: `恢复失败,已自动回滚,退出码 ${exit_code}`, rollback: rollback_archive };
|
||||
|
||||
return { result: false, error: `恢复失败且自动回滚失败,恢复退出码 ${exit_code},回滚退出码 ${rollback_code}`, rollback: rollback_archive };
|
||||
}
|
||||
|
||||
system(`ucode ${HP_DIR}/scripts/migrate_config.uc >/dev/null 2>&1`);
|
||||
|
||||
let adjustments = normalizeRestoredLocalControllers();
|
||||
system('/sbin/uci commit homeproxy');
|
||||
system(`rm -f ${shellquote(restore_path)} /tmp/luci-indexcache.*`);
|
||||
system('rm -rf /tmp/luci-modulecache/ 2>/dev/null');
|
||||
system('/etc/init.d/homeproxy restart >/dev/null 2>&1');
|
||||
|
||||
sleep(2000);
|
||||
let service_check = validateRestoredService();
|
||||
|
||||
if (!service_check.result) {
|
||||
let rollback_code = extractBackupArchive(rollback_work_dir, rollback_manifest);
|
||||
system(`rm -rf ${shellquote(work_dir)} ${shellquote(rollback_work_dir)}`);
|
||||
system('/sbin/uci commit homeproxy');
|
||||
system('/etc/init.d/homeproxy restart >/dev/null 2>&1');
|
||||
|
||||
if (rollback_code === 0)
|
||||
return { result: false, error: `${service_check.error},已自动回滚`, rollback: rollback_archive };
|
||||
|
||||
return { result: false, error: `${service_check.error},且自动回滚失败`, rollback: rollback_archive };
|
||||
}
|
||||
|
||||
system(`rm -rf ${shellquote(work_dir)} ${shellquote(rollback_work_dir)}`);
|
||||
|
||||
return {
|
||||
result: true,
|
||||
files: check.files,
|
||||
rollback: rollback_archive,
|
||||
adjustments
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { 'luci.homeproxy_backup': methods };
|
||||
@@ -1,270 +0,0 @@
|
||||
#!/usr/bin/ucode
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* HomeProxy 节点和订阅 helper RPC。
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { readfile, writefile } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
import {
|
||||
node_filter_compile,
|
||||
expand_node_filter,
|
||||
preview_manual_node_allowed
|
||||
} from '/etc/homeproxy/scripts/node_filter.uc';
|
||||
import {
|
||||
collect_node_references,
|
||||
remove_subscription_nodes
|
||||
} from '/etc/homeproxy/scripts/node_references.uc';
|
||||
import { first_value } from '/etc/homeproxy/scripts/clash_api.uc';
|
||||
|
||||
function shellquote(s) {
|
||||
return `'${replace(s, "'", "'\\''")}'`;
|
||||
}
|
||||
|
||||
const HP_DIR = '/etc/homeproxy';
|
||||
const RUN_DIR = '/var/run/homeproxy';
|
||||
const CONFIG_DIAGNOSTICS = `${RUN_DIR}/config-diagnostics.json`;
|
||||
const SUBSCRIPTION_DIAGNOSTICS = `${RUN_DIR}/subscription-diagnostics.json`;
|
||||
const SUBSCRIPTION_UPDATE_STATUS = `${RUN_DIR}/subscription-update-status.json`;
|
||||
const MAX_NODE_IDS = 1000;
|
||||
const MAX_NODE_ID_LEN = 128;
|
||||
|
||||
function isEmpty(value) {
|
||||
return !value || value === 'nil' || (type(value) in ['array', 'object'] && length(value) === 0);
|
||||
}
|
||||
|
||||
function readDiagnostics(path) {
|
||||
let content = readfile(path);
|
||||
|
||||
if (!content)
|
||||
return { result: true, time: null, items: [] };
|
||||
|
||||
try {
|
||||
let payload = json(content);
|
||||
return {
|
||||
result: true,
|
||||
time: payload.time,
|
||||
items: payload.items || []
|
||||
};
|
||||
} catch (e) {
|
||||
return { result: false, error: '诊断文件格式错误' };
|
||||
}
|
||||
}
|
||||
|
||||
function combinedDiagnostics() {
|
||||
let items = [],
|
||||
latest_time = null;
|
||||
|
||||
for (let path in [ CONFIG_DIAGNOSTICS, SUBSCRIPTION_DIAGNOSTICS ]) {
|
||||
let payload = readDiagnostics(path);
|
||||
if (!payload.result)
|
||||
return payload;
|
||||
|
||||
if (payload.time && (!latest_time || payload.time > latest_time))
|
||||
latest_time = payload.time;
|
||||
|
||||
for (let item in payload.items)
|
||||
push(items, item);
|
||||
}
|
||||
|
||||
return {
|
||||
result: true,
|
||||
time: latest_time,
|
||||
items
|
||||
};
|
||||
}
|
||||
|
||||
function readSubscriptionUpdateStatus() {
|
||||
let content = readfile(SUBSCRIPTION_UPDATE_STATUS);
|
||||
|
||||
if (!content) {
|
||||
return {
|
||||
result: true,
|
||||
running: false,
|
||||
completed: false,
|
||||
update_result: null,
|
||||
error: null,
|
||||
time: null
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
let payload = json(content);
|
||||
return {
|
||||
result: true,
|
||||
running: !!payload.running,
|
||||
completed: !!payload.completed,
|
||||
update_result: payload.update_result,
|
||||
error: payload.error,
|
||||
time: payload.time
|
||||
};
|
||||
} catch (e) {
|
||||
return { result: false, error: '订阅更新状态文件格式错误' };
|
||||
}
|
||||
}
|
||||
|
||||
function writeSubscriptionUpdateStatus(payload) {
|
||||
system(`mkdir -p ${shellquote(RUN_DIR)}`);
|
||||
writefile(SUBSCRIPTION_UPDATE_STATUS, sprintf('%.J\n', payload));
|
||||
}
|
||||
|
||||
function validNodeId(node_id) {
|
||||
return type(node_id) === 'string' &&
|
||||
length(node_id) > 0 &&
|
||||
length(node_id) <= MAX_NODE_ID_LEN &&
|
||||
match(node_id, /^[A-Za-z0-9_]+$/);
|
||||
}
|
||||
|
||||
function validateNodeIds(node_ids) {
|
||||
if (type(node_ids) !== 'array')
|
||||
return { result: false, error: 'illegal node_ids' };
|
||||
|
||||
if (length(node_ids) > MAX_NODE_IDS)
|
||||
return { result: false, error: 'too many node_ids' };
|
||||
|
||||
for (let node_id in node_ids)
|
||||
if (!validNodeId(node_id))
|
||||
return { result: false, error: 'illegal node id' };
|
||||
|
||||
return { result: true };
|
||||
}
|
||||
|
||||
function expandNodeFilterPreview(manual_nodes, node_filter, node_filter_exclude, node_mode) {
|
||||
const hpuci = cursor();
|
||||
hpuci.load('homeproxy');
|
||||
|
||||
return expand_node_filter({
|
||||
uci: hpuci,
|
||||
config: 'homeproxy',
|
||||
node_type: 'node',
|
||||
allow_manual_node: (node_id) => preview_manual_node_allowed(hpuci, 'homeproxy', node_id, node_mode)
|
||||
}, manual_nodes, node_filter, node_filter_exclude);
|
||||
}
|
||||
|
||||
const methods = {
|
||||
config_diagnostics: {
|
||||
args: {},
|
||||
call: function(_req) {
|
||||
return combinedDiagnostics();
|
||||
}
|
||||
},
|
||||
update_subscriptions_status: {
|
||||
args: {},
|
||||
call: function(_req) {
|
||||
return readSubscriptionUpdateStatus();
|
||||
}
|
||||
},
|
||||
validate_regex: {
|
||||
args: { pattern: 'pattern' },
|
||||
call: function(req) {
|
||||
const pattern = first_value(req.args?.pattern);
|
||||
const validated = node_filter_compile(pattern);
|
||||
|
||||
if (!validated.result)
|
||||
return { result: false, error: validated.error, limits: validated.limits };
|
||||
|
||||
return { result: true, limits: validated.limits };
|
||||
}
|
||||
},
|
||||
preview_node_filter: {
|
||||
args: { manual_nodes: [], node_filter: 'node_filter', node_filter_exclude: 'node_filter_exclude', node: 'node' },
|
||||
call: function(req) {
|
||||
return expandNodeFilterPreview(
|
||||
req.args?.manual_nodes,
|
||||
first_value(req.args?.node_filter),
|
||||
first_value(req.args?.node_filter_exclude),
|
||||
first_value(req.args?.node)
|
||||
);
|
||||
}
|
||||
},
|
||||
node_references: {
|
||||
args: { node_id: 'node_id' },
|
||||
call: function(req) {
|
||||
const node_id = first_value(req.args?.node_id);
|
||||
|
||||
if (!validNodeId(node_id))
|
||||
return { result: false, error: 'illegal node_id' };
|
||||
|
||||
const hpuci = cursor();
|
||||
hpuci.load('homeproxy');
|
||||
|
||||
return {
|
||||
result: true,
|
||||
node_id: node_id,
|
||||
references: collect_node_references(hpuci, 'homeproxy', node_id)
|
||||
};
|
||||
}
|
||||
},
|
||||
remove_subscription_nodes: {
|
||||
args: { node_ids: [] },
|
||||
call: function(req) {
|
||||
const checked = validateNodeIds(req.args?.node_ids);
|
||||
if (!checked.result)
|
||||
return checked;
|
||||
|
||||
const hpuci = cursor();
|
||||
hpuci.load('homeproxy');
|
||||
|
||||
let result = remove_subscription_nodes(hpuci, 'homeproxy', req.args.node_ids);
|
||||
hpuci.commit('homeproxy');
|
||||
|
||||
return {
|
||||
result: true,
|
||||
removed: result.removed,
|
||||
changed: result.changed,
|
||||
changes: result.changes,
|
||||
main_node_fallback: result.main_node_fallback,
|
||||
main_udp_fallback: result.main_udp_fallback
|
||||
};
|
||||
}
|
||||
},
|
||||
update_subscriptions: {
|
||||
args: {},
|
||||
call: function(_req) {
|
||||
const status = readSubscriptionUpdateStatus();
|
||||
if (!status.result)
|
||||
return status;
|
||||
|
||||
if (status.running) {
|
||||
return {
|
||||
result: false,
|
||||
error: '订阅更新正在进行中,请稍候。'
|
||||
};
|
||||
}
|
||||
|
||||
writeSubscriptionUpdateStatus({
|
||||
time: time(),
|
||||
running: true,
|
||||
completed: false,
|
||||
update_result: null,
|
||||
error: null
|
||||
});
|
||||
|
||||
const exit_code = system(`(${HP_DIR}/scripts/update_subscriptions.uc </dev/null >/dev/null 2>&1) &`);
|
||||
if (exit_code !== 0) {
|
||||
writeSubscriptionUpdateStatus({
|
||||
time: time(),
|
||||
running: false,
|
||||
completed: true,
|
||||
update_result: false,
|
||||
error: '启动订阅更新失败,请查看 HomeProxy 日志。'
|
||||
});
|
||||
|
||||
return {
|
||||
result: false,
|
||||
error: '启动订阅更新失败,请查看 HomeProxy 日志。'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
result: true,
|
||||
started: true
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { 'luci.homeproxy_node_tools': methods };
|
||||
@@ -1,416 +0,0 @@
|
||||
#!/usr/bin/ucode
|
||||
'use strict';
|
||||
|
||||
import { popen, readfile } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
import { urlencode } from 'luci.http';
|
||||
|
||||
import { build_outbound_tag_map, get_outbound_tag } from '/etc/homeproxy/scripts/outbound_tag.uc';
|
||||
import { parse_controller } from '/etc/homeproxy/scripts/clash_api.uc';
|
||||
|
||||
const TCPING_MAX_NODES = 300;
|
||||
const SECTION_MAX_LEN = 128;
|
||||
const BATCH_SIZE = 50;
|
||||
const TCPING_COOLDOWN = 3;
|
||||
const DELAY_TIMEOUT = 5000;
|
||||
const FETCH_TIMEOUT = 30000;
|
||||
const DELAY_TEST_URL = 'https://www.gstatic.com/generate_204';
|
||||
|
||||
let last_tcping_at = 0;
|
||||
|
||||
// urlencode() 只负责 URL/path 编码,不能作为 shell 注入防护;所有进入 shell 命令的动态值都必须继续经过 shellquote()。
|
||||
function shellquote(s) {
|
||||
return `'${replace(s, "'", "'\\''")}'`;
|
||||
}
|
||||
|
||||
function pathSegment(value) {
|
||||
return replace(urlencode(value), /\+/g, '%20');
|
||||
}
|
||||
|
||||
function controllerBase(hpuci) {
|
||||
let controller = parse_controller(
|
||||
hpuci.get('homeproxy', 'config', 'clash_api_external_controller') || '127.0.0.1:9090',
|
||||
9090
|
||||
);
|
||||
let port_num = controller.port || 9090;
|
||||
|
||||
if (port_num < 1 || port_num > 65535)
|
||||
return null;
|
||||
|
||||
return 'http://127.0.0.1:' + port_num;
|
||||
}
|
||||
|
||||
function clashApiGet(hpuci, path, timeout) {
|
||||
let timeout_sec = int(((timeout || FETCH_TIMEOUT) + 999) / 1000),
|
||||
controller = controllerBase(hpuci),
|
||||
secret = hpuci.get('homeproxy', 'config', 'clash_api_secret') || '';
|
||||
|
||||
if (!controller)
|
||||
return {};
|
||||
|
||||
let command = sprintf('/usr/bin/wget -qO- -T %d --no-proxy %s %s',
|
||||
timeout_sec,
|
||||
secret ? sprintf('--header %s', shellquote('Authorization: Bearer ' + secret)) : '',
|
||||
shellquote(controller + path)),
|
||||
fd = popen(command + ' 2>/dev/null'),
|
||||
out = '';
|
||||
|
||||
if (fd) {
|
||||
for (let line = fd.read('line'); length(line); line = fd.read('line'))
|
||||
out += line;
|
||||
|
||||
fd.close();
|
||||
}
|
||||
|
||||
try {
|
||||
return json(trim(out) || '{}');
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function delayQuery() {
|
||||
return sprintf('?timeout=%d&url=%s',
|
||||
DELAY_TIMEOUT,
|
||||
urlencode(DELAY_TEST_URL));
|
||||
}
|
||||
|
||||
function proxyDelayPath(name) {
|
||||
return sprintf('/proxies/%s/delay%s',
|
||||
pathSegment(name),
|
||||
delayQuery());
|
||||
}
|
||||
|
||||
function delayRequestTimeout() {
|
||||
let timeout = DELAY_TIMEOUT * 2 + 10000;
|
||||
|
||||
return (timeout > FETCH_TIMEOUT) ? timeout : FETCH_TIMEOUT;
|
||||
}
|
||||
|
||||
function parseProxyDelayResult(result) {
|
||||
let error = '';
|
||||
|
||||
if (type(result) !== 'object')
|
||||
return { status: 'failed', error: '连通性测试无结果' };
|
||||
|
||||
error = result.message || result.error || '';
|
||||
|
||||
if (type(result.delay) === 'int' || type(result.delay) === 'double')
|
||||
return { status: 'ok', delay: int(result.delay) };
|
||||
|
||||
return { status: 'timeout', error: error || '连通性测试超时或失败' };
|
||||
}
|
||||
|
||||
function stampResult(result) {
|
||||
let now = time();
|
||||
result.tested_at = now;
|
||||
return result;
|
||||
}
|
||||
|
||||
function reserveTcpingSlot() {
|
||||
let now = time(),
|
||||
wait = TCPING_COOLDOWN - (now - last_tcping_at);
|
||||
|
||||
if (last_tcping_at > 0 && wait > 0)
|
||||
return {
|
||||
result: false,
|
||||
error: sprintf('测速请求过于频繁,请 %d 秒后重试。', wait)
|
||||
};
|
||||
|
||||
last_tcping_at = now;
|
||||
return null;
|
||||
}
|
||||
|
||||
function loadedProxies(hpuci) {
|
||||
return clashApiGet(hpuci, '/proxies', FETCH_TIMEOUT);
|
||||
}
|
||||
|
||||
function loadedProxyTags(payload) {
|
||||
let tags = {};
|
||||
|
||||
if (type(payload) === 'object' && type(payload.proxies) === 'object')
|
||||
for (let name in payload.proxies)
|
||||
tags[name] = true;
|
||||
else
|
||||
return null;
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
function nodeTag(tag_map, section) {
|
||||
return get_outbound_tag(tag_map, section);
|
||||
}
|
||||
|
||||
function runtimeNodeTag(hpuci, tag_map, section) {
|
||||
let routing_mode = hpuci.get('homeproxy', 'config', 'routing_mode') || 'bypass_mainland_china',
|
||||
main_node = hpuci.get('homeproxy', 'config', 'main_node') || 'nil',
|
||||
main_udp_node = hpuci.get('homeproxy', 'config', 'main_udp_node') || 'nil';
|
||||
|
||||
if (routing_mode !== 'custom') {
|
||||
if (section === main_node)
|
||||
return 'main-out';
|
||||
|
||||
if (main_udp_node !== 'nil' &&
|
||||
main_udp_node !== 'same' &&
|
||||
main_udp_node !== main_node &&
|
||||
section === main_udp_node)
|
||||
return 'main-udp-out';
|
||||
}
|
||||
|
||||
return nodeTag(tag_map, section);
|
||||
}
|
||||
|
||||
function validSectionName(section) {
|
||||
return type(section) === 'string' &&
|
||||
section !== '' &&
|
||||
length(section) <= SECTION_MAX_LEN &&
|
||||
match(section, /^[A-Za-z0-9_]+$/);
|
||||
}
|
||||
|
||||
function resolveNode(hpuci, tag_map, section) {
|
||||
if (!validSectionName(section))
|
||||
return { result: false, error: 'illegal section' };
|
||||
|
||||
let cfg = hpuci.get_all('homeproxy', section),
|
||||
tag = runtimeNodeTag(hpuci, tag_map, section);
|
||||
|
||||
if (!cfg || cfg['.type'] !== 'node')
|
||||
return { result: true, node: stampResult({ status: 'missing', target: tag, error: '节点不存在' }) };
|
||||
|
||||
if (cfg.type in ['direct', 'block'])
|
||||
return {
|
||||
result: true,
|
||||
node: stampResult({
|
||||
status: 'unsupported',
|
||||
target: tag,
|
||||
error: '直连或阻断节点不测速'
|
||||
})
|
||||
};
|
||||
|
||||
return { result: true, node: null, tag };
|
||||
}
|
||||
|
||||
function testNodeByProxyDelay(hpuci, payload, tag) {
|
||||
let loaded = loadedProxyTags(payload);
|
||||
|
||||
if (loaded === null)
|
||||
return stampResult({ status: 'failed', target: tag, error: 'Clash API 未响应,请确认 HomeProxy 客户端正在运行。' });
|
||||
|
||||
if (!(tag in loaded))
|
||||
return stampResult({ status: 'unloaded', target: tag, error: '节点未加入当前运行配置' });
|
||||
|
||||
let result = stampResult(parseProxyDelayResult(clashApiGet(hpuci, proxyDelayPath(tag), delayRequestTimeout())));
|
||||
|
||||
result.target = tag;
|
||||
return result;
|
||||
}
|
||||
|
||||
function testNodesByProxyDelayBatch(hpuci, payload, tags) {
|
||||
let results = {},
|
||||
loaded = loadedProxyTags(payload);
|
||||
|
||||
if (loaded === null) {
|
||||
for (let tag in tags)
|
||||
results[tag] = stampResult({
|
||||
status: 'failed',
|
||||
target: tag,
|
||||
error: 'Clash API 未响应,请确认 HomeProxy 客户端正在运行。'
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
let pending = [];
|
||||
|
||||
for (let tag in tags) {
|
||||
if (!(tag in loaded)) {
|
||||
results[tag] = stampResult({
|
||||
status: 'unloaded',
|
||||
target: tag,
|
||||
error: '节点未加入当前运行配置'
|
||||
});
|
||||
} else {
|
||||
push(pending, tag);
|
||||
}
|
||||
}
|
||||
|
||||
if (!length(pending))
|
||||
return results;
|
||||
|
||||
let timeout_sec = int((delayRequestTimeout() + 999) / 1000),
|
||||
controller = controllerBase(hpuci),
|
||||
secret = hpuci.get('homeproxy', 'config', 'clash_api_secret') || '',
|
||||
header = secret ? sprintf('--header %s', shellquote('Authorization: Bearer ' + secret)) : '',
|
||||
temp_dir_cmd = 'mktemp -d /tmp/homeproxy-delay.XXXXXX',
|
||||
fd = popen(temp_dir_cmd + ' 2>/dev/null'),
|
||||
temp_dir = '';
|
||||
|
||||
if (fd) {
|
||||
temp_dir = trim(fd.read('line') || '');
|
||||
fd.close();
|
||||
}
|
||||
|
||||
if (!temp_dir) {
|
||||
for (let tag in pending)
|
||||
results[tag] = stampResult({ status: 'failed', target: tag, error: '无法创建测速临时目录' });
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
for (let offset = 0; offset < length(pending); offset += BATCH_SIZE) {
|
||||
let commands = [];
|
||||
|
||||
for (let i = offset; i < length(pending) && i < offset + BATCH_SIZE; i++) {
|
||||
let tag = pending[i],
|
||||
url = controller + proxyDelayPath(tag),
|
||||
out = temp_dir + '/' + i + '.json';
|
||||
|
||||
// url 与输出路径来自动态值,即使 URL 已编码,拼接 shell 命令前仍必须 shellquote()。
|
||||
push(commands, sprintf('( /usr/bin/wget -qO- -T %d --no-proxy %s %s > %s 2>/dev/null ) &',
|
||||
timeout_sec,
|
||||
header,
|
||||
shellquote(url),
|
||||
shellquote(out)));
|
||||
}
|
||||
|
||||
push(commands, 'wait');
|
||||
system(join(' ', commands));
|
||||
}
|
||||
|
||||
for (let i = 0; i < length(pending); i++) {
|
||||
let tag = pending[i],
|
||||
body = trim(readfile(temp_dir + '/' + i + '.json') || ''),
|
||||
parsed = {};
|
||||
|
||||
try {
|
||||
parsed = json(body || '{}');
|
||||
} catch (e) {
|
||||
parsed = {};
|
||||
}
|
||||
|
||||
results[tag] = stampResult(parseProxyDelayResult(parsed));
|
||||
results[tag].target = tag;
|
||||
}
|
||||
|
||||
system('rm -rf ' + shellquote(temp_dir));
|
||||
return results;
|
||||
}
|
||||
|
||||
const methods = {
|
||||
node_tcping: {
|
||||
args: { section: '' },
|
||||
call: function(req) {
|
||||
let section = req.args ? req.args.section : null;
|
||||
|
||||
const hpuci = cursor();
|
||||
hpuci.load('homeproxy');
|
||||
|
||||
let tag_map = build_outbound_tag_map(hpuci),
|
||||
resolved = resolveNode(hpuci, tag_map, section);
|
||||
|
||||
if (!resolved.result || resolved.node)
|
||||
return resolved;
|
||||
|
||||
if (hpuci.get('homeproxy', 'config', 'clash_api_enabled') !== '1')
|
||||
return { result: false, error: '请先开启 Clash API 后再进行连通性测试。' };
|
||||
|
||||
if (!controllerBase(hpuci))
|
||||
return { result: false, error: 'Clash API 端口无效,请设置为 1-65535。' };
|
||||
|
||||
let cooldown = reserveTcpingSlot();
|
||||
if (cooldown)
|
||||
return cooldown;
|
||||
|
||||
let payload = loadedProxies(hpuci);
|
||||
|
||||
return {
|
||||
result: true,
|
||||
node: testNodeByProxyDelay(hpuci, payload, resolved.tag)
|
||||
};
|
||||
}
|
||||
},
|
||||
nodes_tcping: {
|
||||
args: { sections: [] },
|
||||
call: function(req) {
|
||||
let sections = req.args ? req.args.sections : null;
|
||||
if (type(sections) !== 'array')
|
||||
return { result: false, error: 'illegal sections' };
|
||||
|
||||
const hpuci = cursor();
|
||||
let nodes = {},
|
||||
tested = 0,
|
||||
tags = [],
|
||||
seen_sections = {},
|
||||
warning = '';
|
||||
|
||||
hpuci.load('homeproxy');
|
||||
let tag_map = build_outbound_tag_map(hpuci);
|
||||
|
||||
for (let i = 0; i < length(sections); i++) {
|
||||
let section = sections[i];
|
||||
if (seen_sections[section])
|
||||
continue;
|
||||
seen_sections[section] = true;
|
||||
|
||||
if (++tested > TCPING_MAX_NODES)
|
||||
nodes[section] = { status: 'skipped', error: sprintf('一次最多测试 %d 个节点', TCPING_MAX_NODES) };
|
||||
else {
|
||||
let resolved = resolveNode(hpuci, tag_map, section);
|
||||
|
||||
if (!resolved.result)
|
||||
return { result: false, error: resolved.error };
|
||||
|
||||
if (resolved.node)
|
||||
nodes[section] = resolved.node;
|
||||
else
|
||||
push(tags, resolved.tag);
|
||||
}
|
||||
}
|
||||
|
||||
if (length(tags)) {
|
||||
if (hpuci.get('homeproxy', 'config', 'clash_api_enabled') !== '1')
|
||||
return { result: false, error: '请先开启 Clash API 后再进行连通性测试。' };
|
||||
|
||||
if (!controllerBase(hpuci))
|
||||
return { result: false, error: 'Clash API 端口无效,请设置为 1-65535。' };
|
||||
|
||||
let cooldown = reserveTcpingSlot();
|
||||
if (cooldown)
|
||||
return cooldown;
|
||||
|
||||
let payload = loadedProxies(hpuci),
|
||||
tag_results = testNodesByProxyDelayBatch(hpuci, payload, tags);
|
||||
|
||||
if (loadedProxyTags(payload) === null)
|
||||
warning = 'Clash API 未响应,请确认 HomeProxy 客户端正在运行。';
|
||||
|
||||
for (let i = 0; i < length(sections); i++) {
|
||||
let section = sections[i],
|
||||
tag = runtimeNodeTag(hpuci, tag_map, section);
|
||||
|
||||
if (nodes[section])
|
||||
continue;
|
||||
|
||||
if (tag in tag_results)
|
||||
nodes[section] = tag_results[tag];
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < length(sections); i++) {
|
||||
let section = sections[i];
|
||||
if (seen_sections[section] !== true)
|
||||
continue;
|
||||
if (!nodes[section])
|
||||
nodes[section] = stampResult({
|
||||
status: 'failed',
|
||||
target: nodeTag(tag_map, section),
|
||||
error: '本次未返回结果'
|
||||
});
|
||||
}
|
||||
|
||||
return { result: true, nodes, warning };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { 'luci.homeproxy_tcping': methods };
|
||||
Reference in new issue
Block a user