3 var ReadPreference = require('./read_preference'),
4 parser = require('url'),
5 f = require('util').format;
7 module.exports = function(url, options) {
8 // Ensure we have a default options object if none set
9 options = options || {};
11 var connection_part = '';
13 var query_string_part = '';
17 var result = parser.parse(url, true);
19 if(result.protocol != 'mongodb:') {
20 throw new Error('invalid schema, expected mongodb');
23 if((result.hostname == null || result.hostname == '') && url.indexOf('.sock') == -1) {
24 throw new Error('no hostname or hostnames provided in connection string');
27 if(result.port == '0') {
28 throw new Error('invalid port (zero) with hostname');
31 if(!isNaN(parseInt(result.port, 10)) && parseInt(result.port, 10) > 65535) {
32 throw new Error('invalid port (larger than 65535) with hostname');
36 && result.path.length > 0
37 && result.path[0] != '/'
38 && url.indexOf('.sock') == -1) {
39 throw new Error('missing delimiting slash between hosts and options');
43 for(var name in result.query) {
44 if(name.indexOf('::') != -1) {
45 throw new Error('double colon in host identifier');
48 if(result.query[name] == '') {
49 throw new Error('query parameter ' + name + ' is an incomplete value pair');
55 var parts = result.auth.split(':');
56 if(url.indexOf(result.auth) != -1 && parts.length > 2) {
57 throw new Error('Username with password containing an unescaped colon');
60 if(url.indexOf(result.auth) != -1 && result.auth.indexOf('@') != -1) {
61 throw new Error('Username containing an unescaped at-sign');
66 var clean = url.split('?').shift();
68 // Extract the list of hosts
69 var strings = clean.split(',');
72 for(var i = 0; i < strings.length; i++) {
73 var hostString = strings[i];
75 if(hostString.indexOf('mongodb') != -1) {
76 if(hostString.indexOf('@') != -1) {
77 hosts.push(hostString.split('@').pop())
79 hosts.push(hostString.substr('mongodb://'.length));
81 } else if(hostString.indexOf('/') != -1) {
82 hosts.push(hostString.split('/').shift());
83 } else if(hostString.indexOf('/') == -1) {
84 hosts.push(hostString.trim());
88 for(var i = 0; i < hosts.length; i++) {
89 var r = parser.parse(f('mongodb://%s', hosts[i].trim()));
90 if(r.path && r.path.indexOf(':') != -1) {
91 throw new Error('double colon in host identifier');
95 // If we have a ? mark cut the query elements off
96 if(url.indexOf("?") != -1) {
97 query_string_part = url.substr(url.indexOf("?") + 1);
98 connection_part = url.substring("mongodb://".length, url.indexOf("?"))
100 connection_part = url.substring("mongodb://".length);
103 // Check if we have auth params
104 if(connection_part.indexOf("@") != -1) {
105 auth_part = connection_part.split("@")[0];
106 connection_part = connection_part.split("@")[1];
109 // Check if the connection string has a db
110 if(connection_part.indexOf(".sock") != -1) {
111 if(connection_part.indexOf(".sock/") != -1) {
112 dbName = connection_part.split(".sock/")[1];
113 // Check if multiple database names provided, or just an illegal trailing backslash
114 if (dbName.indexOf("/") != -1) {
115 if (dbName.split("/").length == 2 && dbName.split("/")[1].length == 0) {
116 throw new Error('Illegal trailing backslash after database name');
118 throw new Error('More than 1 database name in URL');
120 connection_part = connection_part.split("/", connection_part.indexOf(".sock") + ".sock".length);
122 } else if(connection_part.indexOf("/") != -1) {
123 // Check if multiple database names provided, or just an illegal trailing backslash
124 if (connection_part.split("/").length > 2) {
125 if (connection_part.split("/")[2].length == 0) {
126 throw new Error('Illegal trailing backslash after database name');
128 throw new Error('More than 1 database name in URL');
130 dbName = connection_part.split("/")[1];
131 connection_part = connection_part.split("/")[0];
137 // Pick apart the authentication part of the string
138 var authPart = auth_part || '';
139 var auth = authPart.split(':', 2);
141 // Decode the URI components
142 auth[0] = decodeURIComponent(auth[0]);
144 auth[1] = decodeURIComponent(auth[1]);
147 // Add auth to final object if we have 2 elements
148 if(auth.length == 2) object.auth = {user: auth[0], password: auth[1]};
150 // Variables used for temporary storage
154 var serverOptions = {socketOptions: {}};
155 var dbOptions = {read_preference_tags: []};
156 var replSetServersOptions = {socketOptions: {}};
157 var mongosOptions = {socketOptions: {}};
158 // Add server options to final object
159 object.server_options = serverOptions;
160 object.db_options = dbOptions;
161 object.rs_options = replSetServersOptions;
162 object.mongos_options = mongosOptions;
164 // Let's check if we are using a domain socket
165 if(url.match(/\.sock/)) {
166 // Split out the socket part
167 var domainSocket = url.substring(
168 url.indexOf("mongodb://") + "mongodb://".length
169 , url.lastIndexOf(".sock") + ".sock".length);
170 // Clean out any auth stuff if any
171 if(domainSocket.indexOf("@") != -1) domainSocket = domainSocket.split("@")[1];
172 servers = [{domain_socket: domainSocket}];
175 hostPart = connection_part;
176 // Deduplicate servers
177 var deduplicatedServers = {};
179 // Parse all server results
180 servers = hostPart.split(',').map(function(h) {
181 var _host, _port, ipv6match;
182 //check if it matches [IPv6]:port, where the port number is optional
183 if ((ipv6match = /\[([^\]]+)\](?:\:(.+))?/.exec(h))) {
184 _host = ipv6match[1];
185 _port = parseInt(ipv6match[2], 10) || 27017;
187 //otherwise assume it's IPv4, or plain hostname
188 var hostPort = h.split(':', 2);
189 _host = hostPort[0] || 'localhost';
190 _port = hostPort[1] != null ? parseInt(hostPort[1], 10) : 27017;
191 // Check for localhost?safe=true style case
192 if(_host.indexOf("?") != -1) _host = _host.split(/\?/)[0];
195 // No entry returned for duplicate servr
196 if(deduplicatedServers[_host + "_" + _port]) return null;
197 deduplicatedServers[_host + "_" + _port] = 1;
199 // Return the mapped object
200 return {host: _host, port: _port};
201 }).filter(function(x) {
207 object.dbName = dbName || 'admin';
208 // Split up all the options
209 urlOptions = (query_string_part || '').split(/[&;]/);
210 // Ugh, we have to figure out which options go to which constructor manually.
211 urlOptions.forEach(function(opt) {
213 var splitOpt = opt.split('='), name = splitOpt[0], value = splitOpt[1];
214 // Options implementations
218 serverOptions.slave_ok = (value == 'true');
219 dbOptions.slaveOk = (value == 'true');
223 serverOptions.poolSize = parseInt(value, 10);
224 replSetServersOptions.poolSize = parseInt(value, 10);
227 object.appname = decodeURIComponent(value);
228 case 'autoReconnect':
229 case 'auto_reconnect':
230 serverOptions.auto_reconnect = (value == 'true');
233 throw new Error("minPoolSize not supported");
234 case 'maxIdleTimeMS':
235 throw new Error("maxIdleTimeMS not supported");
236 case 'waitQueueMultiple':
237 throw new Error("waitQueueMultiple not supported");
238 case 'waitQueueTimeoutMS':
239 throw new Error("waitQueueTimeoutMS not supported");
240 case 'uuidRepresentation':
241 throw new Error("uuidRepresentation not supported");
243 if(value == 'prefer') {
244 serverOptions.ssl = value;
245 replSetServersOptions.ssl = value;
246 mongosOptions.ssl = value;
249 serverOptions.ssl = (value == 'true');
250 replSetServersOptions.ssl = (value == 'true');
251 mongosOptions.ssl = (value == 'true');
254 serverOptions.sslValidate = (value == 'true');
255 replSetServersOptions.sslValidate = (value == 'true');
256 mongosOptions.sslValidate = (value == 'true');
260 replSetServersOptions.rs_name = value;
262 case 'reconnectWait':
263 replSetServersOptions.reconnectWait = parseInt(value, 10);
266 replSetServersOptions.retries = parseInt(value, 10);
268 case 'readSecondary':
269 case 'read_secondary':
270 replSetServersOptions.read_secondary = (value == 'true');
273 dbOptions.fsync = (value == 'true');
276 dbOptions.j = (value == 'true');
279 dbOptions.safe = (value == 'true');
282 case 'native_parser':
283 dbOptions.native_parser = (value == 'true');
285 case 'readConcernLevel':
286 dbOptions.readConcern = {level: value};
288 case 'connectTimeoutMS':
289 serverOptions.socketOptions.connectTimeoutMS = parseInt(value, 10);
290 replSetServersOptions.socketOptions.connectTimeoutMS = parseInt(value, 10);
291 mongosOptions.socketOptions.connectTimeoutMS = parseInt(value, 10);
293 case 'socketTimeoutMS':
294 serverOptions.socketOptions.socketTimeoutMS = parseInt(value, 10);
295 replSetServersOptions.socketOptions.socketTimeoutMS = parseInt(value, 10);
296 mongosOptions.socketOptions.socketTimeoutMS = parseInt(value, 10);
299 dbOptions.w = parseInt(value, 10);
300 if(isNaN(dbOptions.w)) dbOptions.w = value;
303 dbOptions.authSource = value;
305 case 'gssapiServiceName':
306 dbOptions.gssapiServiceName = value;
308 case 'authMechanism':
309 if(value == 'GSSAPI') {
310 // If no password provided decode only the principal
311 if(object.auth == null) {
312 var urlDecodeAuthPart = decodeURIComponent(authPart);
313 if(urlDecodeAuthPart.indexOf("@") == -1) throw new Error("GSSAPI requires a provided principal");
314 object.auth = {user: urlDecodeAuthPart, password: null};
316 object.auth.user = decodeURIComponent(object.auth.user);
318 } else if(value == 'MONGODB-X509') {
319 object.auth = {user: decodeURIComponent(authPart)};
322 // Only support GSSAPI or MONGODB-CR for now
324 && value != 'MONGODB-X509'
325 && value != 'MONGODB-CR'
326 && value != 'DEFAULT'
327 && value != 'SCRAM-SHA-1'
329 throw new Error("only DEFAULT, GSSAPI, PLAIN, MONGODB-X509, SCRAM-SHA-1 or MONGODB-CR is supported by authMechanism");
331 // Authentication mechanism
332 dbOptions.authMechanism = value;
334 case 'authMechanismProperties':
335 // Split up into key, value pairs
336 var values = value.split(',');
338 // For each value split into key, value
339 values.forEach(function(x) {
340 var v = x.split(':');
344 // Set all authMechanismProperties
345 dbOptions.authMechanismProperties = o;
346 // Set the service name value
347 if(typeof o.SERVICE_NAME == 'string') dbOptions.gssapiServiceName = o.SERVICE_NAME;
348 if(typeof o.SERVICE_REALM == 'string') dbOptions.gssapiServiceRealm = o.SERVICE_REALM;
349 if(typeof o.CANONICALIZE_HOST_NAME == 'string') dbOptions.gssapiCanonicalizeHostName = o.CANONICALIZE_HOST_NAME == 'true' ? true : false;
352 dbOptions.wtimeout = parseInt(value, 10);
354 case 'readPreference':
355 if(!ReadPreference.isValid(value)) throw new Error("readPreference must be either primary/primaryPreferred/secondary/secondaryPreferred/nearest");
356 dbOptions.readPreference = value;
358 case 'maxStalenessMS':
359 dbOptions.maxStalenessMS = parseInt(value, 10);
361 case 'readPreferenceTags':
363 value = decodeURIComponent(value);
364 // Contains the tag object
366 if(value == null || value == '') {
367 dbOptions.read_preference_tags.push(tagObject);
372 var tags = value.split(/\,/);
373 for(var i = 0; i < tags.length; i++) {
374 var parts = tags[i].trim().split(/\:/);
375 tagObject[parts[0]] = parts[1];
378 // Set the preferences tags
379 dbOptions.read_preference_tags.push(tagObject);
386 // No tags: should be null (not [])
387 if(dbOptions.read_preference_tags.length === 0) {
388 dbOptions.read_preference_tags = null;
391 // Validate if there are an invalid write concern combinations
392 if((dbOptions.w == -1 || dbOptions.w == 0) && (
393 dbOptions.journal == true
394 || dbOptions.fsync == true
395 || dbOptions.safe == true)) throw new Error("w set to -1 or 0 cannot be combined with safe/w/journal/fsync")
397 // If no read preference set it to primary
398 if(!dbOptions.readPreference) {
399 dbOptions.readPreference = 'primary';
402 // Add servers to result
403 object.servers = servers;
404 // Returned parsed object