mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-07 12:44:06 +08:00
⛅ Sync 2026-09-29 21:45:00
This commit is contained in:
1 parent
a4d6e3e4b9
commit
da77daf88e
191 files changed
+8077
-6247
No files matched your search
@@ -8,14 +8,14 @@
|
||||
'use strict';
|
||||
|
||||
import { md5 } from 'digest';
|
||||
import { open } from 'fs';
|
||||
import { lstat, 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
|
||||
wGETVerbose, decodeBase64Str, getTime, isEmpty, shellQuote, HP_DIR, RUN_DIR
|
||||
} from 'homeproxy';
|
||||
|
||||
import { parse_uri } from 'parse_uri';
|
||||
@@ -88,6 +88,94 @@ function log(...args) {
|
||||
logfile.close();
|
||||
}
|
||||
|
||||
/*
|
||||
* Subscription URLs usually carry the provider token in the query string;
|
||||
* the log is surfaced in LuCI, so keep it out.
|
||||
*/
|
||||
function redact(url) {
|
||||
return replace(url, /[?#].*$/, '');
|
||||
}
|
||||
|
||||
/*
|
||||
* mkdir is the lock: atomic everywhere this runs and no flock binding needed.
|
||||
* A lock older than LOCK_STALE is broken, so a killed run cannot block every
|
||||
* later update. Two concurrent runs (cron + the LuCI button) used to
|
||||
* interleave commits and service restarts.
|
||||
*/
|
||||
const LOCK_DIR = RUN_DIR + '/update_subscriptions.lock';
|
||||
const LOCK_STALE = 600;
|
||||
|
||||
function acquire_lock() {
|
||||
if (system(sprintf('mkdir %s 2>/dev/null', shellQuote(LOCK_DIR))) === 0)
|
||||
return true;
|
||||
|
||||
const st = lstat(LOCK_DIR);
|
||||
const age = st ? max(0, time() - (st.mtime || 0)) : null;
|
||||
|
||||
if (age != null && age > LOCK_STALE) {
|
||||
log(sprintf('Breaking a stale update lock (%s seconds old).', age));
|
||||
system(sprintf('rmdir %s 2>/dev/null', shellQuote(LOCK_DIR)));
|
||||
|
||||
return system(sprintf('mkdir %s 2>/dev/null', shellQuote(LOCK_DIR))) === 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function release_lock() {
|
||||
system(sprintf('rmdir %s 2>/dev/null', shellQuote(LOCK_DIR)));
|
||||
}
|
||||
|
||||
/*
|
||||
* sing-box rejects the whole generated config over one malformed node, and the
|
||||
* generator keeps the old config when it fails, so a node that cannot possibly
|
||||
* work must not reach UCI. Returns the reason, or null when the node is usable.
|
||||
*/
|
||||
function node_problem(cfg) {
|
||||
if (isEmpty(cfg.address) || isEmpty(cfg.port))
|
||||
return 'missing address or port';
|
||||
if (!validation_port(cfg.port))
|
||||
return 'invalid port';
|
||||
|
||||
switch (cfg.type) {
|
||||
case 'shadowsocks':
|
||||
if (isEmpty(cfg.shadowsocks_encrypt_method) || isEmpty(cfg.password))
|
||||
return 'missing shadowsocks method or password';
|
||||
break;
|
||||
case 'vless':
|
||||
case 'vmess':
|
||||
if (isEmpty(cfg.uuid))
|
||||
return 'missing uuid';
|
||||
break;
|
||||
case 'trojan':
|
||||
case 'anytls':
|
||||
case 'snell':
|
||||
if (isEmpty(cfg.password))
|
||||
return 'missing password';
|
||||
break;
|
||||
case 'tuic':
|
||||
if (isEmpty(cfg.uuid) || isEmpty(cfg.password))
|
||||
return 'missing uuid or password';
|
||||
break;
|
||||
case 'hysteria':
|
||||
case 'hysteria2':
|
||||
if (isEmpty(cfg.password) && isEmpty(cfg.hysteria_auth_payload))
|
||||
return 'missing auth';
|
||||
break;
|
||||
case 'socks':
|
||||
case 'http':
|
||||
break;
|
||||
default:
|
||||
return 'unsupported type';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function validation_port(port) {
|
||||
return match(port, /^\d{1,5}$/) != null && int(port) > 0 && int(port) <= 65535;
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (via_proxy !== '1') {
|
||||
log('Stopping service...');
|
||||
@@ -98,10 +186,11 @@ function main() {
|
||||
url = replace(url, /#.*$/, '');
|
||||
const groupHash = md5(url);
|
||||
node_cache[groupHash] = {};
|
||||
const shown_url = redact(url);
|
||||
|
||||
const fetched = wGETVerbose(url, user_agent);
|
||||
if (isEmpty(fetched.content)) {
|
||||
log(sprintf('Failed to fetch resources from %s: %s', url, fetched.error || 'empty response'));
|
||||
log(sprintf('Failed to fetch resources from %s: %s', shown_url, fetched.error || 'empty response'));
|
||||
continue;
|
||||
}
|
||||
const res = fetched.content;
|
||||
@@ -114,19 +203,43 @@ function main() {
|
||||
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));
|
||||
/* Clash/mihomo subscriptions are YAML, not base64; say so instead
|
||||
of reporting a base64/JSON failure followed by "no valid node". */
|
||||
if (match(res, /(^|\n)[ \t]*(proxies|proxy-groups)[ \t]*:/)) {
|
||||
log(sprintf('Unsupported subscription format (Clash/mihomo YAML) at %s.', shown_url));
|
||||
continue;
|
||||
}
|
||||
|
||||
log(sprintf('JSON parse failed for %s, trying base64: %s', shown_url, e.message));
|
||||
nodes = decodeBase64Str(res);
|
||||
nodes = nodes ? split(trim(nodes), '\n') : [];
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
let count = 0, skipped = 0;
|
||||
for (let node in nodes) {
|
||||
let config;
|
||||
if (!isEmpty(node))
|
||||
config = parse_uri(node, sing_features, log);
|
||||
if (isEmpty(config))
|
||||
if (isEmpty(node))
|
||||
continue;
|
||||
|
||||
const config = parse_uri(node, sing_features, log);
|
||||
if (isEmpty(config)) {
|
||||
/* No credentials in the log: only the scheme says whether a
|
||||
parser is missing or the entry is simply not a node. Only
|
||||
the first few are printed, the rest is summarized below. */
|
||||
if (skipped < 5)
|
||||
log(sprintf('Skipping unparsable entry (%s) from %s.',
|
||||
type(node) === 'string' ? replace(node, /:.*$/, '') : type(node), shown_url));
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const problem = node_problem(config);
|
||||
if (problem) {
|
||||
if (skipped < 5)
|
||||
log(sprintf('Skipping invalid node %s: %s.', config.label || config.address, problem));
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = config.label;
|
||||
config.label = null;
|
||||
const confHash = md5(sprintf('%J', config)),
|
||||
@@ -153,10 +266,13 @@ function main() {
|
||||
}
|
||||
}
|
||||
|
||||
if (skipped > 0)
|
||||
log(sprintf('%s entries skipped from %s.', skipped, shown_url));
|
||||
|
||||
if (count === 0)
|
||||
log(sprintf('No valid node found in %s.', url));
|
||||
log(sprintf('No valid node found in %s.', shown_url));
|
||||
else
|
||||
log(sprintf('Successfully fetched %s nodes of total %s from %s.', count, length(nodes), url));
|
||||
log(sprintf('Successfully fetched %s nodes of total %s from %s.', count, length(nodes), shown_url));
|
||||
}
|
||||
|
||||
if (isEmpty(node_result)) {
|
||||
@@ -170,7 +286,7 @@ function main() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let added = 0, removed = 0;
|
||||
let added = 0, removed = 0, changed = false;
|
||||
uci.foreach(uciconfig, ucinode, (cfg) => {
|
||||
/* Nodes created by the user */
|
||||
if (!cfg.grouphash)
|
||||
@@ -183,14 +299,30 @@ function main() {
|
||||
if (!node_cache[cfg.grouphash][cfg['.name']]) {
|
||||
uci.delete(uciconfig, cfg['.name']);
|
||||
removed++;
|
||||
changed = true;
|
||||
|
||||
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
|
||||
/* skip the cursor's own metadata (.name/.type/...): deleting
|
||||
those is a no-op and would mark every run as changed */
|
||||
if (substr(v, 0, 1) === '.')
|
||||
return null;
|
||||
|
||||
if (v in node_cache[cfg.grouphash][cfg['.name']]) {
|
||||
/* only write what actually differs, so an unchanged
|
||||
subscription does not fake a change below */
|
||||
const fresh = node_cache[cfg.grouphash][cfg['.name']][v];
|
||||
const current = uci.get(uciconfig, cfg['.name'], v);
|
||||
if (type(fresh) === 'array' ? (sprintf('%J', uci.get_all(uciconfig, cfg['.name'], v) || []) !== sprintf('%J', fresh))
|
||||
: (current !== sprintf('%s', fresh))) {
|
||||
uci.set(uciconfig, cfg['.name'], v, fresh);
|
||||
changed = true;
|
||||
}
|
||||
} else {
|
||||
uci.delete(uciconfig, cfg['.name'], v);
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
node_cache[cfg.grouphash][cfg['.name']].isExisting = true;
|
||||
}
|
||||
@@ -205,11 +337,15 @@ function main() {
|
||||
map(keys(node), (v) => uci.set(uciconfig, nameHash, v, node[v]));
|
||||
|
||||
added++;
|
||||
changed = true;
|
||||
log(sprintf('Adding node: %s.', node.label));
|
||||
});
|
||||
uci.commit(uciconfig);
|
||||
|
||||
let need_restart = (via_proxy !== '1');
|
||||
/* via_proxy keeps the service up during the fetch, but a configuration
|
||||
change still has to be applied - it used to be committed and then
|
||||
ignored until the next restart. */
|
||||
let need_restart = (via_proxy !== '1') || changed;
|
||||
if (!isEmpty(main_node)) {
|
||||
const first_server = uci.get_first(uciconfig, ucinode);
|
||||
if (first_server) {
|
||||
@@ -299,7 +435,12 @@ function main() {
|
||||
log('Successfully updated subscriptions.');
|
||||
}
|
||||
|
||||
if (!isEmpty(subscription_urls))
|
||||
if (!isEmpty(subscription_urls)) {
|
||||
if (!acquire_lock()) {
|
||||
log('Another subscription update is running, aborting.');
|
||||
exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
call(main);
|
||||
} catch(e) {
|
||||
@@ -311,3 +452,6 @@ if (!isEmpty(subscription_urls))
|
||||
init_action('homeproxy', 'stop');
|
||||
init_action('homeproxy', 'start');
|
||||
}
|
||||
|
||||
release_lock();
|
||||
}
|
||||
Reference in new issue
Block a user