Files
op-packages/luci-app-homeproxy-pro/tests/ucode/test_protocol_inventory.sh
T

226 lines
9.1 KiB
Bash
Executable File

#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
#
# Protocol inventory invariant.
#
# The protocol surface is spread over several places that must agree. PR-02
# moved two of them, so the paths here are the current ones:
#
# parser/uri.uc scheme -> type (what a share link becomes)
# model.uc CREDENTIALS (which UCI option holds each credential)
# parser/mapping.uc PROTOCOL_TO_UCI (which UCI option holds each protocol option;
# the Loader imports it as PROTOCOL_OPTIONS)
# adapter.uc REQUIRED_CREDENTIALS (what a buildable node needs)
# adapter.uc OPTION_FIELDS (what becomes a sing-box field)
# snapshots/generator/outbounds.json (what is actually emitted)
#
# The frontend has a matching check of its own,
# tests/frontend-protocol-inventory.js: the browser's protocol list must be a
# subset of what the backend models, which is the direction this shape test
# cannot see.
#
# Adding a protocol to one of them and forgetting another is the failure mode
# this test exists for: WireGuard shipped with no loader row (so the endpoint
# builder read fields that did not exist) and SSH shipped with no loader row
# and the wrong credential option name. Neither failed any test.
#
# Usage: sh tests/ucode/test_protocol_inventory.sh <repo-root> [work-dir]
ROOT="${1:-.}"
WORK="${2:-/tmp/hp-inventory-test}"
ROOT="$(cd "$ROOT" && pwd)"
SCRIPTS="$ROOT/root/etc/homeproxy-pro/scripts"
SNAPSHOT="$ROOT/tests/snapshots/generator/outbounds.json"
FAILED=0
rm -rf "$WORK"
mkdir -p "$WORK/parser" "$WORK/inventory/config"
# --- stage 1: what does the parser produce? ------------------------------
# PR-02 moved the parsers to scripts/parser/{uri,protocols,validator,
# normalize,mapping}.uc; stage the whole tree plus the homeproxy-pro mock
# so the share-link importer can find every import it needs.
cp "$ROOT/tests/ucode/mocks/homeproxy-pro.uc" "$WORK/parser/homeproxy-pro.uc"
# The test module imports 'parser/uri.uc', which resolves relative to
# the -L search root, so the parser tree must be staged as a `parser/`
# subdirectory of that root (not flattened into it).
mkdir -p "$WORK/parser/parser"
cp "$SCRIPTS/parser/"*.uc "$WORK/parser/parser/"
# The Loader imports PROTOCOL_OPTIONS from '../parser/mapping.uc'.
# After staging, the loader lives at $WORK/inventory/config/loader.uc,
# so the parser tree must also live at $WORK/inventory/parser/ for the
# import to resolve. Mirror it once now so inventory.uc below can use
# the loader as-is.
mkdir -p "$WORK/inventory/parser"
cp "$SCRIPTS/parser/"*.uc "$WORK/inventory/parser/"
cat > "$WORK/parser/types.uc" <<'EOF'
'use strict';
import { parse_uri } from 'parser/uri.uc';
const FEATURES = { with_quic: true, with_utls: true };
const LOG = function() {};
/* One sample per scheme the parser accepts. The values are the same ones
* test_parse_uri.uc asserts on, so this list stays honest. */
const SAMPLES = [
'anytls://secret@a.example.com:443?sni=a.example.com#AnyTLS',
'https://user:pass@b.example.com:8443#HTTPS',
'http://c.example.com:8080#HTTP',
'hysteria://d.example.com:36712?auth=pass&peer=d.example.com&protocol=udp#Hy1',
'hysteria2://pass@e.example.com:443?sni=e.example.com&obfs=salamander&obfs-password=op#Hy2',
'hy2://pass@e.example.com:443#Hy2Alias',
'snell://f.example.com:443?psk=pskvalue&version=4&obfs=http&obfs-host=bing.com#Snell',
'socks5://user:pass@g.example.com:1080#Socks',
'ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@i.example.com:8388#SS',
'trojan://pass@l.example.com:443?type=grpc&serviceName=gs&sni=l.example.com#Trojan',
'tuic://tuic-uuid:pass@m.example.com:443?congestion_control=bbr&sni=m.example.com#Tuic',
'vless://vless-uuid@n.example.com:443?security=reality&pbk=PUBKEY&sid=abcd&type=grpc&serviceName=gs&sni=n.example.com#Vless',
'vmess://eyJ2IjoiMiIsInBzIjoiVk1lc3MiLCJhZGQiOiJwLmV4YW1wbGUuY29tIiwicG9ydCI6IjQ0MyIsImlkIjoiM2FmODg1NjEtOWM2OS00YjE5LThmN2UtZjA4ZDU4MGJjMzM5IiwiYWlkIjoiMCIsIm5ldCI6IndzIiwidHlwZSI6Im5vbmUiLCJob3N0IjoicC5leGFtcGxlLmNvbSIsInBhdGgiOiIvd3MiLCJ0bHMiOiJ0bHMiLCJzbmkiOiJwLmV4YW1wbGUuY29tIn0='
];
let types = [];
for (let uri in SAMPLES) {
const config = parse_uri(uri, FEATURES, LOG);
if (config)
push(types, config.type);
}
printf('%.J\n', types);
EOF
if ! ( cd "$WORK/parser" && ucode -L "$WORK/parser" types.uc > "$WORK/parser/types.json" 2> "$WORK/parser/types.err" ); then
echo "FAIL: could not run the share-link parsers"
head -8 "$WORK/parser/types.err"
exit 1
fi
# --- stage 2: do the tables and the golden snapshot agree? ---------------
VALIDATE_DATA="${HP_VALIDATE_DATA:-/sbin/validate_data}"
sed -e "s#/sbin/validate_data#${VALIDATE_DATA}#" \
"$SCRIPTS/homeproxy-pro.uc" > "$WORK/inventory/homeproxy-pro.uc"
cp "$SCRIPTS/config/loader.uc" "$WORK/inventory/config/"
cp "$SCRIPTS/config/model.uc" "$WORK/inventory/config/"
cp "$SCRIPTS/config/adapter.uc" "$WORK/inventory/config/"
cp "$WORK/parser/types.json" "$WORK/inventory/parser-types.json"
cp "$SNAPSHOT" "$WORK/inventory/outbounds.json"
cat > "$WORK/inventory/inventory.uc" <<'EOF'
'use strict';
import { readfile } from 'fs';
import {
CREDENTIALS, INBOUND_CREDENTIALS, INBOUND_OPTIONS
} from './config/model.uc';
/* PR-02 moved the client option table to parser/mapping.uc; the Loader
* imports it from there as PROTOCOL_OPTIONS. */
import { PROTOCOL_TO_UCI as PROTOCOL_OPTIONS } from './parser/mapping.uc';
import { OPTION_FIELDS, REQUIRED_CREDENTIALS } from './config/adapter.uc';
const parser_types = json(readfile('parser-types.json')) || [];
const golden = json(readfile('outbounds.json')) || {};
/* Types the adapter turns into a sing-box *outbound*. */
const emitted_types = [];
for (let id in keys(golden))
push(emitted_types, golden[id].type);
/* Protocols that are modelled but deliberately produce something other than
* an outbound: WireGuard becomes an endpoint (generate_endpoint()), and is
* covered by tests/fixtures/generators/wireguard.uci instead of the golden
* snapshot. */
const ENDPOINT_TYPES = ['wireguard'];
let failures = 0, checks = 0;
function check(what, ok, detail) {
checks++;
if (ok)
return;
printf('FAIL %s%s\n', what, detail ? ': ' + detail : '');
failures++;
}
function contains(list, value) {
return index(list, value) !== -1;
}
/* 1. Every protocol any layer names must exist in the loader's option table;
* that table is what makes the protocol reachable from UCI at all. */
const LAYERS = {
'OPTION_FIELDS': keys(OPTION_FIELDS),
'REQUIRED_CREDENTIALS': keys(REQUIRED_CREDENTIALS),
'CREDENTIALS': keys(CREDENTIALS),
'ENDPOINT_TYPES': ENDPOINT_TYPES
};
for (let layer in keys(LAYERS)) {
for (let name in LAYERS[layer]) {
check(sprintf('%s.%s is modelled by loader PROTOCOL_OPTIONS', layer, name),
name in PROTOCOL_OPTIONS);
}
}
/* 2. Every type the parser can produce must be fully modelled: the loader
* must know its options and the adapter must be able to emit it. */
/* Non-empty guard: with an empty parser-types.json every check below would
* vanish and the section would "pass" by having nothing to check - which is
* how a parser that stopped producing types could go unnoticed. */
check('the parser produced at least one type to cross-check', length(parser_types) > 0);
for (let type_name in parser_types) {
check(sprintf("parser type '%s' is in PROTOCOL_OPTIONS", type_name),
type_name in PROTOCOL_OPTIONS);
check(sprintf("parser type '%s' is in OPTION_FIELDS", type_name),
type_name in OPTION_FIELDS);
check(sprintf("parser type '%s' has a golden outbound", type_name),
contains(emitted_types, type_name));
}
/* 3. Every protocol the adapter can emit, and every protocol it requires
* credentials for, must be pinned by the golden snapshot - otherwise a
* field regression in that protocol is invisible. */
for (let type_name in keys(OPTION_FIELDS)) {
check(sprintf("OPTION_FIELDS type '%s' has a golden outbound", type_name),
contains(emitted_types, type_name));
}
for (let type_name in keys(REQUIRED_CREDENTIALS)) {
if (contains(ENDPOINT_TYPES, type_name))
continue;
check(sprintf("REQUIRED_CREDENTIALS type '%s' has a golden outbound", type_name),
contains(emitted_types, type_name));
}
/* 4. The server side of the surface must be modelled too. Every protocol
* with a per-protocol option row must also have a credential row, or
* the Loader reads options for a protocol whose credentials it never
* loads. (The reverse does not hold: trojan / shadowtls / http /
* mixed / naive / socks have credentials and no per-protocol
* options.) */
for (let name in keys(INBOUND_OPTIONS)) {
check(sprintf("INBOUND_OPTIONS.%s has an INBOUND_CREDENTIALS row", name),
name in INBOUND_CREDENTIALS);
}
/* 5. The golden snapshot must not carry a protocol nobody models. */
for (let type_name in emitted_types) {
check(sprintf("golden type '%s' is in PROTOCOL_OPTIONS", type_name),
type_name in PROTOCOL_OPTIONS);
}
printf('%d checks, %d failures\n', checks, failures);
exit(failures ? 1 : 0);
EOF
if ( cd "$WORK/inventory" && ucode -L "$WORK/inventory" inventory.uc ); then
echo "PASS: protocol inventory is consistent"
else
echo "FAIL: protocol inventory is inconsistent"
FAILED=1
fi
exit $FAILED