forked from mscdex/ssh2
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathserver-chat.js
238 lines (214 loc) · 6.84 KB
/
server-chat.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
// **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`
'use strict';
const { readFileSync } = require('fs');
const blessed = require('blessed');
const { Server } = require('ssh2');
const RE_SPECIAL =
// eslint-disable-next-line no-control-regex
/[\x00-\x1F\x7F]+|(?:\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K])/g;
const MAX_MSG_LEN = 128;
const MAX_NAME_LEN = 10;
const PROMPT_NAME = `Enter a nickname to use (max ${MAX_NAME_LEN} chars): `;
const users = [];
function formatMessage(msg, output) {
output.parseTags = true;
msg = output._parseTags(msg);
output.parseTags = false;
return msg;
}
function userBroadcast(msg, source) {
const sourceMsg = `> ${msg}`;
const name = `{cyan-fg}{bold}${source.name}{/}`;
msg = `: ${msg}`;
for (const user of users) {
const output = user.output;
if (source === user)
output.add(sourceMsg);
else
output.add(formatMessage(name, output) + msg);
}
}
function localMessage(msg, source) {
const output = source.output;
output.add(formatMessage(msg, output));
}
function noop(v) {}
new Server({
hostKeys: [readFileSync('host.key')],
}, (client) => {
let stream;
let name;
client.on('authentication', (ctx) => {
let nick = ctx.username;
let prompt = PROMPT_NAME;
let lowered;
// Try to use username as nickname
if (nick.length > 0 && nick.length <= MAX_NAME_LEN) {
lowered = nick.toLowerCase();
let ok = true;
for (const user of users) {
if (user.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 (const user of users) {
if (user.name.toLowerCase() === lowered) {
return ctx.prompt(`That nickname is already in use.\n${PROMPT_NAME}`,
retryPrompt);
}
}
name = nick;
ctx.accept();
});
}).on('ready', () => {
let rows;
let cols;
let term;
client.once('session', (accept, reject) => {
accept().once('pty', (accept, reject, info) => {
rows = info.rows;
cols = info.cols;
term = info.term;
accept && accept();
}).on('window-change', (accept, reject, info) => {
rows = info.rows;
cols = info.cols;
if (stream) {
stream.rows = rows;
stream.columns = cols;
stream.emit('resize');
}
accept && accept();
}).once('shell', (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);
const 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);
const 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: '='
}));
const 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 (const user of users) {
const 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', (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('close', () => {
if (stream !== undefined) {
users.splice(users.indexOf(stream), 1);
// Let everyone else know that this user just left
for (const user of users) {
const output = user.output;
output.add(formatMessage('{magenta-fg}*** {bold}', output)
+ name
+ formatMessage('{/bold} has left the chat{/}', output));
}
}
}).on('error', (err) => {
// Ignore errors
});
}).listen(0, function() {
console.log('Listening on port ' + this.address().port);
});