From 9301b8a9c69d112b98c7d60e074c845d80342b4e Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Sat, 23 Dec 2017 05:55:37 +0100 Subject: [PATCH] tls: make deprecated tls.createSecurePair() use public API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the deprecated `tls.createSecurePair()` method use other public APIs only (`TLSSocket` in particular). Since `tls.createSecurePair()` has been runtime-deprecated only since Node 8, it probably isn’t quite time to remove it yet, but this patch removes almost all of the code complexity that is retained by it. The API, as it is documented, is retained. However, it is very likely that some users have come to rely on parts of undocumented API of the `SecurePair` class, especially since some of the existing tests checked for those. Therefore, this should definitely be considered a breaking change. PR-URL: https://github.com/nodejs/node/pull/17882 Reviewed-By: Ben Noordhuis Reviewed-By: James M Snell Reviewed-By: Minwoo Jung Reviewed-By: Ruben Bridgewater Reviewed-By: Tiancheng "Timothy" Gu --- lib/_tls_legacy.js | 956 ------------------ lib/internal/streams/duplexpair.js | 42 + lib/tls.js | 37 +- node.gyp | 2 +- src/async_wrap.h | 1 - src/env.h | 4 - src/node_crypto.cc | 632 +----------- src/node_crypto.h | 82 -- test/async-hooks/test-connection.ssl.js | 86 -- test/async-hooks/test-graph.connection.js | 55 - test/parallel/test-accessor-properties.js | 10 - test/parallel/test-tls-basic-validations.js | 2 +- test/parallel/test-tls-legacy-onselect.js | 27 - test/parallel/test-tls-securepair-leak.js | 18 +- test/sequential/test-async-wrap-getasyncid.js | 7 - 15 files changed, 90 insertions(+), 1871 deletions(-) delete mode 100644 lib/_tls_legacy.js create mode 100644 lib/internal/streams/duplexpair.js delete mode 100644 test/async-hooks/test-connection.ssl.js delete mode 100644 test/async-hooks/test-graph.connection.js delete mode 100644 test/parallel/test-tls-legacy-onselect.js diff --git a/lib/_tls_legacy.js b/lib/_tls_legacy.js deleted file mode 100644 index 7beec4805c1b8b..00000000000000 --- a/lib/_tls_legacy.js +++ /dev/null @@ -1,956 +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. - -'use strict'; - -const internalUtil = require('internal/util'); -internalUtil.assertCrypto(); - -const assert = require('assert'); -const { Buffer } = require('buffer'); -const common = require('_tls_common'); -const { Connection } = process.binding('crypto'); -const EventEmitter = require('events'); -const stream = require('stream'); -const { Timer } = process.binding('timer_wrap'); -const tls = require('tls'); -const util = require('util'); - -const debug = util.debuglog('tls-legacy'); - -function SlabBuffer() { - this.create(); -} - - -SlabBuffer.prototype.create = function create() { - this.isFull = false; - this.pool = Buffer.allocUnsafe(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.shutdownSSL() !== 1) { - if (this.pair.ssl && this.pair.ssl.error) - return this.pair.error(); - - this.pair.ssl.shutdownSSL(); - } - - 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'); - } -} - - -// 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 ALPN, 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() { - const data = this._pending; - const encoding = this._pendingEncoding; - const 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; - const start = this._buffer.offset; - var last = start; - do { - assert(last === this._buffer.offset); - var read = this._buffer.use(this.pair.ssl, out, size - bytesRead); - if (read > 0) { - bytesRead += read; - } - last = this._buffer.offset; - - // Handle and report errors - if (this.pair.ssl && this.pair.ssl.error) { - this.pair.error(); - break; - } - } while (read > 0 && - !this._buffer.isFull && - bytesRead < size && - this.pair.ssl !== null); - - // Get ALPN, NPN and Server name when ready - this.pair.maybeInitFinished(); - - // 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 || - this.pair.ssl && this.pair.ssl.receivedShutdown) { - // Perform graceful shutdown - this._done(); - - // No half-open, sorry! - if (this === this.pair.cleartext) { - this._opposite._done(); - - // EOF - this.push(null); - } else if (!this.pair.ssl || !this.pair.ssl.receivedShutdown) { - // EOF - this.push(null); - } - } else { - // Bail out - this.push(''); - } - } else { - // Give them requested data - 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); -}; - -Object.defineProperty(CryptoStream.prototype, 'bytesWritten', { - configurable: true, - enumerable: true, - get: function() { - return this.socket ? this.socket.bytesWritten : 0; - } -}); - -CryptoStream.prototype.getPeerCertificate = function(detailed) { - if (this.pair.ssl) { - return common.translatePeerCertificate( - this.pair.ssl.getPeerCertificate(detailed)); - } - - 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 = 1; - function finish() { - if (--waiting === 0) self.destroy(); - } - this._opposite.once('end', finish); - if (!this._finished) { - this.once('finish', finish); - ++waiting; - } - } -}; - - -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(); - - process.nextTick(destroyNT, this, err ? true : false); -}; - - -function destroyNT(self, hadErr) { - // Force EOF - self.push(null); - - // Emit 'close' event - self.emit('close', hadErr); -} - - -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. -// Deprecation Code: DEP0004 -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.readableLength > 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(); -}; - -Object.defineProperty(CleartextStream.prototype, 'remoteAddress', { - configurable: true, - enumerable: true, - get: function() { - return this.socket && this.socket.remoteAddress; - } -}); - -Object.defineProperty(CleartextStream.prototype, 'remoteFamily', { - configurable: true, - enumerable: true, - get: function() { - return this.socket && this.socket.remoteFamily; - } -}); - -Object.defineProperty(CleartextStream.prototype, 'remotePort', { - configurable: true, - enumerable: true, - get: function() { - return this.socket && this.socket.remotePort; - } -}); - -Object.defineProperty(CleartextStream.prototype, 'localAddress', { - configurable: true, - enumerable: true, - get: function() { - return this.socket && this.socket.localAddress; - } -}); - -Object.defineProperty(CleartextStream.prototype, 'localPort', { - configurable: true, - enumerable: true, - get: 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() { - // Old-style error is not being migrated to the newer style - // internal/errors.js because _tls_legacy.js has been deprecated. - 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) { - const self = this; - var once = false; - - this._resumingSession = true; - function callback(err, session) { - if (once) return; - once = true; - - if (err) return self.socket.destroy(err); - - setImmediate(function() { - self.ssl.loadSession(session); - self.ssl.endParser(); - - // 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; - - var self = this; - var once = false; - - if (!self.server.emit('newSession', key, session, done)) - done(); - - function done() { - if (once) - return; - once = true; - - if (self.ssl) - self.ssl.newSessionDone(); - } -} - - -function onocspresponse(resp) { - this.emit('OCSPResponse', resp); -} - - -/** - * Provides a pair of streams to do encrypted communication. - */ - -function SecurePair(context, isServer, requestCert, rejectUnauthorized, - options) { - if (!(this instanceof SecurePair)) { - return new SecurePair(context, - isServer, - requestCert, - rejectUnauthorized, - options); - } - - options || (options = {}); - - 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 (!context) { - this.credentials = tls.createSecureContext(); - } else { - this.credentials = context; - } - - 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.call(this); - this.ssl.onhandshakedone = () => onhandshakedone.call(this); - this.ssl.onclienthello = (hello) => onclienthello.call(this, hello); - this.ssl.onnewsession = - (key, session) => onnewsession.call(this, key, session); - this.ssl.lastHandshakeTime = 0; - this.ssl.handshakes = 0; - } else { - this.ssl.onocspresponse = (resp) => onocspresponse.call(this, resp); - } - - 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; - } - - if (process.features.tls_alpn && options.ALPNProtocols) { - // keep reference in secureContext not to be GC-ed - this.ssl._secureContext.alpnBuffer = options.ALPNProtocols; - this.ssl.setALPNrotocols(this.ssl._secureContext.alpnBuffer); - this.alpnProtocol = 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(securePairNT, this, options); -} - -util.inherits(SecurePair, EventEmitter); - -function securePairNT(self, options) { - /* The Connection may be destroyed by an abort call */ - if (self.ssl) { - self.ssl.start(); - - if (options.requestOCSP) - self.ssl.requestOCSP(); - - /* In case of cipher suite failures - SSL_accept/SSL_connect may fail */ - if (self.ssl && self.ssl.error) - self.error(); - } -} - - -function createSecurePair(context, isServer, requestCert, - rejectUnauthorized, options) { - return new SecurePair(context, isServer, requestCert, - rejectUnauthorized, options); -} - - -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_alpn) { - this.alpnProtocol = this.ssl.getALPNNegotiatedProtocol(); - } - - 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; -}; - - -function pipe(pair, socket) { - pair.encrypted.pipe(socket); - socket.pipe(pair.encrypted); - - pair.encrypted.on('close', function() { - process.nextTick(pipeCloseNT, pair, socket); - }); - - 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; -} - - -function pipeCloseNT(pair, socket) { - // Encrypted should be unpiped from socket to prevent possible - // write after destroy. - pair.encrypted.unpipe(socket); - socket.destroySoon(); -} - -module.exports = { - createSecurePair: - internalUtil.deprecate(createSecurePair, - 'tls.createSecurePair() is deprecated. Please use ' + - 'tls.TLSSocket instead.', 'DEP0064'), - pipe -}; diff --git a/lib/internal/streams/duplexpair.js b/lib/internal/streams/duplexpair.js new file mode 100644 index 00000000000000..beaf9e9eb3d03d --- /dev/null +++ b/lib/internal/streams/duplexpair.js @@ -0,0 +1,42 @@ +'use strict'; +const { Duplex } = require('stream'); + +const kCallback = Symbol('Callback'); +const kOtherSide = Symbol('Other'); + +class DuplexSocket extends Duplex { + constructor() { + super(); + this[kCallback] = null; + this[kOtherSide] = null; + } + + _read() { + const callback = this[kCallback]; + if (callback) { + this[kCallback] = null; + callback(); + } + } + + _write(chunk, encoding, callback) { + this[kOtherSide][kCallback] = callback; + this[kOtherSide].push(chunk); + } + + _final(callback) { + this[kOtherSide].on('end', callback); + this[kOtherSide].push(null); + } +} + +class DuplexPair { + constructor() { + this.socket1 = new DuplexSocket(); + this.socket2 = new DuplexSocket(); + this.socket1[kOtherSide] = this.socket2; + this.socket2[kOtherSide] = this.socket1; + } +} + +module.exports = DuplexPair; diff --git a/lib/tls.js b/lib/tls.js index 554ddb77b816f9..96b6ec8d340e2f 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -31,6 +31,8 @@ const net = require('net'); const url = require('url'); const binding = process.binding('crypto'); const Buffer = require('buffer').Buffer; +const EventEmitter = require('events'); +const DuplexPair = require('internal/streams/duplexpair'); const canonicalizeIP = process.binding('cares_wrap').canonicalizeIP; // Allow {CLIENT_RENEG_LIMIT} client-initiated session renegotiations @@ -230,6 +232,33 @@ exports.checkServerIdentity = function checkServerIdentity(host, cert) { } }; + +class SecurePair extends EventEmitter { + constructor(secureContext = exports.createSecureContext(), + isServer = false, + requestCert = !isServer, + rejectUnauthorized = false, + options = {}) { + super(); + const { socket1, socket2 } = new DuplexPair(); + + this.server = options.server; + this.credentials = secureContext; + + this.encrypted = socket1; + this.cleartext = new exports.TLSSocket(socket2, Object.assign({ + secureContext, isServer, requestCert, rejectUnauthorized + }, options)); + this.cleartext.once('secure', () => this.emit('secure')); + } + + destroy() { + this.cleartext.destroy(); + this.encrypted.destroy(); + } +} + + exports.parseCertString = internalUtil.deprecate( internalTLS.parseCertString, 'tls.parseCertString() is deprecated. ' + @@ -243,5 +272,9 @@ exports.Server = require('_tls_wrap').Server; exports.createServer = require('_tls_wrap').createServer; exports.connect = require('_tls_wrap').connect; -// Deprecated: DEP0064 -exports.createSecurePair = require('_tls_legacy').createSecurePair; +exports.createSecurePair = internalUtil.deprecate( + function createSecurePair(...args) { + return new SecurePair(...args); + }, + 'tls.createSecurePair() is deprecated. Please use ' + + 'tls.TLSSocket instead.', 'DEP0064'); diff --git a/node.gyp b/node.gyp index 736dc975a354ae..f4e4edd8682800 100644 --- a/node.gyp +++ b/node.gyp @@ -69,7 +69,6 @@ 'lib/timers.js', 'lib/tls.js', 'lib/_tls_common.js', - 'lib/_tls_legacy.js', 'lib/_tls_wrap.js', 'lib/tty.js', 'lib/url.js', @@ -140,6 +139,7 @@ 'lib/internal/streams/lazy_transform.js', 'lib/internal/streams/async_iterator.js', 'lib/internal/streams/BufferList.js', + 'lib/internal/streams/duplexpair.js', 'lib/internal/streams/legacy.js', 'lib/internal/streams/destroy.js', 'lib/internal/wrap_js_stream.js', diff --git a/src/async_wrap.h b/src/async_wrap.h index 9b5632598bcc0b..091224f5708df5 100644 --- a/src/async_wrap.h +++ b/src/async_wrap.h @@ -67,7 +67,6 @@ namespace node { #if HAVE_OPENSSL #define NODE_ASYNC_CRYPTO_PROVIDER_TYPES(V) \ - V(SSLCONNECTION) \ V(PBKDF2REQUEST) \ V(RANDOMBYTESREQUEST) \ V(TLSWRAP) diff --git a/src/env.h b/src/env.h index 94114446161cd2..e7ec682c8ce2e5 100644 --- a/src/env.h +++ b/src/env.h @@ -193,14 +193,12 @@ class ModuleWrap; V(onheaders_string, "onheaders") \ V(onmessage_string, "onmessage") \ V(onnewsession_string, "onnewsession") \ - V(onnewsessiondone_string, "onnewsessiondone") \ V(onocspresponse_string, "onocspresponse") \ V(ongoawaydata_string, "ongoawaydata") \ V(onpriority_string, "onpriority") \ V(onread_string, "onread") \ V(onreadstart_string, "onreadstart") \ V(onreadstop_string, "onreadstop") \ - V(onselect_string, "onselect") \ V(onsettings_string, "onsettings") \ V(onshutdown_string, "onshutdown") \ V(onsignal_string, "onsignal") \ @@ -224,7 +222,6 @@ class ModuleWrap; V(raw_string, "raw") \ V(read_host_object_string, "_readHostObject") \ V(readable_string, "readable") \ - V(received_shutdown_string, "receivedShutdown") \ V(refresh_string, "refresh") \ V(regexp_string, "regexp") \ V(rename_string, "rename") \ @@ -232,7 +229,6 @@ class ModuleWrap; V(retry_string, "retry") \ V(serial_string, "serial") \ V(scopeid_string, "scopeid") \ - V(sent_shutdown_string, "sentShutdown") \ V(serial_number_string, "serialNumber") \ V(service_string, "service") \ V(servername_string, "servername") \ diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 127715e6c1ea65..d512266bffdb37 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -637,8 +637,6 @@ void SecureContext::Init(const FunctionCallbackInfo& args) { SSL_SESS_CACHE_SERVER | SSL_SESS_CACHE_NO_INTERNAL | SSL_SESS_CACHE_NO_AUTO_CLEAR); - SSL_CTX_sess_set_get_cb(sc->ctx_, SSLWrap::GetSessionCallback); - SSL_CTX_sess_set_new_cb(sc->ctx_, SSLWrap::NewSessionCallback); #if OPENSSL_VERSION_NUMBER >= 0x10100000L // OpenSSL 1.1.0 changed the ticket key size, but the OpenSSL 1.0.x size was @@ -1637,18 +1635,6 @@ void SSLWrap::AddMethods(Environment* env, Local t) { env->SetProtoMethod(t, "getALPNNegotiatedProtocol", GetALPNNegotiatedProto); env->SetProtoMethod(t, "setALPNProtocols", SetALPNProtocols); - - Local ssl_getter_templ = - FunctionTemplate::New(env->isolate(), - SSLGetter, - env->as_external(), - Signature::New(env->isolate(), t)); - - t->PrototypeTemplate()->SetAccessorProperty( - FIXED_ONE_BYTE_STRING(env->isolate(), "_external"), - ssl_getter_templ, - Local(), - static_cast(ReadOnly | DontDelete)); } @@ -2808,16 +2794,6 @@ void SSLWrap::CertCbDone(const FunctionCallbackInfo& args) { } -template -void SSLWrap::SSLGetter(const FunctionCallbackInfo& info) { - Base* base; - ASSIGN_OR_RETURN_UNWRAP(&base, info.This()); - SSL* ssl = base->ssl_; - Local ext = External::New(info.GetIsolate(), ssl); - info.GetReturnValue().Set(ext); -} - - template void SSLWrap::DestroySSL() { if (ssl_ == nullptr) @@ -2853,205 +2829,6 @@ int SSLWrap::SetCACerts(SecureContext* sc) { } -Connection::Connection(Environment* env, - v8::Local wrap, - SecureContext* sc, - SSLWrap::Kind kind) - : AsyncWrap(env, wrap, AsyncWrap::PROVIDER_SSLCONNECTION), - SSLWrap(env, sc, kind), - bio_read_(nullptr), - bio_write_(nullptr), - hello_offset_(0) { - MakeWeak(this); - Wrap(wrap, this); - hello_parser_.Start(SSLWrap::OnClientHello, - OnClientHelloParseEnd, - this); - enable_session_callbacks(); -} - - -void Connection::OnClientHelloParseEnd(void* arg) { - Connection* conn = static_cast(arg); - - // Write all accumulated data - int r = BIO_write(conn->bio_read_, - reinterpret_cast(conn->hello_data_), - conn->hello_offset_); - conn->HandleBIOError(conn->bio_read_, "BIO_write", r); - conn->SetShutdownFlags(); -} - - -#ifdef SSL_PRINT_DEBUG -# define DEBUG_PRINT(...) fprintf (stderr, __VA_ARGS__) -#else -# define DEBUG_PRINT(...) -#endif - - -int Connection::HandleBIOError(BIO *bio, const char* func, int rv) { - if (rv >= 0) - return rv; - - int retry = BIO_should_retry(bio); - USE(retry); // unused if !defined(SSL_PRINT_DEBUG) - - if (BIO_should_write(bio)) { - DEBUG_PRINT("[%p] BIO: %s want write. should retry %d\n", - ssl_, - func, - retry); - return 0; - - } else if (BIO_should_read(bio)) { - DEBUG_PRINT("[%p] BIO: %s want read. should retry %d\n", ssl_, func, retry); - return 0; - - } else { - char ssl_error_buf[512]; - ERR_error_string_n(rv, ssl_error_buf, sizeof(ssl_error_buf)); - - HandleScope scope(ssl_env()->isolate()); - Local exception = - Exception::Error(OneByteString(ssl_env()->isolate(), ssl_error_buf)); - object()->Set(ssl_env()->error_string(), exception); - - DEBUG_PRINT("[%p] BIO: %s failed: (%d) %s\n", - ssl_, - func, - rv, - ssl_error_buf); - - return rv; - } - - return 0; -} - - -int Connection::HandleSSLError(const char* func, - int rv, - ZeroStatus zs, - SyscallStatus ss) { - ClearErrorOnReturn clear_error_on_return; - - if (rv > 0) - return rv; - if (rv == 0 && zs == kZeroIsNotAnError) - return rv; - - int err = SSL_get_error(ssl_, rv); - - if (err == SSL_ERROR_NONE) { - return 0; - - } else if (err == SSL_ERROR_WANT_WRITE) { - DEBUG_PRINT("[%p] SSL: %s want write\n", ssl_, func); - return 0; - - } else if (err == SSL_ERROR_WANT_READ) { - DEBUG_PRINT("[%p] SSL: %s want read\n", ssl_, func); - return 0; - - } else if (err == SSL_ERROR_WANT_X509_LOOKUP) { - DEBUG_PRINT("[%p] SSL: %s want x509 lookup\n", ssl_, func); - return 0; - - } else if (err == SSL_ERROR_ZERO_RETURN) { - HandleScope scope(ssl_env()->isolate()); - - Local exception = - Exception::Error(ssl_env()->zero_return_string()); - object()->Set(ssl_env()->error_string(), exception); - return rv; - - } else if (err == SSL_ERROR_SYSCALL && ss == kIgnoreSyscall) { - return 0; - - } else { - HandleScope scope(ssl_env()->isolate()); - BUF_MEM* mem; - BIO *bio; - - CHECK(err == SSL_ERROR_SSL || err == SSL_ERROR_SYSCALL); - - // XXX We need to drain the error queue for this thread or else OpenSSL - // has the possibility of blocking connections? This problem is not well - // understood. And we should be somehow propagating these errors up - // into JavaScript. There is no test which demonstrates this problem. - // https://github.com/joyent/node/issues/1719 - bio = BIO_new(BIO_s_mem()); - if (bio != nullptr) { - ERR_print_errors(bio); - BIO_get_mem_ptr(bio, &mem); - Local exception = Exception::Error( - OneByteString(ssl_env()->isolate(), - mem->data, - mem->length)); - object()->Set(ssl_env()->error_string(), exception); - BIO_free_all(bio); - } - - return rv; - } - - return 0; -} - - -void Connection::SetShutdownFlags() { - HandleScope scope(ssl_env()->isolate()); - - int flags = SSL_get_shutdown(ssl_); - - if (flags & SSL_SENT_SHUTDOWN) { - Local sent_shutdown_key = ssl_env()->sent_shutdown_string(); - object()->Set(sent_shutdown_key, True(ssl_env()->isolate())); - } - - if (flags & SSL_RECEIVED_SHUTDOWN) { - Local received_shutdown_key = ssl_env()->received_shutdown_string(); - object()->Set(received_shutdown_key, True(ssl_env()->isolate())); - } -} - - -void Connection::NewSessionDoneCb() { - HandleScope scope(env()->isolate()); - - MakeCallback(env()->onnewsessiondone_string(), 0, nullptr); -} - - -void Connection::Initialize(Environment* env, Local target) { - Local t = env->NewFunctionTemplate(Connection::New); - t->InstanceTemplate()->SetInternalFieldCount(1); - t->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Connection")); - - AsyncWrap::AddWrapMethods(env, t); - env->SetProtoMethod(t, "encIn", EncIn); - env->SetProtoMethod(t, "clearOut", ClearOut); - env->SetProtoMethod(t, "clearIn", ClearIn); - env->SetProtoMethod(t, "encOut", EncOut); - env->SetProtoMethod(t, "clearPending", ClearPending); - env->SetProtoMethod(t, "encPending", EncPending); - env->SetProtoMethod(t, "start", Start); - env->SetProtoMethod(t, "close", Close); - - SSLWrap::AddMethods(env, t); - - -#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB - env->SetProtoMethod(t, "getServername", GetServername); - env->SetProtoMethod(t, "setSNICallback", SetSNICallback); -#endif - - target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "Connection"), - t->GetFunction()); -} - - inline int compar(const void* a, const void* b) { return memcmp(a, b, CNNIC_WHITELIST_HASH_LEN); } @@ -3103,7 +2880,6 @@ inline bool CheckStartComOrWoSign(X509_NAME* root_name, X509* cert) { return false; } - // Whitelist check for certs issued by CNNIC, StartCom and WoSign. See // https://blog.mozilla.org/security/2015/04/02 // /distrusting-new-cnnic-certificates/ and @@ -3154,8 +2930,7 @@ inline CheckResult CheckWhitelistedServerCert(X509_STORE_CTX* ctx) { return CHECK_OK; } - -inline int VerifyCallback(int preverify_ok, X509_STORE_CTX* ctx) { +int VerifyCallback(int preverify_ok, X509_STORE_CTX* ctx) { // Failure on verification of the cert is handled in // Connection::VerifyError. if (preverify_ok == 0 || X509_STORE_CTX_get_error(ctx) != X509_V_OK) @@ -3177,410 +2952,6 @@ inline int VerifyCallback(int preverify_ok, X509_STORE_CTX* ctx) { return ret; } - -#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB -int Connection::SelectSNIContextCallback_(SSL *s, int *ad, void* arg) { - Connection* conn = static_cast(SSL_get_app_data(s)); - Environment* env = conn->env(); - HandleScope scope(env->isolate()); - - const char* servername = SSL_get_servername(s, TLSEXT_NAMETYPE_host_name); - - if (servername) { - conn->servername_.Reset(env->isolate(), - OneByteString(env->isolate(), servername)); - - // Call the SNI callback and use its return value as context - if (!conn->sniObject_.IsEmpty()) { - conn->sni_context_.Reset(); - - Local sni_obj = PersistentToLocal(env->isolate(), - conn->sniObject_); - - Local arg = PersistentToLocal(env->isolate(), conn->servername_); - Local ret = node::MakeCallback(env->isolate(), - sni_obj, - env->onselect_string(), - 1, - &arg); - - // If ret is SecureContext - Local secure_context_constructor_template = - env->secure_context_constructor_template(); - if (secure_context_constructor_template->HasInstance(ret)) { - conn->sni_context_.Reset(env->isolate(), ret); - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, ret.As(), SSL_TLSEXT_ERR_NOACK); - conn->SetSNIContext(sc); - } else { - return SSL_TLSEXT_ERR_NOACK; - } - } - } - - return SSL_TLSEXT_ERR_OK; -} -#endif - -void Connection::New(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - if (args.Length() < 1 || !args[0]->IsObject()) { - env->ThrowError("First argument must be a tls module SecureContext"); - return; - } - - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, args[0].As()); - - bool is_server = args[1]->BooleanValue(); - - SSLWrap::Kind kind = - is_server ? SSLWrap::kServer : SSLWrap::kClient; - Connection* conn = new Connection(env, args.This(), sc, kind); - conn->bio_read_ = NodeBIO::New(); - conn->bio_write_ = NodeBIO::New(); - - SSL_set_app_data(conn->ssl_, conn); - - if (is_server) - SSL_set_info_callback(conn->ssl_, SSLInfoCallback); - - InitNPN(sc); - - SSL_set_cert_cb(conn->ssl_, SSLWrap::SSLCertCallback, conn); - -#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB - if (is_server) { - SSL_CTX_set_tlsext_servername_callback(sc->ctx_, SelectSNIContextCallback_); - } else if (args[2]->IsString()) { - const node::Utf8Value servername(env->isolate(), args[2]); - SSL_set_tlsext_host_name(conn->ssl_, *servername); - } -#endif - - SSL_set_bio(conn->ssl_, conn->bio_read_, conn->bio_write_); - -#ifdef SSL_MODE_RELEASE_BUFFERS - long mode = SSL_get_mode(conn->ssl_); // NOLINT(runtime/int) - SSL_set_mode(conn->ssl_, mode | SSL_MODE_RELEASE_BUFFERS); -#endif - - - int verify_mode; - if (is_server) { - bool request_cert = args[2]->BooleanValue(); - if (!request_cert) { - // Note reject_unauthorized ignored. - verify_mode = SSL_VERIFY_NONE; - } else { - bool reject_unauthorized = args[3]->BooleanValue(); - verify_mode = SSL_VERIFY_PEER; - if (reject_unauthorized) - verify_mode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT; - } - } else { - // Note request_cert and reject_unauthorized are ignored for clients. - verify_mode = SSL_VERIFY_NONE; - } - - - // Always allow a connection. We'll reject in javascript. - SSL_set_verify(conn->ssl_, verify_mode, VerifyCallback); - - if (is_server) { - SSL_set_accept_state(conn->ssl_); - } else { - SSL_set_connect_state(conn->ssl_); - } -} - - -void Connection::SSLInfoCallback(const SSL *ssl_, int where, int ret) { - if (!(where & (SSL_CB_HANDSHAKE_START | SSL_CB_HANDSHAKE_DONE))) - return; - - // Be compatible with older versions of OpenSSL. SSL_get_app_data() wants - // a non-const SSL* in OpenSSL <= 0.9.7e. - SSL* ssl = const_cast(ssl_); - Connection* conn = static_cast(SSL_get_app_data(ssl)); - Environment* env = conn->env(); - HandleScope handle_scope(env->isolate()); - Context::Scope context_scope(env->context()); - - if (where & SSL_CB_HANDSHAKE_START) { - conn->MakeCallback(env->onhandshakestart_string(), 0, nullptr); - } - - if (where & SSL_CB_HANDSHAKE_DONE) { - conn->MakeCallback(env->onhandshakedone_string(), 0, nullptr); - } -} - - -void Connection::EncIn(const FunctionCallbackInfo& args) { - Connection* conn; - ASSIGN_OR_RETURN_UNWRAP(&conn, args.Holder()); - Environment* env = conn->env(); - - if (args.Length() < 3) { - return env->ThrowTypeError( - "Data, offset, and length arguments are mandatory"); - } - - THROW_AND_RETURN_IF_NOT_BUFFER(args[0], "Data"); - - char* buffer_data = Buffer::Data(args[0]); - size_t buffer_length = Buffer::Length(args[0]); - - size_t off = args[1]->Int32Value(); - size_t len = args[2]->Int32Value(); - - if (!Buffer::IsWithinBounds(off, len, buffer_length)) - return env->ThrowRangeError("offset + length > buffer.length"); - - int bytes_written; - char* data = buffer_data + off; - - if (conn->is_server() && !conn->hello_parser_.IsEnded()) { - // Just accumulate data, everything will be pushed to BIO later - if (conn->hello_parser_.IsPaused()) { - bytes_written = 0; - } else { - // Copy incoming data to the internal buffer - // (which has a size of the biggest possible TLS frame) - size_t available = sizeof(conn->hello_data_) - conn->hello_offset_; - size_t copied = len < available ? len : available; - memcpy(conn->hello_data_ + conn->hello_offset_, data, copied); - conn->hello_offset_ += copied; - - conn->hello_parser_.Parse(conn->hello_data_, conn->hello_offset_); - bytes_written = copied; - } - } else { - bytes_written = BIO_write(conn->bio_read_, data, len); - conn->HandleBIOError(conn->bio_read_, "BIO_write", bytes_written); - conn->SetShutdownFlags(); - } - - args.GetReturnValue().Set(bytes_written); -} - - -void Connection::ClearOut(const FunctionCallbackInfo& args) { - Connection* conn; - ASSIGN_OR_RETURN_UNWRAP(&conn, args.Holder()); - Environment* env = conn->env(); - - if (args.Length() < 3) { - return env->ThrowTypeError( - "Data, offset, and length arguments are mandatory"); - } - - THROW_AND_RETURN_IF_NOT_BUFFER(args[0], "Data"); - - char* buffer_data = Buffer::Data(args[0]); - size_t buffer_length = Buffer::Length(args[0]); - - size_t off = args[1]->Int32Value(); - size_t len = args[2]->Int32Value(); - - if (!Buffer::IsWithinBounds(off, len, buffer_length)) - return env->ThrowRangeError("offset + length > buffer.length"); - - if (!SSL_is_init_finished(conn->ssl_)) { - int rv; - - if (conn->is_server()) { - rv = SSL_accept(conn->ssl_); - conn->HandleSSLError("SSL_accept:ClearOut", - rv, - kZeroIsAnError, - kSyscallError); - } else { - rv = SSL_connect(conn->ssl_); - conn->HandleSSLError("SSL_connect:ClearOut", - rv, - kZeroIsAnError, - kSyscallError); - } - - if (rv < 0) { - return args.GetReturnValue().Set(rv); - } - } - - int bytes_read = SSL_read(conn->ssl_, buffer_data + off, len); - conn->HandleSSLError("SSL_read:ClearOut", - bytes_read, - kZeroIsNotAnError, - kSyscallError); - conn->SetShutdownFlags(); - - args.GetReturnValue().Set(bytes_read); -} - - -void Connection::ClearPending(const FunctionCallbackInfo& args) { - Connection* conn; - ASSIGN_OR_RETURN_UNWRAP(&conn, args.Holder()); - int bytes_pending = BIO_pending(conn->bio_read_); - args.GetReturnValue().Set(bytes_pending); -} - - -void Connection::EncPending(const FunctionCallbackInfo& args) { - Connection* conn; - ASSIGN_OR_RETURN_UNWRAP(&conn, args.Holder()); - int bytes_pending = BIO_pending(conn->bio_write_); - args.GetReturnValue().Set(bytes_pending); -} - - -void Connection::EncOut(const FunctionCallbackInfo& args) { - Connection* conn; - ASSIGN_OR_RETURN_UNWRAP(&conn, args.Holder()); - Environment* env = conn->env(); - - if (args.Length() < 3) { - return env->ThrowTypeError( - "Data, offset, and length arguments are mandatory"); - } - - THROW_AND_RETURN_IF_NOT_BUFFER(args[0], "Data"); - - char* buffer_data = Buffer::Data(args[0]); - size_t buffer_length = Buffer::Length(args[0]); - - size_t off = args[1]->Int32Value(); - size_t len = args[2]->Int32Value(); - - if (!Buffer::IsWithinBounds(off, len, buffer_length)) - return env->ThrowRangeError("offset + length > buffer.length"); - - int bytes_read = BIO_read(conn->bio_write_, buffer_data + off, len); - - conn->HandleBIOError(conn->bio_write_, "BIO_read:EncOut", bytes_read); - conn->SetShutdownFlags(); - - args.GetReturnValue().Set(bytes_read); -} - - -void Connection::ClearIn(const FunctionCallbackInfo& args) { - Connection* conn; - ASSIGN_OR_RETURN_UNWRAP(&conn, args.Holder()); - Environment* env = conn->env(); - - if (args.Length() < 3) { - return env->ThrowTypeError( - "Data, offset, and length arguments are mandatory"); - } - - THROW_AND_RETURN_IF_NOT_BUFFER(args[0], "Data"); - - char* buffer_data = Buffer::Data(args[0]); - size_t buffer_length = Buffer::Length(args[0]); - - size_t off = args[1]->Int32Value(); - size_t len = args[2]->Int32Value(); - - if (!Buffer::IsWithinBounds(off, len, buffer_length)) - return env->ThrowRangeError("offset + length > buffer.length"); - - if (!SSL_is_init_finished(conn->ssl_)) { - int rv; - if (conn->is_server()) { - rv = SSL_accept(conn->ssl_); - conn->HandleSSLError("SSL_accept:ClearIn", - rv, - kZeroIsAnError, - kSyscallError); - } else { - rv = SSL_connect(conn->ssl_); - conn->HandleSSLError("SSL_connect:ClearIn", - rv, - kZeroIsAnError, - kSyscallError); - } - - if (rv < 0) { - return args.GetReturnValue().Set(rv); - } - } - - int bytes_written = SSL_write(conn->ssl_, buffer_data + off, len); - - conn->HandleSSLError("SSL_write:ClearIn", - bytes_written, - len == 0 ? kZeroIsNotAnError : kZeroIsAnError, - kSyscallError); - conn->SetShutdownFlags(); - - args.GetReturnValue().Set(bytes_written); -} - - -void Connection::Start(const FunctionCallbackInfo& args) { - Connection* conn; - ASSIGN_OR_RETURN_UNWRAP(&conn, args.Holder()); - - int rv = 0; - if (!SSL_is_init_finished(conn->ssl_)) { - if (conn->is_server()) { - rv = SSL_accept(conn->ssl_); - conn->HandleSSLError("SSL_accept:Start", - rv, - kZeroIsAnError, - kSyscallError); - } else { - rv = SSL_connect(conn->ssl_); - conn->HandleSSLError("SSL_connect:Start", - rv, - kZeroIsAnError, - kSyscallError); - } - } - args.GetReturnValue().Set(rv); -} - - -void Connection::Close(const FunctionCallbackInfo& args) { - Connection* conn; - ASSIGN_OR_RETURN_UNWRAP(&conn, args.Holder()); - conn->DestroySSL(); -} - - -#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB -void Connection::GetServername(const FunctionCallbackInfo& args) { - Connection* conn; - ASSIGN_OR_RETURN_UNWRAP(&conn, args.Holder()); - - if (conn->is_server() && !conn->servername_.IsEmpty()) { - args.GetReturnValue().Set(conn->servername_); - } else { - args.GetReturnValue().Set(false); - } -} - - -void Connection::SetSNICallback(const FunctionCallbackInfo& args) { - Connection* conn; - ASSIGN_OR_RETURN_UNWRAP(&conn, args.Holder()); - Environment* env = conn->env(); - - if (args.Length() < 1 || !args[0]->IsFunction()) { - return env->ThrowError("Must give a Function as first argument"); - } - - Local obj = Object::New(env->isolate()); - obj->Set(env->onselect_string(), args[0]); - conn->sniObject_.Reset(args.GetIsolate(), obj); -} -#endif - - void CipherBase::Initialize(Environment* env, Local target) { Local t = env->NewFunctionTemplate(New); @@ -6227,7 +5598,6 @@ void InitCrypto(Local target, Environment* env = Environment::GetCurrent(context); SecureContext::Initialize(env, target); - Connection::Initialize(env, target); CipherBase::Initialize(env, target); DiffieHellman::Initialize(env, target); ECDH::Initialize(env, target); diff --git a/src/node_crypto.h b/src/node_crypto.h index b866117f844358..7ce40697d402cd 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -328,7 +328,6 @@ class SSLWrap { void* arg); static int TLSExtStatusCallback(SSL* s, void* arg); static int SSLCertCallback(SSL* s, void* arg); - static void SSLGetter(const v8::FunctionCallbackInfo& info); void DestroySSL(); void WaitForCertCb(CertCb cb, void* arg); @@ -364,87 +363,6 @@ class SSLWrap { friend class SecureContext; }; -// Connection inherits from AsyncWrap because SSLWrap makes calls to -// MakeCallback, but SSLWrap doesn't store the handle itself. Instead it -// assumes that any args.This() called will be the handle from Connection. -class Connection : public AsyncWrap, public SSLWrap { - public: - ~Connection() override { -#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB - sniObject_.Reset(); - servername_.Reset(); -#endif - } - - static void Initialize(Environment* env, v8::Local target); - void NewSessionDoneCb(); - -#ifndef OPENSSL_NO_NEXTPROTONEG - v8::Persistent npnProtos_; - v8::Persistent selectedNPNProto_; -#endif - -#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB - v8::Persistent sniObject_; - v8::Persistent servername_; -#endif - - size_t self_size() const override { return sizeof(*this); } - - protected: - static void New(const v8::FunctionCallbackInfo& args); - static void EncIn(const v8::FunctionCallbackInfo& args); - static void ClearOut(const v8::FunctionCallbackInfo& args); - static void ClearPending(const v8::FunctionCallbackInfo& args); - static void EncPending(const v8::FunctionCallbackInfo& args); - static void EncOut(const v8::FunctionCallbackInfo& args); - static void ClearIn(const v8::FunctionCallbackInfo& args); - static void Start(const v8::FunctionCallbackInfo& args); - static void Close(const v8::FunctionCallbackInfo& args); - -#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB - // SNI - static void GetServername(const v8::FunctionCallbackInfo& args); - static void SetSNICallback(const v8::FunctionCallbackInfo& args); - static int SelectSNIContextCallback_(SSL* s, int* ad, void* arg); -#endif - - static void OnClientHelloParseEnd(void* arg); - - int HandleBIOError(BIO* bio, const char* func, int rv); - - enum ZeroStatus { - kZeroIsNotAnError, - kZeroIsAnError - }; - - enum SyscallStatus { - kIgnoreSyscall, - kSyscallError - }; - - int HandleSSLError(const char* func, int rv, ZeroStatus zs, SyscallStatus ss); - - void SetShutdownFlags(); - - Connection(Environment* env, - v8::Local wrap, - SecureContext* sc, - SSLWrap::Kind kind); - - private: - static void SSLInfoCallback(const SSL *ssl, int where, int ret); - - BIO *bio_read_; - BIO *bio_write_; - - uint8_t hello_data_[18432]; - size_t hello_offset_; - - friend class ClientHelloParser; - friend class SecureContext; -}; - class CipherBase : public BaseObject { public: ~CipherBase() override { diff --git a/test/async-hooks/test-connection.ssl.js b/test/async-hooks/test-connection.ssl.js deleted file mode 100644 index faee0fdf080e42..00000000000000 --- a/test/async-hooks/test-connection.ssl.js +++ /dev/null @@ -1,86 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) - common.skip('missing crypto'); - -const initHooks = require('./init-hooks'); -const tick = require('./tick'); -const assert = require('assert'); -const { checkInvocations } = require('./hook-checks'); - -const tls = require('tls'); -const Connection = process.binding('crypto').Connection; -const hooks = initHooks(); -hooks.enable(); - -function createServerConnection( - onhandshakestart, - certificate = null, - isServer = true, - servername = 'some server', - rejectUnauthorized -) { - if (certificate == null) certificate = tls.createSecureContext(); - const ssl = new Connection( - certificate.context, isServer, servername, rejectUnauthorized - ); - if (isServer) { - ssl.onhandshakestart = onhandshakestart; - ssl.lastHandshakeTime = 0; - } - return ssl; -} - -// creating first server connection -const sc1 = createServerConnection(common.mustCall(onfirstHandShake)); - -let as = hooks.activitiesOfTypes('SSLCONNECTION'); -assert.strictEqual(as.length, 1); -const f1 = as[0]; -assert.strictEqual(f1.type, 'SSLCONNECTION'); -assert.strictEqual(typeof f1.uid, 'number'); -assert.strictEqual(typeof f1.triggerAsyncId, 'number'); -checkInvocations(f1, { init: 1 }, 'first connection, when first created'); - -// creating second server connection -const sc2 = createServerConnection(common.mustCall(onsecondHandShake)); - -as = hooks.activitiesOfTypes('SSLCONNECTION'); -assert.strictEqual(as.length, 2); -const f2 = as[1]; -assert.strictEqual(f2.type, 'SSLCONNECTION'); -assert.strictEqual(typeof f2.uid, 'number'); -assert.strictEqual(typeof f2.triggerAsyncId, 'number'); -checkInvocations(f1, { init: 1 }, 'first connection, when second created'); -checkInvocations(f2, { init: 1 }, 'second connection, when second created'); - -// starting the connections which results in handshake starts -sc1.start(); -sc2.start(); - -function onfirstHandShake() { - checkInvocations(f1, { init: 1, before: 1 }, - 'first connection, when first handshake'); - checkInvocations(f2, { init: 1 }, 'second connection, when first handshake'); -} - -function onsecondHandShake() { - checkInvocations(f1, { init: 1, before: 1, after: 1 }, - 'first connection, when second handshake'); - checkInvocations(f2, { init: 1, before: 1 }, - 'second connection, when second handshake'); - tick(1E4); -} - -process.on('exit', onexit); - -function onexit() { - hooks.disable(); - hooks.sanityCheck('SSLCONNECTION'); - - checkInvocations(f1, { init: 1, before: 1, after: 1 }, - 'first connection, when process exits'); - checkInvocations(f2, { init: 1, before: 1, after: 1 }, - 'second connection, when process exits'); -} diff --git a/test/async-hooks/test-graph.connection.js b/test/async-hooks/test-graph.connection.js deleted file mode 100644 index fcc764b5ccf218..00000000000000 --- a/test/async-hooks/test-graph.connection.js +++ /dev/null @@ -1,55 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) - common.skip('missing crypto'); - -const initHooks = require('./init-hooks'); -const verifyGraph = require('./verify-graph'); - -const tls = require('tls'); -const Connection = process.binding('crypto').Connection; -const hooks = initHooks(); -hooks.enable(); - -function createServerConnection( - onhandshakestart, - certificate = null, - isServer = true, - servername = 'some server', - rejectUnauthorized -) { - if (certificate == null) certificate = tls.createSecureContext(); - const ssl = new Connection( - certificate.context, isServer, servername, rejectUnauthorized - ); - if (isServer) { - ssl.onhandshakestart = onhandshakestart; - ssl.lastHandshakeTime = 0; - } - return ssl; -} - -// creating first server connection and start it -const sc1 = createServerConnection(common.mustCall(onfirstHandShake)); -sc1.start(); - -function onfirstHandShake() { - // Create second connection inside handshake of first to show - // that the triggerAsyncId of the second will be set to id of the first - const sc2 = createServerConnection(common.mustCall(onsecondHandShake)); - sc2.start(); -} -function onsecondHandShake() { } - -process.on('exit', onexit); - -function onexit() { - hooks.disable(); - verifyGraph( - hooks, - [ { type: 'CONNECTION', id: 'connection:1', triggerAsyncId: null }, - { type: 'CONNECTION', id: 'connection:2', - triggerAsyncId: 'connection:1' } ] - ); -} diff --git a/test/parallel/test-accessor-properties.js b/test/parallel/test-accessor-properties.js index 13535ceda9667f..b4ebf30f9b0440 100644 --- a/test/parallel/test-accessor-properties.js +++ b/test/parallel/test-accessor-properties.js @@ -59,20 +59,10 @@ const UDP = process.binding('udp_wrap').UDP; crypto.SecureContext.prototype._external; }, TypeError); - assert.throws(() => { - crypto.Connection.prototype._external; - }, TypeError); - assert.strictEqual( typeof Object.getOwnPropertyDescriptor( crypto.SecureContext.prototype, '_external'), 'object' ); - - assert.strictEqual( - typeof Object.getOwnPropertyDescriptor( - crypto.Connection.prototype, '_external'), - 'object' - ); } } diff --git a/test/parallel/test-tls-basic-validations.js b/test/parallel/test-tls-basic-validations.js index e747f5a5162db5..fc2743ce042064 100644 --- a/test/parallel/test-tls-basic-validations.js +++ b/test/parallel/test-tls-basic-validations.js @@ -40,7 +40,7 @@ assert.throws(() => tls.createServer({ ticketKeys: Buffer.alloc(0) }), /TypeError: Ticket keys length must be 48 bytes/); assert.throws(() => tls.createSecurePair({}), - /Error: First argument must be a tls module SecureContext/); + /TypeError: Second argument should be a SecureContext instance/); { const buffer = Buffer.from('abcd'); diff --git a/test/parallel/test-tls-legacy-onselect.js b/test/parallel/test-tls-legacy-onselect.js deleted file mode 100644 index efcc5c2c92b889..00000000000000 --- a/test/parallel/test-tls-legacy-onselect.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; -const common = require('../common'); - -if (!common.hasCrypto) - common.skip('missing crypto'); - -const tls = require('tls'); -const net = require('net'); - -const server = net.Server(common.mustCall(function(raw) { - const pair = tls.createSecurePair(null, true, false, false); - pair.on('error', function() {}); - pair.ssl.setSNICallback(common.mustCall(function() { - raw.destroy(); - server.close(); - })); - require('_tls_legacy').pipe(pair, raw); -})).listen(0, function() { - tls.connect({ - port: this.address().port, - rejectUnauthorized: false, - servername: 'server' - }, function() { - }).on('error', function() { - // Just ignore - }); -}); diff --git a/test/parallel/test-tls-securepair-leak.js b/test/parallel/test-tls-securepair-leak.js index cbc7c7daddd74b..4cd927d64ac088 100644 --- a/test/parallel/test-tls-securepair-leak.js +++ b/test/parallel/test-tls-securepair-leak.js @@ -7,7 +7,7 @@ if (!common.hasCrypto) const assert = require('assert'); const { createSecureContext } = require('tls'); -const { createSecurePair } = require('_tls_legacy'); +const { createSecurePair } = require('tls'); const before = process.memoryUsage().external; { @@ -16,11 +16,13 @@ const before = process.memoryUsage().external; for (let i = 0; i < 1e4; i += 1) createSecurePair(context, false, false, false, options).destroy(); } -global.gc(); -const after = process.memoryUsage().external; +setImmediate(() => { + global.gc(); + const after = process.memoryUsage().external; -// It's not an exact science but a SecurePair grows .external by about 45 kB. -// Unless AdjustAmountOfExternalAllocatedMemory() is called on destruction, -// 10,000 instances make it grow by well over 400 MB. Allow for some slop -// because objects like buffers also affect the external limit. -assert(after - before < 25 << 20); + // It's not an exact science but a SecurePair grows .external by about 45 kB. + // Unless AdjustAmountOfExternalAllocatedMemory() is called on destruction, + // 10,000 instances make it grow by well over 400 MB. Allow for some slop + // because objects like buffers also affect the external limit. + assert(after - before < 25 << 20); +}); diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js index a96c5032ad6008..58d6b7746985c5 100644 --- a/test/sequential/test-async-wrap-getasyncid.js +++ b/test/sequential/test-async-wrap-getasyncid.js @@ -87,13 +87,6 @@ function testInitialized(req, ctor_name) { } -if (common.hasCrypto) { // eslint-disable-line crypto-check - const tls = require('tls'); - // SecurePair - testInitialized(tls.createSecurePair().ssl, 'Connection'); -} - - if (common.hasCrypto) { // eslint-disable-line crypto-check const crypto = require('crypto');