var util = require('util'); var events = require('events'); var stream = require('stream'); var assert = process.assert; var debugLevel = parseInt(process.env.NODE_DEBUG, 16); function debug () { if (debugLevel & 0x2) { util.error.apply(this, arguments); } } /* Lazy Loaded crypto object */ var SecureStream = null; /** * Provides a pair of streams to do encrypted communication. */ function SecurePair(credentials, isServer) { if (!(this instanceof SecurePair)) { return new SecurePair(credentials, isServer); } 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 */ this.credentials.shouldVerify = true; } this._secureEstablished = false; this._encInPending = []; this._clearInPending = []; this._ssl = new SecureStream(this.credentials.context, this._isServer ? true : false, this.credentials.shouldVerify); /* 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) { var pair = new SecurePair(credentials, isServer); 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(); delete this._ssl; 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; } };