diff --git a/doc/api/tls.markdown b/doc/api/tls.markdown index 536343d9edf..fcf2569c699 100644 --- a/doc/api/tls.markdown +++ b/doc/api/tls.markdown @@ -49,7 +49,7 @@ server-side resources, which makes it a potential vector for denial-of-service attacks. To mitigate this, renegotiations are limited to three times every 10 minutes. An -error is emitted on the [CleartextStream][] instance when the threshold is +error is emitted on the [tls.TLSSocket][] instance when the threshold is exceeded. The limits are configurable: - `tls.CLIENT_RENEG_LIMIT`: renegotiation limit, default is 3. @@ -188,12 +188,12 @@ Here is a simple example echo server: ca: [ fs.readFileSync('client-cert.pem') ] }; - var server = tls.createServer(options, function(cleartextStream) { + var server = tls.createServer(options, function(socket) { console.log('server connected', - cleartextStream.authorized ? 'authorized' : 'unauthorized'); - cleartextStream.write("welcome!\n"); - cleartextStream.setEncoding('utf8'); - cleartextStream.pipe(cleartextStream); + socket.authorized ? 'authorized' : 'unauthorized'); + socket.write("welcome!\n"); + socket.setEncoding('utf8'); + socket.pipe(socket); }); server.listen(8000, function() { console.log('server bound'); @@ -212,12 +212,12 @@ Or }; - var server = tls.createServer(options, function(cleartextStream) { + var server = tls.createServer(options, function(socket) { console.log('server connected', - cleartextStream.authorized ? 'authorized' : 'unauthorized'); - cleartextStream.write("welcome!\n"); - cleartextStream.setEncoding('utf8'); - cleartextStream.pipe(cleartextStream); + socket.authorized ? 'authorized' : 'unauthorized'); + socket.write("welcome!\n"); + socket.setEncoding('utf8'); + socket.pipe(socket); }); server.listen(8000, function() { console.log('server bound'); @@ -228,15 +228,6 @@ You can test this server by connecting to it with `openssl s_client`: openssl s_client -connect 127.0.0.1:8000 -## tls.SLAB_BUFFER_SIZE - -Size of slab buffer used by all tls servers and clients. -Default: `10 * 1024 * 1024`. - - -Don't change the defaults unless you know what you are doing. - - ## tls.connect(options, [callback]) ## tls.connect(port, [host], [options], [callback]) @@ -285,7 +276,7 @@ Creates a new client connection to the given `port` and `host` (old API) or The `callback` parameter will be added as a listener for the ['secureConnect'][] event. -`tls.connect()` returns a [CleartextStream][] object. +`tls.connect()` returns a [tls.TLSSocket][] object. Here is an example of a client of echo server as described previously: @@ -301,17 +292,17 @@ Here is an example of a client of echo server as described previously: ca: [ fs.readFileSync('server-cert.pem') ] }; - var cleartextStream = tls.connect(8000, options, function() { + var socket = tls.connect(8000, options, function() { console.log('client connected', - cleartextStream.authorized ? 'authorized' : 'unauthorized'); - process.stdin.pipe(cleartextStream); + socket.authorized ? 'authorized' : 'unauthorized'); + process.stdin.pipe(socket); process.stdin.resume(); }); - cleartextStream.setEncoding('utf8'); - cleartextStream.on('data', function(data) { + socket.setEncoding('utf8'); + socket.on('data', function(data) { console.log(data); }); - cleartextStream.on('end', function() { + socket.on('end', function() { server.close(); }); @@ -324,22 +315,24 @@ Or pfx: fs.readFileSync('client.pfx') }; - var cleartextStream = tls.connect(8000, options, function() { + var socket = tls.connect(8000, options, function() { console.log('client connected', - cleartextStream.authorized ? 'authorized' : 'unauthorized'); - process.stdin.pipe(cleartextStream); + socket.authorized ? 'authorized' : 'unauthorized'); + process.stdin.pipe(socket); process.stdin.resume(); }); - cleartextStream.setEncoding('utf8'); - cleartextStream.on('data', function(data) { + socket.setEncoding('utf8'); + socket.on('data', function(data) { console.log(data); }); - cleartextStream.on('end', function() { + socket.on('end', function() { server.close(); }); ## tls.createSecurePair([credentials], [isServer], [requestCert], [rejectUnauthorized]) +** Deprecated ** + Creates a new secure pair object with two streams, one of which reads/writes encrypted data, and one reads/writes cleartext data. Generally the encrypted one is piped to/from an incoming encrypted data stream, @@ -357,9 +350,11 @@ and the cleartext one is used as a replacement for the initial encrypted stream. automatically reject clients with invalid certificates. Only applies to servers with `requestCert` enabled. -`tls.createSecurePair()` returns a SecurePair object with [cleartext][] and +`tls.createSecurePair()` returns a SecurePair object with `cleartext` and `encrypted` stream properties. +NOTE: `cleartext` has the same APIs as [tls.TLSSocket][] + ## Class: SecurePair Returned by tls.createSecurePair. @@ -381,50 +376,31 @@ connections using TLS or SSL. ### Event: 'secureConnection' -`function (cleartextStream) {}` +`function (tlsSocket) {}` This event is emitted after a new connection has been successfully -handshaked. The argument is a instance of [CleartextStream][]. It has all the +handshaked. The argument is a instance of [tls.TLSSocket][]. It has all the common stream methods and events. -`cleartextStream.authorized` is a boolean value which indicates if the +`socket.authorized` is a boolean value which indicates if the client has verified by one of the supplied certificate authorities for the -server. If `cleartextStream.authorized` is false, then -`cleartextStream.authorizationError` is set to describe how authorization +server. If `socket.authorized` is false, then +`socket.authorizationError` is set to describe how authorization failed. Implied but worth mentioning: depending on the settings of the TLS server, you unauthorized connections may be accepted. -`cleartextStream.npnProtocol` is a string containing selected NPN protocol. -`cleartextStream.servername` is a string containing servername requested with +`socket.npnProtocol` is a string containing selected NPN protocol. +`socket.servername` is a string containing servername requested with SNI. ### Event: 'clientError' -`function (exception, securePair) { }` +`function (exception, tlsSocket) { }` When a client connection emits an 'error' event before secure connection is established - it will be forwarded here. -`securePair` is the `tls.SecurePair` that the error originated from. - - -### Event: 'newSession' - -`function (sessionId, sessionData) { }` - -Emitted on creation of TLS session. May be used to store sessions in external -storage. - - -### Event: 'resumeSession' - -`function (sessionId, callback) { }` - -Emitted when client wants to resume previous TLS session. Event listener may -perform lookup in external storage using given `sessionId`, and invoke -`callback(null, sessionData)` once finished. If session can't be resumed -(i.e. doesn't exist in storage) one may call `callback(null, null)`. Calling -`callback(err)` will terminate incoming connection and destroy socket. +`tlsSocket` is the [tls.TLSSocket][] that the error originated from. ### server.listen(port, [host], [callback]) @@ -469,6 +445,8 @@ The number of concurrent connections on the server. ## Class: CryptoStream +** Deprecated ** + This is an encrypted stream. ### cryptoStream.bytesWritten @@ -476,37 +454,35 @@ This is an encrypted stream. A proxy to the underlying socket's bytesWritten accessor, this will return the total bytes written to the socket, *including the TLS overhead*. -## Class: tls.CleartextStream +## Class: tls.TLSSocket -This is a stream on top of the *Encrypted* stream that makes it possible to -read/write an encrypted data as a cleartext data. +This is a wrapped version of [net.Socket][] that does transparent encryption +of written data and all required TLS negotiation. This instance implements a duplex [Stream][] interfaces. It has all the common stream methods and events. -A ClearTextStream is the `clear` member of a SecurePair object. - ### Event: 'secureConnect' -This event is emitted after a new connection has been successfully handshaked. +This event is emitted after a new connection has been successfully handshaked. The listener will be called no matter if the server's certificate was -authorized or not. It is up to the user to test `cleartextStream.authorized` +authorized or not. It is up to the user to test `tlsSocket.authorized` to see if the server certificate was signed by one of the specified CAs. -If `cleartextStream.authorized === false` then the error can be found in -`cleartextStream.authorizationError`. Also if NPN was used - you can check -`cleartextStream.npnProtocol` for negotiated protocol. +If `tlsSocket.authorized === false` then the error can be found in +`tlsSocket.authorizationError`. Also if NPN was used - you can check +`tlsSocket.npnProtocol` for negotiated protocol. -### cleartextStream.authorized +### tlsSocket.authorized A boolean that is `true` if the peer certificate was signed by one of the specified CAs, otherwise `false` -### cleartextStream.authorizationError +### tlsSocket.authorizationError The reason why the peer's certificate has not been verified. This property -becomes available only when `cleartextStream.authorized === false`. +becomes available only when `tlsSocket.authorized === false`. -### cleartextStream.getPeerCertificate() +### tlsSocket.getPeerCertificate() Returns an object representing the peer's certificate. The returned object has some properties corresponding to the field of the certificate. @@ -534,7 +510,7 @@ Example: If the peer does not provide a certificate, it returns `null` or an empty object. -### cleartextStream.getCipher() +### tlsSocket.getCipher() Returns an object representing the cipher name and the SSL/TLS protocol version of the current connection. @@ -545,33 +521,33 @@ See SSL_CIPHER_get_name() and SSL_CIPHER_get_version() in http://www.openssl.org/docs/ssl/ssl.html#DEALING_WITH_CIPHERS for more information. -### cleartextStream.address() +### tlsSocket.address() Returns the bound address, the address family name and port of the underlying socket as reported by the operating system. Returns an object with three properties, e.g. `{ port: 12346, family: 'IPv4', address: '127.0.0.1' }` -### cleartextStream.remoteAddress +### tlsSocket.remoteAddress The string representation of the remote IP address. For example, `'74.125.127.100'` or `'2001:4860:a005::68'`. -### cleartextStream.remotePort +### tlsSocket.remotePort The numeric representation of the remote port. For example, `443`. -### cleartextStream.localAddress +### tlsSocket.localAddress The string representation of the local IP address. -### cleartextStream.localPort +### tlsSocket.localPort The numeric representation of the local port. [OpenSSL cipher list format documentation]: http://www.openssl.org/docs/apps/ciphers.html#CIPHER_LIST_FORMAT [BEAST attacks]: http://blog.ivanristic.com/2011/10/mitigating-the-beast-attack-on-tls.html -[CleartextStream]: #tls_class_tls_cleartextstream +[tls.TLSSocket]: #tls_class_tls_tlssocket [net.Server.address()]: net.html#net_server_address ['secureConnect']: #tls_event_secureconnect [secureConnection]: #tls_event_secureconnection diff --git a/lib/_tls_legacy.js b/lib/_tls_legacy.js new file mode 100644 index 00000000000..6c5653e6543 --- /dev/null +++ b/lib/_tls_legacy.js @@ -0,0 +1,802 @@ +var assert = require('assert'); +var crypto = require('crypto'); +var events = require('events'); +var stream = require('stream'); +var tls = require('tls'); +var util = require('util'); + +var Timer = process.binding('timer_wrap').Timer; +var Connection = null; +try { + Connection = process.binding('crypto').Connection; +} catch (e) { + throw new Error('node.js not compiled with openssl crypto support.'); +} + +var debug = util.debuglog('tls'); + +function SlabBuffer() { + this.create(); +} + + +SlabBuffer.prototype.create = function create() { + this.isFull = false; + this.pool = new Buffer(tls.SLAB_BUFFER_SIZE); + this.offset = 0; + this.remaining = this.pool.length; +}; + + +SlabBuffer.prototype.use = function use(context, fn, size) { + if (this.remaining === 0) { + this.isFull = true; + return 0; + } + + var actualSize = this.remaining; + + if (size !== null) actualSize = Math.min(size, actualSize); + + var bytes = fn.call(context, this.pool, this.offset, actualSize); + if (bytes > 0) { + this.offset += bytes; + this.remaining -= bytes; + } + + assert(this.remaining >= 0); + + return bytes; +}; + + +var slabBuffer = null; + + +// Base class of both CleartextStream and EncryptedStream +function CryptoStream(pair, options) { + stream.Duplex.call(this, options); + + this.pair = pair; + this._pending = null; + this._pendingEncoding = ''; + this._pendingCallback = null; + this._doneFlag = false; + this._retryAfterPartial = false; + this._halfRead = false; + this._sslOutCb = null; + this._resumingSession = false; + this._reading = true; + this._destroyed = false; + this._ended = false; + this._finished = false; + this._opposite = null; + + if (slabBuffer === null) slabBuffer = new SlabBuffer(); + this._buffer = slabBuffer; + + this.once('finish', onCryptoStreamFinish); + + // net.Socket calls .onend too + this.once('end', onCryptoStreamEnd); +} +util.inherits(CryptoStream, stream.Duplex); + + +function onCryptoStreamFinish() { + this._finished = true; + + if (this === this.pair.cleartext) { + debug('cleartext.onfinish'); + if (this.pair.ssl) { + // Generate close notify + // NOTE: first call checks if client has sent us shutdown, + // second call enqueues shutdown into the BIO. + if (this.pair.ssl.shutdown() !== 1) { + if (this.pair.ssl && this.pair.ssl.error) + return this.pair.error(); + + this.pair.ssl.shutdown(); + } + + if (this.pair.ssl && this.pair.ssl.error) + return this.pair.error(); + } + } else { + debug('encrypted.onfinish'); + } + + // Try to read just to get sure that we won't miss EOF + if (this._opposite.readable) this._opposite.read(0); + + if (this._opposite._ended) { + this._done(); + + // No half-close, sorry + if (this === this.pair.cleartext) this._opposite._done(); + } +} + + +function onCryptoStreamEnd() { + this._ended = true; + if (this === this.pair.cleartext) { + debug('cleartext.onend'); + } else { + debug('encrypted.onend'); + } + + if (this.onend) this.onend(); +} + + +// NOTE: Called once `this._opposite` is set. +CryptoStream.prototype.init = function init() { + var self = this; + this._opposite.on('sslOutEnd', function() { + if (self._sslOutCb) { + var cb = self._sslOutCb; + self._sslOutCb = null; + cb(null); + } + }); +}; + + +CryptoStream.prototype._write = function write(data, encoding, cb) { + assert(this._pending === null); + + // Black-hole data + if (!this.pair.ssl) return cb(null); + + // When resuming session don't accept any new data. + // And do not put too much data into openssl, before writing it from encrypted + // side. + // + // TODO(indutny): Remove magic number, use watermark based limits + if (!this._resumingSession && + this._opposite._internallyPendingBytes() < 128 * 1024) { + // Write current buffer now + var written; + if (this === this.pair.cleartext) { + debug('cleartext.write called with %d bytes', data.length); + written = this.pair.ssl.clearIn(data, 0, data.length); + } else { + debug('encrypted.write called with %d bytes', data.length); + written = this.pair.ssl.encIn(data, 0, data.length); + } + + // Handle and report errors + if (this.pair.ssl && this.pair.ssl.error) { + return cb(this.pair.error(true)); + } + + // Force SSL_read call to cycle some states/data inside OpenSSL + this.pair.cleartext.read(0); + + // Cycle encrypted data + if (this.pair.encrypted._internallyPendingBytes()) + this.pair.encrypted.read(0); + + // Get NPN and Server name when ready + this.pair.maybeInitFinished(); + + // Whole buffer was written + if (written === data.length) { + if (this === this.pair.cleartext) { + debug('cleartext.write succeed with ' + written + ' bytes'); + } else { + debug('encrypted.write succeed with ' + written + ' bytes'); + } + + // Invoke callback only when all data read from opposite stream + if (this._opposite._halfRead) { + assert(this._sslOutCb === null); + this._sslOutCb = cb; + } else { + cb(null); + } + return; + } else if (written !== 0 && written !== -1) { + assert(!this._retryAfterPartial); + this._retryAfterPartial = true; + this._write(data.slice(written), encoding, cb); + this._retryAfterPartial = false; + return; + } + } else { + debug('cleartext.write queue is full'); + + // Force SSL_read call to cycle some states/data inside OpenSSL + this.pair.cleartext.read(0); + } + + // No write has happened + this._pending = data; + this._pendingEncoding = encoding; + this._pendingCallback = cb; + + if (this === this.pair.cleartext) { + debug('cleartext.write queued with %d bytes', data.length); + } else { + debug('encrypted.write queued with %d bytes', data.length); + } +}; + + +CryptoStream.prototype._writePending = function writePending() { + var data = this._pending, + encoding = this._pendingEncoding, + cb = this._pendingCallback; + + this._pending = null; + this._pendingEncoding = ''; + this._pendingCallback = null; + this._write(data, encoding, cb); +}; + + +CryptoStream.prototype._read = function read(size) { + // XXX: EOF?! + if (!this.pair.ssl) return this.push(null); + + // Wait for session to be resumed + // Mark that we're done reading, but don't provide data or EOF + if (this._resumingSession || !this._reading) return this.push(''); + + var out; + if (this === this.pair.cleartext) { + debug('cleartext.read called with %d bytes', size); + out = this.pair.ssl.clearOut; + } else { + debug('encrypted.read called with %d bytes', size); + out = this.pair.ssl.encOut; + } + + var bytesRead = 0, + start = this._buffer.offset; + do { + var read = this._buffer.use(this.pair.ssl, out, size); + if (read > 0) { + bytesRead += read; + size -= read; + } + + // Handle and report errors + if (this.pair.ssl && this.pair.ssl.error) { + this.pair.error(); + break; + } + + // Get NPN and Server name when ready + this.pair.maybeInitFinished(); + } while (read > 0 && !this._buffer.isFull && bytesRead < size); + + // Create new buffer if previous was filled up + var pool = this._buffer.pool; + if (this._buffer.isFull) this._buffer.create(); + + assert(bytesRead >= 0); + + if (this === this.pair.cleartext) { + debug('cleartext.read succeed with %d bytes', bytesRead); + } else { + debug('encrypted.read succeed with %d bytes', bytesRead); + } + + // Try writing pending data + if (this._pending !== null) this._writePending(); + if (this._opposite._pending !== null) this._opposite._writePending(); + + if (bytesRead === 0) { + // EOF when cleartext has finished and we have nothing to read + if (this._opposite._finished && this._internallyPendingBytes() === 0) { + // Perform graceful shutdown + this._done(); + + // No half-open, sorry! + if (this === this.pair.cleartext) + this._opposite._done(); + + // EOF + this.push(null); + } else { + // Bail out + this.push(''); + } + } else { + // Give them requested data + if (this.ondata) { + var self = this; + this.ondata(pool, start, start + bytesRead); + + // Consume data automatically + // simple/test-https-drain fails without it + process.nextTick(function() { + self.read(bytesRead); + }); + } + this.push(pool.slice(start, start + bytesRead)); + } + + // Let users know that we've some internal data to read + var halfRead = this._internallyPendingBytes() !== 0; + + // Smart check to avoid invoking 'sslOutEnd' in the most of the cases + if (this._halfRead !== halfRead) { + this._halfRead = halfRead; + + // Notify listeners about internal data end + if (!halfRead) { + if (this === this.pair.cleartext) { + debug('cleartext.sslOutEnd'); + } else { + debug('encrypted.sslOutEnd'); + } + + this.emit('sslOutEnd'); + } + } +}; + + +CryptoStream.prototype.setTimeout = function(timeout, callback) { + if (this.socket) this.socket.setTimeout(timeout, callback); +}; + + +CryptoStream.prototype.setNoDelay = function(noDelay) { + if (this.socket) this.socket.setNoDelay(noDelay); +}; + + +CryptoStream.prototype.setKeepAlive = function(enable, initialDelay) { + if (this.socket) this.socket.setKeepAlive(enable, initialDelay); +}; + +CryptoStream.prototype.__defineGetter__('bytesWritten', function() { + return this.socket ? this.socket.bytesWritten : 0; +}); + +CryptoStream.prototype.getPeerCertificate = function() { + if (this.pair.ssl) { + var c = this.pair.ssl.getPeerCertificate(); + + if (c) { + if (c.issuer) c.issuer = tls.parseCertString(c.issuer); + if (c.subject) c.subject = tls.parseCertString(c.subject); + return c; + } + } + + return null; +}; + +CryptoStream.prototype.getSession = function() { + if (this.pair.ssl) { + return this.pair.ssl.getSession(); + } + + return null; +}; + +CryptoStream.prototype.isSessionReused = function() { + if (this.pair.ssl) { + return this.pair.ssl.isSessionReused(); + } + + return null; +}; + +CryptoStream.prototype.getCipher = function(err) { + if (this.pair.ssl) { + return this.pair.ssl.getCurrentCipher(); + } else { + return null; + } +}; + + +CryptoStream.prototype.end = function(chunk, encoding) { + if (this === this.pair.cleartext) { + debug('cleartext.end'); + } else { + debug('encrypted.end'); + } + + // Write pending data first + if (this._pending !== null) this._writePending(); + + this.writable = false; + + stream.Duplex.prototype.end.call(this, chunk, encoding); +}; + + +CryptoStream.prototype.destroySoon = function(err) { + if (this === this.pair.cleartext) { + debug('cleartext.destroySoon'); + } else { + debug('encrypted.destroySoon'); + } + + if (this.writable) + this.end(); + + if (this._writableState.finished && this._opposite._ended) { + this.destroy(); + } else { + // Wait for both `finish` and `end` events to ensure that all data that + // was written on this side was read from the other side. + var self = this; + var waiting = 2; + function finish() { + if (--waiting === 0) self.destroy(); + } + this._opposite.once('end', finish); + this.once('finish', finish); + } +}; + + +CryptoStream.prototype.destroy = function(err) { + if (this._destroyed) return; + this._destroyed = true; + this.readable = this.writable = false; + + // Destroy both ends + if (this === this.pair.cleartext) { + debug('cleartext.destroy'); + } else { + debug('encrypted.destroy'); + } + this._opposite.destroy(); + + var self = this; + process.nextTick(function() { + // Force EOF + self.push(null); + + // Emit 'close' event + self.emit('close', err ? true : false); + }); +}; + + +CryptoStream.prototype._done = function() { + this._doneFlag = true; + + if (this === this.pair.encrypted && !this.pair._secureEstablished) + return this.pair.error(); + + if (this.pair.cleartext._doneFlag && + this.pair.encrypted._doneFlag && + !this.pair._doneFlag) { + // If both streams are done: + this.pair.destroy(); + } +}; + + +// readyState is deprecated. Don't use it. +Object.defineProperty(CryptoStream.prototype, 'readyState', { + get: function() { + if (this._connecting) { + return 'opening'; + } else if (this.readable && this.writable) { + return 'open'; + } else if (this.readable && !this.writable) { + return 'readOnly'; + } else if (!this.readable && this.writable) { + return 'writeOnly'; + } else { + return 'closed'; + } + } +}); + + +function CleartextStream(pair, options) { + CryptoStream.call(this, pair, options); + + // This is a fake kludge to support how the http impl sits + // on top of net Sockets + var self = this; + this._handle = { + readStop: function() { + self._reading = false; + }, + readStart: function() { + if (self._reading && self._readableState.length > 0) return; + self._reading = true; + self.read(0); + if (self._opposite.readable) self._opposite.read(0); + } + }; +} +util.inherits(CleartextStream, CryptoStream); + + +CleartextStream.prototype._internallyPendingBytes = function() { + if (this.pair.ssl) { + return this.pair.ssl.clearPending(); + } else { + return 0; + } +}; + + +CleartextStream.prototype.address = function() { + return this.socket && this.socket.address(); +}; + + +CleartextStream.prototype.__defineGetter__('remoteAddress', function() { + return this.socket && this.socket.remoteAddress; +}); + + +CleartextStream.prototype.__defineGetter__('remotePort', function() { + return this.socket && this.socket.remotePort; +}); + + +CleartextStream.prototype.__defineGetter__('localAddress', function() { + return this.socket && this.socket.localAddress; +}); + + +CleartextStream.prototype.__defineGetter__('localPort', function() { + return this.socket && this.socket.localPort; +}); + + +function EncryptedStream(pair, options) { + CryptoStream.call(this, pair, options); +} +util.inherits(EncryptedStream, CryptoStream); + + +EncryptedStream.prototype._internallyPendingBytes = function() { + if (this.pair.ssl) { + return this.pair.ssl.encPending(); + } else { + return 0; + } +}; + + +function onhandshakestart() { + debug('onhandshakestart'); + + var self = this; + var ssl = self.ssl; + var now = Timer.now(); + + assert(now >= ssl.lastHandshakeTime); + + if ((now - ssl.lastHandshakeTime) >= tls.CLIENT_RENEG_WINDOW * 1000) { + ssl.handshakes = 0; + } + + var first = (ssl.lastHandshakeTime === 0); + ssl.lastHandshakeTime = now; + if (first) return; + + if (++ssl.handshakes > tls.CLIENT_RENEG_LIMIT) { + // Defer the error event to the next tick. We're being called from OpenSSL's + // state machine and OpenSSL is not re-entrant. We cannot allow the user's + // callback to destroy the connection right now, it would crash and burn. + setImmediate(function() { + var err = new Error('TLS session renegotiation attack detected.'); + if (self.cleartext) self.cleartext.emit('error', err); + }); + } +} + + +function onhandshakedone() { + // for future use + debug('onhandshakedone'); +} + + +function onclienthello(hello) { + var self = this, + once = false; + + this._resumingSession = true; + function callback(err, session) { + if (once) return; + once = true; + + if (err) return self.socket.destroy(err); + + self.ssl.loadSession(session); + + // Cycle data + self._resumingSession = false; + self.cleartext.read(0); + self.encrypted.read(0); + } + + if (hello.sessionId.length <= 0 || + !this.server || + !this.server.emit('resumeSession', hello.sessionId, callback)) { + callback(null, null); + } +} + + +function onnewsession(key, session) { + if (!this.server) return; + this.server.emit('newSession', key, session); +} + + +/** + * Provides a pair of streams to do encrypted communication. + */ + +function SecurePair(credentials, isServer, requestCert, rejectUnauthorized, + options) { + if (!(this instanceof SecurePair)) { + return new SecurePair(credentials, + isServer, + requestCert, + rejectUnauthorized, + options); + } + + var self = this; + + options || (options = {}); + + events.EventEmitter.call(this); + + this.server = options.server; + this._secureEstablished = false; + this._isServer = isServer ? true : false; + this._encWriteState = true; + this._clearWriteState = true; + this._doneFlag = false; + this._destroying = false; + + 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._rejectUnauthorized = rejectUnauthorized ? true : false; + this._requestCert = requestCert ? true : false; + + this.ssl = new Connection(this.credentials.context, + this._isServer ? true : false, + this._isServer ? this._requestCert : + options.servername, + this._rejectUnauthorized); + + if (this._isServer) { + this.ssl.onhandshakestart = onhandshakestart.bind(this); + this.ssl.onhandshakedone = onhandshakedone.bind(this); + this.ssl.onclienthello = onclienthello.bind(this); + this.ssl.onnewsession = onnewsession.bind(this); + this.ssl.lastHandshakeTime = 0; + this.ssl.handshakes = 0; + } + + if (process.features.tls_sni) { + if (this._isServer && options.SNICallback) { + this.ssl.setSNICallback(options.SNICallback); + } + this.servername = null; + } + + if (process.features.tls_npn && options.NPNProtocols) { + this.ssl.setNPNProtocols(options.NPNProtocols); + this.npnProtocol = null; + } + + /* Acts as a r/w stream to the cleartext side of the stream. */ + this.cleartext = new CleartextStream(this, options.cleartext); + + /* Acts as a r/w stream to the encrypted side of the stream. */ + this.encrypted = new EncryptedStream(this, options.encrypted); + + /* Let streams know about each other */ + this.cleartext._opposite = this.encrypted; + this.encrypted._opposite = this.cleartext; + this.cleartext.init(); + this.encrypted.init(); + + process.nextTick(function() { + /* The Connection may be destroyed by an abort call */ + if (self.ssl) { + self.ssl.start(); + } + }); +} + +util.inherits(SecurePair, events.EventEmitter); + + +exports.createSecurePair = function(credentials, + isServer, + requestCert, + rejectUnauthorized) { + var pair = new SecurePair(credentials, + isServer, + requestCert, + rejectUnauthorized); + return pair; +}; + + +SecurePair.prototype.maybeInitFinished = function() { + if (this.ssl && !this._secureEstablished && this.ssl.isInitFinished()) { + if (process.features.tls_npn) { + this.npnProtocol = this.ssl.getNegotiatedProtocol(); + } + + if (process.features.tls_sni) { + this.servername = this.ssl.getServername(); + } + + this._secureEstablished = true; + debug('secure established'); + this.emit('secure'); + } +}; + + +SecurePair.prototype.destroy = function() { + if (this._destroying) return; + + if (!this._doneFlag) { + debug('SecurePair.destroy'); + this._destroying = true; + + // SecurePair should be destroyed only after it's streams + this.cleartext.destroy(); + this.encrypted.destroy(); + + this._doneFlag = true; + this.ssl.error = null; + this.ssl.close(); + this.ssl = null; + } +}; + + +SecurePair.prototype.error = function(returnOnly) { + var err = this.ssl.error; + this.ssl.error = null; + + if (!this._secureEstablished) { + // Emit ECONNRESET instead of zero return + if (!err || err.message === 'ZERO_RETURN') { + var connReset = new Error('socket hang up'); + connReset.code = 'ECONNRESET'; + connReset.sslError = err && err.message; + + err = connReset; + } + this.destroy(); + if (!returnOnly) this.emit('error', err); + } else if (this._isServer && + this._rejectUnauthorized && + /peer did not return a certificate/.test(err.message)) { + // Not really an error. + this.destroy(); + } else { + if (!returnOnly) this.cleartext.emit('error', err); + } + return err; +}; diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js new file mode 100644 index 00000000000..e5b2e3bc404 --- /dev/null +++ b/lib/_tls_wrap.js @@ -0,0 +1,571 @@ +var assert = require('assert'); +var constants = require('constants'); +var crypto = require('crypto'); +var events = require('events'); +var net = require('net'); +var tls = require('tls'); +var util = require('util'); + +var Timer = process.binding('timer_wrap').Timer; +var tls_wrap = process.binding('tls_wrap'); + +var debug = util.debuglog('tls'); + +function onhandshakestart() { + debug('onhandshakestart'); + + var self = this; + var ssl = self.ssl; + var now = Timer.now(); + + assert(now >= ssl.lastHandshakeTime); + + if ((now - ssl.lastHandshakeTime) >= tls.CLIENT_RENEG_WINDOW * 1000) { + ssl.handshakes = 0; + } + + var first = (ssl.lastHandshakeTime === 0); + ssl.lastHandshakeTime = now; + if (first) return; + + if (++ssl.handshakes > tls.CLIENT_RENEG_LIMIT) { + // Defer the error event to the next tick. We're being called from OpenSSL's + // state machine and OpenSSL is not re-entrant. We cannot allow the user's + // callback to destroy the connection right now, it would crash and burn. + setImmediate(function() { + var err = new Error('TLS session renegotiation attack detected.'); + self._tlsError(err); + }); + } +} + + +function onhandshakedone() { + // for future use + debug('onhandshakedone'); + this._finishInit(); +} + + +/** + * Provides a wrap of socket stream to do encrypted communication. + */ + +function TLSSocket(socket, options) { + net.Socket.call(this, socket && { + handle: socket._handle, + allowHalfOpen: socket.allowHalfOpen, + readable: socket.readable, + writable: socket.writable + }); + + var self = this; + + this._tlsOptions = options; + this._secureEstablished = false; + this._controlReleased = false; + this.ssl = null; + this.servername = null; + this.npnProtocol = null; + this.authorized = false; + this.authorizationError = null; + + if (!this._handle) + this.once('connect', this._init.bind(this)); + else + this._init(); +} +util.inherits(TLSSocket, net.Socket); + +TLSSocket.prototype._init = function() { + assert(this._handle); + + // lib/net.js expect this value to be non-zero if write hasn't been flushed + // immediately + // TODO(indutny): rewise this solution, it might be 1 before handshake and + // repersent real writeQueueSize during regular writes. + this._handle.writeQueueSize = 1; + + var self = this; + var options = this._tlsOptions; + + // Wrap socket's handle + var credentials = options.credentials || crypto.createCredentials(); + this.ssl = tls_wrap.wrap(this._handle, credentials.context, options.isServer); + + // For clients, we will always have either a given ca list or be using + // default one + var requestCert = !!options.requestCert || !options.isServer, + rejectUnauthorized = !!options.rejectUnauthorized; + + if (requestCert || rejectUnauthorized) + this.ssl.setVerifyMode(requestCert, rejectUnauthorized); + + if (options.isServer) { + this.ssl.onhandshakestart = onhandshakestart.bind(this); + this.ssl.onhandshakedone = onhandshakedone.bind(this); + this.ssl.lastHandshakeTime = 0; + this.ssl.handshakes = 0; + } else { + this.ssl.onhandshakestart = function() {}; + this.ssl.onhandshakedone = this._finishInit.bind(this); + } + + this.ssl.onerror = function(err) { + // Destroy socket if error happened before handshake's finish + if (!this._secureEstablished) { + self._tlsError(err); + self.destroy(); + } else if (options.isServer && + rejectUnauthorized && + /peer did not return a certificate/.test(err.message)) { + // Ignore server's authorization errors + self.destroy(); + } else { + // Throw error + self._tlsError(err); + } + }; + + if (process.features.tls_sni && + options.isServer && + options.SNICallback && options.server._contexts.length) { + this.ssl.onsniselect = options.SNICallback; + } + + if (process.features.tls_npn && options.NPNProtocols) + this.ssl.setNPNProtocols(options.NPNProtocols); +}; + +TLSSocket.prototype._tlsError = function(err) { + this.emit('_tlsError', err); + if (this._controlReleased) + this.emit('error', err); +}; + +TLSSocket.prototype._finishInit = function() { + if (process.features.tls_npn) { + this.npnProtocol = this.ssl.getNegotiatedProtocol(); + } + + if (process.features.tls_sni && this._tlsOptions.isServer) { + this.servername = this.ssl.getServername(); + } + + debug('secure established'); + this._secureEstablished = true; + this.emit('secure'); +}; + +TLSSocket.prototype._start = function() { + this.ssl.start(); +}; + +TLSSocket.prototype.setServername = function(name) { + this.ssl.setServername(name); +}; + +TLSSocket.prototype.setSession = function(session) { + if (typeof session === 'string') + session = new Buffer(session, 'binary'); + this.ssl.setSession(session); +}; + +TLSSocket.prototype.getPeerCertificate = function() { + if (this.ssl) { + var c = this.ssl.getPeerCertificate(); + + if (c) { + if (c.issuer) c.issuer = tls.parseCertString(c.issuer); + if (c.subject) c.subject = tls.parseCertString(c.subject); + return c; + } + } + + return null; +}; + +TLSSocket.prototype.getSession = function() { + if (this.ssl) { + return this.ssl.getSession(); + } + + return null; +}; + +TLSSocket.prototype.isSessionReused = function() { + if (this.ssl) { + return this.ssl.isSessionReused(); + } + + return null; +}; + +TLSSocket.prototype.getCipher = function(err) { + if (this.ssl) { + return this.ssl.getCurrentCipher(); + } else { + return null; + } +}; + +// TODO: support anonymous (nocert) and PSK + + +// AUTHENTICATION MODES +// +// There are several levels of authentication that TLS/SSL supports. +// Read more about this in "man SSL_set_verify". +// +// 1. The server sends a certificate to the client but does not request a +// cert from the client. This is common for most HTTPS servers. The browser +// can verify the identity of the server, but the server does not know who +// the client is. Authenticating the client is usually done over HTTP using +// login boxes and cookies and stuff. +// +// 2. The server sends a cert to the client and requests that the client +// also send it a cert. The client knows who the server is and the server is +// requesting the client also identify themselves. There are several +// outcomes: +// +// A) verifyError returns null meaning the client's certificate is signed +// by one of the server's CAs. The server know's the client idenity now +// and the client is authorized. +// +// B) For some reason the client's certificate is not acceptable - +// verifyError returns a string indicating the problem. The server can +// either (i) reject the client or (ii) allow the client to connect as an +// unauthorized connection. +// +// The mode is controlled by two boolean variables. +// +// requestCert +// If true the server requests a certificate from client connections. For +// the common HTTPS case, users will want this to be false, which is what +// it defaults to. +// +// rejectUnauthorized +// If true clients whose certificates are invalid for any reason will not +// be allowed to make connections. If false, they will simply be marked as +// unauthorized but secure communication will continue. By default this is +// true. +// +// +// +// Options: +// - requestCert. Send verify request. Default to false. +// - rejectUnauthorized. Boolean, default to true. +// - key. string. +// - cert: string. +// - ca: string or array of strings. +// - sessionTimeout: integer. +// +// emit 'secureConnection' +// function (tlsSocket) { } +// +// "UNABLE_TO_GET_ISSUER_CERT", "UNABLE_TO_GET_CRL", +// "UNABLE_TO_DECRYPT_CERT_SIGNATURE", "UNABLE_TO_DECRYPT_CRL_SIGNATURE", +// "UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY", "CERT_SIGNATURE_FAILURE", +// "CRL_SIGNATURE_FAILURE", "CERT_NOT_YET_VALID" "CERT_HAS_EXPIRED", +// "CRL_NOT_YET_VALID", "CRL_HAS_EXPIRED" "ERROR_IN_CERT_NOT_BEFORE_FIELD", +// "ERROR_IN_CERT_NOT_AFTER_FIELD", "ERROR_IN_CRL_LAST_UPDATE_FIELD", +// "ERROR_IN_CRL_NEXT_UPDATE_FIELD", "OUT_OF_MEM", +// "DEPTH_ZERO_SELF_SIGNED_CERT", "SELF_SIGNED_CERT_IN_CHAIN", +// "UNABLE_TO_GET_ISSUER_CERT_LOCALLY", "UNABLE_TO_VERIFY_LEAF_SIGNATURE", +// "CERT_CHAIN_TOO_LONG", "CERT_REVOKED" "INVALID_CA", +// "PATH_LENGTH_EXCEEDED", "INVALID_PURPOSE" "CERT_UNTRUSTED", +// "CERT_REJECTED" +// +function Server(/* [options], listener */) { + var options, listener; + if (typeof arguments[0] == 'object') { + options = arguments[0]; + listener = arguments[1]; + } else if (typeof arguments[0] == 'function') { + options = {}; + listener = arguments[0]; + } + + if (!(this instanceof Server)) return new Server(options, listener); + + this._contexts = []; + + var self = this; + + // Handle option defaults: + this.setOptions(options); + + if (!self.pfx && (!self.cert || !self.key)) { + throw new Error('Missing PFX or certificate + private key.'); + } + + var sharedCreds = crypto.createCredentials({ + pfx: self.pfx, + key: self.key, + passphrase: self.passphrase, + cert: self.cert, + ca: self.ca, + ciphers: self.ciphers || tls.DEFAULT_CIPHERS, + secureProtocol: self.secureProtocol, + secureOptions: self.secureOptions, + crl: self.crl, + sessionIdContext: self.sessionIdContext + }); + + var timeout = options.handshakeTimeout || (120 * 1000); + + if (typeof timeout !== 'number') { + throw new TypeError('handshakeTimeout must be a number'); + } + + if (self.sessionTimeout) { + sharedCreds.context.setSessionTimeout(self.sessionTimeout); + } + + // constructor call + net.Server.call(this, function(raw_socket) { + var socket = new TLSSocket(raw_socket, { + credentials: sharedCreds, + isServer: true, + server: self, + requestCert: self.requestCert, + rejectUnauthorized: self.rejectUnauthorized, + NPNProtocols: self.NPNProtocols, + SNICallback: self.SNICallback + }); + + function listener() { + socket._tlsError(new Error('TLS handshake timeout')); + } + + if (timeout > 0) { + socket.setTimeout(timeout, listener); + } + + socket.once('secure', function() { + socket.setTimeout(0, listener); + + if (self.requestCert) { + var verifyError = socket.ssl.verifyError(); + if (verifyError) { + socket.authorizationError = verifyError.message; + + if (self.rejectUnauthorized) + socket.destroy(); + } else { + socket.authorized = true; + } + } + + if (!socket.destroyed) { + socket._controlReleased = true; + self.emit('secureConnection', socket); + } + }); + + socket.on('_tlsError', function(err) { + if (!socket._controlReleased) + self.emit('clientError', err, socket); + }); + }); + + if (listener) { + this.on('secureConnection', listener); + } +} + +util.inherits(Server, net.Server); +exports.Server = Server; +exports.createServer = function(options, listener) { + return new Server(options, listener); +}; + + +Server.prototype.setOptions = function(options) { + if (typeof options.requestCert == 'boolean') { + this.requestCert = options.requestCert; + } else { + this.requestCert = false; + } + + if (typeof options.rejectUnauthorized == 'boolean') { + this.rejectUnauthorized = options.rejectUnauthorized; + } else { + this.rejectUnauthorized = false; + } + + if (options.pfx) this.pfx = options.pfx; + if (options.key) this.key = options.key; + if (options.passphrase) this.passphrase = options.passphrase; + if (options.cert) this.cert = options.cert; + if (options.ca) this.ca = options.ca; + if (options.secureProtocol) this.secureProtocol = options.secureProtocol; + if (options.crl) this.crl = options.crl; + if (options.ciphers) this.ciphers = options.ciphers; + if (options.sessionTimeout) this.sessionTimeout = options.sessionTimeout; + var secureOptions = options.secureOptions || 0; + if (options.honorCipherOrder) { + secureOptions |= constants.SSL_OP_CIPHER_SERVER_PREFERENCE; + } + if (secureOptions) this.secureOptions = secureOptions; + if (options.NPNProtocols) tls.convertNPNProtocols(options.NPNProtocols, this); + if (options.SNICallback) { + this.SNICallback = options.SNICallback; + } else { + this.SNICallback = this.SNICallback.bind(this); + } + if (options.sessionIdContext) { + this.sessionIdContext = options.sessionIdContext; + } else if (this.requestCert) { + this.sessionIdContext = crypto.createHash('md5') + .update(process.argv.join(' ')) + .digest('hex'); + } +}; + +// SNI Contexts High-Level API +Server.prototype.addContext = function(servername, credentials) { + if (!servername) { + throw 'Servername is required parameter for Server.addContext'; + } + + var re = new RegExp('^' + + servername.replace(/([\.^$+?\-\\[\]{}])/g, '\\$1') + .replace(/\*/g, '.*') + + '$'); + this._contexts.push([re, crypto.createCredentials(credentials).context]); +}; + +Server.prototype.SNICallback = function(servername) { + var ctx; + + this._contexts.some(function(elem) { + if (servername.match(elem[0]) !== null) { + ctx = elem[1]; + return true; + } + }); + + return ctx; +}; + + +// Target API: +// +// var s = tls.connect({port: 8000, host: "google.com"}, function() { +// if (!s.authorized) { +// s.destroy(); +// return; +// } +// +// // s.socket; +// +// s.end("hello world\n"); +// }); +// +// +function normalizeConnectArgs(listArgs) { + var args = net._normalizeConnectArgs(listArgs); + var options = args[0]; + var cb = args[1]; + + if (typeof listArgs[1] === 'object') { + options = util._extend(options, listArgs[1]); + } else if (typeof listArgs[2] === 'object') { + options = util._extend(options, listArgs[2]); + } + + return (cb) ? [options, cb] : [options]; +} + +exports.connect = function(/* [port, host], options, cb */) { + var args = normalizeConnectArgs(arguments); + var options = args[0]; + var cb = args[1]; + + var defaults = { + rejectUnauthorized: '0' !== process.env.NODE_TLS_REJECT_UNAUTHORIZED + }; + options = util._extend(defaults, options || {}); + + var hostname = options.servername || options.host || 'localhost', + NPN = {}; + tls.convertNPNProtocols(options.NPNProtocols, NPN); + + var socket = new TLSSocket(options.socket, { + credentials: crypto.createCredentials(options), + isServer: false, + requestCert: true, + rejectUnauthorized: options.rejectUnauthorized, + NPNProtocols: NPN.NPNProtocols + }); + + function onHandle() { + socket._controlReleased = true; + + if (options.session) + socket.setSession(options.session); + + if (options.servername) + socket.setServername(options.servername); + + socket._start(); + socket.on('secure', function() { + var verifyError = socket.ssl.verifyError(); + + // Verify that server's identity matches it's certificate's names + if (!verifyError) { + var validCert = tls.checkServerIdentity(hostname, + socket.getPeerCertificate()); + if (!validCert) { + verifyError = new Error('Hostname/IP doesn\'t match certificate\'s ' + + 'altnames'); + } + } + + if (verifyError) { + socket.authorizationError = verifyError.message; + + if (options.rejectUnauthorized) { + socket.emit('error', verifyError); + socket.destroy(); + return; + } else { + socket.emit('secureConnect'); + } + } else { + socket.authorized = true; + socket.emit('secureConnect'); + } + + // Uncork incoming data + socket.removeListener('end', onHangUp); + }); + + function onHangUp() { + var error = new Error('socket hang up'); + error.code = 'ECONNRESET'; + socket.destroy(); + socket.emit('error', error); + } + socket.once('end', onHangUp); + } + if (socket._handle) + onHandle(); + else + socket.once('connect', onHandle); + + if (cb) + socket.once('secureConnect', cb); + + if (!options.socket) { + var connect_opt = (options.path && !options.port) ? {path: options.path} : { + port: options.port, + host: options.host, + localAddress: options.localAddress + }; + socket.connect(connect_opt); + } + + return socket; +}; diff --git a/lib/https.js b/lib/https.js index fe329ec76ec..8631d0d0140 100644 --- a/lib/https.js +++ b/lib/https.js @@ -41,7 +41,7 @@ function Server(opts, requestListener) { } this.addListener('clientError', function(err, conn) { - conn.destroy(err); + conn.destroy(); }); this.timeout = 2 * 60 * 1000; diff --git a/lib/tls.js b/lib/tls.js index f5b175fb07b..e199cc57054 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -19,19 +19,13 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -var crypto = require('crypto'); -var util = require('util'); var net = require('net'); var url = require('url'); -var events = require('events'); -var stream = require('stream'); -var assert = require('assert').ok; -var constants = require('constants'); +var util = require('util'); -var Timer = process.binding('timer_wrap').Timer; - -var DEFAULT_CIPHERS = 'ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:' + // TLS 1.2 - 'RC4:HIGH:!MD5:!aNULL:!EDH'; // TLS 1.0 +exports.DEFAULT_CIPHERS = + 'ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:' + // TLS 1.2 + 'RC4:HIGH:!MD5:!aNULL:!EDH'; // TLS 1.0 // Allow {CLIENT_RENEG_LIMIT} client-initiated session renegotiations // every {CLIENT_RENEG_WINDOW} seconds. An error event is emitted if more @@ -53,19 +47,9 @@ exports.getCiphers = function() { return Object.getOwnPropertyNames(ctx).sort(); }; - -var debug = util.debuglog('tls'); - -var Connection = null; -try { - Connection = process.binding('crypto').Connection; -} catch (e) { - throw new Error('node.js not compiled with openssl crypto support.'); -} - // Convert protocols array into valid OpenSSL protocols list // ("\x06spdy/2\x08http/1.1\x08http/1.0") -function convertNPNProtocols(NPNProtocols, out) { +exports.convertNPNProtocols = function convertNPNProtocols(NPNProtocols, out) { // If NPNProtocols is Array - translate it into buffer if (Array.isArray(NPNProtocols)) { var buff = new Buffer(NPNProtocols.reduce(function(p, c) { @@ -87,10 +71,9 @@ function convertNPNProtocols(NPNProtocols, out) { if (Buffer.isBuffer(NPNProtocols)) { out.NPNProtocols = NPNProtocols; } -} +}; - -function checkServerIdentity(host, cert) { +exports.checkServerIdentity = function checkServerIdentity(host, cert) { // Create regexp to much hostnames function regexpify(host, wildcards) { // Add trailing dot (make hostnames uniform) @@ -198,357 +181,11 @@ function checkServerIdentity(host, cert) { } return valid; -} -exports.checkServerIdentity = checkServerIdentity; - - -function SlabBuffer() { - this.create(); -} - - -SlabBuffer.prototype.create = function create() { - this.isFull = false; - this.pool = new Buffer(exports.SLAB_BUFFER_SIZE); - this.offset = 0; - this.remaining = this.pool.length; }; - -SlabBuffer.prototype.use = function use(context, fn, size) { - if (this.remaining === 0) { - this.isFull = true; - return 0; - } - - var actualSize = this.remaining; - - if (size !== null) actualSize = Math.min(size, actualSize); - - var bytes = fn.call(context, this.pool, this.offset, actualSize); - if (bytes > 0) { - this.offset += bytes; - this.remaining -= bytes; - } - - assert(this.remaining >= 0); - - return bytes; -}; - - -var slabBuffer = null; - - -// Base class of both CleartextStream and EncryptedStream -function CryptoStream(pair, options) { - stream.Duplex.call(this, options); - - this.pair = pair; - this._pending = null; - this._pendingEncoding = ''; - this._pendingCallback = null; - this._doneFlag = false; - this._retryAfterPartial = false; - this._halfRead = false; - this._sslOutCb = null; - this._resumingSession = false; - this._reading = true; - this._destroyed = false; - this._ended = false; - this._finished = false; - this._opposite = null; - - if (slabBuffer === null) slabBuffer = new SlabBuffer(); - this._buffer = slabBuffer; - - this.once('finish', onCryptoStreamFinish); - - // net.Socket calls .onend too - this.once('end', onCryptoStreamEnd); -} -util.inherits(CryptoStream, stream.Duplex); - - -function onCryptoStreamFinish() { - this._finished = true; - - if (this === this.pair.cleartext) { - debug('cleartext.onfinish'); - if (this.pair.ssl) { - // Generate close notify - // NOTE: first call checks if client has sent us shutdown, - // second call enqueues shutdown into the BIO. - if (this.pair.ssl.shutdown() !== 1) { - if (this.pair.ssl && this.pair.ssl.error) - return this.pair.error(); - - this.pair.ssl.shutdown(); - } - - if (this.pair.ssl && this.pair.ssl.error) - return this.pair.error(); - } - } else { - debug('encrypted.onfinish'); - } - - // Try to read just to get sure that we won't miss EOF - if (this._opposite.readable) this._opposite.read(0); - - if (this._opposite._ended) { - this._done(); - - // No half-close, sorry - if (this === this.pair.cleartext) this._opposite._done(); - } -} - - -function onCryptoStreamEnd() { - this._ended = true; - if (this === this.pair.cleartext) { - debug('cleartext.onend'); - } else { - debug('encrypted.onend'); - } - - if (this.onend) this.onend(); -} - - -// NOTE: Called once `this._opposite` is set. -CryptoStream.prototype.init = function init() { - var self = this; - this._opposite.on('sslOutEnd', function() { - if (self._sslOutCb) { - var cb = self._sslOutCb; - self._sslOutCb = null; - cb(null); - } - }); -}; - - -CryptoStream.prototype._write = function write(data, encoding, cb) { - assert(this._pending === null); - - // Black-hole data - if (!this.pair.ssl) return cb(null); - - // When resuming session don't accept any new data. - // And do not put too much data into openssl, before writing it from encrypted - // side. - // - // TODO(indutny): Remove magic number, use watermark based limits - if (!this._resumingSession && - this._opposite._internallyPendingBytes() < 128 * 1024) { - // Write current buffer now - var written; - if (this === this.pair.cleartext) { - debug('cleartext.write called with %d bytes', data.length); - written = this.pair.ssl.clearIn(data, 0, data.length); - } else { - debug('encrypted.write called with %d bytes', data.length); - written = this.pair.ssl.encIn(data, 0, data.length); - } - - // Handle and report errors - if (this.pair.ssl && this.pair.ssl.error) { - return cb(this.pair.error(true)); - } - - // Force SSL_read call to cycle some states/data inside OpenSSL - this.pair.cleartext.read(0); - - // Cycle encrypted data - if (this.pair.encrypted._internallyPendingBytes()) - this.pair.encrypted.read(0); - - // Get NPN and Server name when ready - this.pair.maybeInitFinished(); - - // Whole buffer was written - if (written === data.length) { - if (this === this.pair.cleartext) { - debug('cleartext.write succeed with ' + written + ' bytes'); - } else { - debug('encrypted.write succeed with ' + written + ' bytes'); - } - - // Invoke callback only when all data read from opposite stream - if (this._opposite._halfRead) { - assert(this._sslOutCb === null); - this._sslOutCb = cb; - } else { - cb(null); - } - return; - } else if (written !== 0 && written !== -1) { - assert(!this._retryAfterPartial); - this._retryAfterPartial = true; - this._write(data.slice(written), encoding, cb); - this._retryAfterPartial = false; - return; - } - } else { - debug('cleartext.write queue is full'); - - // Force SSL_read call to cycle some states/data inside OpenSSL - this.pair.cleartext.read(0); - } - - // No write has happened - this._pending = data; - this._pendingEncoding = encoding; - this._pendingCallback = cb; - - if (this === this.pair.cleartext) { - debug('cleartext.write queued with %d bytes', data.length); - } else { - debug('encrypted.write queued with %d bytes', data.length); - } -}; - - -CryptoStream.prototype._writePending = function writePending() { - var data = this._pending, - encoding = this._pendingEncoding, - cb = this._pendingCallback; - - this._pending = null; - this._pendingEncoding = ''; - this._pendingCallback = null; - this._write(data, encoding, cb); -}; - - -CryptoStream.prototype._read = function read(size) { - // XXX: EOF?! - if (!this.pair.ssl) return this.push(null); - - // Wait for session to be resumed - // Mark that we're done reading, but don't provide data or EOF - if (this._resumingSession || !this._reading) return this.push(''); - - var out; - if (this === this.pair.cleartext) { - debug('cleartext.read called with %d bytes', size); - out = this.pair.ssl.clearOut; - } else { - debug('encrypted.read called with %d bytes', size); - out = this.pair.ssl.encOut; - } - - var bytesRead = 0, - start = this._buffer.offset; - do { - var read = this._buffer.use(this.pair.ssl, out, size); - if (read > 0) { - bytesRead += read; - size -= read; - } - - // Handle and report errors - if (this.pair.ssl && this.pair.ssl.error) { - this.pair.error(); - break; - } - - // Get NPN and Server name when ready - this.pair.maybeInitFinished(); - } while (read > 0 && !this._buffer.isFull && bytesRead < size); - - // Create new buffer if previous was filled up - var pool = this._buffer.pool; - if (this._buffer.isFull) this._buffer.create(); - - assert(bytesRead >= 0); - - if (this === this.pair.cleartext) { - debug('cleartext.read succeed with %d bytes', bytesRead); - } else { - debug('encrypted.read succeed with %d bytes', bytesRead); - } - - // Try writing pending data - if (this._pending !== null) this._writePending(); - if (this._opposite._pending !== null) this._opposite._writePending(); - - if (bytesRead === 0) { - // EOF when cleartext has finished and we have nothing to read - if (this._opposite._finished && this._internallyPendingBytes() === 0) { - // Perform graceful shutdown - this._done(); - - // No half-open, sorry! - if (this === this.pair.cleartext) - this._opposite._done(); - - // EOF - this.push(null); - } else { - // Bail out - this.push(''); - } - } else { - // Give them requested data - if (this.ondata) { - var self = this; - this.ondata(pool, start, start + bytesRead); - - // Consume data automatically - // simple/test-https-drain fails without it - process.nextTick(function() { - self.read(bytesRead); - }); - } - this.push(pool.slice(start, start + bytesRead)); - } - - // Let users know that we've some internal data to read - var halfRead = this._internallyPendingBytes() !== 0; - - // Smart check to avoid invoking 'sslOutEnd' in the most of the cases - if (this._halfRead !== halfRead) { - this._halfRead = halfRead; - - // Notify listeners about internal data end - if (!halfRead) { - if (this === this.pair.cleartext) { - debug('cleartext.sslOutEnd'); - } else { - debug('encrypted.sslOutEnd'); - } - - this.emit('sslOutEnd'); - } - } -}; - - -CryptoStream.prototype.setTimeout = function(timeout, callback) { - if (this.socket) this.socket.setTimeout(timeout, callback); -}; - - -CryptoStream.prototype.setNoDelay = function(noDelay) { - if (this.socket) this.socket.setNoDelay(noDelay); -}; - - -CryptoStream.prototype.setKeepAlive = function(enable, initialDelay) { - if (this.socket) this.socket.setKeepAlive(enable, initialDelay); -}; - -CryptoStream.prototype.__defineGetter__('bytesWritten', function() { - return this.socket ? this.socket.bytesWritten : 0; -}); - - // Example: // C=US\nST=CA\nL=SF\nO=Joyent\nOU=Node.js\nCN=ca1\nemailAddress=ry@clouds.org -function parseCertString(s) { +exports.parseCertString = function parseCertString(s) { var out = {}; var parts = s.split('\n'); for (var i = 0, len = parts.length; i < len; i++) { @@ -567,886 +204,14 @@ function parseCertString(s) { } } return out; -} - - -CryptoStream.prototype.getPeerCertificate = function() { - if (this.pair.ssl) { - var c = this.pair.ssl.getPeerCertificate(); - - if (c) { - if (c.issuer) c.issuer = parseCertString(c.issuer); - if (c.subject) c.subject = parseCertString(c.subject); - return c; - } - } - - return null; }; -CryptoStream.prototype.getSession = function() { - if (this.pair.ssl) { - return this.pair.ssl.getSession(); - } - - return null; -}; - -CryptoStream.prototype.isSessionReused = function() { - if (this.pair.ssl) { - return this.pair.ssl.isSessionReused(); - } - - return null; -}; - -CryptoStream.prototype.getCipher = function(err) { - if (this.pair.ssl) { - return this.pair.ssl.getCurrentCipher(); - } else { - return null; - } -}; - - -CryptoStream.prototype.end = function(chunk, encoding) { - if (this === this.pair.cleartext) { - debug('cleartext.end'); - } else { - debug('encrypted.end'); - } - - // Write pending data first - if (this._pending !== null) this._writePending(); - - this.writable = false; - - stream.Duplex.prototype.end.call(this, chunk, encoding); -}; - - -CryptoStream.prototype.destroySoon = function(err) { - if (this === this.pair.cleartext) { - debug('cleartext.destroySoon'); - } else { - debug('encrypted.destroySoon'); - } - - if (this.writable) - this.end(); - - if (this._writableState.finished && this._opposite._ended) { - this.destroy(); - } else { - // Wait for both `finish` and `end` events to ensure that all data that - // was written on this side was read from the other side. - var self = this; - var waiting = 2; - function finish() { - if (--waiting === 0) self.destroy(); - } - this._opposite.once('end', finish); - this.once('finish', finish); - } -}; - - -CryptoStream.prototype.destroy = function(err) { - if (this._destroyed) return; - this._destroyed = true; - this.readable = this.writable = false; - - // Destroy both ends - if (this === this.pair.cleartext) { - debug('cleartext.destroy'); - } else { - debug('encrypted.destroy'); - } - this._opposite.destroy(); - - var self = this; - process.nextTick(function() { - // Force EOF - self.push(null); - - // Emit 'close' event - self.emit('close', err ? true : false); - }); -}; - - -CryptoStream.prototype._done = function() { - this._doneFlag = true; - - if (this === this.pair.encrypted && !this.pair._secureEstablished) - return this.pair.error(); - - if (this.pair.cleartext._doneFlag && - this.pair.encrypted._doneFlag && - !this.pair._doneFlag) { - // If both streams are done: - this.pair.destroy(); - } -}; - - -// readyState is deprecated. Don't use it. -Object.defineProperty(CryptoStream.prototype, 'readyState', { - get: function() { - if (this._connecting) { - return 'opening'; - } else if (this.readable && this.writable) { - return 'open'; - } else if (this.readable && !this.writable) { - return 'readOnly'; - } else if (!this.readable && this.writable) { - return 'writeOnly'; - } else { - return 'closed'; - } - } -}); - - -function CleartextStream(pair, options) { - CryptoStream.call(this, pair, options); - - // This is a fake kludge to support how the http impl sits - // on top of net Sockets - var self = this; - this._handle = { - readStop: function() { - self._reading = false; - }, - readStart: function() { - if (self._reading && self._readableState.length > 0) return; - self._reading = true; - self.read(0); - if (self._opposite.readable) self._opposite.read(0); - } - }; -} -util.inherits(CleartextStream, CryptoStream); - - -CleartextStream.prototype._internallyPendingBytes = function() { - if (this.pair.ssl) { - return this.pair.ssl.clearPending(); - } else { - return 0; - } -}; - - -CleartextStream.prototype.address = function() { - return this.socket && this.socket.address(); -}; - - -CleartextStream.prototype.__defineGetter__('remoteAddress', function() { - return this.socket && this.socket.remoteAddress; -}); - - -CleartextStream.prototype.__defineGetter__('remotePort', function() { - return this.socket && this.socket.remotePort; -}); - - -CleartextStream.prototype.__defineGetter__('localAddress', function() { - return this.socket && this.socket.localAddress; -}); - - -CleartextStream.prototype.__defineGetter__('localPort', function() { - return this.socket && this.socket.localPort; -}); - - -function EncryptedStream(pair, options) { - CryptoStream.call(this, pair, options); -} -util.inherits(EncryptedStream, CryptoStream); - - -EncryptedStream.prototype._internallyPendingBytes = function() { - if (this.pair.ssl) { - return this.pair.ssl.encPending(); - } else { - return 0; - } -}; - - -function onhandshakestart() { - debug('onhandshakestart'); - - var self = this; - var ssl = self.ssl; - var now = Timer.now(); - - assert(now >= ssl.lastHandshakeTime); - - if ((now - ssl.lastHandshakeTime) >= exports.CLIENT_RENEG_WINDOW * 1000) { - ssl.handshakes = 0; - } - - var first = (ssl.lastHandshakeTime === 0); - ssl.lastHandshakeTime = now; - if (first) return; - - if (++ssl.handshakes > exports.CLIENT_RENEG_LIMIT) { - // Defer the error event to the next tick. We're being called from OpenSSL's - // state machine and OpenSSL is not re-entrant. We cannot allow the user's - // callback to destroy the connection right now, it would crash and burn. - setImmediate(function() { - var err = new Error('TLS session renegotiation attack detected.'); - if (self.cleartext) self.cleartext.emit('error', err); - }); - } -} - - -function onhandshakedone() { - // for future use - debug('onhandshakedone'); -} - - -function onclienthello(hello) { - var self = this, - once = false; - - this._resumingSession = true; - function callback(err, session) { - if (once) return; - once = true; - - if (err) return self.socket.destroy(err); - - self.ssl.loadSession(session); - - // Cycle data - self._resumingSession = false; - self.cleartext.read(0); - self.encrypted.read(0); - } - - if (hello.sessionId.length <= 0 || - !this.server || - !this.server.emit('resumeSession', hello.sessionId, callback)) { - callback(null, null); - } -} - - -function onnewsession(key, session) { - if (!this.server) return; - this.server.emit('newSession', key, session); -} - - -/** - * Provides a pair of streams to do encrypted communication. - */ - -function SecurePair(credentials, isServer, requestCert, rejectUnauthorized, - options) { - if (!(this instanceof SecurePair)) { - return new SecurePair(credentials, - isServer, - requestCert, - rejectUnauthorized, - options); - } - - var self = this; - - options || (options = {}); - - events.EventEmitter.call(this); - - this.server = options.server; - this._secureEstablished = false; - this._isServer = isServer ? true : false; - this._encWriteState = true; - this._clearWriteState = true; - this._doneFlag = false; - this._destroying = false; - - 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._rejectUnauthorized = rejectUnauthorized ? true : false; - this._requestCert = requestCert ? true : false; - - this.ssl = new Connection(this.credentials.context, - this._isServer ? true : false, - this._isServer ? this._requestCert : - options.servername, - this._rejectUnauthorized); - - if (this._isServer) { - this.ssl.onhandshakestart = onhandshakestart.bind(this); - this.ssl.onhandshakedone = onhandshakedone.bind(this); - this.ssl.onclienthello = onclienthello.bind(this); - this.ssl.onnewsession = onnewsession.bind(this); - this.ssl.lastHandshakeTime = 0; - this.ssl.handshakes = 0; - } - - if (process.features.tls_sni) { - if (this._isServer && options.SNICallback) { - this.ssl.setSNICallback(options.SNICallback); - } - this.servername = null; - } - - if (process.features.tls_npn && options.NPNProtocols) { - this.ssl.setNPNProtocols(options.NPNProtocols); - this.npnProtocol = null; - } - - /* Acts as a r/w stream to the cleartext side of the stream. */ - this.cleartext = new CleartextStream(this, options.cleartext); - - /* Acts as a r/w stream to the encrypted side of the stream. */ - this.encrypted = new EncryptedStream(this, options.encrypted); - - /* Let streams know about each other */ - this.cleartext._opposite = this.encrypted; - this.encrypted._opposite = this.cleartext; - this.cleartext.init(); - this.encrypted.init(); - - process.nextTick(function() { - /* The Connection may be destroyed by an abort call */ - if (self.ssl) { - self.ssl.start(); - } - }); -} - -util.inherits(SecurePair, events.EventEmitter); - - -exports.createSecurePair = function(credentials, - isServer, - requestCert, - rejectUnauthorized) { - var pair = new SecurePair(credentials, - isServer, - requestCert, - rejectUnauthorized); - return pair; -}; - - -SecurePair.prototype.maybeInitFinished = function() { - if (this.ssl && !this._secureEstablished && this.ssl.isInitFinished()) { - if (process.features.tls_npn) { - this.npnProtocol = this.ssl.getNegotiatedProtocol(); - } - - if (process.features.tls_sni) { - this.servername = this.ssl.getServername(); - } - - this._secureEstablished = true; - debug('secure established'); - this.emit('secure'); - } -}; - - -SecurePair.prototype.destroy = function() { - if (this._destroying) return; - - if (!this._doneFlag) { - debug('SecurePair.destroy'); - this._destroying = true; - - // SecurePair should be destroyed only after it's streams - this.cleartext.destroy(); - this.encrypted.destroy(); - - this._doneFlag = true; - this.ssl.error = null; - this.ssl.close(); - this.ssl = null; - } -}; - - -SecurePair.prototype.error = function(returnOnly) { - var err = this.ssl.error; - this.ssl.error = null; - - if (!this._secureEstablished) { - // Emit ECONNRESET instead of zero return - if (!err || err.message === 'ZERO_RETURN') { - var connReset = new Error('socket hang up'); - connReset.code = 'ECONNRESET'; - connReset.sslError = err && err.message; - - err = connReset; - } - this.destroy(); - if (!returnOnly) this.emit('error', err); - } else if (this._isServer && - this._rejectUnauthorized && - /peer did not return a certificate/.test(err.message)) { - // Not really an error. - this.destroy(); - } else { - if (!returnOnly) this.cleartext.emit('error', err); - } - return err; -}; - -// TODO: support anonymous (nocert) and PSK - - -// AUTHENTICATION MODES -// -// There are several levels of authentication that TLS/SSL supports. -// Read more about this in "man SSL_set_verify". -// -// 1. The server sends a certificate to the client but does not request a -// cert from the client. This is common for most HTTPS servers. The browser -// can verify the identity of the server, but the server does not know who -// the client is. Authenticating the client is usually done over HTTP using -// login boxes and cookies and stuff. -// -// 2. The server sends a cert to the client and requests that the client -// also send it a cert. The client knows who the server is and the server is -// requesting the client also identify themselves. There are several -// outcomes: -// -// A) verifyError returns null meaning the client's certificate is signed -// by one of the server's CAs. The server know's the client idenity now -// and the client is authorized. -// -// B) For some reason the client's certificate is not acceptable - -// verifyError returns a string indicating the problem. The server can -// either (i) reject the client or (ii) allow the client to connect as an -// unauthorized connection. -// -// The mode is controlled by two boolean variables. -// -// requestCert -// If true the server requests a certificate from client connections. For -// the common HTTPS case, users will want this to be false, which is what -// it defaults to. -// -// rejectUnauthorized -// If true clients whose certificates are invalid for any reason will not -// be allowed to make connections. If false, they will simply be marked as -// unauthorized but secure communication will continue. By default this is -// true. -// -// -// -// Options: -// - requestCert. Send verify request. Default to false. -// - rejectUnauthorized. Boolean, default to true. -// - key. string. -// - cert: string. -// - ca: string or array of strings. -// - sessionTimeout: integer. -// -// emit 'secureConnection' -// function (cleartextStream, encryptedStream) { } -// -// 'cleartextStream' has the boolean property 'authorized' to determine if -// it was verified by the CA. If 'authorized' is false, a property -// 'authorizationError' is set on cleartextStream and has the possible -// values: -// -// "UNABLE_TO_GET_ISSUER_CERT", "UNABLE_TO_GET_CRL", -// "UNABLE_TO_DECRYPT_CERT_SIGNATURE", "UNABLE_TO_DECRYPT_CRL_SIGNATURE", -// "UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY", "CERT_SIGNATURE_FAILURE", -// "CRL_SIGNATURE_FAILURE", "CERT_NOT_YET_VALID" "CERT_HAS_EXPIRED", -// "CRL_NOT_YET_VALID", "CRL_HAS_EXPIRED" "ERROR_IN_CERT_NOT_BEFORE_FIELD", -// "ERROR_IN_CERT_NOT_AFTER_FIELD", "ERROR_IN_CRL_LAST_UPDATE_FIELD", -// "ERROR_IN_CRL_NEXT_UPDATE_FIELD", "OUT_OF_MEM", -// "DEPTH_ZERO_SELF_SIGNED_CERT", "SELF_SIGNED_CERT_IN_CHAIN", -// "UNABLE_TO_GET_ISSUER_CERT_LOCALLY", "UNABLE_TO_VERIFY_LEAF_SIGNATURE", -// "CERT_CHAIN_TOO_LONG", "CERT_REVOKED" "INVALID_CA", -// "PATH_LENGTH_EXCEEDED", "INVALID_PURPOSE" "CERT_UNTRUSTED", -// "CERT_REJECTED" -// -// -// TODO: -// cleartext.credentials (by mirroring from pair object) -// cleartext.getCertificate() (by mirroring from pair.credentials.context) -function Server(/* [options], listener */) { - var options, listener; - if (typeof arguments[0] == 'object') { - options = arguments[0]; - listener = arguments[1]; - } else if (typeof arguments[0] == 'function') { - options = {}; - listener = arguments[0]; - } - - if (!(this instanceof Server)) return new Server(options, listener); - - this._contexts = []; - - var self = this; - - // Handle option defaults: - this.setOptions(options); - - if (!self.pfx && (!self.cert || !self.key)) { - throw new Error('Missing PFX or certificate + private key.'); - } - - var sharedCreds = crypto.createCredentials({ - pfx: self.pfx, - key: self.key, - passphrase: self.passphrase, - cert: self.cert, - ca: self.ca, - ciphers: self.ciphers || DEFAULT_CIPHERS, - secureProtocol: self.secureProtocol, - secureOptions: self.secureOptions, - crl: self.crl, - sessionIdContext: self.sessionIdContext - }); - - var timeout = options.handshakeTimeout || (120 * 1000); - - if (typeof timeout !== 'number') { - throw new TypeError('handshakeTimeout must be a number'); - } - - if (self.sessionTimeout) { - sharedCreds.context.setSessionTimeout(self.sessionTimeout); - } - - // constructor call - net.Server.call(this, function(socket) { - var creds = crypto.createCredentials(null, sharedCreds.context); - - var pair = new SecurePair(creds, - true, - self.requestCert, - self.rejectUnauthorized, - { - server: self, - NPNProtocols: self.NPNProtocols, - SNICallback: self.SNICallback, - - // Stream options - cleartext: self._cleartext, - encrypted: self._encrypted - }); - - var cleartext = pipe(pair, socket); - cleartext._controlReleased = false; - - function listener() { - pair.emit('error', new Error('TLS handshake timeout')); - } - - if (timeout > 0) { - socket.setTimeout(timeout, listener); - } - - pair.once('secure', function() { - socket.setTimeout(0, listener); - - pair.cleartext.authorized = false; - pair.cleartext.npnProtocol = pair.npnProtocol; - pair.cleartext.servername = pair.servername; - - if (!self.requestCert) { - cleartext._controlReleased = true; - self.emit('secureConnection', pair.cleartext, pair.encrypted); - } else { - var verifyError = pair.ssl.verifyError(); - if (verifyError) { - pair.cleartext.authorizationError = verifyError.message; - - if (self.rejectUnauthorized) { - socket.destroy(); - pair.destroy(); - } else { - cleartext._controlReleased = true; - self.emit('secureConnection', pair.cleartext, pair.encrypted); - } - } else { - pair.cleartext.authorized = true; - cleartext._controlReleased = true; - self.emit('secureConnection', pair.cleartext, pair.encrypted); - } - } - }); - pair.on('error', function(err) { - self.emit('clientError', err, this); - }); - }); - - if (listener) { - this.on('secureConnection', listener); - } -} - -util.inherits(Server, net.Server); -exports.Server = Server; -exports.createServer = function(options, listener) { - return new Server(options, listener); -}; - - -Server.prototype.setOptions = function(options) { - if (typeof options.requestCert == 'boolean') { - this.requestCert = options.requestCert; - } else { - this.requestCert = false; - } - - if (typeof options.rejectUnauthorized == 'boolean') { - this.rejectUnauthorized = options.rejectUnauthorized; - } else { - this.rejectUnauthorized = false; - } - - if (options.pfx) this.pfx = options.pfx; - if (options.key) this.key = options.key; - if (options.passphrase) this.passphrase = options.passphrase; - if (options.cert) this.cert = options.cert; - if (options.ca) this.ca = options.ca; - if (options.secureProtocol) this.secureProtocol = options.secureProtocol; - if (options.crl) this.crl = options.crl; - if (options.ciphers) this.ciphers = options.ciphers; - if (options.sessionTimeout) this.sessionTimeout = options.sessionTimeout; - var secureOptions = options.secureOptions || 0; - if (options.honorCipherOrder) { - secureOptions |= constants.SSL_OP_CIPHER_SERVER_PREFERENCE; - } - if (secureOptions) this.secureOptions = secureOptions; - if (options.NPNProtocols) convertNPNProtocols(options.NPNProtocols, this); - if (options.SNICallback) { - this.SNICallback = options.SNICallback; - } else { - this.SNICallback = this.SNICallback.bind(this); - } - if (options.sessionIdContext) { - this.sessionIdContext = options.sessionIdContext; - } else if (this.requestCert) { - this.sessionIdContext = crypto.createHash('md5') - .update(process.argv.join(' ')) - .digest('hex'); - } - if (options.cleartext) this.cleartext = options.cleartext; - if (options.encrypted) this.encrypted = options.encrypted; -}; - -// SNI Contexts High-Level API -Server.prototype.addContext = function(servername, credentials) { - if (!servername) { - throw 'Servername is required parameter for Server.addContext'; - } - - var re = new RegExp('^' + - servername.replace(/([\.^$+?\-\\[\]{}])/g, '\\$1') - .replace(/\*/g, '.*') + - '$'); - this._contexts.push([re, crypto.createCredentials(credentials).context]); -}; - -Server.prototype.SNICallback = function(servername) { - var ctx; - - this._contexts.some(function(elem) { - if (servername.match(elem[0]) !== null) { - ctx = elem[1]; - return true; - } - }); - - return ctx; -}; - - -// Target API: -// -// var s = tls.connect({port: 8000, host: "google.com"}, function() { -// if (!s.authorized) { -// s.destroy(); -// return; -// } -// -// // s.socket; -// -// s.end("hello world\n"); -// }); -// -// -function normalizeConnectArgs(listArgs) { - var args = net._normalizeConnectArgs(listArgs); - var options = args[0]; - var cb = args[1]; - - if (typeof listArgs[1] === 'object') { - options = util._extend(options, listArgs[1]); - } else if (typeof listArgs[2] === 'object') { - options = util._extend(options, listArgs[2]); - } - - return (cb) ? [options, cb] : [options]; -} - -exports.connect = function(/* [port, host], options, cb */) { - var args = normalizeConnectArgs(arguments); - var options = args[0]; - var cb = args[1]; - - var defaults = { - rejectUnauthorized: '0' !== process.env.NODE_TLS_REJECT_UNAUTHORIZED - }; - options = util._extend(defaults, options || {}); - - var socket = options.socket ? options.socket : new net.Stream(); - - var sslcontext = crypto.createCredentials(options); - - convertNPNProtocols(options.NPNProtocols, this); - var hostname = options.servername || options.host || 'localhost', - pair = new SecurePair(sslcontext, false, true, - options.rejectUnauthorized === true ? true : false, - { - NPNProtocols: this.NPNProtocols, - servername: hostname, - cleartext: options.cleartext, - encrypted: options.encrypted - }); - - if (options.session) { - var session = options.session; - if (typeof session === 'string') - session = new Buffer(session, 'binary'); - pair.ssl.setSession(session); - } - - var cleartext = pipe(pair, socket); - if (cb) { - cleartext.once('secureConnect', cb); - } - - if (!options.socket) { - var connect_opt = (options.path && !options.port) ? {path: options.path} : { - port: options.port, - host: options.host, - localAddress: options.localAddress - }; - socket.connect(connect_opt); - } - - pair.on('secure', function() { - var verifyError = pair.ssl.verifyError(); - - cleartext.npnProtocol = pair.npnProtocol; - - // Verify that server's identity matches it's certificate's names - if (!verifyError) { - var validCert = checkServerIdentity(hostname, - pair.cleartext.getPeerCertificate()); - if (!validCert) { - verifyError = new Error('Hostname/IP doesn\'t match certificate\'s ' + - 'altnames'); - } - } - - if (verifyError) { - cleartext.authorized = false; - cleartext.authorizationError = verifyError.message; - - if (pair._rejectUnauthorized) { - cleartext.emit('error', verifyError); - pair.destroy(); - } else { - cleartext.emit('secureConnect'); - } - } else { - cleartext.authorized = true; - cleartext.emit('secureConnect'); - } - }); - pair.on('error', function(err) { - cleartext.emit('error', err); - }); - - cleartext._controlReleased = true; - return cleartext; -}; - - -function pipe(pair, socket) { - pair.encrypted.pipe(socket); - socket.pipe(pair.encrypted); - - pair.encrypted.on('close', function() { - process.nextTick(function() { - // Encrypted should be unpiped from socket to prevent possible - // write after destroy. - pair.encrypted.unpipe(socket); - socket.destroy(); - }); - }); - - pair.fd = socket.fd; - var cleartext = pair.cleartext; - cleartext.socket = socket; - cleartext.encrypted = pair.encrypted; - cleartext.authorized = false; - - // cycle the data whenever the socket drains, so that - // we can pull some more into it. normally this would - // be handled by the fact that pipe() triggers read() calls - // on writable.drain, but CryptoStreams are a bit more - // complicated. Since the encrypted side actually gets - // its data from the cleartext side, we have to give it a - // light kick to get in motion again. - socket.on('drain', function() { - if (pair.encrypted._pending) - pair.encrypted._writePending(); - if (pair.cleartext._pending) - pair.cleartext._writePending(); - pair.encrypted.read(0); - pair.cleartext.read(0); - }); - - function onerror(e) { - if (cleartext._controlReleased) { - cleartext.emit('error', e); - } - } - - function onclose() { - socket.removeListener('error', onerror); - socket.removeListener('timeout', ontimeout); - } - - function ontimeout() { - cleartext.emit('timeout'); - } - - socket.on('error', onerror); - socket.on('close', onclose); - socket.on('timeout', ontimeout); - - return cleartext; -} +// Public API +exports.Server = require('_tls_wrap').Server; +exports.createServer = require('_tls_wrap').createServer; +exports.connect = require('_tls_wrap').connect; + +// Legacy API +exports.__defineGetter__('createSecurePair', util.deprecate(function() { + return require('_tls_legacy').createSecurePair; +}, 'createSecurePair() is deprecated, use TLSSocket instead')); diff --git a/node.gyp b/node.gyp index 931eca42749..8ee17a70f9f 100644 --- a/node.gyp +++ b/node.gyp @@ -59,6 +59,8 @@ 'lib/sys.js', 'lib/timers.js', 'lib/tls.js', + 'lib/_tls_legacy.js', + 'lib/_tls_wrap.js', 'lib/tty.js', 'lib/url.js', 'lib/util.js', diff --git a/test/simple/test-http-url.parse-https.request.js b/test/simple/test-http-url.parse-https.request.js index bcf37526a85..b83f0df9fcd 100644 --- a/test/simple/test-http-url.parse-https.request.js +++ b/test/simple/test-http-url.parse-https.request.js @@ -37,7 +37,7 @@ testURL.rejectUnauthorized = false; function check(request) { // assert that I'm https - assert.ok(request.socket.encrypted); + assert.ok(request.socket._secureEstablished); } var server = https.createServer(httpsOptions, function(request, response) { diff --git a/test/simple/test-https-eof-for-eom.js b/test/simple/test-https-eof-for-eom.js index 2b35835e901..7a465b865d4 100644 --- a/test/simple/test-https-eof-for-eom.js +++ b/test/simple/test-https-eof-for-eom.js @@ -86,7 +86,7 @@ server.listen(common.PORT, function() { bodyBuffer += s; }); - res.on('close', function() { + res.on('end', function() { console.log('5) Client got "end" event.'); gotEnd = true; }); diff --git a/test/simple/test-https-localaddress.js b/test/simple/test-https-localaddress.js index f577af3ac8d..f703d41aef1 100644 --- a/test/simple/test-https-localaddress.js +++ b/test/simple/test-https-localaddress.js @@ -35,8 +35,8 @@ var options = { }; var server = https.createServer(options, function (req, res) { - console.log("Connect from: " + req.connection.socket.remoteAddress); - assert.equal('127.0.0.2', req.connection.socket.remoteAddress); + console.log("Connect from: " + req.connection.remoteAddress); + assert.equal('127.0.0.2', req.connection.remoteAddress); req.on('end', function() { res.writeHead(200, { 'Content-Type': 'text/plain' }); diff --git a/test/simple/test-https-timeout-server.js b/test/simple/test-https-timeout-server.js index 18dc4b8fbe0..57d6c57b59c 100644 --- a/test/simple/test-https-timeout-server.js +++ b/test/simple/test-https-timeout-server.js @@ -47,8 +47,6 @@ server.on('clientError', function(err, conn) { // the cleartext object ever changes. We're checking that the https.Server // has closed the client connection. assert.equal(conn._secureEstablished, false); - assert.equal(conn._doneFlag, true); - assert.equal(conn.ssl, null); server.close(); clientErrors++; }); diff --git a/test/simple/test-tls-client-abort3.js b/test/simple/test-tls-client-abort3.js deleted file mode 100644 index 72725187c4b..00000000000 --- a/test/simple/test-tls-client-abort3.js +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -if (!process.versions.openssl) { - console.error('Skipping because node compiled without OpenSSL.'); - process.exit(0); -} - -var common = require('../common'); -var common = require('../common'); -var tls = require('tls'); -var fs = require('fs'); -var assert = require('assert'); - -var options = { - key: fs.readFileSync(common.fixturesDir + '/test_key.pem'), - cert: fs.readFileSync(common.fixturesDir + '/test_cert.pem') -}; - -var gotError = 0, - gotRequest = 0, - connected = 0; - -var server = tls.createServer(options, function(c) { - gotRequest++; - c.on('data', function(data) { - console.log(data.toString()); - }); - - c.on('close', function() { - server.close(); - }); -}).listen(common.PORT, function() { - var c = tls.connect(common.PORT, { rejectUnauthorized: false }, function() { - connected++; - c.pair.ssl.shutdown(); - c.write('123'); - c.destroy(); - }); - - c.once('error', function() { - gotError++; - }); -}); - -process.once('exit', function() { - assert.equal(gotError, 1); - assert.equal(gotRequest, 1); - assert.equal(connected, 1); -}); diff --git a/test/simple/test-tls-pause-close.js b/test/simple/test-tls-pause-close.js deleted file mode 100644 index 8e5d897d170..00000000000 --- a/test/simple/test-tls-pause-close.js +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -if (!process.versions.openssl) { - console.error('Skipping because node compiled without OpenSSL.'); - process.exit(0); -} - -var common = require('../common'); -var assert = require('assert'); -var tls = require('tls'); -var fs = require('fs'); -var path = require('path'); - -var serverClosed = false; -var serverSocketClosed = false; -var clientClosed = false; -var clientSocketClosed = false; - -var options = { - key: fs.readFileSync(path.join(common.fixturesDir, 'test_key.pem')), - cert: fs.readFileSync(path.join(common.fixturesDir, 'test_cert.pem')) -}; - -var server = tls.createServer(options, function(s) { - console.log('server connected'); - s.socket.on('end', function() { - console.log('server socket ended'); - }); - s.socket.on('close', function() { - console.log('server socket closed'); - serverSocketClosed = true; - }); - s.on('end', function() { - console.log('server ended'); - }); - s.on('close', function() { - console.log('server closed'); - serverClosed = true; - }); - s.pause(); - console.log('server paused'); - process.nextTick(function() { - s.resume(); - console.log('server resumed'); - }); - s.end(); -}); - -server.listen(common.PORT, function() { - var c = tls.connect({ - port: common.PORT, - rejectUnauthorized: false - }, function() { - console.log('client connected'); - c.socket.on('end', function() { - console.log('client socket ended'); - }); - c.socket.on('close', function() { - console.log('client socket closed'); - clientSocketClosed = true; - }); - c.pause(); - console.log('client paused'); - process.nextTick(function() { - c.resume(); - console.log('client resumed'); - }); - }); - c.on('end', function() { - console.log('client ended'); - }); - c.on('close', function() { - console.log('client closed'); - clientClosed = true; - server.close(); - }); -}); - -process.on('exit', function() { - assert(serverClosed); - assert(serverSocketClosed); - assert(clientClosed); - assert(clientSocketClosed); -}); diff --git a/test/simple/test-tls-remote.js b/test/simple/test-tls-remote.js deleted file mode 100644 index 3df83f8bc3d..00000000000 --- a/test/simple/test-tls-remote.js +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -if (!process.versions.openssl) { - console.error('Skipping because node compiled without OpenSSL.'); - process.exit(0); -} - -var common = require('../common'); -var assert = require('assert'); -var tls = require('tls'); -var fs = require('fs'); -var path = require('path'); - -var options = { - key: fs.readFileSync(path.join(common.fixturesDir, 'test_key.pem')), - cert: fs.readFileSync(path.join(common.fixturesDir, 'test_cert.pem')) -}; - -var server = tls.Server(options, function(s) { - assert.equal(s.address().address, s.socket.address().address); - assert.equal(s.address().port, s.socket.address().port); - - assert.equal(s.remoteAddress, s.socket.remoteAddress); - assert.equal(s.remotePort, s.socket.remotePort); - - assert.equal(s.localAddress, s.socket.localAddress); - assert.equal(s.localPort, s.socket.localPort); - s.end(); -}); - -server.listen(common.PORT, '127.0.0.1', function() { - assert.equal(server.address().address, '127.0.0.1'); - assert.equal(server.address().port, common.PORT); - - var c = tls.connect({ - host: '127.0.0.1', - port: common.PORT, - rejectUnauthorized: false - }, function() { - assert.equal(c.address().address, c.socket.address().address); - assert.equal(c.address().port, c.socket.address().port); - - assert.equal(c.remoteAddress, '127.0.0.1'); - assert.equal(c.remotePort, common.PORT); - }); - c.on('end', function() { - server.close(); - }); -}); diff --git a/test/simple/test-tls-server-slab.js b/test/simple/test-tls-server-slab.js deleted file mode 100644 index de4ac01f4f5..00000000000 --- a/test/simple/test-tls-server-slab.js +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -var common = require('../common'); -var assert = require('assert'); -var tls = require('tls'); -var fs = require('fs'); - -var clientConnected = 0; -var serverConnected = 0; - -var options = { - key: fs.readFileSync(common.fixturesDir + '/keys/agent1-key.pem'), - cert: fs.readFileSync(common.fixturesDir + '/keys/agent1-cert.pem') -}; - -tls.SLAB_BUFFER_SIZE = 100 * 1024; - -var server = tls.Server(options, function(socket) { - assert(socket._buffer.pool.length == tls.SLAB_BUFFER_SIZE); - if (++serverConnected === 2) { - server.close(); - } -}); - -server.listen(common.PORT, function() { - var client1 = tls.connect({ - port: common.PORT, - rejectUnauthorized: false - }, function() { - ++clientConnected; - client1.end(); - }); - - var client2 = tls.connect({ - port: common.PORT, - rejectUnauthorized: false - }); - client2.on('secureConnect', function() { - ++clientConnected; - client2.end(); - }); -}); - -process.on('exit', function() { - assert.equal(clientConnected, 2); - assert.equal(serverConnected, 2); -}); diff --git a/test/simple/test-tls-session-cache.js b/test/simple/test-tls-session-cache.js deleted file mode 100644 index fdc4ae17ddb..00000000000 --- a/test/simple/test-tls-session-cache.js +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -if (!process.versions.openssl) { - console.error('Skipping because node compiled without OpenSSL.'); - process.exit(0); -} -require('child_process').exec('openssl version', function(err) { - if (err !== null) { - console.error('Skipping because openssl command is not available.'); - process.exit(0); - } - doTest(); -}); - -function doTest() { - var common = require('../common'); - var assert = require('assert'); - var tls = require('tls'); - var fs = require('fs'); - var join = require('path').join; - var spawn = require('child_process').spawn; - - var keyFile = join(common.fixturesDir, 'agent.key'); - var certFile = join(common.fixturesDir, 'agent.crt'); - var key = fs.readFileSync(keyFile); - var cert = fs.readFileSync(certFile); - var options = { - key: key, - cert: cert, - ca: [cert], - requestCert: true - }; - var requestCount = 0; - var session; - var badOpenSSL = false; - - var server = tls.createServer(options, function(cleartext) { - cleartext.on('error', function(er) { - // We're ok with getting ECONNRESET in this test, but it's - // timing-dependent, and thus unreliable. Any other errors - // are just failures, though. - if (er.code !== 'ECONNRESET') - throw er; - }); - ++requestCount; - cleartext.end(); - }); - server.on('newSession', function(id, data) { - assert.ok(!session); - session = { - id: id, - data: data - }; - }); - server.on('resumeSession', function(id, callback) { - assert.ok(session); - assert.equal(session.id.toString('hex'), id.toString('hex')); - - // Just to check that async really works there - setTimeout(function() { - callback(null, session.data); - }, 100); - }); - server.listen(common.PORT, function() { - var client = spawn('openssl', [ - 's_client', - '-connect', 'localhost:' + common.PORT, - '-key', join(common.fixturesDir, 'agent.key'), - '-cert', join(common.fixturesDir, 'agent.crt'), - '-reconnect', - '-no_ticket' - ], { - stdio: [ 0, 1, 'pipe' ] - }); - var err = ''; - client.stderr.setEncoding('utf8'); - client.stderr.on('data', function(chunk) { - err += chunk; - }); - client.on('exit', function(code) { - if (/^unknown option/.test(err)) { - // using an incompatible version of openssl - assert(code); - badOpenSSL = true; - } else - assert.equal(code, 0); - server.close(); - }); - }); - - process.on('exit', function() { - if (!badOpenSSL) { - assert.ok(session); - - // initial request + reconnect requests (5 times) - assert.equal(requestCount, 6); - } - }); -}