663e5dc5a892f54302b9d5a8d2bb435ab08ddfda
[aai/esr-gui.git] /
1 "use strict";
2
3 var ReadPreference = require('./read_preference'),
4   parser = require('url'),
5   f = require('util').format;
6
7 module.exports = function(url, options) {
8   // Ensure we have a default options object if none set
9   options = options || {};
10   // Variables
11   var connection_part = '';
12   var auth_part = '';
13   var query_string_part = '';
14   var dbName = 'admin';
15
16   // Url parser result
17   var result = parser.parse(url, true);
18
19   if(result.protocol != 'mongodb:') {
20     throw new Error('invalid schema, expected mongodb');
21   }
22
23   if((result.hostname == null || result.hostname == '') && url.indexOf('.sock') == -1) {
24     throw new Error('no hostname or hostnames provided in connection string');
25   }
26
27   if(result.port == '0') {
28     throw new Error('invalid port (zero) with hostname');
29   }
30
31   if(!isNaN(parseInt(result.port, 10)) && parseInt(result.port, 10) > 65535) {
32     throw new Error('invalid port (larger than 65535) with hostname');
33   }
34
35   if(result.path
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');
40   }
41
42   if(result.query) {
43     for(var name in result.query) {
44       if(name.indexOf('::') != -1) {
45         throw new Error('double colon in host identifier');
46       }
47
48       if(result.query[name] == '') {
49         throw new Error('query parameter ' + name + ' is an incomplete value pair');
50       }
51     }
52   }
53
54   if(result.auth) {
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');
58     }
59
60     if(url.indexOf(result.auth) != -1 && result.auth.indexOf('@') != -1) {
61       throw new Error('Username containing an unescaped at-sign');
62     }
63   }
64
65   // Remove query
66   var clean = url.split('?').shift();
67
68   // Extract the list of hosts
69   var strings = clean.split(',');
70   var hosts = [];
71
72   for(var i = 0; i < strings.length; i++) {
73     var hostString = strings[i];
74
75     if(hostString.indexOf('mongodb') != -1) {
76       if(hostString.indexOf('@') != -1) {
77         hosts.push(hostString.split('@').pop())
78       } else {
79         hosts.push(hostString.substr('mongodb://'.length));
80       }
81     } else if(hostString.indexOf('/') != -1) {
82       hosts.push(hostString.split('/').shift());
83     } else if(hostString.indexOf('/') == -1) {
84       hosts.push(hostString.trim());
85     }
86   }
87
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');
92     }
93   }
94
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("?"))
99   } else {
100     connection_part = url.substring("mongodb://".length);
101   }
102
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];
107   }
108
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');
117         }
118         throw new Error('More than 1 database name in URL');
119       }
120       connection_part = connection_part.split("/", connection_part.indexOf(".sock") + ".sock".length);
121     }
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');
127       }
128       throw new Error('More than 1 database name in URL');
129     }
130     dbName = connection_part.split("/")[1];
131     connection_part = connection_part.split("/")[0];
132   }
133
134   // Result object
135   var object = {};
136
137   // Pick apart the authentication part of the string
138   var authPart = auth_part || '';
139   var auth = authPart.split(':', 2);
140
141   // Decode the URI components
142   auth[0] = decodeURIComponent(auth[0]);
143   if(auth[1]){
144     auth[1] = decodeURIComponent(auth[1]);
145   }
146
147   // Add auth to final object if we have 2 elements
148   if(auth.length == 2) object.auth = {user: auth[0], password: auth[1]};
149
150   // Variables used for temporary storage
151   var hostPart;
152   var urlOptions;
153   var servers;
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;
163
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}];
173   } else {
174     // Split up the db
175     hostPart = connection_part;
176     // Deduplicate servers
177     var deduplicatedServers = {};
178
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;
186       } else {
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];
193       }
194
195       // No entry returned for duplicate servr
196       if(deduplicatedServers[_host + "_" + _port]) return null;
197       deduplicatedServers[_host + "_" + _port] = 1;
198
199       // Return the mapped object
200       return {host: _host, port: _port};
201     }).filter(function(x) {
202       return x != null;
203     });
204   }
205
206   // Get the db name
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) {
212     if(!opt) return;
213     var splitOpt = opt.split('='), name = splitOpt[0], value = splitOpt[1];
214     // Options implementations
215     switch(name) {
216       case 'slaveOk':
217       case 'slave_ok':
218         serverOptions.slave_ok = (value == 'true');
219         dbOptions.slaveOk = (value == 'true');
220         break;
221       case 'maxPoolSize':
222       case 'poolSize':
223         serverOptions.poolSize = parseInt(value, 10);
224         replSetServersOptions.poolSize = parseInt(value, 10);
225         break;
226       case 'appname':
227         object.appname = decodeURIComponent(value);
228       case 'autoReconnect':
229       case 'auto_reconnect':
230         serverOptions.auto_reconnect = (value == 'true');
231         break;
232       case 'minPoolSize':
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");
242       case 'ssl':
243         if(value == 'prefer') {
244           serverOptions.ssl = value;
245           replSetServersOptions.ssl = value;
246           mongosOptions.ssl = value;
247           break;
248         }
249         serverOptions.ssl = (value == 'true');
250         replSetServersOptions.ssl = (value == 'true');
251         mongosOptions.ssl = (value == 'true');
252         break;
253       case 'sslValidate':
254         serverOptions.sslValidate = (value == 'true');
255         replSetServersOptions.sslValidate = (value == 'true');
256         mongosOptions.sslValidate = (value == 'true');
257         break;
258       case 'replicaSet':
259       case 'rs_name':
260         replSetServersOptions.rs_name = value;
261         break;
262       case 'reconnectWait':
263         replSetServersOptions.reconnectWait = parseInt(value, 10);
264         break;
265       case 'retries':
266         replSetServersOptions.retries = parseInt(value, 10);
267         break;
268       case 'readSecondary':
269       case 'read_secondary':
270         replSetServersOptions.read_secondary = (value == 'true');
271         break;
272       case 'fsync':
273         dbOptions.fsync = (value == 'true');
274         break;
275       case 'journal':
276         dbOptions.j = (value == 'true');
277         break;
278       case 'safe':
279         dbOptions.safe = (value == 'true');
280         break;
281       case 'nativeParser':
282       case 'native_parser':
283         dbOptions.native_parser = (value == 'true');
284         break;
285       case 'readConcernLevel':
286         dbOptions.readConcern = {level: value};
287         break;
288       case 'connectTimeoutMS':
289         serverOptions.socketOptions.connectTimeoutMS = parseInt(value, 10);
290         replSetServersOptions.socketOptions.connectTimeoutMS = parseInt(value, 10);
291         mongosOptions.socketOptions.connectTimeoutMS = parseInt(value, 10);
292         break;
293       case 'socketTimeoutMS':
294         serverOptions.socketOptions.socketTimeoutMS = parseInt(value, 10);
295         replSetServersOptions.socketOptions.socketTimeoutMS = parseInt(value, 10);
296         mongosOptions.socketOptions.socketTimeoutMS = parseInt(value, 10);
297         break;
298       case 'w':
299         dbOptions.w = parseInt(value, 10);
300         if(isNaN(dbOptions.w)) dbOptions.w = value;
301         break;
302       case 'authSource':
303         dbOptions.authSource = value;
304         break;
305       case 'gssapiServiceName':
306         dbOptions.gssapiServiceName = value;
307         break;
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};
315           } else {
316             object.auth.user = decodeURIComponent(object.auth.user);
317           }
318         } else if(value == 'MONGODB-X509') {
319           object.auth = {user: decodeURIComponent(authPart)};
320         }
321
322         // Only support GSSAPI or MONGODB-CR for now
323         if(value != 'GSSAPI'
324           && value != 'MONGODB-X509'
325           && value != 'MONGODB-CR'
326           && value != 'DEFAULT'
327           && value != 'SCRAM-SHA-1'
328           && value != 'PLAIN')
329             throw new Error("only DEFAULT, GSSAPI, PLAIN, MONGODB-X509, SCRAM-SHA-1 or MONGODB-CR is supported by authMechanism");
330
331         // Authentication mechanism
332         dbOptions.authMechanism = value;
333         break;
334       case 'authMechanismProperties':
335         // Split up into key, value pairs
336         var values = value.split(',');
337         var o = {};
338         // For each value split into key, value
339         values.forEach(function(x) {
340           var v = x.split(':');
341           o[v[0]] = v[1];
342         });
343
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;
350         break;
351       case 'wtimeoutMS':
352         dbOptions.wtimeout = parseInt(value, 10);
353         break;
354       case 'readPreference':
355         if(!ReadPreference.isValid(value)) throw new Error("readPreference must be either primary/primaryPreferred/secondary/secondaryPreferred/nearest");
356         dbOptions.readPreference = value;
357         break;
358       case 'maxStalenessMS':
359         dbOptions.maxStalenessMS = parseInt(value, 10);
360         break;
361       case 'readPreferenceTags':
362         // Decode the value
363         value = decodeURIComponent(value);
364         // Contains the tag object
365         var tagObject = {};
366         if(value == null || value == '') {
367           dbOptions.read_preference_tags.push(tagObject);
368           break;
369         }
370
371         // Split up the tags
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];
376         }
377
378         // Set the preferences tags
379         dbOptions.read_preference_tags.push(tagObject);
380         break;
381       default:
382         break;
383     }
384   });
385
386   // No tags: should be null (not [])
387   if(dbOptions.read_preference_tags.length === 0) {
388     dbOptions.read_preference_tags = null;
389   }
390
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")
396
397   // If no read preference set it to primary
398   if(!dbOptions.readPreference) {
399     dbOptions.readPreference = 'primary';
400   }
401
402   // Add servers to result
403   object.servers = servers;
404   // Returned parsed object
405   return object;
406 }