245 lines
7.5 KiB
JavaScript
245 lines
7.5 KiB
JavaScript
// **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('bare-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)
|
|
})
|