04 Jul 2011 TLS/SSL tunneling with Node.js
I was recently faced with a bit of a coding challenge whereby I needed to get LDAP authentication working via SSL/TLS using Node. Unfortunately for me Node.js is a relatively new language and a secure LDAP library is still on the wish list. When I was first given this task, I actually didn’t know where to start. I looked into creating a Node wrapper for some of the OpenLDAP libraries written in C. My project team was already using node-ldapauth, which utilizes OpenLDAP behind the scenes, so extending that was a possibility. I felt though that there must be an easier alternative, especially given how powerful node is with I/O. So I decided to implement a kind of TLS/SSL tunnel/port forward solution and use it in conjunction with node-ldapauth. Node v0.4.7 already has a built-in TLS connection library, so it was just a matter of constructing a ‘tunnel’ with a non-secure socket on one end, and a secure socket on the other.
Before trying anything too fancy, I thought I would prove the concept worked with a basic test. To do this I created three small apps:
- A Server (Only accepts secure connections)
- The Tunnel
- A Client (Only makes non secure connections)
In order to make this a true SSL connection, I first needed to generate a certificate and a private server key. Thankfully OpenSSL makes this task a breeze:
openssl genrsa -out server.key 1024
openssl req -new -key server.key -out csr.pem
openssl x509 -req -in csr.pem -signkey server.key -out cert.pem
Here is the code for each of the apps:
server.js:
var tls = require('tls'), fs = require('fs'), sys = require('sys'); var options = { key: fs.readFileSync('server.key'), cert: fs.readFileSync('cert.pem') }; sys.puts("TLS server started."); tls.createServer(options, function (socket) { sys.puts("TLS connection established"); socket.addListener("data", function (data) { sys.puts("Data received: " + data); }); socket.pipe(socket); }).listen(8000);
tunnel.js:
var tls = require('tls'), fs = require('fs'), sys = require('sys'), net = require('net'); var options = { cert: fs.readFileSync('cert.pem'), ca: fs.readFileSync('cert.pem') }; sys.puts("Tunnel started."); var client = this; // try to connect to the server client.socket = tls.connect(8000, options, function() { if (client.socket.authorized) { sys.puts("Auth success, connected to TLS server"); } else { //Something may be wrong with your certificates sys.puts("Failed to auth TLS connection: "); sys.puts(client.socket.authorizationError); } }); client.socket.addListener("data", function (data) { sys.puts("Data received from server: " + data); }); var server = net.createServer(function (socket) { socket.addListener("connect", function () { sys.puts("Connection from " + socket.remoteAddress); //sync the file descriptors, so that the socket data structures are the same client.socket.fd = socket.fd; //pipe the incoming data from the client directly onto the server client.socket.pipe(socket); //and the response from the server back to the client socket.pipe(client.socket); }); socket.addListener("data", function (data) { sys.puts("Data received from client: " + data); }); socket.addListener("close", function () { //close the tunnel when the client finishes the connection. server.close(); }); }); server.listen(7000);
client.js:
var net = require('net'), sys = require('sys'); var msg = "Hello from net client!"; client = net.createConnection(7000, function() { sys.puts("Sending data: " + msg); client.write(msg); }); client.addListener("data", function (data) { sys.puts("Received: " + data); });
Make sure the certificate and key files are in the same directory as the server/tunnel/client and then start them up in different terminals in the following order:
- node server.js
- node tunnel.js
- node client.js
If all goes well you should see the text: “Hello from client!” across all terminals.
To get this working with our existing LDAP library, I wrapped the tunnel code in a method ‘connect’ with a callback function, which then invokes the LDAP authenticate code:
tunnel.connect(ldap_port, ldap_host, ssloptions, function() { ldap.authenticate(tunnel.port, etc...) }
The LDAP authenticate call is configured to point to the tunnel, and the tunnel points to the actual LDAP server on the TLS port (636).
I love how easy this task was made, simply by using Node. It’s a powerful language and I look forward to using it in the future.
Alexander Wieser
Posted at 23:43h, 25 SeptemberNode is no language, the language is still Javascript.
Salehen Rahman
Posted at 04:04h, 27 MarchThat’s a moot point. There’s a reason why people use the term “ECMAScript” instead of JavaScript all the time. JavaScript written for the browser isn’t necessarily compatible for Node.js, or for vanilla V8 for that matter.
Hence why Node.js is considered a different programming language by many, simply because it adds to the standard ECMAScript features. It doesn’t add much, other than a few objects not defined in the ECMAScript specification, such as the “require” function, or the “process” object, to name a few.
And the debate will go on about whether or not those added objects really add much to the ECMAScript standard. Or is it merely a set of libraries injected by the V8 runtime that supposedly is so compatible with the ECMAScript language. Can we turn those off, or are the integral to Node.js? Again, it’s up for debate.
Not that I’m disagreeing with you. I’m just trying to point out why many call Node.js a language.
Ami
Posted at 11:39h, 25 MayIf you want to keep your files more secure and don’t want it to be get haeckd by anyone or to have more control than the host then you should move on to a dedicated server.