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.
		
		
		
		
		
			
		
			
				
					638 lines
				
				21 KiB
			
		
		
			
		
	
	
					638 lines
				
				21 KiB
			| 
											3 years ago
										 | 'use strict'; | ||
|  | 
 | ||
|  | const ReadPreference = require('./core').ReadPreference; | ||
|  | const parser = require('url'); | ||
|  | const f = require('util').format; | ||
|  | const Logger = require('./core').Logger; | ||
|  | const dns = require('dns'); | ||
|  | const ReadConcern = require('./read_concern'); | ||
|  | const qs = require('querystring'); | ||
|  | const MongoParseError = require('./core/error').MongoParseError; | ||
|  | 
 | ||
|  | module.exports = function(url, options, callback) { | ||
|  |   if (typeof options === 'function') (callback = options), (options = {}); | ||
|  |   options = options || {}; | ||
|  | 
 | ||
|  |   let result; | ||
|  |   try { | ||
|  |     result = parser.parse(url, true); | ||
|  |   } catch (e) { | ||
|  |     return callback(new Error('URL malformed, cannot be parsed')); | ||
|  |   } | ||
|  | 
 | ||
|  |   if (result.protocol !== 'mongodb:' && result.protocol !== 'mongodb+srv:') { | ||
|  |     return callback(new Error('Invalid schema, expected `mongodb` or `mongodb+srv`')); | ||
|  |   } | ||
|  | 
 | ||
|  |   if (result.protocol === 'mongodb:') { | ||
|  |     return parseHandler(url, options, callback); | ||
|  |   } | ||
|  | 
 | ||
|  |   // Otherwise parse this as an SRV record
 | ||
|  |   if (result.hostname.split('.').length < 3) { | ||
|  |     return callback(new Error('URI does not have hostname, domain name and tld')); | ||
|  |   } | ||
|  | 
 | ||
|  |   result.domainLength = result.hostname.split('.').length; | ||
|  | 
 | ||
|  |   const hostname = url.substring('mongodb+srv://'.length).split('/')[0]; | ||
|  |   if (hostname.match(',')) { | ||
|  |     return callback(new Error('Invalid URI, cannot contain multiple hostnames')); | ||
|  |   } | ||
|  | 
 | ||
|  |   if (result.port) { | ||
|  |     return callback(new Error('Ports not accepted with `mongodb+srv` URIs')); | ||
|  |   } | ||
|  | 
 | ||
|  |   let srvAddress = `_mongodb._tcp.${result.host}`; | ||
|  |   dns.resolveSrv(srvAddress, function(err, addresses) { | ||
|  |     if (err) return callback(err); | ||
|  | 
 | ||
|  |     if (addresses.length === 0) { | ||
|  |       return callback(new Error('No addresses found at host')); | ||
|  |     } | ||
|  | 
 | ||
|  |     for (let i = 0; i < addresses.length; i++) { | ||
|  |       if (!matchesParentDomain(addresses[i].name, result.hostname, result.domainLength)) { | ||
|  |         return callback(new Error('Server record does not share hostname with parent URI')); | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     let base = result.auth ? `mongodb://${result.auth}@` : `mongodb://`; | ||
|  |     let connectionStrings = addresses.map(function(address, i) { | ||
|  |       if (i === 0) return `${base}${address.name}:${address.port}`; | ||
|  |       else return `${address.name}:${address.port}`; | ||
|  |     }); | ||
|  | 
 | ||
|  |     let connectionString = connectionStrings.join(',') + '/'; | ||
|  |     let connectionStringOptions = []; | ||
|  | 
 | ||
|  |     // Add the default database if needed
 | ||
|  |     if (result.path) { | ||
|  |       let defaultDb = result.path.slice(1); | ||
|  |       if (defaultDb.indexOf('?') !== -1) { | ||
|  |         defaultDb = defaultDb.slice(0, defaultDb.indexOf('?')); | ||
|  |       } | ||
|  | 
 | ||
|  |       connectionString += defaultDb; | ||
|  |     } | ||
|  | 
 | ||
|  |     // Default to SSL true
 | ||
|  |     if (!options.ssl && !result.search) { | ||
|  |       connectionStringOptions.push('ssl=true'); | ||
|  |     } else if (!options.ssl && result.search && !result.search.match('ssl')) { | ||
|  |       connectionStringOptions.push('ssl=true'); | ||
|  |     } | ||
|  | 
 | ||
|  |     // Keep original uri options
 | ||
|  |     if (result.search) { | ||
|  |       connectionStringOptions.push(result.search.replace('?', '')); | ||
|  |     } | ||
|  | 
 | ||
|  |     dns.resolveTxt(result.host, function(err, record) { | ||
|  |       if (err && err.code !== 'ENODATA' && err.code !== 'ENOTFOUND') return callback(err); | ||
|  |       if (err && err.code === 'ENODATA') record = null; | ||
|  | 
 | ||
|  |       if (record) { | ||
|  |         if (record.length > 1) { | ||
|  |           return callback(new MongoParseError('Multiple text records not allowed')); | ||
|  |         } | ||
|  | 
 | ||
|  |         record = record[0].join(''); | ||
|  |         const parsedRecord = qs.parse(record); | ||
|  |         const items = Object.keys(parsedRecord); | ||
|  |         if (Object.keys(items).some(k => k.toLowerCase() === 'loadbalanced')) { | ||
|  |           return callback(new MongoParseError('Load balancer mode requires driver version 4+')); | ||
|  |         } | ||
|  |         if (items.some(item => item !== 'authSource' && item !== 'replicaSet')) { | ||
|  |           return callback( | ||
|  |             new MongoParseError('Text record must only set `authSource` or `replicaSet`') | ||
|  |           ); | ||
|  |         } | ||
|  | 
 | ||
|  |         if (items.length > 0) { | ||
|  |           connectionStringOptions.push(record); | ||
|  |         } | ||
|  |       } | ||
|  | 
 | ||
|  |       // Add any options to the connection string
 | ||
|  |       if (connectionStringOptions.length) { | ||
|  |         connectionString += `?${connectionStringOptions.join('&')}`; | ||
|  |       } | ||
|  | 
 | ||
|  |       parseHandler(connectionString, options, callback); | ||
|  |     }); | ||
|  |   }); | ||
|  | }; | ||
|  | 
 | ||
|  | function matchesParentDomain(srvAddress, parentDomain) { | ||
|  |   let regex = /^.*?\./; | ||
|  |   let srv = `.${srvAddress.replace(regex, '')}`; | ||
|  |   let parent = `.${parentDomain.replace(regex, '')}`; | ||
|  |   if (srv.endsWith(parent)) return true; | ||
|  |   else return false; | ||
|  | } | ||
|  | 
 | ||
|  | function parseHandler(address, options, callback) { | ||
|  |   let result, err; | ||
|  |   try { | ||
|  |     result = parseConnectionString(address, options); | ||
|  |   } catch (e) { | ||
|  |     err = e; | ||
|  |   } | ||
|  | 
 | ||
|  |   return err ? callback(err, null) : callback(null, result); | ||
|  | } | ||
|  | 
 | ||
|  | function parseConnectionString(url, options) { | ||
|  |   // Variables
 | ||
|  |   let connection_part = ''; | ||
|  |   let auth_part = ''; | ||
|  |   let query_string_part = ''; | ||
|  |   let dbName = 'admin'; | ||
|  | 
 | ||
|  |   // Url parser result
 | ||
|  |   let result = parser.parse(url, true); | ||
|  |   if ((result.hostname == null || result.hostname === '') && url.indexOf('.sock') === -1) { | ||
|  |     throw new Error('No hostname or hostnames provided in connection string'); | ||
|  |   } | ||
|  | 
 | ||
|  |   if (result.port === '0') { | ||
|  |     throw new Error('Invalid port (zero) with hostname'); | ||
|  |   } | ||
|  | 
 | ||
|  |   if (!isNaN(parseInt(result.port, 10)) && parseInt(result.port, 10) > 65535) { | ||
|  |     throw new Error('Invalid port (larger than 65535) with hostname'); | ||
|  |   } | ||
|  | 
 | ||
|  |   if ( | ||
|  |     result.path && | ||
|  |     result.path.length > 0 && | ||
|  |     result.path[0] !== '/' && | ||
|  |     url.indexOf('.sock') === -1 | ||
|  |   ) { | ||
|  |     throw new Error('Missing delimiting slash between hosts and options'); | ||
|  |   } | ||
|  | 
 | ||
|  |   if (result.query) { | ||
|  |     for (let name in result.query) { | ||
|  |       if (name.indexOf('::') !== -1) { | ||
|  |         throw new Error('Double colon in host identifier'); | ||
|  |       } | ||
|  | 
 | ||
|  |       if (result.query[name] === '') { | ||
|  |         throw new Error('Query parameter ' + name + ' is an incomplete value pair'); | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   if (result.auth) { | ||
|  |     let parts = result.auth.split(':'); | ||
|  |     if (url.indexOf(result.auth) !== -1 && parts.length > 2) { | ||
|  |       throw new Error('Username with password containing an unescaped colon'); | ||
|  |     } | ||
|  | 
 | ||
|  |     if (url.indexOf(result.auth) !== -1 && result.auth.indexOf('@') !== -1) { | ||
|  |       throw new Error('Username containing an unescaped at-sign'); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   // Remove query
 | ||
|  |   let clean = url.split('?').shift(); | ||
|  | 
 | ||
|  |   // Extract the list of hosts
 | ||
|  |   let strings = clean.split(','); | ||
|  |   let hosts = []; | ||
|  | 
 | ||
|  |   for (let i = 0; i < strings.length; i++) { | ||
|  |     let hostString = strings[i]; | ||
|  | 
 | ||
|  |     if (hostString.indexOf('mongodb') !== -1) { | ||
|  |       if (hostString.indexOf('@') !== -1) { | ||
|  |         hosts.push(hostString.split('@').pop()); | ||
|  |       } else { | ||
|  |         hosts.push(hostString.substr('mongodb://'.length)); | ||
|  |       } | ||
|  |     } else if (hostString.indexOf('/') !== -1) { | ||
|  |       hosts.push(hostString.split('/').shift()); | ||
|  |     } else if (hostString.indexOf('/') === -1) { | ||
|  |       hosts.push(hostString.trim()); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   for (let i = 0; i < hosts.length; i++) { | ||
|  |     let r = parser.parse(f('mongodb://%s', hosts[i].trim())); | ||
|  |     if (r.path && r.path.indexOf('.sock') !== -1) continue; | ||
|  |     if (r.path && r.path.indexOf(':') !== -1) { | ||
|  |       // Not connecting to a socket so check for an extra slash in the hostname.
 | ||
|  |       // Using String#split as perf is better than match.
 | ||
|  |       if (r.path.split('/').length > 1 && r.path.indexOf('::') === -1) { | ||
|  |         throw new Error('Slash in host identifier'); | ||
|  |       } else { | ||
|  |         throw new Error('Double colon in host identifier'); | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   // If we have a ? mark cut the query elements off
 | ||
|  |   if (url.indexOf('?') !== -1) { | ||
|  |     query_string_part = url.substr(url.indexOf('?') + 1); | ||
|  |     connection_part = url.substring('mongodb://'.length, url.indexOf('?')); | ||
|  |   } else { | ||
|  |     connection_part = url.substring('mongodb://'.length); | ||
|  |   } | ||
|  | 
 | ||
|  |   // Check if we have auth params
 | ||
|  |   if (connection_part.indexOf('@') !== -1) { | ||
|  |     auth_part = connection_part.split('@')[0]; | ||
|  |     connection_part = connection_part.split('@')[1]; | ||
|  |   } | ||
|  | 
 | ||
|  |   // Check there is not more than one unescaped slash
 | ||
|  |   if (connection_part.split('/').length > 2) { | ||
|  |     throw new Error( | ||
|  |       "Unsupported host '" + | ||
|  |         connection_part.split('?')[0] + | ||
|  |         "', hosts must be URL encoded and contain at most one unencoded slash" | ||
|  |     ); | ||
|  |   } | ||
|  | 
 | ||
|  |   // Check if the connection string has a db
 | ||
|  |   if (connection_part.indexOf('.sock') !== -1) { | ||
|  |     if (connection_part.indexOf('.sock/') !== -1) { | ||
|  |       dbName = connection_part.split('.sock/')[1]; | ||
|  |       // Check if multiple database names provided, or just an illegal trailing backslash
 | ||
|  |       if (dbName.indexOf('/') !== -1) { | ||
|  |         if (dbName.split('/').length === 2 && dbName.split('/')[1].length === 0) { | ||
|  |           throw new Error('Illegal trailing backslash after database name'); | ||
|  |         } | ||
|  |         throw new Error('More than 1 database name in URL'); | ||
|  |       } | ||
|  |       connection_part = connection_part.split( | ||
|  |         '/', | ||
|  |         connection_part.indexOf('.sock') + '.sock'.length | ||
|  |       ); | ||
|  |     } | ||
|  |   } else if (connection_part.indexOf('/') !== -1) { | ||
|  |     // Check if multiple database names provided, or just an illegal trailing backslash
 | ||
|  |     if (connection_part.split('/').length > 2) { | ||
|  |       if (connection_part.split('/')[2].length === 0) { | ||
|  |         throw new Error('Illegal trailing backslash after database name'); | ||
|  |       } | ||
|  |       throw new Error('More than 1 database name in URL'); | ||
|  |     } | ||
|  |     dbName = connection_part.split('/')[1]; | ||
|  |     connection_part = connection_part.split('/')[0]; | ||
|  |   } | ||
|  | 
 | ||
|  |   // URI decode the host information
 | ||
|  |   connection_part = decodeURIComponent(connection_part); | ||
|  | 
 | ||
|  |   // Result object
 | ||
|  |   let object = {}; | ||
|  | 
 | ||
|  |   // Pick apart the authentication part of the string
 | ||
|  |   let authPart = auth_part || ''; | ||
|  |   let auth = authPart.split(':', 2); | ||
|  | 
 | ||
|  |   // Decode the authentication URI components and verify integrity
 | ||
|  |   let user = decodeURIComponent(auth[0]); | ||
|  |   if (auth[0] !== encodeURIComponent(user)) { | ||
|  |     throw new Error('Username contains an illegal unescaped character'); | ||
|  |   } | ||
|  |   auth[0] = user; | ||
|  | 
 | ||
|  |   if (auth[1]) { | ||
|  |     let pass = decodeURIComponent(auth[1]); | ||
|  |     if (auth[1] !== encodeURIComponent(pass)) { | ||
|  |       throw new Error('Password contains an illegal unescaped character'); | ||
|  |     } | ||
|  |     auth[1] = pass; | ||
|  |   } | ||
|  | 
 | ||
|  |   // Add auth to final object if we have 2 elements
 | ||
|  |   if (auth.length === 2) object.auth = { user: auth[0], password: auth[1] }; | ||
|  |   // if user provided auth options, use that
 | ||
|  |   if (options && options.auth != null) object.auth = options.auth; | ||
|  | 
 | ||
|  |   // Variables used for temporary storage
 | ||
|  |   let hostPart; | ||
|  |   let urlOptions; | ||
|  |   let servers; | ||
|  |   let compression; | ||
|  |   let serverOptions = { socketOptions: {} }; | ||
|  |   let dbOptions = { read_preference_tags: [] }; | ||
|  |   let replSetServersOptions = { socketOptions: {} }; | ||
|  |   let mongosOptions = { socketOptions: {} }; | ||
|  |   // Add server options to final object
 | ||
|  |   object.server_options = serverOptions; | ||
|  |   object.db_options = dbOptions; | ||
|  |   object.rs_options = replSetServersOptions; | ||
|  |   object.mongos_options = mongosOptions; | ||
|  | 
 | ||
|  |   // Let's check if we are using a domain socket
 | ||
|  |   if (url.match(/\.sock/)) { | ||
|  |     // Split out the socket part
 | ||
|  |     let domainSocket = url.substring( | ||
|  |       url.indexOf('mongodb://') + 'mongodb://'.length, | ||
|  |       url.lastIndexOf('.sock') + '.sock'.length | ||
|  |     ); | ||
|  |     // Clean out any auth stuff if any
 | ||
|  |     if (domainSocket.indexOf('@') !== -1) domainSocket = domainSocket.split('@')[1]; | ||
|  |     domainSocket = decodeURIComponent(domainSocket); | ||
|  |     servers = [{ domain_socket: domainSocket }]; | ||
|  |   } else { | ||
|  |     // Split up the db
 | ||
|  |     hostPart = connection_part; | ||
|  |     // Deduplicate servers
 | ||
|  |     let deduplicatedServers = {}; | ||
|  | 
 | ||
|  |     // Parse all server results
 | ||
|  |     servers = hostPart | ||
|  |       .split(',') | ||
|  |       .map(function(h) { | ||
|  |         let _host, _port, ipv6match; | ||
|  |         //check if it matches [IPv6]:port, where the port number is optional
 | ||
|  |         if ((ipv6match = /\[([^\]]+)\](?::(.+))?/.exec(h))) { | ||
|  |           _host = ipv6match[1]; | ||
|  |           _port = parseInt(ipv6match[2], 10) || 27017; | ||
|  |         } else { | ||
|  |           //otherwise assume it's IPv4, or plain hostname
 | ||
|  |           let hostPort = h.split(':', 2); | ||
|  |           _host = hostPort[0] || 'localhost'; | ||
|  |           _port = hostPort[1] != null ? parseInt(hostPort[1], 10) : 27017; | ||
|  |           // Check for localhost?safe=true style case
 | ||
|  |           if (_host.indexOf('?') !== -1) _host = _host.split(/\?/)[0]; | ||
|  |         } | ||
|  | 
 | ||
|  |         // No entry returned for duplicate server
 | ||
|  |         if (deduplicatedServers[_host + '_' + _port]) return null; | ||
|  |         deduplicatedServers[_host + '_' + _port] = 1; | ||
|  | 
 | ||
|  |         // Return the mapped object
 | ||
|  |         return { host: _host, port: _port }; | ||
|  |       }) | ||
|  |       .filter(function(x) { | ||
|  |         return x != null; | ||
|  |       }); | ||
|  |   } | ||
|  | 
 | ||
|  |   // Get the db name
 | ||
|  |   object.dbName = dbName || 'admin'; | ||
|  |   // Split up all the options
 | ||
|  |   urlOptions = (query_string_part || '').split(/[&;]/); | ||
|  | 
 | ||
|  |   if (urlOptions.some(k => k.toLowerCase() === 'loadbalanced')) { | ||
|  |     throw new MongoParseError('Load balancer mode requires driver version 4+'); | ||
|  |   } | ||
|  | 
 | ||
|  |   // Ugh, we have to figure out which options go to which constructor manually.
 | ||
|  |   urlOptions.forEach(function(opt) { | ||
|  |     if (!opt) return; | ||
|  |     var splitOpt = opt.split('='), | ||
|  |       name = splitOpt[0], | ||
|  |       value = splitOpt[1]; | ||
|  | 
 | ||
|  |     // Options implementations
 | ||
|  |     switch (name) { | ||
|  |       case 'slaveOk': | ||
|  |       case 'slave_ok': | ||
|  |         serverOptions.slave_ok = value === 'true'; | ||
|  |         dbOptions.slaveOk = value === 'true'; | ||
|  |         break; | ||
|  |       case 'maxPoolSize': | ||
|  |       case 'poolSize': | ||
|  |         serverOptions.poolSize = parseInt(value, 10); | ||
|  |         replSetServersOptions.poolSize = parseInt(value, 10); | ||
|  |         break; | ||
|  |       case 'appname': | ||
|  |         object.appname = decodeURIComponent(value); | ||
|  |         break; | ||
|  |       case 'autoReconnect': | ||
|  |       case 'auto_reconnect': | ||
|  |         serverOptions.auto_reconnect = value === 'true'; | ||
|  |         break; | ||
|  |       case 'ssl': | ||
|  |         if (value === 'prefer') { | ||
|  |           serverOptions.ssl = value; | ||
|  |           replSetServersOptions.ssl = value; | ||
|  |           mongosOptions.ssl = value; | ||
|  |           break; | ||
|  |         } | ||
|  |         serverOptions.ssl = value === 'true'; | ||
|  |         replSetServersOptions.ssl = value === 'true'; | ||
|  |         mongosOptions.ssl = value === 'true'; | ||
|  |         break; | ||
|  |       case 'sslValidate': | ||
|  |         serverOptions.sslValidate = value === 'true'; | ||
|  |         replSetServersOptions.sslValidate = value === 'true'; | ||
|  |         mongosOptions.sslValidate = value === 'true'; | ||
|  |         break; | ||
|  |       case 'replicaSet': | ||
|  |       case 'rs_name': | ||
|  |         replSetServersOptions.rs_name = value; | ||
|  |         break; | ||
|  |       case 'reconnectWait': | ||
|  |         replSetServersOptions.reconnectWait = parseInt(value, 10); | ||
|  |         break; | ||
|  |       case 'retries': | ||
|  |         replSetServersOptions.retries = parseInt(value, 10); | ||
|  |         break; | ||
|  |       case 'readSecondary': | ||
|  |       case 'read_secondary': | ||
|  |         replSetServersOptions.read_secondary = value === 'true'; | ||
|  |         break; | ||
|  |       case 'fsync': | ||
|  |         dbOptions.fsync = value === 'true'; | ||
|  |         break; | ||
|  |       case 'journal': | ||
|  |         dbOptions.j = value === 'true'; | ||
|  |         break; | ||
|  |       case 'safe': | ||
|  |         dbOptions.safe = value === 'true'; | ||
|  |         break; | ||
|  |       case 'nativeParser': | ||
|  |       case 'native_parser': | ||
|  |         dbOptions.native_parser = value === 'true'; | ||
|  |         break; | ||
|  |       case 'readConcernLevel': | ||
|  |         dbOptions.readConcern = new ReadConcern(value); | ||
|  |         break; | ||
|  |       case 'connectTimeoutMS': | ||
|  |         serverOptions.socketOptions.connectTimeoutMS = parseInt(value, 10); | ||
|  |         replSetServersOptions.socketOptions.connectTimeoutMS = parseInt(value, 10); | ||
|  |         mongosOptions.socketOptions.connectTimeoutMS = parseInt(value, 10); | ||
|  |         break; | ||
|  |       case 'socketTimeoutMS': | ||
|  |         serverOptions.socketOptions.socketTimeoutMS = parseInt(value, 10); | ||
|  |         replSetServersOptions.socketOptions.socketTimeoutMS = parseInt(value, 10); | ||
|  |         mongosOptions.socketOptions.socketTimeoutMS = parseInt(value, 10); | ||
|  |         break; | ||
|  |       case 'w': | ||
|  |         dbOptions.w = parseInt(value, 10); | ||
|  |         if (isNaN(dbOptions.w)) dbOptions.w = value; | ||
|  |         break; | ||
|  |       case 'authSource': | ||
|  |         dbOptions.authSource = value; | ||
|  |         break; | ||
|  |       case 'gssapiServiceName': | ||
|  |         dbOptions.gssapiServiceName = value; | ||
|  |         break; | ||
|  |       case 'authMechanism': | ||
|  |         if (value === 'GSSAPI') { | ||
|  |           // If no password provided decode only the principal
 | ||
|  |           if (object.auth == null) { | ||
|  |             let urlDecodeAuthPart = decodeURIComponent(authPart); | ||
|  |             if (urlDecodeAuthPart.indexOf('@') === -1) | ||
|  |               throw new Error('GSSAPI requires a provided principal'); | ||
|  |             object.auth = { user: urlDecodeAuthPart, password: null }; | ||
|  |           } else { | ||
|  |             object.auth.user = decodeURIComponent(object.auth.user); | ||
|  |           } | ||
|  |         } else if (value === 'MONGODB-X509') { | ||
|  |           object.auth = { user: decodeURIComponent(authPart) }; | ||
|  |         } | ||
|  | 
 | ||
|  |         // Only support GSSAPI or MONGODB-CR for now
 | ||
|  |         if ( | ||
|  |           value !== 'GSSAPI' && | ||
|  |           value !== 'MONGODB-X509' && | ||
|  |           value !== 'MONGODB-CR' && | ||
|  |           value !== 'DEFAULT' && | ||
|  |           value !== 'SCRAM-SHA-1' && | ||
|  |           value !== 'SCRAM-SHA-256' && | ||
|  |           value !== 'PLAIN' | ||
|  |         ) | ||
|  |           throw new Error( | ||
|  |             'Only DEFAULT, GSSAPI, PLAIN, MONGODB-X509, or SCRAM-SHA-1 is supported by authMechanism' | ||
|  |           ); | ||
|  | 
 | ||
|  |         // Authentication mechanism
 | ||
|  |         dbOptions.authMechanism = value; | ||
|  |         break; | ||
|  |       case 'authMechanismProperties': | ||
|  |         { | ||
|  |           // Split up into key, value pairs
 | ||
|  |           let values = value.split(','); | ||
|  |           let o = {}; | ||
|  |           // For each value split into key, value
 | ||
|  |           values.forEach(function(x) { | ||
|  |             let v = x.split(':'); | ||
|  |             o[v[0]] = v[1]; | ||
|  |           }); | ||
|  | 
 | ||
|  |           // Set all authMechanismProperties
 | ||
|  |           dbOptions.authMechanismProperties = o; | ||
|  |           // Set the service name value
 | ||
|  |           if (typeof o.SERVICE_NAME === 'string') dbOptions.gssapiServiceName = o.SERVICE_NAME; | ||
|  |           if (typeof o.SERVICE_REALM === 'string') dbOptions.gssapiServiceRealm = o.SERVICE_REALM; | ||
|  |           if (typeof o.CANONICALIZE_HOST_NAME === 'string') | ||
|  |             dbOptions.gssapiCanonicalizeHostName = | ||
|  |               o.CANONICALIZE_HOST_NAME === 'true' ? true : false; | ||
|  |         } | ||
|  |         break; | ||
|  |       case 'wtimeoutMS': | ||
|  |         dbOptions.wtimeout = parseInt(value, 10); | ||
|  |         break; | ||
|  |       case 'readPreference': | ||
|  |         if (!ReadPreference.isValid(value)) | ||
|  |           throw new Error( | ||
|  |             'readPreference must be either primary/primaryPreferred/secondary/secondaryPreferred/nearest' | ||
|  |           ); | ||
|  |         dbOptions.readPreference = value; | ||
|  |         break; | ||
|  |       case 'maxStalenessSeconds': | ||
|  |         dbOptions.maxStalenessSeconds = parseInt(value, 10); | ||
|  |         break; | ||
|  |       case 'readPreferenceTags': | ||
|  |         { | ||
|  |           // Decode the value
 | ||
|  |           value = decodeURIComponent(value); | ||
|  |           // Contains the tag object
 | ||
|  |           let tagObject = {}; | ||
|  |           if (value == null || value === '') { | ||
|  |             dbOptions.read_preference_tags.push(tagObject); | ||
|  |             break; | ||
|  |           } | ||
|  | 
 | ||
|  |           // Split up the tags
 | ||
|  |           let tags = value.split(/,/); | ||
|  |           for (let i = 0; i < tags.length; i++) { | ||
|  |             let parts = tags[i].trim().split(/:/); | ||
|  |             tagObject[parts[0]] = parts[1]; | ||
|  |           } | ||
|  | 
 | ||
|  |           // Set the preferences tags
 | ||
|  |           dbOptions.read_preference_tags.push(tagObject); | ||
|  |         } | ||
|  |         break; | ||
|  |       case 'compressors': | ||
|  |         { | ||
|  |           compression = serverOptions.compression || {}; | ||
|  |           let compressors = value.split(','); | ||
|  |           if ( | ||
|  |             !compressors.every(function(compressor) { | ||
|  |               return compressor === 'snappy' || compressor === 'zlib'; | ||
|  |             }) | ||
|  |           ) { | ||
|  |             throw new Error('Compressors must be at least one of snappy or zlib'); | ||
|  |           } | ||
|  | 
 | ||
|  |           compression.compressors = compressors; | ||
|  |           serverOptions.compression = compression; | ||
|  |         } | ||
|  |         break; | ||
|  |       case 'zlibCompressionLevel': | ||
|  |         { | ||
|  |           compression = serverOptions.compression || {}; | ||
|  |           let zlibCompressionLevel = parseInt(value, 10); | ||
|  |           if (zlibCompressionLevel < -1 || zlibCompressionLevel > 9) { | ||
|  |             throw new Error('zlibCompressionLevel must be an integer between -1 and 9'); | ||
|  |           } | ||
|  | 
 | ||
|  |           compression.zlibCompressionLevel = zlibCompressionLevel; | ||
|  |           serverOptions.compression = compression; | ||
|  |         } | ||
|  |         break; | ||
|  |       case 'retryWrites': | ||
|  |         dbOptions.retryWrites = value === 'true'; | ||
|  |         break; | ||
|  |       case 'minSize': | ||
|  |         dbOptions.minSize = parseInt(value, 10); | ||
|  |         break; | ||
|  |       default: | ||
|  |         { | ||
|  |           let logger = Logger('URL Parser'); | ||
|  |           logger.warn(`${name} is not supported as a connection string option`); | ||
|  |         } | ||
|  |         break; | ||
|  |     } | ||
|  |   }); | ||
|  | 
 | ||
|  |   // No tags: should be null (not [])
 | ||
|  |   if (dbOptions.read_preference_tags.length === 0) { | ||
|  |     dbOptions.read_preference_tags = null; | ||
|  |   } | ||
|  | 
 | ||
|  |   // Validate if there are an invalid write concern combinations
 | ||
|  |   if ( | ||
|  |     (dbOptions.w === -1 || dbOptions.w === 0) && | ||
|  |     (dbOptions.journal === true || dbOptions.fsync === true || dbOptions.safe === true) | ||
|  |   ) | ||
|  |     throw new Error('w set to -1 or 0 cannot be combined with safe/w/journal/fsync'); | ||
|  | 
 | ||
|  |   // If no read preference set it to primary
 | ||
|  |   if (!dbOptions.readPreference) { | ||
|  |     dbOptions.readPreference = 'primary'; | ||
|  |   } | ||
|  | 
 | ||
|  |   // make sure that user-provided options are applied with priority
 | ||
|  |   dbOptions = Object.assign(dbOptions, options); | ||
|  | 
 | ||
|  |   // Add servers to result
 | ||
|  |   object.servers = servers; | ||
|  | 
 | ||
|  |   // Returned parsed object
 | ||
|  |   return object; | ||
|  | } |