Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

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