#!/bin/sh # SPDX-License-Identifier: GPL-2.0-only # # update_resources.sh: the content-verification path. # # The download URL is pinned to a commit, and the script now refuses to install # a file whose git blob id does not match the one GitHub reports for that path # and commit. The whole point of the check is that it *refuses*, so a driver # that only walks the happy path would keep passing with the check deleted - # this one drives the four outcomes and asserts what each leaves behind: # # 1. digest matches -> installed, .ver advanced # 2. digest differs -> NOT installed, previous file and .ver intact # 3. no digest from the API-> NOT installed (fail closed) # 4. no local digest -> NOT installed (fail closed) # plus: already up to date -> no download at all # # Everything external is stubbed (uclient-fetch, jsonfilter, ucode, uci, flock) and the # script's absolute paths are rewritten into a sandbox, so this is pure shell # and runs on a laptop, in CI and on a target. The ucode stub answers with a # fixed digest: what the *shipped* helper computes is # tests/ucode/test_resource_blob_sha.sh's job, which checks it against git. # # Usage: sh tests/runtime/test_resource_update.sh [work-dir] set -u ROOT="$(cd "${1:-.}" && pwd)" WORK="${2:-/tmp/hp-resource-update-test}" rm -rf "$WORK" mkdir -p "$WORK/scripts" "$WORK/resources" "$WORK/run" "$WORK/bin" FAILED=0 CHECKS=0 FAILURES=0 expect() { # expect CHECKS=$((CHECKS + 1)) if [ "$2" = "$3" ]; then echo "PASS: $1" else echo "FAIL: $1 (expected '$3', got '$2')" FAILED=1 FAILURES=$((FAILURES + 1)) fi } # --- stage the script and its helper ---------------------------------------- cp "$ROOT/root/etc/homeproxy-pro/scripts/update_resources.sh" "$WORK/scripts/" cp "$ROOT/root/etc/homeproxy-pro/scripts/resource_blob_sha.uc" "$WORK/scripts/" # Rewrite the two absolute runtime paths. The anchors are asserted below: a sed # that quietly stopped matching would leave the test writing to /etc. sed -e "s|^RESOURCES_DIR=\"/etc/\$NAME/resources\"|RESOURCES_DIR=\"$WORK/resources\"|" \ -e "s|^RUN_DIR=\"/var/run/\$NAME\"|RUN_DIR=\"$WORK/run\"|" \ "$WORK/scripts/update_resources.sh" > "$WORK/scripts/update_resources.sh.new" mv -f "$WORK/scripts/update_resources.sh.new" "$WORK/scripts/update_resources.sh" for anchor in "RESOURCES_DIR=\"$WORK/resources\"" "RUN_DIR=\"$WORK/run\""; do grep -qF "$anchor" "$WORK/scripts/update_resources.sh" || { echo "FAIL: could not sandbox update_resources.sh - missing anchor: $anchor" exit 1 } done # --- stubs ------------------------------------------------------------------ # uclient-fetch serves three shapes, chosen by the URL: the commit list, the # contents metadata, and the file itself. What the contents API reports and what # the file contains are control files, so each case can make them agree or not. # # It replaced a wget stub, and the option handling is the point rather than an # afterthought: this is the only thing that would have noticed the real # regression, where the script passed GNU-only flags (--timeout= --spider) # that a busybox-wget target rejects outright. A permissive stub ("ignore what # you do not recognise") cannot catch that, so unknown options are a hard # failure here - the same way the applet behaves. cat > "$WORK/bin/uclient-fetch" <<'EOF' #!/bin/sh url="" out="" probe=0 while [ $# -gt 0 ]; do case "$1" in -O) out="$2"; shift 2 ;; -O*) out="${1#-O}"; shift ;; --user-agent=*) shift ;; --header=*) shift ;; --timeout=*) shift ;; -T) shift 2 ;; -s|--spider) probe=1; shift ;; -q|--quiet|-4|-6) shift ;; -*) printf 'uclient-fetch: unrecognized option: %s\n' "${1#-}" >&2 printf 'Usage: uclient-fetch [options] \n' >&2 exit 1 ;; *) url="$1"; shift ;; esac done # "-" means stdout, which is the applet's own convention - getting that wrong # writes the body to a file named "-" and leaves the parser reading nothing. emit() { if [ -n "$out" ] && [ "$out" != "-" ]; then printf '%s' "$1" > "$out" else printf '%s' "$1" fi } case "$url" in *api.github.com/*/commits*) # Filtered by path, the way the real endpoint is. Asking for a file # that is not at that path is not a narrower question, it is a different # one: it matches no commit and comes back []. That is the whole reason # the in-repo path is a separate argument from the file name, and it is # what this branch can now catch - every list this project ships used to # live in the repo root, so path= and the file name were the same string # and nothing could tell them apart. case "$url" in *repos/MetaCubeX/*) case "$url" in *"path=geo/geoip/cn.list"*) emit "$(cat "$HP_T_API_COMMITS")" ;; *) emit '[]' ;; esac ;; *) emit "$(cat "$HP_T_API_COMMITS")" ;; esac ;; *api.github.com/*/contents*) emit "$(cat "$HP_T_API_CONTENTS")" ;; *) # A spider probe (-s) checks reachability only, and its stdout is captured # by pick_mirror into $base - so emitting a body here would splice the body # into the mirror name and every later URL would be nonsense. The real # applet prints nothing for a probe. [ "$probe" = "0" ] || exit 0 if [ -n "${HP_T_FETCH_FAIL:-}" ]; then exit 1; fi # The same wrong-path mistake reaches the download as a 404, and # pick_mirror turns that into "all mirrors unreachable" - a second # symptom of the one bug, reported as an unrelated-looking network # failure. Scoped to this source so the other lists, whose file name is # their in-repo path, keep resolving. case "$url" in *MetaCubeX/meta-rules-dat*) case "$url" in *geo/geoip/cn.list*) ;; *) exit 1 ;; esac ;; esac emit "$(cat "$HP_T_BODY")" ;; esac exit 0 EOF # jsonfilter: answer the two expressions the script actually asks for. A stub # that simply returned "the first sha" also answered the commit-date query with # the commit sha, which silently made the expected .ver string wrong - the real # jsonfilter reads the expression it is handed, so this one does too. cat > "$WORK/bin/jsonfilter" <<'EOF' #!/bin/sh expr="" for a in "$@"; do expr="$a"; done payload="$(cat)" case "$expr" in *commit.committer.date*) printf '%s' "$payload" | sed -n 's/.*"date"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1 ;; *) printf '%s' "$payload" | sed -n 's/.*"sha"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1 ;; esac EOF # ucode: the shipped helper's contract, answered from a control file. An empty # control file means "cannot compute" (the helper exits 1 and prints nothing). # # It has to tell the two callers apart, because the china_list case below is # about the ORDER they are called in: resource_blob_sha.uc prints a digest on # stdout, while the rule-set generators take and write a # file. A stub that answered both the same way could not observe whether the # generator was handed the raw download or the normalised list, which is the # whole point of that case. cat > "$WORK/bin/ucode" <<'EOF' #!/bin/sh # -S