mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-28 02:42:51 +08:00
314 lines
9.1 KiB
Ucode
Executable File
314 lines
9.1 KiB
Ucode
Executable File
#!/usr/bin/ucode
|
|
/*
|
|
* SPDX-License-Identifier: GPL-2.0-only
|
|
*
|
|
* Copyright (C) 2023 ImmortalWrt.org
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
import { md5 } from 'digest';
|
|
import { open } from 'fs';
|
|
import { connect } from 'ubus';
|
|
import { cursor } from 'uci';
|
|
|
|
import { init_action } from 'luci.sys';
|
|
|
|
import {
|
|
wGETVerbose, decodeBase64Str, getTime, isEmpty, HP_DIR, RUN_DIR
|
|
} from 'homeproxy';
|
|
|
|
import { parse_uri } from 'parse_uri';
|
|
|
|
/* UCI config start */
|
|
const uci = cursor();
|
|
|
|
const uciconfig = 'homeproxy';
|
|
uci.load(uciconfig);
|
|
|
|
const ucimain = 'config',
|
|
ucinode = 'node',
|
|
ucisubscription = 'subscription';
|
|
|
|
const allow_insecure = uci.get(uciconfig, ucisubscription, 'allow_insecure') || '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',
|
|
subscription_urls = uci.get(uciconfig, ucisubscription, 'subscription_url') || [],
|
|
user_agent = uci.get(uciconfig, ucisubscription, 'user_agent'),
|
|
via_proxy = uci.get(uciconfig, ucisubscription, 'update_via_proxy') || '0';
|
|
|
|
const routing_mode = uci.get(uciconfig, ucimain, 'routing_mode') || 'bypass_mainland_china';
|
|
let main_node, main_udp_node;
|
|
if (routing_mode !== 'custom') {
|
|
main_node = uci.get(uciconfig, ucimain, 'main_node') || 'nil';
|
|
main_udp_node = uci.get(uciconfig, ucimain, 'main_udp_node') || 'nil';
|
|
}
|
|
/* UCI config end */
|
|
|
|
/* String helper start */
|
|
function filter_check(name) {
|
|
if (isEmpty(name) || filter_mode === 'disabled' || isEmpty(filter_keywords))
|
|
return false;
|
|
|
|
let ret = false;
|
|
for (let i in filter_keywords) {
|
|
let patten;
|
|
try {
|
|
patten = regexp(i);
|
|
} catch(e) {
|
|
log(sprintf('Skipping invalid filter keyword regex: %s.', i));
|
|
continue;
|
|
}
|
|
if (patten && match(name, patten))
|
|
ret = true;
|
|
}
|
|
if (filter_mode === 'whitelist')
|
|
ret = !ret;
|
|
|
|
return ret;
|
|
}
|
|
/* String helper end */
|
|
|
|
/* Common var start */
|
|
const node_cache = {},
|
|
node_result = [];
|
|
|
|
const ubus = connect();
|
|
const sing_features = ubus.call('luci.homeproxy', 'singbox_get_features', {}) || {};
|
|
if (isEmpty(sing_features))
|
|
log('Warning: Failed to query sing-box features via ubus, assuming defaults.');
|
|
/* Common var end */
|
|
|
|
/* Log */
|
|
system(`mkdir -p ${RUN_DIR}`);
|
|
function log(...args) {
|
|
const logfile = open(`${RUN_DIR}/homeproxy.log`, 'a');
|
|
logfile.write(`${getTime()} [SUBSCRIBE] ${join(' ', args)}\n`);
|
|
logfile.close();
|
|
}
|
|
|
|
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 fetched = wGETVerbose(url, user_agent);
|
|
if (isEmpty(fetched.content)) {
|
|
log(sprintf('Failed to fetch resources from %s: %s', url, fetched.error || 'empty response'));
|
|
continue;
|
|
}
|
|
const res = fetched.content;
|
|
|
|
let nodes;
|
|
try {
|
|
nodes = json(res).servers || json(res);
|
|
|
|
/* Shadowsocks SIP008 format */
|
|
if (nodes[0].server && nodes[0].method)
|
|
map(nodes, (_, i) => nodes[i].nodetype = 'sip008');
|
|
} catch(e) {
|
|
log(sprintf('JSON parse failed for %s, trying base64: %s', url, e.message));
|
|
nodes = decodeBase64Str(res);
|
|
nodes = nodes ? split(trim(nodes), '\n') : [];
|
|
}
|
|
|
|
let count = 0;
|
|
for (let node in nodes) {
|
|
let config;
|
|
if (!isEmpty(node))
|
|
config = parse_uri(node, sing_features, log);
|
|
if (isEmpty(config))
|
|
continue;
|
|
|
|
const label = config.label;
|
|
config.label = null;
|
|
const confHash = md5(sprintf('%J', config)),
|
|
nameHash = md5(groupHash + label);
|
|
config.label = label;
|
|
|
|
if (filter_check(config.label))
|
|
log(sprintf('Skipping blacklist node: %s.', config.label));
|
|
else if (node_cache[groupHash][confHash] || node_cache[groupHash][nameHash])
|
|
log(sprintf('Skipping duplicate node: %s.', config.label));
|
|
else {
|
|
if (config.tls === '1' && allow_insecure === '1')
|
|
config.tls_insecure = '1';
|
|
if (config.type in ['vless', 'vmess'])
|
|
config.packet_encoding = packet_encoding;
|
|
|
|
config.grouphash = groupHash;
|
|
push(node_result, []);
|
|
push(node_result[length(node_result)-1], config);
|
|
node_cache[groupHash][confHash] = config;
|
|
node_cache[groupHash][nameHash] = config;
|
|
|
|
count++;
|
|
}
|
|
}
|
|
|
|
if (count === 0)
|
|
log(sprintf('No valid node found in %s.', url));
|
|
else
|
|
log(sprintf('Successfully fetched %s nodes of total %s from %s.', count, length(nodes), url));
|
|
}
|
|
|
|
if (isEmpty(node_result)) {
|
|
log('Failed to update subscriptions: no valid node found.');
|
|
|
|
if (via_proxy !== '1') {
|
|
log('Starting service...');
|
|
init_action('homeproxy', 'start');
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
let added = 0, removed = 0;
|
|
uci.foreach(uciconfig, ucinode, (cfg) => {
|
|
/* Nodes created by the user */
|
|
if (!cfg.grouphash)
|
|
return null;
|
|
|
|
/* Empty object - failed to fetch nodes, or subscription URL not yet processed */
|
|
if (!node_cache[cfg.grouphash] || length(node_cache[cfg.grouphash]) === 0)
|
|
return null;
|
|
|
|
if (!node_cache[cfg.grouphash][cfg['.name']]) {
|
|
uci.delete(uciconfig, cfg['.name']);
|
|
removed++;
|
|
|
|
log(sprintf('Removing node: %s.', cfg.label || cfg['name']));
|
|
} else {
|
|
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
|
|
uci.delete(uciconfig, cfg['.name'], v);
|
|
});
|
|
node_cache[cfg.grouphash][cfg['.name']].isExisting = true;
|
|
}
|
|
});
|
|
for (let nodes in node_result)
|
|
map(nodes, (node) => {
|
|
if (node.isExisting)
|
|
return null;
|
|
|
|
const nameHash = md5(node.grouphash + node.label);
|
|
uci.set(uciconfig, nameHash, 'node');
|
|
map(keys(node), (v) => uci.set(uciconfig, nameHash, v, node[v]));
|
|
|
|
added++;
|
|
log(sprintf('Adding node: %s.', node.label));
|
|
});
|
|
uci.commit(uciconfig);
|
|
|
|
let need_restart = (via_proxy !== '1');
|
|
if (!isEmpty(main_node)) {
|
|
const first_server = uci.get_first(uciconfig, ucinode);
|
|
if (first_server) {
|
|
let main_urltest_nodes;
|
|
if (main_node === 'urltest') {
|
|
const old_urltest_nodes = uci.get(uciconfig, ucimain, 'main_urltest_nodes') || [];
|
|
main_urltest_nodes = filter(old_urltest_nodes, (v) => {
|
|
if (!uci.get(uciconfig, v)) {
|
|
log(sprintf('Node %s is gone, removing from urltest list.', v));
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
if (length(main_urltest_nodes) !== length(old_urltest_nodes)) {
|
|
uci.set(uciconfig, ucimain, 'main_urltest_nodes', main_urltest_nodes);
|
|
uci.commit(uciconfig);
|
|
need_restart = true;
|
|
}
|
|
}
|
|
|
|
if ((main_node === 'urltest') ? !length(main_urltest_nodes) : !uci.get(uciconfig, main_node)) {
|
|
uci.set(uciconfig, ucimain, 'main_node', first_server);
|
|
uci.commit(uciconfig);
|
|
need_restart = true;
|
|
|
|
log('Main node is gone, switching to the first node.');
|
|
}
|
|
|
|
if (!isEmpty(main_udp_node) && main_udp_node !== 'same') {
|
|
let main_udp_urltest_nodes;
|
|
if (main_udp_node === 'urltest') {
|
|
const old_udp_urltest_nodes = uci.get(uciconfig, ucimain, 'main_udp_urltest_nodes') || [];
|
|
main_udp_urltest_nodes = filter(old_udp_urltest_nodes, (v) => {
|
|
if (!uci.get(uciconfig, v)) {
|
|
log(sprintf('Node %s is gone, removing from urltest list.', v));
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
if (length(main_udp_urltest_nodes) !== length(old_udp_urltest_nodes)) {
|
|
uci.set(uciconfig, ucimain, 'main_udp_urltest_nodes', main_udp_urltest_nodes);
|
|
uci.commit(uciconfig);
|
|
need_restart = true;
|
|
}
|
|
}
|
|
|
|
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.commit(uciconfig);
|
|
need_restart = true;
|
|
|
|
log('Main UDP node is gone, switching to the first node.');
|
|
}
|
|
}
|
|
} else {
|
|
uci.set(uciconfig, ucimain, 'main_node', 'nil');
|
|
uci.set(uciconfig, ucimain, 'main_udp_node', 'nil');
|
|
uci.commit(uciconfig);
|
|
need_restart = true;
|
|
|
|
log('No available node, disable tproxy.');
|
|
}
|
|
}
|
|
|
|
/* Scrub stale urltest member references in custom routing nodes */
|
|
uci.foreach(uciconfig, 'routing_node', (cfg) => {
|
|
if (cfg.node !== 'urltest' || isEmpty(cfg.urltest_nodes))
|
|
return null;
|
|
|
|
const cleaned_nodes = filter(cfg.urltest_nodes, (v) => uci.get(uciconfig, v));
|
|
if (length(cleaned_nodes) !== length(cfg.urltest_nodes)) {
|
|
uci.set(uciconfig, cfg['.name'], 'urltest_nodes', cleaned_nodes);
|
|
uci.commit(uciconfig);
|
|
need_restart = true;
|
|
|
|
log(sprintf('Routing node %s: removed gone nodes from urltest list.', cfg['.name']));
|
|
}
|
|
});
|
|
|
|
if (need_restart) {
|
|
log('Restarting service...');
|
|
init_action('homeproxy', 'stop');
|
|
init_action('homeproxy', 'start');
|
|
}
|
|
|
|
log(sprintf('%s nodes added, %s removed.', added, removed));
|
|
log('Successfully updated subscriptions.');
|
|
}
|
|
|
|
if (!isEmpty(subscription_urls))
|
|
try {
|
|
call(main);
|
|
} catch(e) {
|
|
log('[FATAL ERROR] An error occurred during updating subscriptions:');
|
|
log(sprintf('%s: %s', e.type, e.message));
|
|
log(e.stacktrace[0].context);
|
|
|
|
log('Restarting service...');
|
|
init_action('homeproxy', 'stop');
|
|
init_action('homeproxy', 'start');
|
|
}
|