You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							394 lines
						
					
					
						
							16 KiB
						
					
					
				
			
		
		
	
	
							394 lines
						
					
					
						
							16 KiB
						
					
					
				| "use strict";
 | |
| Object.defineProperty(exports, "__esModule", { value: true });
 | |
| exports.LEGAL_TCP_SOCKET_OPTIONS = exports.LEGAL_TLS_SOCKET_OPTIONS = exports.prepareHandshakeDocument = exports.connect = void 0;
 | |
| const net = require("net");
 | |
| const socks_1 = require("socks");
 | |
| const tls = require("tls");
 | |
| const bson_1 = require("../bson");
 | |
| const constants_1 = require("../constants");
 | |
| const error_1 = require("../error");
 | |
| const utils_1 = require("../utils");
 | |
| const auth_provider_1 = require("./auth/auth_provider");
 | |
| const gssapi_1 = require("./auth/gssapi");
 | |
| const mongocr_1 = require("./auth/mongocr");
 | |
| const mongodb_aws_1 = require("./auth/mongodb_aws");
 | |
| const plain_1 = require("./auth/plain");
 | |
| const providers_1 = require("./auth/providers");
 | |
| const scram_1 = require("./auth/scram");
 | |
| const x509_1 = require("./auth/x509");
 | |
| const connection_1 = require("./connection");
 | |
| const constants_2 = require("./wire_protocol/constants");
 | |
| const AUTH_PROVIDERS = new Map([
 | |
|     [providers_1.AuthMechanism.MONGODB_AWS, new mongodb_aws_1.MongoDBAWS()],
 | |
|     [providers_1.AuthMechanism.MONGODB_CR, new mongocr_1.MongoCR()],
 | |
|     [providers_1.AuthMechanism.MONGODB_GSSAPI, new gssapi_1.GSSAPI()],
 | |
|     [providers_1.AuthMechanism.MONGODB_PLAIN, new plain_1.Plain()],
 | |
|     [providers_1.AuthMechanism.MONGODB_SCRAM_SHA1, new scram_1.ScramSHA1()],
 | |
|     [providers_1.AuthMechanism.MONGODB_SCRAM_SHA256, new scram_1.ScramSHA256()],
 | |
|     [providers_1.AuthMechanism.MONGODB_X509, new x509_1.X509()]
 | |
| ]);
 | |
| function connect(options, callback) {
 | |
|     makeConnection({ ...options, existingSocket: undefined }, (err, socket) => {
 | |
|         if (err || !socket) {
 | |
|             return callback(err);
 | |
|         }
 | |
|         let ConnectionType = options.connectionType ?? connection_1.Connection;
 | |
|         if (options.autoEncrypter) {
 | |
|             ConnectionType = connection_1.CryptoConnection;
 | |
|         }
 | |
|         performInitialHandshake(new ConnectionType(socket, options), options, callback);
 | |
|     });
 | |
| }
 | |
| exports.connect = connect;
 | |
| function checkSupportedServer(hello, options) {
 | |
|     const serverVersionHighEnough = hello &&
 | |
|         (typeof hello.maxWireVersion === 'number' || hello.maxWireVersion instanceof bson_1.Int32) &&
 | |
|         hello.maxWireVersion >= constants_2.MIN_SUPPORTED_WIRE_VERSION;
 | |
|     const serverVersionLowEnough = hello &&
 | |
|         (typeof hello.minWireVersion === 'number' || hello.minWireVersion instanceof bson_1.Int32) &&
 | |
|         hello.minWireVersion <= constants_2.MAX_SUPPORTED_WIRE_VERSION;
 | |
|     if (serverVersionHighEnough) {
 | |
|         if (serverVersionLowEnough) {
 | |
|             return null;
 | |
|         }
 | |
|         const message = `Server at ${options.hostAddress} reports minimum wire version ${JSON.stringify(hello.minWireVersion)}, but this version of the Node.js Driver requires at most ${constants_2.MAX_SUPPORTED_WIRE_VERSION} (MongoDB ${constants_2.MAX_SUPPORTED_SERVER_VERSION})`;
 | |
|         return new error_1.MongoCompatibilityError(message);
 | |
|     }
 | |
|     const message = `Server at ${options.hostAddress} reports maximum wire version ${JSON.stringify(hello.maxWireVersion) ?? 0}, but this version of the Node.js Driver requires at least ${constants_2.MIN_SUPPORTED_WIRE_VERSION} (MongoDB ${constants_2.MIN_SUPPORTED_SERVER_VERSION})`;
 | |
|     return new error_1.MongoCompatibilityError(message);
 | |
| }
 | |
| function performInitialHandshake(conn, options, _callback) {
 | |
|     const callback = function (err, ret) {
 | |
|         if (err && conn) {
 | |
|             conn.destroy();
 | |
|         }
 | |
|         _callback(err, ret);
 | |
|     };
 | |
|     const credentials = options.credentials;
 | |
|     if (credentials) {
 | |
|         if (!(credentials.mechanism === providers_1.AuthMechanism.MONGODB_DEFAULT) &&
 | |
|             !AUTH_PROVIDERS.get(credentials.mechanism)) {
 | |
|             callback(new error_1.MongoInvalidArgumentError(`AuthMechanism '${credentials.mechanism}' not supported`));
 | |
|             return;
 | |
|         }
 | |
|     }
 | |
|     const authContext = new auth_provider_1.AuthContext(conn, credentials, options);
 | |
|     prepareHandshakeDocument(authContext, (err, handshakeDoc) => {
 | |
|         if (err || !handshakeDoc) {
 | |
|             return callback(err);
 | |
|         }
 | |
|         const handshakeOptions = Object.assign({}, options);
 | |
|         if (typeof options.connectTimeoutMS === 'number') {
 | |
|             // The handshake technically is a monitoring check, so its socket timeout should be connectTimeoutMS
 | |
|             handshakeOptions.socketTimeoutMS = options.connectTimeoutMS;
 | |
|         }
 | |
|         const start = new Date().getTime();
 | |
|         conn.command((0, utils_1.ns)('admin.$cmd'), handshakeDoc, handshakeOptions, (err, response) => {
 | |
|             if (err) {
 | |
|                 callback(err);
 | |
|                 return;
 | |
|             }
 | |
|             if (response?.ok === 0) {
 | |
|                 callback(new error_1.MongoServerError(response));
 | |
|                 return;
 | |
|             }
 | |
|             if (!('isWritablePrimary' in response)) {
 | |
|                 // Provide hello-style response document.
 | |
|                 response.isWritablePrimary = response[constants_1.LEGACY_HELLO_COMMAND];
 | |
|             }
 | |
|             if (response.helloOk) {
 | |
|                 conn.helloOk = true;
 | |
|             }
 | |
|             const supportedServerErr = checkSupportedServer(response, options);
 | |
|             if (supportedServerErr) {
 | |
|                 callback(supportedServerErr);
 | |
|                 return;
 | |
|             }
 | |
|             if (options.loadBalanced) {
 | |
|                 if (!response.serviceId) {
 | |
|                     return callback(new error_1.MongoCompatibilityError('Driver attempted to initialize in load balancing mode, ' +
 | |
|                         'but the server does not support this mode.'));
 | |
|                 }
 | |
|             }
 | |
|             // NOTE: This is metadata attached to the connection while porting away from
 | |
|             //       handshake being done in the `Server` class. Likely, it should be
 | |
|             //       relocated, or at very least restructured.
 | |
|             conn.hello = response;
 | |
|             conn.lastHelloMS = new Date().getTime() - start;
 | |
|             if (!response.arbiterOnly && credentials) {
 | |
|                 // store the response on auth context
 | |
|                 authContext.response = response;
 | |
|                 const resolvedCredentials = credentials.resolveAuthMechanism(response);
 | |
|                 const provider = AUTH_PROVIDERS.get(resolvedCredentials.mechanism);
 | |
|                 if (!provider) {
 | |
|                     return callback(new error_1.MongoInvalidArgumentError(`No AuthProvider for ${resolvedCredentials.mechanism} defined.`));
 | |
|                 }
 | |
|                 provider.auth(authContext, err => {
 | |
|                     if (err) {
 | |
|                         if (err instanceof error_1.MongoError) {
 | |
|                             err.addErrorLabel(error_1.MongoErrorLabel.HandshakeError);
 | |
|                             if ((0, error_1.needsRetryableWriteLabel)(err, response.maxWireVersion)) {
 | |
|                                 err.addErrorLabel(error_1.MongoErrorLabel.RetryableWriteError);
 | |
|                             }
 | |
|                         }
 | |
|                         return callback(err);
 | |
|                     }
 | |
|                     callback(undefined, conn);
 | |
|                 });
 | |
|                 return;
 | |
|             }
 | |
|             callback(undefined, conn);
 | |
|         });
 | |
|     });
 | |
| }
 | |
| /**
 | |
|  * @internal
 | |
|  *
 | |
|  * This function is only exposed for testing purposes.
 | |
|  */
 | |
| function prepareHandshakeDocument(authContext, callback) {
 | |
|     const options = authContext.options;
 | |
|     const compressors = options.compressors ? options.compressors : [];
 | |
|     const { serverApi } = authContext.connection;
 | |
|     const handshakeDoc = {
 | |
|         [serverApi?.version ? 'hello' : constants_1.LEGACY_HELLO_COMMAND]: 1,
 | |
|         helloOk: true,
 | |
|         client: options.metadata || (0, utils_1.makeClientMetadata)(options),
 | |
|         compression: compressors
 | |
|     };
 | |
|     if (options.loadBalanced === true) {
 | |
|         handshakeDoc.loadBalanced = true;
 | |
|     }
 | |
|     const credentials = authContext.credentials;
 | |
|     if (credentials) {
 | |
|         if (credentials.mechanism === providers_1.AuthMechanism.MONGODB_DEFAULT && credentials.username) {
 | |
|             handshakeDoc.saslSupportedMechs = `${credentials.source}.${credentials.username}`;
 | |
|             const provider = AUTH_PROVIDERS.get(providers_1.AuthMechanism.MONGODB_SCRAM_SHA256);
 | |
|             if (!provider) {
 | |
|                 // This auth mechanism is always present.
 | |
|                 return callback(new error_1.MongoInvalidArgumentError(`No AuthProvider for ${providers_1.AuthMechanism.MONGODB_SCRAM_SHA256} defined.`));
 | |
|             }
 | |
|             return provider.prepare(handshakeDoc, authContext, callback);
 | |
|         }
 | |
|         const provider = AUTH_PROVIDERS.get(credentials.mechanism);
 | |
|         if (!provider) {
 | |
|             return callback(new error_1.MongoInvalidArgumentError(`No AuthProvider for ${credentials.mechanism} defined.`));
 | |
|         }
 | |
|         return provider.prepare(handshakeDoc, authContext, callback);
 | |
|     }
 | |
|     callback(undefined, handshakeDoc);
 | |
| }
 | |
| exports.prepareHandshakeDocument = prepareHandshakeDocument;
 | |
| /** @public */
 | |
| exports.LEGAL_TLS_SOCKET_OPTIONS = [
 | |
|     'ALPNProtocols',
 | |
|     'ca',
 | |
|     'cert',
 | |
|     'checkServerIdentity',
 | |
|     'ciphers',
 | |
|     'crl',
 | |
|     'ecdhCurve',
 | |
|     'key',
 | |
|     'minDHSize',
 | |
|     'passphrase',
 | |
|     'pfx',
 | |
|     'rejectUnauthorized',
 | |
|     'secureContext',
 | |
|     'secureProtocol',
 | |
|     'servername',
 | |
|     'session'
 | |
| ];
 | |
| /** @public */
 | |
| exports.LEGAL_TCP_SOCKET_OPTIONS = [
 | |
|     'family',
 | |
|     'hints',
 | |
|     'localAddress',
 | |
|     'localPort',
 | |
|     'lookup'
 | |
| ];
 | |
| function parseConnectOptions(options) {
 | |
|     const hostAddress = options.hostAddress;
 | |
|     if (!hostAddress)
 | |
|         throw new error_1.MongoInvalidArgumentError('Option "hostAddress" is required');
 | |
|     const result = {};
 | |
|     for (const name of exports.LEGAL_TCP_SOCKET_OPTIONS) {
 | |
|         if (options[name] != null) {
 | |
|             result[name] = options[name];
 | |
|         }
 | |
|     }
 | |
|     if (typeof hostAddress.socketPath === 'string') {
 | |
|         result.path = hostAddress.socketPath;
 | |
|         return result;
 | |
|     }
 | |
|     else if (typeof hostAddress.host === 'string') {
 | |
|         result.host = hostAddress.host;
 | |
|         result.port = hostAddress.port;
 | |
|         return result;
 | |
|     }
 | |
|     else {
 | |
|         // This should never happen since we set up HostAddresses
 | |
|         // But if we don't throw here the socket could hang until timeout
 | |
|         // TODO(NODE-3483)
 | |
|         throw new error_1.MongoRuntimeError(`Unexpected HostAddress ${JSON.stringify(hostAddress)}`);
 | |
|     }
 | |
| }
 | |
| function parseSslOptions(options) {
 | |
|     const result = parseConnectOptions(options);
 | |
|     // Merge in valid SSL options
 | |
|     for (const name of exports.LEGAL_TLS_SOCKET_OPTIONS) {
 | |
|         if (options[name] != null) {
 | |
|             result[name] = options[name];
 | |
|         }
 | |
|     }
 | |
|     if (options.existingSocket) {
 | |
|         result.socket = options.existingSocket;
 | |
|     }
 | |
|     // Set default sni servername to be the same as host
 | |
|     if (result.servername == null && result.host && !net.isIP(result.host)) {
 | |
|         result.servername = result.host;
 | |
|     }
 | |
|     return result;
 | |
| }
 | |
| const SOCKET_ERROR_EVENT_LIST = ['error', 'close', 'timeout', 'parseError'];
 | |
| const SOCKET_ERROR_EVENTS = new Set(SOCKET_ERROR_EVENT_LIST);
 | |
| function makeConnection(options, _callback) {
 | |
|     const useTLS = options.tls ?? false;
 | |
|     const keepAlive = options.keepAlive ?? true;
 | |
|     const socketTimeoutMS = options.socketTimeoutMS ?? Reflect.get(options, 'socketTimeout') ?? 0;
 | |
|     const noDelay = options.noDelay ?? true;
 | |
|     const connectTimeoutMS = options.connectTimeoutMS ?? 30000;
 | |
|     const rejectUnauthorized = options.rejectUnauthorized ?? true;
 | |
|     const keepAliveInitialDelay = ((options.keepAliveInitialDelay ?? 120000) > socketTimeoutMS
 | |
|         ? Math.round(socketTimeoutMS / 2)
 | |
|         : options.keepAliveInitialDelay) ?? 120000;
 | |
|     const existingSocket = options.existingSocket;
 | |
|     let socket;
 | |
|     const callback = function (err, ret) {
 | |
|         if (err && socket) {
 | |
|             socket.destroy();
 | |
|         }
 | |
|         _callback(err, ret);
 | |
|     };
 | |
|     if (options.proxyHost != null) {
 | |
|         // Currently, only Socks5 is supported.
 | |
|         return makeSocks5Connection({
 | |
|             ...options,
 | |
|             connectTimeoutMS // Should always be present for Socks5
 | |
|         }, callback);
 | |
|     }
 | |
|     if (useTLS) {
 | |
|         const tlsSocket = tls.connect(parseSslOptions(options));
 | |
|         if (typeof tlsSocket.disableRenegotiation === 'function') {
 | |
|             tlsSocket.disableRenegotiation();
 | |
|         }
 | |
|         socket = tlsSocket;
 | |
|     }
 | |
|     else if (existingSocket) {
 | |
|         // In the TLS case, parseSslOptions() sets options.socket to existingSocket,
 | |
|         // so we only need to handle the non-TLS case here (where existingSocket
 | |
|         // gives us all we need out of the box).
 | |
|         socket = existingSocket;
 | |
|     }
 | |
|     else {
 | |
|         socket = net.createConnection(parseConnectOptions(options));
 | |
|     }
 | |
|     socket.setKeepAlive(keepAlive, keepAliveInitialDelay);
 | |
|     socket.setTimeout(connectTimeoutMS);
 | |
|     socket.setNoDelay(noDelay);
 | |
|     const connectEvent = useTLS ? 'secureConnect' : 'connect';
 | |
|     let cancellationHandler;
 | |
|     function errorHandler(eventName) {
 | |
|         return (err) => {
 | |
|             SOCKET_ERROR_EVENTS.forEach(event => socket.removeAllListeners(event));
 | |
|             if (cancellationHandler && options.cancellationToken) {
 | |
|                 options.cancellationToken.removeListener('cancel', cancellationHandler);
 | |
|             }
 | |
|             socket.removeListener(connectEvent, connectHandler);
 | |
|             callback(connectionFailureError(eventName, err));
 | |
|         };
 | |
|     }
 | |
|     function connectHandler() {
 | |
|         SOCKET_ERROR_EVENTS.forEach(event => socket.removeAllListeners(event));
 | |
|         if (cancellationHandler && options.cancellationToken) {
 | |
|             options.cancellationToken.removeListener('cancel', cancellationHandler);
 | |
|         }
 | |
|         if ('authorizationError' in socket) {
 | |
|             if (socket.authorizationError && rejectUnauthorized) {
 | |
|                 return callback(socket.authorizationError);
 | |
|             }
 | |
|         }
 | |
|         socket.setTimeout(socketTimeoutMS);
 | |
|         callback(undefined, socket);
 | |
|     }
 | |
|     SOCKET_ERROR_EVENTS.forEach(event => socket.once(event, errorHandler(event)));
 | |
|     if (options.cancellationToken) {
 | |
|         cancellationHandler = errorHandler('cancel');
 | |
|         options.cancellationToken.once('cancel', cancellationHandler);
 | |
|     }
 | |
|     if (existingSocket) {
 | |
|         process.nextTick(connectHandler);
 | |
|     }
 | |
|     else {
 | |
|         socket.once(connectEvent, connectHandler);
 | |
|     }
 | |
| }
 | |
| function makeSocks5Connection(options, callback) {
 | |
|     const hostAddress = utils_1.HostAddress.fromHostPort(options.proxyHost ?? '', // proxyHost is guaranteed to set here
 | |
|     options.proxyPort ?? 1080);
 | |
|     // First, connect to the proxy server itself:
 | |
|     makeConnection({
 | |
|         ...options,
 | |
|         hostAddress,
 | |
|         tls: false,
 | |
|         proxyHost: undefined
 | |
|     }, (err, rawSocket) => {
 | |
|         if (err) {
 | |
|             return callback(err);
 | |
|         }
 | |
|         const destination = parseConnectOptions(options);
 | |
|         if (typeof destination.host !== 'string' || typeof destination.port !== 'number') {
 | |
|             return callback(new error_1.MongoInvalidArgumentError('Can only make Socks5 connections to TCP hosts'));
 | |
|         }
 | |
|         // Then, establish the Socks5 proxy connection:
 | |
|         socks_1.SocksClient.createConnection({
 | |
|             existing_socket: rawSocket,
 | |
|             timeout: options.connectTimeoutMS,
 | |
|             command: 'connect',
 | |
|             destination: {
 | |
|                 host: destination.host,
 | |
|                 port: destination.port
 | |
|             },
 | |
|             proxy: {
 | |
|                 // host and port are ignored because we pass existing_socket
 | |
|                 host: 'iLoveJavaScript',
 | |
|                 port: 0,
 | |
|                 type: 5,
 | |
|                 userId: options.proxyUsername || undefined,
 | |
|                 password: options.proxyPassword || undefined
 | |
|             }
 | |
|         }).then(({ socket }) => {
 | |
|             // Finally, now treat the resulting duplex stream as the
 | |
|             // socket over which we send and receive wire protocol messages:
 | |
|             makeConnection({
 | |
|                 ...options,
 | |
|                 existingSocket: socket,
 | |
|                 proxyHost: undefined
 | |
|             }, callback);
 | |
|         }, error => callback(connectionFailureError('error', error)));
 | |
|     });
 | |
| }
 | |
| function connectionFailureError(type, err) {
 | |
|     switch (type) {
 | |
|         case 'error':
 | |
|             return new error_1.MongoNetworkError(err);
 | |
|         case 'timeout':
 | |
|             return new error_1.MongoNetworkTimeoutError('connection timed out');
 | |
|         case 'close':
 | |
|             return new error_1.MongoNetworkError('connection closed');
 | |
|         case 'cancel':
 | |
|             return new error_1.MongoNetworkError('connection establishment was cancelled');
 | |
|         default:
 | |
|             return new error_1.MongoNetworkError('unknown network error');
 | |
|     }
 | |
| }
 | |
| //# sourceMappingURL=connect.js.map
 |