Ssh-chat +16


Привет, Хабр. 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
  • Добавить поддержку ботов
  • Отправка файлов по scp
  • Увеличим безопасность паролей(хэш и соль)

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




К сожалению, не доступен сервер mySQL