Drucker-Pi holt sich die Einrichtung, statt sie geschickt zu bekommen

Bisher richtete die App den Pi per SSH ein: Adresse, Benutzer und Passwort ins
Webformular, Knopf druecken. Das verlangte ein SSH-Passwort in einem Browser,
ein weiteres Paket (ssh2) und eine Verbindung vom Server zum Pi.

Jetzt andersherum. Die App legt einen Link bereit und wartet. Auf dem Pi wird
das Skript geholt und gestartet, dort wird der Drucker ausgewaehlt, und am
Ende meldet sich der Pi von selbst an. Wer einen Drucker ansteckt, steht
ohnehin am Pi - dort gehoert auch die Frage hin, welcher es sein soll.

Die Auswahl zeigt Klartextnamen aus dem sysfs: "EPSON TM-T88IV" statt "lp0".
Bei mehreren Druckern ist das der Unterschied zwischen Wissen und Raten.

Das ausgelieferte Skript ist zusammengesetzt, nicht statisch: einrichten.sh ist
eine Vorlage, in die setup.js Adresse, Code, print-server.py und die
systemd-Unit einsetzt. Heraus kommt EINE Datei - auf dem Pi ist nichts
nachzuladen, und eine halb geglueckte Uebertragung kann es nicht geben.

Zwei Endpunkte muessen ohne Anmeldung auskommen, weil der Pi sich bei Authentik
nicht anmelden kann. Der Einrichtungscode ist dort der einzige Schluessel: 32
Zeichen, gilt einmal und 30 Minuten, nur im Arbeitsspeicher, zeitkonstant
verglichen, hinter einem eigenen Zaehler.

Die Adresse des Pi wird der Verbindung entnommen und nicht dem Rumpf der
Anmeldung - was der Pi ueber sich behauptet, koennte auch jemand anders
behaupten. Die eigene Adresse leitet die App aus dem Host-Kopf ab und prueft
sie streng, statt sie zu maskieren: der Wert landet in einem Shell-Skript.

Geprueft am echten Pi (Debian 13, TM-T88IV): Link erzeugen, am Pi abholen,
Drucker aus der Liste waehlen, Anmeldung, Uebernahme, Probedruck. Dazu die
Abweise: verbrauchter Code, falscher Code, Abholung ohne Code, und dass die
uebrige API weiterhin 401 liefert.

Entfaellt: provision.js, install.sh, die Abhaengigkeit ssh2, das SSH-Formular.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kerim (portable/claudecode)andClaude Opus 5 committed 2026-09-12 13:37:26 +02:00
1 parent 8c5cdf16c4
commit e68daee457
14 files changed
+834 -605

No files matched your search

+286
View File
@@ -0,0 +1,286 @@
#!/bin/bash
# ---------------------------------------------------------------------------
# Bondrucker-Bruecke einrichten - wird AUF DEM PI ausgefuehrt.
#
# Dieses Skript wird nicht von Hand gepflegt heruntergeladen, sondern von der
# Timer-App ausgeliefert: sie setzt ihre eigene Adresse und einen einmaligen
# Einrichtungscode ein und haengt den Druckdienst sowie die systemd-Unit
# gleich mit an. Heraus kommt eine einzige Datei, die alles enthaelt.
#
# Aufruf auf dem Pi:
# curl -fsSL "<Link aus der Timer-App>" -o bondrucker.sh
# sudo bash bondrucker.sh
#
# Der Link steht in der Timer-App unter Einstellungen -> Drucker einrichten.
#
# Ohne Netz zur Timer-App laeuft das Skript trotzdem durch; es zeigt am Ende
# Adresse und Token an, die dann von Hand in die Einstellungen gehoeren.
#
# Schalter:
# --device /dev/usb/lp0 Drucker vorgeben, statt danach zu fragen
# --port 9100 Lausch-Port der Bruecke
# ---------------------------------------------------------------------------
set -euo pipefail
# Diese beiden Werte ersetzt die Timer-App beim Ausliefern.
APP_URL="@@APP_URL@@"
SETUP_CODE="@@SETUP_CODE@@"
ZIEL=/opt/bondrucker
GERAET=""
PORT=9100
# --- Schalter --------------------------------------------------------------
while [ $# -gt 0 ]; do
case "$1" in
--device) GERAET="${2:-}"; shift 2 ;;
--port) PORT="${2:-9100}"; shift 2 ;;
-h|--help) sed -n '2,22p' "$0"; exit 0 ;;
*) echo "Unbekannter Schalter: $1" >&2; exit 1 ;;
esac
done
if [ "$(id -u)" -ne 0 ]; then
echo "Bitte mit sudo starten: sudo bash $0" >&2
exit 1
fi
# ---------------------------------------------------------------------------
# Hilfsmittel
# ---------------------------------------------------------------------------
titel() { printf '\n\033[1m%s\033[0m\n' "$1"; }
punkt() { printf ' %s\n' "$1"; }
warnung() { printf ' \033[33m%s\033[0m\n' "$1"; }
fehler() { printf '\n\033[31m%s\033[0m\n' "$1" >&2; exit 1; }
# Fragt den Benutzer und gibt die Antwort auf stdout zurueck.
#
# Die Umleitung auf /dev/tty ist der Grund, warum auch
# "curl ... | sudo bash" noch bedienbar bleibt: dort ist die Standardeingabe
# die Pipe und nicht die Tastatur. Gibt es gar kein Terminal, wird die Vorgabe
# genommen, statt in einer Endlosschleife zu haengen.
frage() {
local text="$1" vorgabe="${2:-}" antwort=""
if [ -t 0 ]; then
read -r -p "$text" antwort || true
elif [ -e /dev/tty ]; then
read -r -p "$text" antwort < /dev/tty || true
fi
printf '%s' "${antwort:-$vorgabe}"
}
# Klartextname eines USB-Druckers aus dem sysfs, damit in der Auswahl nicht
# nur "lp0" steht, sondern "EPSON TM-T88IV".
usb_name() {
local pfad
pfad=$(readlink -f "/sys/class/usbmisc/$(basename "$1")/device" 2>/dev/null) || return 1
while [ -n "$pfad" ] && [ "$pfad" != "/" ]; do
if [ -r "$pfad/product" ]; then
printf '%s %s' "$(cat "$pfad/manufacturer" 2>/dev/null || true)" "$(cat "$pfad/product")"
return 0
fi
pfad=$(dirname "$pfad")
done
return 1
}
# ---------------------------------------------------------------------------
# 1. System pruefen
# ---------------------------------------------------------------------------
titel "1. System"
command -v python3 >/dev/null || fehler "python3 fehlt. Bitte nachinstallieren: apt install python3"
command -v systemctl >/dev/null || fehler "systemd fehlt - dieses Skript richtet einen systemd-Dienst ein."
punkt "$(. /etc/os-release 2>/dev/null && echo "$PRETTY_NAME" || uname -s) ($(uname -m))"
punkt "$(python3 --version)"
# ---------------------------------------------------------------------------
# 2. Drucker auswaehlen
# ---------------------------------------------------------------------------
titel "2. Drucker"
if [ -z "$GERAET" ]; then
GEFUNDEN=()
for d in /dev/usb/lp* /dev/lp*; do
[ -e "$d" ] && GEFUNDEN+=("$d")
done
if [ "${#GEFUNDEN[@]}" -eq 0 ]; then
warnung "Gerade haengt kein Drucker unter /dev/usb/lp* am Pi."
warnung "Das ist kein Hindernis: der Dienst findet ihn, sobald er angesteckt wird."
GERAET=$(frage " Geraetepfad [/dev/usb/lp0]: " "/dev/usb/lp0")
else
echo
for i in "${!GEFUNDEN[@]}"; do
printf ' %d) %-16s %s\n' "$((i + 1))" "${GEFUNDEN[$i]}" "$(usb_name "${GEFUNDEN[$i]}" || echo '')"
done
printf ' a) anderer Pfad\n\n'
WAHL=$(frage " Welcher Drucker? [1]: " "1")
if [ "$WAHL" = "a" ] || [ "$WAHL" = "A" ]; then
GERAET=$(frage " Geraetepfad: " "/dev/usb/lp0")
elif [ "$WAHL" -ge 1 ] 2>/dev/null && [ "$WAHL" -le "${#GEFUNDEN[@]}" ]; then
GERAET="${GEFUNDEN[$((WAHL - 1))]}"
else
fehler "Ungueltige Auswahl: $WAHL"
fi
fi
fi
punkt "gewaehlt: $GERAET"
# ---------------------------------------------------------------------------
# 3. Dienstbenutzer
# ---------------------------------------------------------------------------
titel "3. Dienstbenutzer"
if ! id bondrucker >/dev/null 2>&1; then
useradd --system --no-create-home --shell /usr/sbin/nologin --groups lp bondrucker
punkt "Benutzer 'bondrucker' angelegt (Gruppe lp)."
else
usermod -aG lp bondrucker
punkt "Benutzer 'bondrucker' war schon da."
fi
# ---------------------------------------------------------------------------
# 4. Programmdateien
# ---------------------------------------------------------------------------
titel "4. Programmdateien"
install -d -m 755 "$ZIEL"
cat > "$ZIEL/print-server.py" <<'PRINT_SERVER_ENDE'
@@PRINT_SERVER@@
PRINT_SERVER_ENDE
chmod 755 "$ZIEL/print-server.py"
punkt "$ZIEL/print-server.py"
cat > /etc/systemd/system/bondrucker.service <<'SERVICE_ENDE'
@@SERVICE@@
SERVICE_ENDE
chmod 644 /etc/systemd/system/bondrucker.service
punkt "/etc/systemd/system/bondrucker.service"
# ---------------------------------------------------------------------------
# 5. Konfiguration
# ---------------------------------------------------------------------------
titel "5. Konfiguration"
TOKEN="$(head -c 24 /dev/urandom | base64 | tr -d '/+=' | head -c 32)"
install -d -m 750 /etc/bondrucker
{
printf 'PRINT_TOKEN=%s\n' "$TOKEN"
printf 'PRINT_DEVICE=%s\n' "$GERAET"
printf 'PRINT_PORT=%s\n' "$PORT"
} > /etc/bondrucker/env
chmod 640 /etc/bondrucker/env
chown root:bondrucker /etc/bondrucker/env
punkt "/etc/bondrucker/env geschrieben, neues Token erzeugt."
# Ohne diese Regel gehoert /dev/usb/lp0 nach jedem Einstecken wieder root
# allein - der Dienst laeuft aber nicht als root und koennte nicht drucken.
cat > /etc/udev/rules.d/99-bondrucker.rules <<'RULE'
# USB-Bondrucker: Gruppe lp und Schreibrecht, damit der Dienst 'bondrucker'
# ohne root drucken kann. Gilt fuer alle usblp-Geraete.
SUBSYSTEM=="usbmisc", KERNEL=="lp[0-9]*", GROUP="lp", MODE="0660"
SUBSYSTEM=="usb", ATTR{bInterfaceClass}=="07", GROUP="lp", MODE="0660"
RULE
udevadm control --reload-rules >/dev/null 2>&1 || true
udevadm trigger --subsystem-match=usbmisc >/dev/null 2>&1 || true
punkt "udev-Regel gesetzt (Geraet ueberlebt das Aus- und Einstecken)."
# ---------------------------------------------------------------------------
# 6. Dienst starten
# ---------------------------------------------------------------------------
titel "6. Dienst"
systemctl daemon-reload
systemctl enable bondrucker.service >/dev/null 2>&1
# Bewusst "restart" und nicht "enable --now": bei einer erneuten Einrichtung
# laeuft der Dienst schon, und "enable --now" wuerde ihn in Ruhe lassen. Er
# liefe dann mit dem ALTEN Token weiter und wuerde jeden Bon ablehnen.
systemctl restart bondrucker.service
sleep 1
if ! systemctl is-active --quiet bondrucker.service; then
systemctl --no-pager --lines=20 status bondrucker.service || true
fehler "Der Dienst laeuft nicht. Die Ausgabe oben sagt, woran es liegt."
fi
punkt "bondrucker.service laeuft."
# Gegenprobe von innen: antwortet die Bruecke, und nimmt sie das neue Token?
ANTWORT="$(curl -fsS -m 5 -H "X-Print-Token: $TOKEN" "http://127.0.0.1:$PORT/status" 2>/dev/null || true)"
case "$ANTWORT" in
*'"tokenAccepted": true'*|*'"tokenAccepted":true'*) punkt "Bruecke antwortet und nimmt das Token an." ;;
"") warnung "Die Bruecke antwortet auf 127.0.0.1:$PORT noch nicht." ;;
*) warnung "Die Bruecke antwortet, lehnt das Token aber ab: $ANTWORT" ;;
esac
case "$ANTWORT" in
*'"deviceExists": false'*|*'"deviceExists":false'*)
warnung "Das Geraet $GERAET ist gerade nicht da - Kabel pruefen." ;;
esac
# ---------------------------------------------------------------------------
# 7. Bei der Timer-App anmelden
# ---------------------------------------------------------------------------
IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
titel "7. Timer-App"
angemeldet=0
case "$APP_URL" in
@@*|"")
punkt "Kein Einrichtungscode enthalten - dieses Skript wurde nicht ueber"
punkt "die Timer-App geladen. Anmeldung wird uebersprungen."
;;
*)
if ! command -v curl >/dev/null; then
warnung "curl fehlt - die Anmeldung muss von Hand erfolgen."
else
RUMPF="$(printf '{"code":"%s","port":%s,"token":"%s","device":"%s","hostHint":"%s"}' \
"$SETUP_CODE" "$PORT" "$TOKEN" "$GERAET" "$IP")"
if ERGEBNIS="$(curl -fsS -m 15 -X POST \
-H 'Content-Type: application/json' \
-d "$RUMPF" "$APP_URL/api/setup/register" 2>&1)"; then
punkt "Bei $APP_URL angemeldet."
punkt "Die Timer-App druckt ab jetzt auf diesen Pi."
angemeldet=1
else
warnung "Anmeldung fehlgeschlagen: $ERGEBNIS"
warnung "Meist ist der Code abgelaufen - in der Timer-App einen neuen erzeugen."
fi
fi
;;
esac
# ---------------------------------------------------------------------------
# Fertig
# ---------------------------------------------------------------------------
titel "Fertig"
punkt "Drucker: $GERAET"
punkt "Adresse: ${IP:-<IP des Pi>}"
punkt "Port: $PORT"
if [ "$angemeldet" -eq 0 ]; then
echo
punkt "Von Hand eintragen in der Timer-App unter Einstellungen:"
printf ' Adresse: %s\n Port: %s\n Token: %s\n' "${IP:-?}" "$PORT" "$TOKEN"
fi
echo
punkt "Zustand abfragen: curl -s http://${IP:-127.0.0.1}:$PORT/status"
punkt "Protokoll: journalctl -u bondrucker -f"
echo