Перейти к содержимому

HTTP API ​

Чтобы управлять Signal Lab из скрипта, конвейера CI или другого инструмента, обращайтесь к серверу Signal Lab (signal-lab-server или образу Docker) по HTTP. Интерфейс, который сервер показывает в браузере, использует именно этот API: каждая кнопка — вызов /api/invoke/<command>, каждое живое число приходит по /api/events. Поэтому всё, что человек делает на странице сервера, может сделать и скрипт.

У настольного приложения HTTP API нет: его окно обращается к движку внутри приложения. Чтобы автоматизировать работу на настольном компьютере, запустите сервер на этой же машине (см. сервер) или используйте командную строку signallab.

Эндпоинты ​

Метод и путьЧто делаетТокен
GET /api/healthОтвечает ли сервер, его версия, нужен ли токенне нужен
POST /api/invoke/<command>Выполняет одну команду движка: аргументы JSON на входе, результат JSON на выходе (команды)нужен
POST /api/runВыполняет эксперимент до конца: возвращает результат или его шаги построчно (запуски)нужен
GET /api/eventsWebSocket со всеми событиями движка (события)нужен
GET /api/files?path=…Файл, который движок записал в папке данных, как загрузканужен
GET /api/openapi.jsonОписание этого API в OpenAPI 3.1нужен
GET /login, POST /loginСтраница и форма входа для браузеровне нужен
POST /logoutЗавершает сессию браузерасессия

Любой другой путь под /api/ отвечает 404 с кодом api.not_found; метод, который путь не принимает (GET /api/invoke/…), — это 405 с пустым телом. Всё остальное — интерфейс; если на сервере есть токен, браузер без сессии сначала перенаправляется на /login.

Базовый URL ​

Сервер слушает http://127.0.0.1:1430, если --listen (или SIGNALLAB_LISTEN) не сказал иначе. Образ Docker слушает на всех сетевых картах, 0.0.0.0:1430. В примерах на этих страницах используется:

bash
SERVER=http://127.0.0.1:1430

Сервер говорит по обычному HTTP. Для HTTPS поставьте перед ним обратный прокси, который завершает TLS, и запустите сервер с --secure-cookie.

Аутентификация ​

Сервер, запущенный без токена, слушает только loopback и не требует аутентификации: пользоваться им может любой на этой машине. Сервер, до которого могут добраться другие, всегда имеет токен, и тогда каждый запрос, кроме /api/health и /login, должен его нести.

Скрипты отправляют токен в заголовке Authorization:

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

Заголовок должен быть ровно таким: Bearer, один пробел и токен. Отсутствующий или неверный токен — это 401 с кодом auth.required.

Браузеры один раз входят на /login с токеном и получают cookie сессии, signallab_session: HttpOnly, SameSite=Strict, хранится 7 дней и с Secure, когда сервер запущен с --secure-cookie. POST /logout её завершает. Неверный токен в форме входа стоит секунды ожидания до ответа — подбор идёт медленно. Сессии живут в памяти сервера: перезапуск выводит из системы все браузеры, а скрипты с токеном он не затрагивает. Сервер хранит не больше 1024 сессий; сверх этого уходит самая старая.

Откуда берётся токен — зависит от настройки сервера (--token-file, SIGNALLAB_TOKEN или файл token, который --generate-token создаёт в папке данных): см. безопасность сервера. Токен содержит не меньше 24 символов, без пробелов; signal-lab-server token печатает новый.

WARNING

Тот, у кого есть токен, может заставить сервер отправлять трафик. Держите файл с ним доступным только вам и никогда не ставьте его в URL — сервер всё равно не читает токен оттуда.

Host и Origin ​

Две проверки выполняются раньше всего, на любом пути, включая /api/health.

Host. Заголовок Host должен называть этот сервер:

  • Имена loopback проходят всегда: localhost, имена с окончанием .localhost, 127.x.x.x и [::1].
  • Проходят имена, заданные через --allowed-host (или SIGNALLAB_ALLOWED_HOSTS).
  • Сервер с токеном и без --allowed-host отвечает на любое имя.

Всё остальное — 403 с auth.host. Обращайтесь к серверу по имени, которое он принимает: curl отправляет хост того URL, который вы ему дали.

Origin. Запрос, который что-то меняет (любой метод, кроме GET и HEAD), и переход на WebSocket, если несут заголовок Origin, должны приходить со страницы самого сервера: его хост и порт должны совпадать с Host. Иначе ответ — 403 с auth.origin; Origin: null тоже отклоняется. Скрипты и curl не отправляют Origin и эту проверку проходят — токен им по-прежнему нужен.

Только JSON. POST /api/invoke/… и POST /api/run принимают Content-Type: application/json (параметры вроде ; charset=utf-8 допустимы). Всё остальное — 415 с command.json_required. Веб-страница с другого сайта не может отправить такой запрос, не спросив сервер заранее, а сервер никогда не соглашается.

Вызов команды ​

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

{ "argument": "value", … }
  • Тело — один объект JSON с аргументами команды. Команда без аргументов принимает {} или пустое тело (заголовок Content-Type всё равно нужен).
  • Имена аргументов — в camelCase, как их отправляет интерфейс: jobId, nodeId. Объекты, переданные как аргумент (config, request, document, library…), сохраняют имена полей, которые пишет движок, в основном в snake_case: timeout_ms, port_start.
  • Незнакомый команде аргумент — ошибка, а не игнорирование: 422 с command.args_invalid, где названа команда, а слова разборщика — в detail. Так же и с пропущенным обязательным аргументом. Команда без аргументов тело не читает вовсе.
  • Необязательный аргумент можно пропустить или отправить как null.
  • Ответ — 200 с результатом команды в виде JSON. Команда, которой нечего вернуть, отвечает null.

Каждая команда с её аргументами и результатом описана в разделе команды.

Ошибки ​

СтатусКогдаТело
200Команда выполнена; /api/run запустил запускРезультат
400Тело не JSON; запрос запуска не удаётся прочитатьEngineError: command.args_invalid, api.run_invalid, api.run_source
400/api/files без pathПростой текст от веб-сервера, не EngineError
401Нет токена или он неверныйEngineError: auth.required
403Host или Origin, которые сервер отклоняетEngineError: auth.host, auth.origin
404Нет такого пути API; файла нет в папке данныхEngineError: api.not_found, file.not_found
405Метод, который путь не принимаетПусто
413Тело запроса больше 24 МиБПростой текст от веб-сервера, не EngineError
413Загрузка больше 256 МиБEngineError: file.too_large
415Не application/jsonEngineError: command.json_required
422Команда не выполнилась или запуск не удалось начатьEngineError: любой код движка
500Файл не удалось прочитатьEngineError: file.io

Неизвестное имя команды — 422 с command.unknown.

Каждый сбой, о котором сообщает движок, имеет одну форму — 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)"
}
ПолеЧто это
codeЧто пошло не так: стабильный идентификатор. Каждый код и его сообщение перечислены в сообщениях об ошибках, сгруппированные по части до точки (например, transport)
paramsЗначения, которые называет сообщение, все как строки. Пропускается, если их нет
nodeУзел эксперимента, о котором речь. Пропускается, если такого нет
fieldПоле, о котором речь: key (названо в полях) и index, считая с 1, для повторяющихся полей, например заголовка. Пропускается, если такого нет
detailСобственные слова операционной системы, разборщика или библиотеки, по-английски. Пропускается, если их нет

Ветвитесь по code, а не по detail. Значения секретов, которые использует запуск или одиночная отправка, маскируются (••••) во всех ошибках, о которых они сообщают.

Запрос, который дошёл до своего сервера, но получил статус ошибки или не получил ответа вовсе, — это не сбой команды: http_request отвечает 200 с ответом, а ok, error и cause говорят, что произошло. См. http_request.

Лимиты ​

ЧтоЛимитНа пределе
Тело запроса24 МиБ413
Документ эксперимента4 МиБfile.too_large
Загрузка из /api/files256 МиБ413, file.too_large
Длительность запуска300 сЗапуск завершается ошибкой run.timeout
Форма обратной связи (feedback_send)15 МиБ всегоfeedback.too_large
События в очереди одного WebSocket4096Он получает server://lagged с числом пропущенных

События ​

GET /api/events, переведённый в WebSocket, передаёт каждое событие движка — шаги запуска, завершение задачи, сообщения монитора, кадры Инспектора — каждому подключённому клиенту в виде текстовых сообщений:

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

Действуют те же правила токена и Origin, что и везде. Каждый канал и его содержимое описаны в разделе события.

Файлы ​

GET /api/files?path=<path> скачивает файл, который движок записал в папке данных сервера: отчёт о запуске (report_path в результате запуска), экспорт эксперимента или Инспектора, библиотеку сигналов или эмуляторов. path — это путь, который вам дал движок, на сервере (в URL-кодировке):

bash
curl -fsS -G "$SERVER/api/files" --data-urlencode "path=/data/runs/run-1759600000000-3.json" \
  -H "Authorization: Bearer $TOKEN" -o report.json
  • Отдаются только файлы внутри папки данных. Всё остальное — папка или несуществующий файл — это 404 с file.not_found.
  • Ответ — application/octet-stream с Content-Disposition: attachment. Символы имени файла, кроме букв, цифр, ., _ и -, заменяются на _.
  • Файл больше 256 МиБ — 413 с file.too_large.

Что хранится в папке данных — в разделе файлы и папки.

Состояние ​

GET /api/health открыт: токен не нужен, нужен только Host, который сервер принимает.

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

auth говорит, нужен ли запросам токен. signal-lab-server healthcheck обращается к тому же адресу, который слушает сервер, и завершается с кодом 0, если тот отвечает; проверку состояния образа Docker выполняет именно он.

Описание OpenAPI ​

GET /api/openapi.json описывает этот API в OpenAPI 3.1: эндпоинты, каждую команду с её аргументами, а также запрос и результат запуска. Нужен токен, как и для остального /api/. Эти страницы — полный справочник; где они расходятся, правы эти страницы: они следуют движку.

Задачи ​

Долгая работа — монитор, генератор, нагрузочный поток, реле, соединение, эмулятор, запуск — это задача. Команда, которая её начинает, возвращает её JobInfo сразу, как только та заработала:

json
{ "id": 4, "kind": "osc-monitor", "label": "OSC monitor 0.0.0.0:9000", "params": { "bind": "0.0.0.0:9000" }, "started_ms": 1759600000000 }
ПолеЧто это
idНомер задачи, уникальный, пока работает сервер; другие команды принимают его как jobId или id
kindexperiment, osc-monitor, osc-gen, http-burst, netsim, storm, scan, beacon, discovery, mqtt, websocket или emulator
labelОдна строка по-английски, для журналов
paramsЗначения, из которых составлена подпись (цель, адрес прослушивания, хост…). Пропускается, если их нет
started_msКогда началась, в миллисекундах с 1970 года

Задачу начинают эти команды: 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 и emulator_start. Сервер записывает в журнал каждый старт с адресом клиента, который его запросил; POST /api/run тоже начинает задачу.

  • jobs_list перечисляет работающие задачи, job_stop останавливает одну, jobs_stop_all — все.
  • Задача, которая завершилась сама или с ошибкой, отправляет job://ended. Остановленная вами больше ничего не отправляет: подтверждение — ответ true от job_stop.
  • Задачи принадлежат серверу, а не клиенту, который их запустил. Закрытая страница или завершённый скрипт их не останавливают, их видят все клиенты и каждый может их остановить, а сервер при завершении останавливает все.

Полный пример ​

Спросите, работает ли сервер, запустите небольшой эмулятор HTTP одной командой, выполните на нём встроенный эксперимент http-check и дождитесь результата, затем остановите эмулятор. jq достаёт поля из ответов.

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 превращает статус ошибки в неудавшуюся команду; уберите его (как на шаге 3), чтобы увидеть EngineError в теле.