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/events | WebSocket со всеми событиями движка (события) | нужен |
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. В примерах на этих страницах используется:
SERVER=http://127.0.0.1:1430Сервер говорит по обычному HTTP. Для HTTPS поставьте перед ним обратный прокси, который завершает TLS, и запустите сервер с --secure-cookie.
Аутентификация
Сервер, запущенный без токена, слушает только loopback и не требует аутентификации: пользоваться им может любой на этой машине. Сервер, до которого могут добраться другие, всегда имеет токен, и тогда каждый запрос, кроме /api/health и /login, должен его нести.
Скрипты отправляют токен в заголовке Authorization:
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. Веб-страница с другого сайта не может отправить такой запрос, не спросив сервер заранее, а сервер никогда не соглашается.
Вызов команды
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 |
403 | Host или 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/json | EngineError: command.json_required |
422 | Команда не выполнилась или запуск не удалось начать | EngineError: любой код движка |
500 | Файл не удалось прочитать | EngineError: file.io |
Неизвестное имя команды — 422 с command.unknown.
Каждый сбой, о котором сообщает движок, имеет одну форму — EngineError:
{
"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/files | 256 МиБ | 413, file.too_large |
| Длительность запуска | 300 с | Запуск завершается ошибкой run.timeout |
Форма обратной связи (feedback_send) | 15 МиБ всего | feedback.too_large |
| События в очереди одного WebSocket | 4096 | Он получает server://lagged с числом пропущенных |
События
GET /api/events, переведённый в WebSocket, передаёт каждое событие движка — шаги запуска, завершение задачи, сообщения монитора, кадры Инспектора — каждому подключённому клиенту в виде текстовых сообщений:
{ "event": "job://ended", "payload": { "job_id": 7, "kind": "storm", "error": null } }Действуют те же правила токена и Origin, что и везде. Каждый канал и его содержимое описаны в разделе события.
Файлы
GET /api/files?path=<path> скачивает файл, который движок записал в папке данных сервера: отчёт о запуске (report_path в результате запуска), экспорт эксперимента или Инспектора, библиотеку сигналов или эмуляторов. path — это путь, который вам дал движок, на сервере (в URL-кодировке):
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, который сервер принимает.
curl -fsS "$SERVER/api/health"{ "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 сразу, как только та заработала:
{ "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 |
kind | experiment, 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 достаёт поля из ответов.
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}"
# truecurl -f превращает статус ошибки в неудавшуюся команду; уберите его (как на шаге 3), чтобы увидеть EngineError в теле.