Skip to content

2 · uGo-Server — unsere eigene Schnittstelle unter /api/**

Note

Der Bauplan in einem Satz: Der Browser auf dem Tablett spricht ausschliesslich mit dem eigenen Server, dieser spricht mit dem Fahrgestell. Drei Gründe, alle drei tragend:

  1. Das Fahrgestell setzt keine CORS-Kopfzeilen. Ein unmittelbarer Zugriff aus dem Browser scheitert.
  2. Die Geräteadresse steht damit an genau einer Stelle in der Betriebsumgebung (CHASSIS_BASIS) und nicht im ausgelieferten Javascript. Beim Kunden stehen mehrere Geräte, jedes mit eigener Adresse.
  3. Kein Zugriff nach draussen: die Oberfläche kennt nur den eigenen Server.

Alle Antworten sind application/json mit cache-control: no-store, ausser wo unten etwas anderes steht.

2.1 Anmeldemodell

Punkt Wie es ist Warum
Träger Sitzungskeks, httpOnly, sameSite: strict, Laufzeit 8 Stunden httpOnly: ein eingeschleustes Skript in der Oberfläche kommt nicht an den Keks heran
secure ausdrücklich false SvelteKit setzt secure von sich aus auf true, ausser bei localhost. Am Tablett über HTTP warf die Webansicht den Keks stillschweigend weg: die Anmeldung meldete 200, angemeldet war niemand. Der Roboter spricht im eigenen Netz HTTP; der Schutz kommt aus dem Netz, nicht aus dem Keks
Rolle ergibt sich aus dem Kennwort
Bremse 5 Fehlversuche je Absender, danach 60 Sekunden Sperre → 429 Ein vierstelliges Kennwort ist in Sekunden geraten, wenn man beliebig oft raten darf
Kein Kennwort eingerichtet dann verlangt keine Route eine Anmeldung; POST /api/anmeldung antwortet 409 Ein frisch aufgesetztes Gerät soll bedienbar sein. Sobald ein Kennwort steht, greifen alle Prüfungen

Jede Route prüft selbst. Der Türsteher in hooks.server.ts schützt Seiten, nicht die Schnittstelle. Eine Schnittstelle, die sich darauf verlässt, dass jemand anders vorher aufpasst, ist irgendwann offen.

2.2 Alle Routen

Route Verf. Zweck / Rumpf Antwort Anmeldung
/api/anmeldung GET Anmeldezustand {kennwortEingerichtet, angemeldet, rolle}nie Kennwort oder Kennung nein
POST {kennwort} 200 {rolle} · 401 falsch · 409 kein Kennwort eingerichtet · 429 gesperrt nein
DELETE abmelden {abgemeldet: true} nein
/api/chassis/<pfad> GET Durchleitung zum Fahrgestell, Pfad und Suchteil unverändert Antwort des Gerätes, durchgereicht nein
POST Durchleitung, Rumpf unverändert dito nein
PATCH Durchleitung, Rumpf unverändert dito nein
DELETE eng begrenzt: nur maps/<Zahl> und mappings/<Zahl> 403 bei jedem anderen Pfad ja
/api/einstellungen GET Einstellungen lesen Objekt nein — die Oberfläche muss wissen, wann sie ins Ruhebild geht
POST Teilmenge setzen. Anerkannt: ruhebildSekunden, zweitanzeige, medien, medienTon, medienLautstaerke, zweitanzeigeAdresse, fahrstuhlumgang, anstellart, uiotAdresse der gespeicherte Stand ja
/api/fahrtenbuch GET Auswertung, bewusst alle Tage auf einmal 365 Zeilen mit drei Zahlen sind wenige Kilobyte; die Anzeige rechnet jeden Zeitraum ohne erneute Frage, und es gibt keine Datumsangaben in der Adresse, die man falsch verstehen kann nein
/api/funk GET Dauerverbindung zum Fahrgestell, als SSE weitergereicht text/event-stream. Ereignisse: lage ({verbunden, grund?}) und meldung (Rohtext des Gerätes). Herzschlag alle 15 s. 501, wenn der Server keinen WebSocket kann nein
/api/karte-aus-aufnahme POST {aufnahmeId, name} — aus einer fertigen Kartenfahrt eine Karte machen 200 {karte} · 409 mit Klartextgrund ja
/api/kartenarchiv GET Abgleich Gerät ↔ Archiv Stand · 502 wenn das Gerät nicht antwortet ja
POST {aktion}sichern (karteId), alleSichern, zurueckspielen (archivId, zweitname), entfernen (archivId) 200 je Aktion · 400 unbekannte Aktion · 409 mit Klartextgrund ja
/api/medien GET Liste {medien:[{datei, art, bytes}]} nein
POST mehrteiliges Formular, Feld datei 200 · 413 über 200 MB · 415 keine anzeigbare Art ja
/api/medien/<datei> GET Datei ausliefern, mit Bereichsanfragen 200 / 206 / 416 · cache-control: public, max-age=3600 nein
/api/protokolle GET alle Betriebsdaten in einem Zip application/zip. Enthalten: Serverprotokoll (aktuell + vorher), Fahrtenbuch, Einstellungen, Touren, Regeln, /device/info, /chassis/current-map, /mappings/. Nicht enthalten: Kennwörter, Sitzungsmarken, Mediendateien ja
/api/protokolle/zeilen?anzahl=N GET letzte Protokollzeilen aus dem Ringspeicher, N zwischen 5 und 500, Vorgabe 80 {zeilen:[…]} ja — im Protokoll stehen Geräteadressen und Fehlertexte
/api/regeln GET Regeln und Warteschlange {regeln, planer} nein — der Bedienbildschirm zeigt an, was ansteht; das ist Auskunft
PUT {regeln} {regeln, planer} ja — eine Regel legt fest, wann der Roboter von allein losfährt
/api/regeln/aktion POST {aktion}ausloesen (regelId), taster (kennung), alarm (an, grund), weiter, entfernen (auftragId), alles-anhalten {text, planer} · 400 unbekannte Anweisung · 409 mit Grund nein, bewusst — siehe unten
/api/touren GET Touren lesen {touren} nein
PUT {touren} {touren} ja
/api/touren/lauf GET Stand des Laufs Laufzustand + planer nein
POST {aktion}start (tourId), stop, pause, fortsetzen, bestaetigen Laufzustand + planer · 409 mit Grund nein, bewusst — siehe unten
/api/uiot/<pfad> GET nur lesende Durchleitung zum u-IoT-Modul; Ziel aus einstellungen.uiotAdresse Antwort des Moduls im Rohtext · 403 gesperrter Pfad · 409 keine/ungültige Adresse · 502 keine Antwort ja
/api/zonen GET ?karte=<id> — Zonen einer Karte {karteId, kartenname, fassung, zonen} · 502 wenn das Gerät nicht antwortet ja
POST {aktion: "anlegen", karteId, name, ecken} oder {aktion: "entfernen", karteId, kennung} {zone} mit kennung, ecken, zonenNachher, fassungVorher, fassungNachher ja
/api/wolke HINWEIS GET Stand der Cloud-Anbindung {wolke, identitaet} — Verbindung, Puffer, Sperre, letzte Fassungsfreigabe, Umgebung, Thing-Name, Kunde ja
POST {aktion: "anmelden", beitrittscode} 200 {angemeldet, thingName, kunde, seriennummer} · 409 mit Grund ja

Angelegt wird über /api/zonen ausschliesslich die Langsamzone (regionType 2). Sperrzonen bietet dieser Endpunkt bewusst nicht an, weil ihre Kennzahl nicht belegt ist (siehe 1.8). Ein doppelter Zonenname in derselben Karte wird abgewiesen — technisch wäre er zulässig, aber der Techniker unterscheidet zwei Zonen „Treppenhaus" hinterher nur noch an 24 Hexzeichen und entfernt dann die falsche.

Warning

Warum zwei Routen bewusst OHNE Anmeldung arbeiten.

/api/touren/lauf: Das Anlegen einer Tour ist Technikerarbeit und verlangt ein Kennwort. Das Starten und vor allem das Anhalten ist Bedienung. Wer neben einem fahrenden Roboter steht und ihn stoppen will, hat kein Kennwort dabei und soll auch keins brauchen. Ein Halteknopf, der erst eine Anmeldung verlangt, ist kein Halteknopf. stop leert deshalb auch die Warteschlange: wer hält, will einen stehenden Roboter, nicht einen, der gleich mit dem nächsten Auftrag weitermacht.

/api/regeln/aktion: Hier kommen die Anstösse aus der Umgebung an — ein Ruftaster an der Wand, der Alarmkontakt des Hauses. Diese Geräte tragen kein Kennwort und können keines tragen. Der Schutz liegt eine Ebene tiefer: der Server hört nur im Roboternetz. Das Anlegen von Regeln verlangt weiterhin eine Anmeldung — dort entscheidet sich, was ein Tasterdruck überhaupt bewirken darf.

2.3 Die Durchleitung /api/chassis/** im Einzelnen

Verhalten Wert Warum
Fehlerbild 504 Zeitüberschreitung, 502 nicht erreichbar Genau diese beiden Nummern wertet die Oberfläche als Verbindungsverlust — alles andere ist eine Gerätestörung
Zeitgrenzen 5 s im Regelfall, 45 s für mappings und maps sowie schreibend für chassis/pose und chassis/current-map Lesen bleibt schnell, Schreiben darf dauern. GET /chassis/pose wird im Takt abgefragt; bekäme es 45 s, stünde die Oberfläche bei einem hängenden Gerät eine Dreiviertelminute still
Weiterleitungen redirect: 'manual'. Verfolgt wird genau eine: derselbe Ursprung, derselbe Suchteil, nur ein Schrägstrich angehängt Die Schnittstelle des Fahrgestells hat keine Anmeldung. Wer im Netz antwortet, soll unseren Server nicht auf eine fremde Adresse schicken können. Der relative Verweis Djangos wird vorher zu einer vollständigen Adresse aufgelöst — fetch in Node wirft sonst „Failed to parse URL"
Schrägstrich export const trailingSlash = 'ignore', und der Pfad wird aus url.pathname genommen, nicht aus params SvelteKit schneidet den Schrägstrich sonst ab und leitet mit 308 um, bevor die Handhabung läuft. Beim Fahrgestell käme wieder /maps ohne Schrägstrich an — und POST scheitert dann am APPEND_SLASH-Fehler
Kopfzeilen durchgelassen werden nur content-type, content-length, cache-control, etag
Grösse Antworten über 4 MB (CHASSIS_GROESSTANTWORT_BYTES) werden verworfen Ein Gerät, das Megabytes liefert, soll weder das Tablett noch den Server beschäftigen
Fehlermeldung enthält nie die Geräteadresse Sie ist der einzige Wert, den der Server vor dem Browser voraushat; in einer Fehlermeldung stünde sie in jedem Fehlerbericht

2.4 Die Durchleitung /api/uiot/** — drei Riegel

  1. Nur GET. Kein POST, kein PUT, kein DELETE. Was das Modul verändert — Türen öffnen, Aufzüge rufen — kommt erst dazu, wenn die Schnittstelle feststeht und jede Handlung eine Rückfrage bekommt.
  2. Gesperrte Pfade (auch lesend): config, wifi, cmd, reboot, export, password, passwd, secret. Der Grund ist belegt: das Modul bietet ein „Export Config JSON", und darin stehen Kennwörter im Klartext. Ein Leserecht darauf wäre ein Leserecht auf die WLAN-Zugangsdaten des Kunden.
  3. Nur mit Anmeldung.

Die Antwort wird unverändert durchgereicht — hier wird nichts gedeutet. Was das Modul meldet, soll der Techniker im Rohtext sehen; alles andere wäre Ratenwerk, solange die Schnittstellenbeschreibung des Moduls aussteht. Das ist der Platzhalter, der ersetzt wird, sobald diese Beschreibung vorliegt.