Skip to content
Эта страница переведена с помощью ИИ и может содержать неточности. Помогите улучшить перевод на GitHub.

Соглашения API веб-терминала ​

Все эндпоинты смонтированы под /api/webterminal/ и требуют входа. Ошибки всегда возвращаются как {"error": "..."}.

1. Структура конфигурации (файл webterminal в conf) ​

jsonc
{
  "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):

jsonc
{
  "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-подключения):

jsonc
{ "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 переносятся вместе).

Распространяется по лицензии Apache-2.0.