"use strict";
|
|
|
|
var ReadPreference = require('./read_preference'),
|
|
parser = require('url'),
|
|
f = require('util').format,
|
|
assign = require('./utils').assign,
|
|
dns = require('dns');
|
|
|
|
module.exports = function(url, options, callback) {
|
|
if (typeof options === 'function') (callback = options), (options = {});
|
|
options = options || {};
|
|
|
|
var result = parser.parse(url, true);
|
|
if (result.protocol !== 'mongodb:' && result.protocol !== 'mongodb+srv:') {
|
|
return callback(new Error('invalid schema, expected mongodb or mongodb+srv'));
|
|
}
|
|
|
|
if (result.protocol === 'mongodb+srv:') {
|
|
|
|
if (result.hostname.split('.').length < 3) {
|
|
return callback(new Error('uri does not have hostname, domainname 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'));
|
|
}
|
|
|
|
var 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 (var i = 0; i < addresses.length; i++) {
|
|
if (!matchesParentDomain(addresses[i].name, result.hostname, result.domainLength)) {
|
|
return callback(new Error('srv record does not share hostname with parent uri'));
|
|
}
|
|
}
|
|
|
|
var connectionStrings = addresses.map(function(address, i) {
|
|
if (i === 0) return 'mongodb://' + address.name + ':' + address.port;
|
|
else return address.name + ':' + address.port;
|
|
});
|
|
|
|
var connectionString = connectionStrings.join(',') + '/';
|
|
var connectionStringOptions = [];
|
|
|
|
// 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);
|
|
});
|
|
});
|
|
} else {
|
|
parseHandler(url, options, callback);
|
|
}
|
|
};
|
|
|
|
function matchesParentDomain(srvAddress, parentDomain) {
|
|
var regex = /^.*?\./;
|
|
var srv = '.' + srvAddress.replace(regex, '');
|
|
var parent = '.' + parentDomain.replace(regex, '');
|
|
if (srv.endsWith(parent)) return true;
|
|
else return false;
|
|
}
|
|
|
|
function parseHandler(address, options, callback) {
|
|
var result, err;
|
|
try {
|
|
result = parseConnectionString(address, options);
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
|
|
return err ? callback(err, null) : callback(null, result);
|
|
}
|
|
|
|
function parseConnectionString(url, options) {
|
|
// Variables
|
|
var connection_part = '';
|
|
var auth_part = '';
|
|
var query_string_part = '';
|
|
var dbName = 'admin';
|
|
|
|
// Url parser result
|
|
var 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(var 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) {
|
|
var 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
|
|
var clean = url.split('?').shift();
|
|
|
|
// Extract the list of hosts
|
|
var strings = clean.split(',');
|
|
var hosts = [];
|
|
|
|
for(var i = 0; i < strings.length; i++) {
|
|
var 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(i = 0; i < hosts.length; i++) {
|
|
var r = parser.parse(f('mongodb://%s', hosts[i].trim()));
|
|
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) {
|
|
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 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];
|
|
}
|
|
|
|
// Result object
|
|
var object = {};
|
|
|
|
// Pick apart the authentication part of the string
|
|
var authPart = auth_part || '';
|
|
var auth = authPart.split(':', 2);
|
|
|
|
// Decode the URI components
|
|
auth[0] = decodeURIComponent(auth[0]);
|
|
if(auth[1]){
|
|
auth[1] = decodeURIComponent(auth[1]);
|
|
}
|
|
|
|
// 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
|
|
var hostPart;
|
|
var urlOptions;
|
|
var servers;
|
|
var serverOptions = {socketOptions: {}};
|
|
var dbOptions = {read_preference_tags: []};
|
|
var replSetServersOptions = {socketOptions: {}};
|
|
var 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
|
|
var 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];
|
|
servers = [{domain_socket: domainSocket}];
|
|
} else {
|
|
// Split up the db
|
|
hostPart = connection_part;
|
|
// Deduplicate servers
|
|
var deduplicatedServers = {};
|
|
|
|
// Parse all server results
|
|
servers = hostPart.split(',').map(function(h) {
|
|
var _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
|
|
var 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 'minPoolSize':
|
|
throw new Error("minPoolSize not supported");
|
|
case 'maxIdleTimeMS':
|
|
throw new Error("maxIdleTimeMS not supported");
|
|
case 'waitQueueMultiple':
|
|
throw new Error("waitQueueMultiple not supported");
|
|
case 'waitQueueTimeoutMS':
|
|
throw new Error("waitQueueTimeoutMS not supported");
|
|
case 'uuidRepresentation':
|
|
throw new Error("uuidRepresentation not supported");
|
|
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 = {level: 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) {
|
|
var 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 != 'PLAIN')
|
|
throw new Error("only DEFAULT, GSSAPI, PLAIN, MONGODB-X509, SCRAM-SHA-1 or MONGODB-CR is supported by authMechanism");
|
|
|
|
// Authentication mechanism
|
|
dbOptions.authMechanism = value;
|
|
break;
|
|
case 'authMechanismProperties':
|
|
// Split up into key, value pairs
|
|
var values = value.split(',');
|
|
var o = {};
|
|
// For each value split into key, value
|
|
values.forEach(function(x) {
|
|
var 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
|
|
var tagObject = {};
|
|
if(value == null || value == '') {
|
|
dbOptions.read_preference_tags.push(tagObject);
|
|
break;
|
|
}
|
|
|
|
// Split up the tags
|
|
var tags = value.split(/\,/);
|
|
for(var i = 0; i < tags.length; i++) {
|
|
var parts = tags[i].trim().split(/\:/);
|
|
tagObject[parts[0]] = parts[1];
|
|
}
|
|
|
|
// Set the preferences tags
|
|
dbOptions.read_preference_tags.push(tagObject);
|
|
break;
|
|
default:
|
|
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 = assign(dbOptions, options);
|
|
|
|
// Add servers to result
|
|
object.servers = servers;
|
|
|
|
// Returned parsed object
|
|
return object;
|
|
}
|