url_parser.js 20 KB

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