Files

72 lines
2.8 KiB
Ucode

/*
* SPDX-License-Identifier: GPL-2.0-only
*
* Pure subscription decoder. The fetcher returns raw bytes
* and the orchestrator used to inline the JSON / base64 / SIP008
* handling. Pulling it out gives the orchestrator a single
* `decode(content, log, url)` call and lets us unit-test each input
* shape without involving the network or UCI.
*
* Three input shapes are recognised, in order:
* 1. JSON with a top-level 'servers' array (clash proxy-provider)
* 2. JSON array directly (the same data, just unwrapped)
* 3. Shadowsocks SIP008 JSON: top-level array of objects with
* server+method. Each entry is tagged nodetype='sip008' so the
* orchestrator can dispatch them differently downstream.
*
* On JSON failure, decode the content as base64 and split on '\n'
* to get one share-link URI per line (sing-box subscription format).
*
* On total failure (no JSON, no base64), return an empty list and
* let the caller log the empty result.
*
* `url` is optional and only used to enrich the parse-failed log
* line; the decoder itself never makes a network call.
*/
'use strict';
import { isEmpty, decodeBase64Str, redactUrl } from '../homeproxy-pro.uc';
export function decode(content, log, url) {
if (isEmpty(content))
return [];
let nodes;
try {
const parsed = json(content);
nodes = parsed.servers || parsed;
/* Shadowsocks SIP008: each entry is a JSON object with
* server + method, not a URI string.
*
* The probe has to check the type first. It used to be a bare
* `nodes[0].server`, which THROWS in ucode when nodes[0] is a
* string ("left-hand side expression is not an array or
* object") - and the throw was swallowed by the JSON
* try/catch below, so a perfectly valid `{"servers":["vless://…"]}`
* or `["vless://…"]` subscription fell through to the base64
* fallback, failed there too and decoded to an empty list. Two
* real subscription shapes were silently discarded, and the
* decoder test had recorded that as the contract. The same
* guard applies inside the loop so a mixed list (SIP008 objects
* plus share links) keeps both halves instead of losing
* everything on the first string. */
if (type(nodes[0]) === 'object' && nodes[0].server && nodes[0].method) {
for (let i = 0; i < length(nodes); i++)
if (type(nodes[i]) === 'object')
nodes[i].nodetype = 'sip008';
}
} catch (e) {
/* The URL carries the subscription token and this line lands in
* homeproxy-pro.log, which is world-readable. fetcher.uc and the
* orchestrator both redact; this path was missed. */
const tag = url ? sprintf('for %s, ', redactUrl(url)) : '';
log(sprintf('JSON parse failed %strying base64: %s', tag, e.message));
const decoded = decodeBase64Str(content);
nodes = decoded ? split(trim(decoded), '\n') : [];
}
return nodes;
};