mirror of
https://github.com/nodejs/node.git
synced 2024-11-24 03:07:54 +01:00
71785889c8
PR-URL: https://github.com/nodejs/node/pull/55044 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: LiviaMedeiros <livia@cirno.name>
1831 lines
52 KiB
JavaScript
1831 lines
52 KiB
JavaScript
// 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,
|
|
ReflectApply,
|
|
RegExp,
|
|
Symbol,
|
|
SymbolFor,
|
|
} = primordials;
|
|
|
|
const {
|
|
assertCrypto,
|
|
deprecate,
|
|
kEmptyObject,
|
|
} = 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 { kReinitializeHandle } = require('internal/net');
|
|
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: {
|
|
ERR_INVALID_ARG_TYPE,
|
|
ERR_INVALID_ARG_VALUE,
|
|
ERR_MULTIPLE_CALLBACK,
|
|
ERR_SOCKET_CLOSED,
|
|
ERR_TLS_ALPN_CALLBACK_INVALID_RESULT,
|
|
ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS,
|
|
ERR_TLS_DH_PARAM_SIZE,
|
|
ERR_TLS_HANDSHAKE_TIMEOUT,
|
|
ERR_TLS_INVALID_CONTEXT,
|
|
ERR_TLS_INVALID_STATE,
|
|
ERR_TLS_RENEGOTIATION_DISABLED,
|
|
ERR_TLS_REQUIRED_SERVER_NAME,
|
|
ERR_TLS_SESSION_ATTACK,
|
|
ERR_TLS_SNI_FROM_SERVER,
|
|
},
|
|
} = require('internal/errors');
|
|
const { onpskexchange: kOnPskExchange } = internalBinding('symbols');
|
|
const {
|
|
getOptionValue,
|
|
getAllowUnauthorized,
|
|
} = require('internal/options');
|
|
const {
|
|
validateBoolean,
|
|
validateBuffer,
|
|
validateFunction,
|
|
validateInt32,
|
|
validateNumber,
|
|
validateObject,
|
|
validateString,
|
|
validateUint32,
|
|
} = require('internal/validators');
|
|
const {
|
|
InternalX509Certificate,
|
|
} = require('internal/crypto/x509');
|
|
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 kALPNCallback = Symbol('alpncallback');
|
|
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 callALPNCallback(protocolsBuffer) {
|
|
const handle = this;
|
|
const socket = handle[owner_symbol];
|
|
|
|
const servername = handle.getServername();
|
|
|
|
// Collect all the protocols from the given buffer:
|
|
const protocols = [];
|
|
let offset = 0;
|
|
while (offset < protocolsBuffer.length) {
|
|
const protocolLen = protocolsBuffer[offset];
|
|
offset += 1;
|
|
|
|
const protocol = protocolsBuffer.slice(offset, offset + protocolLen);
|
|
offset += protocolLen;
|
|
|
|
protocols.push(protocol.toString('ascii'));
|
|
}
|
|
|
|
const selectedProtocol = socket[kALPNCallback]({
|
|
servername,
|
|
protocols,
|
|
});
|
|
|
|
// Undefined -> all proposed protocols rejected
|
|
if (selectedProtocol === undefined) return undefined;
|
|
|
|
const protocolIndex = protocols.indexOf(selectedProtocol);
|
|
if (protocolIndex === -1) {
|
|
throw new ERR_TLS_ALPN_CALLBACK_INVALID_RESULT(selectedProtocol, protocols);
|
|
}
|
|
let protocolOffset = 0;
|
|
for (let i = 0; i < protocolIndex; i++) {
|
|
protocolOffset += 1 + protocols[i].length;
|
|
}
|
|
|
|
return protocolOffset;
|
|
}
|
|
|
|
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;
|
|
|
|
validateObject(ret, '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',
|
|
(typeof owner._tlsOptions === 'object' && owner._tlsOptions !== null) ?
|
|
owner._tlsOptions.isServer ? 'server' : 'client' :
|
|
'unknown',
|
|
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
|
|
|
|
// Set closing the socket after emitting an event since the socket needs to
|
|
// be accessible when the `tlsClientError` event is emitted.
|
|
owner._closeAfterHandlingError = true;
|
|
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?.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 {
|
|
validateBoolean(enableTrace, 'options.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[kALPNCallback] = null;
|
|
this.servername = null;
|
|
this.alpnProtocol = null;
|
|
this.authorized = false;
|
|
this.authorizationError = null;
|
|
this[kRes] = null;
|
|
this[kIsVerified] = false;
|
|
this[kPendingSession] = null;
|
|
|
|
let wrap;
|
|
let handle;
|
|
let wrapHasActiveWriteFromPrevOwner;
|
|
|
|
if (socket) {
|
|
if (socket instanceof net.Socket && socket._handle) {
|
|
// 1. connected socket
|
|
wrap = socket;
|
|
} else {
|
|
// 2. socket has no handle so it is js not c++
|
|
// 3. 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);
|
|
}
|
|
|
|
handle = wrap._handle;
|
|
wrapHasActiveWriteFromPrevOwner = wrap.writableLength > 0;
|
|
} else {
|
|
// 4. no socket, one will be created with net.Socket().connect
|
|
wrap = null;
|
|
wrapHasActiveWriteFromPrevOwner = false;
|
|
}
|
|
|
|
// Just a documented property to make secure sockets
|
|
// distinguishable from regular ones.
|
|
this.encrypted = true;
|
|
|
|
ReflectApply(net.Socket, this, [{
|
|
handle: this._wrapHandle(wrap, handle, wrapHasActiveWriteFromPrevOwner),
|
|
allowHalfOpen: socket ? socket.allowHalfOpen : tlsOptions.allowHalfOpen,
|
|
pauseOnCreate: tlsOptions.pauseOnConnect,
|
|
manualStart: true,
|
|
highWaterMark: tlsOptions.highWaterMark,
|
|
onread: !socket ? tlsOptions.onread : null,
|
|
signal: tlsOptions.signal,
|
|
}]);
|
|
|
|
// 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();
|
|
|
|
if (wrapHasActiveWriteFromPrevOwner) {
|
|
// `wrap` is a streams.Writable in JS. This empty write will be queued
|
|
// and hence finish after all existing writes, which is the timing
|
|
// we want to start to send any tls data to `wrap`.
|
|
wrap.write('', (err) => {
|
|
if (err) {
|
|
debug('error got before writing any tls data to the underlying stream');
|
|
this.destroy(err);
|
|
return;
|
|
}
|
|
|
|
this._handle.writesIssuedByPrevListenerDone();
|
|
});
|
|
}
|
|
|
|
// 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 ReflectApply(this._parent[name], 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) {
|
|
if (this._parentWrap._handle === null) {
|
|
// The socket handle was already closed.
|
|
done();
|
|
return;
|
|
}
|
|
|
|
if (this._parentWrap._handle === this._parent) {
|
|
this._parentWrap.once('close', done);
|
|
this._parentWrap.destroy();
|
|
return;
|
|
}
|
|
}
|
|
|
|
return this._parent.close(done);
|
|
};
|
|
|
|
TLSSocket.prototype.disableRenegotiation = function disableRenegotiation() {
|
|
this[kDisableRenegotiation] = true;
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @param {null|net.Socket} wrap
|
|
* @param {null|object} handle
|
|
* @param {boolean} wrapHasActiveWriteFromPrevOwner
|
|
* @returns {object}
|
|
*/
|
|
TLSSocket.prototype._wrapHandle = function(wrap, handle, wrapHasActiveWriteFromPrevOwner) {
|
|
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,
|
|
wrapHasActiveWriteFromPrevOwner);
|
|
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);
|
|
|
|
// Guard against adding multiple listeners, as this method may be called
|
|
// repeatedly on the same socket by reinitializeHandle
|
|
if (this.listenerCount('close', onSocketCloseDestroySSL) === 0) {
|
|
this.on('close', onSocketCloseDestroySSL);
|
|
}
|
|
|
|
if (wrap) {
|
|
wrap.on('close', () => this.destroy());
|
|
}
|
|
|
|
return res;
|
|
};
|
|
|
|
TLSSocket.prototype[kReinitializeHandle] = function reinitializeHandle(handle) {
|
|
const originalServername = this.ssl ? this._handle.getServername() : null;
|
|
const originalSession = this.ssl ? this._handle.getSession() : null;
|
|
|
|
this.handle = this._wrapHandle(null, handle, false);
|
|
this.ssl = this._handle;
|
|
|
|
net.Socket.prototype[kReinitializeHandle].call(this, this.handle);
|
|
this._init();
|
|
|
|
if (this._tlsOptions.enableTrace) {
|
|
this._handle.enableTrace();
|
|
}
|
|
|
|
if (originalSession) {
|
|
this.setSession(originalSession);
|
|
}
|
|
|
|
if (originalServername) {
|
|
this.setServername(originalServername);
|
|
}
|
|
};
|
|
|
|
// This eliminates a cyclic reference to TLSWrap
|
|
// Ref: https://github.com/nodejs/node/commit/f7620fb96d339f704932f9bb9a0dceb9952df2d4
|
|
function defineHandleReading(socket, handle) {
|
|
ObjectDefineProperty(handle, 'reading', {
|
|
__proto__: null,
|
|
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;
|
|
};
|
|
|
|
function keylogNewListener(event) {
|
|
if (event !== 'keylog')
|
|
return;
|
|
|
|
// Guard against enableKeylogCallback after destroy
|
|
if (!this._handle) return;
|
|
this._handle.enableKeylogCallback();
|
|
|
|
// Remove this listener since it's no longer needed.
|
|
this.removeListener('newListener', keylogNewListener);
|
|
}
|
|
|
|
function newListener(event) {
|
|
if (event !== 'session')
|
|
return;
|
|
|
|
// Guard against enableSessionCallbacks after destroy
|
|
if (!this._handle) return;
|
|
this._handle.enableSessionCallbacks();
|
|
|
|
// Remove this listener since it's no longer needed.
|
|
this.removeListener('newListener', newListener);
|
|
}
|
|
|
|
// 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;
|
|
|
|
if (this.listenerCount('newListener', keylogNewListener) === 0) {
|
|
this.on('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 (options.ALPNCallback) {
|
|
validateFunction(options.ALPNCallback, 'options.ALPNCallback');
|
|
this[kALPNCallback] = options.ALPNCallback;
|
|
ssl.ALPNCallback = callALPNCallback;
|
|
ssl.enableALPNCb();
|
|
}
|
|
|
|
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.
|
|
if (this.listenerCount('newListener', newListener) === 0) {
|
|
this.on('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))) {
|
|
validateFunction(options.SNICallback, 'options.SNICallback');
|
|
this._SNICallback = options.SNICallback;
|
|
ssl.enableCertCb();
|
|
}
|
|
|
|
if (options.ALPNProtocols)
|
|
ssl.setALPNProtocols(options.ALPNProtocols);
|
|
|
|
if (options.pskCallback && ssl.enablePskCallback) {
|
|
validateFunction(options.pskCallback, 'pskCallback');
|
|
|
|
ssl[kOnPskExchange] = options.isServer ?
|
|
onPskServerCallback : onPskClientCallback;
|
|
|
|
this[kPskCallback] = options.pskCallback;
|
|
ssl.enablePskCallback();
|
|
|
|
if (options.pskIdentityHint) {
|
|
validateString(options.pskIdentityHint, 'options.pskIdentityHint');
|
|
ssl.setPskIdentityHint(options.pskIdentityHint);
|
|
}
|
|
}
|
|
|
|
// We can only come here via [kWrapConnectedHandle]() call that happens
|
|
// if the connection is established with `autoSelectFamily` set to `true`.
|
|
const connectOptions = this[kConnectOptions];
|
|
if (!options.isServer && connectOptions) {
|
|
if (connectOptions.servername) {
|
|
this.setServername(connectOptions.servername);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
validateObject(options, 'options');
|
|
if (callback !== undefined) {
|
|
validateFunction(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) {
|
|
validateInt32(size, '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;
|
|
};
|
|
|
|
TLSSocket.prototype.getPeerX509Certificate = function(detailed) {
|
|
const cert = this._handle?.getPeerX509Certificate();
|
|
return cert ? new InternalX509Certificate(cert) : undefined;
|
|
};
|
|
|
|
TLSSocket.prototype.getX509Certificate = function() {
|
|
const cert = this._handle?.getX509Certificate();
|
|
return cert ? new InternalX509Certificate(cert) : undefined;
|
|
};
|
|
|
|
TLSSocket.prototype.setKeyCert = function(context) {
|
|
if (this._handle) {
|
|
let secureContext;
|
|
if (context instanceof common.SecureContext)
|
|
secureContext = context;
|
|
else
|
|
secureContext = tls.createSecureContext(context);
|
|
this._handle.setKeyCert(secureContext.context);
|
|
}
|
|
};
|
|
|
|
// Proxy TLSSocket handle methods
|
|
function makeSocketMethodProxy(name) {
|
|
return function socketMethodProxy(...args) {
|
|
if (this._handle)
|
|
return ReflectApply(this._handle[name], 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 = new 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,
|
|
ALPNCallback: this.ALPNCallback,
|
|
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 = kEmptyObject;
|
|
} else if (options == null || typeof options === 'object') {
|
|
options ??= kEmptyObject;
|
|
} else {
|
|
throw new ERR_INVALID_ARG_TYPE('options', 'Object', options);
|
|
}
|
|
|
|
this._contexts = [];
|
|
this.requestCert = options.requestCert === true;
|
|
this.rejectUnauthorized = options.rejectUnauthorized !== false;
|
|
|
|
this.ALPNCallback = options.ALPNCallback;
|
|
if (this.ALPNCallback && options.ALPNProtocols) {
|
|
throw new ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS();
|
|
}
|
|
|
|
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;
|
|
|
|
validateNumber(this[kHandshakeTimeout], 'options.handshakeTimeout');
|
|
|
|
if (this[kSNICallback]) {
|
|
validateFunction(this[kSNICallback], 'options.SNICallback');
|
|
}
|
|
|
|
if (this[kPskCallback]) {
|
|
validateFunction(this[kPskCallback], 'options.pskCallback');
|
|
}
|
|
|
|
if (this[kPskIdentityHint]) {
|
|
validateString(this[kPskIdentityHint], 'options.pskIdentityHint');
|
|
}
|
|
|
|
// constructor call
|
|
ReflectApply(net.Server, 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) {
|
|
validateObject(options, '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.privateKeyIdentifier = options.privateKeyIdentifier;
|
|
this.privateKeyEngine = options.privateKeyEngine;
|
|
|
|
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,
|
|
privateKeyIdentifier: this.privateKeyIdentifier,
|
|
privateKeyEngine: this.privateKeyEngine,
|
|
});
|
|
};
|
|
|
|
|
|
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) {
|
|
validateBuffer(keys);
|
|
assert(keys.byteLength === 48,
|
|
'Session ticket keys must be a 48-byte buffer');
|
|
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;
|
|
if (options.sigalgs) this.sigalgs = options.sigalgs;
|
|
if (options.privateKeyIdentifier !== undefined)
|
|
this.privateKeyIdentifier = options.privateKeyIdentifier;
|
|
if (options.privateKeyEngine !== undefined)
|
|
this.privateKeyEngine = options.privateKeyEngine;
|
|
}, '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')
|
|
.replaceAll('*', '[^.]*')
|
|
}$`);
|
|
|
|
const secureContext =
|
|
context instanceof common.SecureContext ? context : tls.createSecureContext(context);
|
|
this._contexts.push([re, secureContext.context]);
|
|
};
|
|
|
|
Server.prototype[EE.captureRejectionSymbol] = function(
|
|
err, event, sock) {
|
|
|
|
switch (event) {
|
|
case 'secureConnection':
|
|
sock.destroy(err);
|
|
break;
|
|
default:
|
|
ReflectApply(net.Server.prototype[SymbolFor('nodejs.rejection')], this,
|
|
[err, event, sock]);
|
|
}
|
|
};
|
|
|
|
function SNICallback(servername, callback) {
|
|
const contexts = this.server._contexts;
|
|
|
|
for (let i = contexts.length - 1; i >= 0; --i) {
|
|
const elem = contexts[i];
|
|
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?._host) ||
|
|
'localhost';
|
|
const cert = this.getPeerCertificate(true);
|
|
verifyError = options.checkServerIdentity(hostname, cert);
|
|
}
|
|
|
|
if (verifyError) {
|
|
this.authorized = false;
|
|
this.authorizationError = verifyError.code || verifyError.message;
|
|
|
|
// rejectUnauthorized property can be explicitly defined as `undefined`
|
|
// causing the assignment to default value (`true`) fail. Before assigning
|
|
// it to the tlssock connection options, explicitly check if it is false
|
|
// and update rejectUnauthorized property. The property gets used by
|
|
// TLSSocket connection handler to allow or reject connection if
|
|
// unauthorized.
|
|
// This check is potentially redundant, however it is better to keep it
|
|
// in case the option object gets modified somewhere.
|
|
if (options.rejectUnauthorized !== false) {
|
|
this.destroy(verifyError);
|
|
return;
|
|
}
|
|
debug('client emit secureConnect. rejectUnauthorized: %s, ' +
|
|
'authorizationError: %s', options.rejectUnauthorized,
|
|
this.authorizationError);
|
|
} 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 = new 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;
|
|
|
|
validateFunction(options.checkServerIdentity, 'options.checkServerIdentity');
|
|
validateNumber(options.minDHSize, 'options.minDHSize', 1);
|
|
|
|
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,
|
|
onread: options.onread,
|
|
signal: options.signal,
|
|
});
|
|
|
|
// rejectUnauthorized property can be explicitly defined as `undefined`
|
|
// causing the assignment to default value (`true`) fail. Before assigning
|
|
// it to the tlssock connection options, explicitly check if it is false
|
|
// and update rejectUnauthorized property. The property gets used by TLSSocket
|
|
// connection handler to allow or reject connection if unauthorized
|
|
options.rejectUnauthorized = options.rejectUnauthorized !== false;
|
|
|
|
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;
|
|
};
|