mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-30 02:05:13 +08:00
⛄ Sync 2026-07-31 00:26:24
This commit is contained in:
1 parent
47d0743808
commit
b44a4364c4
125 files changed
+3954
-140418
No files matched your search
@@ -8,15 +8,17 @@
|
||||
'use strict';
|
||||
|
||||
import { md5 } from 'digest';
|
||||
import { open } from 'fs';
|
||||
import { open, writefile } from 'fs';
|
||||
import { connect } from 'ubus';
|
||||
import { cursor } from 'uci';
|
||||
|
||||
import { urldecode, urlencode } from 'luci.http';
|
||||
import { init_action } from 'luci.sys';
|
||||
import { remove_subscription_nodes } from '/etc/homeproxy/scripts/node_references.uc';
|
||||
import { parse_surge_subscription } from '/etc/homeproxy/scripts/subscription_parsers.uc';
|
||||
|
||||
import {
|
||||
wGET, decodeBase64Str, getTime, isEmpty, parseURL,
|
||||
wGETResult, decodeBase64Str, getTime, isEmpty, parseURL,
|
||||
validation, HP_DIR, RUN_DIR
|
||||
} from 'homeproxy';
|
||||
|
||||
@@ -31,6 +33,7 @@ const ucimain = 'config',
|
||||
ucisubscription = 'subscription';
|
||||
|
||||
const allow_insecure = uci.get(uciconfig, ucisubscription, 'allow_insecure') || '0',
|
||||
allow_unsupported_tls_pin_fallback = uci.get(uciconfig, ucisubscription, 'allow_unsupported_tls_pin_fallback') || '0',
|
||||
filter_mode = uci.get(uciconfig, ucisubscription, 'filter_nodes') || 'disabled',
|
||||
filter_keywords = uci.get(uciconfig, ucisubscription, 'filter_keywords') || [],
|
||||
packet_encoding = uci.get(uciconfig, ucisubscription, 'packet_encoding') || 'xudp',
|
||||
@@ -72,6 +75,10 @@ const ubus = connect();
|
||||
const sing_features = ubus.call('luci.homeproxy', 'singbox_get_features', {}) || {};
|
||||
/* Common var end */
|
||||
|
||||
const SUBSCRIPTION_DIAGNOSTICS_PATH = RUN_DIR + '/subscription-diagnostics.json';
|
||||
const SUBSCRIPTION_UPDATE_STATUS_PATH = RUN_DIR + '/subscription-update-status.json';
|
||||
let subscription_diagnostics = [];
|
||||
|
||||
/* Log */
|
||||
system(`mkdir -p ${RUN_DIR}`);
|
||||
function log(...args) {
|
||||
@@ -80,6 +87,135 @@ function log(...args) {
|
||||
logfile.close();
|
||||
}
|
||||
|
||||
function reportSubscriptionDiagnostic(type, message, suggestion) {
|
||||
push(subscription_diagnostics, {
|
||||
type,
|
||||
source: 'subscription',
|
||||
message,
|
||||
suggestion
|
||||
});
|
||||
}
|
||||
|
||||
function writeSubscriptionDiagnostics() {
|
||||
system(`mkdir -p ${RUN_DIR}`);
|
||||
|
||||
if (length(subscription_diagnostics)) {
|
||||
writefile(SUBSCRIPTION_DIAGNOSTICS_PATH, sprintf('%.J\n', {
|
||||
time: time(),
|
||||
items: subscription_diagnostics
|
||||
}));
|
||||
} else {
|
||||
system(`rm -f ${SUBSCRIPTION_DIAGNOSTICS_PATH}`);
|
||||
}
|
||||
}
|
||||
|
||||
function writeSubscriptionUpdateStatus(running, completed, update_result, error) {
|
||||
system(`mkdir -p ${RUN_DIR}`);
|
||||
writefile(SUBSCRIPTION_UPDATE_STATUS_PATH, sprintf('%.J\n', {
|
||||
time: time(),
|
||||
running: !!running,
|
||||
completed: !!completed,
|
||||
update_result,
|
||||
error: error || null
|
||||
}));
|
||||
}
|
||||
|
||||
function sanitizeCommandError(text) {
|
||||
text = trim(text || '');
|
||||
if (isEmpty(text))
|
||||
return null;
|
||||
|
||||
text = split(text, '\n')[0] || text;
|
||||
return text;
|
||||
}
|
||||
|
||||
function uci_value_equal(a, b) {
|
||||
if (type(a) == 'array')
|
||||
a = map(a, (v) => sprintf('%s', v));
|
||||
else
|
||||
a = sprintf('%s', a);
|
||||
|
||||
if (type(b) == 'array')
|
||||
b = map(b, (v) => sprintf('%s', v));
|
||||
else
|
||||
b = sprintf('%s', b);
|
||||
|
||||
return sprintf('%J', a) == sprintf('%J', b);
|
||||
}
|
||||
|
||||
function setOrDeleteList(section, option, values) {
|
||||
if (length(values))
|
||||
uci.set(uciconfig, section, option, values);
|
||||
else
|
||||
uci.delete(uciconfig, section, option);
|
||||
}
|
||||
|
||||
function logCleanupChange(change) {
|
||||
if (!change || !change.node_id)
|
||||
return;
|
||||
|
||||
let target = sprintf('%s.%s', change.section || '-', change.option || '-'),
|
||||
action = change.action || 'update',
|
||||
value = (action === 'set') ? sprintf(' to %s', change.value) : '';
|
||||
|
||||
log(sprintf('Cleaned reference to deleted subscription node %s: %s %s%s.',
|
||||
change.node_id, target, action, value));
|
||||
}
|
||||
|
||||
function version_at_least(version, major, minor, patch) {
|
||||
const m = match(version || '', /^([0-9]+)\.([0-9]+)\.([0-9]+)/);
|
||||
if (!m)
|
||||
return false;
|
||||
|
||||
const current = [ int(m[1]), int(m[2]), int(m[3]) ],
|
||||
required = [ major, minor, patch ];
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (current[i] > required[i])
|
||||
return true;
|
||||
if (current[i] < required[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function tls_cert_pin_unsupported() {
|
||||
return !version_at_least(sing_features.version, 1, 13, 0);
|
||||
}
|
||||
|
||||
function apply_tls_cert_pin_policy(config, label, pin) {
|
||||
if (!pin || !tls_cert_pin_unsupported())
|
||||
return;
|
||||
|
||||
const node_label = label || config.label || 'NULL',
|
||||
version = sing_features.version || 'unknown';
|
||||
|
||||
if (config.tls_insecure === '1')
|
||||
return;
|
||||
|
||||
if (allow_unsupported_tls_pin_fallback === '1' || config.type === 'hysteria2') {
|
||||
config.tls_insecure = '1';
|
||||
log(sprintf('Node %s uses server certificate fingerprint, but sing-box %s cannot express it; enabling explicit TLS insecure compatibility fallback.',
|
||||
node_label,
|
||||
version));
|
||||
reportSubscriptionDiagnostic('warning',
|
||||
sprintf('订阅节点 %s 使用证书指纹,但当前 sing-box %s 不支持 certificate_public_key_sha256;已降级为 TLS insecure 继续兼容。', node_label, version),
|
||||
(config.type === 'hysteria2')
|
||||
? '这是延续旧行为的兼容回退;如需更严格校验,请升级到支持 certificate_public_key_sha256 的 sing-box'
|
||||
: '若安全要求较高,请关闭“Allow unsupported certificate pin fallback”,让此类节点默认跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
config.__skip_reason = sprintf('该节点使用证书指纹,但当前 sing-box %s 不支持 certificate_public_key_sha256,已跳过。', version);
|
||||
reportSubscriptionDiagnostic('warning',
|
||||
sprintf('订阅节点 %s 使用证书指纹,但当前 sing-box %s 不支持 certificate_public_key_sha256,已跳过。', node_label, version),
|
||||
'升级到支持 certificate_public_key_sha256 的 sing-box,或显式开启兼容 fallback 后重新更新订阅');
|
||||
log(sprintf('Skipping node %s: server certificate fingerprint is unsupported by sing-box %s.',
|
||||
node_label,
|
||||
version));
|
||||
}
|
||||
|
||||
function parse_uri(uri) {
|
||||
let config, url, params;
|
||||
|
||||
@@ -190,6 +326,7 @@ function parse_uri(uri) {
|
||||
tls_insecure: (params.insecure === '1') ? '1' : '0',
|
||||
tls_sni: params.sni
|
||||
};
|
||||
apply_tls_cert_pin_policy(config, config.label, params.pinSHA256);
|
||||
|
||||
break;
|
||||
case 'socks':
|
||||
@@ -474,23 +611,26 @@ function parse_uri(uri) {
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (via_proxy !== '1') {
|
||||
log('Stopping service...');
|
||||
init_action('homeproxy', 'stop');
|
||||
}
|
||||
|
||||
for (let url in subscription_urls) {
|
||||
url = replace(url, /#.*$/, '');
|
||||
const groupHash = md5(url);
|
||||
node_cache[groupHash] = {};
|
||||
|
||||
const res = wGET(url, user_agent);
|
||||
const fetch_result = wGETResult(url, user_agent) || {},
|
||||
res = fetch_result.stdout;
|
||||
if (isEmpty(res)) {
|
||||
const reason = sanitizeCommandError(fetch_result.stderr);
|
||||
|
||||
log(sprintf('Failed to fetch resources from %s.', url));
|
||||
if (!isEmpty(reason))
|
||||
log(sprintf('Fetch stderr: %s', reason));
|
||||
reportSubscriptionDiagnostic('error',
|
||||
sprintf('订阅地址拉取失败:%s', url),
|
||||
reason || '请检查路由器到订阅源的连通性、DNS 解析和 TLS 访问是否正常');
|
||||
continue;
|
||||
}
|
||||
|
||||
let nodes;
|
||||
let nodes, parsed_as_surge = false;
|
||||
try {
|
||||
nodes = json(res).servers || json(res);
|
||||
|
||||
@@ -498,17 +638,41 @@ function main() {
|
||||
if (nodes[0].server && nodes[0].method)
|
||||
map(nodes, (_, i) => nodes[i].nodetype = 'sip008');
|
||||
} catch(e) {
|
||||
nodes = decodeBase64Str(res);
|
||||
nodes = nodes ? split(trim(replace(nodes, / /g, '_')), '\n') : [];
|
||||
/* 先识别 Surge/Clash 托管配置格式(YAML 风格),再回退 Base64。 */
|
||||
if (match(res, /(^|\n)[ \t]*proxies\s*:\s*(#[^\n]*)?(\n|$)/)) {
|
||||
nodes = parse_surge_subscription({
|
||||
isEmpty,
|
||||
validation,
|
||||
sing_features,
|
||||
log,
|
||||
apply_tls_cert_pin_policy
|
||||
}, res);
|
||||
parsed_as_surge = true;
|
||||
} else {
|
||||
nodes = decodeBase64Str(res);
|
||||
nodes = nodes ? split(trim(replace(nodes, / /g, '_')), '\n') : [];
|
||||
}
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for (let node in nodes) {
|
||||
let config;
|
||||
if (!isEmpty(node))
|
||||
config = parse_uri(node);
|
||||
if (!isEmpty(node)) {
|
||||
/* 信任边界来自控制流,而不是节点内容:只有 parse_surge_subscription()
|
||||
* 分支产出的节点才会被当作已解析 config。任意 JSON 中伪造的
|
||||
* 'nodetype'/'type' 不会绕过 parse_uri(),因为 JSON 路径下
|
||||
* parsed_as_surge=false,仍然走原有校验和白名单映射。 */
|
||||
if (parsed_as_surge && type(node) == 'object')
|
||||
config = node;
|
||||
else
|
||||
config = parse_uri(node);
|
||||
}
|
||||
if (isEmpty(config))
|
||||
continue;
|
||||
if (!isEmpty(config.__skip_reason)) {
|
||||
log(sprintf('Skipping node %s: %s', config.label || 'NULL', config.__skip_reason));
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = config.label;
|
||||
config.label = null;
|
||||
@@ -543,17 +707,20 @@ function main() {
|
||||
}
|
||||
|
||||
if (isEmpty(node_result)) {
|
||||
log('Failed to update subscriptions: no valid node found.');
|
||||
let fetch_errors = filter(subscription_diagnostics, (item) => item?.type === 'error' && item?.source === 'subscription');
|
||||
|
||||
if (via_proxy !== '1') {
|
||||
log('Starting service...');
|
||||
init_action('homeproxy', 'start');
|
||||
}
|
||||
log('Failed to update subscriptions: no valid node found.');
|
||||
if (!length(fetch_errors))
|
||||
reportSubscriptionDiagnostic('error',
|
||||
'订阅更新失败:没有找到可用节点。',
|
||||
'请检查订阅地址、过滤规则,以及是否有节点因当前 sing-box 不支持证书指纹而被跳过');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
let added = 0, removed = 0;
|
||||
let added = 0, updated = 0, removed = 0,
|
||||
stale_nodes = [],
|
||||
stale_labels = {};
|
||||
uci.foreach(uciconfig, ucinode, (cfg) => {
|
||||
/* Nodes created by the user */
|
||||
if (!cfg.grouphash)
|
||||
@@ -564,20 +731,59 @@ function main() {
|
||||
return null;
|
||||
|
||||
if (!node_cache[cfg.grouphash] || !node_cache[cfg.grouphash][cfg['.name']]) {
|
||||
uci.delete(uciconfig, cfg['.name']);
|
||||
removed++;
|
||||
|
||||
log(sprintf('Removing node: %s.', cfg.label || cfg['name']));
|
||||
push(stale_nodes, cfg['.name']);
|
||||
stale_labels[cfg['.name']] = cfg.label || cfg['.name'];
|
||||
return null;
|
||||
} else {
|
||||
const node = node_cache[cfg.grouphash][cfg['.name']];
|
||||
let node_changed = false;
|
||||
|
||||
for (let v in node) {
|
||||
if (isEmpty(node[v]))
|
||||
continue;
|
||||
|
||||
if (!(v in cfg) || !uci_value_equal(cfg[v], node[v]))
|
||||
node_changed = true;
|
||||
|
||||
uci.set(uciconfig, cfg['.name'], v, node[v]);
|
||||
}
|
||||
map(keys(cfg), (v) => {
|
||||
if (v in node_cache[cfg.grouphash][cfg['.name']])
|
||||
uci.set(uciconfig, cfg['.name'], v, node_cache[cfg.grouphash][cfg['.name']][v]);
|
||||
else
|
||||
if (substr(v, 0, 1) == '.')
|
||||
return null;
|
||||
|
||||
if (!(v in node) || isEmpty(node[v])) {
|
||||
uci.delete(uciconfig, cfg['.name'], v);
|
||||
node_changed = true;
|
||||
}
|
||||
});
|
||||
node_cache[cfg.grouphash][cfg['.name']].isExisting = true;
|
||||
|
||||
if (node_changed)
|
||||
updated++;
|
||||
|
||||
node.isExisting = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (length(stale_nodes)) {
|
||||
const cleanup = remove_subscription_nodes(uci, uciconfig, stale_nodes);
|
||||
removed += cleanup.removed;
|
||||
|
||||
for (let node_id in stale_nodes)
|
||||
log(sprintf('Removing node: %s.', stale_labels[node_id] || node_id));
|
||||
|
||||
if (cleanup.changed) {
|
||||
if (uci.get(uciconfig, ucimain, 'main_node') !== main_node)
|
||||
log(sprintf('Deleted subscription node affected main node, falling back to %s.',
|
||||
uci.get(uciconfig, ucimain, 'main_node') || 'nil'));
|
||||
if (uci.get(uciconfig, ucimain, 'main_udp_node') !== main_udp_node)
|
||||
log(sprintf('Deleted subscription node affected main UDP node, falling back to %s.',
|
||||
uci.get(uciconfig, ucimain, 'main_udp_node') || 'nil'));
|
||||
|
||||
for (let change in cleanup.changes)
|
||||
logCleanupChange(change);
|
||||
}
|
||||
}
|
||||
|
||||
for (let nodes in node_result)
|
||||
map(nodes, (node) => {
|
||||
if (node.isExisting)
|
||||
@@ -589,50 +795,54 @@ function main() {
|
||||
|
||||
added++;
|
||||
log(sprintf('Adding node: %s.', node.label));
|
||||
});
|
||||
});
|
||||
uci.commit(uciconfig);
|
||||
|
||||
let need_restart = (via_proxy !== '1');
|
||||
let need_restart = (via_proxy !== '1') || added > 0 || updated > 0 || removed > 0;
|
||||
if (!isEmpty(main_node)) {
|
||||
const first_server = uci.get_first(uciconfig, ucinode);
|
||||
if (first_server) {
|
||||
const has_server = !!uci.get_first(uciconfig, ucinode);
|
||||
if (has_server) {
|
||||
let main_urltest_nodes;
|
||||
if (main_node === 'urltest') {
|
||||
main_urltest_nodes = filter(uci.get(uciconfig, ucimain, 'main_urltest_nodes'), (v) => {
|
||||
main_urltest_nodes = filter(uci.get(uciconfig, ucimain, 'main_urltest_nodes') || [], (v) => {
|
||||
if (!uci.get(uciconfig, v)) {
|
||||
log(sprintf('Node %s is gone, removing from urltest list.', v));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
setOrDeleteList(ucimain, 'main_urltest_nodes', main_urltest_nodes);
|
||||
uci.commit(uciconfig);
|
||||
}
|
||||
|
||||
if ((main_node === 'urltest') ? !length(main_urltest_nodes) : !uci.get(uciconfig, main_node)) {
|
||||
uci.set(uciconfig, ucimain, 'main_node', first_server);
|
||||
uci.set(uciconfig, ucimain, 'main_node', 'nil');
|
||||
uci.commit(uciconfig);
|
||||
need_restart = true;
|
||||
|
||||
log('Main node is gone, switching to the first node.');
|
||||
log('Main node is gone, disabling main node.');
|
||||
}
|
||||
|
||||
if (!isEmpty(main_udp_node) && main_udp_node !== 'same') {
|
||||
let main_udp_urltest_nodes;
|
||||
if (main_udp_node === 'urltest') {
|
||||
main_udp_urltest_nodes = filter(uci.get(uciconfig, ucimain, 'main_udp_urltest_nodes'), (v) => {
|
||||
main_udp_urltest_nodes = filter(uci.get(uciconfig, ucimain, 'main_udp_urltest_nodes') || [], (v) => {
|
||||
if (!uci.get(uciconfig, v)) {
|
||||
log(sprintf('Node %s is gone, removing from urltest list.', v));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
setOrDeleteList(ucimain, 'main_udp_urltest_nodes', main_udp_urltest_nodes);
|
||||
uci.commit(uciconfig);
|
||||
}
|
||||
|
||||
if ((main_udp_node === 'urltest') ? !length(main_udp_urltest_nodes) : !uci.get(uciconfig, main_udp_node)) {
|
||||
uci.set(uciconfig, ucimain, 'main_udp_node', first_server);
|
||||
uci.set(uciconfig, ucimain, 'main_udp_node', 'same');
|
||||
uci.commit(uciconfig);
|
||||
need_restart = true;
|
||||
|
||||
log('Main UDP node is gone, switching to the first node.');
|
||||
log('Main UDP node is gone, falling back to the main node.');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -651,19 +861,37 @@ function main() {
|
||||
init_action('homeproxy', 'start');
|
||||
}
|
||||
|
||||
log(sprintf('%s nodes added, %s removed.', added, removed));
|
||||
log(sprintf('%s nodes added, %s updated, %s removed.', added, updated, removed));
|
||||
log('Successfully updated subscriptions.');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!isEmpty(subscription_urls))
|
||||
if (!isEmpty(subscription_urls)) {
|
||||
writeSubscriptionUpdateStatus(true, false, null, null);
|
||||
|
||||
try {
|
||||
call(main);
|
||||
const ok = call(main);
|
||||
writeSubscriptionUpdateStatus(false, true, ok === false ? false : true,
|
||||
(ok === false) ? (subscription_diagnostics[0]?.message || '订阅更新失败。') : null);
|
||||
} catch(e) {
|
||||
const status_error = sprintf('订阅更新异常:%s: %s', e.type || 'error', e.message || e);
|
||||
|
||||
log('[FATAL ERROR] An error occurred during updating subscriptions:');
|
||||
log(sprintf('%s: %s', e.type, e.message));
|
||||
log(e.stacktrace[0].context);
|
||||
reportSubscriptionDiagnostic('error',
|
||||
status_error,
|
||||
'请查看 HomeProxy 日志中的 [SUBSCRIBE] 记录并修复订阅配置');
|
||||
|
||||
log('Restarting service...');
|
||||
init_action('homeproxy', 'stop');
|
||||
init_action('homeproxy', 'start');
|
||||
writeSubscriptionUpdateStatus(false, true, false, status_error);
|
||||
}
|
||||
} else {
|
||||
subscription_diagnostics = [];
|
||||
writeSubscriptionUpdateStatus(false, true, false, '未配置订阅地址。');
|
||||
}
|
||||
|
||||
writeSubscriptionDiagnostics();
|
||||
Reference in new issue
Block a user