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
 |