mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-11 04:17:51 +08:00
✨ Sync 2026-10-11 20:48:25
This commit is contained in:
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) => {
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in new issue
Block a user