var util = require('util'); var events = require('events'); var stream = require('stream'); var assert = process.assert; var debugLevel = parseInt(process.env.NODE_DEBUG, 16); var debug; if (debugLevel & 0x2) { debug = function() { util.error.apply(this, arguments); }; } else { debug = function() { }; } /* Lazy Loaded crypto object */ var SecureStream = null; /** * Provides a pair of streams to do encrypted communication. */ function SecurePair(credentials, isServer, shouldVerifyPeer) { if (!(this instanceof SecurePair)) { return new SecurePair(credentials, isServer, shouldVerifyPeer); } var self = this; try { SecureStream = process.binding('crypto').SecureStream; } catch (e) { throw new Error('node.js not compiled with openssl crypto support.'); } events.EventEmitter.call(this); this._secureEstablished = false; this._isServer = isServer ? true : false; this._encWriteState = true; this._clearWriteState = true; this._done = false; var crypto = require('crypto'); if (!credentials) { this.credentials = crypto.createCredentials(); } else { this.credentials = credentials; } if (!this._isServer) { // For clients, we will always have either a given ca list or be using // default one shouldVerifyPeer = true; } this._secureEstablished = false; this._encInPending = []; this._clearInPending = []; this._ssl = new SecureStream(this.credentials.context, this._isServer ? true : false, shouldVerifyPeer ? true : false); /* Acts as a r/w stream to the cleartext side of the stream. */ this.cleartext = new stream.Stream(); this.cleartext.readable = true; this.cleartext.writable = true; /* Acts as a r/w stream to the encrypted side of the stream. */ this.encrypted = new stream.Stream(); this.encrypted.readable = true; this.encrypted.writable = true; this.cleartext.write = function(data) { if (typeof data == 'string') data = Buffer(data); debug('clearIn data'); self._clearInPending.push(data); self._cycle(); return self._cleartextWriteState; }; this.cleartext.pause = function() { debug('paused cleartext'); self._cleartextWriteState = false; }; this.cleartext.resume = function() { debug('resumed cleartext'); self._cleartextWriteState = true; }; this.cleartext.end = function(err) { debug('cleartext end'); if (!self._done) { self._ssl.shutdown(); self._cycle(); } self._destroy(err); }; this.encrypted.write = function(data) { debug('encIn data'); self._encInPending.push(data); self._cycle(); return self._encryptedWriteState; }; this.encrypted.pause = function() { if (typeof data == 'string') data = Buffer(data); debug('pause encrypted'); self._encryptedWriteState = false; }; this.encrypted.resume = function() { debug('resume encrypted'); self._encryptedWriteState = true; }; this.encrypted.end = function(err) { debug('encrypted end'); if (!self._done) { self._ssl.shutdown(); self._cycle(); } self._destroy(err); }; this.cleartext.on('end', function(err) { debug('clearIn end'); if (!self._done) { self._ssl.shutdown(); self._cycle(); } self._destroy(err); }); this.cleartext.on('close', function() { debug('source close'); self.emit('close'); self._destroy(); }); this.encrypted.on('end', function() { if (!self._done) { self._error( new Error('Encrypted stream ended before secure pair was done')); } }); this.encrypted.on('close', function() { if (!self._done) { self._error( new Error('Encrypted stream closed before secure pair was done')); } }); this.cleartext.on('drain', function() { debug('source drain'); self._cycle(); self.encrypted.resume(); }); this.encrypted.on('drain', function() { debug('target drain'); self._cycle(); self.cleartext.resume(); }); process.nextTick(function() { self._ssl.start(); self._cycle(); }); } util.inherits(SecurePair, events.EventEmitter); exports.createSecurePair = function(credentials, isServer, shouldVerifyPeer) { var pair = new SecurePair(credentials, isServer, shouldVerifyPeer); return pair; }; /** * Attempt to cycle OpenSSLs buffers in various directions. * * An SSL Connection can be viewed as four separate piplines, * interacting with one has no connection to the behavoir of * any of the other 3 -- This might not sound reasonable, * but consider things like mid-stream renegotiation of * the ciphers. * * The four pipelines, using terminology of the client (server is just * reversed): * (1) Encrypted Output stream (Writing encrypted data to peer) * (2) Encrypted Input stream (Reading encrypted data from peer) * (3) Cleartext Output stream (Decrypted content from the peer) * (4) Cleartext Input stream (Cleartext content to send to the peer) * * This function attempts to pull any available data out of the Cleartext * input stream (4), and the Encrypted input stream (2). Then it pushes any * data available from the cleartext output stream (3), and finally from the * Encrypted output stream (1) * * It is called whenever we do something with OpenSSL -- post reciving * content, trying to flush, trying to change ciphers, or shutting down the * connection. * * Because it is also called everywhere, we also check if the connection has * completed negotiation and emit 'secure' from here if it has. */ SecurePair.prototype._cycle = function() { if (this._done) { return; } var self = this; var rv; var tmp; var bytesRead; var bytesWritten; var chunkBytes; var chunk = null; var pool = null; // Pull in incoming encrypted data from the socket. // This arrives via some code like this: // // socket.on('data', function (d) { // pair.encrypted.write(d) // }); // while (this._encInPending.length > 0) { tmp = this._encInPending.shift(); try { debug('writing from encIn'); rv = this._ssl.encIn(tmp, 0, tmp.length); } catch (e) { return this._error(e); } if (rv === 0) { this._encInPending.unshift(tmp); break; } assert(rv === tmp.length); } // Pull in any clear data coming from the application. // This arrives via some code like this: // // pair.cleartext.write("hello world"); // while (this._clearInPending.length > 0) { tmp = this._clearInPending.shift(); try { debug('writng from clearIn'); rv = this._ssl.clearIn(tmp, 0, tmp.length); } catch (e) { return this._error(e); } if (rv === 0) { this._clearInPending.unshift(tmp); break; } assert(rv === tmp.length); } function mover(reader, writer, checker) { var bytesRead; var pool; var chunkBytes; do { bytesRead = 0; chunkBytes = 0; pool = new Buffer(4096); pool.used = 0; do { try { chunkBytes = reader(pool, pool.used + bytesRead, pool.length - pool.used - bytesRead); } catch (e) { return self._error(e); } if (chunkBytes >= 0) { bytesRead += chunkBytes; } } while ((chunkBytes > 0) && (pool.used + bytesRead < pool.length)); if (bytesRead > 0) { chunk = pool.slice(0, bytesRead); writer(chunk); } } while (checker(bytesRead)); } // Move decryptoed, clear data out into the application. // From the user's perspective this occurs as a 'data' event // on the pair.cleartext. mover( function(pool, offset, length) { debug('reading from clearOut'); return self._ssl.clearOut(pool, offset, length); }, function(chunk) { self.cleartext.emit('data', chunk); }, function(bytesRead) { return bytesRead > 0 && self._cleartextWriteState === true; }); // Move encrypted data to the stream. From the user's perspective this // occurs as a 'data' event on the pair.encrypted. Usually the application // will have some code which pipes the stream to a socket: // // pair.encrypted.on('data', function (d) { // socket.write(d); // }); // mover( function(pool, offset, length) { debug('reading from encOut'); return self._ssl.encOut(pool, offset, length); }, function(chunk) { self.encrypted.emit('data', chunk); }, function(bytesRead) { return bytesRead > 0 && self._encryptedWriteState === true; }); if (!this._secureEstablished && this._ssl.isInitFinished()) { this._secureEstablished = true; debug('secure established'); this.emit('secure'); this._cycle(); } }; SecurePair.prototype._destroy = function(err) { if (!this._done) { this._done = true; this._ssl.close(); this._ssl = null; this.emit('end', err); } }; SecurePair.prototype._error = function(err) { this.emit('error', err); }; SecurePair.prototype.getPeerCertificate = function(err) { if (this._ssl) { return this._ssl.getPeerCertificate(); } else { return null; } }; SecurePair.prototype.getCipher = function(err) { if (this._ssl) { return this._ssl.getCurrentCipher(); } else { return null; } };