#!/bin/bash set -euo pipefail PACKAGE=${1:?usage: verify-fnos-package.sh package.fpk} VERIFY_TMP_ROOT="${LIVERECORDER_VERIFY_TMPDIR:-${TMPDIR:-/tmp}}" MAX_APP_UNCOMPRESSED_BYTES=$((512 * 1024 * 1024)) mkdir -p "$VERIFY_TMP_ROOT" WORK_DIR=$(mktemp -d "${VERIFY_TMP_ROOT%/}/fnos-package-verify.XXXXXX") trap 'rm -rf -- "$WORK_DIR"' EXIT manifest_value() { sed -n "s/^$1[[:space:]]*=[[:space:]]*//p" "$WORK_DIR/manifest" | head -n 1 | tr -d '\r' } tar -xzf "$PACKAGE" -C "$WORK_DIR" appname=$(manifest_value appname) version=$(manifest_value version) case "$appname" in liverecorder|nxsir.postgresql) ;; *) printf 'unexpected fnOS appname: %s\n' "$appname" >&2; exit 1 ;; esac test -n "$version" test "$(manifest_value platform)" = "x86" test -x "$WORK_DIR/cmd/main" test -x "$WORK_DIR/cmd/install_callback" test -s "$WORK_DIR/wizard/install" test -s "$WORK_DIR/wizard/upgrade" test -s "$WORK_DIR/ICON.PNG" test -s "$WORK_DIR/ICON_256.PNG" expected=$(manifest_value checksum) actual=$(md5sum "$WORK_DIR/app.tgz" | cut -d' ' -f1) test -n "$expected" test "$expected" = "$actual" gzip -t "$WORK_DIR/app.tgz" gzip -dc "$WORK_DIR/app.tgz" >"$WORK_DIR/app.tar" uncompressed_bytes=$(wc -c <"$WORK_DIR/app.tar") if [ "$uncompressed_bytes" -gt "$MAX_APP_UNCOMPRESSED_BYTES" ]; then printf 'app.tgz expands to %s bytes; limit is %s bytes\n' \ "$uncompressed_bytes" "$MAX_APP_UNCOMPRESSED_BYTES" >&2 exit 1 fi tar -tf "$WORK_DIR/app.tar" >"$WORK_DIR/app-files.txt" if awk '/^\// || /(^|\/)\.\.($|\/)/ { unsafe = 1; exit } END { exit unsafe ? 0 : 1 }' "$WORK_DIR/app-files.txt"; then printf 'app.tgz contains an unsafe member path\n' >&2 exit 1 fi tar -tvf "$WORK_DIR/app.tar" >"$WORK_DIR/app-metadata.txt" if awk ' /^l/ { marker = " -> " offset = index($0, marker) if (offset > 0) { target = substr($0, offset + length(marker)) if (target ~ /^\//) { unsafe = 1; exit } } } /^h/ { marker = " link to " offset = index($0, marker) if (offset > 0) { target = substr($0, offset + length(marker)) if (target ~ /^\//) { unsafe = 1; exit } } } END { exit unsafe ? 0 : 1 } ' "$WORK_DIR/app-metadata.txt"; then printf 'app.tgz contains an unsafe symbolic or hard link\n' >&2 exit 1 fi grep -q '^server/wwwroot/index.html$' "$WORK_DIR/app-files.txt" grep -q '^ui/config$' "$WORK_DIR/app-files.txt" grep -q '^ui/images/icon_64.png$' "$WORK_DIR/app-files.txt" if [ "$appname" = "liverecorder" ]; then grep -q '^server/LiveRecorder.WebApi$' "$WORK_DIR/app-files.txt" grep -q '^server/Platforms/Douyin/Signing/sign-xbogus.js$' "$WORK_DIR/app-files.txt" grep -q '^runtime/bin/curl$' "$WORK_DIR/app-files.txt" grep -q '^runtime/etc/ssl/certs/ca-certificates.crt$' "$WORK_DIR/app-files.txt" test "$(manifest_value install_dep_apps)" = "nxsir.postgresql:nodejs_v22" if grep -Eq '^runtime/(bin/node|usr/(lib|share)/postgresql/|lib/(libLLVM|libz3))' "$WORK_DIR/app-files.txt"; then printf 'Live Recorder must use shared PostgreSQL and the fnOS nodejs_v22 dependency\n' >&2 exit 1 fi else grep -q '^server/PostgresService.WebApi$' "$WORK_DIR/app-files.txt" grep -q '^runtime/usr/lib/postgresql/15/bin/postgres$' "$WORK_DIR/app-files.txt" grep -q '^runtime/usr/lib/postgresql/15/bin/initdb$' "$WORK_DIR/app-files.txt" grep -q '^runtime/usr/lib/postgresql/15/bin/pg_ctl$' "$WORK_DIR/app-files.txt" grep -q '^runtime/usr/share/postgresql/15/postgresql.conf.sample$' "$WORK_DIR/app-files.txt" grep -q '^runtime/usr/lib/postgresql/15/lib/vector.so$' "$WORK_DIR/app-files.txt" grep -q '^runtime/usr/share/postgresql/15/extension/vector.control$' "$WORK_DIR/app-files.txt" fi if grep -Eq '^runtime/.*/(ffmpeg|ffprobe)$' "$WORK_DIR/app-files.txt"; then printf 'ffmpeg and ffprobe must come from the fnOS system environment\n' >&2 exit 1 fi if grep -Eq '^runtime/lib/(ld-linux-.*|libc\.so\..*|libBrokenLocale\.so\..*|libanl\.so\..*|libdl\.so\..*|libm(vec)?\.so\..*|libnss_(compat|dns|files|hesiod)\.so\..*|libpthread\.so\..*|libresolv\.so\..*|librt\.so\..*|libthread_db\.so\..*|libutil\.so\..*)$' "$WORK_DIR/app-files.txt"; then printf 'the FPK must use the fnOS glibc and matching system loader\n' >&2 exit 1 fi if grep -Eq '^(data|records|postgres|log|var)/' "$WORK_DIR/app-files.txt"; then printf 'persistent data must not be included in app.tgz\n' >&2 exit 1 fi mkdir -p "$WORK_DIR/app" tar -xf "$WORK_DIR/app.tar" -C "$WORK_DIR/app" while IFS= read -r -d '' link_path; do target=$(readlink -- "$link_path") case "$target" in /*) printf 'absolute symbolic link in app.tgz: %s -> %s\n' "$link_path" "$target" >&2; exit 1 ;; esac resolved=$(realpath -m -- "$(dirname -- "$link_path")/$target") case "$resolved" in "$WORK_DIR/app"/*) ;; *) printf 'escaping symbolic link in app.tgz: %s -> %s\n' "$link_path" "$target" >&2; exit 1 ;; esac done < <(find "$WORK_DIR/app" -type l -print0) printf 'fnOS package verified: %s (%s bytes uncompressed)\n' "$PACKAGE" "$uncompressed_bytes"