Привет, Хабр. 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
- Увеличим безопасность паролей(хэш и соль)
Комментарии (40)
apaskal
08.09.2019 20:50+1Во введении имело смысл сказать о существовании команды write в unix/linux
man write
write — send a message to another user…
Gurturok
08.09.2019 21:19+3Опять js… фу.
Вот уже есть на go: github.com/shazow/ssh-chat пользуйтесь.maximmasterr Автор
08.09.2019 21:21+1Язык программирования всего лишь инструмент, так что писать нужно на том на чём умеешь лучше всего
WGH
10.09.2019 14:05Не согласен.
Некоторые языки лучше годятся для определенных задач. У Go горутины и «автоматический» M:N, что делает его подходящим для написания сетевых программ. В тех же условиях, например, Python будет сильно проигрывать, потому что из-за проблемы раскраски функций далеко не все протоколы реализованы в asyncio-варианте. А на тредах/форках хорошо масштабирующееся приложение не напишешь.
staticmain
08.09.2019 21:25После этого система спрашивает пароль и сверяет его с паролем в специальном файле.
Звучит небезопасно.maximmasterr Автор
08.09.2019 21:29-1Может быть, зато просто. Конечно можно прикрепить БД но код увеличиться.
staticmain
08.09.2019 21:31-1Может быть, зато просто
Т.е. вы делаете защищенный ssh-чат, а потом говорите, что надо делать не безопасно, а просто? И база вам не поможет, если там как, например, на cyberforum пароле в плейнтексте.maximmasterr Автор
08.09.2019 21:35+1Я ничего не говорил про безопасность, я говорил про чат в терминале не требующий инсталяции.
Код открытый, если вам нужна безопасность сделайте форк и увеличьте безопасностьstaticmain
08.09.2019 21:36-1Безопасность у вас в заголовке:
SSH provides a secure channel over an unsecured network in a client–server architecture, connecting an SSH client application with an SSH server.
maximmasterr Автор
08.09.2019 21:37Secure channel
staticmain
08.09.2019 21:39-1И? SSH используется для скрытия данных от посторонних глаз. Если вы что-то основываете на ssh то вы автоматически обосновываете чат как безопасный.
Если и в мыслях не было, то почему, например, клиентом не было сделать telnet? Тоже не требует установки и в каждой кофеварке есть.maximmasterr Автор
08.09.2019 21:43Ssh первое что пришло в голову, но если хотите то в продолжение статьи увеличу безопасность по вашим советам, предлагайте
staticmain
08.09.2019 21:46Безопасность нужно увеличивать не по советам анонима с хабра, а по умным статьям, книгам и гайдам. А то выглядит как будто вы вообще ничего не зная про сетевую безопасность пошли писать чат.
В первом приближении: nakedsecurity.sophos.com/2013/11/20/serious-security-how-to-store-your-users-passwords-safelytitulusdesiderio
09.09.2019 09:59+1В ssh есть своя надёжная система авторизации по ключам. Зачем городить велосипеды?
trapwalker
09.09.2019 14:50-1Именно! А ещё можно развертывать сервер в докер-контейнере и дать возможность регистрироваться пользователям из-под гостевого аккаунта. Предусмотреть уничтожение аккаунтов, которые очень давно не заходили.
qWici
11.09.2019 17:16Вы не думали что цель статьи рассказать об самой идеи и простой реализации? Он же не enterprise решение продает вам.
AC130
09.09.2019 00:27Если вы что-то основываете на ssh то вы автоматически обосновываете чат как безопасный.
В какой модели угроз?
salas
09.09.2019 08:54СУБД — ну вообще не про ту безопасность, которую обычно называют этим словом без пояснений. Пока у вас таблица умеренных размеров и постоянная на всё время работы приложения — всё правильно делаете, записать в json и не усложнять. Как только в приложении появляется кнопка добавления пользователя — надо не изобретать велосипед, а подключать обычную СУБД.
mayorovp
09.09.2019 10:48Добавьте возможность в том же самом файле хранить ключи вместо паролей — это будет всё так же просто, но безопасность значительно увеличится.
eigrad
10.09.2019 11:24-1Ой да ладно, пароль в текстовом файле, я как представлю сколько там RCE в любую сторону...
mayorovp
10.09.2019 11:30А RCE-то откуда? Код слишком простой, так накосячить тут попросту негде.
eigrad
10.09.2019 13:20Криптография, левые библиотеки nodejs с кривыми биндингами, сам nodejs, управляющие последовательности в терминале, люди которые ходят с ssh-agent по-дефолту.
mayorovp
10.09.2019 13:29Криптография
OpenSSL может быть сколько угодно кривым, но лучше всё равно не придумано.
левые библиотеки nodejs с кривыми биндингами
Найдите хоть одну библиотеку с биндингами в предложенном решении.
сам nodejs
И много RCE вы в ноде знаете?
управляющие последовательности в терминале
А они-то каким боком могут RCE устроить?
люди которые ходят с ssh-agent по-дефолту
И что дальше?
eigrad
10.09.2019 14:03-1OpenSSL
И что дальше? Зачем вообще тащить криптографию в чат сервер написанный на коленке?
Найдите хоть одну библиотеку с биндингами в предложенном решении.
Лучше бы она была, жирная реализация ssh на pure js — ещё больший скоуп для уязвимостей :).
И много RCE вы в ноде знаете?
Правильный вопрос будет — о скольких RCE в ноде я не знаю.
А они-то каким боком могут RCE устроить?
Ну вот тем, что могут :). Их можно использовать для формирования произвольного текста на ввод в терминале. Не везде это отключено/исправлено.
И что дальше?
RCE на серверах куда у этих людей есть доступ.
maximmasterr Автор
10.09.2019 15:39Ssh2 не такая уж и левая библиотека, nodejs используется даже крупные компании, так что rce будут находиться и патчиться
orion76
09.09.2019 06:09Хм… продумать архитектуру, наверное «модульную»,
разработать-стандартизировать-описать протокол обмена данными…
перевести на TypeScript…
и (имхо) может получиться очень интересно и с «низким порогом» входа в совместную разработку.maximmasterr Автор
09.09.2019 07:12Я вот думаю ещё одну статью сделать 'hello world' по максимуму. Там будут все инструменты которые я использую при разработке
trapwalker
09.09.2019 14:52-1И докер-контейнер нужен обязательно для сервака на случай если кто-то захочет поиграться.
maximmasterr Автор
09.09.2019 15:37Зачем? Года очень просто развёртывается, да и можно собрать бинарник
trapwalker
09.09.2019 15:03-1А что если правда сделать докер-файл с настроенным sshd и пользователем guest:guest. У этого пользователя зарублены все права кроме возможности выполнения одного единственного скрипта сразу при входе — скрипта регистрации нового пользователя. Этот скрипт в консольном режиме принимает желаемое имя пользователя и пароль (или ключ). После регистрации соединение рвётся. Далее происходит коннект на тот же сервер но уже по зарегистрированным реквизитам.
При коннекте происходит подключение к буферизированному пайпу, а весь ввод построчно прогоняется через парсилку и оформлялку, которая кроме прочего дописывает юзернейм и дату-время в каждый пост.
Можно даже чуточку отступить от идеала и сделать в тот же контейнер еще и крон-скрипт с вытесняющим автоудалением сильно старых пользователей. Грубо говоря, если пользователей, скажем, больше 1000, то удаляем всех самых наиболее давно не заходивших, пока не останется 1000.
apro
Такую же функциональность наверное можно получить с помощью мультиплексора терминала и wall(1) ?
maximmasterr Автор
Может быть, но статья про возможности ssh в nodejs
DjPhoeniX
Возможности не раскрыты. Например, где обмен файлами между юзерами через SCP? :)
maximmasterr Автор
Ок, во второй части (если она будет) добавлю возможность отправки файлов
MasMaX
Да банально зайти в одну сессию в tmux. будете видеть вводимые команды друг друга