UNPKG

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