Ssh-chat

    Привет, Хабр. Console chat отличная вещь, но для фронтендеров, а что если вы хотите такой же, но для бэкэнда. Если да, то эта статья для вас. Но какой инструмент часто используют в бэкенде? Правильно ssh, так что представляю sshchat.


    Как это будет выглядеть


    Где-то на сервере крутится программа на ноде.
    Как только кто-то хочет, подключится к чату он вводит:


    ssh server -p 8022

    После этого система спрашивает пароль и сверяет его с паролем в специальном файле. Если пароль совпал, то подключаем к чату(юзер получает 100 предыдущих сообщений и все остальные видят что он подключился).


    Дальше он принимает сообщения других, и может написать своё.


    Вот с сообщениями поинтереснее:


    @box{@color(red){Red text in box}}

    Отправит красный текст в коробке.


    Приступим


    Для работы с ssh мы будем использовать https://www.npmjs.com/package/ssh2.
    Для форматирования используем chalk и boxen.
    Так что установим их:


    npm i ssh2 chalk boxen

    Теперь сам код одна из самых важных частей это парсер сообщений (GitHub):


    // Подключаем chalk и boxen
    const chalk = require('chalk');
    const boxen = require('boxen');
    
    // Здесь прописаны методы которые мы сможем использовать через @
    // Функции принимают 2 аргумента то что в скобках и текс в фигурных скобках
    let methods = {
      color: function(args, text) {
        return chalk.keyword(args)(text);
      },
    
      bold: function(args, text) {
        return chalk.bold(text);
      },
    
      underline: function(args, text) {
        return chalk.underline(text);
      },
    
      hex: function(args, text) {
        return chalk.hex(args)(text);
      },
    
      box: function(args, text) {
        return boxen(text, {
          borderStyle: 'round',
          padding: 1,
          borderColor: 'blueBright'
        });
      }
    };
    
    // Сам парсер 
    function parseAndExecute(str) {
      let pos = 0;
      let stage = 0;
      let nS = '';
      let bufs = ['', '', '', ''];
      let level = 0;
    
      while (pos < str.length) {
        let symbol = str[pos];
        pos++;
    
        if (symbol == '\\' && '(){}@'.indexOf(str[pos]) !== -1) {
          bufs[stage] += str[pos];
          pos++;
          continue;
        }
    
        if (stage == 0 && symbol == '@') {
          stage++;
          nS += bufs[0];
          bufs[0] = '';
          continue;
        } else if (stage >= 1) {
          if (symbol == '(')
            if (stage < 2) {
              stage = 2;
            } else {
              level++;
            }
    
          if (symbol == ')' && stage >= 2 && level > 0) level--;
    
          if (symbol == '{')
            if (stage != 3) {
              stage = 3;
            } else {
              level++;
            }
    
          if (symbol == '}') {
            if (level == 0) {
              bufs[3] += '}';
    
              nS += methods[bufs[1]](bufs[2].slice(1, -1), parseAndExecute(bufs[3].slice(1, -1)));
    
              bufs = ['', '', '', ''];
              stage = 0;
              continue;
            } else {
              level--;
            }
          }
        }
        bufs[stage] += symbol;
      }
      return nS + bufs[0];
    }
    
    module.exports.parseAndExecute = parseAndExecute;

    Форматирование (GitHub):


    const chalk = require('chalk');
    const { parseAndExecute } = require('./parserExec')
    
    // Стилизуем ник(Генерируем цвет и делаем жирным)
    function getNick(nick) {
      let hash = 0;
      for (var i = 0; i < nick.length; i++) hash += nick.charCodeAt(i) - 32;
    
      return chalk.hsv((hash + 160) % 360, 90, 90)(chalk.bold(nick));
    }
    
    module.exports.format = function(nick, message) {
      const nickSpace = '\r  ' + ' '.repeat(nick.length);
      nick = getNick(nick) + ': ';
    
      message = message.replace(/\\n/gm, '\n'); // Заменяем \n новыми строками
      message = parseAndExecute(message) // Парсим
    
      // Добавлям к каждой новой строке отступ
      message = message
        .split('\n')
        .map((e, i) => '' + (i !== 0 ? nickSpace : '') + e)
        .join('\n');
    
      return nick + message;
    };

    Методы для отправки сообщения всем пользователям и сохранения 100 сообщений (GitHub):


    let listeners = []; // Все пользователи
    let cache = new Array(100).fill('') // Кэш 
    
    // Добавления и удаление подписчиков
    module.exports.addListener = write => listeners.push(write) - 1;
    module.exports.delListener = id => listeners.splice(id, 1);
    
    // Отправляем сообщение
    module.exports.broadcast = msg => {
    
      cache.shift()
      cache.push(msg)
      process.stdout.write(msg)
      listeners.forEach(wr => wr(msg));
    }
    
    // Получаем кэш
    module.exports.getCache = ()=>cache.join('\r\033[1K')

    Лобби, создание сервера и авторизация (GitHub):


    const { Server } = require('ssh2');
    const { readFileSync } = require('fs');
    
    const hostKey = readFileSync('./ssh'); // Читаем ключ
    const users = JSON.parse(readFileSync('./users.json')); // Юзеры
    
    let connectionCallback = () => {};
    
    module.exports.createServer = function createServer({ lobby }) {
      // Создаём сервер
      const server = new Server(
        {
          banner: lobby, // Баннер встречает до ввода пароля
          hostKeys: [hostKey]
        },
        function(client) {
          nick = '';
          client
            .on('authentication', ctx => {  // Авторизация
              if (ctx.method !== 'password') return ctx.reject();
              if (ctx.password !== users[ctx.username]) ctx.reject();
              nick = ctx.username;
              ctx.accept();
            })
            .on('ready', function() {
              connectionCallback(client, nick);
            });
        }
      );
    
      return server
    };
    
    module.exports.setConnectCallback = callback => { // Устанавливает колбэк при подключении
      connectionCallback = callback;
    };

    Различные методы (GitHub):


    const { createInterface } = require('readline');
    
    module.exports.getStream = function(client, onStream, onEnd){
      client  // Получает стрим и клиента
        .on('session', function(accept, reject) {
          accept()
            .on('pty', accept => accept & accept())
            .on('shell', accept => onStream(accept()));
        })
        .on('end', () => onEnd());
    }
    
    // Создаём коммуникатор 
    module.exports.getCommunicator = function(stream, onMessage, onEnd){
    
      let readline = createInterface({ // Интерфейс для считывания строк
        input: stream,
        output: stream,
        prompt: '> ',
        historySize: 0,
        terminal: true
      })
      readline.prompt()
    
      readline.on('close', ()=>{
        radline = null;
        onEnd()
        stream.end()
      })
    
      readline.on('line', (msg)=>{
        stream.write('\033[s\033[1A\033[1K\r')
        onMessage(msg)
        readline.prompt()
      })
    
      // Метод для записи сообщения
      return msg=>{
        stream.write('\033[1K\r' + msg)
        readline.prompt()
      }
    }

    А теперь объединим (GitHub):


    const { createServer, setConnectCallback } = require('./lobby');
    const { getStream, getCommunicator } = require('./utils');
    const { addListener, delListener, broadcast, getCache } = require('./broadcaster');
    const { format, getNick } = require('./format');
    
    //  Функция создания сервера 
    module.exports = function({ lobby = 'Hi' } = {}) {
      const server = createServer({
        lobby
      });
    
      setConnectCallback((client, nick) => { // Ожидание соединения
        console.log('Client authenticated!');
        let id = null;
        getStream( // Получаем стрим
          client,
          stream => {
            const write = getCommunicator( // И интерфейс
              stream,
              msg => {
                if (msg == '') return;
                try {
                  broadcast(format(nick, msg) + '\n'); // Как только получим сообщение, отправим его всем
                } catch (e) {}
              },
              () => {}
            );
    
            id = addListener(write); // Слушаем сообщения
            write('\033c' + getCache()); // Отправляем кэш
            broadcast(getNick(nick) + ' connected\n'); // Сообщаем о подключении
          },
          () => {
    
            delListener(id);
            broadcast(getNick(nick) + ' disconnected\n') // Сообщаем об отключении
          }
        );
      });
    
      server.listen(8022);
    };

    И финальный этап пример сервера:


    const chat = require('.')
    
    chat({})

    Так же в файле users.json описаны юзеры и их пароли.


    Выводы


    Вот так можно написать не самый простой чат в ssh.
    Для такого чата не нужно писать клиент, он обладает возможностями оформления, и его может развернуть любой желающий.


    Что ещё можно сделать:


    • Добавить возможность создания своих функций оформления
    • Добавить поддержку markdown
    • Добавить поддержку ботов
    • Увеличим безопасность паролей(хэш и соль)

    Финальный репозиторий

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    Стоит ли продолжить

    • 57.5%Да95
    • 42.4%Нет70
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 41

      0

      Такую же функциональность наверное можно получить с помощью мультиплексора терминала и wall(1) ?

        0

        Может быть, но статья про возможности ssh в nodejs

          0
          Возможности не раскрыты. Например, где обмен файлами между юзерами через SCP? :)
            +1

            Ок, во второй части (если она будет) добавлю возможность отправки файлов

          0
          Да банально зайти в одну сессию в tmux. будете видеть вводимые команды друг друга
          +3
          Во введении имело смысл сказать о существовании команды write в unix/linux
          man write
          write — send a message to another user…
            0
            Ок, исправил
            +1
            Опять js… фу.
            Вот уже есть на go: github.com/shazow/ssh-chat пользуйтесь.
              +2
              Язык программирования всего лишь инструмент, так что писать нужно на том на чём умеешь лучше всего
                0
                Не согласен.

                Некоторые языки лучше годятся для определенных задач. У Go горутины и «автоматический» M:N, что делает его подходящим для написания сетевых программ. В тех же условиях, например, Python будет сильно проигрывать, потому что из-за проблемы раскраски функций далеко не все протоколы реализованы в asyncio-варианте. А на тредах/форках хорошо масштабирующееся приложение не напишешь.
              +1
              После этого система спрашивает пароль и сверяет его с паролем в специальном файле.
              Звучит небезопасно.
                –2
                Может быть, зато просто. Конечно можно прикрепить БД но код увеличиться.
                  0
                  Может быть, зато просто

                  Т.е. вы делаете защищенный ssh-чат, а потом говорите, что надо делать не безопасно, а просто? И база вам не поможет, если там как, например, на cyberforum пароле в плейнтексте.
                    0
                    Я ничего не говорил про безопасность, я говорил про чат в терминале не требующий инсталяции.
                    Код открытый, если вам нужна безопасность сделайте форк и увеличьте безопасность
                      0
                      Безопасность у вас в заголовке:
                      SSH provides a secure channel over an unsecured network in a client–server architecture, connecting an SSH client application with an SSH server.
                        0
                        Secure channel
                          0
                          И? SSH используется для скрытия данных от посторонних глаз. Если вы что-то основываете на ssh то вы автоматически обосновываете чат как безопасный.
                          Если и в мыслях не было, то почему, например, клиентом не было сделать telnet? Тоже не требует установки и в каждой кофеварке есть.
                            0
                            Ssh первое что пришло в голову, но если хотите то в продолжение статьи увеличу безопасность по вашим советам, предлагайте
                              +1
                              Безопасность нужно увеличивать не по советам анонима с хабра, а по умным статьям, книгам и гайдам. А то выглядит как будто вы вообще ничего не зная про сетевую безопасность пошли писать чат.

                              В первом приближении: nakedsecurity.sophos.com/2013/11/20/serious-security-how-to-store-your-users-passwords-safely
                                –2
                                Ок, добавил в список планов хэш и соль
                                  +2

                                  В ssh есть своя надёжная система авторизации по ключам. Зачем городить велосипеды?

                                    –1
                                    Именно! А ещё можно развертывать сервер в докер-контейнере и дать возможность регистрироваться пользователям из-под гостевого аккаунта. Предусмотреть уничтожение аккаунтов, которые очень давно не заходили.
                                    +1
                                    Вы не думали что цель статьи рассказать об самой идеи и простой реализации? Он же не enterprise решение продает вам.
                                  +2
                                  Если вы что-то основываете на ssh то вы автоматически обосновываете чат как безопасный.

                                  В какой модели угроз?
                          0

                          СУБД — ну вообще не про ту безопасность, которую обычно называют этим словом без пояснений. Пока у вас таблица умеренных размеров и постоянная на всё время работы приложения — всё правильно делаете, записать в json и не усложнять. Как только в приложении появляется кнопка добавления пользователя — надо не изобретать велосипед, а подключать обычную СУБД.

                            0

                            Добавьте возможность в том же самом файле хранить ключи вместо паролей — это будет всё так же просто, но безопасность значительно увеличится.

                            –1

                            Ой да ладно, пароль в текстовом файле, я как представлю сколько там RCE в любую сторону...

                              +1

                              А RCE-то откуда? Код слишком простой, так накосячить тут попросту негде.

                                –1
                                Криптография, левые библиотеки nodejs с кривыми биндингами, сам nodejs, управляющие последовательности в терминале, люди которые ходят с ssh-agent по-дефолту.
                                  +1
                                  Криптография

                                  OpenSSL может быть сколько угодно кривым, но лучше всё равно не придумано.


                                  левые библиотеки nodejs с кривыми биндингами

                                  Найдите хоть одну библиотеку с биндингами в предложенном решении.


                                  сам nodejs

                                  И много RCE вы в ноде знаете?


                                  управляющие последовательности в терминале

                                  А они-то каким боком могут RCE устроить?


                                  люди которые ходят с ssh-agent по-дефолту

                                  И что дальше?

                                    –2
                                    OpenSSL

                                    И что дальше? Зачем вообще тащить криптографию в чат сервер написанный на коленке?
                                    Найдите хоть одну библиотеку с биндингами в предложенном решении.

                                    Лучше бы она была, жирная реализация ssh на pure js — ещё больший скоуп для уязвимостей :).
                                    И много RCE вы в ноде знаете?

                                    Правильный вопрос будет — о скольких RCE в ноде я не знаю.
                                    А они-то каким боком могут RCE устроить?

                                    Ну вот тем, что могут :). Их можно использовать для формирования произвольного текста на ввод в терминале. Не везде это отключено/исправлено.
                                    И что дальше?

                                    RCE на серверах куда у этих людей есть доступ.
                                    0
                                    Ssh2 не такая уж и левая библиотека, nodejs используется даже крупные компании, так что rce будут находиться и патчиться
                              +1

                              Старая добрая ирка (irc) на новый лад.

                                0
                                Хм… продумать архитектуру, наверное «модульную»,
                                разработать-стандартизировать-описать протокол обмена данными…
                                перевести на TypeScript…
                                и (имхо) может получиться очень интересно и с «низким порогом» входа в совместную разработку.
                                  0
                                  Я вот думаю ещё одну статью сделать 'hello world' по максимуму. Там будут все инструменты которые я использую при разработке
                                    0
                                    И докер-контейнер нужен обязательно для сервака на случай если кто-то захочет поиграться.
                                      0
                                      Зачем? Года очень просто развёртывается, да и можно собрать бинарник
                                  0
                                  А что если правда сделать докер-файл с настроенным sshd и пользователем guest:guest. У этого пользователя зарублены все права кроме возможности выполнения одного единственного скрипта сразу при входе — скрипта регистрации нового пользователя. Этот скрипт в консольном режиме принимает желаемое имя пользователя и пароль (или ключ). После регистрации соединение рвётся. Далее происходит коннект на тот же сервер но уже по зарегистрированным реквизитам.
                                  При коннекте происходит подключение к буферизированному пайпу, а весь ввод построчно прогоняется через парсилку и оформлялку, которая кроме прочего дописывает юзернейм и дату-время в каждый пост.
                                  Можно даже чуточку отступить от идеала и сделать в тот же контейнер еще и крон-скрипт с вытесняющим автоудалением сильно старых пользователей. Грубо говоря, если пользователей, скажем, больше 1000, то удаляем всех самых наиболее давно не заходивших, пока не останется 1000.
                                    +1
                                    Конечно, можно написать на чём угодно и как угодно
                                    0
                                    Йииии-хааа… изысканый способ побольней наступить на грабли
                                      –1

                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                      Самое читаемое