diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/_tls_legacy.js | 956 | ||||
-rw-r--r-- | lib/internal/streams/duplexpair.js | 42 | ||||
-rw-r--r-- | lib/tls.js | 37 |
3 files changed, 77 insertions, 958 deletions
diff --git a/lib/_tls_legacy.js b/lib/_tls_legacy.js deleted file mode 100644 index 7beec4805c..0000000000 --- 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 0000000000..beaf9e9eb3 --- /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 554ddb77b8..96b6ec8d34 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'); |