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
Теперь сам код одна из самых важных частей это парсер сообщений:
https://github.com/maximmasterr/ssh-server/blob/master/parserExec.js
// Подключаем 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;
Форматирование:
https://github.com/maximmasterr/ssh-server/blob/master/format.js
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 сообщений:
https://github.com/maximmasterr/ssh-server/blob/master/broadcaster.js
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')
Лобби, создание сервера и авторизация
https://github.com/maximmasterr/ssh-server/blob/master/lobby.js
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;
};
Различные методы:
https://github.com/maximmasterr/ssh-server/blob/master/utils.js
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()
}
}
А теперь объединим
https://github.com/maximmasterr/ssh-server/blob/master/index.js
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
Финальный репозиторий