"use strict"; var ReadPreference = require('./read_preference'), parser = require('url'), f = require('util').format; module.exports = function(url) { // 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.protocol != 'mongodb:') { throw new Error('invalid schema, expected mongodb'); } 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) { 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]}; // 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 servr 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'; } // Add servers to result object.servers = servers; // Returned parsed object return object; }