Создать ssh-сервер на Go можно при помощи модуля golang.org/x/crypto/ssh.
А при помощи пакета github.com/gliderlabs/ssh можно разработать ssh-сервер легко и быстро. Ssh подразумевает не только доступ к оболочке(shell), но и прочие возможности: файловый сервер(sftp), проброс портов.
Репозиторий проекта содержит минимальный пример, выводящий строку "Hello world" любому подключенному ssh-клиенту.
package main import ( "github.com/gliderlabs/ssh" "io" "log" ) func main() { ssh.Handle(func(s ssh.Session) { io.WriteString(s, "Hello world\n") }) log.Fatal(ssh.ListenAndServe(":2222", nil)) }
Терминал
Полноценный терминальный эмулятор можно реализовать при помощи модуля golang.org/x/term.
Упрощенно обработчик будет выглядеть вот так:
import ( ... terminal "golang.org/x/term" ) func sessionHandler(s gssh.Session) { defer s.Close() if s.RawCommand() != "" { io.WriteString(s, "raw commands are not supported") return } // создаем терминал term := terminal.NewTerminal(s, fmt.Sprintf("/%s/ > ", s.User())) // добавляем обработку pty-request pty, winCh, isPty := s.Pty() if isPty { _ = pty go func() { // реагируем на изменение размеров терминала for chInfo := range winCh { _ = term.SetSize(chInfo.Width, chInfo.Height) } }() } for { // считываем ввод пользователя line, err := term.ReadLine() if err == io.EOF { _, _ = io.WriteString(s, "EOF.\n") break } // обработаем результат result = processInput(line) // выведем в терминал io.WriteString(term, result) } }
Обрабатываем команды, пакет flag
Пользователь может вводить команды и их необходимо обработать и исполнить. По-началу я интегрировал github.com/spf13/cobra, но что-то пошло не так - повторный запуск rootCmd.Execute() приводил к неожиданным результатам и ошибкам. После коротких раздумий от cobra решено было отказаться и обойтись средствами попроще.
Можно воспользоваться стандартным пакетом flag, предварительно обработав пользовательский ввод лексером github.com/google/shlex:
import ( ... "github.com/google/shlex" ) ...SNIP.. // скармливаем лексеру args, err := shlex.Split(line) if err != nil { io.WriteString(term, fmt.Errorf("splitting args: %w\n", err).Error()) continue } if len(args) == 0 { continue } cmdName := args[0] args = args[1:] .. // теперь парсим флаги flagCmd := flag.NewFlagSet("foo", flag.ContinueOnError) enableP := flagCmd.Bool("enable", false, "enable") nameP := flagCmd.String("name", "", "name") flagCmd.SetOutput(term) err := flagCmd.Parse(args) if err != nil { return fmt.Errorf("error parsing flags: %w", err) } // и выполнение нужных действий io.WriteString(term, fmt.Sprintf("parsed flags:\n")) io.WriteString(term, fmt.Sprintf(" - enable: \t%v\n", *enableP)) io.WriteString(term, fmt.Sprintf(" - name: \t%v\n", *nameP)) io.WriteString(term, fmt.Sprintf(" - tail: \t%v\n", flagCmd.Args())) ...SNIP..
Добавляем аутентификацию
Было бы глупо игнорировать такую тему как аутентификция пользователя ssh-сессии. Очень заманчива идея в бекенд сервисе дать пользователю возможность управлять разрешенными публичными ключами.
Пример обработчика аутентификации по публичному ключу. Пример простой - проверяется совпадение публичного ключа сессии и ключа из файла. Имя пользователя не проверяется никак, хотя его можно получить вызовом ctx.User().
import ( ... gssh "github.com/gliderlabs/ssh" ) func pubkeyAuth(ctx gssh.Context, key gssh.PublicKey) bool { mykey, err := os.ReadFile("./mykey.pub") if err != nil { log.Printf("reading file: %w\n", err) return false } pk, _, _, _, err := gssh.ParseAuthorizedKey(mykey) if err != nil { log.Printf("parse auth key: %\n", err) return false } if !bytes.Equal(key.Marshal(), pk.Marshal()) { return false } return true }
Аналогично можно сделать и аутентификацию по паролю.
Все вместе
Соединяя все вместе получаем следующее: ssh-server с аутентификацией по паролю и по публичному ключу, с поддержкой pty-терминала и обработкой команд стандартным пакетом flag.
Исходный код проекта: https://github.com/shabunin/sshebra
Проект маленький - всего три go файла:
sshebra/sshebra.go - обработчик ssh-сессии и парсер команд
commands/command.go - определение интерфейса и примеры команд
main.go - пример использования
В примере используется три команды: whoami, flag и exit.
Прочие проекты
Вдохновили меня на работу с ssh следующие чаты поверх ssh:
И недавно я наткнулся на проект https://github.com/charmbracelet/wish. Проверить его возможности и посмотреть на терминальный интерфейс git-сервера можно прямо из своего терминала: ssh git.charm.sh.
Прочие возможности ssh
Текстовый интерфейс может быть и удобным и красивым. Однако любим мы ssh за гораздо более полезные вещи: пробросы портов, трансфер файлов. Для данной статьи материала по ним не хватило. Примеры же можно посмотреть здесь.
