diff options
Diffstat (limited to 'node_modules/mongodb/lib/url_parser.js')
-rw-r--r-- | node_modules/mongodb/lib/url_parser.js | 623 |
1 files changed, 623 insertions, 0 deletions
diff --git a/node_modules/mongodb/lib/url_parser.js b/node_modules/mongodb/lib/url_parser.js new file mode 100644 index 0000000..c0f10b4 --- /dev/null +++ b/node_modules/mongodb/lib/url_parser.js @@ -0,0 +1,623 @@ +'use strict'; + +const ReadPreference = require('./core').ReadPreference, + parser = require('url'), + f = require('util').format, + Logger = require('./core').Logger, + dns = require('dns'); +const ReadConcern = require('./read_concern'); + +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; + + if (result.pathname && result.pathname.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') return callback(err); + if (err && err.code === 'ENODATA') record = null; + + if (record) { + if (record.length > 1) { + return callback(new Error('Multiple text records not allowed')); + } + + record = record[0]; + if (record.length > 1) record = record.join(''); + else record = record[0]; + + if (!record.includes('authSource') && !record.includes('replicaSet')) { + return callback(new Error('Text record must only set `authSource` or `replicaSet`')); + } + + 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(/[&;]/); + // 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; +} |