123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357 |
- 'use strict';
- const net = require('net');
- const tls = require('tls');
- const Connection = require('./connection');
- const Query = require('./commands').Query;
- const createClientInfo = require('../topologies/shared').createClientInfo;
- const MongoError = require('../error').MongoError;
- const defaultAuthProviders = require('../auth/defaultAuthProviders').defaultAuthProviders;
- const WIRE_CONSTANTS = require('../wireprotocol/constants');
- const MAX_SUPPORTED_WIRE_VERSION = WIRE_CONSTANTS.MAX_SUPPORTED_WIRE_VERSION;
- const MAX_SUPPORTED_SERVER_VERSION = WIRE_CONSTANTS.MAX_SUPPORTED_SERVER_VERSION;
- const MIN_SUPPORTED_WIRE_VERSION = WIRE_CONSTANTS.MIN_SUPPORTED_WIRE_VERSION;
- const MIN_SUPPORTED_SERVER_VERSION = WIRE_CONSTANTS.MIN_SUPPORTED_SERVER_VERSION;
- let AUTH_PROVIDERS;
- function connect(options, callback) {
- if (AUTH_PROVIDERS == null) {
- AUTH_PROVIDERS = defaultAuthProviders(options.bson);
- }
- if (options.family !== void 0) {
- makeConnection(options.family, options, (err, socket) => {
- if (err) {
- callback(err, socket); // in the error case, `socket` is the originating error event name
- return;
- }
- performInitialHandshake(new Connection(socket, options), options, callback);
- });
- return;
- }
- return makeConnection(6, options, (err, ipv6Socket) => {
- if (err) {
- makeConnection(4, options, (err, ipv4Socket) => {
- if (err) {
- callback(err, ipv4Socket); // in the error case, `ipv4Socket` is the originating error event name
- return;
- }
- performInitialHandshake(new Connection(ipv4Socket, options), options, callback);
- });
- return;
- }
- performInitialHandshake(new Connection(ipv6Socket, options), options, callback);
- });
- }
- function getSaslSupportedMechs(options) {
- if (!(options && options.credentials)) {
- return {};
- }
- const credentials = options.credentials;
- // TODO: revisit whether or not items like `options.user` and `options.dbName` should be checked here
- const authMechanism = credentials.mechanism;
- const authSource = credentials.source || options.dbName || 'admin';
- const user = credentials.username || options.user;
- if (typeof authMechanism === 'string' && authMechanism.toUpperCase() !== 'DEFAULT') {
- return {};
- }
- if (!user) {
- return {};
- }
- return { saslSupportedMechs: `${authSource}.${user}` };
- }
- function checkSupportedServer(ismaster, options) {
- const serverVersionHighEnough =
- ismaster &&
- typeof ismaster.maxWireVersion === 'number' &&
- ismaster.maxWireVersion >= MIN_SUPPORTED_WIRE_VERSION;
- const serverVersionLowEnough =
- ismaster &&
- typeof ismaster.minWireVersion === 'number' &&
- ismaster.minWireVersion <= MAX_SUPPORTED_WIRE_VERSION;
- if (serverVersionHighEnough) {
- if (serverVersionLowEnough) {
- return null;
- }
- const message = `Server at ${options.host}:${options.port} reports minimum wire version ${
- ismaster.minWireVersion
- }, but this version of the Node.js Driver requires at most ${MAX_SUPPORTED_WIRE_VERSION} (MongoDB ${MAX_SUPPORTED_SERVER_VERSION})`;
- return new MongoError(message);
- }
- const message = `Server at ${options.host}:${
- options.port
- } reports maximum wire version ${ismaster.maxWireVersion ||
- 0}, but this version of the Node.js Driver requires at least ${MIN_SUPPORTED_WIRE_VERSION} (MongoDB ${MIN_SUPPORTED_SERVER_VERSION})`;
- return new MongoError(message);
- }
- function performInitialHandshake(conn, options, _callback) {
- const callback = function(err, ret) {
- if (err && conn) {
- conn.destroy();
- }
- _callback(err, ret);
- };
- let compressors = [];
- if (options.compression && options.compression.compressors) {
- compressors = options.compression.compressors;
- }
- const handshakeDoc = Object.assign(
- {
- ismaster: true,
- client: createClientInfo(options),
- compression: compressors
- },
- getSaslSupportedMechs(options)
- );
- const start = new Date().getTime();
- runCommand(conn, 'admin.$cmd', handshakeDoc, options, (err, ismaster) => {
- if (err) {
- callback(err, null);
- return;
- }
- if (ismaster.ok === 0) {
- callback(new MongoError(ismaster), null);
- return;
- }
- const supportedServerErr = checkSupportedServer(ismaster, options);
- if (supportedServerErr) {
- callback(supportedServerErr, null);
- return;
- }
- // resolve compression
- if (ismaster.compression) {
- const agreedCompressors = compressors.filter(
- compressor => ismaster.compression.indexOf(compressor) !== -1
- );
- if (agreedCompressors.length) {
- conn.agreedCompressor = agreedCompressors[0];
- }
- if (options.compression && options.compression.zlibCompressionLevel) {
- conn.zlibCompressionLevel = options.compression.zlibCompressionLevel;
- }
- }
- // 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.ismaster = ismaster;
- conn.lastIsMasterMS = new Date().getTime() - start;
- const credentials = options.credentials;
- if (!ismaster.arbiterOnly && credentials) {
- credentials.resolveAuthMechanism(ismaster);
- authenticate(conn, credentials, callback);
- return;
- }
- callback(null, conn);
- });
- }
- const LEGAL_SSL_SOCKET_OPTIONS = [
- 'pfx',
- 'key',
- 'passphrase',
- 'cert',
- 'ca',
- 'ciphers',
- 'NPNProtocols',
- 'ALPNProtocols',
- 'servername',
- 'ecdhCurve',
- 'secureProtocol',
- 'secureContext',
- 'session',
- 'minDHSize',
- 'crl',
- 'rejectUnauthorized'
- ];
- function parseConnectOptions(family, options) {
- const host = typeof options.host === 'string' ? options.host : 'localhost';
- if (host.indexOf('/') !== -1) {
- return { path: host };
- }
- const result = {
- family,
- host,
- port: typeof options.port === 'number' ? options.port : 27017,
- rejectUnauthorized: false
- };
- return result;
- }
- function parseSslOptions(family, options) {
- const result = parseConnectOptions(family, options);
- // Merge in valid SSL options
- for (const name in options) {
- if (options[name] != null && LEGAL_SSL_SOCKET_OPTIONS.indexOf(name) !== -1) {
- result[name] = options[name];
- }
- }
- // Override checkServerIdentity behavior
- if (options.checkServerIdentity === false) {
- // Skip the identiy check by retuning undefined as per node documents
- // https://nodejs.org/api/tls.html#tls_tls_connect_options_callback
- result.checkServerIdentity = function() {
- return undefined;
- };
- } else if (typeof options.checkServerIdentity === 'function') {
- result.checkServerIdentity = options.checkServerIdentity;
- }
- // Set default sni servername to be the same as host
- if (result.servername == null) {
- result.servername = result.host;
- }
- return result;
- }
- function makeConnection(family, options, _callback) {
- const useSsl = typeof options.ssl === 'boolean' ? options.ssl : false;
- const keepAlive = typeof options.keepAlive === 'boolean' ? options.keepAlive : true;
- let keepAliveInitialDelay =
- typeof options.keepAliveInitialDelay === 'number' ? options.keepAliveInitialDelay : 300000;
- const noDelay = typeof options.noDelay === 'boolean' ? options.noDelay : true;
- const connectionTimeout =
- typeof options.connectionTimeout === 'number' ? options.connectionTimeout : 30000;
- const socketTimeout = typeof options.socketTimeout === 'number' ? options.socketTimeout : 360000;
- const rejectUnauthorized =
- typeof options.rejectUnauthorized === 'boolean' ? options.rejectUnauthorized : true;
- if (keepAliveInitialDelay > socketTimeout) {
- keepAliveInitialDelay = Math.round(socketTimeout / 2);
- }
- let socket;
- const callback = function(err, ret) {
- if (err && socket) {
- socket.destroy();
- }
- _callback(err, ret);
- };
- try {
- if (useSsl) {
- socket = tls.connect(parseSslOptions(family, options));
- if (typeof socket.disableRenegotiation === 'function') {
- socket.disableRenegotiation();
- }
- } else {
- socket = net.createConnection(parseConnectOptions(family, options));
- }
- } catch (err) {
- return callback(err);
- }
- socket.setKeepAlive(keepAlive, keepAliveInitialDelay);
- socket.setTimeout(connectionTimeout);
- socket.setNoDelay(noDelay);
- const errorEvents = ['error', 'close', 'timeout', 'parseError', 'connect'];
- function errorHandler(eventName) {
- return err => {
- if (err == null || err === false) err = true;
- errorEvents.forEach(event => socket.removeAllListeners(event));
- socket.removeListener('connect', connectHandler);
- callback(err, eventName);
- };
- }
- function connectHandler() {
- errorEvents.forEach(event => socket.removeAllListeners(event));
- if (socket.authorizationError && rejectUnauthorized) {
- return callback(socket.authorizationError);
- }
- socket.setTimeout(socketTimeout);
- callback(null, socket);
- }
- socket.once('error', errorHandler('error'));
- socket.once('close', errorHandler('close'));
- socket.once('timeout', errorHandler('timeout'));
- socket.once('parseError', errorHandler('parseError'));
- socket.once('connect', connectHandler);
- }
- const CONNECTION_ERROR_EVENTS = ['error', 'close', 'timeout', 'parseError'];
- function runCommand(conn, ns, command, options, callback) {
- if (typeof options === 'function') (callback = options), (options = {});
- const socketTimeout = typeof options.socketTimeout === 'number' ? options.socketTimeout : 360000;
- const bson = conn.options.bson;
- const query = new Query(bson, ns, command, {
- numberToSkip: 0,
- numberToReturn: 1
- });
- function errorHandler(err) {
- conn.resetSocketTimeout();
- CONNECTION_ERROR_EVENTS.forEach(eventName => conn.removeListener(eventName, errorHandler));
- conn.removeListener('message', messageHandler);
- callback(err, null);
- }
- function messageHandler(msg) {
- if (msg.responseTo !== query.requestId) {
- return;
- }
- conn.resetSocketTimeout();
- CONNECTION_ERROR_EVENTS.forEach(eventName => conn.removeListener(eventName, errorHandler));
- conn.removeListener('message', messageHandler);
- msg.parse({ promoteValues: true });
- callback(null, msg.documents[0]);
- }
- conn.setSocketTimeout(socketTimeout);
- CONNECTION_ERROR_EVENTS.forEach(eventName => conn.once(eventName, errorHandler));
- conn.on('message', messageHandler);
- conn.write(query.toBin());
- }
- function authenticate(conn, credentials, callback) {
- const mechanism = credentials.mechanism;
- if (!AUTH_PROVIDERS[mechanism]) {
- callback(new MongoError(`authMechanism '${mechanism}' not supported`));
- return;
- }
- const provider = AUTH_PROVIDERS[mechanism];
- provider.auth(runCommand, [conn], credentials, err => {
- if (err) return callback(err);
- callback(null, conn);
- });
- }
- module.exports = connect;
|