PageRenderTime 48ms CodeModel.GetById 2ms app.highlight 39ms RepoModel.GetById 1ms app.codeStats 0ms

/node_modules/mongoose/node_modules/mongodb/lib/url_parser.js

https://bitbucket.org/coleman333/smartsite
JavaScript | 610 lines | 484 code | 70 blank | 56 comment | 132 complexity | 40c22b7b5e696a1792bba792c12bb200 MD5 | raw file
  1'use strict';
  2
  3const ReadPreference = require('mongodb-core').ReadPreference,
  4  parser = require('url'),
  5  f = require('util').format,
  6  Logger = require('mongodb-core').Logger,
  7  dns = require('dns');
  8
  9module.exports = function(url, options, callback) {
 10  if (typeof options === 'function') (callback = options), (options = {});
 11  options = options || {};
 12
 13  let result;
 14  try {
 15    result = parser.parse(url, true);
 16  } catch (e) {
 17    return callback(new Error('URL malformed, cannot be parsed'));
 18  }
 19
 20  if (result.protocol !== 'mongodb:' && result.protocol !== 'mongodb+srv:') {
 21    return callback(new Error('Invalid schema, expected `mongodb` or `mongodb+srv`'));
 22  }
 23
 24  if (result.protocol === 'mongodb:') {
 25    return parseHandler(url, options, callback);
 26  }
 27
 28  // Otherwise parse this as an SRV record
 29  if (result.hostname.split('.').length < 3) {
 30    return callback(new Error('URI does not have hostname, domain name and tld'));
 31  }
 32
 33  result.domainLength = result.hostname.split('.').length;
 34
 35  if (result.pathname && result.pathname.match(',')) {
 36    return callback(new Error('Invalid URI, cannot contain multiple hostnames'));
 37  }
 38
 39  if (result.port) {
 40    return callback(new Error('Ports not accepted with `mongodb+srv` URIs'));
 41  }
 42
 43  let srvAddress = `_mongodb._tcp.${result.host}`;
 44  dns.resolveSrv(srvAddress, function(err, addresses) {
 45    if (err) return callback(err);
 46
 47    if (addresses.length === 0) {
 48      return callback(new Error('No addresses found at host'));
 49    }
 50
 51    for (let i = 0; i < addresses.length; i++) {
 52      if (!matchesParentDomain(addresses[i].name, result.hostname, result.domainLength)) {
 53        return callback(new Error('Server record does not share hostname with parent URI'));
 54      }
 55    }
 56
 57    let base = result.auth ? `mongodb://${result.auth}@` : `mongodb://`;
 58    let connectionStrings = addresses.map(function(address, i) {
 59      if (i === 0) return `${base}${address.name}:${address.port}`;
 60      else return `${address.name}:${address.port}`;
 61    });
 62
 63    let connectionString = connectionStrings.join(',') + '/';
 64    let connectionStringOptions = [];
 65
 66    // Default to SSL true
 67    if (!options.ssl && !result.search) {
 68      connectionStringOptions.push('ssl=true');
 69    } else if (!options.ssl && result.search && !result.search.match('ssl')) {
 70      connectionStringOptions.push('ssl=true');
 71    }
 72
 73    // Keep original uri options
 74    if (result.search) {
 75      connectionStringOptions.push(result.search.replace('?', ''));
 76    }
 77
 78    dns.resolveTxt(result.host, function(err, record) {
 79      if (err && err.code !== 'ENODATA') return callback(err);
 80      if (err && err.code === 'ENODATA') record = null;
 81
 82      if (record) {
 83        if (record.length > 1) {
 84          return callback(new Error('Multiple text records not allowed'));
 85        }
 86
 87        record = record[0];
 88        if (record.length > 1) record = record.join('');
 89        else record = record[0];
 90
 91        if (!record.includes('authSource') && !record.includes('replicaSet')) {
 92          return callback(new Error('Text record must only set `authSource` or `replicaSet`'));
 93        }
 94
 95        connectionStringOptions.push(record);
 96      }
 97
 98      // Add any options to the connection string
 99      if (connectionStringOptions.length) {
100        connectionString += `?${connectionStringOptions.join('&')}`;
101      }
102
103      parseHandler(connectionString, options, callback);
104    });
105  });
106};
107
108function matchesParentDomain(srvAddress, parentDomain) {
109  let regex = /^.*?\./;
110  let srv = `.${srvAddress.replace(regex, '')}`;
111  let parent = `.${parentDomain.replace(regex, '')}`;
112  if (srv.endsWith(parent)) return true;
113  else return false;
114}
115
116function parseHandler(address, options, callback) {
117  let result, err;
118  try {
119    result = parseConnectionString(address, options);
120  } catch (e) {
121    err = e;
122  }
123
124  return err ? callback(err, null) : callback(null, result);
125}
126
127function parseConnectionString(url, options) {
128  // Variables
129  let connection_part = '';
130  let auth_part = '';
131  let query_string_part = '';
132  let dbName = 'admin';
133
134  // Url parser result
135  let result = parser.parse(url, true);
136  if ((result.hostname == null || result.hostname === '') && url.indexOf('.sock') === -1) {
137    throw new Error('No hostname or hostnames provided in connection string');
138  }
139
140  if (result.port === '0') {
141    throw new Error('Invalid port (zero) with hostname');
142  }
143
144  if (!isNaN(parseInt(result.port, 10)) && parseInt(result.port, 10) > 65535) {
145    throw new Error('Invalid port (larger than 65535) with hostname');
146  }
147
148  if (
149    result.path &&
150    result.path.length > 0 &&
151    result.path[0] !== '/' &&
152    url.indexOf('.sock') === -1
153  ) {
154    throw new Error('Missing delimiting slash between hosts and options');
155  }
156
157  if (result.query) {
158    for (let name in result.query) {
159      if (name.indexOf('::') !== -1) {
160        throw new Error('Double colon in host identifier');
161      }
162
163      if (result.query[name] === '') {
164        throw new Error('Query parameter ' + name + ' is an incomplete value pair');
165      }
166    }
167  }
168
169  if (result.auth) {
170    let parts = result.auth.split(':');
171    if (url.indexOf(result.auth) !== -1 && parts.length > 2) {
172      throw new Error('Username with password containing an unescaped colon');
173    }
174
175    if (url.indexOf(result.auth) !== -1 && result.auth.indexOf('@') !== -1) {
176      throw new Error('Username containing an unescaped at-sign');
177    }
178  }
179
180  // Remove query
181  let clean = url.split('?').shift();
182
183  // Extract the list of hosts
184  let strings = clean.split(',');
185  let hosts = [];
186
187  for (let i = 0; i < strings.length; i++) {
188    let hostString = strings[i];
189
190    if (hostString.indexOf('mongodb') !== -1) {
191      if (hostString.indexOf('@') !== -1) {
192        hosts.push(hostString.split('@').pop());
193      } else {
194        hosts.push(hostString.substr('mongodb://'.length));
195      }
196    } else if (hostString.indexOf('/') !== -1) {
197      hosts.push(hostString.split('/').shift());
198    } else if (hostString.indexOf('/') === -1) {
199      hosts.push(hostString.trim());
200    }
201  }
202
203  for (let i = 0; i < hosts.length; i++) {
204    let r = parser.parse(f('mongodb://%s', hosts[i].trim()));
205    if (r.path && r.path.indexOf(':') !== -1) {
206      // Not connecting to a socket so check for an extra slash in the hostname.
207      // Using String#split as perf is better than match.
208      if (r.path.split('/').length > 1 && r.path.indexOf('::') === -1) {
209        throw new Error('Slash in host identifier');
210      } else {
211        throw new Error('Double colon in host identifier');
212      }
213    }
214  }
215
216  // If we have a ? mark cut the query elements off
217  if (url.indexOf('?') !== -1) {
218    query_string_part = url.substr(url.indexOf('?') + 1);
219    connection_part = url.substring('mongodb://'.length, url.indexOf('?'));
220  } else {
221    connection_part = url.substring('mongodb://'.length);
222  }
223
224  // Check if we have auth params
225  if (connection_part.indexOf('@') !== -1) {
226    auth_part = connection_part.split('@')[0];
227    connection_part = connection_part.split('@')[1];
228  }
229
230  // Check there is not more than one unescaped slash
231  if (connection_part.split('/').length > 2) {
232    throw new Error(
233      "Unsupported host '" +
234        connection_part.split('?')[0] +
235        "', hosts must be URL encoded and contain at most one unencoded slash"
236    );
237  }
238
239  // Check if the connection string has a db
240  if (connection_part.indexOf('.sock') !== -1) {
241    if (connection_part.indexOf('.sock/') !== -1) {
242      dbName = connection_part.split('.sock/')[1];
243      // Check if multiple database names provided, or just an illegal trailing backslash
244      if (dbName.indexOf('/') !== -1) {
245        if (dbName.split('/').length === 2 && dbName.split('/')[1].length === 0) {
246          throw new Error('Illegal trailing backslash after database name');
247        }
248        throw new Error('More than 1 database name in URL');
249      }
250      connection_part = connection_part.split(
251        '/',
252        connection_part.indexOf('.sock') + '.sock'.length
253      );
254    }
255  } else if (connection_part.indexOf('/') !== -1) {
256    // Check if multiple database names provided, or just an illegal trailing backslash
257    if (connection_part.split('/').length > 2) {
258      if (connection_part.split('/')[2].length === 0) {
259        throw new Error('Illegal trailing backslash after database name');
260      }
261      throw new Error('More than 1 database name in URL');
262    }
263    dbName = connection_part.split('/')[1];
264    connection_part = connection_part.split('/')[0];
265  }
266
267  // URI decode the host information
268  connection_part = decodeURIComponent(connection_part);
269
270  // Result object
271  let object = {};
272
273  // Pick apart the authentication part of the string
274  let authPart = auth_part || '';
275  let auth = authPart.split(':', 2);
276
277  // Decode the authentication URI components and verify integrity
278  let user = decodeURIComponent(auth[0]);
279  if (auth[0] !== encodeURIComponent(user)) {
280    throw new Error('Username contains an illegal unescaped character');
281  }
282  auth[0] = user;
283
284  if (auth[1]) {
285    let pass = decodeURIComponent(auth[1]);
286    if (auth[1] !== encodeURIComponent(pass)) {
287      throw new Error('Password contains an illegal unescaped character');
288    }
289    auth[1] = pass;
290  }
291
292  // Add auth to final object if we have 2 elements
293  if (auth.length === 2) object.auth = { user: auth[0], password: auth[1] };
294  // if user provided auth options, use that
295  if (options && options.auth != null) object.auth = options.auth;
296
297  // Variables used for temporary storage
298  let hostPart;
299  let urlOptions;
300  let servers;
301  let compression;
302  let serverOptions = { socketOptions: {} };
303  let dbOptions = { read_preference_tags: [] };
304  let replSetServersOptions = { socketOptions: {} };
305  let mongosOptions = { socketOptions: {} };
306  // Add server options to final object
307  object.server_options = serverOptions;
308  object.db_options = dbOptions;
309  object.rs_options = replSetServersOptions;
310  object.mongos_options = mongosOptions;
311
312  // Let's check if we are using a domain socket
313  if (url.match(/\.sock/)) {
314    // Split out the socket part
315    let domainSocket = url.substring(
316      url.indexOf('mongodb://') + 'mongodb://'.length,
317      url.lastIndexOf('.sock') + '.sock'.length
318    );
319    // Clean out any auth stuff if any
320    if (domainSocket.indexOf('@') !== -1) domainSocket = domainSocket.split('@')[1];
321    domainSocket = decodeURIComponent(domainSocket);
322    servers = [{ domain_socket: domainSocket }];
323  } else {
324    // Split up the db
325    hostPart = connection_part;
326    // Deduplicate servers
327    let deduplicatedServers = {};
328
329    // Parse all server results
330    servers = hostPart
331      .split(',')
332      .map(function(h) {
333        let _host, _port, ipv6match;
334        //check if it matches [IPv6]:port, where the port number is optional
335        if ((ipv6match = /\[([^\]]+)\](?::(.+))?/.exec(h))) {
336          _host = ipv6match[1];
337          _port = parseInt(ipv6match[2], 10) || 27017;
338        } else {
339          //otherwise assume it's IPv4, or plain hostname
340          let hostPort = h.split(':', 2);
341          _host = hostPort[0] || 'localhost';
342          _port = hostPort[1] != null ? parseInt(hostPort[1], 10) : 27017;
343          // Check for localhost?safe=true style case
344          if (_host.indexOf('?') !== -1) _host = _host.split(/\?/)[0];
345        }
346
347        // No entry returned for duplicate servr
348        if (deduplicatedServers[_host + '_' + _port]) return null;
349        deduplicatedServers[_host + '_' + _port] = 1;
350
351        // Return the mapped object
352        return { host: _host, port: _port };
353      })
354      .filter(function(x) {
355        return x != null;
356      });
357  }
358
359  // Get the db name
360  object.dbName = dbName || 'admin';
361  // Split up all the options
362  urlOptions = (query_string_part || '').split(/[&;]/);
363  // Ugh, we have to figure out which options go to which constructor manually.
364  urlOptions.forEach(function(opt) {
365    if (!opt) return;
366    var splitOpt = opt.split('='),
367      name = splitOpt[0],
368      value = splitOpt[1];
369
370    // Options implementations
371    switch (name) {
372      case 'slaveOk':
373      case 'slave_ok':
374        serverOptions.slave_ok = value === 'true';
375        dbOptions.slaveOk = value === 'true';
376        break;
377      case 'maxPoolSize':
378      case 'poolSize':
379        serverOptions.poolSize = parseInt(value, 10);
380        replSetServersOptions.poolSize = parseInt(value, 10);
381        break;
382      case 'appname':
383        object.appname = decodeURIComponent(value);
384        break;
385      case 'autoReconnect':
386      case 'auto_reconnect':
387        serverOptions.auto_reconnect = value === 'true';
388        break;
389      case 'ssl':
390        if (value === 'prefer') {
391          serverOptions.ssl = value;
392          replSetServersOptions.ssl = value;
393          mongosOptions.ssl = value;
394          break;
395        }
396        serverOptions.ssl = value === 'true';
397        replSetServersOptions.ssl = value === 'true';
398        mongosOptions.ssl = value === 'true';
399        break;
400      case 'sslValidate':
401        serverOptions.sslValidate = value === 'true';
402        replSetServersOptions.sslValidate = value === 'true';
403        mongosOptions.sslValidate = value === 'true';
404        break;
405      case 'replicaSet':
406      case 'rs_name':
407        replSetServersOptions.rs_name = value;
408        break;
409      case 'reconnectWait':
410        replSetServersOptions.reconnectWait = parseInt(value, 10);
411        break;
412      case 'retries':
413        replSetServersOptions.retries = parseInt(value, 10);
414        break;
415      case 'readSecondary':
416      case 'read_secondary':
417        replSetServersOptions.read_secondary = value === 'true';
418        break;
419      case 'fsync':
420        dbOptions.fsync = value === 'true';
421        break;
422      case 'journal':
423        dbOptions.j = value === 'true';
424        break;
425      case 'safe':
426        dbOptions.safe = value === 'true';
427        break;
428      case 'nativeParser':
429      case 'native_parser':
430        dbOptions.native_parser = value === 'true';
431        break;
432      case 'readConcernLevel':
433        dbOptions.readConcern = { level: value };
434        break;
435      case 'connectTimeoutMS':
436        serverOptions.socketOptions.connectTimeoutMS = parseInt(value, 10);
437        replSetServersOptions.socketOptions.connectTimeoutMS = parseInt(value, 10);
438        mongosOptions.socketOptions.connectTimeoutMS = parseInt(value, 10);
439        break;
440      case 'socketTimeoutMS':
441        serverOptions.socketOptions.socketTimeoutMS = parseInt(value, 10);
442        replSetServersOptions.socketOptions.socketTimeoutMS = parseInt(value, 10);
443        mongosOptions.socketOptions.socketTimeoutMS = parseInt(value, 10);
444        break;
445      case 'w':
446        dbOptions.w = parseInt(value, 10);
447        if (isNaN(dbOptions.w)) dbOptions.w = value;
448        break;
449      case 'authSource':
450        dbOptions.authSource = value;
451        break;
452      case 'gssapiServiceName':
453        dbOptions.gssapiServiceName = value;
454        break;
455      case 'authMechanism':
456        if (value === 'GSSAPI') {
457          // If no password provided decode only the principal
458          if (object.auth == null) {
459            let urlDecodeAuthPart = decodeURIComponent(authPart);
460            if (urlDecodeAuthPart.indexOf('@') === -1)
461              throw new Error('GSSAPI requires a provided principal');
462            object.auth = { user: urlDecodeAuthPart, password: null };
463          } else {
464            object.auth.user = decodeURIComponent(object.auth.user);
465          }
466        } else if (value === 'MONGODB-X509') {
467          object.auth = { user: decodeURIComponent(authPart) };
468        }
469
470        // Only support GSSAPI or MONGODB-CR for now
471        if (
472          value !== 'GSSAPI' &&
473          value !== 'MONGODB-X509' &&
474          value !== 'MONGODB-CR' &&
475          value !== 'DEFAULT' &&
476          value !== 'SCRAM-SHA-1' &&
477          value !== 'PLAIN'
478        )
479          throw new Error(
480            'Only DEFAULT, GSSAPI, PLAIN, MONGODB-X509, SCRAM-SHA-1 or MONGODB-CR is supported by authMechanism'
481          );
482
483        // Authentication mechanism
484        dbOptions.authMechanism = value;
485        break;
486      case 'authMechanismProperties':
487        {
488          // Split up into key, value pairs
489          let values = value.split(',');
490          let o = {};
491          // For each value split into key, value
492          values.forEach(function(x) {
493            let v = x.split(':');
494            o[v[0]] = v[1];
495          });
496
497          // Set all authMechanismProperties
498          dbOptions.authMechanismProperties = o;
499          // Set the service name value
500          if (typeof o.SERVICE_NAME === 'string') dbOptions.gssapiServiceName = o.SERVICE_NAME;
501          if (typeof o.SERVICE_REALM === 'string') dbOptions.gssapiServiceRealm = o.SERVICE_REALM;
502          if (typeof o.CANONICALIZE_HOST_NAME === 'string')
503            dbOptions.gssapiCanonicalizeHostName =
504              o.CANONICALIZE_HOST_NAME === 'true' ? true : false;
505        }
506        break;
507      case 'wtimeoutMS':
508        dbOptions.wtimeout = parseInt(value, 10);
509        break;
510      case 'readPreference':
511        if (!ReadPreference.isValid(value))
512          throw new Error(
513            'readPreference must be either primary/primaryPreferred/secondary/secondaryPreferred/nearest'
514          );
515        dbOptions.readPreference = value;
516        break;
517      case 'maxStalenessSeconds':
518        dbOptions.maxStalenessSeconds = parseInt(value, 10);
519        break;
520      case 'readPreferenceTags':
521        {
522          // Decode the value
523          value = decodeURIComponent(value);
524          // Contains the tag object
525          let tagObject = {};
526          if (value == null || value === '') {
527            dbOptions.read_preference_tags.push(tagObject);
528            break;
529          }
530
531          // Split up the tags
532          let tags = value.split(/,/);
533          for (let i = 0; i < tags.length; i++) {
534            let parts = tags[i].trim().split(/:/);
535            tagObject[parts[0]] = parts[1];
536          }
537
538          // Set the preferences tags
539          dbOptions.read_preference_tags.push(tagObject);
540        }
541        break;
542      case 'compressors':
543        {
544          compression = serverOptions.compression || {};
545          let compressors = value.split(',');
546          if (
547            !compressors.every(function(compressor) {
548              return compressor === 'snappy' || compressor === 'zlib';
549            })
550          ) {
551            throw new Error('Compressors must be at least one of snappy or zlib');
552          }
553
554          compression.compressors = compressors;
555          serverOptions.compression = compression;
556        }
557        break;
558      case 'zlibCompressionLevel':
559        {
560          compression = serverOptions.compression || {};
561          let zlibCompressionLevel = parseInt(value, 10);
562          if (zlibCompressionLevel < -1 || zlibCompressionLevel > 9) {
563            throw new Error('zlibCompressionLevel must be an integer between -1 and 9');
564          }
565
566          compression.zlibCompressionLevel = zlibCompressionLevel;
567          serverOptions.compression = compression;
568        }
569        break;
570      case 'retryWrites':
571        dbOptions.retryWrites = value === 'true';
572        break;
573      case 'minSize':
574        dbOptions.minSize = parseInt(value, 10);
575        break;
576      default:
577        {
578          let logger = Logger('URL Parser');
579          logger.warn(`${name} is not supported as a connection string option`);
580        }
581        break;
582    }
583  });
584
585  // No tags: should be null (not [])
586  if (dbOptions.read_preference_tags.length === 0) {
587    dbOptions.read_preference_tags = null;
588  }
589
590  // Validate if there are an invalid write concern combinations
591  if (
592    (dbOptions.w === -1 || dbOptions.w === 0) &&
593    (dbOptions.journal === true || dbOptions.fsync === true || dbOptions.safe === true)
594  )
595    throw new Error('w set to -1 or 0 cannot be combined with safe/w/journal/fsync');
596
597  // If no read preference set it to primary
598  if (!dbOptions.readPreference) {
599    dbOptions.readPreference = 'primary';
600  }
601
602  // make sure that user-provided options are applied with priority
603  dbOptions = Object.assign(dbOptions, options);
604
605  // Add servers to result
606  object.servers = servers;
607
608  // Returned parsed object
609  return object;
610}