✨ Sync 2026-10-11 20:48:25

This commit is contained in:
github-actions[bot] committed 2026-10-11 20:48:25 +08:00
1 parent 78f0ba8251
commit 5ce75c9f2b
45 files changed
+54 -879

No files matched your search

@@ -2,14 +2,6 @@
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2025 ImmortalWrt.org
*
* Test double for root/etc/homeproxy/scripts/homeproxy.uc.
*
* Only `validation()` is stubbed: the real one shells out to
* /sbin/validate_data, which does not exist outside OpenWrt. Everything else
* (isEmpty, decodeBase64Str, parseURL) is a verbatim copy so the parser tests
* exercise the same string handling as production. Keep these in sync when
* homeproxy.uc changes.
*/
import { urldecode_params } from 'luci.http';
@@ -33,7 +25,6 @@ export function decodeBase64Str(str) {
return b64dec(str);
};
/* Pure-ucode stand-in for /sbin/validate_data (host/port/ip4/ip6/hostname). */
export function validation(datatype, data) {
if (!datatype || !data)
return null;
@@ -62,14 +53,6 @@ export function validation(datatype, data) {
}
};
/*
* Decode %XX escapes without touching '+'. urldecode() maps '+' to a space,
* which corrupts base64 userinfo ("+" is part of the alphabet), so share-link
* userinfo is percent-decoded with this instead.
*/
/*
* ucode has no tolower(), so both hex cases are mapped by hand.
*/
function hexValue(ch) {
const lower = index('0123456789abcdef', ch);
if (lower >= 0)
@@ -80,10 +63,6 @@ function hexValue(ch) {
return upper < 0 ? -1 : 10 + upper;
}
/*
* ucode's regex engine rejects \x00 inside a character class ("Missing ']'"),
* so control bytes are checked by hand.
*/
function hasControlChar(str) {
for (let i = 0; i < length(str); i++) {
const code = ord(str, i);
@@ -160,7 +139,6 @@ export function parseURL(url) {
return null;
if (objurl.userinfo) {
/* keep in sync with the production parser (homeproxy.uc) */
let userinfo = percentDecode(objurl.userinfo);
userinfo = replace(userinfo, /:(.+)$/, (_, val) => {
-14
View File
@@ -1,11 +1,5 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
#
# Run the ucode-level tests: the parse_uri unit tests, the fw4 inventory check
# and the generator regression fixtures. Requires ucode; sing-box is needed for
# the generator cases (they validate the emitted config with `sing-box check`).
#
# Usage: sh tests/ucode/run.sh <repo-root> [work-dir]
ROOT="${1:-.}"
WORK="${2:-/tmp/hp-ucode-tests}"
@@ -16,8 +10,6 @@ FAILED=0
echo "== ucode syntax check =="
for file in "$ROOT"/root/etc/homeproxy/scripts/*.uc "$ROOT"/root/usr/share/rpcd/ucode/*; do
[ -f "$file" ] || continue
# Modules (with export statements) cannot be compiled as a program; they
# are loaded through `import` below instead.
case "$file" in
*homeproxy.uc|*parse_uri.uc) continue ;;
esac
@@ -82,10 +74,6 @@ else
fi
echo "== wget flavour selection =="
# A stub /usr/bin/wget whose --version answer comes from the environment, so
# both flavours can be exercised on a single host. The empty-answer/non-zero
# case is uclient-fetch, whose getopt rejects --version just like it rejects
# -nv before sending anything.
rm -rf "$WORK/wget_flavor"
mkdir -p "$WORK/wget_flavor"
cat > "$WORK/wget_flavor/wget-stub" <<'STUB'
@@ -125,8 +113,6 @@ for flavor in gnu compat; do
done
echo "== real uclient-fetch compatibility =="
# uclient-fetch is the default /usr/bin/wget on OpenWrt/ImmortalWrt, so a
# GNU-only option anywhere in the generated command line has to fail here.
if [ -x /bin/uclient-fetch ]; then
rm -rf "$WORK/uclient"
mkdir -p "$WORK/uclient"
@@ -1,15 +1,5 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
#
# Render firewall_post.ut and verify the homeproxy fw4 objects survive as real
# nft statements.
#
# The template opens with `{%-`, which trims the whitespace in front of it: any
# text placed above that tag loses its trailing newline and is glued onto the
# first generated statement, turning it into a comment. That silently dropped
# every homeproxy chain/set from the fw4 ruleset once, so keep this check.
#
# Usage: sh tests/ucode/test_firewall_template.sh <repo-root>
ROOT="${1:-.}"
ROOT="$(cd "$ROOT" && pwd)"
@@ -23,8 +13,6 @@ if ! utpl -S "$ROOT/root/etc/homeproxy/scripts/firewall_post.ut" > "$OUT" 2>"/tm
exit 1
fi
# Declared unconditionally by the template, so they must always be present as
# standalone statements.
for name in homeproxy_local_addr_v4 homeproxy_wan_proxy_addr_v4; do
if ! grep -q "^set $name {" "$OUT"; then
echo "FAIL: 'set $name {' is missing from the rendered firewall template"
@@ -33,8 +21,6 @@ for name in homeproxy_local_addr_v4 homeproxy_wan_proxy_addr_v4; do
fi
done
# A statement glued onto the preceding comment looks like
# "# <comment text>.set homeproxy_x {" -- exactly the failure mode above.
if grep -nE "^#[^!].*homeproxy_[a-z0-9_]+ \{" "$OUT" > "/dev/null"; then
echo "FAIL: an nft statement is glued onto a comment line"
grep -nE "^#[^!].*homeproxy_[a-z0-9_]+ \{" "$OUT" | head -2
@@ -1,13 +1,5 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
#
# The fw4 chain/set inventory in scripts/fw4_names.sh must stay in sync with
# the objects declared by scripts/firewall_post.ut: the first drives the
# cleanup in init.d/homeproxy, the second creates them. A drift means either a
# leftover object or a delete of something that never existed (which used to
# abort the whole cleanup batch).
#
# Usage: sh tests/ucode/test_fw4_names.sh <repo-root>
ROOT="${1:-.}"
ROOT="$(cd "$ROOT" && pwd)"
@@ -18,7 +10,6 @@ PRE="$ROOT/root/etc/homeproxy/scripts/firewall_pre.uc"
FAILED=0
# shellcheck source=/dev/null
. "$NAMES"
check_group() {
@@ -57,8 +48,6 @@ post_sets="$(grep -oE '^set homeproxy_[a-z0-9_]+' "$POST" | awk '{print $2}' | s
check_group "chain" "$HP_FW4_CHAINS" "$post_chains"
check_group "set" "$HP_FW4_SETS" "$post_sets"
# firewall_pre.uc only writes forward/input accept rules; it must not own any
# chain/set of its own.
pre_refs="$(grep -oE 'homeproxy_[a-z0-9_]+' "$PRE" | sort -u)"
if [ -n "$pre_refs" ]; then
echo "NOTE: firewall_pre.uc references fw4 objects:$pre_refs"
@@ -1,13 +1,5 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
#
# Run generate_client.uc / generate_server.uc against the UCI fixtures and let
# sing-box validate the result. The generators read /etc/config and write
# /var/run, so this script materialises an isolated copy of them: homeproxy.uc
# is rewritten to point HP_DIR/RUN_DIR at a scratch directory, and the uci
# cursor is pointed at the fixture config.
#
# Usage: sh tests/ucode/test_generators.sh <repo-root> [work-dir]
ROOT="${1:-.}"
WORK="${2:-/tmp/hp-generator-test}"
@@ -29,8 +21,6 @@ run_case() {
: > "$dir/resources/proxy_list.txt"
if [ "$local_rs" = "1" ]; then
# with the .srs files that update_resources.sh downloads in place, the
# generator has to load them instead of the remote URLs
printf '%s' '{"version":1,"rules":[{"domain_suffix":["example.com"]}]}' > "$dir/rs-src.json"
if ! sing-box rule-set compile "$dir/rs-src.json" -o "$dir/resources/geoip_cn.srs" \
|| ! sing-box rule-set compile "$dir/rs-src.json" -o "$dir/resources/geosite_cn.srs"; then
@@ -41,7 +31,6 @@ run_case() {
fi
if grep -q "__RULESET_DIR__" "$fixture"; then
# The fixture needs a real local rule-set on disk.
printf '%s' '{"version":1,"rules":[{"domain_suffix":["example.com"]}]}' > "$dir/ruleset/src.json"
if ! sing-box rule-set compile "$dir/ruleset/src.json" -o "$dir/ruleset/test.srs"; then
echo "FAIL: $name: could not compile the local rule-set fixture"
@@ -83,10 +72,6 @@ run_case() {
run_case client "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json
# Golden comparison for the client generator. `sing-box check` only proves the
# config parses; this proves the generated config did not change silently (rule
# order, DNS routing decisions, whether a field is emitted at all). The work
# directory is normalized out of the paths before comparing.
CLIENT_GOLDEN="$ROOT/tests/snapshots/client.generated.json"
CLIENT_OUT="$WORK/client/run/sing-box-c.json"
if [ ! -f "$CLIENT_GOLDEN" ]; then
@@ -103,10 +88,6 @@ else
FAILED=1
fi
# The preset remote rule-sets must be fetched through the node. A direct
# download depends on the CDN staying reachable from mainland China and fails
# intermittently under DNS pollution, which shows up as "open connection to
# <ip>:443 using outbound/direct[direct]: i/o timeout" in sing-box-c.log.
if grep -q '"detour": "direct-out"' "$WORK/client/run/sing-box-c.json"; then
echo "FAIL: client: a remote rule-set would still be downloaded directly"
FAILED=1
@@ -116,9 +97,6 @@ if ! grep -q '"detour": "main-out"' "$WORK/client/run/sing-box-c.json"; then
FAILED=1
fi
# Local rule-sets (downloaded by update_resources.sh) must be preferred over
# the remote URLs, so a cold start does not depend on reaching the CDN through
# the node.
run_case client-local "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json 1
if grep -q '"type": "local"' "$WORK/client-local/run/sing-box-c.json" \
&& ! grep -q '"type": "remote"' "$WORK/client-local/run/sing-box-c.json"; then
@@ -129,12 +107,6 @@ else
fi
run_case custom "$ROOT/tests/fixtures/generators/custom.uci" generate_client.uc sing-box-c.json
# Custom mode without a default outbound: the default shape of the custom UI.
# `sing-box check` alone is not enough -- before the fix the generator emitted a
# config with none of the user's DNS servers, nodes, rules or rule-sets, and
# sing-box 1.14 then refused to start it for a missing
# route.default_domain_resolver. Assert on the emitted config so a partial
# regression cannot pass as green.
run_case custom_no_default "$ROOT/tests/fixtures/generators/custom-no-default-outbound.uci" generate_client.uc sing-box-c.json
CUSTOM_OUT="$WORK/custom_no_default/run/sing-box-c.json"
@@ -150,12 +122,8 @@ assert_json() {
fi
}
# sing-box always emits default-dns and system-dns, so more than one DNS server
# is always present and 1.14 requires a global domain resolver.
assert_json "custom/no-default: a global domain resolver is declared" '"default_domain_resolver"'
assert_json "custom/no-default: the user DNS server survived" '"cfg-DNS_Server_Proxy-dns"'
# A sing-box rule has no label field, so the UCI `option label` never reaches the
# JSON. Match the marker the rule body actually carries.
assert_json "custom/no-default: the user DNS rule survived" 'dns-rule-marker.example.com'
assert_json "custom/no-default: the urltest group survived" '"cfg-Routing_Node_Proxy-out"'
assert_json "custom/no-default: its member nodes survived" '"cfg-n_hk-out"'
@@ -3,10 +3,6 @@
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2025 ImmortalWrt.org
*
* Regression tests for homeproxy.uc helpers, focused on executeCommand():
* return shape, stderr/exit-code capture, binary detection and (most
* importantly) that the temporary descriptors are closed on every run.
*/
'use strict';
@@ -32,7 +28,6 @@ function fd_count() {
return n;
}
/* successful command */
const ok = executeCommand('echo', 'hello');
expect('ok.exitcode', ok.exitcode, 0);
expect('ok.stdout', ok.stdout, 'hello\n');
@@ -40,29 +35,22 @@ expect('ok.stderr', ok.stderr, '');
expect('ok.binary', ok.binary, false);
expect('ok.command', ok.command, 'echo hello');
/* failing command keeps stderr and the exit code */
const bad = executeCommand('sh', '-c', shellQuote('echo oops >&2; exit 3'));
expect('bad.exitcode', bad.exitcode, 3);
expect('bad.stderr', bad.stderr, 'oops\n');
expect('bad.stdout', bad.stdout, '');
/* nondescript exit code */
expect('false.exitcode', executeCommand('false').exitcode, 1);
/* binary output is detected and withheld */
const bin = executeCommand('sh', '-c', shellQuote('printf "\\001\\002\\003"'));
expect('bin.binary', bin.binary, true);
expect('bin.stdout', bin.stdout, null);
/* output larger than the old one-shot 512 KiB read must not be truncated
(subscription lists above that size used to arrive cut in half) */
const big_want = 700 * 1024;
const big = executeCommand('sh', '-c', shellQuote('yes a | head -c ' + big_want));
expect('big.exitcode', big.exitcode, 0);
expect('big.length', length(big.stdout), big_want);
/* isValidCIDR(): the prefix needs the same strict validation as the address,
otherwise a poisoned resource file reaches the nftables template */
expect('cidr.ok', isValidCIDR('1.2.3.4/24', 4), true);
expect('cidr.bare', isValidCIDR('1.2.3.4', 4), true);
expect('cidr.bad-prefix', isValidCIDR('1.2.3.4/abc', 4), false);
@@ -73,7 +61,6 @@ expect('cidr.injection', isValidCIDR('1.2.3.4/0 } flush ruleset; table inet x {'
expect('cidr.v6-ok', isValidCIDR('2001:db8::/32', 6), true);
expect('cidr.v6-injection', isValidCIDR('2001:db8::/32 } flush ruleset;', 6), false);
/* isValidPEM(): certificate vs private key, boundaries and body */
const pem_cert = '-----BEGIN CERTIFICATE-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END CERTIFICATE-----';
const pem_key = '-----BEGIN RSA PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END RSA PRIVATE KEY-----';
expect('pem.cert', isValidPEM(pem_cert, false), true);
@@ -83,7 +70,6 @@ expect('pem.key-as-cert', isValidPEM(pem_key, false), false);
expect('pem.garbage', isValidPEM('not a pem at all', false), false);
expect('pem.empty', isValidPEM('', false), false);
/* descriptors must not leak across calls */
const before = fd_count();
for (let i = 0; i < 200; i++)
executeCommand('true');
@@ -3,11 +3,6 @@
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2025 ImmortalWrt.org
*
* Exceptional-path test for executeCommand(): the copy of homeproxy.uc staged
* next to this file has its `system()` call replaced by `die()`, so every call
* throws before the descriptors are read. ucode has no `finally`, so this
* checks that the catch block still closes both descriptors.
*/
'use strict';
@@ -3,10 +3,6 @@
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2025 ImmortalWrt.org
*
* Unit tests for the share-link parsers in parse_uri.uc. Run through
* tests/ucode/run.sh, which stages parse_uri.uc next to the mocked
* homeproxy.uc.
*/
'use strict';
@@ -44,13 +40,11 @@ function expect_null(name, cfg) {
}
}
/* anytls */
expect_fields('anytls', parse_uri('anytls://secret@a.example.com:443?sni=a.example.com&insecure=1#AnyTLS-Node', FEATURES, LOG), {
label: 'AnyTLS-Node', type: 'anytls', address: 'a.example.com', port: '443',
password: 'secret', tls: '1', tls_sni: 'a.example.com', tls_insecure: '1'
});
/* http / https */
expect_fields('https', parse_uri('https://user:pass@b.example.com:8443#HTTPS-Node', FEATURES, LOG), {
label: 'HTTPS-Node', type: 'http', address: 'b.example.com', port: '8443',
username: 'user', password: 'pass', tls: '1'
@@ -59,7 +53,6 @@ expect_fields('http', parse_uri('http://c.example.com:8080#HTTP-Node', FEATURES,
label: 'HTTP-Node', type: 'http', address: 'c.example.com', port: '8080', tls: '0'
});
/* hysteria */
expect_fields('hysteria', parse_uri('hysteria://d.example.com:36712?auth=pass&peer=d.example.com&insecure=1&upmbps=100&downmbps=200&alpn=h3&protocol=udp#Hy1', FEATURES, LOG), {
label: 'Hy1', type: 'hysteria', address: 'd.example.com', port: '36712',
hysteria_protocol: 'udp', hysteria_auth_type: 'string', hysteria_auth_payload: 'pass',
@@ -67,7 +60,6 @@ expect_fields('hysteria', parse_uri('hysteria://d.example.com:36712?auth=pass&pe
tls: '1', tls_insecure: '1', tls_sni: 'd.example.com', tls_alpn: 'h3'
});
/* hysteria2 */
expect_fields('hysteria2', parse_uri('hysteria2://pass@e.example.com:443?sni=e.example.com&obfs=salamander&obfs-password=op#Hy2', FEATURES, LOG), {
label: 'Hy2', type: 'hysteria2', address: 'e.example.com', port: '443',
password: 'pass', hysteria_obfs_type: 'salamander', hysteria_obfs_password: 'op',
@@ -77,14 +69,12 @@ expect_fields('hy2-alias', parse_uri('hy2://pass@e.example.com:443#Hy2Alias', FE
label: 'Hy2Alias', type: 'hysteria2', address: 'e.example.com', port: '443'
});
/* snell */
expect_fields('snell', parse_uri('snell://f.example.com:443?psk=pskvalue&version=4&obfs=http&obfs-host=bing.com&reuse=1#Snell', FEATURES, LOG), {
label: 'Snell', type: 'snell', address: 'f.example.com', port: '443',
password: 'pskvalue', snell_version: '4', snell_obfs_mode: 'http',
snell_obfs_host: 'bing.com', snell_reuse: '1'
});
/* socks */
expect_fields('socks5', parse_uri('socks5://user:pass@g.example.com:1080#Socks', FEATURES, LOG), {
label: 'Socks', type: 'socks', address: 'g.example.com', port: '1080',
username: 'user', password: 'pass', socks_version: '5'
@@ -93,25 +83,21 @@ expect_fields('socks4', parse_uri('socks4://h.example.com:1080#Socks4', FEATURES
socks_version: '4'
});
/* shadowsocks: SIP002 with base64 user info */
expect_fields('ss-sip002-b64', parse_uri('ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@i.example.com:8388#SS-Node', FEATURES, LOG), {
label: 'SS-Node', type: 'shadowsocks', address: 'i.example.com', port: '8388',
shadowsocks_encrypt_method: 'aes-256-gcm', password: 'password'
});
/* shadowsocks: SIP002 with plugin */
expect_fields('ss-sip002-plugin', parse_uri('ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@j.example.com:8388?plugin=simple-obfs%3Bobfs%3Dhttp#SS-Plugin', FEATURES, LOG), {
label: 'SS-Plugin', type: 'shadowsocks', address: 'j.example.com', port: '8388',
shadowsocks_plugin: 'obfs-local', shadowsocks_plugin_opts: 'obfs=http'
});
/* shadowsocks: Shadowrocket whole-payload base64 */
expect_fields('ss-shadowrocket', parse_uri('ss://YWVzLTI1Ni1nY206cGFzc3dvcmRAay5leGFtcGxlLmNvbTo4Mzg4#SS-SR', FEATURES, LOG), {
label: 'SS-SR', type: 'shadowsocks', address: 'k.example.com', port: '8388',
shadowsocks_encrypt_method: 'aes-256-gcm', password: 'password'
});
/* trojan */
expect_fields('trojan-ws', parse_uri('trojan://pass@l.example.com:443?type=ws&host=l.example.com&path=%2Fpath%3Fed%3D2048&sni=l.example.com#TrojanWs', FEATURES, LOG), {
label: 'TrojanWs', type: 'trojan', address: 'l.example.com', port: '443', password: 'pass',
transport: 'ws', ws_host: 'l.example.com', ws_path: '/path',
@@ -122,14 +108,12 @@ expect_fields('trojan-grpc', parse_uri('trojan://pass@l.example.com:443?type=grp
transport: 'grpc', grpc_servicename: 'gs'
});
/* tuic */
expect_fields('tuic', parse_uri('tuic://tuic-uuid:pass@m.example.com:443?congestion_control=bbr&udp_relay_mode=native&alpn=h3&sni=m.example.com#Tuic', FEATURES, LOG), {
label: 'Tuic', type: 'tuic', address: 'm.example.com', port: '443',
uuid: 'tuic-uuid', password: 'pass', tuic_congestion_control: 'bbr',
tuic_udp_relay_mode: 'native', tls: '1', tls_sni: 'm.example.com', tls_alpn: [ 'h3' ]
});
/* vless */
expect_fields('vless-reality-grpc', parse_uri('vless://vless-uuid@n.example.com:443?security=reality&pbk=PUBKEY&sid=abcd&fp=chrome&type=grpc&serviceName=gs&sni=n.example.com&flow=xtls-rprx-vision#Vless', FEATURES, LOG), {
label: 'Vless', type: 'vless', address: 'n.example.com', port: '443', uuid: 'vless-uuid',
transport: 'grpc', grpc_servicename: 'gs', tls: '1', tls_sni: 'n.example.com',
@@ -142,7 +126,6 @@ expect_fields('vless-ws-early-data', parse_uri('vless://vless-uuid@o.example.com
tls: '1', tls_alpn: [ 'h2', 'http/1.1' ]
});
/* vmess */
expect_fields('vmess-ws', parse_uri('vmess://eyJ2IjoiMiIsInBzIjoiVk1lc3MiLCJhZGQiOiJwLmV4YW1wbGUuY29tIiwicG9ydCI6IjQ0MyIsImlkIjoiM2FmODg1NjEtOWM2OS00YjE5LThmN2UtZjA4ZDU4MGJjMzM5IiwiYWlkIjoiMCIsIm5ldCI6IndzIiwidHlwZSI6Im5vbmUiLCJob3N0IjoicC5leGFtcGxlLmNvbSIsInBhdGgiOiIvd3MiLCJ0bHMiOiJ0bHMiLCJzbmkiOiJwLmV4YW1wbGUuY29tIn0=', FEATURES, LOG), {
label: 'VMess', type: 'vmess', address: 'p.example.com', port: '443',
uuid: '3af88561-9c69-4b19-8f7e-f08d580bc339', vmess_alterid: '0', vmess_encrypt: 'auto',
@@ -150,7 +133,6 @@ expect_fields('vmess-ws', parse_uri('vmess://eyJ2IjoiMiIsInBzIjoiVk1lc3MiLCJhZGQ
tls: '1', tls_sni: 'p.example.com'
});
/* sip008 object */
expect_fields('sip008', parse_uri({
nodetype: 'sip008', remarks: 'SIP008', server: 'q.example.com', server_port: 8388,
method: 'aes-256-gcm', password: 'pw'
@@ -159,30 +141,23 @@ expect_fields('sip008', parse_uri({
shadowsocks_encrypt_method: 'aes-256-gcm', password: 'pw'
});
/* rejection paths */
expect_null('vless-kcp-unsupported', parse_uri('vless://u@r.example.com:443?type=kcp#Kcp', FEATURES, LOG));
expect_null('hysteria-without-quic', parse_uri('hysteria://s.example.com:443?auth=a#NoQuic', NO_QUIC, LOG));
expect_null('tuic-without-quic', parse_uri('tuic://u:p@s.example.com:443#NoQuic', NO_QUIC, LOG));
expect_null('invalid-port', parse_uri('vless://u@t.example.com:99999?security=tls#BadPort', FEATURES, LOG));
expect_null('unknown-scheme', parse_uri('unknown://u@t.example.com:443', FEATURES, LOG));
/* sing-box has no xhttp transport; importing it used to make the whole
generated config fail with "unknown transport type" */
expect_null('vless-xhttp-unsupported', parse_uri('vless://u@t.example.com:443?security=tls&type=xhttp#Xhttp', FEATURES, LOG));
/* percent-encoded base64 userinfo (%3D instead of =): used to lose method and
password, which made the node break the generated config */
expect_fields('ss-percent-encoded-userinfo', parse_uri('ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ%3D@j.example.com:8388#SSEnc', FEATURES, LOG), {
label: 'SSEnc', type: 'shadowsocks', address: 'j.example.com', port: '8388',
shadowsocks_encrypt_method: 'aes-256-gcm', password: 'password'
});
/* unsupported flow must import as "no flow" instead of breaking sing-box */
expect_fields('vless-flow-udp443-dropped', parse_uri('vless://vless-uuid@n.example.com:443?security=tls&flow=xtls-rprx-vision-udp443&sni=n.example.com#FlowDrop', FEATURES, LOG), {
label: 'FlowDrop', type: 'vless', address: 'n.example.com', port: '443',
uuid: 'vless-uuid', tls: '1', tls_sni: 'n.example.com', vless_flow: null
});
/* punctuation in a password survives now that userinfo is not whitelisted */
expect_fields('trojan-punctuation-password', parse_uri('trojan://pa!ss~1@m.example.com:443?sni=m.example.com#Punct', FEATURES, LOG), {
label: 'Punct', type: 'trojan', address: 'm.example.com', port: '443',
password: 'pa!ss~1', tls: '1', tls_sni: 'm.example.com'
@@ -3,17 +3,6 @@
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2026 ImmortalWrt.org
*
* Regression tests for the wget flavour probe and for the command line
* wGETVerbose() builds from it.
*
* OpenWrt/ImmortalWrt's default /usr/bin/wget is uclient-fetch, which rejects
* GNU-only flags such as -nv with "unrecognized option: n" *before* it opens a
* connection, so a single GNU-ism turns every subscription update into
* "no valid node found". run.sh stages a copy of homeproxy.uc whose
* /usr/bin/wget points at a stub; HP_WGET_STUB_VERSION steers the answer of
* the `--version` probe (empty output plus a non-zero exit mimics
* uclient-fetch, whose getopt rejects the option and returns 1).
*/
'use strict';
@@ -39,7 +28,6 @@ function has(list, value) {
return false;
}
/* The value following the first occurrence of `key` */
function after(list, key) {
let prev = null;
@@ -53,7 +41,6 @@ function after(list, key) {
return null;
}
/* Classification of the `wget --version` probe. */
expect('gnu.version', classifyWgetFlavor(0, 'GNU Wget 1.25.0 built on linux-gnu.\n'), 'gnu');
expect('gnu.multiline', classifyWgetFlavor(0, "GNU Wget 1.21.4 built on linux-gnu.\n\nCopyright (C) 2015 Free Software Foundation.\n"), 'gnu');
expect('uclient.rejects-version', classifyWgetFlavor(1, null), 'compat');
@@ -61,7 +48,6 @@ expect('busybox.version', classifyWgetFlavor(0, 'BusyBox v1.36.1 (2024-01-01) mu
expect('empty.stdout', classifyWgetFlavor(0, ''), 'compat');
expect('missing.binary', classifyWgetFlavor(127, null), 'compat');
/* wgetFlavor() probes the stub and caches the answer. */
const want_flavor = getenv('HP_WGET_EXPECT_FLAVOR');
expect('flavor', wgetFlavor(), want_flavor);
expect('flavor.cached', wgetFlavor(), want_flavor);
@@ -79,8 +65,6 @@ expect('timeout flag', after(args, '-T'), '10');
expect('output flag', has(args, '-O-'), true);
expect('url', has(args, 'https://example.invalid/subs?token=abc'), true);
/* -nv is GNU wget's --no-verbose; every other wget on the target rejects it,
* and -q is the meter switch they all share. */
expect('meter flag', has(args, want_flavor === 'gnu' ? '-nv' : '-q'), true);
expect('no gnu-only flag', has(args, '-nv'), want_flavor === 'gnu');
expect('no long options', has(args, '--user-agent') || has(args, '--timeout=10'), false);
@@ -3,17 +3,6 @@
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2026 ImmortalWrt.org
*
* Feeds the command line wGETVerbose() builds to the target's real
* uclient-fetch - the default /usr/bin/wget on OpenWrt/ImmortalWrt. run.sh
* rewrites the module's wget path to /bin/uclient-fetch and skips this file
* where that binary does not exist.
*
* A closed loopback port is the entire request: what matters is that
* uclient-fetch gets past getopt. Before the flavour probe existed it did not -
* `-nv` made it print "unrecognized option: n" and exit before the request was
* sent, so every subscription fetch came back empty. Any failure observed here
* must therefore come from the network, never from the option parser.
*/
'use strict';
@@ -30,7 +19,6 @@ function expect(name, actual, want) {
}
}
/* uclient-fetch has no --version and rejects it, so it is classified compat. */
expect('flavor', wgetFlavor(), 'compat');
const res = wGETVerbose('http://127.0.0.1:1/subs', 'HomeProxy test UA');