Изначально я просто хотел нормально подключаться к своим серверам. В итоге получился SSH-клиент с вкладками, историей команд, сниппетами и собственным терминальным экраном. А заодно появилась возможность проверить EvertyUI на совершенно другом классе приложений.
С чего всё началось
Я много работаю с серверами. Разрабатываю EvertyDesk Next, экспериментирую с сетевыми протоколами, поднимаю контейнеры, собираю приложения, иногда просто захожу посмотреть, почему опять что-нибудь перестало работать.
Для всего этого нужен SSH.
Много лет для подобных задач существует PuTTY. Программа хорошая, проверенная, выполняет свою работу. Но лично меня она постепенно начала раздражать.
Открываешь одно соединение, потом второе. Где-то ошибка подключения, где-то соединение оборвалось. Где-то требуется вспомнить команду, которую выполнял вчера. Где-то снова заниматься ключами.
И каждый раз ощущение, что вокруг обычного терминального подключения приходится выполнять слишком много дополнительных действий.
Мне не нужен комбайн. Я хочу открыть приложение, выбрать сервер и работать.
И если соединение оборвалось, это не должно превращаться в маленькую катастрофу.
В какой-то момент я подумал: а почему бы не написать собственный SSH-клиент?
Тем более что у меня уже есть Rust и собственный интерфейсный движок EvertyUI.
Так появился EvertySSH.
Зачем вообще писать ещё один SSH-клиент?
Понятно, что SSH-клиентов существует огромное количество.
Есть PuTTY, OpenSSH, Termius, MobaXterm, Tabby и множество других решений.
Я не собираюсь утверждать, что все они плохие, а мой клиент внезапно решил проблемы человечества.
У меня была гораздо более приземлённая задача.
Я хотел собрать в одном приложении то, чем пользуюсь постоянно:
-
несколько SSH-сессий в одном окне;
-
быстрое подключение к сохранённым серверам;
-
генерацию и использование SSH-ключей;
-
историю подключений и команд;
-
библиотеку собственных команд;
-
нормальную работу терминала с привычными консольными приложениями;
-
возможность продолжать работу после сетевых ошибок, не пересоздавая всё рабочее окружение.
И без браузерных движков внутри.
Rust, EvertyUI и никакого Chromium
EvertySSH написан на Rust.
Для SSH используется библиотека russh, для асинхронных задач — Tokio. Хранение истории реализовано через SQLite, конфигурация серверов — через TOML.
А интерфейс работает на EvertyUI.
И вот здесь начинается самая интересная часть.
EvertyUI — это мой собственный интерфейсный слой, который развивается в рамках экосистемы Everty.
Он появился не ради спортивного интереса. Когда работаешь над несколькими приложениями, постепенно понимаешь, сколько времени уходит на интерфейсы, управление состоянием, обработку ввода и постоянную адаптацию сторонних библиотек.
При этом сами приложения могут быть совершенно разными.
EvertyDesk работает с удалёнными рабочими столами. Файловый менеджер — с каталогами и файлами. SSH-клиент — с потоками терминального вывода.
Но кнопки, поля ввода, списки, прокрутка, обработка мыши и клавиатуры нужны везде.
Я решил вынести эту часть в собственную библиотеку.
В репозитории EvertySSH сейчас два основных Rust-crate:
EvertySSH/├── Cargo.toml└── crates/ ├── everty-ui/ │ ├── src/ │ ├── tests/ │ └── benches/ └── everty-ssh/ └── src/ ├── main.rs ├── app.rs ├── ui.rs ├── ssh.rs ├── terminal.rs ├── config.rs └── db.rs
EvertyUI не должен знать, что такое SSH, удалённый сервер или история команд.
Он должен предоставлять механизмы построения и отображения интерфейса.
И наоборот: SSH-подсистеме не нужно знать, какой цвет у кнопки подключения.
Звучит очевидно, но на практике такое разделение приходится постоянно защищать от соблазна сделать исключение «всего для одной маленькой функции».
Поэтому в EvertyUI даже есть архитектурный тест, который проверяет отсутствие зависимостей от компонентов других продуктов.
А как вообще рисуется интерфейс?
В EvertySSH я не использую Chromium или WebView.
Для создания окна и обработки событий применяется winit, а вывод изображения выполняется через softbuffer.
Сам EvertyUI содержит средства геометрии, компоновки, обработки ввода, работы с текстом и программной растеризации.
В результате интерфейс формируется в обычном буфере пикселей, который передаётся оконной поверхности.
Упрощённо схема выглядит так:
События Windows / Linux / macOS | winit | Состояние UI | EvertyUI | RGBA framebuffer | softbuffer | Окно приложения
Сейчас EvertyUI использует cosmic-text для текстового конвейера и resvg для векторной графики.
При этом в самом движке есть отдельные компоненты для layout, DPI, прокрутки, текстовых полей, всплывающих окон и виртуализированных списков.
Зачем так много возни ради простого SSH-клиента?
Потому что мне нужен не только SSH-клиент.
Я хочу иметь интерфейсную основу, которую можно использовать в других проектах.
EvertySSH стал удобной проверкой этой идеи: достаточно сложное приложение, чтобы выявить проблемы интерфейсного слоя, но достаточно самостоятельное, чтобы не тащить за собой архитектуру EvertyDesk.
SSH-сессия — отдельная асинхронная задача
Сетевую часть я вынес в ssh.rs.
Подключение проходит несколько этапов: TCP, аутентификация, создание SSH-канала, запрос PTY и запуск удалённой оболочки.
На эти операции установлены отдельные ограничения по времени.
Например, подключение к серверу ограничено семью секундами, а создание оболочки — пятью.
Это сделано ради предсказуемого поведения интерфейса.
Если сервер недоступен, программа должна сообщить об этом, а не бесконечно изображать активность.
После установления соединения для сессии запускается асинхронная задача, которая обрабатывает входящие и исходящие данные.
Упрощённо:
tokio::select! { Some(data) = input_rx.recv() => { // Отправка пользовательского ввода } Some((cols, rows)) = resize_rx.recv() => { // Изменение размера удалённого PTY } msg = channel.wait() => { // Чтение вывода удалённой оболочки }}
Конкретная реализация находится в ssh.rs.
Так терминальный интерфейс и сетевое соединение не обязаны работать в одном синхронном цикле.
Если подключение завершилось с ошибкой, приложение может сохранить сведения о сессии и отобразить причину.
Для меня это важнее попыток сделать какую-нибудь эффектную анимацию подключения.
Терминал — это не просто текстовое поле
Когда я начинал думать о собственном SSH-клиенте, самой очевидной частью казался терминал.
Получаем байты с сервера, выводим символы на экран.
Казалось бы, что может быть проще?
А потом вспоминаешь про ANSI escape-последовательности, перемещение курсора, очистку строк, цвета, альтернативный экран, прокрутку и полноэкранные консольные приложения.
Терминал — это целая машина состояний.
В EvertySSH для обработки управляющих последовательностей используется vte, а само экранное состояние реализовано отдельно.
Каждая ячейка хранит символ и его параметры отображения:
pub struct Cell { pub ch: char, pub fg: Color, pub bg: Color, pub bold: bool, pub underline: bool,}
У экрана есть собственные размеры, положение курсора, текущие цвета, история прокрутки и альтернативный буфер.
Последний особенно важен для программ вроде vim, nano или htop.
Они не просто печатают текст последовательно, а управляют целыми областями экрана.
И если неправильно реализовать переключение буферов, терминал очень быстро превратится в кашу.
Я не утверждаю, что моя реализация уже поддерживает абсолютно все особенности современных терминалов. Это большая область, в которой всегда найдётся очередная управляющая последовательность, способная удивить разработчика.
Но сама архитектура уже позволяет заниматься терминальным экраном независимо от SSH и интерфейсных виджетов.
Вкладки и разделение экрана
Следующая задача — работа с несколькими серверами.
Мне не хотелось открывать отдельное окно приложения на каждое соединение.
В EvertySSH используются вкладки, а также режим разделения рабочей области для одновременной работы с двумя терминалами.
Само по себе это не революция.
Но в ежедневной работе такой режим удобен: в одной части экрана можно следить за журналом, а в другой проверять состояние сервиса.
Для меня интерфейс администратора должен в первую очередь экономить переключения между окнами.
Не создавать их.
История команд — отдельно от истории терминала
Ещё одна вещь, которой мне постоянно не хватало, — нормальная история собственной работы.
Да, у Bash есть .bash_history.
Но история оболочки и история действий SSH-клиента — разные сущности.
Одна и та же команда может выполняться на разных серверах. Иногда важно знать не только что было введено, но и где, когда и в какой сессии.
Поэтому в EvertySSH используется SQLite.
В базе есть три основные таблицы:
sessionscommand_historysnippets
Для сессий сохраняются адрес сервера, пользователь, время начала и завершения, состояние подключения и сообщение об ошибке.
Для команд — текст, время, сервер, принадлежность к сессии и отметка избранного.
Это даёт основу для поиска по истории и повторного использования команд.
Отдельно реализована палитра команд, доступная через Ctrl+R.
При этом я сознательно разделяю историю введённых команд и экранный буфер терминала. Полный поток терминального вывода и журнал действий пользователя не должны быть одной и той же структурой данных.
Есть, конечно, вопрос приватности: в командах иногда оказываются токены, пароли и другие секреты. Поэтому фильтрация таких данных — одна из важных задач дальнейшей доработки.
Сниппеты
Постоянно набираемые команды — ещё один источник мелкого раздражения.
Посмотреть контейнеры Docker, проверить порты, вывести нагрузку, открыть журналы systemd.
Каждый раз вспоминаешь синтаксис либо лезешь в историю команд.
В EvertySSH я добавил библиотеку сниппетов.
Сниппет содержит название, команду, категорию и описание.
Например, в базе есть такой вариант:
docker ps --format \'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
Или быстрый просмотр состояния системы:
uname -a && uptime && free -h && df -h
Можно создавать свои команды и группировать их по категориям.
При запуске сниппета предусмотрено подтверждение выполнения.
Особенно это важно для команд, которые меняют состояние системы.
Но здесь я не хочу превращать SSH-клиент в визуальный конструктор автоматизации.
Мне нужен обычный терминал с несколькими удобными инструментами рядом.
Генерация SSH-ключей
Отдельная история — авторизация.
В проекте предусмотрены работа с SSH Agent, паролями и ключами, а также генерация Ed25519 и RSA-4096.
В интерфейсе можно подготовить ключевую пару и организовать установку публичного ключа на удалённый сервер.
Идея простая: основные операции настройки доступа должны быть доступны из одного приложения.
Но это та часть, где удобство нельзя ставить выше корректной криптографии.
В текущем коде есть недоработка, которую я считаю необходимым исправить до рекомендации использовать клиент в чувствительных производственных средах.
Сейчас обработчик SSH принимает предъявленный сервером ключ без проверки по known_hosts.
Фактически это означает, что приложение пока не подтверждает подлинность сервера так, как должно.
Это не мелочь и не косметический недостаток: подобное поведение может создавать условия для атаки посредника.
Следующий шаг — нормальная проверка host key, сохранение доверенных ключей и явное предупреждение при их изменении.
Также требуют доработки хранение чувствительных параметров авторизации и механизм защиты мастер-паролем.
Я предпочитаю обозначить эти ограничения сразу, а не объявлять приложение безопасным только потому, что оно написано на Rust.
Rust защищает от целого класса ошибок памяти, но не исправляет ошибки в проектировании безопасности.
Почему мне важен именно EvertyUI
Можно задать вполне логичный вопрос: зачем было писать собственный UI-движок?
Ответ довольно простой.
Я не считаю, что миру срочно необходим ещё один универсальный интерфейсный фреймворк.
EvertyUI создаётся потому, что он нужен моим приложениям.
Когда интерфейс становится общим компонентом нескольких продуктов, его развитие начинает приносить практическую пользу.
Исправил обработку фокуса — изменение можно перенести в другие приложения.
Оптимизировал текстовый конвейер — выигрыш получает не один экран.
Доработал компоненты прокрутки или работу с DPI — не нужно заново решать эти задачи в следующем проекте.
При этом собственный UI-движок — это и дополнительная ответственность.
Теперь проблемы с вводом, компоновкой, рендерингом и платформенной совместимостью тоже мои.
Никто не исправит их за меня автоматически.
Но именно такого контроля я и добивался.
Вместо заключения
EvertySSH появился не потому, что мне захотелось доказать превосходство Rust над другими языками или написать убийцу PuTTY.
Просто в какой-то момент мне надоело подстраивать собственную работу под неудобные мелочи.
А поскольку у меня уже был EvertyUI, появилась возможность попробовать решить эту задачу самостоятельно.
В результате получился ещё один продукт и, что для меня даже интереснее, ещё одна практическая проверка собственного интерфейсного движка.
Можно бесконечно проектировать архитектуру, рисовать диаграммы и обсуждать абстракции.
Но настоящее испытание начинается тогда, когда на этой архитектуре появляется приложение, которым ты сам пользуешься.
EvertyUI нужен мне не для того, чтобы писать EvertyUI. Он нужен, чтобы писать программы.
Исходный код EvertySSH опубликован на GitHub:
https://github.com/vaalimusic/EvertySSH
Проект распространяется по лицензии MIT.
Буду рад техническим замечаниям, особенно по совместимости терминального эмулятора, обработке SSH-сессий и архитектуре интерфейсного слоя.
ссылка на оригинал статьи https://habr.com/ru/articles/1098070/