diff --git a/tools/build_nocreds.sh b/tools/build_nocreds.sh new file mode 100755 index 0000000..7eb6434 --- /dev/null +++ b/tools/build_nocreds.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Baut Firmwareabzuege OHNE einkompilierte WLAN-Zugangsdaten: die Lampen starten damit +# im Onboarding-AP (ninlight-), statt sich in ein fremdes WLAN einzuwaehlen. +# +# Aufruf: tools/build_nocreds.sh [ziel ...] (ohne Argumente: alle acht Ziele) +# +# Ergebnis: je Ziel ein Build-Verzeichnis unter $NINLIGHT_NOCREDS_BUILD_REL (Vorgabe +# "build-nocreds", relativ zum Repo-Wurzelverzeichnis) mit den ueblichen Artefakten +# (ninlight__gz.img, ninlight__ota.img, ...). Bewusst NICHT build/: die +# dortigen Abzuege sind die des Entwicklers - mit seinem WLAN - und bleiben unangetastet. +# +# Dieses Skript ist ausdruecklich zum Wiederverwenden da: tools/release.sh baut damit das +# Auslieferungsarchiv, und die Migrationsstation wird damit die Wahl "Lampe startet im +# Onboarding-AP" bedienen. Deshalb steht die Logik hier und nicht in release.sh. +# +# Umgebungsvariablen: +# NINLIGHT_DOCKER Build-Vorspann (Vorgabe tools/docker.sh); Tests stubben ihn +# NINLIGHT_NOCREDS_BUILD_REL Ausgabeverzeichnis relativ zur Repo-Wurzel +set -euo pipefail + +ROOT=$(cd "$(dirname "$0")/.." && pwd) +DOCKER=${NINLIGHT_DOCKER:-$ROOT/tools/docker.sh} +BUILD_REL=${NINLIGHT_NOCREDS_BUILD_REL:-build-nocreds} + +DEVICES=(evk e27_rgb e14_rgb gu10_rgb e27_cw e14_cw gu10_cw g95_cw) +if [ $# -gt 0 ]; then + TARGETS=("$@") +else + TARGETS=("${DEVICES[@]}") +fi +for t in "${TARGETS[@]}"; do + ok=0 + for d in "${DEVICES[@]}"; do [ "$t" = "$d" ] && ok=1; done + [ "$ok" -eq 1 ] || { echo "Fehler: unbekanntes Ziel '$t' (bekannt: ${DEVICES[*]})" >&2; exit 1; } +done + +# config.local.cmake wird in CMakeLists.txt mit gewoehnlichem set() eingebunden und +# SCHLAEGT damit jedes -D auf der Kommandozeile. Ein Bau ohne Zugangsdaten ist deshalb +# nur moeglich, wenn die Datei fuer die Dauer des Baus beiseite liegt. In ihr stehen die +# echte SSID, das WLAN-Passwort und das API-Token - sie muss auch dann zurueckkommen, +# wenn der Bau abbricht oder jemand Strg-C drueckt. Darum ein trap auf EXIT UND auf +# INT/TERM (EXIT allein laeuft bei einem Signal zwar auch, aber der Exit-Code soll das +# Signal widerspiegeln, statt als Erfolg durchzugehen). +LOCAL="$ROOT/config.local.cmake" +STASH_DIR="" +restore_local() { + [ -n "$STASH_DIR" ] || return 0 + if [ -f "$STASH_DIR/config.local.cmake" ]; then + cp -p "$STASH_DIR/config.local.cmake" "$LOCAL" || { + echo "ACHTUNG: config.local.cmake konnte nicht zurueckgelegt werden - Kopie liegt unter $STASH_DIR" >&2 + STASH_DIR="" + return 1 + } + fi + rm -rf "$STASH_DIR" + STASH_DIR="" +} +if [ -f "$LOCAL" ]; then + STASH_DIR=$(mktemp -d) + cp -p "$LOCAL" "$STASH_DIR/config.local.cmake" + trap 'restore_local' EXIT + trap 'restore_local; trap - INT; kill -INT $$' INT + trap 'restore_local; exit 143' TERM + rm -f "$LOCAL" + echo "config.local.cmake liegt fuer diesen Bau beiseite (keine WLAN-Zugangsdaten im Abzug)" +fi + +for t in "${TARGETS[@]}"; do + echo "== $t (ohne Zugangsdaten)" + # Relative Pfade, weil der Bau ueblicherweise im Container laeuft, der das Repo unter + # /work einhaengt - absolute Hostpfade gaebe es dort nicht. + "$DOCKER" cmake -S . -B "$BUILD_REL/$t" -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE=cmake/arm-none-eabi.cmake -DNINLIGHT_DEVICE="$t" >/dev/null + "$DOCKER" cmake --build "$BUILD_REL/$t" +done + +echo "fertig - Abzuege ohne Zugangsdaten unter $ROOT/$BUILD_REL" diff --git a/tools/release.sh b/tools/release.sh new file mode 100755 index 0000000..1e958da --- /dev/null +++ b/tools/release.sh @@ -0,0 +1,309 @@ +#!/usr/bin/env bash +# Baut ein Auslieferungsarchiv: alle acht Ziele, OHNE einkompilierte WLAN-Zugangsdaten +# (die Lampen starten also im Onboarding-AP), in allen drei Bildformen - OTA, Migration +# (dev und prod signiert) und serielle Erstbespielung. +# +# Aufruf: tools/release.sh +# Ergebnis: dist/ninlight-.tar.gz +# +# Umgebungsvariablen: +# NINLIGHT_SIGN_DIR Schluesselverzeichnis (Vorgabe ~/workspace/fw-sign) +# NINLIGHT_NOCREDS_BUILD_REL Build-Verzeichnis relativ zur Repo-Wurzel (s. build_nocreds.sh) +# NINLIGHT_DOCKER Build-Vorspann (s. build_nocreds.sh); Tests stubben ihn +set -euo pipefail + +VERSION=${1:?Aufruf: tools/release.sh } +ROOT=$(cd "$(dirname "$0")/.." && pwd) +TARGETS=(evk e27_rgb e14_rgb gu10_rgb e27_cw e14_cw gu10_cw g95_cw) +BUILD_REL=${NINLIGHT_NOCREDS_BUILD_REL:-build-nocreds} +BUILD_DIR="$ROOT/$BUILD_REL" +NAME="ninlight-$VERSION" +TGZ="$ROOT/dist/$NAME.tar.gz" + +# Ein vorhandenes Archiv wird NICHT ueberschrieben: eine schon weitergegebene Version +# still durch einen anderen Inhalt zu ersetzen, ist der Fehler, den man am spaetesten +# bemerkt. +[ -e "$TGZ" ] && { echo "Fehler: $TGZ existiert bereits - Version erhoehen oder Datei entfernen" >&2; exit 1; } + +# sha256: GNU zuerst (Debian/Pi), BSD/macOS als Rueckfall. +if command -v sha256sum >/dev/null 2>&1; then + SHA_CMD="sha256sum" +elif command -v shasum >/dev/null 2>&1; then + SHA_CMD="shasum -a 256" +else + echo "Fehler: weder sha256sum noch shasum gefunden" >&2; exit 1 +fi + +echo "== Firmware ohne Zugangsdaten bauen ==" +# Der Bauweg liegt bewusst in einem eigenen, wiederverwendbaren Skript (die +# Migrationsstation braucht dieselben Abzuege) - inklusive des Beiseitelegens von +# config.local.cmake, das jedes -D schlaegt. +"$ROOT/tools/build_nocreds.sh" "${TARGETS[@]}" + +STAGE=$(mktemp -d) +trap 'rm -rf "$STAGE"' EXIT +OUT="$STAGE/$NAME" +mkdir -p "$OUT/ota" "$OUT/flash" + +echo "== Images einsammeln ==" +for t in "${TARGETS[@]}"; do + ota="$BUILD_DIR/$t/ninlight_${t}_ota.img" + gz="$BUILD_DIR/$t/ninlight_${t}_gz.img" + [ -f "$ota" ] || { echo "Fehler: $ota fehlt" >&2; exit 1; } + [ -f "$gz" ] || { echo "Fehler: $gz fehlt" >&2; exit 1; } + cp "$ota" "$OUT/ota/" + cp "$gz" "$OUT/flash/" +done + +echo "== Migrationsimages signieren ==" +# Wie tools/station/deploy.sh: die Schluessel werden nur gelesen, sie gehoeren nie ins +# Repo, nie auf den Pi und schon gar nicht ins Archiv. Fehlt das Verzeichnis oder ein +# einzelner Schluessel, entfaellt die jeweilige Variante mit einer Meldung - das Archiv +# entsteht trotzdem, und README.md sagt dann, warum migrate/ fehlt. +# ${HOME:-} statt $HOME, damit `set -u` unter env -i/cron nicht unnoetig abbricht. +SIGN_DIR=${NINLIGHT_SIGN_DIR:-${HOME:-}/workspace/fw-sign} +KEY_PROD="$SIGN_DIR/QCX_Device_CA/qcx_firmware_signing_key.pem" +KEY_DEV="$SIGN_DIR/fw_sign.pem" +NL_SIGN="$ROOT/tools/mkimage/nl_sign.py" +MIGRATE_NOTE="" +HAVE_PROD=0 +HAVE_DEV=0 +if [ -d "$SIGN_DIR" ]; then + if [ -f "$KEY_PROD" ]; then HAVE_PROD=1; else + echo "Hinweis: Produktionsschluessel fehlt - keine _prod.img" >&2 + fi + if [ -f "$KEY_DEV" ]; then HAVE_DEV=1; else + echo "Hinweis: Entwicklungsschluessel fehlt - keine _dev.img" >&2 + fi +else + echo "Hinweis: Schluesselverzeichnis $SIGN_DIR nicht gefunden - keine Migrationsimages erzeugt" >&2 +fi +if [ "$HAVE_PROD" -eq 1 ] || [ "$HAVE_DEV" -eq 1 ]; then + mkdir -p "$OUT/migrate" + for t in "${TARGETS[@]}"; do + # Signiert wird IMMER das rohe _gz.img, niemals das fertige _ota.img: beide belegen + # denselben 512-Byte-Block am Dateiende (Signatur bzw. Trailer). + gz="$BUILD_DIR/$t/ninlight_${t}_gz.img" + if [ "$HAVE_PROD" -eq 1 ]; then + python3 "$NL_SIGN" sign --key "$KEY_PROD" --in "$gz" --out "$OUT/migrate/${t}_prod.img" \ + || { echo "Fehler: Signieren von $t (prod) fehlgeschlagen" >&2; exit 1; } + fi + if [ "$HAVE_DEV" -eq 1 ]; then + python3 "$NL_SIGN" sign --key "$KEY_DEV" --in "$gz" --out "$OUT/migrate/${t}_dev.img" \ + || { echo "Fehler: Signieren von $t (dev) fehlgeschlagen" >&2; exit 1; } + fi + done +fi +if [ "$HAVE_PROD" -eq 0 ] && [ "$HAVE_DEV" -eq 0 ]; then + MIGRATE_NOTE="**\`migrate/\` fehlt in diesem Archiv.** Beim Bauen war kein Signierschluessel +erreichbar (\`$SIGN_DIR\`). Ohne Signatur nimmt die alte Klyqa-Firmware kein Image an; +alte Lampen lassen sich mit diesem Archiv also nicht migrieren. Die Schluessel gehoeren +nie in ein Archiv - wer sie hat, baut sich das Archiv mit \`tools/release.sh\` neu." +elif [ "$HAVE_PROD" -eq 0 ]; then + MIGRATE_NOTE="**In \`migrate/\` fehlen die \`_prod.img\`.** Beim Bauen war nur der +Entwicklungsschluessel erreichbar. Lampen mit Produktionssignatur lassen sich damit nicht migrieren." +elif [ "$HAVE_DEV" -eq 0 ]; then + MIGRATE_NOTE="**In \`migrate/\` fehlen die \`_dev.img\`.** Beim Bauen war nur der +Produktionsschluessel erreichbar. Lampen mit Entwicklungssignatur lassen sich damit nicht migrieren." +fi + +echo "== Bruno-Collection, Onboarding-Skript und README ==" +mkdir -p "$OUT/bruno" +cp "$ROOT/bruno/ninlight.yml" "$OUT/bruno/" + +cat >"$OUT/onboard.sh" <<'ONBOARD' +#!/usr/bin/env bash +# Bringt eine frisch geflashte ninlight-Lampe ins WLAN. +# +# Aufruf: ./onboard.sh [wlan-passwort] +# +# VORHER VON HAND (das kann dir kein Skript portabel abnehmen - sich per Skript mit +# einem WLAN zu verbinden ist auf jedem Betriebssystem anders): +# 1. Lampe einschalten. Ohne gespeicherte Zugangsdaten pulsiert sie warmweiss und +# spannt den Zugangspunkt "ninlight-" auf (Passwort: ninlight). +# 2. Diesen Rechner mit genau diesem WLAN verbinden. +# 3. Dieses Skript aufrufen. +# 4. Danach wieder ins normale WLAN wechseln; die Lampe ist per mDNS +# (ninlight-.local, Port 3333) erreichbar. +# +# Optionen: +# --host Adresse der Lampe im AP (Vorgabe 192.168.4.1) +# --port REST-Port (Vorgabe 3333) +# --token API-Token. Im unkonfigurierten Onboarding-AP NICHT noetig (der +# Zugang ist dort offen). Noetig im Fallback-AP, den eine Lampe +# aufspannt, die bereits Zugangsdaten hat (Router > 5 min weg) - +# dort ist system/settings wie ueberall tokengeschuetzt. +# Vorgabewert der Firmware: aabbccddeeff0011223344 +set -euo pipefail + +command -v curl >/dev/null 2>&1 || { echo "Fehler: curl wird gebraucht" >&2; exit 1; } +command -v python3 >/dev/null 2>&1 || { echo "Fehler: python3 wird gebraucht (JSON-Body)" >&2; exit 1; } + +HOST=192.168.4.1 +PORT=3333 +TOKEN="" +SSID="" +PASSWORD="" +# Hilfetext ist der Kommentarblock oben - keine feste Zeilennummer, die beim naechsten +# Satz im Kopf stillschweigend falsch wird. +usage() { awk 'NR>1 && /^#/ {sub(/^# ?/, ""); print; next} NR>1 {exit}' "$0"; } + +while [ $# -gt 0 ]; do + case "$1" in + --host) HOST=${2:?--host braucht einen Wert}; shift 2 ;; + --port) PORT=${2:?--port braucht einen Wert}; shift 2 ;; + --token) TOKEN=${2:?--token braucht einen Wert}; shift 2 ;; + -h|--help) usage; exit 0 ;; + -*) echo "Unbekannte Option: $1" >&2; usage >&2; exit 2 ;; + *) if [ -z "$SSID" ]; then SSID=$1; else PASSWORD=$1; fi; shift ;; + esac +done +[ -n "$SSID" ] || { echo "Fehler: SSID fehlt" >&2; usage >&2; exit 2; } + +URL="http://$HOST:$PORT/api/v1/system/settings" +BODY=$(SSID="$SSID" PASSWORD="$PASSWORD" python3 -c ' +import json, os +print(json.dumps({"type": "wifi", "ssid": os.environ["SSID"], "password": os.environ["PASSWORD"]}))') + +set -- -X PUT "$URL" -H "Content-Type: application/json" -d "$BODY" --max-time 15 +[ -n "$TOKEN" ] && set -- "$@" -H "Authorization: $TOKEN" +echo "== WLAN setzen ($URL) ==" +curl -fsS "$@" || { + echo "Fehler: die Lampe hat die Zugangsdaten nicht angenommen." >&2 + echo " 401 -> Fallback-AP: --token mitgeben." >&2 + echo " keine Verbindung -> bist du wirklich im WLAN ninlight-?" >&2 + exit 1 +} +echo +echo "== Verbindung pruefen (system/info) ==" +# Die Lampe braucht ein paar Sekunden, bis sie im neuen WLAN haengt; danach schliesst +# der AP nach 60 s. wifi.sta_ip zeigt die neue Adresse, wifi.error den Grund, falls es +# nicht geklappt hat (z.B. falsches Passwort). Wartezeit fuer Tests ueberschreibbar. +sleep "${NINLIGHT_ONBOARD_WAIT:-8}" +curl -fsS "http://$HOST:$PORT/api/v1/system/info" --max-time 15 || { + echo "Hinweis: system/info nicht mehr erreichbar - meist ein gutes Zeichen (der AP ist" >&2 + echo " zu, die Lampe haengt im neuen WLAN). Im normalen WLAN nachsehen." >&2 +} +echo +ONBOARD +chmod +x "$OUT/onboard.sh" + +cat >"$OUT/README.md" <\` (Passwort \`ninlight\`) und +wird mit \`onboard.sh\` ins WLAN gebracht. + +Ziele: evk, e27_rgb, e14_rgb, gu10_rgb, e27_cw, e14_cw, gu10_cw, g95_cw. + +## Was liegt wo + +| Verzeichnis | Inhalt | wofuer | +|---|---|---| +| \`ota/\` | \`ninlight__ota.img\` | Update einer Lampe, auf der ninlight schon laeuft (REST \`fw_update\`), und die Auslieferung an die Rettungsstation | +| \`migrate/\` | \`_prod.img\`, \`_dev.img\` | Erstmigration einer Lampe, auf der noch die alte Klyqa-Firmware laeuft | +| \`flash/\` | \`ninlight__gz.img\` | serielle Erstbespielung ueber die Platine | +| \`bruno/\` | \`ninlight.yml\` | REST-Schnittstelle als Bruno-Collection | + +\`SHA256SUMS\` deckt jede Datei ab: \`sha256sum -c SHA256SUMS\` (macOS: \`shasum -a 256 -c SHA256SUMS\`). + +$MIGRATE_NOTE + +## Alte Klyqa-Lampe migrieren + +Die alte Firmware nimmt nur signierte Images an; welche Signatur (\`_prod\` oder \`_dev\`) +gilt, haengt an Produkt-ID und Version der Lampe. Der Weg laeuft ueber die +Migrationsstation (\`tools/station/\` im Repo, ein Raspberry Pi mit zwei Zugangspunkten): + +1. Station einschalten und \`ninlight-station migrate on\` (oder den Taster druecken). +2. Lampe einschalten - sie waehlt sich in den Klyqa-Zugangspunkt ein und holt das + passende Image aus \`migrate/\`. +3. Nach dem Neustart laeuft ninlight; die Lampe spannt ihren Onboarding-AP auf. + +## Frisch geflashte Lampe onboarden + + ./onboard.sh "MeinWLAN" "meinpasswort" + +Vorher von Hand: Lampe einschalten (sie pulsiert warmweiss), diesen Rechner mit dem WLAN +\`ninlight-\` verbinden (Passwort \`ninlight\`). \`./onboard.sh --help\` zeigt die +Schritte und die Optionen (\`--host\`, \`--port\`, \`--token\`). Im unkonfigurierten +Onboarding-AP ist der Zugang offen, im Fallback-AP einer bereits onboardeten Lampe ist das +Token noetig. + +Serielle Erstbespielung: \`flash/ninlight__gz.img\` mit \`tools/flash.sh\` aus dem Repo +bzw. \`wm_tool\` auf die Platine schreiben. + +## Vertrauensgrenze + +Der 512-Byte-Trailer der \`_ota.img\` prueft Geraetetyp und SHA256 - das ist eine +Integritaets- und Geraetetyppruefung, **kein kryptografisches Signaturverfahren**. Der +Download laeuft ueber \`http://\` ohne TLS, und der Rettungskanal ist nur durch ein WLAN- +Passwort geschuetzt, das mit einem oeffentlichen Default im Repo steht. Wer im selben WLAN +ist und das API-Token kennt (bzw. beim Recovery in Funkreichweite ist und das Passwort +kennt), kann der Lampe beliebige Firmware unterschieben. Das ist eine bewusste +Entscheidung dieses Projekts: Ziel ist, die Signaturpruefung der alten, abgekuendigten +Original-Firmware zu umgehen, damit die Lampen ueberhaupt weiter nutzbar bleiben - nicht, +ein neues, gehaertetes Sicherheitsmodell zu bauen. + +Die RSA-Signatur der \`migrate/\`-Images ist die der ALTEN Firmware und existiert nur, damit +die alte Firmware das Image ueberhaupt annimmt. Die privaten Schluessel liegen nie in +diesem Archiv. +README + +echo "== Auf Zugangsdaten pruefen ==" +# Doppelter Boden zum Beiseitelegen von config.local.cmake: bevor irgendetwas gepackt +# wird, wird JEDE Datei des Archivs binaer gegen die echten Werte aus +# config.local.cmake geprueft. Lieber ein abgebrochener Release als ein Archiv mit +# Michaels WLAN-Passwort bei Dritten. +LOCAL="$ROOT/config.local.cmake" +if [ -f "$LOCAL" ]; then + python3 - "$OUT" "$LOCAL" <<'PY' || exit 1 +import os +import re +import sys + +stage, local = sys.argv[1], sys.argv[2] +text = open(local, encoding="utf-8", errors="replace").read() +# Nur die drei Werte, die wirklich geheim sind. NINLIGHT_AP_PASSWORD und der +# Vorgabetoken stehen oeffentlich im Repo (README, Bruno) - sie hier mitzupruefen +# haette nur falsche Alarme erzeugt. +SECRETS = ("NINLIGHT_WIFI_SSID", "NINLIGHT_WIFI_PASSWORD", "NINLIGHT_TOKEN") +PUBLIC = {"", "ninlight", "aabbccddeeff0011223344", "ninlight-recovery", "change-me-please"} +needles = [] +for name in SECRETS: + m = re.search(r'^\s*set\s*\(\s*%s\s+"([^"]*)"' % name, text, re.M) + if m and m.group(1) not in PUBLIC: + needles.append((name, m.group(1).encode())) + +hits = [] +for root, _dirs, files in os.walk(stage): + for fn in sorted(files): + path = os.path.join(root, fn) + with open(path, "rb") as fh: + data = fh.read() + for name, needle in needles: + if needle in data: + hits.append("%s in %s" % (name, os.path.relpath(path, stage))) +if hits: + sys.stderr.write( + "Fehler: Zugangsdaten aus config.local.cmake im Archiv gefunden, nichts gepackt:\n " + + "\n ".join(hits) + "\n") + sys.exit(1) +print(" sauber (%d geprueft)" % len(needles)) +PY +fi + +echo "== SHA256SUMS ==" +( + cd "$OUT" + # $SHA_CMD absichtlich ohne Anfuehrungszeichen: "shasum -a 256" sind drei Woerter. + # shellcheck disable=SC2086 + find . -type f ! -name SHA256SUMS | sed 's|^\./||' | sort | xargs $SHA_CMD >SHA256SUMS +) + +echo "== packen ==" +mkdir -p "$ROOT/dist" +tar czf "$TGZ" -C "$STAGE" "$NAME" +echo "fertig: $TGZ" diff --git a/tools/test_host.sh b/tools/test_host.sh index eb38429..1c1852a 100755 --- a/tools/test_host.sh +++ b/tools/test_host.sh @@ -38,6 +38,7 @@ SHELL_SUITES=( "$ROOT/tools/station/tests/test_cli.sh" "$ROOT/tools/station/tests/test_deploy.sh" "$ROOT/tools/station/tests/test_setup_pi.sh" + "$ROOT/tools/tests/test_release.sh" ) for suite in "${SHELL_SUITES[@]}"; do "$suite" diff --git a/tools/tests/test_release.sh b/tools/tests/test_release.sh new file mode 100755 index 0000000..fe1ebf5 --- /dev/null +++ b/tools/tests/test_release.sh @@ -0,0 +1,496 @@ +#!/usr/bin/env bash +# Stub-Tests fuer tools/release.sh (und tools/build_nocreds.sh) - ohne Docker, ohne +# echten Firmwarebau ausfuehrbar. +# +# Gestubbt wird nur die UNTERSTE Ebene: NINLIGHT_DOCKER zeigt auf ein Skript, das sich +# wie der Bau im Container verhaelt (relative Pfade ab Repo-Wurzel, legt _gz.img und +# _ota.img ab). Alles darueber - das Beiseitelegen von config.local.cmake, das +# Signieren, README/SHA256SUMS, das Packen - laeuft echt. Wichtig dabei: der Stub +# verhaelt sich wie ein Compiler und SCHREIBT die Werte aus config.local.cmake in die +# Images, wenn die Datei zum Bauzeitpunkt noch da ist. Nur so pruefen die Faelle 5/6 +# wirklich das Skript und nicht den Stub: vergisst release.sh/build_nocreds.sh das +# Beiseitelegen, landen die Zugangsdaten tatsaechlich im Abzug. +# +# Aufruf: tools/tests/test_release.sh +set -euo pipefail + +HERE=$(cd "$(dirname "$0")" && pwd) +TOOLS_DIR=$(cd "$HERE/.." && pwd) +REPO=$(cd "$TOOLS_DIR/.." && pwd) + +# Sicherheitsnetz (wie in tools/station/tests/test_deploy.sh): NINLIGHT_SIGN_DIR zeigt +# auf diesem Rechner auf ECHTE Signierschluessel. Sie darf nicht aus der Umgebung in +# diese Suite durchsickern - jedes Szenario setzt sie hart, Testschluessel kommen ueber +# die separate Variable TEST_SIGN_DIR. +unset NINLIGHT_SIGN_DIR +unset TEST_SIGN_DIR +unset NINLIGHT_NOCREDS_BUILD_REL +unset NINLIGHT_DOCKER + +# GNU vor BSD: auf dem Pi/Debian gibt es sha256sum, auf dem Mac nur shasum. +if command -v sha256sum >/dev/null 2>&1; then + SHA_CHECK=(sha256sum -c) +else + SHA_CHECK=(shasum -a 256 -c) +fi + +TMPROOT=$(mktemp -d) +trap 'rm -rf "$TMPROOT"' EXIT + +FAILED=0 +pass() { echo "ok - $1"; } +fail() { echo "FEHLER - $1" >&2; FAILED=1; } + +TARGETS=(evk e27_rgb e14_rgb gu10_rgb e27_cw e14_cw gu10_cw g95_cw) + +# Wegwerf-Zugangsdaten - niemals die echten aus dem Repo. +SECRET_SSID="TestWLAN-4b7f-geheim" +SECRET_PASS="testpasswort-4b7f" +SECRET_TOKEN="0badc0de4b7f0badc0de4b7f" + +# --- Sandbox-Repo: release.sh, build_nocreds.sh, nl_sign.py, bruno/, config.local.cmake --- +setup_sandbox() { + local sandbox=$1 + rm -rf "$sandbox" + mkdir -p "$sandbox/tools/mkimage" "$sandbox/bruno" "$sandbox/dist" + cp "$TOOLS_DIR/release.sh" "$sandbox/tools/release.sh" + cp "$TOOLS_DIR/build_nocreds.sh" "$sandbox/tools/build_nocreds.sh" + chmod +x "$sandbox/tools/release.sh" "$sandbox/tools/build_nocreds.sh" + # Das ECHTE Signierwerkzeug, nur die Schluessel sind Wegwerf-Testschluessel. + cp "$TOOLS_DIR/mkimage/nl_sign.py" "$sandbox/tools/mkimage/nl_sign.py" + # Die ECHTE Bruno-Collection: die Zusage ist, dass sie im Archiv liegt - mit einer + # erfundenen Attrappe wuerde der Test nur sich selbst pruefen. + cp "$REPO/bruno/ninlight.yml" "$sandbox/bruno/ninlight.yml" + cat >"$sandbox/config.local.cmake" <"$stub" <<'STUB' +#!/usr/bin/env bash +# Erwartet "cmake -S . -B / ..." bzw. "cmake --build /". +set -euo pipefail +cd "$NINLIGHT_STUB_ROOT" +mode="" +outdir="" +args=("$@") +for ((i = 0; i < ${#args[@]}; i++)); do + case "${args[$i]}" in + --build) mode=build; outdir=${args[$((i + 1))]} ;; + -B) mode=${mode:-configure}; outdir=${args[$((i + 1))]} ;; + esac +done +[ -n "$outdir" ] || { echo "Stub: kein Build-Verzeichnis in '$*'" >&2; exit 1; } +target=$(basename "$outdir") +mkdir -p "$outdir" +[ "$mode" = "build" ] || exit 0 + +if [ "${STUB_TERM_TARGET:-}" = "$target" ]; then + kill -TERM "$PPID" + sleep 5 + exit 1 +fi +if [ "${STUB_FAIL_TARGET:-}" = "$target" ]; then + echo "Stub: Bau von $target fehlgeschlagen" >&2 + exit 1 +fi + +# "Compiler": die Zugangsdaten aus config.local.cmake landen im Abzug - genau wie beim +# echten Bau, wo CMakeLists.txt sie per set() einkompiliert. +creds="" +if [ -f config.local.cmake ]; then + creds=$(sed -n 's/.*set(NINLIGHT_[A-Z_]* "\(.*\)").*/\1/p' config.local.cmake | tr '\n' ' ') +fi +if [ "${STUB_LEAK:-}" = "1" ]; then + creds="$STUB_LEAK_VALUE" +fi +CREDS="$creds" TARGET="$target" OUTDIR="$outdir" python3 - <<'PY' +import os + +target, outdir, creds = os.environ["TARGET"], os.environ["OUTDIR"], os.environ["CREDS"] +body = bytearray(("firmware-%s-" % target).encode() * 200) +body = body[:4096] + b"\0" * (4096 - len(body[:4096])) +blob = creds.encode() +if blob: + body[100:100 + len(blob)] = blob +gz = bytes(body) +with open(os.path.join(outdir, "ninlight_%s_gz.img" % target), "wb") as fh: + fh.write(gz) # roh, ohne NINL-Trailer - das signiert nl_sign.py +trailer = bytearray(b"\xff" * 512) +trailer[0:4] = b"NINL" +with open(os.path.join(outdir, "ninlight_%s_ota.img" % target), "wb") as fh: + fh.write(gz + bytes(trailer)) +PY +STUB + chmod +x "$stub" +} + +# Wegwerf-Testschluessel ueber nl_sign.generate_test_key() - nie echte Schluessel. +make_test_keys() { + local keydir=$1 with_prod=$2 with_dev=$3 + mkdir -p "$keydir/QCX_Device_CA" + PYTHONPATH="$TOOLS_DIR/mkimage" python3 - "$keydir" "$with_prod" "$with_dev" <<'PY' +import sys +import nl_sign + +keydir, with_prod, with_dev = sys.argv[1], sys.argv[2] == "1", sys.argv[3] == "1" +if with_prod: + k = nl_sign.generate_test_key() + with open(keydir + "/QCX_Device_CA/qcx_firmware_signing_key.pem", "wb") as fh: + fh.write(k.private_pem) + with open(keydir + "/prod_pub.der", "wb") as fh: + fh.write(k.public_der) +if with_dev: + k = nl_sign.generate_test_key() + with open(keydir + "/fw_sign.pem", "wb") as fh: + fh.write(k.private_pem) + with open(keydir + "/dev_pub.der", "wb") as fh: + fh.write(k.public_der) +PY +} + +# Ruft release.sh in der Sandbox auf. NINLIGHT_SIGN_DIR wird hier HART gesetzt, nie aus +# der geerbten Umgebung uebernommen (siehe Sicherheitsnetz oben). +run_release() { + local sandbox=$1 version=$2 + local sign_dir="${TEST_SIGN_DIR:-$sandbox/kein-schluesselverzeichnis}" + env NINLIGHT_SIGN_DIR="$sign_dir" \ + NINLIGHT_DOCKER="$sandbox/stub-docker" \ + NINLIGHT_STUB_ROOT="$sandbox" \ + STUB_FAIL_TARGET="${STUB_FAIL_TARGET:-}" \ + STUB_TERM_TARGET="${STUB_TERM_TARGET:-}" \ + STUB_LEAK="${STUB_LEAK:-}" \ + STUB_LEAK_VALUE="$SECRET_SSID" \ + "$sandbox/tools/release.sh" "$version" +} + +# Sucht die Zugangsdaten BINAER in jeder Datei unterhalb eines Verzeichnisses. +grep_secrets() { + local dir=$1 + SSID="$SECRET_SSID" PASS="$SECRET_PASS" TOKEN="$SECRET_TOKEN" python3 - "$dir" <<'PY' +import os +import sys + +root = sys.argv[1] +needles = {k: os.environ[k].encode() for k in ("SSID", "PASS", "TOKEN")} +for dirpath, _dirs, files in os.walk(root): + for fn in sorted(files): + p = os.path.join(dirpath, fn) + if os.path.islink(p): + continue + with open(p, "rb") as fh: + data = fh.read() + for name, needle in needles.items(): + if needle in data: + print("%s in %s" % (name, os.path.relpath(p, root))) +PY +} + +sha_of() { python3 -c " +import hashlib,sys +print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "$1"; } + +echo "=== Szenario 1: Erfolgslauf mit beiden Schluesseln ===" +SANDBOX="$TMPROOT/s1" +setup_sandbox "$SANDBOX" +setup_stub_docker "$SANDBOX/stub-docker" "$SANDBOX" +KEYS_FULL="$TMPROOT/keys-full" +make_test_keys "$KEYS_FULL" 1 1 +CONF_BEFORE=$(sha_of "$SANDBOX/config.local.cmake") +set +e +OUT=$(TEST_SIGN_DIR="$KEYS_FULL" run_release "$SANDBOX" 0.9.9 2>&1) +RC=$? +set -e +if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC: $OUT"; fi +TGZ="$SANDBOX/dist/ninlight-0.9.9.tar.gz" +if [ -f "$TGZ" ]; then pass "Archiv entstanden"; else fail "kein Archiv unter $TGZ"; fi + +EXTRACT="$TMPROOT/x1" +mkdir -p "$EXTRACT" +tar xzf "$TGZ" -C "$EXTRACT" +A="$EXTRACT/ninlight-0.9.9" +if [ -d "$A" ]; then pass "Archiv entpackt nach ninlight-0.9.9/"; else fail "Verzeichnis ninlight-0.9.9/ fehlt"; fi + +echo "--- Fall 1/2/3: Verzeichnisse und Dateizahlen ---" +for d in ota migrate flash bruno; do + if [ -d "$A/$d" ]; then pass "$d/ vorhanden"; else fail "$d/ fehlt"; fi +done +for f in SHA256SUMS README.md onboard.sh bruno/ninlight.yml; do + if [ -f "$A/$f" ]; then pass "$f vorhanden"; else fail "$f fehlt"; fi +done +if [ -x "$A/onboard.sh" ]; then pass "onboard.sh ist ausfuehrbar"; else fail "onboard.sh ist nicht ausfuehrbar"; fi +N_OTA=$(find "$A/ota" -type f | wc -l | tr -d ' ') +N_FLASH=$(find "$A/flash" -type f | wc -l | tr -d ' ') +N_MIG=$(find "$A/migrate" -type f | wc -l | tr -d ' ') +[ "$N_OTA" = "8" ] && pass "ota/ enthaelt 8 Dateien" || fail "ota/ enthaelt $N_OTA statt 8" +[ "$N_FLASH" = "8" ] && pass "flash/ enthaelt 8 Dateien" || fail "flash/ enthaelt $N_FLASH statt 8" +[ "$N_MIG" = "16" ] && pass "migrate/ enthaelt 16 Dateien" || fail "migrate/ enthaelt $N_MIG statt 16" +MISS="" +for t in "${TARGETS[@]}"; do + [ -f "$A/ota/ninlight_${t}_ota.img" ] || MISS="$MISS ota/$t" + [ -f "$A/flash/ninlight_${t}_gz.img" ] || MISS="$MISS flash/$t" + [ -f "$A/migrate/${t}_prod.img" ] || MISS="$MISS migrate/${t}_prod" + [ -f "$A/migrate/${t}_dev.img" ] || MISS="$MISS migrate/${t}_dev" +done +[ -z "$MISS" ] && pass "je Ziel genau die erwarteten Dateinamen" || fail "fehlende Dateien:$MISS" + +echo "--- Migrationsimages sind echt signiert (und zwar das _gz.img) ---" +if python3 "$TOOLS_DIR/mkimage/nl_sign.py" verify --pub "$KEYS_FULL/prod_pub.der" \ + --in "$A/migrate/evk_prod.img" >/dev/null 2>&1; then + pass "evk_prod.img traegt eine gueltige Produktionssignatur" +else + fail "evk_prod.img besteht die Signaturpruefung nicht" +fi +if python3 "$TOOLS_DIR/mkimage/nl_sign.py" verify --pub "$KEYS_FULL/dev_pub.der" \ + --in "$A/migrate/g95_cw_dev.img" >/dev/null 2>&1; then + pass "g95_cw_dev.img traegt eine gueltige Entwicklungssignatur" +else + fail "g95_cw_dev.img besteht die Signaturpruefung nicht" +fi +# Die Signatur belegt denselben 512-Byte-Block wie der Trailer: signiert werden muss das +# rohe _gz.img. Ein versehentlich signiertes _ota.img waere 512 Byte laenger. +GZ_SIZE=$(python3 -c "import os,sys;print(os.path.getsize(sys.argv[1]))" "$A/flash/ninlight_evk_gz.img") +MIG_SIZE=$(python3 -c "import os,sys;print(os.path.getsize(sys.argv[1]))" "$A/migrate/evk_prod.img") +if [ "$MIG_SIZE" -eq $((GZ_SIZE + 512)) ]; then + pass "Migrationsimage = _gz.img + 512 Byte Signatur (nicht das _ota.img signiert)" +else + fail "Migrationsimage hat $MIG_SIZE Byte, erwartet $((GZ_SIZE + 512))" +fi + +echo "--- Fall 4: SHA256SUMS deckt jede Datei ab und stimmt ---" +LISTED=$(awk '{print $2}' "$A/SHA256SUMS" | sed 's|^\*||' | sort) +PRESENT=$( (cd "$A" && find . -type f ! -name SHA256SUMS | sed 's|^\./||' | sort) ) +if [ "$LISTED" = "$PRESENT" ]; then + pass "SHA256SUMS listet genau die Dateien des Archivs ($(echo "$PRESENT" | wc -l | tr -d ' '))" +else + fail "SHA256SUMS und Archivinhalt weichen ab: +$(diff <(echo "$LISTED") <(echo "$PRESENT") || true)" +fi +if (cd "$A" && "${SHA_CHECK[@]}" SHA256SUMS >/dev/null 2>&1); then + pass "Pruefsummen stimmen" +else + fail "Pruefsummenpruefung fehlgeschlagen" +fi + +echo "--- Fall 5: keine Zugangsdaten im Archiv (binaer gesucht) ---" +HITS=$(grep_secrets "$A") +if [ -z "$HITS" ]; then + pass "weder SSID noch Passwort noch Token in irgendeiner Datei des Archivs" +else + fail "Zugangsdaten im Archiv gefunden: +$HITS" +fi +# Auch im gepackten Archiv selbst (tar speichert Inhalte ungefiltert, gzip ist keine +# Verschleierung - aber eine unkomprimierbare Kopie wuerde hier auffallen). +if python3 -c " +import gzip,sys +data = gzip.open(sys.argv[1],'rb').read() +sys.exit(0 if not any(s.encode() in data for s in sys.argv[2:]) else 1)" \ + "$TGZ" "$SECRET_SSID" "$SECRET_PASS" "$SECRET_TOKEN"; then + pass "auch im entpackten tar-Strom keine Zugangsdaten" +else + fail "Zugangsdaten im tar-Strom des Archivs" +fi +# Der Stub verhaelt sich nur dann wie ein Compiler, wenn er die Werte ueberhaupt +# einbauen KANN - sonst waere Fall 5 wertlos. Gegenprobe: mit vorhandener +# config.local.cmake landen sie im Abzug. +mkdir -p "$TMPROOT/probe" +env NINLIGHT_STUB_ROOT="$SANDBOX" "$SANDBOX/stub-docker" cmake --build "$TMPROOT/probe/evk" >/dev/null +if grep_secrets "$TMPROOT/probe" | grep -q SSID; then + pass "Gegenprobe: der Stub baut die Zugangsdaten ein, solange config.local.cmake da ist" +else + fail "Gegenprobe fehlgeschlagen - der Stub baut nie Zugangsdaten ein, Fall 5 waere wertlos" +fi + +echo "--- Fall 6a: config.local.cmake nach dem Erfolgslauf unveraendert ---" +if [ -f "$SANDBOX/config.local.cmake" ] && [ "$(sha_of "$SANDBOX/config.local.cmake")" = "$CONF_BEFORE" ]; then + pass "config.local.cmake liegt unveraendert an seinem Platz" +else + fail "config.local.cmake fehlt oder wurde veraendert" +fi + +echo "--- README nennt die Vertrauensgrenze und die Verzeichnisse ---" +for phrase in "kein kryptografisches Signaturverfahren" "ohne TLS" "onboard.sh" "migrate/" "flash/"; do + if grep -qF "$phrase" "$A/README.md"; then pass "README nennt '$phrase'"; else fail "README nennt '$phrase' nicht"; fi +done + +echo "=== Szenario 2: Bau bricht mittendrin ab (Fall 6) ===" +SANDBOX="$TMPROOT/s2" +setup_sandbox "$SANDBOX" +setup_stub_docker "$SANDBOX/stub-docker" "$SANDBOX" +CONF_BEFORE=$(sha_of "$SANDBOX/config.local.cmake") +set +e +OUT=$(STUB_FAIL_TARGET=e27_cw TEST_SIGN_DIR="$KEYS_FULL" run_release "$SANDBOX" 0.9.9 2>&1) +RC=$? +set -e +if [ "$RC" -ne 0 ]; then pass "Exit-Code != 0 ($RC)"; else fail "Exit-Code war 0"; fi +if [ ! -e "$SANDBOX/dist/ninlight-0.9.9.tar.gz" ]; then pass "kein halbes Archiv zurueckgelassen"; else fail "Archiv trotz Abbruch entstanden"; fi +if [ -f "$SANDBOX/config.local.cmake" ] && [ "$(sha_of "$SANDBOX/config.local.cmake")" = "$CONF_BEFORE" ]; then + pass "config.local.cmake nach dem Abbruch unveraendert an seinem Platz" +else + fail "config.local.cmake nach dem Abbruch verschwunden oder veraendert" +fi + +echo "=== Szenario 3: Abbruch per Signal (SIGTERM mitten im Bau) ===" +SANDBOX="$TMPROOT/s3" +setup_sandbox "$SANDBOX" +setup_stub_docker "$SANDBOX/stub-docker" "$SANDBOX" +CONF_BEFORE=$(sha_of "$SANDBOX/config.local.cmake") +set +e +OUT=$(STUB_TERM_TARGET=gu10_rgb TEST_SIGN_DIR="$KEYS_FULL" run_release "$SANDBOX" 0.9.9 2>&1) +RC=$? +set -e +if [ "$RC" -ne 0 ]; then pass "Exit-Code != 0 ($RC)"; else fail "Exit-Code war 0"; fi +if [ -f "$SANDBOX/config.local.cmake" ] && [ "$(sha_of "$SANDBOX/config.local.cmake")" = "$CONF_BEFORE" ]; then + pass "config.local.cmake nach dem Signalabbruch unveraendert an seinem Platz" +else + fail "config.local.cmake nach dem Signalabbruch verschwunden oder veraendert" +fi +if [ ! -e "$SANDBOX/dist/ninlight-0.9.9.tar.gz" ]; then pass "kein Archiv nach dem Signalabbruch"; else fail "Archiv trotz Signalabbruch entstanden"; fi + +echo "=== Szenario 4: ohne Schluesselverzeichnis (Fall 3) ===" +SANDBOX="$TMPROOT/s4" +setup_sandbox "$SANDBOX" +setup_stub_docker "$SANDBOX/stub-docker" "$SANDBOX" +set +e +OUT=$(run_release "$SANDBOX" 0.9.9 2>&1) # kein TEST_SIGN_DIR -> Verzeichnis gibt es nicht +RC=$? +set -e +if [ "$RC" -eq 0 ]; then pass "Exit-Code 0 - das Archiv entsteht trotzdem"; else fail "Exit-Code $RC: $OUT"; fi +if echo "$OUT" | grep -q "Schluesselverzeichnis"; then pass "deutliche Meldung zum fehlenden Schluesselverzeichnis"; else fail "keine Meldung zum fehlenden Schluesselverzeichnis"; fi +EXTRACT="$TMPROOT/x4"; mkdir -p "$EXTRACT" +tar xzf "$SANDBOX/dist/ninlight-0.9.9.tar.gz" -C "$EXTRACT" +A4="$EXTRACT/ninlight-0.9.9" +if [ ! -d "$A4/migrate" ]; then pass "migrate/ liegt nicht vor"; else fail "migrate/ trotzdem vorhanden"; fi +if [ -d "$A4/ota" ] && [ "$(find "$A4/ota" -type f | wc -l | tr -d ' ')" = "8" ]; then + pass "ota/ trotzdem vollstaendig" +else + fail "ota/ unvollstaendig" +fi +if grep -q "migrate/\` fehlt in diesem Archiv" "$A4/README.md"; then + pass "README.md sagt, warum migrate/ fehlt" +else + fail "README.md erklaert das fehlende migrate/ nicht" +fi +if (cd "$A4" && "${SHA_CHECK[@]}" SHA256SUMS >/dev/null 2>&1); then + pass "SHA256SUMS stimmt auch ohne migrate/" +else + fail "SHA256SUMS stimmt ohne migrate/ nicht" +fi + +echo "=== Szenario 5: zweiter Lauf mit derselben Version bricht ab (Fall 7) ===" +BEFORE=$(sha_of "$SANDBOX/dist/ninlight-0.9.9.tar.gz") +set +e +OUT=$(run_release "$SANDBOX" 0.9.9 2>&1) +RC=$? +set -e +if [ "$RC" -ne 0 ]; then pass "Exit-Code != 0 ($RC)"; else fail "zweiter Lauf war erfolgreich"; fi +if echo "$OUT" | grep -q "existiert bereits"; then pass "Meldung 'existiert bereits'"; else fail "keine Meldung zum vorhandenen Archiv: $OUT"; fi +if [ "$(sha_of "$SANDBOX/dist/ninlight-0.9.9.tar.gz")" = "$BEFORE" ]; then + pass "vorhandenes Archiv unveraendert" +else + fail "vorhandenes Archiv wurde ueberschrieben" +fi +# Und eine andere Version laeuft weiterhin durch. +set +e +OUT=$(run_release "$SANDBOX" 0.9.10 2>&1) +RC=$? +set -e +if [ "$RC" -eq 0 ] && [ -f "$SANDBOX/dist/ninlight-0.9.10.tar.gz" ]; then + pass "andere Version laeuft normal durch" +else + fail "andere Version schlaegt fehl: $OUT" +fi + +echo "=== Szenario 6: doppelter Boden - Zugangsdaten im Abzug brechen den Lauf ab ===" +SANDBOX="$TMPROOT/s6" +setup_sandbox "$SANDBOX" +setup_stub_docker "$SANDBOX/stub-docker" "$SANDBOX" +set +e +OUT=$(STUB_LEAK=1 TEST_SIGN_DIR="$KEYS_FULL" run_release "$SANDBOX" 0.9.9 2>&1) +RC=$? +set -e +if [ "$RC" -ne 0 ]; then pass "Exit-Code != 0 ($RC)"; else fail "Lauf war trotz Zugangsdaten im Abzug erfolgreich"; fi +if echo "$OUT" | grep -q "Zugangsdaten aus config.local.cmake im Archiv gefunden"; then + pass "Meldung benennt den Fund" +else + fail "keine Meldung zum Fund: $OUT" +fi +if echo "$OUT" | grep -qF "$SECRET_SSID"; then + fail "die Meldung druckt den geheimen Wert selbst" +else + pass "die Meldung druckt den Wert nicht, nur den Namen" +fi +if [ ! -e "$SANDBOX/dist/ninlight-0.9.9.tar.gz" ]; then pass "kein Archiv gepackt"; else fail "Archiv trotz Fund gepackt"; fi + +echo "=== Szenario 7: onboard.sh spricht die richtige Schnittstelle an ===" +ONB="$TMPROOT/x1/ninlight-0.9.9/onboard.sh" +STUBBIN="$TMPROOT/onb-bin" +mkdir -p "$STUBBIN" +CURLLOG="$TMPROOT/curl.log" +cat >"$STUBBIN/curl" <<'CURL' +#!/usr/bin/env bash +{ for a in "$@"; do printf '%s\n' "$a"; done; echo "--"; } >>"$CURL_LOG" +echo '{}' +CURL +chmod +x "$STUBBIN/curl" +: >"$CURLLOG" +CURL_LOG="$CURLLOG" PATH="$STUBBIN:$PATH" NINLIGHT_ONBOARD_WAIT=0 "$ONB" "MeinWLAN" "geheim123" >/dev/null +if grep -q "^http://192.168.4.1:3333/api/v1/system/settings$" "$CURLLOG"; then + pass "PUT geht an system/settings im Onboarding-AP" +else + fail "falsche URL: $(cat "$CURLLOG")" +fi +if grep -q '^PUT$' "$CURLLOG"; then pass "Methode PUT"; else fail "keine PUT-Methode"; fi +if grep -qF '{"type": "wifi", "ssid": "MeinWLAN", "password": "geheim123"}' "$CURLLOG"; then + pass "Rumpf entspricht der Bruno-Anfrage 'Set WiFi'" +else + fail "falscher Rumpf: $(cat "$CURLLOG")" +fi +if grep -q "^http://192.168.4.1:3333/api/v1/system/info$" "$CURLLOG"; then + pass "danach wird system/info abgefragt" +else + fail "system/info wird nicht abgefragt" +fi +if grep -q "^Authorization:" "$CURLLOG"; then + fail "ohne --token wird trotzdem ein Authorization-Header geschickt" +else + pass "ohne --token kein Authorization-Header (offener Onboarding-AP)" +fi +: >"$CURLLOG" +CURL_LOG="$CURLLOG" PATH="$STUBBIN:$PATH" NINLIGHT_ONBOARD_WAIT=0 "$ONB" --token abc12345 "MeinWLAN" "geheim123" >/dev/null +if grep -q "^Authorization: abc12345$" "$CURLLOG"; then + pass "mit --token wird der Header geschickt (Fallback-AP)" +else + fail "--token kommt nicht als Authorization-Header an" +fi +HELP=$("$ONB" --help) +if echo "$HELP" | grep -qi "von hand" && echo "$HELP" | grep -q "ninlight-"; then + pass "Hilfetext nennt die manuellen Schritte und den AP-Namen" +else + fail "Hilfetext erklaert die manuellen Schritte nicht: $HELP" +fi +set +e +"$ONB" >/dev/null 2>&1 +RC=$? +set -e +if [ "$RC" -ne 0 ]; then pass "ohne SSID bricht onboard.sh ab"; else fail "ohne SSID lief onboard.sh durch"; fi + +echo +if [ "$FAILED" -eq 0 ]; then + echo "alle Zusagen erfuellt" +else + echo "es gab Fehler" >&2 +fi +exit "$FAILED"