123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- // **BEFORE RUNNING THIS SCRIPT:**
- // 1. The server portion is best run on non-Windows systems because they have
- // terminfo databases which are needed to properly work with different
- // terminal types of client connections
- // 2. Install `blessed`: `npm install blessed`
- // 3. Create a server host key in this same directory and name it `host.key`
- var fs = require('fs');
- var blessed = require('blessed');
- var Server = require('ssh2').Server;
- var RE_SPECIAL = /[\x00-\x1F\x7F]+|(?:\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K])/g;
- var MAX_MSG_LEN = 128;
- var MAX_NAME_LEN = 10;
- var PROMPT_NAME = 'Enter a nickname to use (max ' + MAX_NAME_LEN + ' chars): ';
- var users = [];
- function formatMessage(msg, output) {
- var output = output;
- output.parseTags = true;
- msg = output._parseTags(msg);
- output.parseTags = false;
- return msg;
- }
- function userBroadcast(msg, source) {
- var sourceMsg = '> ' + msg;
- var name = '{cyan-fg}{bold}' + source.name + '{/}';
- msg = ': ' + msg;
- for (var i = 0; i < users.length; ++i) {
- var user = users[i];
- var output = user.output;
- if (source === user)
- output.add(sourceMsg);
- else
- output.add(formatMessage(name, output) + msg);
- }
- }
- function localMessage(msg, source) {
- var output = source.output;
- output.add(formatMessage(msg, output));
- }
- function noop(v) {}
- new Server({
- hostKeys: [fs.readFileSync('host.key')],
- }, function(client) {
- var stream;
- var name;
- client.on('authentication', function(ctx) {
- var nick = ctx.username;
- var prompt = PROMPT_NAME;
- var lowered;
- // Try to use username as nickname
- if (nick.length > 0 && nick.length <= MAX_NAME_LEN) {
- lowered = nick.toLowerCase();
- var ok = true;
- for (var i = 0; i < users.length; ++i) {
- if (users[i].name.toLowerCase() === lowered) {
- ok = false;
- prompt = 'That nickname is already in use.\n' + PROMPT_NAME;
- break;
- }
- }
- if (ok) {
- name = nick;
- return ctx.accept();
- }
- } else if (nick.length === 0)
- prompt = 'A nickname is required.\n' + PROMPT_NAME;
- else
- prompt = 'That nickname is too long.\n' + PROMPT_NAME;
- if (ctx.method !== 'keyboard-interactive')
- return ctx.reject(['keyboard-interactive']);
- ctx.prompt(prompt, function retryPrompt(answers) {
- if (answers.length === 0)
- return ctx.reject(['keyboard-interactive']);
- nick = answers[0];
- if (nick.length > MAX_NAME_LEN) {
- return ctx.prompt('That nickname is too long.\n' + PROMPT_NAME,
- retryPrompt);
- } else if (nick.length === 0) {
- return ctx.prompt('A nickname is required.\n' + PROMPT_NAME,
- retryPrompt);
- }
- lowered = nick.toLowerCase();
- for (var i = 0; i < users.length; ++i) {
- if (users[i].name.toLowerCase() === lowered) {
- return ctx.prompt('That nickname is already in use.\n' + PROMPT_NAME,
- retryPrompt);
- }
- }
- name = nick;
- ctx.accept();
- });
- }).on('ready', function() {
- var rows;
- var cols;
- var term;
- client.once('session', function(accept, reject) {
- accept().once('pty', function(accept, reject, info) {
- rows = info.rows;
- cols = info.cols;
- term = info.term;
- accept && accept();
- }).on('window-change', function(accept, reject, info) {
- rows = info.rows;
- cols = info.cols;
- if (stream) {
- stream.rows = rows;
- stream.columns = cols;
- stream.emit('resize');
- }
- accept && accept();
- }).once('shell', function(accept, reject) {
- stream = accept();
- users.push(stream);
- stream.name = name;
- stream.rows = rows || 24;
- stream.columns = cols || 80;
- stream.isTTY = true;
- stream.setRawMode = noop;
- stream.on('error', noop);
- var screen = new blessed.screen({
- autoPadding: true,
- smartCSR: true,
- program: new blessed.program({
- input: stream,
- output: stream
- }),
- terminal: term || 'ansi'
- });
- screen.title = 'SSH Chatting as ' + name;
- // Disable local echo
- screen.program.attr('invisible', true);
- var output = stream.output = new blessed.log({
- screen: screen,
- top: 0,
- left: 0,
- width: '100%',
- bottom: 2,
- scrollOnInput: true
- })
- screen.append(output);
- screen.append(new blessed.box({
- screen: screen,
- height: 1,
- bottom: 1,
- left: 0,
- width: '100%',
- type: 'line',
- ch: '='
- }));
- var input = new blessed.textbox({
- screen: screen,
- bottom: 0,
- height: 1,
- width: '100%',
- inputOnFocus: true
- });
- screen.append(input);
- input.focus();
- // Local greetings
- localMessage('{blue-bg}{white-fg}{bold}Welcome to SSH Chat!{/}\n'
- + 'There are {bold}'
- + (users.length - 1)
- + '{/} other user(s) connected.\n'
- + 'Type /quit or /exit to exit the chat.',
- stream);
- // Let everyone else know that this user just joined
- for (var i = 0; i < users.length; ++i) {
- var user = users[i];
- var output = user.output;
- if (user === stream)
- continue;
- output.add(formatMessage('{green-fg}*** {bold}', output)
- + name
- + formatMessage('{/bold} has joined the chat{/}', output));
- }
- screen.render();
- // XXX This fake resize event is needed for some terminals in order to
- // have everything display correctly
- screen.program.emit('resize');
- // Read a line of input from the user
- input.on('submit', function(line) {
- input.clearValue();
- screen.render();
- if (!input.focused)
- input.focus();
- line = line.replace(RE_SPECIAL, '').trim();
- if (line.length > MAX_MSG_LEN)
- line = line.substring(0, MAX_MSG_LEN);
- if (line.length > 0) {
- if (line === '/quit' || line === '/exit')
- stream.end();
- else
- userBroadcast(line, stream);
- }
- });
- });
- });
- }).on('end', function() {
- if (stream !== undefined) {
- spliceOne(users, users.indexOf(stream));
- // Let everyone else know that this user just left
- for (var i = 0; i < users.length; ++i) {
- var user = users[i];
- var output = user.output;
- output.add(formatMessage('{magenta-fg}*** {bold}', output)
- + name
- + formatMessage('{/bold} has left the chat{/}', output));
- }
- }
- }).on('error', function(err) {
- // Ignore errors
- });
- }).listen(0, function() {
- console.log('Listening on port ' + this.address().port);
- });
- function spliceOne(list, index) {
- for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1)
- list[i] = list[k];
- list.pop();
- }
|