From: Ryan Dahl Date: Mon, 6 Dec 2010 02:19:18 +0000 (-0800) Subject: Move securepair stuff into tls.js X-Git-Tag: v0.3.2~56 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=0b0faceb198c68669d078f00ebda0d80c3793866;p=platform%2Fupstream%2Fnodejs.git Move securepair stuff into tls.js --- diff --git a/lib/crypto.js b/lib/crypto.js index c5edc59..130d17d 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -105,7 +105,3 @@ exports.Verify = Verify; exports.createVerify = function(algorithm) { return (new Verify).init(algorithm); }; - - -var securepair = require('securepair'); -exports.createPair = securepair.createSecurePair; diff --git a/lib/securepair.js b/lib/securepair.js deleted file mode 100644 index 2db5a23..0000000 --- a/lib/securepair.js +++ /dev/null @@ -1,390 +0,0 @@ -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, requestCert, rejectUnauthorized) { - if (!(this instanceof SecurePair)) { - return new SecurePair(credentials, isServer, requestCert, rejectUnauthorized); - } - - 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 - requestCert = true; - } - - this._secureEstablished = false; - this._encInPending = []; - this._clearInPending = []; - - this._rejectUnauthorized = rejectUnauthorized ? true : false; - this._requestCert = requestCert ? true : false; - - this._ssl = new SecureStream(this.credentials.context, - this._isServer ? true : false, - this._requestCert, - this._rejectUnauthorized); - - - /* 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.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, - requestCert, - rejectUnauthorized) { - var pair = new SecurePair(credentials, - isServer, - requestCert, - rejectUnauthorized); - 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'); - if (!self._ssl) return -1; - return self._ssl.encOut(pool, offset, length); - }, - function(chunk) { - self.encrypted.emit('data', chunk); - }, - function(bytesRead) { - if (!self._ssl) return false; - return bytesRead > 0 && self._encryptedWriteState === true; - }); - - - - if (this._ssl && !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.encrypted.emit('close'); - this.cleartext.emit('close'); - this.emit('end', err); - } -}; - - -SecurePair.prototype._error = function(err) { - if (this._isServer && - this._rejectUnauthorized && - /peer did not return a certificate/.test(err.message)) { - // Not really an error. - this._destroy(); - } else { - 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; - } -}; diff --git a/lib/tls.js b/lib/tls.js index 7d57426..fe05ff5 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -1,11 +1,396 @@ var crypto = require('crypto'); -var securepair = require('securepair'); +var util = require('util'); var net = require('net'); var events = require('events'); -var inherits = require('util').inherits; +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, requestCert, rejectUnauthorized) { + if (!(this instanceof SecurePair)) { + return new SecurePair(credentials, isServer, requestCert, rejectUnauthorized); + } + + 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 + requestCert = true; + } + + this._secureEstablished = false; + this._encInPending = []; + this._clearInPending = []; + + this._rejectUnauthorized = rejectUnauthorized ? true : false; + this._requestCert = requestCert ? true : false; + + this._ssl = new SecureStream(this.credentials.context, + this._isServer ? true : false, + this._requestCert, + this._rejectUnauthorized); + + + /* 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.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, + requestCert, + rejectUnauthorized) { + var pair = new SecurePair(credentials, + isServer, + requestCert, + rejectUnauthorized); + 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'); + if (!self._ssl) return -1; + return self._ssl.encOut(pool, offset, length); + }, + function(chunk) { + self.encrypted.emit('data', chunk); + }, + function(bytesRead) { + if (!self._ssl) return false; + return bytesRead > 0 && self._encryptedWriteState === true; + }); + + + + if (this._ssl && !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.encrypted.emit('close'); + this.cleartext.emit('close'); + this.emit('end', err); + } +}; + + +SecurePair.prototype._error = function(err) { + if (this._isServer && + this._rejectUnauthorized && + /peer did not return a certificate/.test(err.message)) { + // Not really an error. + this._destroy(); + } else { + 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; + } +}; + // TODO: support anonymous (nocert) and PSK // TODO: how to proxy maxConnections? @@ -100,10 +485,10 @@ function Server(/* [options], listener */) { { key: self.key, cert: self.cert, ca: self.ca }); creds.context.setCiphers('RC4-SHA:AES128-SHA:AES256-SHA'); - var pair = securepair.createSecurePair(creds, - true, - self.requestCert, - self.rejectUnauthorized); + var pair = new SecurePair(creds, + true, + self.requestCert, + self.rejectUnauthorized); pair.encrypted.pipe(socket); socket.pipe(pair.encrypted); @@ -143,7 +528,7 @@ function Server(/* [options], listener */) { this.setOptions(options); } -inherits(Server, net.Server); +util.inherits(Server, net.Server); exports.Server = Server; exports.createServer = function(options, listener) { return new Server(options, listener); diff --git a/test/simple/test-securepair-client.js b/test/simple/test-securepair-client.js index 1e48e23..3ba7fa9 100644 --- a/test/simple/test-securepair-client.js +++ b/test/simple/test-securepair-client.js @@ -4,6 +4,7 @@ var net = require('net'); var assert = require('assert'); var fs = require('fs'); var crypto = require('crypto'); +var tls = require('tls'); var spawn = require('child_process').spawn; // FIXME: Avoid the common PORT as this test currently hits a C-level @@ -71,7 +72,7 @@ function startClient() { var sslcontext = crypto.createCredentials({key: key, cert: cert}); sslcontext.context.setCiphers('RC4-SHA:AES128-SHA:AES256-SHA'); - var pair = crypto.createPair(sslcontext, false); + var pair = tls.createSecurePair(sslcontext, false); assert.ok(pair.encrypted.writable); assert.ok(pair.cleartext.writable); diff --git a/test/simple/test-securepair-server.js b/test/simple/test-securepair-server.js index fd98419..00798ac 100644 --- a/test/simple/test-securepair-server.js +++ b/test/simple/test-securepair-server.js @@ -5,6 +5,7 @@ var join = require('path').join; var net = require('net'); var fs = require('fs'); var crypto = require('crypto'); +var tls = require('tls'); var spawn = require('child_process').spawn; var connections = 0; @@ -21,7 +22,7 @@ var server = net.createServer(function(socket) { var sslcontext = crypto.createCredentials({key: key, cert: cert}); sslcontext.context.setCiphers('RC4-SHA:AES128-SHA:AES256-SHA'); - var pair = crypto.createPair(sslcontext, true); + var pair = tls.createSecurePair(sslcontext, true); assert.ok(pair.encrypted.writable); assert.ok(pair.cleartext.writable);