| // 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 { |
| ObjectAssign, |
| ObjectDefineProperty, |
| ObjectSetPrototypeOf, |
| RegExp, |
| Symbol, |
| SymbolFor, |
| } = primordials; |
| |
| const { |
| assertCrypto, |
| deprecate |
| } = require('internal/util'); |
| |
| assertCrypto(); |
| |
| const { setImmediate } = require('timers'); |
| const assert = require('internal/assert'); |
| const crypto = require('crypto'); |
| const EE = require('events'); |
| const net = require('net'); |
| const tls = require('tls'); |
| const common = require('_tls_common'); |
| const JSStreamSocket = require('internal/js_stream_socket'); |
| const { Buffer } = require('buffer'); |
| let debug = require('internal/util/debuglog').debuglog('tls', (fn) => { |
| debug = fn; |
| }); |
| const { TCP, constants: TCPConstants } = internalBinding('tcp_wrap'); |
| const tls_wrap = internalBinding('tls_wrap'); |
| const { Pipe, constants: PipeConstants } = internalBinding('pipe_wrap'); |
| const { owner_symbol } = require('internal/async_hooks').symbols; |
| const { isArrayBufferView } = require('internal/util/types'); |
| const { SecureContext: NativeSecureContext } = internalBinding('crypto'); |
| const { connResetException, codes } = require('internal/errors'); |
| const { |
| ERR_INVALID_ARG_TYPE, |
| ERR_INVALID_ARG_VALUE, |
| ERR_INVALID_CALLBACK, |
| ERR_MULTIPLE_CALLBACK, |
| ERR_SOCKET_CLOSED, |
| ERR_TLS_DH_PARAM_SIZE, |
| ERR_TLS_HANDSHAKE_TIMEOUT, |
| ERR_TLS_INVALID_CONTEXT, |
| ERR_TLS_RENEGOTIATION_DISABLED, |
| ERR_TLS_REQUIRED_SERVER_NAME, |
| ERR_TLS_SESSION_ATTACK, |
| ERR_TLS_SNI_FROM_SERVER, |
| ERR_TLS_INVALID_STATE |
| } = codes; |
| const { onpskexchange: kOnPskExchange } = internalBinding('symbols'); |
| const { |
| getOptionValue, |
| getAllowUnauthorized, |
| } = require('internal/options'); |
| const { |
| validateString, |
| validateBuffer, |
| validateUint32 |
| } = require('internal/validators'); |
| const traceTls = getOptionValue('--trace-tls'); |
| const tlsKeylog = getOptionValue('--tls-keylog'); |
| const { appendFile } = require('fs'); |
| const kConnectOptions = Symbol('connect-options'); |
| const kDisableRenegotiation = Symbol('disable-renegotiation'); |
| const kErrorEmitted = Symbol('error-emitted'); |
| const kHandshakeTimeout = Symbol('handshake-timeout'); |
| const kRes = Symbol('res'); |
| const kSNICallback = Symbol('snicallback'); |
| const kEnableTrace = Symbol('enableTrace'); |
| const kPskCallback = Symbol('pskcallback'); |
| const kPskIdentityHint = Symbol('pskidentityhint'); |
| const kPendingSession = Symbol('pendingSession'); |
| const kIsVerified = Symbol('verified'); |
| |
| const noop = () => {}; |
| |
| let ipServernameWarned = false; |
| let tlsTracingWarned = false; |
| |
| // Server side times how long a handshake is taking to protect against slow |
| // handshakes being used for DoS. |
| function onhandshakestart(now) { |
| debug('server onhandshakestart'); |
| |
| const { lastHandshakeTime } = this; |
| assert(now >= lastHandshakeTime, |
| `now (${now}) < lastHandshakeTime (${lastHandshakeTime})`); |
| |
| this.lastHandshakeTime = now; |
| |
| // If this is the first handshake we can skip the rest of the checks. |
| if (lastHandshakeTime === 0) |
| return; |
| |
| if ((now - lastHandshakeTime) >= tls.CLIENT_RENEG_WINDOW * 1000) |
| this.handshakes = 1; |
| else |
| this.handshakes++; |
| |
| const owner = this[owner_symbol]; |
| |
| assert(owner._tlsOptions.isServer); |
| |
| if (this.handshakes > tls.CLIENT_RENEG_LIMIT) { |
| owner._emitTLSError(new ERR_TLS_SESSION_ATTACK()); |
| return; |
| } |
| |
| if (owner[kDisableRenegotiation]) |
| owner._emitTLSError(new ERR_TLS_RENEGOTIATION_DISABLED()); |
| } |
| |
| function onhandshakedone() { |
| debug('server onhandshakedone'); |
| |
| const owner = this[owner_symbol]; |
| assert(owner._tlsOptions.isServer); |
| |
| // `newSession` callback wasn't called yet |
| if (owner._newSessionPending) { |
| owner._securePending = true; |
| return; |
| } |
| |
| owner._finishInit(); |
| } |
| |
| |
| function loadSession(hello) { |
| debug('server onclienthello', |
| 'sessionid.len', hello.sessionId.length, |
| 'ticket?', hello.tlsTicket |
| ); |
| const owner = this[owner_symbol]; |
| |
| let once = false; |
| function onSession(err, session) { |
| debug('server resumeSession callback(err %j, sess? %s)', err, !!session); |
| if (once) |
| return owner.destroy(new ERR_MULTIPLE_CALLBACK()); |
| once = true; |
| |
| if (err) |
| return owner.destroy(err); |
| |
| if (owner._handle === null) |
| return owner.destroy(new ERR_SOCKET_CLOSED()); |
| |
| owner._handle.loadSession(session); |
| // Session is loaded. End the parser to allow handshaking to continue. |
| owner._handle.endParser(); |
| } |
| |
| if (hello.sessionId.length <= 0 || |
| hello.tlsTicket || |
| (owner.server && |
| !owner.server.emit('resumeSession', hello.sessionId, onSession))) { |
| // Sessions without identifiers can't be resumed. |
| // Sessions with tickets can be resumed directly from the ticket, no server |
| // session storage is necessary. |
| // Without a call to a resumeSession listener, a session will never be |
| // loaded, so end the parser to allow handshaking to continue. |
| owner._handle.endParser(); |
| } |
| } |
| |
| |
| function loadSNI(info) { |
| const owner = this[owner_symbol]; |
| const servername = info.servername; |
| if (!servername || !owner._SNICallback) |
| return requestOCSP(owner, info); |
| |
| let once = false; |
| owner._SNICallback(servername, (err, context) => { |
| if (once) |
| return owner.destroy(new ERR_MULTIPLE_CALLBACK()); |
| once = true; |
| |
| if (err) |
| return owner.destroy(err); |
| |
| if (owner._handle === null) |
| return owner.destroy(new ERR_SOCKET_CLOSED()); |
| |
| // TODO(indutny): eventually disallow raw `SecureContext` |
| if (context) |
| owner._handle.sni_context = context.context || context; |
| |
| requestOCSP(owner, info); |
| }); |
| } |
| |
| |
| function requestOCSP(socket, info) { |
| if (!info.OCSPRequest || !socket.server) |
| return requestOCSPDone(socket); |
| |
| let ctx = socket._handle.sni_context; |
| |
| if (!ctx) { |
| ctx = socket.server._sharedCreds; |
| |
| // TLS socket is using a `net.Server` instead of a tls.TLSServer. |
| // Some TLS properties like `server._sharedCreds` will not be present |
| if (!ctx) |
| return requestOCSPDone(socket); |
| } |
| |
| // TODO(indutny): eventually disallow raw `SecureContext` |
| if (ctx.context) |
| ctx = ctx.context; |
| |
| if (socket.server.listenerCount('OCSPRequest') === 0) { |
| return requestOCSPDone(socket); |
| } |
| |
| let once = false; |
| const onOCSP = (err, response) => { |
| debug('server OCSPRequest done', 'handle?', !!socket._handle, 'once?', once, |
| 'response?', !!response, 'err?', err); |
| if (once) |
| return socket.destroy(new ERR_MULTIPLE_CALLBACK()); |
| once = true; |
| |
| if (err) |
| return socket.destroy(err); |
| |
| if (socket._handle === null) |
| return socket.destroy(new ERR_SOCKET_CLOSED()); |
| |
| if (response) |
| socket._handle.setOCSPResponse(response); |
| requestOCSPDone(socket); |
| }; |
| |
| debug('server oncertcb emit OCSPRequest'); |
| socket.server.emit('OCSPRequest', |
| ctx.getCertificate(), |
| ctx.getIssuer(), |
| onOCSP); |
| } |
| |
| function requestOCSPDone(socket) { |
| debug('server certcb done'); |
| try { |
| socket._handle.certCbDone(); |
| } catch (e) { |
| debug('server certcb done errored', e); |
| socket.destroy(e); |
| } |
| } |
| |
| function onnewsessionclient(sessionId, session) { |
| debug('client emit session'); |
| const owner = this[owner_symbol]; |
| if (owner[kIsVerified]) { |
| owner.emit('session', session); |
| } else { |
| owner[kPendingSession] = session; |
| } |
| } |
| |
| function onnewsession(sessionId, session) { |
| debug('onnewsession'); |
| const owner = this[owner_symbol]; |
| |
| // TODO(@sam-github) no server to emit the event on, but handshake won't |
| // continue unless newSessionDone() is called, should it be, or is that |
| // situation unreachable, or only occurring during shutdown? |
| if (!owner.server) |
| return; |
| |
| let once = false; |
| const done = () => { |
| debug('onnewsession done'); |
| if (once) |
| return; |
| once = true; |
| |
| if (owner._handle === null) |
| return owner.destroy(new ERR_SOCKET_CLOSED()); |
| |
| this.newSessionDone(); |
| |
| owner._newSessionPending = false; |
| if (owner._securePending) |
| owner._finishInit(); |
| owner._securePending = false; |
| }; |
| |
| owner._newSessionPending = true; |
| if (!owner.server.emit('newSession', sessionId, session, done)) |
| done(); |
| } |
| |
| function onPskServerCallback(identity, maxPskLen) { |
| const owner = this[owner_symbol]; |
| const ret = owner[kPskCallback](owner, identity); |
| if (ret == null) |
| return undefined; |
| |
| let psk; |
| if (isArrayBufferView(ret)) { |
| psk = ret; |
| } else { |
| if (typeof ret !== 'object') { |
| throw new ERR_INVALID_ARG_TYPE( |
| 'ret', |
| ['Object', 'Buffer', 'TypedArray', 'DataView'], |
| ret |
| ); |
| } |
| psk = ret.psk; |
| validateBuffer(psk, 'psk'); |
| } |
| |
| if (psk.length > maxPskLen) { |
| throw new ERR_INVALID_ARG_VALUE( |
| 'psk', |
| psk, |
| `Pre-shared key exceeds ${maxPskLen} bytes` |
| ); |
| } |
| |
| return psk; |
| } |
| |
| function onPskClientCallback(hint, maxPskLen, maxIdentityLen) { |
| const owner = this[owner_symbol]; |
| const ret = owner[kPskCallback](hint); |
| if (ret == null) |
| return undefined; |
| |
| if (typeof ret !== 'object') |
| throw new ERR_INVALID_ARG_TYPE('ret', 'Object', ret); |
| |
| validateBuffer(ret.psk, 'psk'); |
| if (ret.psk.length > maxPskLen) { |
| throw new ERR_INVALID_ARG_VALUE( |
| 'psk', |
| ret.psk, |
| `Pre-shared key exceeds ${maxPskLen} bytes` |
| ); |
| } |
| |
| validateString(ret.identity, 'identity'); |
| if (Buffer.byteLength(ret.identity) > maxIdentityLen) { |
| throw new ERR_INVALID_ARG_VALUE( |
| 'identity', |
| ret.identity, |
| `PSK identity exceeds ${maxIdentityLen} bytes` |
| ); |
| } |
| |
| return { psk: ret.psk, identity: ret.identity }; |
| } |
| |
| function onkeylog(line) { |
| debug('onkeylog'); |
| this[owner_symbol].emit('keylog', line); |
| } |
| |
| function onocspresponse(resp) { |
| debug('client onocspresponse'); |
| this[owner_symbol].emit('OCSPResponse', resp); |
| } |
| |
| function onerror(err) { |
| const owner = this[owner_symbol]; |
| debug('%s onerror %s had? %j', |
| owner._tlsOptions.isServer ? 'server' : 'client', err, |
| owner._hadError); |
| |
| if (owner._hadError) |
| return; |
| |
| owner._hadError = true; |
| |
| // Destroy socket if error happened before handshake's finish |
| if (!owner._secureEstablished) { |
| // When handshake fails control is not yet released, |
| // so self._tlsError will return null instead of actual error |
| owner.destroy(err); |
| } else if (owner._tlsOptions.isServer && |
| owner._rejectUnauthorized && |
| /peer did not return a certificate/.test(err.message)) { |
| // Ignore server's authorization errors |
| owner.destroy(); |
| } else { |
| // Emit error |
| owner._emitTLSError(err); |
| } |
| } |
| |
| // Used by both client and server TLSSockets to start data flowing from _handle, |
| // read(0) causes a StreamBase::ReadStart, via Socket._read. |
| function initRead(tlsSocket, socket) { |
| debug('%s initRead', |
| tlsSocket._tlsOptions.isServer ? 'server' : 'client', |
| 'handle?', !!tlsSocket._handle, |
| 'buffered?', !!socket && socket.readableLength |
| ); |
| // If we were destroyed already don't bother reading |
| if (!tlsSocket._handle) |
| return; |
| |
| // Socket already has some buffered data - emulate receiving it |
| if (socket && socket.readableLength) { |
| let buf; |
| while ((buf = socket.read()) !== null) |
| tlsSocket._handle.receive(buf); |
| } |
| |
| tlsSocket.read(0); |
| } |
| |
| /** |
| * Provides a wrap of socket stream to do encrypted communication. |
| */ |
| |
| function TLSSocket(socket, opts) { |
| const tlsOptions = { ...opts }; |
| let enableTrace = tlsOptions.enableTrace; |
| |
| if (enableTrace == null) { |
| enableTrace = traceTls; |
| |
| if (enableTrace && !tlsTracingWarned) { |
| tlsTracingWarned = true; |
| process.emitWarning('Enabling --trace-tls can expose sensitive data in ' + |
| 'the resulting log.'); |
| } |
| } else if (typeof enableTrace !== 'boolean') { |
| throw new ERR_INVALID_ARG_TYPE( |
| 'options.enableTrace', 'boolean', enableTrace); |
| } |
| |
| if (tlsOptions.ALPNProtocols) |
| tls.convertALPNProtocols(tlsOptions.ALPNProtocols, tlsOptions); |
| |
| this._tlsOptions = tlsOptions; |
| this._secureEstablished = false; |
| this._securePending = false; |
| this._newSessionPending = false; |
| this._controlReleased = false; |
| this.secureConnecting = true; |
| this._SNICallback = null; |
| this.servername = null; |
| this.alpnProtocol = null; |
| this.authorized = false; |
| this.authorizationError = null; |
| this[kRes] = null; |
| this[kIsVerified] = false; |
| this[kPendingSession] = null; |
| |
| let wrap; |
| if ((socket instanceof net.Socket && socket._handle) || !socket) { |
| // 1. connected socket |
| // 2. no socket, one will be created with net.Socket().connect |
| wrap = socket; |
| } else { |
| // 3. socket has no handle so it is js not c++ |
| // 4. unconnected sockets are wrapped |
| // TLS expects to interact from C++ with a net.Socket that has a C++ stream |
| // handle, but a JS stream doesn't have one. Wrap it up to make it look like |
| // a socket. |
| wrap = new JSStreamSocket(socket); |
| } |
| |
| // Just a documented property to make secure sockets |
| // distinguishable from regular ones. |
| this.encrypted = true; |
| |
| net.Socket.call(this, { |
| handle: this._wrapHandle(wrap), |
| allowHalfOpen: socket ? socket.allowHalfOpen : tlsOptions.allowHalfOpen, |
| pauseOnCreate: tlsOptions.pauseOnConnect, |
| manualStart: true, |
| highWaterMark: tlsOptions.highWaterMark, |
| }); |
| |
| // Proxy for API compatibility |
| this.ssl = this._handle; // C++ TLSWrap object |
| |
| this.on('error', this._tlsError); |
| |
| this._init(socket, wrap); |
| |
| if (enableTrace && this._handle) |
| this._handle.enableTrace(); |
| |
| // Read on next tick so the caller has a chance to setup listeners |
| process.nextTick(initRead, this, socket); |
| } |
| ObjectSetPrototypeOf(TLSSocket.prototype, net.Socket.prototype); |
| ObjectSetPrototypeOf(TLSSocket, net.Socket); |
| exports.TLSSocket = TLSSocket; |
| |
| const proxiedMethods = [ |
| 'ref', 'unref', 'open', 'bind', 'listen', 'connect', 'bind6', |
| 'connect6', 'getsockname', 'getpeername', 'setNoDelay', 'setKeepAlive', |
| 'setSimultaneousAccepts', 'setBlocking', |
| |
| // PipeWrap |
| 'setPendingInstances', |
| ]; |
| |
| // Proxy HandleWrap, PipeWrap and TCPWrap methods |
| function makeMethodProxy(name) { |
| return function methodProxy(...args) { |
| if (this._parent[name]) |
| return this._parent[name].apply(this._parent, args); |
| }; |
| } |
| for (const proxiedMethod of proxiedMethods) { |
| tls_wrap.TLSWrap.prototype[proxiedMethod] = |
| makeMethodProxy(proxiedMethod); |
| } |
| |
| tls_wrap.TLSWrap.prototype.close = function close(cb) { |
| let ssl; |
| if (this[owner_symbol]) { |
| ssl = this[owner_symbol].ssl; |
| this[owner_symbol].ssl = null; |
| } |
| |
| // Invoke `destroySSL` on close to clean up possibly pending write requests |
| // that may self-reference TLSWrap, leading to leak |
| const done = () => { |
| if (ssl) { |
| ssl.destroySSL(); |
| if (ssl._secureContext.singleUse) { |
| ssl._secureContext.context.close(); |
| ssl._secureContext.context = null; |
| } |
| } |
| if (cb) |
| cb(); |
| }; |
| |
| if (this._parentWrap && this._parentWrap._handle === this._parent) { |
| this._parentWrap.once('close', done); |
| return this._parentWrap.destroy(); |
| } |
| return this._parent.close(done); |
| }; |
| |
| TLSSocket.prototype.disableRenegotiation = function disableRenegotiation() { |
| this[kDisableRenegotiation] = true; |
| }; |
| |
| TLSSocket.prototype._wrapHandle = function(wrap) { |
| let handle; |
| |
| if (wrap) |
| handle = wrap._handle; |
| |
| const options = this._tlsOptions; |
| if (!handle) { |
| handle = options.pipe ? |
| new Pipe(PipeConstants.SOCKET) : |
| new TCP(TCPConstants.SOCKET); |
| handle[owner_symbol] = this; |
| } |
| |
| // Wrap socket's handle |
| const context = options.secureContext || |
| options.credentials || |
| tls.createSecureContext(options); |
| assert(handle.isStreamBase, 'handle must be a StreamBase'); |
| if (!(context.context instanceof NativeSecureContext)) { |
| throw new ERR_TLS_INVALID_CONTEXT('context'); |
| } |
| const res = tls_wrap.wrap(handle, context.context, !!options.isServer); |
| res._parent = handle; // C++ "wrap" object: TCPWrap, JSStream, ... |
| res._parentWrap = wrap; // JS object: net.Socket, JSStreamSocket, ... |
| res._secureContext = context; |
| res.reading = handle.reading; |
| this[kRes] = res; |
| defineHandleReading(this, handle); |
| |
| this.on('close', onSocketCloseDestroySSL); |
| |
| return res; |
| }; |
| |
| // This eliminates a cyclic reference to TLSWrap |
| // Ref: https://github.com/nodejs/node/commit/f7620fb96d339f704932f9bb9a0dceb9952df2d4 |
| function defineHandleReading(socket, handle) { |
| ObjectDefineProperty(handle, 'reading', { |
| get: () => { |
| return socket[kRes].reading; |
| }, |
| set: (value) => { |
| socket[kRes].reading = value; |
| } |
| }); |
| } |
| |
| function onSocketCloseDestroySSL() { |
| // Make sure we are not doing it on OpenSSL's stack |
| setImmediate(destroySSL, this); |
| this[kRes] = null; |
| } |
| |
| function destroySSL(self) { |
| self._destroySSL(); |
| } |
| |
| TLSSocket.prototype._destroySSL = function _destroySSL() { |
| if (!this.ssl) return; |
| this.ssl.destroySSL(); |
| if (this.ssl._secureContext.singleUse) { |
| this.ssl._secureContext.context.close(); |
| this.ssl._secureContext.context = null; |
| } |
| this.ssl = null; |
| this[kPendingSession] = null; |
| this[kIsVerified] = false; |
| }; |
| |
| // Constructor guts, arbitrarily factored out. |
| let warnOnTlsKeylog = true; |
| let warnOnTlsKeylogError = true; |
| TLSSocket.prototype._init = function(socket, wrap) { |
| const options = this._tlsOptions; |
| const ssl = this._handle; |
| this.server = options.server; |
| |
| debug('%s _init', |
| options.isServer ? 'server' : 'client', |
| 'handle?', !!ssl |
| ); |
| |
| // Clients (!isServer) always request a cert, servers request a client cert |
| // only on explicit configuration. |
| const requestCert = !!options.requestCert || !options.isServer; |
| const rejectUnauthorized = !!options.rejectUnauthorized; |
| |
| this._requestCert = requestCert; |
| this._rejectUnauthorized = rejectUnauthorized; |
| if (requestCert || rejectUnauthorized) |
| ssl.setVerifyMode(requestCert, rejectUnauthorized); |
| |
| // Only call .onkeylog if there is a keylog listener. |
| ssl.onkeylog = onkeylog; |
| this.on('newListener', keylogNewListener); |
| |
| function keylogNewListener(event) { |
| if (event !== 'keylog') |
| return; |
| |
| ssl.enableKeylogCallback(); |
| |
| // Remove this listener since it's no longer needed. |
| this.removeListener('newListener', keylogNewListener); |
| } |
| |
| if (options.isServer) { |
| ssl.onhandshakestart = onhandshakestart; |
| ssl.onhandshakedone = onhandshakedone; |
| ssl.onclienthello = loadSession; |
| ssl.oncertcb = loadSNI; |
| ssl.onnewsession = onnewsession; |
| ssl.lastHandshakeTime = 0; |
| ssl.handshakes = 0; |
| |
| if (this.server) { |
| if (this.server.listenerCount('resumeSession') > 0 || |
| this.server.listenerCount('newSession') > 0) { |
| // Also starts the client hello parser as a side effect. |
| ssl.enableSessionCallbacks(); |
| } |
| if (this.server.listenerCount('OCSPRequest') > 0) |
| ssl.enableCertCb(); |
| } |
| } else { |
| ssl.onhandshakestart = noop; |
| ssl.onhandshakedone = () => { |
| debug('client onhandshakedone'); |
| this._finishInit(); |
| }; |
| ssl.onocspresponse = onocspresponse; |
| |
| if (options.session) |
| ssl.setSession(options.session); |
| |
| ssl.onnewsession = onnewsessionclient; |
| |
| // Only call .onnewsession if there is a session listener. |
| this.on('newListener', newListener); |
| |
| function newListener(event) { |
| if (event !== 'session') |
| return; |
| |
| ssl.enableSessionCallbacks(); |
| |
| // Remove this listener since it's no longer needed. |
| this.removeListener('newListener', newListener); |
| } |
| } |
| |
| if (tlsKeylog) { |
| if (warnOnTlsKeylog) { |
| warnOnTlsKeylog = false; |
| process.emitWarning('Using --tls-keylog makes TLS connections insecure ' + |
| 'by writing secret key material to file ' + tlsKeylog); |
| } |
| this.on('keylog', (line) => { |
| appendFile(tlsKeylog, line, { mode: 0o600 }, (err) => { |
| if (err && warnOnTlsKeylogError) { |
| warnOnTlsKeylogError = false; |
| process.emitWarning('Failed to write TLS keylog (this warning ' + |
| 'will not be repeated): ' + err); |
| } |
| }); |
| }); |
| } |
| |
| ssl.onerror = onerror; |
| |
| // If custom SNICallback was given, or if |
| // there're SNI contexts to perform match against - |
| // set `.onsniselect` callback. |
| if (options.isServer && |
| options.SNICallback && |
| (options.SNICallback !== SNICallback || |
| (options.server && options.server._contexts.length))) { |
| assert(typeof options.SNICallback === 'function'); |
| this._SNICallback = options.SNICallback; |
| ssl.enableCertCb(); |
| } |
| |
| if (options.ALPNProtocols) { |
| // Keep reference in secureContext not to be GC-ed |
| ssl._secureContext.alpnBuffer = options.ALPNProtocols; |
| ssl.setALPNProtocols(ssl._secureContext.alpnBuffer); |
| } |
| |
| if (options.pskCallback && ssl.enablePskCallback) { |
| if (typeof options.pskCallback !== 'function') { |
| throw new ERR_INVALID_ARG_TYPE('pskCallback', |
| 'function', |
| options.pskCallback); |
| } |
| |
| ssl[kOnPskExchange] = options.isServer ? |
| onPskServerCallback : onPskClientCallback; |
| |
| this[kPskCallback] = options.pskCallback; |
| ssl.enablePskCallback(); |
| |
| if (options.pskIdentityHint) { |
| if (typeof options.pskIdentityHint !== 'string') { |
| throw new ERR_INVALID_ARG_TYPE( |
| 'options.pskIdentityHint', |
| 'string', |
| options.pskIdentityHint |
| ); |
| } |
| ssl.setPskIdentityHint(options.pskIdentityHint); |
| } |
| } |
| |
| |
| if (options.handshakeTimeout > 0) |
| this.setTimeout(options.handshakeTimeout, this._handleTimeout); |
| |
| if (socket instanceof net.Socket) { |
| this._parent = socket; |
| |
| // To prevent assertion in afterConnect() and properly kick off readStart |
| this.connecting = socket.connecting || !socket._handle; |
| socket.once('connect', () => { |
| this.connecting = false; |
| this.emit('connect'); |
| }); |
| } |
| |
| // Assume `tls.connect()` |
| if (wrap) { |
| wrap.on('error', (err) => this._emitTLSError(err)); |
| } else { |
| assert(!socket); |
| this.connecting = true; |
| } |
| }; |
| |
| TLSSocket.prototype.renegotiate = function(options, callback) { |
| if (options === null || typeof options !== 'object') |
| throw new ERR_INVALID_ARG_TYPE('options', 'Object', options); |
| if (callback !== undefined && typeof callback !== 'function') |
| throw new ERR_INVALID_CALLBACK(callback); |
| |
| debug('%s renegotiate()', |
| this._tlsOptions.isServer ? 'server' : 'client', |
| 'destroyed?', this.destroyed |
| ); |
| |
| if (this.destroyed) |
| return; |
| |
| let requestCert = !!this._requestCert; |
| let rejectUnauthorized = !!this._rejectUnauthorized; |
| |
| if (options.requestCert !== undefined) |
| requestCert = !!options.requestCert; |
| if (options.rejectUnauthorized !== undefined) |
| rejectUnauthorized = !!options.rejectUnauthorized; |
| |
| if (requestCert !== this._requestCert || |
| rejectUnauthorized !== this._rejectUnauthorized) { |
| this._handle.setVerifyMode(requestCert, rejectUnauthorized); |
| this._requestCert = requestCert; |
| this._rejectUnauthorized = rejectUnauthorized; |
| } |
| // Ensure that we'll cycle through internal openssl's state |
| this.write(''); |
| |
| try { |
| this._handle.renegotiate(); |
| } catch (err) { |
| if (callback) { |
| process.nextTick(callback, err); |
| } |
| return false; |
| } |
| |
| // Ensure that we'll cycle through internal openssl's state |
| this.write(''); |
| |
| if (callback) { |
| this.once('secure', () => callback(null)); |
| } |
| |
| return true; |
| }; |
| |
| TLSSocket.prototype.exportKeyingMaterial = function(length, label, context) { |
| validateUint32(length, 'length', true); |
| validateString(label, 'label'); |
| if (context !== undefined) |
| validateBuffer(context, 'context'); |
| |
| if (!this._secureEstablished) |
| throw new ERR_TLS_INVALID_STATE(); |
| |
| return this._handle.exportKeyingMaterial(length, label, context); |
| }; |
| |
| TLSSocket.prototype.setMaxSendFragment = function setMaxSendFragment(size) { |
| return this._handle.setMaxSendFragment(size) === 1; |
| }; |
| |
| TLSSocket.prototype._handleTimeout = function() { |
| this._emitTLSError(new ERR_TLS_HANDSHAKE_TIMEOUT()); |
| }; |
| |
| TLSSocket.prototype._emitTLSError = function(err) { |
| const e = this._tlsError(err); |
| if (e) |
| this.emit('error', e); |
| }; |
| |
| TLSSocket.prototype._tlsError = function(err) { |
| this.emit('_tlsError', err); |
| if (this._controlReleased) |
| return err; |
| return null; |
| }; |
| |
| TLSSocket.prototype._releaseControl = function() { |
| if (this._controlReleased) |
| return false; |
| this._controlReleased = true; |
| this.removeListener('error', this._tlsError); |
| return true; |
| }; |
| |
| TLSSocket.prototype._finishInit = function() { |
| // Guard against getting onhandshakedone() after .destroy(). |
| // * 1.2: If destroy() during onocspresponse(), then write of next handshake |
| // record fails, the handshake done info callbacks does not occur, and the |
| // socket closes. |
| // * 1.3: The OCSP response comes in the same record that finishes handshake, |
| // so even after .destroy(), the handshake done info callback occurs |
| // immediately after onocspresponse(). Ignore it. |
| if (!this._handle) |
| return; |
| |
| this.alpnProtocol = this._handle.getALPNNegotiatedProtocol(); |
| // The servername could be set by TLSWrap::SelectSNIContextCallback(). |
| if (this.servername === null) { |
| this.servername = this._handle.getServername(); |
| } |
| |
| debug('%s _finishInit', |
| this._tlsOptions.isServer ? 'server' : 'client', |
| 'handle?', !!this._handle, |
| 'alpn', this.alpnProtocol, |
| 'servername', this.servername); |
| |
| this._secureEstablished = true; |
| if (this._tlsOptions.handshakeTimeout > 0) |
| this.setTimeout(0, this._handleTimeout); |
| this.emit('secure'); |
| }; |
| |
| TLSSocket.prototype._start = function() { |
| debug('%s _start', |
| this._tlsOptions.isServer ? 'server' : 'client', |
| 'handle?', !!this._handle, |
| 'connecting?', this.connecting, |
| 'requestOCSP?', !!this._tlsOptions.requestOCSP, |
| ); |
| if (this.connecting) { |
| this.once('connect', this._start); |
| return; |
| } |
| |
| // Socket was destroyed before the connection was established |
| if (!this._handle) |
| return; |
| |
| if (this._tlsOptions.requestOCSP) |
| this._handle.requestOCSP(); |
| this._handle.start(); |
| }; |
| |
| TLSSocket.prototype.setServername = function(name) { |
| validateString(name, 'name'); |
| |
| if (this._tlsOptions.isServer) { |
| throw new ERR_TLS_SNI_FROM_SERVER(); |
| } |
| |
| this._handle.setServername(name); |
| }; |
| |
| TLSSocket.prototype.setSession = function(session) { |
| if (typeof session === 'string') |
| session = Buffer.from(session, 'latin1'); |
| this._handle.setSession(session); |
| }; |
| |
| TLSSocket.prototype.getPeerCertificate = function(detailed) { |
| if (this._handle) { |
| return common.translatePeerCertificate( |
| this._handle.getPeerCertificate(detailed)) || {}; |
| } |
| |
| return null; |
| }; |
| |
| TLSSocket.prototype.getCertificate = function() { |
| if (this._handle) { |
| // It's not a peer cert, but the formatting is identical. |
| return common.translatePeerCertificate( |
| this._handle.getCertificate()) || {}; |
| } |
| |
| return null; |
| }; |
| |
| // Proxy TLSSocket handle methods |
| function makeSocketMethodProxy(name) { |
| return function socketMethodProxy(...args) { |
| if (this._handle) |
| return this._handle[name].apply(this._handle, args); |
| return null; |
| }; |
| } |
| |
| [ |
| 'getCipher', |
| 'getSharedSigalgs', |
| 'getEphemeralKeyInfo', |
| 'getFinished', |
| 'getPeerFinished', |
| 'getProtocol', |
| 'getSession', |
| 'getTLSTicket', |
| 'isSessionReused', |
| 'enableTrace', |
| ].forEach((method) => { |
| TLSSocket.prototype[method] = makeSocketMethodProxy(method); |
| }); |
| |
| // TODO: support anonymous (nocert) |
| |
| |
| function onServerSocketSecure() { |
| if (this._requestCert) { |
| const verifyError = this._handle.verifyError(); |
| if (verifyError) { |
| this.authorizationError = verifyError.code; |
| |
| if (this._rejectUnauthorized) |
| this.destroy(); |
| } else { |
| this.authorized = true; |
| } |
| } |
| |
| if (!this.destroyed && this._releaseControl()) { |
| debug('server emit secureConnection'); |
| this.secureConnecting = false; |
| this._tlsOptions.server.emit('secureConnection', this); |
| } |
| } |
| |
| function onSocketTLSError(err) { |
| if (!this._controlReleased && !this[kErrorEmitted]) { |
| this[kErrorEmitted] = true; |
| debug('server emit tlsClientError:', err); |
| this._tlsOptions.server.emit('tlsClientError', err, this); |
| } |
| } |
| |
| function onSocketKeylog(line) { |
| this._tlsOptions.server.emit('keylog', line, this); |
| } |
| |
| function onSocketClose(err) { |
| // Closed because of error - no need to emit it twice |
| if (err) |
| return; |
| |
| // Emit ECONNRESET |
| if (!this._controlReleased && !this[kErrorEmitted]) { |
| this[kErrorEmitted] = true; |
| const connReset = connResetException('socket hang up'); |
| this._tlsOptions.server.emit('tlsClientError', connReset, this); |
| } |
| } |
| |
| function tlsConnectionListener(rawSocket) { |
| debug('net.Server.on(connection): new TLSSocket'); |
| const socket = new TLSSocket(rawSocket, { |
| secureContext: this._sharedCreds, |
| isServer: true, |
| server: this, |
| requestCert: this.requestCert, |
| rejectUnauthorized: this.rejectUnauthorized, |
| handshakeTimeout: this[kHandshakeTimeout], |
| ALPNProtocols: this.ALPNProtocols, |
| SNICallback: this[kSNICallback] || SNICallback, |
| enableTrace: this[kEnableTrace], |
| pauseOnConnect: this.pauseOnConnect, |
| pskCallback: this[kPskCallback], |
| pskIdentityHint: this[kPskIdentityHint], |
| }); |
| |
| socket.on('secure', onServerSocketSecure); |
| |
| if (this.listenerCount('keylog') > 0) |
| socket.on('keylog', onSocketKeylog); |
| |
| socket[kErrorEmitted] = false; |
| socket.on('close', onSocketClose); |
| socket.on('_tlsError', onSocketTLSError); |
| } |
| |
| // 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 now knows the client's identity |
| // 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. |
| // - clientCertEngine: 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) { |
| if (!(this instanceof Server)) |
| return new Server(options, listener); |
| |
| if (typeof options === 'function') { |
| listener = options; |
| options = {}; |
| } else if (options == null || typeof options === 'object') { |
| options = options || {}; |
| } else { |
| throw new ERR_INVALID_ARG_TYPE('options', 'Object', options); |
| } |
| |
| this._contexts = []; |
| this.requestCert = options.requestCert === true; |
| this.rejectUnauthorized = options.rejectUnauthorized !== false; |
| |
| if (options.sessionTimeout) |
| this.sessionTimeout = options.sessionTimeout; |
| |
| if (options.ticketKeys) |
| this.ticketKeys = options.ticketKeys; |
| |
| if (options.ALPNProtocols) |
| tls.convertALPNProtocols(options.ALPNProtocols, this); |
| |
| this.setSecureContext(options); |
| |
| this[kHandshakeTimeout] = options.handshakeTimeout || (120 * 1000); |
| this[kSNICallback] = options.SNICallback; |
| this[kPskCallback] = options.pskCallback; |
| this[kPskIdentityHint] = options.pskIdentityHint; |
| |
| if (typeof this[kHandshakeTimeout] !== 'number') { |
| throw new ERR_INVALID_ARG_TYPE( |
| 'options.handshakeTimeout', 'number', options.handshakeTimeout); |
| } |
| |
| if (this[kSNICallback] && typeof this[kSNICallback] !== 'function') { |
| throw new ERR_INVALID_ARG_TYPE( |
| 'options.SNICallback', 'function', options.SNICallback); |
| } |
| |
| if (this[kPskCallback] && typeof this[kPskCallback] !== 'function') { |
| throw new ERR_INVALID_ARG_TYPE( |
| 'options.pskCallback', 'function', options.pskCallback); |
| } |
| if (this[kPskIdentityHint] && typeof this[kPskIdentityHint] !== 'string') { |
| throw new ERR_INVALID_ARG_TYPE( |
| 'options.pskIdentityHint', |
| 'string', |
| options.pskIdentityHint |
| ); |
| } |
| |
| // constructor call |
| net.Server.call(this, options, tlsConnectionListener); |
| |
| if (listener) { |
| this.on('secureConnection', listener); |
| } |
| |
| this[kEnableTrace] = options.enableTrace; |
| } |
| |
| ObjectSetPrototypeOf(Server.prototype, net.Server.prototype); |
| ObjectSetPrototypeOf(Server, net.Server); |
| exports.Server = Server; |
| exports.createServer = function createServer(options, listener) { |
| return new Server(options, listener); |
| }; |
| |
| |
| Server.prototype.setSecureContext = function(options) { |
| if (options === null || typeof options !== 'object') |
| throw new ERR_INVALID_ARG_TYPE('options', 'Object', options); |
| |
| if (options.pfx) |
| this.pfx = options.pfx; |
| else |
| this.pfx = undefined; |
| |
| if (options.key) |
| this.key = options.key; |
| else |
| this.key = undefined; |
| |
| if (options.passphrase) |
| this.passphrase = options.passphrase; |
| else |
| this.passphrase = undefined; |
| |
| if (options.cert) |
| this.cert = options.cert; |
| else |
| this.cert = undefined; |
| |
| if (options.clientCertEngine) |
| this.clientCertEngine = options.clientCertEngine; |
| else |
| this.clientCertEngine = undefined; |
| |
| if (options.ca) |
| this.ca = options.ca; |
| else |
| this.ca = undefined; |
| |
| if (options.minVersion) |
| this.minVersion = options.minVersion; |
| else |
| this.minVersion = undefined; |
| |
| if (options.maxVersion) |
| this.maxVersion = options.maxVersion; |
| else |
| this.maxVersion = undefined; |
| |
| if (options.secureProtocol) |
| this.secureProtocol = options.secureProtocol; |
| else |
| this.secureProtocol = undefined; |
| |
| if (options.crl) |
| this.crl = options.crl; |
| else |
| this.crl = undefined; |
| |
| this.sigalgs = options.sigalgs; |
| |
| if (options.ciphers) |
| this.ciphers = options.ciphers; |
| else |
| this.ciphers = undefined; |
| |
| this.ecdhCurve = options.ecdhCurve; |
| |
| if (options.dhparam) |
| this.dhparam = options.dhparam; |
| else |
| this.dhparam = undefined; |
| |
| if (options.honorCipherOrder !== undefined) |
| this.honorCipherOrder = !!options.honorCipherOrder; |
| else |
| this.honorCipherOrder = true; |
| |
| const secureOptions = options.secureOptions || 0; |
| |
| if (secureOptions) |
| this.secureOptions = secureOptions; |
| else |
| this.secureOptions = undefined; |
| |
| if (options.sessionIdContext) { |
| this.sessionIdContext = options.sessionIdContext; |
| } else { |
| this.sessionIdContext = crypto.createHash('sha1') |
| .update(process.argv.join(' ')) |
| .digest('hex') |
| .slice(0, 32); |
| } |
| |
| if (options.sessionTimeout) |
| this.sessionTimeout = options.sessionTimeout; |
| |
| if (options.ticketKeys) |
| this.ticketKeys = options.ticketKeys; |
| |
| this._sharedCreds = tls.createSecureContext({ |
| pfx: this.pfx, |
| key: this.key, |
| passphrase: this.passphrase, |
| cert: this.cert, |
| clientCertEngine: this.clientCertEngine, |
| ca: this.ca, |
| ciphers: this.ciphers, |
| sigalgs: this.sigalgs, |
| ecdhCurve: this.ecdhCurve, |
| dhparam: this.dhparam, |
| minVersion: this.minVersion, |
| maxVersion: this.maxVersion, |
| secureProtocol: this.secureProtocol, |
| secureOptions: this.secureOptions, |
| honorCipherOrder: this.honorCipherOrder, |
| crl: this.crl, |
| sessionIdContext: this.sessionIdContext, |
| ticketKeys: this.ticketKeys, |
| sessionTimeout: this.sessionTimeout |
| }); |
| }; |
| |
| |
| Server.prototype._getServerData = function() { |
| return { |
| ticketKeys: this.getTicketKeys().toString('hex') |
| }; |
| }; |
| |
| |
| Server.prototype._setServerData = function(data) { |
| this.setTicketKeys(Buffer.from(data.ticketKeys, 'hex')); |
| }; |
| |
| |
| Server.prototype.getTicketKeys = function getTicketKeys() { |
| return this._sharedCreds.context.getTicketKeys(); |
| }; |
| |
| |
| Server.prototype.setTicketKeys = function setTicketKeys(keys) { |
| this._sharedCreds.context.setTicketKeys(keys); |
| }; |
| |
| |
| Server.prototype.setOptions = deprecate(function(options) { |
| this.requestCert = options.requestCert === true; |
| this.rejectUnauthorized = options.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.clientCertEngine) |
| this.clientCertEngine = options.clientCertEngine; |
| if (options.ca) this.ca = options.ca; |
| if (options.minVersion) this.minVersion = options.minVersion; |
| if (options.maxVersion) this.maxVersion = options.maxVersion; |
| if (options.secureProtocol) this.secureProtocol = options.secureProtocol; |
| if (options.crl) this.crl = options.crl; |
| if (options.ciphers) this.ciphers = options.ciphers; |
| if (options.ecdhCurve !== undefined) |
| this.ecdhCurve = options.ecdhCurve; |
| if (options.dhparam) this.dhparam = options.dhparam; |
| if (options.sessionTimeout) this.sessionTimeout = options.sessionTimeout; |
| if (options.ticketKeys) this.ticketKeys = options.ticketKeys; |
| const secureOptions = options.secureOptions || 0; |
| if (options.honorCipherOrder !== undefined) |
| this.honorCipherOrder = !!options.honorCipherOrder; |
| else |
| this.honorCipherOrder = true; |
| if (secureOptions) this.secureOptions = secureOptions; |
| if (options.ALPNProtocols) |
| tls.convertALPNProtocols(options.ALPNProtocols, this); |
| if (options.sessionIdContext) { |
| this.sessionIdContext = options.sessionIdContext; |
| } else { |
| this.sessionIdContext = crypto.createHash('sha1') |
| .update(process.argv.join(' ')) |
| .digest('hex') |
| .slice(0, 32); |
| } |
| if (options.pskCallback) this[kPskCallback] = options.pskCallback; |
| if (options.pskIdentityHint) this[kPskIdentityHint] = options.pskIdentityHint; |
| }, 'Server.prototype.setOptions() is deprecated', 'DEP0122'); |
| |
| // SNI Contexts High-Level API |
| Server.prototype.addContext = function(servername, context) { |
| if (!servername) { |
| throw new ERR_TLS_REQUIRED_SERVER_NAME(); |
| } |
| |
| const re = new RegExp('^' + |
| servername.replace(/([.^$+?\-\\[\]{}])/g, '\\$1') |
| .replace(/\*/g, '[^.]*') + |
| '$'); |
| this._contexts.push([re, tls.createSecureContext(context).context]); |
| }; |
| |
| Server.prototype[EE.captureRejectionSymbol] = function( |
| err, event, sock) { |
| |
| switch (event) { |
| case 'secureConnection': |
| sock.destroy(err); |
| break; |
| default: |
| net.Server.prototype[SymbolFor('nodejs.rejection')] |
| .call(this, err, event, sock); |
| } |
| }; |
| |
| function SNICallback(servername, callback) { |
| const contexts = this.server._contexts; |
| |
| for (const elem of contexts) { |
| if (elem[0].test(servername)) { |
| callback(null, elem[1]); |
| return; |
| } |
| } |
| |
| callback(null, undefined); |
| } |
| |
| |
| // Target API: |
| // |
| // let 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) { |
| const args = net._normalizeArgs(listArgs); |
| const options = args[0]; |
| const cb = args[1]; |
| |
| // If args[0] was options, then normalize dealt with it. |
| // If args[0] is port, or args[0], args[1] is host, port, we need to |
| // find the options and merge them in, normalize's options has only |
| // the host/port/path args that it knows about, not the tls options. |
| // This means that options.host overrides a host arg. |
| if (listArgs[1] !== null && typeof listArgs[1] === 'object') { |
| ObjectAssign(options, listArgs[1]); |
| } else if (listArgs[2] !== null && typeof listArgs[2] === 'object') { |
| ObjectAssign(options, listArgs[2]); |
| } |
| |
| return cb ? [options, cb] : [options]; |
| } |
| |
| function onConnectSecure() { |
| const options = this[kConnectOptions]; |
| |
| // Check the size of DHE parameter above minimum requirement |
| // specified in options. |
| const ekeyinfo = this.getEphemeralKeyInfo(); |
| if (ekeyinfo.type === 'DH' && ekeyinfo.size < options.minDHSize) { |
| const err = new ERR_TLS_DH_PARAM_SIZE(ekeyinfo.size); |
| debug('client emit:', err); |
| this.emit('error', err); |
| this.destroy(); |
| return; |
| } |
| |
| let verifyError = this._handle.verifyError(); |
| |
| // Verify that server's identity matches it's certificate's names |
| // Unless server has resumed our existing session |
| if (!verifyError && !this.isSessionReused()) { |
| const hostname = options.servername || |
| options.host || |
| (options.socket && options.socket._host) || |
| 'localhost'; |
| const cert = this.getPeerCertificate(true); |
| verifyError = options.checkServerIdentity(hostname, cert); |
| } |
| |
| if (verifyError) { |
| this.authorized = false; |
| this.authorizationError = verifyError.code || verifyError.message; |
| |
| if (options.rejectUnauthorized) { |
| this.destroy(verifyError); |
| return; |
| } |
| debug('client emit secureConnect. rejectUnauthorized: %s, ' + |
| 'authorizationError: %s', options.rejectUnauthorized, |
| this.authorizationError); |
| this.secureConnecting = false; |
| this.emit('secureConnect'); |
| } else { |
| this.authorized = true; |
| debug('client emit secureConnect. authorized:', this.authorized); |
| this.secureConnecting = false; |
| this.emit('secureConnect'); |
| } |
| |
| this[kIsVerified] = true; |
| const session = this[kPendingSession]; |
| this[kPendingSession] = null; |
| if (session) |
| this.emit('session', session); |
| |
| this.removeListener('end', onConnectEnd); |
| } |
| |
| function onConnectEnd() { |
| // NOTE: This logic is shared with _http_client.js |
| if (!this._hadError) { |
| const options = this[kConnectOptions]; |
| this._hadError = true; |
| const error = connResetException('Client network socket disconnected ' + |
| 'before secure TLS connection was ' + |
| 'established'); |
| error.path = options.path; |
| error.host = options.host; |
| error.port = options.port; |
| error.localAddress = options.localAddress; |
| this.destroy(error); |
| } |
| } |
| |
| // Arguments: [port,] [host,] [options,] [cb] |
| exports.connect = function connect(...args) { |
| args = normalizeConnectArgs(args); |
| let options = args[0]; |
| const cb = args[1]; |
| const allowUnauthorized = getAllowUnauthorized(); |
| |
| options = { |
| rejectUnauthorized: !allowUnauthorized, |
| ciphers: tls.DEFAULT_CIPHERS, |
| checkServerIdentity: tls.checkServerIdentity, |
| minDHSize: 1024, |
| ...options |
| }; |
| |
| if (!options.keepAlive) |
| options.singleUse = true; |
| |
| assert(typeof options.checkServerIdentity === 'function'); |
| assert(typeof options.minDHSize === 'number', |
| 'options.minDHSize is not a number: ' + options.minDHSize); |
| assert(options.minDHSize > 0, |
| 'options.minDHSize is not a positive number: ' + |
| options.minDHSize); |
| |
| const context = options.secureContext || tls.createSecureContext(options); |
| |
| const tlssock = new TLSSocket(options.socket, { |
| allowHalfOpen: options.allowHalfOpen, |
| pipe: !!options.path, |
| secureContext: context, |
| isServer: false, |
| requestCert: true, |
| rejectUnauthorized: options.rejectUnauthorized !== false, |
| session: options.session, |
| ALPNProtocols: options.ALPNProtocols, |
| requestOCSP: options.requestOCSP, |
| enableTrace: options.enableTrace, |
| pskCallback: options.pskCallback, |
| highWaterMark: options.highWaterMark, |
| }); |
| |
| tlssock[kConnectOptions] = options; |
| |
| if (cb) |
| tlssock.once('secureConnect', cb); |
| |
| if (!options.socket) { |
| // If user provided the socket, it's their responsibility to manage its |
| // connectivity. If we created one internally, we connect it. |
| if (options.timeout) { |
| tlssock.setTimeout(options.timeout); |
| } |
| |
| tlssock.connect(options, tlssock._start); |
| } |
| |
| tlssock._releaseControl(); |
| |
| if (options.session) |
| tlssock.setSession(options.session); |
| |
| if (options.servername) { |
| if (!ipServernameWarned && net.isIP(options.servername)) { |
| process.emitWarning( |
| 'Setting the TLS ServerName to an IP address is not permitted by ' + |
| 'RFC 6066. This will be ignored in a future version.', |
| 'DeprecationWarning', |
| 'DEP0123' |
| ); |
| ipServernameWarned = true; |
| } |
| tlssock.setServername(options.servername); |
| } |
| |
| if (options.socket) |
| tlssock._start(); |
| |
| tlssock.on('secure', onConnectSecure); |
| tlssock.prependListener('end', onConnectEnd); |
| |
| return tlssock; |
| }; |