Zum Inhalt springen

Die HTTP-API ​

Um Signal Lab aus einem Skript, einer CI-Pipeline oder einem anderen Werkzeug zu steuern, sprechen Sie mit einem Signal-Lab-Server (signal-lab-server oder dem Docker-Image) über HTTP. Die Oberfläche, die der Server in einem Browser zeigt, verwendet genau diese API: Jede Schaltfläche ist ein Aufruf von /api/invoke/<command>, jede Live-Zahl trifft auf /api/events ein. Alles, was eine Person auf der Seite des Servers tun kann, kann ein Skript also auch.

Die Desktop-App hat keine HTTP-API: Ihr Fenster erreicht ihre Engine innerhalb der App. Um auf einem Desktop zu automatisieren, führen Sie einen Server auf demselben Rechner aus (siehe den Server) oder verwenden Sie die Kommandozeile signallab.

Endpunkte ​

Methode und PfadWas sie tutToken
GET /api/healthOb der Server antwortet, seine Version, ob er ein Token verlangtnicht nötig
POST /api/invoke/<command>Führt einen Engine-Befehl aus: JSON-Argumente hinein, JSON-Ergebnis hinaus (Befehle)nötig
POST /api/runFührt ein Experiment bis zum Ende aus: das Ergebnis oder seine Schritte als Zeilen (Durchläufe)nötig
GET /api/eventsWebSocket jedes Engine-Ereignisses (Ereignisse)nötig
GET /api/files?path=…Eine Datei, die die Engine im Datenordner geschrieben hat, als Downloadnötig
GET /api/openapi.jsonDiese API, beschrieben in OpenAPI 3.1nötig
GET /login, POST /loginDie Anmeldeseite und das Formular für Browsernicht nötig
POST /logoutBeendet die Sitzung eines Browserseine Sitzung

Jeder andere Pfad unter /api/ antwortet mit 404 und dem Code api.not_found; eine Methode, die ein Pfad nicht annimmt (GET /api/invoke/…), ist 405 mit leerem Body. Alles andere ist die Oberfläche; auf einem Server mit Token wird ein Browser ohne Sitzung zuerst zu /login geschickt.

Basis-URL ​

Ein Server empfängt auf http://127.0.0.1:1430, sofern nichts anderes mit --listen (oder SIGNALLAB_LISTEN) angegeben wird. Das Docker-Image empfängt auf jeder Netzwerkkarte, 0.0.0.0:1430. Die Beispiele auf diesen Seiten verwenden:

bash
SERVER=http://127.0.0.1:1430

Der Server spricht einfaches HTTP. Für HTTPS stellen Sie einen Reverse-Proxy vor ihn, der TLS beendet, und starten den Server mit --secure-cookie.

Authentifizierung ​

Ein ohne Token gestarteter Server empfängt nur auf Loopback und braucht keine Authentifizierung: Jeder auf diesem Rechner darf ihn verwenden. Ein Server, den andere erreichen können, hat immer ein Token, und dann muss jede Anfrage außer /api/health und /login es mitführen.

Skripte senden das Token im Authorization-Header:

bash
TOKEN=$(cat token.txt)
curl -fsS "$SERVER/api/invoke/jobs_list" -X POST \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json"

Der Header muss genau Bearer, ein Leerzeichen und das Token sein. Ein fehlendes oder falsches Token ist 401 mit dem Code auth.required.

Browser melden sich einmal unter /login mit dem Token an und erhalten ein Sitzungs-Cookie, signallab_session: HttpOnly, SameSite=Strict, 7 Tage lang gültig und Secure, wenn der Server mit --secure-cookie läuft. POST /logout beendet sie. Ein falsches Token im Anmeldeformular kostet eine Sekunde vor der Antwort, was das Raten langsam macht. Sitzungen liegen im Arbeitsspeicher des Servers: Ein Neustart meldet jeden Browser ab, während Skripte mit dem Token nicht betroffen sind. Der Server hält höchstens 1024 Sitzungen; darüber hinaus geht die älteste weg.

Woher das Token kommt, ist Sache der Einrichtung des Servers (--token-file, SIGNALLAB_TOKEN oder die Datei token, die --generate-token im Datenordner anlegt): siehe Server-Sicherheit. Ein Token hat mindestens 24 Zeichen und keine Leerzeichen; signal-lab-server token gibt ein neues aus.

WARNING

Wer das Token hat, kann den Server Verkehr senden lassen. Halten Sie die Datei, in der es liegt, nur für sich lesbar, und setzen Sie es nie in eine URL — der Server liest dort ohnehin kein Token.

Host und Origin ​

Zwei Prüfungen laufen vor allem anderen, auf jedem Pfad, /api/health eingeschlossen.

Host. Der Host-Header muss diesen Server benennen:

  • Loopback-Namen bestehen immer: localhost, Namen mit der Endung .localhost, 127.x.x.x und [::1].
  • Namen, die mit --allowed-host (oder SIGNALLAB_ALLOWED_HOSTS) angegeben wurden, bestehen.
  • Ein Server mit Token und ohne --allowed-host antwortet auf jeden Namen.

Alles andere ist 403 mit auth.host. Sprechen Sie den Server unter einem Namen an, den er akzeptiert: curl sendet den Host der URL, die Sie ihm geben.

Origin. Eine Anfrage, die etwas ändert (jede Methode außer GET und HEAD), und das WebSocket-Upgrade müssen von der eigenen Seite des Servers kommen, wenn sie einen Origin-Header mitführen: Ihr Host und Port müssen gleich Host sein. Andernfalls lautet die Antwort 403 mit auth.origin; Origin: null wird ebenfalls abgelehnt. Skripte und curl senden kein Origin und bestehen diese Prüfung — sie brauchen trotzdem das Token.

Nur JSON. POST /api/invoke/… und POST /api/run nehmen Content-Type: application/json an (Parameter wie ; charset=utf-8 sind in Ordnung). Alles andere ist 415 mit command.json_required. Eine Webseite auf einer anderen Seite kann das nicht senden, ohne den Server zuerst zu fragen, und der Server stimmt nie zu.

Einen Befehl aufrufen ​

http
POST /api/invoke/<command>
Content-Type: application/json

{ "argument": "value", … }
  • Der Body ist ein JSON-Objekt mit den Argumenten des Befehls. Ein Befehl ohne Argumente nimmt {} oder einen leeren Body (der Content-Type-Header ist trotzdem nötig).
  • Argumentnamen sind camelCase, wie die Oberfläche sie sendet: jobId, nodeId. Als Argument übergebene Objekte (config, request, document, library…) behalten die Feldnamen, die die Engine schreibt, und die sind meist snake_case: timeout_ms, port_start.
  • Ein Argument, das ein Befehl nicht kennt, ist ein Fehler, wird nie ignoriert: 422 mit command.args_invalid, das den Befehl nennt, und den Worten des Parsers in detail. So ist es auch bei einem fehlenden Pflichtargument. Ein Befehl ohne Argumente liest den Body gar nicht.
  • Ein optionales Argument darf weggelassen oder als null gesendet werden.
  • Die Antwort ist 200 mit dem Ergebnis des Befehls als JSON. Ein Befehl, der nichts zurückzugeben hat, antwortet mit null.

Jeder Befehl mit seinen Argumenten und seinem Ergebnis steht in Befehle.

Fehler ​

StatusWannBody
200Der Befehl lief; /api/run hat den Durchlauf gestartetDas Ergebnis
400Der Body ist kein JSON; eine Durchlauf-Anfrage kann nicht gelesen werdenEngineError: command.args_invalid, api.run_invalid, api.run_source
400/api/files ohne pathReiner Text des Webservers, kein EngineError
401Kein Token oder ein falschesEngineError: auth.required
403Ein Host oder Origin, den der Server ablehntEngineError: auth.host, auth.origin
404Kein solcher API-Pfad; eine Datei, die nicht im Datenordner liegtEngineError: api.not_found, file.not_found
405Eine Methode, die der Pfad nicht annimmtLeer
413Ein Anfrage-Body über 24 MiBReiner Text des Webservers, kein EngineError
413Ein Download über 256 MiBEngineError: file.too_large
415Nicht application/jsonEngineError: command.json_required
422Der Befehl schlug fehl oder der Durchlauf konnte nicht startenEngineError: jeder Code der Engine
500Eine Datei konnte nicht gelesen werdenEngineError: file.io

Ein unbekannter Befehlsname ist 422 mit command.unknown.

Jeder Fehler, den die Engine meldet, hat eine Gestalt, den EngineError:

json
{
  "code": "transport.refused",
  "params": { "target": "http://127.0.0.1:8080/" },
  "node": "request",
  "field": { "key": "url" },
  "detail": "error sending request for url (http://127.0.0.1:8080/): tcp connect error: Connection refused (os error 111)"
}
FeldWas es ist
codeWas schiefging: eine stabile Kennung. Jeder Code und seine Meldung stehen in Fehlermeldungen, gruppiert nach dem Teil vor dem Punkt (zum Beispiel transport)
paramsDie Werte, die die Meldung nennt, alle als Text. Weggelassen, wenn es keine gibt
nodeDer Experimentknoten, um den es geht. Weggelassen, wenn es keinen gibt
fieldDas Feld, um das es geht: key (benannt in Felder) und index, 1-basiert, für wiederholte Felder wie einen Header. Weggelassen, wenn es keines gibt
detailDie eigenen Worte des Betriebssystems, eines Parsers oder einer Bibliothek, auf Englisch. Weggelassen, wenn es keine gibt

Verzweigen Sie nach code, nie nach detail. Die geheimen Werte, die ein Durchlauf oder ein einzelnes Senden verwendet, sind in jedem Fehler, den er meldet, maskiert (••••).

Eine Anfrage, die ihren Server erreicht, aber einen Fehlerstatus bekommt oder gar keinen, ist kein fehlgeschlagener Befehl: http_request antwortet mit 200 und der Antwort, und ok, error und cause sagen, was geschah. Siehe http_request.

Grenzen ​

WasGrenzeAn der Grenze
Ein Anfrage-Body24 MiB413
Ein Experimentdokument4 MiBfile.too_large
Ein Download von /api/files256 MiB413, file.too_large
Die Länge eines Durchlaufs300 sDer Durchlauf schlägt mit run.timeout fehl
Das Feedback-Formular (feedback_send)15 MiB insgesamtfeedback.too_large
Ereignisse, die auf einen WebSocket warten4096Er erhält server://lagged mit der Zahl, die er verpasste

Ereignisse ​

GET /api/events, zu einem WebSocket hochgestuft, strömt jedes Ereignis, das die Engine sendet — die Schritte eines Durchlaufs, das Ende eines Jobs, die Nachrichten eines Monitors, die Frames des Inspektors —, als Textnachrichten an jeden verbundenen Client:

json
{ "event": "job://ended", "payload": { "job_id": 7, "kind": "storm", "error": null } }

Er nimmt dieselben Token- und Origin-Regeln an wie der Rest. Jeder Kanal und seine Nutzdaten stehen in Ereignisse.

Dateien ​

GET /api/files?path=<path> lädt eine Datei herunter, die die Engine im Datenordner des Servers geschrieben hat: einen Bericht eines Durchlaufs (report_path des Ergebnisses eines Durchlaufs), einen Export des Experiments oder des Inspektors, die Signal- oder Emulatorbibliothek. path ist der Pfad, den die Engine Ihnen gegeben hat, auf dem Server (URL-kodiert):

bash
curl -fsS -G "$SERVER/api/files" --data-urlencode "path=/data/runs/run-1759600000000-3.json" \
  -H "Authorization: Bearer $TOKEN" -o report.json
  • Nur Dateien innerhalb des Datenordners werden ausgeliefert. Alles andere, ein Ordner oder eine Datei, die es nicht gibt, ist 404 mit file.not_found.
  • Die Antwort ist application/octet-stream mit Content-Disposition: attachment. Zeichen des Dateinamens außer Buchstaben, Ziffern, ., _ und - werden zu _.
  • Eine Datei über 256 MiB ist 413 mit file.too_large.

Was der Datenordner enthält, steht in Dateien und Ordner.

Zustand ​

GET /api/health ist offen: Es braucht kein Token, nur einen Host, den der Server akzeptiert.

bash
curl -fsS "$SERVER/api/health"
json
{ "status": "ok", "version": "1.0.0", "auth": true }

auth sagt, ob Anfragen ein Token brauchen. signal-lab-server healthcheck fragt dieselbe Adresse, auf der der Server empfängt, und endet mit 0, wenn er antwortet; die Zustandsprüfung des Docker-Images führt ihn aus.

OpenAPI-Beschreibung ​

GET /api/openapi.json beschreibt diese API in OpenAPI 3.1: die Endpunkte, jeden Befehl mit seinen Argumenten, und die Durchlauf-Anfrage und das Ergebnis. Es braucht das Token wie der Rest von /api/. Diese Seiten sind die vollständige Referenz; wo die beiden voneinander abweichen, folgen diese Seiten der Engine.

Jobs ​

Lang laufende Arbeit — ein Monitor, ein Generator, ein Burst, ein Relais, eine Verbindung, ein Emulator, ein Durchlauf — ist ein Job. Ein Befehl, der einen startet, gibt seine JobInfo zurück, sobald er läuft:

json
{ "id": 4, "kind": "osc-monitor", "label": "OSC monitor 0.0.0.0:9000", "params": { "bind": "0.0.0.0:9000" }, "started_ms": 1759600000000 }
FeldWas es ist
idDie Nummer des Jobs, eindeutig, solange der Server läuft; andere Befehle nehmen sie als jobId oder id
kindexperiment, osc-monitor, osc-gen, http-burst, netsim, storm, scan, beacon, discovery, mqtt, websocket oder emulator
labelEine englische Zeile, für Logs
paramsDie Werte, aus denen das Label besteht (Ziel, Bind, Host…). Weggelassen, wenn es keine gibt
started_msWann er startete, Millisekunden seit 1970

Diese Befehle starten einen Job: experiment_start, osc_monitor_start, osc_generator_start, http_burst_start, netsim_start, storm_start, scan_start, broadcast_beacon_start, discovery_start, mqtt_connect, ws_connect und emulator_start. Der Server protokolliert jeden Start mit der Adresse des Clients, der ihn angefordert hat; POST /api/run startet ebenfalls einen Job.

  • jobs_list listet die laufenden Jobs, job_stop stoppt einen, jobs_stop_all stoppt jeden.
  • Ein Job, der von selbst endet oder fehlschlägt, sendet job://ended. Ein Job, den Sie stoppen, sendet nichts mehr: job_stop mit der Antwort true ist die Bestätigung.
  • Jobs gehören dem Server, nicht dem Client, der sie gestartet hat. Die Seite zu schließen oder das Skript zu beenden stoppt sie nicht, jeder Client sieht sie und kann sie stoppen, und ein Server, der herunterfährt, stoppt sie alle.

Ein vollständiges Beispiel ​

Fragen Sie, ob der Server läuft, starten Sie mit einem Befehl einen kleinen HTTP-Emulator, führen Sie das mitgelieferte Experiment http-check dagegen aus und warten Sie auf das Ergebnis, stoppen Sie dann den Emulator. jq holt Felder aus den Antworten.

bash
SERVER=http://127.0.0.1:1430
TOKEN=$(cat token.txt)            # leave out with a loopback server without a token
AUTH="Authorization: Bearer $TOKEN"
JSON="Content-Type: application/json"

# 1. Up? Which version? Does it want a token?
curl -fsS "$SERVER/api/health"
# {"status":"ok","version":"1.0.0","auth":true}

# 2. One command: an HTTP emulator on 127.0.0.1:8080 that answers GET / with 200
EMULATOR=$(curl -fsS -X POST "$SERVER/api/invoke/emulator_start" -H "$AUTH" -H "$JSON" -d '{
  "emulator": { "name": "Example", "bind": "127.0.0.1:8080", "protocol": "http",
                "routes": [ { "method": "GET", "path": "/", "responses": [ { "body": "ok" } ] } ] }
}' | jq .id)

# 3. Run the bundled experiment that expects 200 from http://127.0.0.1:8080/, and wait
curl -sS -X POST "$SERVER/api/run" -H "$AUTH" -H "$JSON" -d '{"template":"http-check"}' \
  | jq '{outcome, error, report_path}'
# {"outcome":"passed","error":null,"report_path":"/data/runs/run-1759600000000-2.json"}

# 4. Stop the emulator
curl -fsS -X POST "$SERVER/api/invoke/job_stop" -H "$AUTH" -H "$JSON" -d "{\"id\":$EMULATOR}"
# true

curl -f verwandelt einen Fehlerstatus in einen fehlgeschlagenen Befehl; lassen Sie es weg (wie in Schritt 3), um den EngineError im Body zu sehen.