Last active
January 18, 2019 17:12
-
-
Save alenaksu/d32b50b888e5b1f908a3700519b12949 to your computer and use it in GitHub Desktop.
Node.js Telnet Chat Server
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"server": { | |
"maxConnections": 100, | |
"host": "0.0.0.0", | |
"port": 10000 | |
}, | |
"chat": { | |
"messageLength": 150, | |
"messages": { | |
"welcome": "Welcome to chat server. Press CTRL+C to leave.", | |
"nickname": "Enter a nickname: ", | |
"logout": "%s has left the chat", | |
"login": "%s has joined the chat" | |
}, | |
"serverNickname": "server" | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const net = require('net'); | |
const { format } = require('util'); | |
const telnet = require('./telnet'); | |
const Config = require('./config.json'); | |
const Messages = Config.chat.messages; | |
const clients = []; | |
const messageListener = fn => data => { | |
console.log('Received data', data, data.toString()); | |
data = data | |
.toString() | |
.trim() | |
.replace(/[^\w\d \t]/g, '') | |
.substring(0, Config.chat.messageLength); | |
data && fn(data); | |
}; | |
const exitHandler = socket => { | |
return data => telnet.isInterrput(data) && socket.end('Bye bye.'); | |
}; | |
const broadcastHandler = (socket, nickname) => | |
messageListener(message => sendBroadcast(socket, nickname, message)); | |
const endHandler = (socket, nickname) => () => { | |
clients.splice(clients.indexOf(socket), 1); | |
sendBroadcast(null, null, format(Messages.logout, nickname)); | |
console.log(`${socket.remoteAddress} disconnected`); | |
}; | |
function sendMessageLn(socket, from, message) { | |
sendMessage(socket, from, message + '\r\n'); | |
} | |
function sendMessage(socket, from, message) { | |
if (from) message = `<${from}> ${message}`; | |
socket.write(message); | |
} | |
function sendBroadcast(socket, from, message) { | |
if (!message) return; | |
clients.forEach(client => { | |
telnet.sendEraseLine(client); | |
if (client !== socket) sendMessageLn(client, from, message); | |
}); | |
} | |
function askNickname(socket) { | |
return new Promise((resolve, reject) => { | |
const ask = () => { | |
socket.once('data', onData); | |
sendMessage(socket, null, Messages.nickname); | |
}; | |
const onData = messageListener(data => { | |
if (!data.length) return ask(); | |
resolve(data); | |
}); | |
ask(); | |
}); | |
} | |
function createServer() { | |
const server = net.createServer(async socket => { | |
try { | |
console.log(`Connection from ${socket.remoteAddress}`); | |
sendMessageLn(socket, null, Messages.welcome); | |
socket.on('data', exitHandler(socket)); | |
const nickname = await askNickname(socket); | |
clients.push(socket); | |
socket.on('data', broadcastHandler(socket, nickname)); | |
// telnet.sendCommand(socket, telnet.Commands.NOP); | |
// telnet.sendCommand(socket, telnet.Commands.WILL, telnet.Options.ECH); | |
// telnet.sendCommand(socket, telnet.Commands.WILL, telnet.Options.SGA); | |
sendBroadcast(null, null, format(Messages.login, nickname)); | |
console.log( | |
`${socket.remoteAddress} joined the chat as ${nickname}` | |
); | |
} catch (e) { | |
console.log('error', e); | |
socket.end(); | |
} | |
}); | |
server.maxConnections = Config.server.maxConnections; | |
server.on('close', () => { | |
console.log(`Server closed.`); | |
}); | |
return server; | |
} | |
if (require.main === module) { | |
createServer().listen(Config.server.port); | |
console.log(`Server listening on port ${Config.server.port}`); | |
} | |
module.exports = { | |
createServer | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const Commands = { | |
IAC: 0xff, | |
SE: 0xf0, | |
NOP: 0xf1, | |
DataMark: 0xf2, | |
BRK: 0xf3, | |
IP: 0xf4, | |
AO: 0xf5, | |
AYT: 0xf6, | |
EC: 0xf7, | |
EL: 0xf8, | |
GA: 0xf9, | |
SB: 0xfa, | |
WILL: 0xfb, | |
WONT: 0xfc, | |
DO: 0xfd, | |
DONT: 0xfe | |
}; | |
const Options = { | |
ECH: 0x1, | |
SGA: 0x3, | |
STS: 0x5, | |
TimingMark: 0x6, | |
TerminalType: 0x18, | |
WindowSize: 0x1f, | |
TerminalSpeed: 0x20, | |
RFC: 0x21, | |
LineMode: 0x22, | |
EnvVars: 0x24 | |
} | |
const isCommand = buffer => buffer[0] === Commands.IAC; | |
const isInterrput = buffer => isCommand(buffer) && buffer[1] === Commands.IP; | |
const sendCommand = (socket, command, option) => | |
socket.write( | |
Uint8Array.from( | |
[Commands.IAC, command, option].filter(b => b !== undefined) | |
) | |
); | |
const sendEraseLine = socket => sendCommand(socket, Commands.EL); | |
module.exports = { | |
isInterrput, | |
Commands, | |
sendCommand, | |
sendEraseLine, | |
Options | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import test from 'ava'; | |
import Telnet from 'telnet-client'; | |
import Config from '../src/chat-server/config.json'; | |
import chatServer from '../src/chat-server'; | |
const connectionParams = { | |
host: '127.0.0.1', | |
port: Config.server.port, | |
shellPrompt: '/ # ', | |
ors: '\r\n', | |
timeout: 1500, | |
negotiationMandatory: false, | |
debug: true, | |
initialLFCR: true | |
}; | |
let server; | |
test.before(() => { | |
server = chatServer.createServer(); | |
server.listen(Config.server.port); | |
}); | |
test.after.always(() => { | |
server.close(); | |
}); | |
test(`should listens on port ${Config.server.port}`, async t => { | |
const connection = new Telnet(); | |
await connection.connect(connectionParams); | |
connection.end(); | |
t.pass(); | |
}); | |
test.cb(`should ask for nickname`, t => { | |
const connection = new Telnet(); | |
connection.on('data', buffer => { | |
console.log(buffer); | |
~buffer.toString().indexOf(Config.chat.messages.nickname) | |
? t.pass() | |
: t.fail(); | |
connection.end(); | |
t.end(); | |
}); | |
connection.connect(connectionParams).then(() => connection.send('')); | |
}); | |
test(`should broadcast messages to all clients`, t => { | |
return new Promise(async (resolve, reject) => { | |
const secret = Math.random() | |
.toString(36) | |
.slice(2); | |
const alice = new Telnet(), | |
bob = new Telnet(); | |
await alice.connect(connectionParams); | |
await alice.send('alice'); | |
await bob.connect(connectionParams); | |
await bob.send('bob'); | |
alice.on('data', buffer => { | |
t.true(buffer.toString().indexOf(secret) !== -1); | |
resolve(); | |
}); | |
bob.send(secret); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment