From e06ec920f7a5d784e674c4c4b4e6d1da3dc7391d Mon Sep 17 00:00:00 2001 From: Piotr Russ Date: Mon, 16 Nov 2020 00:10:28 +0100 Subject: api, login, auth --- node_modules/mongodb/lib/operations/connect.js | 806 +++++++++++++++++++++++++ 1 file changed, 806 insertions(+) create mode 100644 node_modules/mongodb/lib/operations/connect.js (limited to 'node_modules/mongodb/lib/operations/connect.js') diff --git a/node_modules/mongodb/lib/operations/connect.js b/node_modules/mongodb/lib/operations/connect.js new file mode 100644 index 0000000..2c3e8bd --- /dev/null +++ b/node_modules/mongodb/lib/operations/connect.js @@ -0,0 +1,806 @@ +'use strict'; + +const deprecate = require('util').deprecate; +const Logger = require('../core').Logger; +const MongoCredentials = require('../core').MongoCredentials; +const MongoError = require('../core').MongoError; +const Mongos = require('../topologies/mongos'); +const NativeTopology = require('../topologies/native_topology'); +const parse = require('../core').parseConnectionString; +const ReadConcern = require('../read_concern'); +const ReadPreference = require('../core').ReadPreference; +const ReplSet = require('../topologies/replset'); +const Server = require('../topologies/server'); +const ServerSessionPool = require('../core').Sessions.ServerSessionPool; +const emitDeprecationWarning = require('../utils').emitDeprecationWarning; +const fs = require('fs'); +const BSON = require('../core/connection/utils').retrieveBSON(); +const CMAP_EVENT_NAMES = require('../cmap/events').CMAP_EVENT_NAMES; + +let client; +function loadClient() { + if (!client) { + client = require('../mongo_client'); + } + return client; +} + +const legacyParse = deprecate( + require('../url_parser'), + 'current URL string parser is deprecated, and will be removed in a future version. ' + + 'To use the new parser, pass option { useNewUrlParser: true } to MongoClient.connect.' +); + +const AUTH_MECHANISM_INTERNAL_MAP = { + DEFAULT: 'default', + PLAIN: 'plain', + GSSAPI: 'gssapi', + 'MONGODB-CR': 'mongocr', + 'MONGODB-X509': 'x509', + 'MONGODB-AWS': 'mongodb-aws', + 'SCRAM-SHA-1': 'scram-sha-1', + 'SCRAM-SHA-256': 'scram-sha-256' +}; + +const monitoringEvents = [ + 'timeout', + 'close', + 'serverOpening', + 'serverDescriptionChanged', + 'serverHeartbeatStarted', + 'serverHeartbeatSucceeded', + 'serverHeartbeatFailed', + 'serverClosed', + 'topologyOpening', + 'topologyClosed', + 'topologyDescriptionChanged', + 'commandStarted', + 'commandSucceeded', + 'commandFailed', + 'joined', + 'left', + 'ping', + 'ha', + 'all', + 'fullsetup', + 'open' +]; + +const VALID_AUTH_MECHANISMS = new Set([ + 'DEFAULT', + 'PLAIN', + 'GSSAPI', + 'MONGODB-CR', + 'MONGODB-X509', + 'MONGODB-AWS', + 'SCRAM-SHA-1', + 'SCRAM-SHA-256' +]); + +const validOptionNames = [ + 'poolSize', + 'ssl', + 'sslValidate', + 'sslCA', + 'sslCert', + 'sslKey', + 'sslPass', + 'sslCRL', + 'autoReconnect', + 'noDelay', + 'keepAlive', + 'keepAliveInitialDelay', + 'connectTimeoutMS', + 'family', + 'socketTimeoutMS', + 'reconnectTries', + 'reconnectInterval', + 'ha', + 'haInterval', + 'replicaSet', + 'secondaryAcceptableLatencyMS', + 'acceptableLatencyMS', + 'connectWithNoPrimary', + 'authSource', + 'w', + 'wtimeout', + 'j', + 'forceServerObjectId', + 'serializeFunctions', + 'ignoreUndefined', + 'raw', + 'bufferMaxEntries', + 'readPreference', + 'pkFactory', + 'promiseLibrary', + 'readConcern', + 'maxStalenessSeconds', + 'loggerLevel', + 'logger', + 'promoteValues', + 'promoteBuffers', + 'promoteLongs', + 'domainsEnabled', + 'checkServerIdentity', + 'validateOptions', + 'appname', + 'auth', + 'user', + 'password', + 'authMechanism', + 'compression', + 'fsync', + 'readPreferenceTags', + 'numberOfRetries', + 'auto_reconnect', + 'minSize', + 'monitorCommands', + 'retryWrites', + 'retryReads', + 'useNewUrlParser', + 'useUnifiedTopology', + 'serverSelectionTimeoutMS', + 'useRecoveryToken', + 'autoEncryption', + 'driverInfo', + 'tls', + 'tlsInsecure', + 'tlsinsecure', + 'tlsAllowInvalidCertificates', + 'tlsAllowInvalidHostnames', + 'tlsCAFile', + 'tlsCertificateFile', + 'tlsCertificateKeyFile', + 'tlsCertificateKeyFilePassword', + 'minHeartbeatFrequencyMS', + 'heartbeatFrequencyMS', + 'directConnection', + 'appName', + + // CMAP options + 'maxPoolSize', + 'minPoolSize', + 'maxIdleTimeMS', + 'waitQueueTimeoutMS' +]; + +const ignoreOptionNames = ['native_parser']; +const legacyOptionNames = ['server', 'replset', 'replSet', 'mongos', 'db']; + +// Validate options object +function validOptions(options) { + const _validOptions = validOptionNames.concat(legacyOptionNames); + + for (const name in options) { + if (ignoreOptionNames.indexOf(name) !== -1) { + continue; + } + + if (_validOptions.indexOf(name) === -1) { + if (options.validateOptions) { + return new MongoError(`option ${name} is not supported`); + } else { + console.warn(`the options [${name}] is not supported`); + } + } + + if (legacyOptionNames.indexOf(name) !== -1) { + console.warn( + `the server/replset/mongos/db options are deprecated, ` + + `all their options are supported at the top level of the options object [${validOptionNames}]` + ); + } + } +} + +const LEGACY_OPTIONS_MAP = validOptionNames.reduce((obj, name) => { + obj[name.toLowerCase()] = name; + return obj; +}, {}); + +function addListeners(mongoClient, topology) { + topology.on('authenticated', createListener(mongoClient, 'authenticated')); + topology.on('error', createListener(mongoClient, 'error')); + topology.on('timeout', createListener(mongoClient, 'timeout')); + topology.on('close', createListener(mongoClient, 'close')); + topology.on('parseError', createListener(mongoClient, 'parseError')); + topology.once('open', createListener(mongoClient, 'open')); + topology.once('fullsetup', createListener(mongoClient, 'fullsetup')); + topology.once('all', createListener(mongoClient, 'all')); + topology.on('reconnect', createListener(mongoClient, 'reconnect')); +} + +function assignTopology(client, topology) { + client.topology = topology; + + if (!(topology instanceof NativeTopology)) { + topology.s.sessionPool = new ServerSessionPool(topology.s.coreTopology); + } +} + +// Clear out all events +function clearAllEvents(topology) { + monitoringEvents.forEach(event => topology.removeAllListeners(event)); +} + +// Collect all events in order from SDAM +function collectEvents(mongoClient, topology) { + let MongoClient = loadClient(); + const collectedEvents = []; + + if (mongoClient instanceof MongoClient) { + monitoringEvents.forEach(event => { + topology.on(event, (object1, object2) => { + if (event === 'open') { + collectedEvents.push({ event: event, object1: mongoClient }); + } else { + collectedEvents.push({ event: event, object1: object1, object2: object2 }); + } + }); + }); + } + + return collectedEvents; +} + +function resolveTLSOptions(options) { + if (options.tls == null) { + return; + } + + ['sslCA', 'sslKey', 'sslCert'].forEach(optionName => { + if (options[optionName]) { + options[optionName] = fs.readFileSync(options[optionName]); + } + }); +} + +const emitDeprecationForNonUnifiedTopology = deprecate(() => {}, +'current Server Discovery and Monitoring engine is deprecated, and will be removed in a future version. ' + 'To use the new Server Discover and Monitoring engine, pass option { useUnifiedTopology: true } to the MongoClient constructor.'); + +function connect(mongoClient, url, options, callback) { + options = Object.assign({}, options); + + // If callback is null throw an exception + if (callback == null) { + throw new Error('no callback function provided'); + } + + let didRequestAuthentication = false; + const logger = Logger('MongoClient', options); + + // Did we pass in a Server/ReplSet/Mongos + if (url instanceof Server || url instanceof ReplSet || url instanceof Mongos) { + return connectWithUrl(mongoClient, url, options, connectCallback); + } + + const useNewUrlParser = options.useNewUrlParser !== false; + + const parseFn = useNewUrlParser ? parse : legacyParse; + const transform = useNewUrlParser ? transformUrlOptions : legacyTransformUrlOptions; + + parseFn(url, options, (err, _object) => { + // Do not attempt to connect if parsing error + if (err) return callback(err); + + // Flatten + const object = transform(_object); + + // Parse the string + const _finalOptions = createUnifiedOptions(object, options); + + // Check if we have connection and socket timeout set + if (_finalOptions.socketTimeoutMS == null) _finalOptions.socketTimeoutMS = 0; + if (_finalOptions.connectTimeoutMS == null) _finalOptions.connectTimeoutMS = 10000; + if (_finalOptions.retryWrites == null) _finalOptions.retryWrites = true; + if (_finalOptions.useRecoveryToken == null) _finalOptions.useRecoveryToken = true; + if (_finalOptions.readPreference == null) _finalOptions.readPreference = 'primary'; + + if (_finalOptions.db_options && _finalOptions.db_options.auth) { + delete _finalOptions.db_options.auth; + } + + // `journal` should be translated to `j` for the driver + if (_finalOptions.journal != null) { + _finalOptions.j = _finalOptions.journal; + _finalOptions.journal = undefined; + } + + // resolve tls options if needed + resolveTLSOptions(_finalOptions); + + // Store the merged options object + mongoClient.s.options = _finalOptions; + + // Failure modes + if (object.servers.length === 0) { + return callback(new Error('connection string must contain at least one seed host')); + } + + if (_finalOptions.auth && !_finalOptions.credentials) { + try { + didRequestAuthentication = true; + _finalOptions.credentials = generateCredentials( + mongoClient, + _finalOptions.auth.user, + _finalOptions.auth.password, + _finalOptions + ); + } catch (err) { + return callback(err); + } + } + + if (_finalOptions.useUnifiedTopology) { + return createTopology(mongoClient, 'unified', _finalOptions, connectCallback); + } + + emitDeprecationForNonUnifiedTopology(); + + // Do we have a replicaset then skip discovery and go straight to connectivity + if (_finalOptions.replicaSet || _finalOptions.rs_name) { + return createTopology(mongoClient, 'replicaset', _finalOptions, connectCallback); + } else if (object.servers.length > 1) { + return createTopology(mongoClient, 'mongos', _finalOptions, connectCallback); + } else { + return createServer(mongoClient, _finalOptions, connectCallback); + } + }); + + function connectCallback(err, topology) { + const warningMessage = `seed list contains no mongos proxies, replicaset connections requires the parameter replicaSet to be supplied in the URI or options object, mongodb://server:port/db?replicaSet=name`; + if (err && err.message === 'no mongos proxies found in seed list') { + if (logger.isWarn()) { + logger.warn(warningMessage); + } + + // Return a more specific error message for MongoClient.connect + return callback(new MongoError(warningMessage)); + } + + if (didRequestAuthentication) { + mongoClient.emit('authenticated', null, true); + } + + // Return the error and db instance + callback(err, topology); + } +} + +function connectWithUrl(mongoClient, url, options, connectCallback) { + // Set the topology + assignTopology(mongoClient, url); + + // Add listeners + addListeners(mongoClient, url); + + // Propagate the events to the client + relayEvents(mongoClient, url); + + let finalOptions = Object.assign({}, options); + + // If we have a readPreference passed in by the db options, convert it from a string + if (typeof options.readPreference === 'string' || typeof options.read_preference === 'string') { + finalOptions.readPreference = new ReadPreference( + options.readPreference || options.read_preference + ); + } + + const isDoingAuth = finalOptions.user || finalOptions.password || finalOptions.authMechanism; + if (isDoingAuth && !finalOptions.credentials) { + try { + finalOptions.credentials = generateCredentials( + mongoClient, + finalOptions.user, + finalOptions.password, + finalOptions + ); + } catch (err) { + return connectCallback(err, url); + } + } + + return url.connect(finalOptions, connectCallback); +} + +function createListener(mongoClient, event) { + const eventSet = new Set(['all', 'fullsetup', 'open', 'reconnect']); + return (v1, v2) => { + if (eventSet.has(event)) { + return mongoClient.emit(event, mongoClient); + } + + mongoClient.emit(event, v1, v2); + }; +} + +function createServer(mongoClient, options, callback) { + // Pass in the promise library + options.promiseLibrary = mongoClient.s.promiseLibrary; + + // Set default options + const servers = translateOptions(options); + + const server = servers[0]; + + // Propagate the events to the client + const collectedEvents = collectEvents(mongoClient, server); + + // Connect to topology + server.connect(options, (err, topology) => { + if (err) { + server.close(true); + return callback(err); + } + // Clear out all the collected event listeners + clearAllEvents(server); + + // Relay all the events + relayEvents(mongoClient, server); + // Add listeners + addListeners(mongoClient, server); + // Check if we are really speaking to a mongos + const ismaster = topology.lastIsMaster(); + + // Set the topology + assignTopology(mongoClient, topology); + + // Do we actually have a mongos + if (ismaster && ismaster.msg === 'isdbgrid') { + // Destroy the current connection + topology.close(); + // Create mongos connection instead + return createTopology(mongoClient, 'mongos', options, callback); + } + + // Fire all the events + replayEvents(mongoClient, collectedEvents); + // Otherwise callback + callback(err, topology); + }); +} + +const DEPRECATED_UNIFIED_EVENTS = new Set([ + 'reconnect', + 'reconnectFailed', + 'attemptReconnect', + 'joined', + 'left', + 'ping', + 'ha', + 'all', + 'fullsetup', + 'open' +]); + +function registerDeprecatedEventNotifiers(client) { + client.on('newListener', eventName => { + if (DEPRECATED_UNIFIED_EVENTS.has(eventName)) { + emitDeprecationWarning( + `The \`${eventName}\` event is no longer supported by the unified topology, please read more by visiting http://bit.ly/2D8WfT6`, + 'DeprecationWarning' + ); + } + }); +} + +function createTopology(mongoClient, topologyType, options, callback) { + // Pass in the promise library + options.promiseLibrary = mongoClient.s.promiseLibrary; + + const translationOptions = {}; + if (topologyType === 'unified') translationOptions.createServers = false; + + // Set default options + const servers = translateOptions(options, translationOptions); + + // determine CSFLE support + if (options.autoEncryption != null) { + let AutoEncrypter; + try { + require.resolve('mongodb-client-encryption'); + } catch (err) { + callback( + new MongoError( + 'Auto-encryption requested, but the module is not installed. Please add `mongodb-client-encryption` as a dependency of your project' + ) + ); + return; + } + + try { + let mongodbClientEncryption = require('mongodb-client-encryption'); + if (typeof mongodbClientEncryption.extension !== 'function') { + callback( + new MongoError( + 'loaded version of `mongodb-client-encryption` does not have property `extension`. Please make sure you are loading the correct version of `mongodb-client-encryption`' + ) + ); + } + AutoEncrypter = mongodbClientEncryption.extension(require('../../index')).AutoEncrypter; + } catch (err) { + callback(err); + return; + } + + const mongoCryptOptions = Object.assign( + { + bson: + options.bson || + new BSON([ + BSON.Binary, + BSON.Code, + BSON.DBRef, + BSON.Decimal128, + BSON.Double, + BSON.Int32, + BSON.Long, + BSON.Map, + BSON.MaxKey, + BSON.MinKey, + BSON.ObjectId, + BSON.BSONRegExp, + BSON.Symbol, + BSON.Timestamp + ]) + }, + options.autoEncryption + ); + + options.autoEncrypter = new AutoEncrypter(mongoClient, mongoCryptOptions); + } + + // Create the topology + let topology; + if (topologyType === 'mongos') { + topology = new Mongos(servers, options); + } else if (topologyType === 'replicaset') { + topology = new ReplSet(servers, options); + } else if (topologyType === 'unified') { + topology = new NativeTopology(options.servers, options); + registerDeprecatedEventNotifiers(mongoClient); + } + + // Add listeners + addListeners(mongoClient, topology); + + // Propagate the events to the client + relayEvents(mongoClient, topology); + + // Open the connection + assignTopology(mongoClient, topology); + + // initialize CSFLE if requested + if (options.autoEncrypter) { + options.autoEncrypter.init(err => { + if (err) { + callback(err); + return; + } + + topology.connect(options, err => { + if (err) { + topology.close(true); + callback(err); + return; + } + + callback(undefined, topology); + }); + }); + + return; + } + + // otherwise connect normally + topology.connect(options, err => { + if (err) { + topology.close(true); + return callback(err); + } + + callback(undefined, topology); + return; + }); +} + +function createUnifiedOptions(finalOptions, options) { + const childOptions = [ + 'mongos', + 'server', + 'db', + 'replset', + 'db_options', + 'server_options', + 'rs_options', + 'mongos_options' + ]; + const noMerge = ['readconcern', 'compression', 'autoencryption']; + + for (const name in options) { + if (noMerge.indexOf(name.toLowerCase()) !== -1) { + finalOptions[name] = options[name]; + } else if (childOptions.indexOf(name.toLowerCase()) !== -1) { + finalOptions = mergeOptions(finalOptions, options[name], false); + } else { + if ( + options[name] && + typeof options[name] === 'object' && + !Buffer.isBuffer(options[name]) && + !Array.isArray(options[name]) + ) { + finalOptions = mergeOptions(finalOptions, options[name], true); + } else { + finalOptions[name] = options[name]; + } + } + } + + return finalOptions; +} + +function generateCredentials(client, username, password, options) { + options = Object.assign({}, options); + + // the default db to authenticate against is 'self' + // if authententicate is called from a retry context, it may be another one, like admin + const source = options.authSource || options.authdb || options.dbName; + + // authMechanism + const authMechanismRaw = options.authMechanism || 'DEFAULT'; + const authMechanism = authMechanismRaw.toUpperCase(); + const mechanismProperties = options.authMechanismProperties; + + if (!VALID_AUTH_MECHANISMS.has(authMechanism)) { + throw MongoError.create({ + message: `authentication mechanism ${authMechanismRaw} not supported', options.authMechanism`, + driver: true + }); + } + + return new MongoCredentials({ + mechanism: AUTH_MECHANISM_INTERNAL_MAP[authMechanism], + mechanismProperties, + source, + username, + password + }); +} + +function legacyTransformUrlOptions(object) { + return mergeOptions(createUnifiedOptions({}, object), object, false); +} + +function mergeOptions(target, source, flatten) { + for (const name in source) { + if (source[name] && typeof source[name] === 'object' && flatten) { + target = mergeOptions(target, source[name], flatten); + } else { + target[name] = source[name]; + } + } + + return target; +} + +function relayEvents(mongoClient, topology) { + const serverOrCommandEvents = [ + // APM + 'commandStarted', + 'commandSucceeded', + 'commandFailed', + + // SDAM + 'serverOpening', + 'serverClosed', + 'serverDescriptionChanged', + 'serverHeartbeatStarted', + 'serverHeartbeatSucceeded', + 'serverHeartbeatFailed', + 'topologyOpening', + 'topologyClosed', + 'topologyDescriptionChanged', + + // Legacy + 'joined', + 'left', + 'ping', + 'ha' + ].concat(CMAP_EVENT_NAMES); + + serverOrCommandEvents.forEach(event => { + topology.on(event, (object1, object2) => { + mongoClient.emit(event, object1, object2); + }); + }); +} + +// +// Replay any events due to single server connection switching to Mongos +// +function replayEvents(mongoClient, events) { + for (let i = 0; i < events.length; i++) { + mongoClient.emit(events[i].event, events[i].object1, events[i].object2); + } +} + +function transformUrlOptions(_object) { + let object = Object.assign({ servers: _object.hosts }, _object.options); + for (let name in object) { + const camelCaseName = LEGACY_OPTIONS_MAP[name]; + if (camelCaseName) { + object[camelCaseName] = object[name]; + } + } + + const hasUsername = _object.auth && _object.auth.username; + const hasAuthMechanism = _object.options && _object.options.authMechanism; + if (hasUsername || hasAuthMechanism) { + object.auth = Object.assign({}, _object.auth); + if (object.auth.db) { + object.authSource = object.authSource || object.auth.db; + } + + if (object.auth.username) { + object.auth.user = object.auth.username; + } + } + + if (_object.defaultDatabase) { + object.dbName = _object.defaultDatabase; + } + + if (object.maxPoolSize) { + object.poolSize = object.maxPoolSize; + } + + if (object.readConcernLevel) { + object.readConcern = new ReadConcern(object.readConcernLevel); + } + + if (object.wTimeoutMS) { + object.wtimeout = object.wTimeoutMS; + } + + if (_object.srvHost) { + object.srvHost = _object.srvHost; + } + + return object; +} + +function translateOptions(options, translationOptions) { + translationOptions = Object.assign({}, { createServers: true }, translationOptions); + + // If we have a readPreference passed in by the db options + if (typeof options.readPreference === 'string' || typeof options.read_preference === 'string') { + options.readPreference = new ReadPreference(options.readPreference || options.read_preference); + } + + // Do we have readPreference tags, add them + if (options.readPreference && (options.readPreferenceTags || options.read_preference_tags)) { + options.readPreference.tags = options.readPreferenceTags || options.read_preference_tags; + } + + // Do we have maxStalenessSeconds + if (options.maxStalenessSeconds) { + options.readPreference.maxStalenessSeconds = options.maxStalenessSeconds; + } + + // Set the socket and connection timeouts + if (options.socketTimeoutMS == null) options.socketTimeoutMS = 0; + if (options.connectTimeoutMS == null) options.connectTimeoutMS = 10000; + + if (!translationOptions.createServers) { + return; + } + + // Create server instances + return options.servers.map(serverObj => { + return serverObj.domain_socket + ? new Server(serverObj.domain_socket, 27017, options) + : new Server(serverObj.host, serverObj.port, options); + }); +} + +module.exports = { validOptions, connect }; -- cgit v1.2.3