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