server-chat.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. // **BEFORE RUNNING THIS SCRIPT:**
  2. // 1. The server portion is best run on non-Windows systems because they have
  3. // terminfo databases which are needed to properly work with different
  4. // terminal types of client connections
  5. // 2. Install `blessed`: `npm install blessed`
  6. // 3. Create a server host key in this same directory and name it `host.key`
  7. var fs = require('fs');
  8. var blessed = require('blessed');
  9. var Server = require('ssh2').Server;
  10. var RE_SPECIAL = /[\x00-\x1F\x7F]+|(?:\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K])/g;
  11. var MAX_MSG_LEN = 128;
  12. var MAX_NAME_LEN = 10;
  13. var PROMPT_NAME = 'Enter a nickname to use (max ' + MAX_NAME_LEN + ' chars): ';
  14. var users = [];
  15. function formatMessage(msg, output) {
  16. var output = output;
  17. output.parseTags = true;
  18. msg = output._parseTags(msg);
  19. output.parseTags = false;
  20. return msg;
  21. }
  22. function userBroadcast(msg, source) {
  23. var sourceMsg = '> ' + msg;
  24. var name = '{cyan-fg}{bold}' + source.name + '{/}';
  25. msg = ': ' + msg;
  26. for (var i = 0; i < users.length; ++i) {
  27. var user = users[i];
  28. var output = user.output;
  29. if (source === user)
  30. output.add(sourceMsg);
  31. else
  32. output.add(formatMessage(name, output) + msg);
  33. }
  34. }
  35. function localMessage(msg, source) {
  36. var output = source.output;
  37. output.add(formatMessage(msg, output));
  38. }
  39. function noop(v) {}
  40. new Server({
  41. hostKeys: [fs.readFileSync('host.key')],
  42. }, function(client) {
  43. var stream;
  44. var name;
  45. client.on('authentication', function(ctx) {
  46. var nick = ctx.username;
  47. var prompt = PROMPT_NAME;
  48. var lowered;
  49. // Try to use username as nickname
  50. if (nick.length > 0 && nick.length <= MAX_NAME_LEN) {
  51. lowered = nick.toLowerCase();
  52. var ok = true;
  53. for (var i = 0; i < users.length; ++i) {
  54. if (users[i].name.toLowerCase() === lowered) {
  55. ok = false;
  56. prompt = 'That nickname is already in use.\n' + PROMPT_NAME;
  57. break;
  58. }
  59. }
  60. if (ok) {
  61. name = nick;
  62. return ctx.accept();
  63. }
  64. } else if (nick.length === 0)
  65. prompt = 'A nickname is required.\n' + PROMPT_NAME;
  66. else
  67. prompt = 'That nickname is too long.\n' + PROMPT_NAME;
  68. if (ctx.method !== 'keyboard-interactive')
  69. return ctx.reject(['keyboard-interactive']);
  70. ctx.prompt(prompt, function retryPrompt(answers) {
  71. if (answers.length === 0)
  72. return ctx.reject(['keyboard-interactive']);
  73. nick = answers[0];
  74. if (nick.length > MAX_NAME_LEN) {
  75. return ctx.prompt('That nickname is too long.\n' + PROMPT_NAME,
  76. retryPrompt);
  77. } else if (nick.length === 0) {
  78. return ctx.prompt('A nickname is required.\n' + PROMPT_NAME,
  79. retryPrompt);
  80. }
  81. lowered = nick.toLowerCase();
  82. for (var i = 0; i < users.length; ++i) {
  83. if (users[i].name.toLowerCase() === lowered) {
  84. return ctx.prompt('That nickname is already in use.\n' + PROMPT_NAME,
  85. retryPrompt);
  86. }
  87. }
  88. name = nick;
  89. ctx.accept();
  90. });
  91. }).on('ready', function() {
  92. var rows;
  93. var cols;
  94. var term;
  95. client.once('session', function(accept, reject) {
  96. accept().once('pty', function(accept, reject, info) {
  97. rows = info.rows;
  98. cols = info.cols;
  99. term = info.term;
  100. accept && accept();
  101. }).on('window-change', function(accept, reject, info) {
  102. rows = info.rows;
  103. cols = info.cols;
  104. if (stream) {
  105. stream.rows = rows;
  106. stream.columns = cols;
  107. stream.emit('resize');
  108. }
  109. accept && accept();
  110. }).once('shell', function(accept, reject) {
  111. stream = accept();
  112. users.push(stream);
  113. stream.name = name;
  114. stream.rows = rows || 24;
  115. stream.columns = cols || 80;
  116. stream.isTTY = true;
  117. stream.setRawMode = noop;
  118. stream.on('error', noop);
  119. var screen = new blessed.screen({
  120. autoPadding: true,
  121. smartCSR: true,
  122. program: new blessed.program({
  123. input: stream,
  124. output: stream
  125. }),
  126. terminal: term || 'ansi'
  127. });
  128. screen.title = 'SSH Chatting as ' + name;
  129. // Disable local echo
  130. screen.program.attr('invisible', true);
  131. var output = stream.output = new blessed.log({
  132. screen: screen,
  133. top: 0,
  134. left: 0,
  135. width: '100%',
  136. bottom: 2,
  137. scrollOnInput: true
  138. })
  139. screen.append(output);
  140. screen.append(new blessed.box({
  141. screen: screen,
  142. height: 1,
  143. bottom: 1,
  144. left: 0,
  145. width: '100%',
  146. type: 'line',
  147. ch: '='
  148. }));
  149. var input = new blessed.textbox({
  150. screen: screen,
  151. bottom: 0,
  152. height: 1,
  153. width: '100%',
  154. inputOnFocus: true
  155. });
  156. screen.append(input);
  157. input.focus();
  158. // Local greetings
  159. localMessage('{blue-bg}{white-fg}{bold}Welcome to SSH Chat!{/}\n'
  160. + 'There are {bold}'
  161. + (users.length - 1)
  162. + '{/} other user(s) connected.\n'
  163. + 'Type /quit or /exit to exit the chat.',
  164. stream);
  165. // Let everyone else know that this user just joined
  166. for (var i = 0; i < users.length; ++i) {
  167. var user = users[i];
  168. var output = user.output;
  169. if (user === stream)
  170. continue;
  171. output.add(formatMessage('{green-fg}*** {bold}', output)
  172. + name
  173. + formatMessage('{/bold} has joined the chat{/}', output));
  174. }
  175. screen.render();
  176. // XXX This fake resize event is needed for some terminals in order to
  177. // have everything display correctly
  178. screen.program.emit('resize');
  179. // Read a line of input from the user
  180. input.on('submit', function(line) {
  181. input.clearValue();
  182. screen.render();
  183. if (!input.focused)
  184. input.focus();
  185. line = line.replace(RE_SPECIAL, '').trim();
  186. if (line.length > MAX_MSG_LEN)
  187. line = line.substring(0, MAX_MSG_LEN);
  188. if (line.length > 0) {
  189. if (line === '/quit' || line === '/exit')
  190. stream.end();
  191. else
  192. userBroadcast(line, stream);
  193. }
  194. });
  195. });
  196. });
  197. }).on('end', function() {
  198. if (stream !== undefined) {
  199. spliceOne(users, users.indexOf(stream));
  200. // Let everyone else know that this user just left
  201. for (var i = 0; i < users.length; ++i) {
  202. var user = users[i];
  203. var output = user.output;
  204. output.add(formatMessage('{magenta-fg}*** {bold}', output)
  205. + name
  206. + formatMessage('{/bold} has left the chat{/}', output));
  207. }
  208. }
  209. }).on('error', function(err) {
  210. // Ignore errors
  211. });
  212. }).listen(0, function() {
  213. console.log('Listening on port ' + this.address().port);
  214. });
  215. function spliceOne(list, index) {
  216. for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1)
  217. list[i] = list[k];
  218. list.pop();
  219. }