Simple Telnet Server in Node.js

I was playing with Node.js and started putting together a simple telnet server to learn a bit about it. I am going to walk through it in phases from “super simple” to more complex. But first, let’s ask ourselves – what the heck is a ‘telnet server’?

Telnet is a client server protocol based on a reliable connection-oriented transport, it is generally text based. Typically telnet is used today when connecting to unix based systems to do administration (though ssh has become more in favor of late, as it provides encryption), for administration of network elements, MUD style games played over the internet, etc. Typically a telnet connection is made over TCP, however telnet actually pre-dates TCP (it originally ran over NCP). For most purposes telnet has largely been replaced by SSH, which is more secure. That said, there is still a thriving community that uses it for text based interaction on the internet, and as one of the most basic protocols I felt it was a good place to learn how to use Node.js.

Some folks tend to confuse telnet with just raw TCP communication – and in fact, many telnet clients are used for testing commands against a TCP server (such as a web server utilizing HTTP). There are true raw TCP clients available today – such as netcat, socat, or PuTTY supports it on Windows. A few things differentiate telnet from raw TCP, namely certain rules around how to handle carriage returns and so forth. Also, telnet is not initially able to transmit ‘binary data’ as it is not ’8-bit clean’ by default. You can negotiate 8-bit clean, which is precisely how FTP works.

In any case, enough theory, let’s get to some code. For my simple starting point I am going to begin with a simple TCP connection. Here’s some Javascript code for Node.js to do it:

var net = require('net');
 
var server = net.createServer(function (socket) {
	socket.write('Welcome to the Telnet server!');
}).listen(8888);

So the first line causes us to load the net module from node which we then use in the next statement. We call net.createServer() and pass an anonymous function to it. The function accepts a socket, and writes out a string to the socket. We then cause this server to begin listening.

Now this code looks pretty awful honestly – but it is how much of the Node.js tutorial code is written up. Let’s pretty it up though into something more manageable/understandable by a normal human being.

var net = require('net');
 
/*
 * Callback method executed when a new TCP socket is opened.
 */
function newSocket(socket) {
	socket.write('Welcome to the Telnet server!');
}
 
// Create a new server and provide a callback for when a connection occurs
var server = net.createServer(newSocket);
 
// Listen on port 8888
server.listen(8888);

So doing the same thing, but with comments and a named function defined separately and such. Put this in Node.js and run it, it will cause node to listen on port 8888. Should you then open a terminal window (or command line on Windows) and type telnet localhost 8888 you will see the message come from the server, and nothing else. Voila, you have now connected via telnet, very impressive.

We’ll find that the server does absolutely nothing when we send commands at it. In fact, there is no way to close the connection without killing telnet itself or issuing the escape character (probably ctrl-] in most clients). I would like to do something with the data, so let’s make a quick chat server as it were. I would like multiple people to be able to connect to my server (already handled for us by node) and anytime a person types something, I would like their text to be sent to other people – thankfully on the Node.js home page there is a short video where they do exactly this, so it isn’t exactly a great leap. We’ll need to listen for a ‘data’ event, and when we get data, we’ll want to send it out to everybody.

var net = require('net');
 
var sockets = [];
 
/*
 * Callback method executed when data is received from a socket
 */
function receiveData(data) {
	for(var i = 0; i<sockets.length; i++) {
		sockets[i].write(data);
	}
}
 
/*
 * Callback method executed when a new TCP socket is opened.
 */
function newSocket(socket) {
	sockets.push(socket);
	socket.write('Welcome to the Telnet server!\n');
	socket.on('data', function(data) {
		receiveData(data);
	})
}
 
// Create a new server and provide a callback for when a connection occurs
var server = net.createServer(newSocket);
 
// Listen on port 1337
server.listen(1337);

So a couple things. We created an array called sockets – this is to hold all the sockets that connect to our server. We then define a receiveData function – this is a call back which will be called any time data comes in from the socket. Our trusty newSocket method from before now registers the receiveData function as a callback for any time data is received. Using that array of sockets, we then send any data we receive out to all the sockets.

This is neat but there are several initial issues with it. First and most obvious, what happens when a socket disconnects? The answer is Bad Things(tm). We need to do something to remove the socket from the sockets array so that we do not try to send messages to a dead socket. So we add the following function:

/*
 * Method executed when a socket ends
 */
function closeSocket(socket) {
	var i = sockets.indexOf(socket);
	if (i != -1) {
		sockets.splice(i, 1);
	}
}

And we can then do this inside our newSocket method:

socket.on('end', function() {
	closeSocket(socket);
})

You can see here I used an anonymous function to call the function I wrote because I wanted to pass in the socket object explicitly. You may have noticed that I also did this for the data event, and the reason I did that is because I know that I also want to provide the socket to the receiveData method. Why? Because the second issue we face here is that data input is echoed back to the person who entered it in the first place. This is not optimal given the telnet interface we are using, and so we can correct this issue by sending to every socket except the one that sent us the data. We can modify receiveData as such:

function receiveData(socket, data) {
	for(var i = 0; i<sockets.length; i++) {
		if (sockets[i] !== socket) {
			sockets[i].write(data);
		}
	}
}

We also need to modify the newSocket function to have the socket as well as the data passed to the receiveData method. Here’s the full code as it stands after these modifications:

var net = require('net');
 
var sockets = [];
 
/*
 * Method executed when data is received from a socket
 */
function receiveData(socket, data) {
	for(var i = 0; i<sockets.length; i++) {
		if (sockets[i] !== socket) {
			sockets[i].write(data);
		}
	}
}
 
/*
 * Method executed when a socket ends
 */
function closeSocket(socket) {
	var i = sockets.indexOf(socket);
	if (i != -1) {
		sockets.splice(i, 1);
	}
}
 
/*
 * Callback method executed when a new TCP socket is opened.
 */
function newSocket(socket) {
	sockets.push(socket);
	socket.write('Welcome to the Telnet server!\n');
	socket.on('data', function(data) {
		receiveData(socket, data);
	})
	socket.on('end', function() {
		closeSocket(socket);
	})
}
 
// Create a new server and provide a callback for when a connection occurs
var server = net.createServer(newSocket);
 
// Listen on port 8888
server.listen(8888);

So that’s cool, but what if the person wants to quit communicating? They could kill their telnet client, or issue the escape character – but this seems less than optimal. Instead, lets give them a command they can type which will close the connection for them. We’ll need to look for a particular text entry that is typed to match against our command, and disconnect the socket if it is received. Here’s some naive code to do that:

if(data == "@quit") {
   socket.end('Goodbye!\n');
}

However, this actually won’t work, because what is actually sent to us is not just the text, but also the carriage return and newline that follows it. We need to strip these out – we’ll make a simple function that does this simplistically using regular expressions:

/*
 * Cleans the input of carriage return, newline
 */
function cleanInput(data) {
	return data.toString().replace(/(\r\n|\n|\r)/gm,"");
}

Now we simply need to call this function on our data and use the return for comparison against our command rather than the raw input. Here’s the complete code with this modification:

var net = require('net');
 
var sockets = [];
 
/*
 * Cleans the input of carriage return, newline
 */
function cleanInput(data) {
	return data.toString().replace(/(\r\n|\n|\r)/gm,"");
}
 
/*
 * Method executed when data is received from a socket
 */
function receiveData(socket, data) {
	var cleanData = cleanInput(data);
	if(cleanData === "@quit") {
		socket.end('Goodbye!\n');
	}
	else {
		for(var i = 0; i<sockets.length; i++) {
			if (sockets[i] !== socket) {
				sockets[i].write(data);
			}
		}
	}
}
 
/*
 * Method executed when a socket ends
 */
function closeSocket(socket) {
	var i = sockets.indexOf(socket);
	if (i != -1) {
		sockets.splice(i, 1);
	}
}
 
/*
 * Callback method executed when a new TCP socket is opened.
 */
function newSocket(socket) {
	sockets.push(socket);
	socket.write('Welcome to the Telnet server!\n');
	socket.on('data', function(data) {
		receiveData(socket, data);
	})
	socket.on('end', function() {
		closeSocket(socket);
	})
}
 
// Create a new server and provide a callback for when a connection occurs
var server = net.createServer(newSocket);
 
// Listen on port 8888
server.listen(8888);

So simple enough – a little telnet chat server. In actuality, it is more of a TCP chat server, as there is a lot more to do, and obviously it is very naive and brittle to error – but that is for another day.

This entry was posted in Javascript, Node.js and tagged , , , , , . Bookmark the permalink.

11 Responses to Simple Telnet Server in Node.js

  1. Tiago Ratto says:

    I’ve pasted the code on my node server and I’ve got an issue with the first connection in wich anything I type on it is broadcasted right away, letter by letter, to all other clients. It only happens with the first position of the array.
    Any tips ?

    • The server itself always sends every bit of data it receives outward as it receives it, which you can see here. The thing is, a telnet client should only send data on carriage return. What are you using to connect to the server?

  2. David Boyer says:

    I had the same issue, but I don’t think it’s really your code.
    The Windows telnet client appears to like character mode (send each character in turn) as opposed to something like Putty which will use line mode (send characters on return). I tried asking Windows telnet to use linemode…
    socket.write(new Buffer([255,252,34])); but it responded with “FF FC 22″ (I won’t linemode). Don’t think it really matters as you’re just providing a simple server, plus the fact that Windows telnet sucks ;)

    • Ah, never tried with Windows Telnet as I use Ubuntu for my dev OS. I will see about posting an update that handles the case – if I had paid more attention to the telnet spec I would have noticed it wasn’t required to send full lines.

  3. Keryy Jiang says:

    It works for some cases, but you cannot assume you can receive a full request in one receiving event.

  4. Doug says:

    Thank you so much for this code, it’s really useful! I have a question, why do we have to put an “@” sign before the word quit, when the command is compared? (“@quit”)

    • Just a convention in my case, it does not need to be there. I used to work on old text based online environments that used ‘@’ to denote commands, and happened to put it there.

  5. Pingback: Creating a Telnet Resume Using Node.js ยป Zachary Flower

  6. Amir says:

    Hi.
    Nice Article and thanks for sharing. I just wondering to have a simple RAW TCP server, not telnet server! how is it possible?

Leave a Reply

Your email address will not be published. Required fields are marked *


*

* Copy This Password *

* Type Or Paste Password Here *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>