Соглашения API веб-терминала
Все эндпоинты смонтированы под /api/webterminal/ и требуют входа. Ошибки всегда возвращаются как {"error": "..."}.
1. Структура конфигурации (файл webterminal в conf)
{
"settings": {
"allowLocalShell": false,
"maxSessions": 20,
"idleTimeout": 0, // минуты; сеанс без ввода и вывода дольше этого времени завершается автоматически; 0 — без ограничения
"bufferSize": 256, // КБ; буфер повтора вывода, хранится для каждого сеанса и воспроизводится при переподключении
"heartbeatInterval": 30, // секунды; heartbeat WebSocket (ping сервера / ping фронтенда)
"sessionKeepAlive": 0 // минуты; сколько сеанс хранится после отключения браузера; 0 — завершается сразу при отключении
},
"connections": [Connection],
"shortcuts": [{ "name": "Show disk usage", "command": "df -h", "sendEnter": true }],
"splitLayout": null, // раскладка панелей, настроенная фронтендом; бэкенд хранит и возвращает её как есть (json.RawMessage)
"security": {
"agreementAccepted": false,
"twoFAEnable": false,
"twoFAKey": "" // секрет TOTP; в ответах API маскируется
},
"credentials": [Credential] // пресеты учётных данных SSH
}Connection (type — ssh / telnet / local):
{
"key": "", "name": "", "type": "ssh", "remark": "", "enable": true,
"quickAccessDirs": ["/etc", "/var/log"], // каталоги быстрого доступа SFTP
"ssh": { "host", "port", "username", "authType": "password|key|credential",
"credentialKey": "", // ключ пресета учётных данных при authType=credential; username тогда может быть пустым и берётся из пресета
"password", "privateKey",
"keyFile": "", // путь к файлу закрытого ключа на самом сервере; используется при authType=key и пустом privateKey
"passphrase", "hostKey", "keepAlive", "workDir": "", "timeout": 15,
"proxy": Proxy },
"telnet": { "host", "port": 23, "username", "password", "timeout": 15, "proxy": Proxy },
"local": { "shell", "workDir", "env": {"K":"V"} }
}
Proxy = { "enabled": false, "type": "socks5|http", "host", "port", "username", "password" }Credential (пресет учётных данных, на который могут ссылаться многие SSH-подключения):
{ "key": "", "name": "", "authType": "password|key", "username": "", // необязательно, используется, если у подключения нет username
"password", "privateKey", "keyFile", "passphrase", "remark": "", "enable": true }Пароли, закрытые ключи, парольные фразы закрытых ключей и пароли прокси маскируются в ответах через crud.Mask и сохраняются при обновлении через crud.KeepSecret.
2. Управление подключениями
| Метод | Путь | Описание |
|---|---|---|
| GET/POST | /connections | Список / создание |
| PUT/DELETE | /connections/{key} | Изменение / удаление |
| PUT | /connections-order | Изменение порядка {"keys":[...]} |
| POST | /connections/{key}/test | Проверка доступности SSH / Telnet |
| POST | /connections/test | Проверка несохранённого подключения (тело — Connection; сохранённые секреты подставляются, если поле key указывает на исходное подключение) |
| DELETE | /connections/{key}/hostkey | Сброс отпечатка хоста |
| PUT | /connections/{key}/quickaccess | Тело {"dirs":["/a","/b"]} |
| GET/POST | /credentials | Список / создание пресетов учётных данных |
| PUT/DELETE | /credentials/{key} | Изменение / удаление (пока пресет используется, возвращается 409 со списком имён подключений) |
| PUT | /credentials-order | Изменение порядка пресетов {"keys":[...]} |
| GET | /shells | Список оболочек, доступных на этой машине ["/bin/bash", ...] (читается /etc/shells и отфильтровывается по существующим файлам) |
Дополнительно (определяющей является реализация):
POST /connections/testиPOST /connections/{key}/testвозвращают 200 с{ok, error?, serverVersion?, newHostKey?, latencyMs}; для типаlocalвозвращается 400. Для Telnet проверяется только доступность по TCP (в том числе через прокси).PUT /connections/{key}/quickaccessвозвращает{ok, dirs}(без пробелов по краям и дубликатов). Если при изменении подключенияquickAccessDirsне передан, сохраняется прежнее значение.ssh.timeout/telnet.timeoutпринимают значения от 1 до 120 секунд, по умолчанию 15.ssh.workDirприменяется черезcdпосле входа.- У подключений с
authType=credentialпри сохранении очищаются встроенные пароль, закрытый ключ, файл ключа и парольная фраза; учётные данные берутся из пресета в момент подключения. Изменение пресета закрывает ссылающиеся на него SSH-подключения из пула. - Автозаполнение входа в Telnet делает по одной попытке и только при обнаружении приглашений вроде
login:/username:/password:.
3. Сеансы терминала (постоянные сеансы, к которым можно переподключиться после обрыва)
GET /connections/{key}/ws?cols=&rows=: создаёт новый сеанс и переключается на WebSocket.GET /sessions/{id}/ws?cols=&rows=: подключается к существующему сеансу, сначала воспроизводит буфер, затем передаёт данные в реальном времени. К одному сеансу одновременно могут подключиться несколько клиентов (ввод общий, вывод рассылается всем).
Протокол WebSocket:
Сервер → браузер: двоичные кадры несут вывод терминала; текстовые кадры несут JSON:
{"t":"s","d":"<sessionId>"}сеанс установлен (отправляется один раз и после создания, и после подключения);{"t":"n","d":"notice"}уведомление;{"t":"x","d":"reason"}сеанс завершён;{"t":"p"}ответ на heartbeat.Браузер → сервер: текстовый JSON:
{"t":"i","d":"input"},{"t":"r","c":cols,"r":rows},{"t":"p"}heartbeat.При отключении WebSocket браузера: если
sessionKeepAlive > 0, сеанс сохраняется (отсоединённым) и закрывается по истечении времени, если никто не подключился; если 0, закрывается сразу.Когда процесс сеанса завершается сам, отправляется сообщение
x, WebSocket закрывается, а сеанс удаляется из таблицы сеансов.Создание сеанса сверх
maxSessionsвозвращает HTTP 429; ошибка подключения сообщается кадром уведомленияn, после чего WebSocket закрывается.bytesIn— число байтов ввода из браузера,bytesOut— число байтов вывода терминала,attached— число подключённых сейчас клиентов (число); поля времени в формате RFC3339.
REST:
| Метод | Путь | Описание |
|---|---|---|
| GET | /sessions | [{id, connKey, connName, type, remark, createdAt, lastActive, attached, cols, rows, bytesIn, bytesOut}] |
| GET | /sessions/{id} | Один сеанс |
| PUT | /sessions/{id}/remark | {"remark":""} |
| GET | /sessions/{id}/stats | {bytesIn, bytesOut, durationSec, idleSec, attached, cols, rows} |
| DELETE | /sessions/{id} | Завершить сеанс |
4. Настройки, быстрые команды и раскладка панелей
| Метод | Путь | Описание |
|---|---|---|
| GET/PUT | /settings | {settings, activeSessions} / тело — Settings |
| GET/PUT | /globalshortcuts | Массив быстрых команд |
| GET/PUT/DELETE | /splitlayout | Произвольная раскладка в JSON |
5. Безопасность (пользовательское соглашение + двухфакторная проверка)
| Метод | Путь | Описание |
|---|---|---|
| GET | /security | {agreementAccepted, twoFAEnable, verified}; verified показывает, что текущий токен входа прошёл двухфакторную проверку |
| POST | /security/agreement | Принять соглашение |
| POST | /security/2fa/setup | Создать ожидающий секрет {secret, url} (не сохраняется) |
| POST | /security/2fa/enable | {secret, code} включает 2FA после успешной проверки |
| POST | /security/2fa/disable | {code} отключает 2FA после успешной проверки |
| POST | /security/verify2fa | {code}; после успешной проверки текущий токен входа считается проверенным на 30 минут |
| GET | /security/check2fa | {need: bool}: нужна ли ещё проверка |
После 5 неверных кодов 2FA подряд токен входа блокируется на 1 минуту (HTTP 429). 2fa/setup возвращает 400, если 2FA уже включена (сначала отключите её). После успешного 2fa/enable текущий токен считается проверенным. Проверка доступа: включение локальной оболочки, создание сеанса, подключение к сеансу и все эндпоинты SFTP возвращают 403 {"error":"...","needAgreement":true}, если соглашение не принято, и 403 {"error":"...","need2fa":true}, если 2FA включена, а текущий токен не проверен. «Включение локальной оболочки» здесь означает PUT /settings, меняющий allowLocalShell с false на true. На стороне Go все они вызывают m.requireAccess(w, r) bool.
6. Управление файлами по SFTP
Параметр пути {key} — это ключ подключения. Подключения SSH используют SFTP (каждый запрос открывает канал подсистемы SFTP на переиспользуемом SSH-соединении); подключения local работают напрямую с файловой системой самого сервера (через те же эндпоинты). Telnet и другие типы возвращают 400; отсутствующее подключение возвращает 404. Бэкенд держит одно SSH-соединение на подключение и закрывает его после 5 минут простоя (m.acquireSSH). Подключению local нужно включённое в настройках «Разрешить локальную оболочку», иначе возвращается 403 (права на локальные файлы равнозначны локальной оболочке). Ошибка SSH-подключения возвращает 502.
Общие соглашения:
- Все
path/from/to/destдолжны быть абсолютными путями, начинающимися с/(еслиpathуlistпуст, используется каталог по умолчанию: каталог входа для SSH, а для local — workDir подключения или домашний каталог пользователя). Относительные пути возвращают 400. - Ошибки всегда
{"error":"сообщение"}: не найдено 404, нет доступа 403, слишком большой размер 413, неподдерживаемый тип 415, цель уже существует 409 (добавлено в этом разделе), неверные параметры 400, всё остальное 500. - Успешные эндпоинты записи возвращают
{"ok":true, ...}.
| Метод | Путь | Описание |
|---|---|---|
| GET | /sftp/{key}/list?path= | {path, parent, entries:[{name, path, isDir, isLink, size, mode, modeStr, modTime, owner?}]}; сначала каталоги, затем сортировка по имени без учёта регистра. mode — восьмеричная строка прав, например "755", modeStr — в виде drwxr-xr-x, modTime — RFC3339, owner — имя пользователя для local и числовой uid для SSH. У символических ссылок isLink=true, а isDir=true, если они указывают на каталог |
| GET | /sftp/{key}/read?path=&download=1 | Без download: текстовый предпросмотр, возвращает {path, name, size, modTime, content}; больше 1MB — 413, двоичное содержимое (с NUL или не в UTF-8) — 415. С download=1: потоковая загрузка (application/octet-stream + Content-Disposition: attachment) с поддержкой Range и If-Modified-Since |
| POST | /sftp/{key}/write | {path, content} сохраняет текст (перезапись; если цель — каталог, возвращается 400) → {ok, path, size} |
| POST | /sftp/{key}/upload | Потоковая запись multipart: path (целевой каталог, который должен существовать и должен идти перед файлами), files (можно несколько) и необязательный relativePaths (по одному на файл; сопоставляется по порядку с идущими следом файлами и должен идти перед соответствующими файлами; принимается и строка с JSON-массивом). Без relativePaths имя файла используется как есть (оно может содержать подпуть, когда браузер загружает каталог). Файлы с тем же именем перезаписываются, недостающие промежуточные каталоги создаются автоматически; пути с .. возвращают 400, что гарантирует запись только внутри целевого каталога → {ok, count, files:[абсолютные пути]} |
| POST | /sftp/{key}/mkdir | {path} создаёт каталог (вместе с недостающими родительскими); 409, если уже существует |
| POST | /sftp/{key}/touch | {path} создаёт пустой файл (вместе с недостающими родительскими каталогами); 409, если уже существует |
| POST | /sftp/{key}/rename | {from, to}; 409, если to уже существует, 400 при перемещении каталога внутрь самого себя, 403 при переименовании корневого каталога; для local между устройствами автоматически выполняется копирование с последующим удалением |
| POST | /sftp/{key}/copy | {from:[...], to}; to — целевой каталог, каждый элемент копируется в to/исходное-имя (рекурсивно; символические ссылки копируются как ссылки; специальные файлы, например устройства и каналы, пропускаются). При совпадении имён копия автоматически переименовывается в name copy.ext, name copy 2.ext и т. д. без ошибки; копирование каталога внутрь самого себя возвращает 400 → {ok, paths:[новые пути]} |
| POST | /sftp/{key}/chmod | {path, mode:"755", recursive}; mode — восьмеричная строка от 0 до 7777; рекурсия не проходит по символическим ссылкам, файлы и каталоги получают одинаковые права |
| DELETE | /sftp/{key}/remove | {paths:[...]} (тело JSON; рекурсивное удаление; у символических ссылок удаляется только сама ссылка); если любой из путей равен /, возвращается 403 и ничего не удаляется → {ok, count} |
| POST | /sftp/{key}/compress | `{paths:[...], dest, format:"zip |
| POST | /sftp/{key}/decompress | {path, dest} поддерживает zip / tar / tar.gz / tgz (определяется по суффиксу; остальное возвращает 415). Если dest пуст, распаковка идёт в каталог, где лежит архив; каталог создаётся при необходимости; файлы с тем же именем перезаписываются. Архив с любой записью .. отклоняется целиком (400, защита от zip-slip); записи, не являющиеся обычными файлами, например символические и жёсткие ссылки, пропускаются; из прав файлов сохраняются только биты rwx → {ok, dest, count} |
| GET | /sftp/{key}/preview-archive?path= | Список записей архива [{name,size,isDir}] (не более 10000 записей; у name нет завершающего /) |
Сжатие и распаковка полностью выполняются в Go через интерфейс файловой системы (os для local, чтение и запись по SFTP для SSH) и не зависят от удалённых команд.
7. Прочее
- Журналы: журналы модуля уже показываются на общей странице журналов, отдельный эндпоинт не нужен.
- Импорт из Lucky: заполняет telnet, proxy, keyFile, workDir, timeout, env, quickAccessDirs, sessionConfig, globalShortcuts и securityConfig (пользовательское соглашение и секрет 2FA переносятся вместе).