mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-05 18:44:03 +08:00
🌴 Sync 2026-10-06 02:20:56
This commit is contained in:
1 parent
5c201dd3af
commit
487c4fad50
111 files changed
+4641
-746
No files matched your search
@@ -395,19 +395,65 @@ cp "$ROOT/tests/ucode/test_homeproxy_utils.uc" "$WORK/homeproxy-pro/"
|
||||
# same thing everywhere and needs no network.
|
||||
#
|
||||
# On a target /bin/uclient-fetch really exists, so fetchBinary() returns it and
|
||||
# the same assertions run against the real applet. Both directions are useful:
|
||||
# the stub catches "an option we invented", the target catches "an option the
|
||||
# real applet lacks".
|
||||
# the assertions run against the real applet. Both directions are useful: the
|
||||
# stub catches "an option we invented", the target catches "an option the real
|
||||
# applet lacks".
|
||||
mkdir -p "$WORK/fetchbin"
|
||||
cp "$ROOT/tests/fixtures/uclient-fetch-stub" "$WORK/fetchbin/uclient-fetch"
|
||||
chmod +x "$WORK/fetchbin/uclient-fetch"
|
||||
|
||||
if ( cd "$WORK/homeproxy-pro" && PATH="$WORK/fetchbin:$PATH" ucode test_homeproxy_utils.uc ); then
|
||||
# The binary and HTTP-error shapes are something the fetcher has to PRODUCE, so
|
||||
# on a target the real applet can only produce them by talking to a real server
|
||||
# - the stub's magic URL paths mean nothing to it (they read as an ordinary URL
|
||||
# it fails to connect to, which is why those three assertions used to fail on
|
||||
# every real device). So stand up a throwaway uhttpd over a temp docroot and
|
||||
# hand the test its base URL via HP_T_HTTP_BASE.
|
||||
#
|
||||
# It is a second uhttpd on a high port serving a temp directory, killed right
|
||||
# after the test and by the trap below: the running LuCI instance, /www and
|
||||
# port 80 are never touched. If it cannot be started the test reports those
|
||||
# three as skipped, which is honest; they must never be made to read as a pass.
|
||||
HP_T_HTTP_BASE=""
|
||||
HP_T_HTTP_PID=""
|
||||
hp_http_stop() {
|
||||
if [ -n "$HP_T_HTTP_PID" ]; then
|
||||
kill "$HP_T_HTTP_PID" 2> /dev/null
|
||||
wait "$HP_T_HTTP_PID" 2> /dev/null
|
||||
HP_T_HTTP_PID=""
|
||||
fi
|
||||
}
|
||||
trap hp_http_stop EXIT INT TERM
|
||||
|
||||
if { [ -x /bin/uclient-fetch ] || [ -x /usr/bin/uclient-fetch ]; } &&
|
||||
command -v uhttpd > /dev/null 2>&1; then
|
||||
hp_docroot="$WORK/httproot"
|
||||
rm -rf "$hp_docroot"
|
||||
mkdir -p "$hp_docroot"
|
||||
# A body executeCommand() must classify as binary: an .srs magic number
|
||||
# with NUL bytes in it, the same shape the stub emits.
|
||||
printf 'SRS\002x\332b\000\014\000\000\001\000\001' > "$hp_docroot/hp-binary.srs"
|
||||
|
||||
for hp_port in 18099 18100 18101 18102 18103 18104; do
|
||||
uhttpd -f -p "127.0.0.1:$hp_port" -h "$hp_docroot" -x /cgi-bin -t 5 -T 5 &
|
||||
HP_T_HTTP_PID=$!
|
||||
# Integer sleep on purpose: busybox ash has no fractional wait, and
|
||||
# uhttpd needs a moment to bind before the first fetch can reach it.
|
||||
sleep 1
|
||||
if kill -0 "$HP_T_HTTP_PID" 2> /dev/null; then
|
||||
HP_T_HTTP_BASE="http://127.0.0.1:$hp_port"
|
||||
break
|
||||
fi
|
||||
HP_T_HTTP_PID=""
|
||||
done
|
||||
fi
|
||||
|
||||
if ( cd "$WORK/homeproxy-pro" && PATH="$WORK/fetchbin:$PATH" HP_T_HTTP_BASE="$HP_T_HTTP_BASE" ucode test_homeproxy_utils.uc ); then
|
||||
echo "PASS: executeCommand() regression tests"
|
||||
else
|
||||
echo "FAIL: executeCommand() regression tests"
|
||||
FAILED=1
|
||||
fi
|
||||
hp_http_stop
|
||||
|
||||
echo "== TLS / transport builder tests =="
|
||||
rm -rf "$WORK/tls_transport"
|
||||
|
||||
@@ -190,13 +190,23 @@ for chain in redirect mangle_prerouting mangle_output; do
|
||||
FAILED=1
|
||||
fi
|
||||
done
|
||||
v4_rules="$(grep -c 'ip daddr @homeproxy_node_addr_v4 counter return' "$TEMPLATE_SRC" || true)"
|
||||
if [ "$v4_rules" -eq 3 ]; then
|
||||
echo "PASS: all three chains carry the node-address return rule"
|
||||
else
|
||||
echo "FAIL: expected 3 'ip daddr @homeproxy_node_addr_v4 counter return' rules, found $v4_rules"
|
||||
FAILED=1
|
||||
fi
|
||||
# Asserted per chain, not as a global count. The global count is what this
|
||||
# check used to be ("expected 3"), and it is exactly how the missing rule went
|
||||
# unnoticed: the TUN chain needed one and the assertion counted the three that
|
||||
# already existed. The four chains below are the ones that steer traffic into
|
||||
# the tunnel or the redirect/tproxy port - each must exempt the node's own
|
||||
# address once, so a new chain that starts steering traffic has to be added
|
||||
# here (and the operator reading the diff sees why).
|
||||
for chain in homeproxy_redirect homeproxy_mangle_prerouting homeproxy_mangle_output homeproxy_mangle_tun; do
|
||||
body="$(awk -v c="$chain" 'index($0, "chain " c " {") == 1 { f = 1 } f { print } f && /^}/ { exit }' "$TEMPLATE_SRC")"
|
||||
n="$(printf '%s\n' "$body" | grep -c 'ip daddr @homeproxy_node_addr_v4 counter return' || true)"
|
||||
if [ "$n" -eq 1 ]; then
|
||||
echo "PASS: $chain carries exactly one node-address return rule"
|
||||
else
|
||||
echo "FAIL: $chain carries $n node-address return rules (expected exactly 1)"
|
||||
FAILED=1
|
||||
fi
|
||||
done
|
||||
|
||||
# Probe with ucode, not utpl: `utpl -e` is not an eval flag, it renders the
|
||||
# argument as template text and always succeeds.
|
||||
@@ -221,12 +231,48 @@ if ! ucode -e 'require("fw4");' > "/dev/null" 2>&1; then
|
||||
exit $FAILED
|
||||
fi
|
||||
|
||||
# Where the render reads its UCI from.
|
||||
#
|
||||
# * on a target: the real /etc/config - the render has to see the
|
||||
# configuration that is actually deployed;
|
||||
# * anywhere else (CI): tests/fixtures/firewall/render.uci, so this layer -
|
||||
# the only one that turns the template into nft text - runs in CI instead
|
||||
# of reporting NOT RUN. That is T1's other half: the module it needs is
|
||||
# staged by the toolchain (tests/toolchain/build-ucode-linux.sh) and the
|
||||
# configuration it reads is staged here.
|
||||
#
|
||||
# HP_T_FW4_CONFIG_DIR overrides both, which is how the fixture path itself is
|
||||
# exercised on a device (real fw4, fixture config).
|
||||
FW4_CONFIG_DIR="${HP_T_FW4_CONFIG_DIR:-}"
|
||||
if [ -z "$FW4_CONFIG_DIR" ] && [ ! -f "/etc/config/homeproxy-pro" ]; then
|
||||
FW4_CONFIG_DIR="$STAGE/cfg"
|
||||
mkdir -p "$FW4_CONFIG_DIR"
|
||||
cp "$ROOT/tests/fixtures/firewall/render.uci" "$FW4_CONFIG_DIR/homeproxy-pro"
|
||||
cp "$ROOT/tests/fixtures/firewall/dhcp" "$FW4_CONFIG_DIR/dhcp"
|
||||
fi
|
||||
|
||||
# The template is written for a device: it imports homeproxy-pro.uc through
|
||||
# /etc/homeproxy-pro/scripts/ and reads /etc/homeproxy-pro/resources. Neither exists
|
||||
# off-target, so render a staged copy with those prefixes rewritten to the
|
||||
# checkout - the same trick run.sh uses for the absolute import in
|
||||
# root/usr/share/rpcd/ucode/luci.homeproxy-pro.
|
||||
sed -e "s#'/etc/homeproxy-pro/#'$ROOT/root/etc/homeproxy-pro/#g" "$TEMPLATE_SRC" > "$STAGED"
|
||||
# root/usr/share/rpcd/ucode/luci.homeproxy-pro. The cursor is rewritten too when
|
||||
# the UCI comes from the fixture rather than from /etc/config.
|
||||
if [ -n "$FW4_CONFIG_DIR" ]; then
|
||||
sed -e "s#'/etc/homeproxy-pro/#'$ROOT/root/etc/homeproxy-pro/#g" \
|
||||
-e "s#^const uci = cursor();#const uci = cursor('$FW4_CONFIG_DIR');#" \
|
||||
"$TEMPLATE_SRC" > "$STAGED"
|
||||
else
|
||||
sed -e "s#'/etc/homeproxy-pro/#'$ROOT/root/etc/homeproxy-pro/#g" "$TEMPLATE_SRC" > "$STAGED"
|
||||
fi
|
||||
|
||||
# Hard guard: the cursor rewrite has to have matched, or the render silently
|
||||
# reads a configuration that is not there (and the assertions would be about a
|
||||
# ruleset nobody asked for).
|
||||
if [ -n "$FW4_CONFIG_DIR" ] && ! grep -qF "cursor('$FW4_CONFIG_DIR')" "$STAGED"; then
|
||||
echo "FAIL: firewall_post.ut: could not point the render at $FW4_CONFIG_DIR"
|
||||
echo " (the 'const uci = cursor();' anchor no longer matches)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Hard guard: if the prefix ever changes, the sed silently no-ops and the
|
||||
# render fails for an unrelated reason, which would look like a template
|
||||
@@ -266,4 +312,36 @@ if [ "$FAILED" -eq 0 ]; then
|
||||
echo "PASS: the gfwlist gate follows the dnsmasq nftset capability"
|
||||
fi
|
||||
|
||||
# The wan guard (S4): with `listen_interfaces` empty the intercept layer has to
|
||||
# exclude the wan zone's devices. Two ways this went wrong on the device and
|
||||
# both render *something*, which is why the assertion is not just "a guard is
|
||||
# there":
|
||||
#
|
||||
# - r46 shipped a template whose guard silently disappeared: fw4 had no state
|
||||
# in this process (`zones()` is null outside fw4's own render), the loop
|
||||
# threw, the catch swallowed it, and the rendered chain had no iifname at
|
||||
# all - the exact hole the guard was added to close;
|
||||
# - resolving it from /var/run/fw4.state instead yields the LOGICAL names
|
||||
# ("wan", "wan_6"), so `meta iifname { wan }` renders and can never match.
|
||||
#
|
||||
# Resolving zone devices needs ubus, so off-target runs report SKIP rather than
|
||||
# pretending: the render needs fw4's real zone data for this one.
|
||||
GUARD_IFACES="$(grep -oE 'meta iifname( !=)? \{[^}]*\}' "$OUT" | sort -u | tr '\n' ' ')"
|
||||
case "$GUARD_IFACES" in
|
||||
*'{ wan }'*|*'{ wan,'*|*'wan_6 }'*|*'wan_6,'*)
|
||||
echo "FAIL: the wan guard rendered a logical network name, which never matches:"
|
||||
echo " $GUARD_IFACES"
|
||||
FAILED=1
|
||||
;;
|
||||
esac
|
||||
|
||||
if grep -q 'not the WAN side' "$OUT"; then
|
||||
echo "PASS: the wan guard is rendered (resolved devices: ${GUARD_IFACES:-none})"
|
||||
elif ubus -v list network.interface > "/dev/null" 2>&1; then
|
||||
echo "FAIL: the wan guard is missing although fw4 could resolve zone devices"
|
||||
FAILED=1
|
||||
else
|
||||
echo "SKIP: the wan guard needs ubus to resolve zone devices (not available here)"
|
||||
fi
|
||||
|
||||
exit $FAILED
|
||||
@@ -12,7 +12,7 @@
|
||||
'use strict';
|
||||
|
||||
import { lsdir } from 'fs';
|
||||
import { executeCommand, isValidCIDR, isValidPEM, redactReason, redactUrl, ruleSetFormatFromBytes,
|
||||
import { executeCommand, fetchBinary, isValidCIDR, isValidPEM, redactReason, redactUrl, ruleSetFormatFromBytes,
|
||||
ruleSetFormatFromPath, RULESET_PROBE_BYTES, shellQuote, wGETVerbose } from 'homeproxy-pro';
|
||||
|
||||
let failures = 0,
|
||||
@@ -104,17 +104,66 @@ expect('fetch.not-a-usage-error',
|
||||
expect('fetch.reports-a-reason', length(fetch.error || '') > 0, true);
|
||||
expect('fetch.no-content-on-failure', fetch.content, null);
|
||||
|
||||
/* The binary and HTTP-error shapes below have to be PRODUCED by the fetcher, so
|
||||
* which fetcher is in play decides which URL can produce them.
|
||||
*
|
||||
* Off-target fetchBinary() resolves to the stub, which manufactures both shapes
|
||||
* from a magic path in the URL. On a target it returns the real applet, and a
|
||||
* real applet only produces them by talking to a real server - a magic path
|
||||
* just looks like an ordinary URL it fails to connect to, which is why these
|
||||
* three used to fail there. run.sh starts a throwaway uhttpd for exactly this
|
||||
* and passes its base URL in HP_T_HTTP_BASE.
|
||||
*
|
||||
* A real applet with no server (no uhttpd, every candidate port taken) cannot
|
||||
* be driven at all: report the three as skipped rather than failing, and never
|
||||
* let them read as a pass. */
|
||||
const fetchBin = fetchBinary();
|
||||
const realApplet = substr(fetchBin, 0, 1) === '/';
|
||||
const httpBase = getenv('HP_T_HTTP_BASE') || '';
|
||||
const canDriveFetchShapes = httpBase || !realApplet;
|
||||
if (realApplet && !httpBase)
|
||||
printf('SKIP: binary / HTTP-error fetch shapes (real applet, no local server)\n');
|
||||
|
||||
/* A 200 whose body is binary. executeCommand() nulls stdout for binary
|
||||
* content, and the "no content but stderr said something" branch then used to
|
||||
* report it as `fetch failed: … Download completed (34185 bytes)` - a message
|
||||
* that contradicts itself and sends the user after a network problem they do
|
||||
* not have. Measured on a device against a real .srs. */
|
||||
const binfetch = wGETVerbose('http://127.0.0.1:1/HP_T_STUB_BINARY');
|
||||
expect('fetch.binary-is-not-a-failure',
|
||||
match(binfetch.error || '', /fetch failed/) == null, true, binfetch.error);
|
||||
expect('fetch.binary-says-so',
|
||||
match(binfetch.error || '', /binary/) != null, true, binfetch.error);
|
||||
expect('fetch.binary-has-no-content', binfetch.content, null);
|
||||
if (canDriveFetchShapes) {
|
||||
const binURL = httpBase ? `${httpBase}/hp-binary.srs` : 'http://127.0.0.1:1/HP_T_STUB_BINARY';
|
||||
const binfetch = wGETVerbose(binURL);
|
||||
expect('fetch.binary-is-not-a-failure',
|
||||
match(binfetch.error || '', /fetch failed/) == null, true, binfetch.error);
|
||||
expect('fetch.binary-says-so',
|
||||
match(binfetch.error || '', /binary/) != null, true, binfetch.error);
|
||||
expect('fetch.binary-has-no-content', binfetch.content, null);
|
||||
}
|
||||
|
||||
/* A truncated transfer must not look like a subscription.
|
||||
*
|
||||
* The fetch used to be `uclient-fetch -O - | head -c N`, and system() returns
|
||||
* the last command's status - head's, always 0. A response that declared 244
|
||||
* bytes and delivered 80 therefore arrived as `{ content: <80 bytes>, error:
|
||||
* null }`: a half-downloaded list would rewrite the node set and nothing
|
||||
* anywhere said so. wGETVerbose() now carries the fetcher's own status out of
|
||||
* the pipeline through a file, and this is the shape that proves it.
|
||||
*
|
||||
* The stub is the only thing that can produce "non-empty body AND non-zero
|
||||
* exit" (a real uhttpd sets Content-Length from the file it serves), so on a
|
||||
* real-applet run this is skipped on purpose - the same path was measured by
|
||||
* hand on the device with a truncated response: real uclient-fetch exits 4
|
||||
* with "Connection reset prematurely" and wGETVerbose() reports
|
||||
* `fetch exited with status 4: …`. */
|
||||
if (canDriveFetchShapes) {
|
||||
if (realApplet) {
|
||||
printf('SKIP: truncated-transfer shape (the real applet cannot be asked for a short body; device-verified by hand)\n');
|
||||
} else {
|
||||
const truncfetch = wGETVerbose('http://127.0.0.1:1/HP_T_STUB_TRUNCATED');
|
||||
expect('fetch.truncated-is-not-a-success', truncfetch.content, null);
|
||||
expect('fetch.truncated-reports-the-status',
|
||||
match(truncfetch.error || '', /status 4/) != null, true, truncfetch.error);
|
||||
}
|
||||
}
|
||||
|
||||
/* Review H3: the fetcher announces the requested URL on stderr before it reports
|
||||
* anything else, query string and all, so this is what would reach the log.
|
||||
@@ -136,11 +185,14 @@ expect('fetch.redacted-still-names-the-target',
|
||||
/* The HTTP-error shape is the one that leaks - "Downloading '<URL>'" followed
|
||||
* by "HTTP error 404" - and it is the common case for a subscription URL that
|
||||
* has expired. */
|
||||
const httpfetch = wGETVerbose('http://127.0.0.1:1/HP_T_STUB_HTTP_ERROR?token=secret');
|
||||
expect('fetch.http-error-token-not-in-error',
|
||||
match(httpfetch.error || '', /token=secret/) == null, true);
|
||||
expect('fetch.http-error-keeps-the-status',
|
||||
match(httpfetch.error || '', /HTTP error/) != null, true, httpfetch.error);
|
||||
if (canDriveFetchShapes) {
|
||||
const errURL = httpBase ? `${httpBase}/hp-missing?token=secret` : 'http://127.0.0.1:1/HP_T_STUB_HTTP_ERROR?token=secret';
|
||||
const httpfetch = wGETVerbose(errURL);
|
||||
expect('fetch.http-error-token-not-in-error',
|
||||
match(httpfetch.error || '', /token=secret/) == null, true);
|
||||
expect('fetch.http-error-keeps-the-status',
|
||||
match(httpfetch.error || '', /HTTP error/) != null, true, httpfetch.error);
|
||||
}
|
||||
|
||||
/* redactReason(): central redaction that protects every wGETVerbose caller.
|
||||
* Tested in isolation so the assertion does not depend on any fetcher being
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
* - a subscription node dropped from the cache is deleted
|
||||
* - a new node in the cache but not in UCI is added under the
|
||||
* md5(groupHash + label) section name
|
||||
* - a node whose label changed but whose content did not keeps its
|
||||
* section (and with it every reference to it), and is not re-added
|
||||
* - two stored sections with the same content: only one may claim it
|
||||
*
|
||||
* PR-03 changes the input contract: Repository now accepts
|
||||
* canonical Node objects (post- parser/normalize), and flattens
|
||||
@@ -413,6 +416,149 @@ expect('scrub: clean state changes nothing', scrub2.changed, 0);
|
||||
*
|
||||
* U+4E2D U+6587 -> e4 b8 ad e6 96 87
|
||||
* U+1F600 -> f0 9f 98 80 */
|
||||
/* --- a renamed node keeps its section --------------------------------- */
|
||||
|
||||
/* Section names are md5(grouphash + label), so a subscription that renames a
|
||||
* node used to make the repository delete the old section and add an identical
|
||||
* one under the new name - taking every reference to it (config.main_node,
|
||||
* main_udp_node, the urltest member lists) with it. Measured on the device
|
||||
* before this: "Removing node: OLD-NAME." + "Adding node: NEW-NAME.", the
|
||||
* section name changed, and main_node was switched to the new section. The
|
||||
* repository recognises the stored section by its *content* now: same server,
|
||||
* same credentials, new label. */
|
||||
{
|
||||
const uci3 = cursor(ARGV[0]);
|
||||
uci3.load(CFG);
|
||||
|
||||
const groupR = 'groupRename';
|
||||
const u_renamed = 'cfgRENAMED001';
|
||||
|
||||
uci3.set(CFG, u_renamed, TYPE);
|
||||
uci3.set(CFG, u_renamed, 'label', 'old-name');
|
||||
uci3.set(CFG, u_renamed, 'grouphash', groupR);
|
||||
uci3.set(CFG, u_renamed, 'type', 'vless');
|
||||
uci3.set(CFG, u_renamed, 'address', 'rename.example.com');
|
||||
uci3.set(CFG, u_renamed, 'port', '443');
|
||||
uci3.commit(CFG);
|
||||
uci3.load(CFG);
|
||||
|
||||
const r_node = canonical_node({
|
||||
grouphash: groupR,
|
||||
label: 'new-name',
|
||||
type: 'vless',
|
||||
address: 'rename.example.com',
|
||||
port: '443'
|
||||
});
|
||||
const r_cache = { [groupR]: { [md5(groupR + 'new-name')]: r_node } };
|
||||
|
||||
const r_result = Repository.apply_nodes(uci3, CFG, TYPE, r_cache, [[ r_node ]], LOG);
|
||||
|
||||
expect('rename: not counted as removed', r_result.removed, 0);
|
||||
expect('rename: not counted as added', r_result.added, 0);
|
||||
|
||||
uci3.commit(CFG);
|
||||
uci3.load(CFG);
|
||||
|
||||
expect('rename: section kept under its old name',
|
||||
'label' in uci3.get_all(CFG, u_renamed), true);
|
||||
expect('rename: label updated in place',
|
||||
uci3.get_all(CFG, u_renamed).label, 'new-name');
|
||||
expect('rename: nothing added under the new name',
|
||||
'label' in uci3.get_all(CFG, md5(groupR + 'new-name')), false);
|
||||
}
|
||||
|
||||
/* Two stored sections with identical content must not both update themselves
|
||||
* into the same node: the first one claims the content, the duplicate is
|
||||
* pruned exactly as before. */
|
||||
{
|
||||
const uci4 = cursor(ARGV[0]);
|
||||
uci4.load(CFG);
|
||||
|
||||
const groupD = 'groupDuplicate';
|
||||
const u_dup_a = 'cfgDUPA00001';
|
||||
const u_dup_b = 'cfgDUPB00001';
|
||||
|
||||
for (let section in [ u_dup_a, u_dup_b ]) {
|
||||
uci4.set(CFG, section, TYPE);
|
||||
uci4.set(CFG, section, 'label', section);
|
||||
uci4.set(CFG, section, 'grouphash', groupD);
|
||||
uci4.set(CFG, section, 'type', 'vless');
|
||||
uci4.set(CFG, section, 'address', 'dup.example.com');
|
||||
uci4.set(CFG, section, 'port', '443');
|
||||
}
|
||||
uci4.commit(CFG);
|
||||
uci4.load(CFG);
|
||||
|
||||
const d_node = canonical_node({
|
||||
grouphash: groupD,
|
||||
label: 'renamed-dup',
|
||||
type: 'vless',
|
||||
address: 'dup.example.com',
|
||||
port: '443'
|
||||
});
|
||||
const d_cache = { [groupD]: { [md5(groupD + 'renamed-dup')]: d_node } };
|
||||
|
||||
const d_result = Repository.apply_nodes(uci4, CFG, TYPE, d_cache, [[ d_node ]], LOG);
|
||||
|
||||
expect('duplicate content: one section claims it', d_result.added, 0);
|
||||
expect('duplicate content: the other is pruned', d_result.removed, 1);
|
||||
|
||||
uci4.commit(CFG);
|
||||
uci4.load(CFG);
|
||||
|
||||
const survived = filter([ u_dup_a, u_dup_b ], (s) => ('label' in uci4.get_all(CFG, s)));
|
||||
expect('duplicate content: exactly one section survives', length(survived), 1);
|
||||
expect('duplicate content: it carries the new label',
|
||||
uci4.get_all(CFG, survived[0]).label, 'renamed-dup');
|
||||
}
|
||||
|
||||
/* --- the orchestrator's key is the section name ----------------------- */
|
||||
|
||||
/* Two servers of one subscription can share a label - it is whatever the
|
||||
* provider put after '#'. The orchestrator keeps both and separates them by
|
||||
* the content fingerprint, so the second node sits in the cache under a key
|
||||
* that is NOT md5(grouphash + label). The repository has to write that key as
|
||||
* the section name, or both nodes end up in one section and one is lost. */
|
||||
{
|
||||
const uci5 = cursor(ARGV[0]);
|
||||
uci5.load(CFG);
|
||||
|
||||
const groupS = 'groupSharedLabel';
|
||||
const s_first = canonical_node({
|
||||
grouphash: groupS,
|
||||
label: 'same-label',
|
||||
type: 'vless',
|
||||
address: 'first.example.com',
|
||||
port: '443'
|
||||
});
|
||||
const s_second = canonical_node({
|
||||
grouphash: groupS,
|
||||
label: 'same-label',
|
||||
type: 'vless',
|
||||
address: 'second.example.com',
|
||||
port: '443'
|
||||
});
|
||||
const disambiguated = md5(groupS + 'same-label' + chr(0x00) + 'different-content');
|
||||
const s_cache = {
|
||||
[groupS]: {
|
||||
[md5(groupS + 'same-label')]: s_first,
|
||||
[disambiguated]: s_second
|
||||
}
|
||||
};
|
||||
|
||||
const s_result = Repository.apply_nodes(uci5, CFG, TYPE, s_cache,
|
||||
[[ s_first ], [ s_second ]], LOG);
|
||||
expect('shared label: both nodes are added', s_result.added, 2);
|
||||
|
||||
uci5.commit(CFG);
|
||||
uci5.load(CFG);
|
||||
|
||||
expect('shared label: the first is under md5(grouphash + label)',
|
||||
uci5.get_all(CFG, md5(groupS + 'same-label')).address, 'first.example.com');
|
||||
expect('shared label: the second is under the disambiguated key',
|
||||
uci5.get_all(CFG, disambiguated).address, 'second.example.com');
|
||||
}
|
||||
|
||||
const MD5_CJK = chr(0xe4) + chr(0xb8) + chr(0xad) + chr(0xe6) + chr(0x96) + chr(0x87);
|
||||
const MD5_EMOJI = chr(0xf0) + chr(0x9f) + chr(0x98) + chr(0x80);
|
||||
|
||||
|
||||
Reference in new issue
Block a user