connection_string.js 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.FEATURE_FLAGS = exports.DEFAULT_OPTIONS = exports.OPTIONS = exports.parseOptions = exports.resolveSRVRecord = void 0;
  4. const dns = require("dns");
  5. const fs = require("fs");
  6. const mongodb_connection_string_url_1 = require("mongodb-connection-string-url");
  7. const url_1 = require("url");
  8. const mongo_credentials_1 = require("./cmap/auth/mongo_credentials");
  9. const providers_1 = require("./cmap/auth/providers");
  10. const client_metadata_1 = require("./cmap/handshake/client_metadata");
  11. const compression_1 = require("./cmap/wire_protocol/compression");
  12. const encrypter_1 = require("./encrypter");
  13. const error_1 = require("./error");
  14. const mongo_client_1 = require("./mongo_client");
  15. const mongo_logger_1 = require("./mongo_logger");
  16. const read_concern_1 = require("./read_concern");
  17. const read_preference_1 = require("./read_preference");
  18. const utils_1 = require("./utils");
  19. const write_concern_1 = require("./write_concern");
  20. const VALID_TXT_RECORDS = ['authSource', 'replicaSet', 'loadBalanced'];
  21. const LB_SINGLE_HOST_ERROR = 'loadBalanced option only supported with a single host in the URI';
  22. const LB_REPLICA_SET_ERROR = 'loadBalanced option not supported with a replicaSet option';
  23. const LB_DIRECT_CONNECTION_ERROR = 'loadBalanced option not supported when directConnection is provided';
  24. /**
  25. * Lookup a `mongodb+srv` connection string, combine the parts and reparse it as a normal
  26. * connection string.
  27. *
  28. * @param uri - The connection string to parse
  29. * @param options - Optional user provided connection string options
  30. */
  31. async function resolveSRVRecord(options) {
  32. if (typeof options.srvHost !== 'string') {
  33. throw new error_1.MongoAPIError('Option "srvHost" must not be empty');
  34. }
  35. if (options.srvHost.split('.').length < 3) {
  36. // TODO(NODE-3484): Replace with MongoConnectionStringError
  37. throw new error_1.MongoAPIError('URI must include hostname, domain name, and tld');
  38. }
  39. // Resolve the SRV record and use the result as the list of hosts to connect to.
  40. const lookupAddress = options.srvHost;
  41. const addresses = await dns.promises.resolveSrv(`_${options.srvServiceName}._tcp.${lookupAddress}`);
  42. if (addresses.length === 0) {
  43. throw new error_1.MongoAPIError('No addresses found at host');
  44. }
  45. for (const { name } of addresses) {
  46. if (!(0, utils_1.matchesParentDomain)(name, lookupAddress)) {
  47. throw new error_1.MongoAPIError('Server record does not share hostname with parent URI');
  48. }
  49. }
  50. const hostAddresses = addresses.map(r => utils_1.HostAddress.fromString(`${r.name}:${r.port ?? 27017}`));
  51. validateLoadBalancedOptions(hostAddresses, options, true);
  52. // Resolve TXT record and add options from there if they exist.
  53. let record;
  54. try {
  55. record = await dns.promises.resolveTxt(lookupAddress);
  56. }
  57. catch (error) {
  58. if (error.code !== 'ENODATA' && error.code !== 'ENOTFOUND') {
  59. throw error;
  60. }
  61. return hostAddresses;
  62. }
  63. if (record.length > 1) {
  64. throw new error_1.MongoParseError('Multiple text records not allowed');
  65. }
  66. const txtRecordOptions = new url_1.URLSearchParams(record[0].join(''));
  67. const txtRecordOptionKeys = [...txtRecordOptions.keys()];
  68. if (txtRecordOptionKeys.some(key => !VALID_TXT_RECORDS.includes(key))) {
  69. throw new error_1.MongoParseError(`Text record may only set any of: ${VALID_TXT_RECORDS.join(', ')}`);
  70. }
  71. if (VALID_TXT_RECORDS.some(option => txtRecordOptions.get(option) === '')) {
  72. throw new error_1.MongoParseError('Cannot have empty URI params in DNS TXT Record');
  73. }
  74. const source = txtRecordOptions.get('authSource') ?? undefined;
  75. const replicaSet = txtRecordOptions.get('replicaSet') ?? undefined;
  76. const loadBalanced = txtRecordOptions.get('loadBalanced') ?? undefined;
  77. if (!options.userSpecifiedAuthSource &&
  78. source &&
  79. options.credentials &&
  80. !providers_1.AUTH_MECHS_AUTH_SRC_EXTERNAL.has(options.credentials.mechanism)) {
  81. options.credentials = mongo_credentials_1.MongoCredentials.merge(options.credentials, { source });
  82. }
  83. if (!options.userSpecifiedReplicaSet && replicaSet) {
  84. options.replicaSet = replicaSet;
  85. }
  86. if (loadBalanced === 'true') {
  87. options.loadBalanced = true;
  88. }
  89. if (options.replicaSet && options.srvMaxHosts > 0) {
  90. throw new error_1.MongoParseError('Cannot combine replicaSet option with srvMaxHosts');
  91. }
  92. validateLoadBalancedOptions(hostAddresses, options, true);
  93. return hostAddresses;
  94. }
  95. exports.resolveSRVRecord = resolveSRVRecord;
  96. /**
  97. * Checks if TLS options are valid
  98. *
  99. * @param allOptions - All options provided by user or included in default options map
  100. * @throws MongoAPIError if TLS options are invalid
  101. */
  102. function checkTLSOptions(allOptions) {
  103. if (!allOptions)
  104. return;
  105. const check = (a, b) => {
  106. if (allOptions.has(a) && allOptions.has(b)) {
  107. throw new error_1.MongoAPIError(`The '${a}' option cannot be used with the '${b}' option`);
  108. }
  109. };
  110. check('tlsInsecure', 'tlsAllowInvalidCertificates');
  111. check('tlsInsecure', 'tlsAllowInvalidHostnames');
  112. check('tlsInsecure', 'tlsDisableCertificateRevocationCheck');
  113. check('tlsInsecure', 'tlsDisableOCSPEndpointCheck');
  114. check('tlsAllowInvalidCertificates', 'tlsDisableCertificateRevocationCheck');
  115. check('tlsAllowInvalidCertificates', 'tlsDisableOCSPEndpointCheck');
  116. check('tlsDisableCertificateRevocationCheck', 'tlsDisableOCSPEndpointCheck');
  117. }
  118. const TRUTHS = new Set(['true', 't', '1', 'y', 'yes']);
  119. const FALSEHOODS = new Set(['false', 'f', '0', 'n', 'no', '-1']);
  120. function getBoolean(name, value) {
  121. if (typeof value === 'boolean')
  122. return value;
  123. const valueString = String(value).toLowerCase();
  124. if (TRUTHS.has(valueString)) {
  125. if (valueString !== 'true') {
  126. (0, utils_1.emitWarningOnce)(`deprecated value for ${name} : ${valueString} - please update to ${name} : true instead`);
  127. }
  128. return true;
  129. }
  130. if (FALSEHOODS.has(valueString)) {
  131. if (valueString !== 'false') {
  132. (0, utils_1.emitWarningOnce)(`deprecated value for ${name} : ${valueString} - please update to ${name} : false instead`);
  133. }
  134. return false;
  135. }
  136. throw new error_1.MongoParseError(`Expected ${name} to be stringified boolean value, got: ${value}`);
  137. }
  138. function getIntFromOptions(name, value) {
  139. const parsedInt = (0, utils_1.parseInteger)(value);
  140. if (parsedInt != null) {
  141. return parsedInt;
  142. }
  143. throw new error_1.MongoParseError(`Expected ${name} to be stringified int value, got: ${value}`);
  144. }
  145. function getUIntFromOptions(name, value) {
  146. const parsedValue = getIntFromOptions(name, value);
  147. if (parsedValue < 0) {
  148. throw new error_1.MongoParseError(`${name} can only be a positive int value, got: ${value}`);
  149. }
  150. return parsedValue;
  151. }
  152. function* entriesFromString(value) {
  153. const keyValuePairs = value.split(',');
  154. for (const keyValue of keyValuePairs) {
  155. const [key, value] = keyValue.split(/:(.*)/);
  156. if (value == null) {
  157. throw new error_1.MongoParseError('Cannot have undefined values in key value pairs');
  158. }
  159. yield [key, value];
  160. }
  161. }
  162. class CaseInsensitiveMap extends Map {
  163. constructor(entries = []) {
  164. super(entries.map(([k, v]) => [k.toLowerCase(), v]));
  165. }
  166. has(k) {
  167. return super.has(k.toLowerCase());
  168. }
  169. get(k) {
  170. return super.get(k.toLowerCase());
  171. }
  172. set(k, v) {
  173. return super.set(k.toLowerCase(), v);
  174. }
  175. delete(k) {
  176. return super.delete(k.toLowerCase());
  177. }
  178. }
  179. function parseOptions(uri, mongoClient = undefined, options = {}) {
  180. if (mongoClient != null && !(mongoClient instanceof mongo_client_1.MongoClient)) {
  181. options = mongoClient;
  182. mongoClient = undefined;
  183. }
  184. // validate BSONOptions
  185. if (options.useBigInt64 && typeof options.promoteLongs === 'boolean' && !options.promoteLongs) {
  186. throw new error_1.MongoAPIError('Must request either bigint or Long for int64 deserialization');
  187. }
  188. if (options.useBigInt64 && typeof options.promoteValues === 'boolean' && !options.promoteValues) {
  189. throw new error_1.MongoAPIError('Must request either bigint or Long for int64 deserialization');
  190. }
  191. const url = new mongodb_connection_string_url_1.default(uri);
  192. const { hosts, isSRV } = url;
  193. const mongoOptions = Object.create(null);
  194. // Feature flags
  195. for (const flag of Object.getOwnPropertySymbols(options)) {
  196. if (exports.FEATURE_FLAGS.has(flag)) {
  197. mongoOptions[flag] = options[flag];
  198. }
  199. }
  200. mongoOptions.hosts = isSRV ? [] : hosts.map(utils_1.HostAddress.fromString);
  201. const urlOptions = new CaseInsensitiveMap();
  202. if (url.pathname !== '/' && url.pathname !== '') {
  203. const dbName = decodeURIComponent(url.pathname[0] === '/' ? url.pathname.slice(1) : url.pathname);
  204. if (dbName) {
  205. urlOptions.set('dbName', [dbName]);
  206. }
  207. }
  208. if (url.username !== '') {
  209. const auth = {
  210. username: decodeURIComponent(url.username)
  211. };
  212. if (typeof url.password === 'string') {
  213. auth.password = decodeURIComponent(url.password);
  214. }
  215. urlOptions.set('auth', [auth]);
  216. }
  217. for (const key of url.searchParams.keys()) {
  218. const values = [...url.searchParams.getAll(key)];
  219. if (values.includes('')) {
  220. throw new error_1.MongoAPIError('URI cannot contain options with no value');
  221. }
  222. if (!urlOptions.has(key)) {
  223. urlOptions.set(key, values);
  224. }
  225. }
  226. const objectOptions = new CaseInsensitiveMap(Object.entries(options).filter(([, v]) => v != null));
  227. // Validate options that can only be provided by one of uri or object
  228. if (urlOptions.has('serverApi')) {
  229. throw new error_1.MongoParseError('URI cannot contain `serverApi`, it can only be passed to the client');
  230. }
  231. const uriMechanismProperties = urlOptions.get('authMechanismProperties');
  232. if (uriMechanismProperties) {
  233. for (const property of uriMechanismProperties) {
  234. if (/(^|,)ALLOWED_HOSTS:/.test(property)) {
  235. throw new error_1.MongoParseError('Auth mechanism property ALLOWED_HOSTS is not allowed in the connection string.');
  236. }
  237. }
  238. }
  239. if (objectOptions.has('loadBalanced')) {
  240. throw new error_1.MongoParseError('loadBalanced is only a valid option in the URI');
  241. }
  242. // All option collection
  243. const allProvidedOptions = new CaseInsensitiveMap();
  244. const allProvidedKeys = new Set([...urlOptions.keys(), ...objectOptions.keys()]);
  245. for (const key of allProvidedKeys) {
  246. const values = [];
  247. const objectOptionValue = objectOptions.get(key);
  248. if (objectOptionValue != null) {
  249. values.push(objectOptionValue);
  250. }
  251. const urlValues = urlOptions.get(key) ?? [];
  252. values.push(...urlValues);
  253. allProvidedOptions.set(key, values);
  254. }
  255. const didMapTLSCertificateFile = allProvidedOptions.has('tlsCertificateKeyFile') &&
  256. !allProvidedOptions.has('tlsCertificateFile');
  257. if (didMapTLSCertificateFile) {
  258. allProvidedOptions.set('tlsCertificateFile', allProvidedOptions.get('tlsCertificateKeyFile'));
  259. }
  260. if (allProvidedOptions.has('tls') || allProvidedOptions.has('ssl')) {
  261. const tlsAndSslOpts = (allProvidedOptions.get('tls') || [])
  262. .concat(allProvidedOptions.get('ssl') || [])
  263. .map(getBoolean.bind(null, 'tls/ssl'));
  264. if (new Set(tlsAndSslOpts).size !== 1) {
  265. throw new error_1.MongoParseError('All values of tls/ssl must be the same.');
  266. }
  267. }
  268. checkTLSOptions(allProvidedOptions);
  269. const unsupportedOptions = (0, utils_1.setDifference)(allProvidedKeys, Array.from(Object.keys(exports.OPTIONS)).map(s => s.toLowerCase()));
  270. if (unsupportedOptions.size !== 0) {
  271. const optionWord = unsupportedOptions.size > 1 ? 'options' : 'option';
  272. const isOrAre = unsupportedOptions.size > 1 ? 'are' : 'is';
  273. throw new error_1.MongoParseError(`${optionWord} ${Array.from(unsupportedOptions).join(', ')} ${isOrAre} not supported`);
  274. }
  275. // Option parsing and setting
  276. for (const [key, descriptor] of Object.entries(exports.OPTIONS)) {
  277. const values = allProvidedOptions.get(key);
  278. if (!values || values.length === 0) {
  279. if (exports.DEFAULT_OPTIONS.has(key)) {
  280. setOption(mongoOptions, key, descriptor, [exports.DEFAULT_OPTIONS.get(key)]);
  281. }
  282. }
  283. else {
  284. const { deprecated } = descriptor;
  285. const shouldEmitTLSCertificateFileDeprecation = didMapTLSCertificateFile && key === 'tlsCertificateFile';
  286. if (deprecated && !shouldEmitTLSCertificateFileDeprecation) {
  287. const deprecatedMsg = typeof deprecated === 'string' ? `: ${deprecated}` : '';
  288. (0, utils_1.emitWarning)(`${key} is a deprecated option${deprecatedMsg}`);
  289. }
  290. setOption(mongoOptions, key, descriptor, values);
  291. }
  292. }
  293. if (mongoOptions.credentials) {
  294. const isGssapi = mongoOptions.credentials.mechanism === providers_1.AuthMechanism.MONGODB_GSSAPI;
  295. const isX509 = mongoOptions.credentials.mechanism === providers_1.AuthMechanism.MONGODB_X509;
  296. const isAws = mongoOptions.credentials.mechanism === providers_1.AuthMechanism.MONGODB_AWS;
  297. const isOidc = mongoOptions.credentials.mechanism === providers_1.AuthMechanism.MONGODB_OIDC;
  298. if ((isGssapi || isX509) &&
  299. allProvidedOptions.has('authSource') &&
  300. mongoOptions.credentials.source !== '$external') {
  301. // If authSource was explicitly given and its incorrect, we error
  302. throw new error_1.MongoParseError(`authMechanism ${mongoOptions.credentials.mechanism} requires an authSource of '$external'`);
  303. }
  304. if (!(isGssapi || isX509 || isAws || isOidc) &&
  305. mongoOptions.dbName &&
  306. !allProvidedOptions.has('authSource')) {
  307. // inherit the dbName unless GSSAPI or X509, then silently ignore dbName
  308. // and there was no specific authSource given
  309. mongoOptions.credentials = mongo_credentials_1.MongoCredentials.merge(mongoOptions.credentials, {
  310. source: mongoOptions.dbName
  311. });
  312. }
  313. if (isAws && mongoOptions.credentials.username && !mongoOptions.credentials.password) {
  314. throw new error_1.MongoMissingCredentialsError(`When using ${mongoOptions.credentials.mechanism} password must be set when a username is specified`);
  315. }
  316. mongoOptions.credentials.validate();
  317. // Check if the only auth related option provided was authSource, if so we can remove credentials
  318. if (mongoOptions.credentials.password === '' &&
  319. mongoOptions.credentials.username === '' &&
  320. mongoOptions.credentials.mechanism === providers_1.AuthMechanism.MONGODB_DEFAULT &&
  321. Object.keys(mongoOptions.credentials.mechanismProperties).length === 0) {
  322. delete mongoOptions.credentials;
  323. }
  324. }
  325. if (!mongoOptions.dbName) {
  326. // dbName default is applied here because of the credential validation above
  327. mongoOptions.dbName = 'test';
  328. }
  329. validateLoadBalancedOptions(hosts, mongoOptions, isSRV);
  330. if (mongoClient && mongoOptions.autoEncryption) {
  331. encrypter_1.Encrypter.checkForMongoCrypt();
  332. mongoOptions.encrypter = new encrypter_1.Encrypter(mongoClient, uri, options);
  333. mongoOptions.autoEncrypter = mongoOptions.encrypter.autoEncrypter;
  334. }
  335. // Potential SRV Overrides and SRV connection string validations
  336. mongoOptions.userSpecifiedAuthSource =
  337. objectOptions.has('authSource') || urlOptions.has('authSource');
  338. mongoOptions.userSpecifiedReplicaSet =
  339. objectOptions.has('replicaSet') || urlOptions.has('replicaSet');
  340. if (isSRV) {
  341. // SRV Record is resolved upon connecting
  342. mongoOptions.srvHost = hosts[0];
  343. if (mongoOptions.directConnection) {
  344. throw new error_1.MongoAPIError('SRV URI does not support directConnection');
  345. }
  346. if (mongoOptions.srvMaxHosts > 0 && typeof mongoOptions.replicaSet === 'string') {
  347. throw new error_1.MongoParseError('Cannot use srvMaxHosts option with replicaSet');
  348. }
  349. // SRV turns on TLS by default, but users can override and turn it off
  350. const noUserSpecifiedTLS = !objectOptions.has('tls') && !urlOptions.has('tls');
  351. const noUserSpecifiedSSL = !objectOptions.has('ssl') && !urlOptions.has('ssl');
  352. if (noUserSpecifiedTLS && noUserSpecifiedSSL) {
  353. mongoOptions.tls = true;
  354. }
  355. }
  356. else {
  357. const userSpecifiedSrvOptions = urlOptions.has('srvMaxHosts') ||
  358. objectOptions.has('srvMaxHosts') ||
  359. urlOptions.has('srvServiceName') ||
  360. objectOptions.has('srvServiceName');
  361. if (userSpecifiedSrvOptions) {
  362. throw new error_1.MongoParseError('Cannot use srvMaxHosts or srvServiceName with a non-srv connection string');
  363. }
  364. }
  365. if (mongoOptions.directConnection && mongoOptions.hosts.length !== 1) {
  366. throw new error_1.MongoParseError('directConnection option requires exactly one host');
  367. }
  368. if (!mongoOptions.proxyHost &&
  369. (mongoOptions.proxyPort || mongoOptions.proxyUsername || mongoOptions.proxyPassword)) {
  370. throw new error_1.MongoParseError('Must specify proxyHost if other proxy options are passed');
  371. }
  372. if ((mongoOptions.proxyUsername && !mongoOptions.proxyPassword) ||
  373. (!mongoOptions.proxyUsername && mongoOptions.proxyPassword)) {
  374. throw new error_1.MongoParseError('Can only specify both of proxy username/password or neither');
  375. }
  376. const proxyOptions = ['proxyHost', 'proxyPort', 'proxyUsername', 'proxyPassword'].map(key => urlOptions.get(key) ?? []);
  377. if (proxyOptions.some(options => options.length > 1)) {
  378. throw new error_1.MongoParseError('Proxy options cannot be specified multiple times in the connection string');
  379. }
  380. const loggerFeatureFlag = Symbol.for('@@mdb.enableMongoLogger');
  381. mongoOptions[loggerFeatureFlag] = mongoOptions[loggerFeatureFlag] ?? false;
  382. let loggerEnvOptions = {};
  383. let loggerClientOptions = {};
  384. if (mongoOptions[loggerFeatureFlag]) {
  385. loggerEnvOptions = {
  386. MONGODB_LOG_COMMAND: process.env.MONGODB_LOG_COMMAND,
  387. MONGODB_LOG_TOPOLOGY: process.env.MONGODB_LOG_TOPOLOGY,
  388. MONGODB_LOG_SERVER_SELECTION: process.env.MONGODB_LOG_SERVER_SELECTION,
  389. MONGODB_LOG_CONNECTION: process.env.MONGODB_LOG_CONNECTION,
  390. MONGODB_LOG_ALL: process.env.MONGODB_LOG_ALL,
  391. MONGODB_LOG_MAX_DOCUMENT_LENGTH: process.env.MONGODB_LOG_MAX_DOCUMENT_LENGTH,
  392. MONGODB_LOG_PATH: process.env.MONGODB_LOG_PATH,
  393. ...mongoOptions[Symbol.for('@@mdb.internalLoggerConfig')]
  394. };
  395. loggerClientOptions = {
  396. mongodbLogPath: mongoOptions.mongodbLogPath
  397. };
  398. }
  399. mongoOptions.mongoLoggerOptions = mongo_logger_1.MongoLogger.resolveOptions(loggerEnvOptions, loggerClientOptions);
  400. mongoOptions.metadata = (0, client_metadata_1.makeClientMetadata)(mongoOptions);
  401. return mongoOptions;
  402. }
  403. exports.parseOptions = parseOptions;
  404. /**
  405. * #### Throws if LB mode is true:
  406. * - hosts contains more than one host
  407. * - there is a replicaSet name set
  408. * - directConnection is set
  409. * - if srvMaxHosts is used when an srv connection string is passed in
  410. *
  411. * @throws MongoParseError
  412. */
  413. function validateLoadBalancedOptions(hosts, mongoOptions, isSrv) {
  414. if (mongoOptions.loadBalanced) {
  415. if (hosts.length > 1) {
  416. throw new error_1.MongoParseError(LB_SINGLE_HOST_ERROR);
  417. }
  418. if (mongoOptions.replicaSet) {
  419. throw new error_1.MongoParseError(LB_REPLICA_SET_ERROR);
  420. }
  421. if (mongoOptions.directConnection) {
  422. throw new error_1.MongoParseError(LB_DIRECT_CONNECTION_ERROR);
  423. }
  424. if (isSrv && mongoOptions.srvMaxHosts > 0) {
  425. throw new error_1.MongoParseError('Cannot limit srv hosts with loadBalanced enabled');
  426. }
  427. }
  428. return;
  429. }
  430. function setOption(mongoOptions, key, descriptor, values) {
  431. const { target, type, transform } = descriptor;
  432. const name = target ?? key;
  433. switch (type) {
  434. case 'boolean':
  435. mongoOptions[name] = getBoolean(name, values[0]);
  436. break;
  437. case 'int':
  438. mongoOptions[name] = getIntFromOptions(name, values[0]);
  439. break;
  440. case 'uint':
  441. mongoOptions[name] = getUIntFromOptions(name, values[0]);
  442. break;
  443. case 'string':
  444. if (values[0] == null) {
  445. break;
  446. }
  447. mongoOptions[name] = String(values[0]);
  448. break;
  449. case 'record':
  450. if (!(0, utils_1.isRecord)(values[0])) {
  451. throw new error_1.MongoParseError(`${name} must be an object`);
  452. }
  453. mongoOptions[name] = values[0];
  454. break;
  455. case 'any':
  456. mongoOptions[name] = values[0];
  457. break;
  458. default: {
  459. if (!transform) {
  460. throw new error_1.MongoParseError('Descriptors missing a type must define a transform');
  461. }
  462. const transformValue = transform({ name, options: mongoOptions, values });
  463. mongoOptions[name] = transformValue;
  464. break;
  465. }
  466. }
  467. }
  468. exports.OPTIONS = {
  469. appName: {
  470. type: 'string'
  471. },
  472. auth: {
  473. target: 'credentials',
  474. transform({ name, options, values: [value] }) {
  475. if (!(0, utils_1.isRecord)(value, ['username', 'password'])) {
  476. throw new error_1.MongoParseError(`${name} must be an object with 'username' and 'password' properties`);
  477. }
  478. return mongo_credentials_1.MongoCredentials.merge(options.credentials, {
  479. username: value.username,
  480. password: value.password
  481. });
  482. }
  483. },
  484. authMechanism: {
  485. target: 'credentials',
  486. transform({ options, values: [value] }) {
  487. const mechanisms = Object.values(providers_1.AuthMechanism);
  488. const [mechanism] = mechanisms.filter(m => m.match(RegExp(String.raw `\b${value}\b`, 'i')));
  489. if (!mechanism) {
  490. throw new error_1.MongoParseError(`authMechanism one of ${mechanisms}, got ${value}`);
  491. }
  492. let source = options.credentials?.source;
  493. if (mechanism === providers_1.AuthMechanism.MONGODB_PLAIN ||
  494. providers_1.AUTH_MECHS_AUTH_SRC_EXTERNAL.has(mechanism)) {
  495. // some mechanisms have '$external' as the Auth Source
  496. source = '$external';
  497. }
  498. let password = options.credentials?.password;
  499. if (mechanism === providers_1.AuthMechanism.MONGODB_X509 && password === '') {
  500. password = undefined;
  501. }
  502. return mongo_credentials_1.MongoCredentials.merge(options.credentials, {
  503. mechanism,
  504. source,
  505. password
  506. });
  507. }
  508. },
  509. authMechanismProperties: {
  510. target: 'credentials',
  511. transform({ options, values }) {
  512. // We can have a combination of options passed in the URI and options passed
  513. // as an object to the MongoClient. So we must transform the string options
  514. // as well as merge them together with a potentially provided object.
  515. let mechanismProperties = Object.create(null);
  516. for (const optionValue of values) {
  517. if (typeof optionValue === 'string') {
  518. for (const [key, value] of entriesFromString(optionValue)) {
  519. try {
  520. mechanismProperties[key] = getBoolean(key, value);
  521. }
  522. catch {
  523. mechanismProperties[key] = value;
  524. }
  525. }
  526. }
  527. else {
  528. if (!(0, utils_1.isRecord)(optionValue)) {
  529. throw new error_1.MongoParseError('AuthMechanismProperties must be an object');
  530. }
  531. mechanismProperties = { ...optionValue };
  532. }
  533. }
  534. return mongo_credentials_1.MongoCredentials.merge(options.credentials, {
  535. mechanismProperties
  536. });
  537. }
  538. },
  539. authSource: {
  540. target: 'credentials',
  541. transform({ options, values: [value] }) {
  542. const source = String(value);
  543. return mongo_credentials_1.MongoCredentials.merge(options.credentials, { source });
  544. }
  545. },
  546. autoEncryption: {
  547. type: 'record'
  548. },
  549. bsonRegExp: {
  550. type: 'boolean'
  551. },
  552. serverApi: {
  553. target: 'serverApi',
  554. transform({ values: [version] }) {
  555. const serverApiToValidate = typeof version === 'string' ? { version } : version;
  556. const versionToValidate = serverApiToValidate && serverApiToValidate.version;
  557. if (!versionToValidate) {
  558. throw new error_1.MongoParseError(`Invalid \`serverApi\` property; must specify a version from the following enum: ["${Object.values(mongo_client_1.ServerApiVersion).join('", "')}"]`);
  559. }
  560. if (!Object.values(mongo_client_1.ServerApiVersion).some(v => v === versionToValidate)) {
  561. throw new error_1.MongoParseError(`Invalid server API version=${versionToValidate}; must be in the following enum: ["${Object.values(mongo_client_1.ServerApiVersion).join('", "')}"]`);
  562. }
  563. return serverApiToValidate;
  564. }
  565. },
  566. checkKeys: {
  567. type: 'boolean'
  568. },
  569. compressors: {
  570. default: 'none',
  571. target: 'compressors',
  572. transform({ values }) {
  573. const compressionList = new Set();
  574. for (const compVal of values) {
  575. const compValArray = typeof compVal === 'string' ? compVal.split(',') : compVal;
  576. if (!Array.isArray(compValArray)) {
  577. throw new error_1.MongoInvalidArgumentError('compressors must be an array or a comma-delimited list of strings');
  578. }
  579. for (const c of compValArray) {
  580. if (Object.keys(compression_1.Compressor).includes(String(c))) {
  581. compressionList.add(String(c));
  582. }
  583. else {
  584. throw new error_1.MongoInvalidArgumentError(`${c} is not a valid compression mechanism. Must be one of: ${Object.keys(compression_1.Compressor)}.`);
  585. }
  586. }
  587. }
  588. return [...compressionList];
  589. }
  590. },
  591. connectTimeoutMS: {
  592. default: 30000,
  593. type: 'uint'
  594. },
  595. dbName: {
  596. type: 'string'
  597. },
  598. directConnection: {
  599. default: false,
  600. type: 'boolean'
  601. },
  602. driverInfo: {
  603. default: {},
  604. type: 'record'
  605. },
  606. enableUtf8Validation: { type: 'boolean', default: true },
  607. family: {
  608. transform({ name, values: [value] }) {
  609. const transformValue = getIntFromOptions(name, value);
  610. if (transformValue === 4 || transformValue === 6) {
  611. return transformValue;
  612. }
  613. throw new error_1.MongoParseError(`Option 'family' must be 4 or 6 got ${transformValue}.`);
  614. }
  615. },
  616. fieldsAsRaw: {
  617. type: 'record'
  618. },
  619. forceServerObjectId: {
  620. default: false,
  621. type: 'boolean'
  622. },
  623. fsync: {
  624. deprecated: 'Please use journal instead',
  625. target: 'writeConcern',
  626. transform({ name, options, values: [value] }) {
  627. const wc = write_concern_1.WriteConcern.fromOptions({
  628. writeConcern: {
  629. ...options.writeConcern,
  630. fsync: getBoolean(name, value)
  631. }
  632. });
  633. if (!wc)
  634. throw new error_1.MongoParseError(`Unable to make a writeConcern from fsync=${value}`);
  635. return wc;
  636. }
  637. },
  638. heartbeatFrequencyMS: {
  639. default: 10000,
  640. type: 'uint'
  641. },
  642. ignoreUndefined: {
  643. type: 'boolean'
  644. },
  645. j: {
  646. deprecated: 'Please use journal instead',
  647. target: 'writeConcern',
  648. transform({ name, options, values: [value] }) {
  649. const wc = write_concern_1.WriteConcern.fromOptions({
  650. writeConcern: {
  651. ...options.writeConcern,
  652. journal: getBoolean(name, value)
  653. }
  654. });
  655. if (!wc)
  656. throw new error_1.MongoParseError(`Unable to make a writeConcern from journal=${value}`);
  657. return wc;
  658. }
  659. },
  660. journal: {
  661. target: 'writeConcern',
  662. transform({ name, options, values: [value] }) {
  663. const wc = write_concern_1.WriteConcern.fromOptions({
  664. writeConcern: {
  665. ...options.writeConcern,
  666. journal: getBoolean(name, value)
  667. }
  668. });
  669. if (!wc)
  670. throw new error_1.MongoParseError(`Unable to make a writeConcern from journal=${value}`);
  671. return wc;
  672. }
  673. },
  674. keepAlive: {
  675. default: true,
  676. type: 'boolean',
  677. deprecated: 'Will not be able to turn off in the future.'
  678. },
  679. keepAliveInitialDelay: {
  680. default: 120000,
  681. type: 'uint',
  682. deprecated: 'Will not be configurable in the future.'
  683. },
  684. loadBalanced: {
  685. default: false,
  686. type: 'boolean'
  687. },
  688. localThresholdMS: {
  689. default: 15,
  690. type: 'uint'
  691. },
  692. maxConnecting: {
  693. default: 2,
  694. transform({ name, values: [value] }) {
  695. const maxConnecting = getUIntFromOptions(name, value);
  696. if (maxConnecting === 0) {
  697. throw new error_1.MongoInvalidArgumentError('maxConnecting must be > 0 if specified');
  698. }
  699. return maxConnecting;
  700. }
  701. },
  702. maxIdleTimeMS: {
  703. default: 0,
  704. type: 'uint'
  705. },
  706. maxPoolSize: {
  707. default: 100,
  708. type: 'uint'
  709. },
  710. maxStalenessSeconds: {
  711. target: 'readPreference',
  712. transform({ name, options, values: [value] }) {
  713. const maxStalenessSeconds = getUIntFromOptions(name, value);
  714. if (options.readPreference) {
  715. return read_preference_1.ReadPreference.fromOptions({
  716. readPreference: { ...options.readPreference, maxStalenessSeconds }
  717. });
  718. }
  719. else {
  720. return new read_preference_1.ReadPreference('secondary', undefined, { maxStalenessSeconds });
  721. }
  722. }
  723. },
  724. minInternalBufferSize: {
  725. type: 'uint'
  726. },
  727. minPoolSize: {
  728. default: 0,
  729. type: 'uint'
  730. },
  731. minHeartbeatFrequencyMS: {
  732. default: 500,
  733. type: 'uint'
  734. },
  735. monitorCommands: {
  736. default: false,
  737. type: 'boolean'
  738. },
  739. name: {
  740. target: 'driverInfo',
  741. transform({ values: [value], options }) {
  742. return { ...options.driverInfo, name: String(value) };
  743. }
  744. },
  745. noDelay: {
  746. default: true,
  747. type: 'boolean'
  748. },
  749. pkFactory: {
  750. default: utils_1.DEFAULT_PK_FACTORY,
  751. transform({ values: [value] }) {
  752. if ((0, utils_1.isRecord)(value, ['createPk']) && typeof value.createPk === 'function') {
  753. return value;
  754. }
  755. throw new error_1.MongoParseError(`Option pkFactory must be an object with a createPk function, got ${value}`);
  756. }
  757. },
  758. promoteBuffers: {
  759. type: 'boolean'
  760. },
  761. promoteLongs: {
  762. type: 'boolean'
  763. },
  764. promoteValues: {
  765. type: 'boolean'
  766. },
  767. useBigInt64: {
  768. type: 'boolean'
  769. },
  770. proxyHost: {
  771. type: 'string'
  772. },
  773. proxyPassword: {
  774. type: 'string'
  775. },
  776. proxyPort: {
  777. type: 'uint'
  778. },
  779. proxyUsername: {
  780. type: 'string'
  781. },
  782. raw: {
  783. default: false,
  784. type: 'boolean'
  785. },
  786. readConcern: {
  787. transform({ values: [value], options }) {
  788. if (value instanceof read_concern_1.ReadConcern || (0, utils_1.isRecord)(value, ['level'])) {
  789. return read_concern_1.ReadConcern.fromOptions({ ...options.readConcern, ...value });
  790. }
  791. throw new error_1.MongoParseError(`ReadConcern must be an object, got ${JSON.stringify(value)}`);
  792. }
  793. },
  794. readConcernLevel: {
  795. target: 'readConcern',
  796. transform({ values: [level], options }) {
  797. return read_concern_1.ReadConcern.fromOptions({
  798. ...options.readConcern,
  799. level: level
  800. });
  801. }
  802. },
  803. readPreference: {
  804. default: read_preference_1.ReadPreference.primary,
  805. transform({ values: [value], options }) {
  806. if (value instanceof read_preference_1.ReadPreference) {
  807. return read_preference_1.ReadPreference.fromOptions({
  808. readPreference: { ...options.readPreference, ...value },
  809. ...value
  810. });
  811. }
  812. if ((0, utils_1.isRecord)(value, ['mode'])) {
  813. const rp = read_preference_1.ReadPreference.fromOptions({
  814. readPreference: { ...options.readPreference, ...value },
  815. ...value
  816. });
  817. if (rp)
  818. return rp;
  819. else
  820. throw new error_1.MongoParseError(`Cannot make read preference from ${JSON.stringify(value)}`);
  821. }
  822. if (typeof value === 'string') {
  823. const rpOpts = {
  824. hedge: options.readPreference?.hedge,
  825. maxStalenessSeconds: options.readPreference?.maxStalenessSeconds
  826. };
  827. return new read_preference_1.ReadPreference(value, options.readPreference?.tags, rpOpts);
  828. }
  829. throw new error_1.MongoParseError(`Unknown ReadPreference value: ${value}`);
  830. }
  831. },
  832. readPreferenceTags: {
  833. target: 'readPreference',
  834. transform({ values, options }) {
  835. const tags = Array.isArray(values[0])
  836. ? values[0]
  837. : values;
  838. const readPreferenceTags = [];
  839. for (const tag of tags) {
  840. const readPreferenceTag = Object.create(null);
  841. if (typeof tag === 'string') {
  842. for (const [k, v] of entriesFromString(tag)) {
  843. readPreferenceTag[k] = v;
  844. }
  845. }
  846. if ((0, utils_1.isRecord)(tag)) {
  847. for (const [k, v] of Object.entries(tag)) {
  848. readPreferenceTag[k] = v;
  849. }
  850. }
  851. readPreferenceTags.push(readPreferenceTag);
  852. }
  853. return read_preference_1.ReadPreference.fromOptions({
  854. readPreference: options.readPreference,
  855. readPreferenceTags
  856. });
  857. }
  858. },
  859. replicaSet: {
  860. type: 'string'
  861. },
  862. retryReads: {
  863. default: true,
  864. type: 'boolean'
  865. },
  866. retryWrites: {
  867. default: true,
  868. type: 'boolean'
  869. },
  870. serializeFunctions: {
  871. type: 'boolean'
  872. },
  873. serverSelectionTimeoutMS: {
  874. default: 30000,
  875. type: 'uint'
  876. },
  877. servername: {
  878. type: 'string'
  879. },
  880. socketTimeoutMS: {
  881. default: 0,
  882. type: 'uint'
  883. },
  884. srvMaxHosts: {
  885. type: 'uint',
  886. default: 0
  887. },
  888. srvServiceName: {
  889. type: 'string',
  890. default: 'mongodb'
  891. },
  892. ssl: {
  893. target: 'tls',
  894. type: 'boolean'
  895. },
  896. sslCA: {
  897. deprecated: 'sslCA is deprecated and will be removed in the next major version. Please use tlsCAFile instead.',
  898. target: 'ca',
  899. transform({ values: [value] }) {
  900. return fs.readFileSync(String(value), { encoding: 'ascii' });
  901. }
  902. },
  903. sslCRL: {
  904. deprecated: 'sslCRL is deprecated and will be removed in the next major version and be replaced by tlsCRLFile in that release.',
  905. target: 'crl',
  906. transform({ values: [value] }) {
  907. return fs.readFileSync(String(value), { encoding: 'ascii' });
  908. }
  909. },
  910. sslCert: {
  911. deprecated: 'sslCert is deprecated and will be removed in the next major version. Please use tlsCertificateKeyFile instead.',
  912. target: 'cert',
  913. transform({ values: [value] }) {
  914. return fs.readFileSync(String(value), { encoding: 'ascii' });
  915. }
  916. },
  917. sslKey: {
  918. deprecated: 'sslKey is deprecated and will be removed in the next major version. Please use tlsCertificateKeyFile instead.',
  919. target: 'key',
  920. transform({ values: [value] }) {
  921. return fs.readFileSync(String(value), { encoding: 'ascii' });
  922. }
  923. },
  924. sslPass: {
  925. deprecated: 'sslPass is deprecated and will be removed in the next major version. Please use tlsCertificateKeyFilePassword instead.',
  926. target: 'passphrase',
  927. type: 'string'
  928. },
  929. sslValidate: {
  930. deprecated: 'sslValidate is deprecated and will be removed in the next major version. Please use tlsAllowInvalidCertificates instead.',
  931. target: 'rejectUnauthorized',
  932. type: 'boolean'
  933. },
  934. tls: {
  935. type: 'boolean'
  936. },
  937. tlsAllowInvalidCertificates: {
  938. target: 'rejectUnauthorized',
  939. transform({ name, values: [value] }) {
  940. // allowInvalidCertificates is the inverse of rejectUnauthorized
  941. return !getBoolean(name, value);
  942. }
  943. },
  944. tlsAllowInvalidHostnames: {
  945. target: 'checkServerIdentity',
  946. transform({ name, values: [value] }) {
  947. // tlsAllowInvalidHostnames means setting the checkServerIdentity function to a noop
  948. return getBoolean(name, value) ? () => undefined : undefined;
  949. }
  950. },
  951. tlsCAFile: {
  952. target: 'ca',
  953. transform({ values: [value] }) {
  954. return fs.readFileSync(String(value), { encoding: 'ascii' });
  955. }
  956. },
  957. tlsCertificateFile: {
  958. deprecated: 'tlsCertificateFile is deprecated and will be removed in the next major version. Please use tlsCertificateKeyFile instead.',
  959. target: 'cert',
  960. transform({ values: [value] }) {
  961. return fs.readFileSync(String(value), { encoding: 'ascii' });
  962. }
  963. },
  964. tlsCertificateKeyFile: {
  965. target: 'key',
  966. transform({ values: [value] }) {
  967. return fs.readFileSync(String(value), { encoding: 'ascii' });
  968. }
  969. },
  970. tlsCertificateKeyFilePassword: {
  971. target: 'passphrase',
  972. type: 'any'
  973. },
  974. tlsInsecure: {
  975. transform({ name, options, values: [value] }) {
  976. const tlsInsecure = getBoolean(name, value);
  977. if (tlsInsecure) {
  978. options.checkServerIdentity = () => undefined;
  979. options.rejectUnauthorized = false;
  980. }
  981. else {
  982. options.checkServerIdentity = options.tlsAllowInvalidHostnames
  983. ? () => undefined
  984. : undefined;
  985. options.rejectUnauthorized = options.tlsAllowInvalidCertificates ? false : true;
  986. }
  987. return tlsInsecure;
  988. }
  989. },
  990. w: {
  991. target: 'writeConcern',
  992. transform({ values: [value], options }) {
  993. return write_concern_1.WriteConcern.fromOptions({ writeConcern: { ...options.writeConcern, w: value } });
  994. }
  995. },
  996. waitQueueTimeoutMS: {
  997. default: 0,
  998. type: 'uint'
  999. },
  1000. writeConcern: {
  1001. target: 'writeConcern',
  1002. transform({ values: [value], options }) {
  1003. if ((0, utils_1.isRecord)(value) || value instanceof write_concern_1.WriteConcern) {
  1004. return write_concern_1.WriteConcern.fromOptions({
  1005. writeConcern: {
  1006. ...options.writeConcern,
  1007. ...value
  1008. }
  1009. });
  1010. }
  1011. else if (value === 'majority' || typeof value === 'number') {
  1012. return write_concern_1.WriteConcern.fromOptions({
  1013. writeConcern: {
  1014. ...options.writeConcern,
  1015. w: value
  1016. }
  1017. });
  1018. }
  1019. throw new error_1.MongoParseError(`Invalid WriteConcern cannot parse: ${JSON.stringify(value)}`);
  1020. }
  1021. },
  1022. wtimeout: {
  1023. deprecated: 'Please use wtimeoutMS instead',
  1024. target: 'writeConcern',
  1025. transform({ values: [value], options }) {
  1026. const wc = write_concern_1.WriteConcern.fromOptions({
  1027. writeConcern: {
  1028. ...options.writeConcern,
  1029. wtimeout: getUIntFromOptions('wtimeout', value)
  1030. }
  1031. });
  1032. if (wc)
  1033. return wc;
  1034. throw new error_1.MongoParseError(`Cannot make WriteConcern from wtimeout`);
  1035. }
  1036. },
  1037. wtimeoutMS: {
  1038. target: 'writeConcern',
  1039. transform({ values: [value], options }) {
  1040. const wc = write_concern_1.WriteConcern.fromOptions({
  1041. writeConcern: {
  1042. ...options.writeConcern,
  1043. wtimeoutMS: getUIntFromOptions('wtimeoutMS', value)
  1044. }
  1045. });
  1046. if (wc)
  1047. return wc;
  1048. throw new error_1.MongoParseError(`Cannot make WriteConcern from wtimeout`);
  1049. }
  1050. },
  1051. zlibCompressionLevel: {
  1052. default: 0,
  1053. type: 'int'
  1054. },
  1055. // Custom types for modifying core behavior
  1056. connectionType: { type: 'any' },
  1057. srvPoller: { type: 'any' },
  1058. // Accepted NodeJS Options
  1059. minDHSize: { type: 'any' },
  1060. pskCallback: { type: 'any' },
  1061. secureContext: { type: 'any' },
  1062. enableTrace: { type: 'any' },
  1063. requestCert: { type: 'any' },
  1064. rejectUnauthorized: { type: 'any' },
  1065. checkServerIdentity: { type: 'any' },
  1066. ALPNProtocols: { type: 'any' },
  1067. SNICallback: { type: 'any' },
  1068. session: { type: 'any' },
  1069. requestOCSP: { type: 'any' },
  1070. localAddress: { type: 'any' },
  1071. localPort: { type: 'any' },
  1072. hints: { type: 'any' },
  1073. lookup: { type: 'any' },
  1074. ca: { type: 'any' },
  1075. cert: { type: 'any' },
  1076. ciphers: { type: 'any' },
  1077. crl: { type: 'any' },
  1078. ecdhCurve: { type: 'any' },
  1079. key: { type: 'any' },
  1080. passphrase: { type: 'any' },
  1081. pfx: { type: 'any' },
  1082. secureProtocol: { type: 'any' },
  1083. index: { type: 'any' },
  1084. // Legacy Options, these are unused but left here to avoid errors with CSFLE lib
  1085. useNewUrlParser: { type: 'boolean' },
  1086. useUnifiedTopology: { type: 'boolean' },
  1087. // MongoLogger
  1088. // TODO(NODE-4849): Tighten the type of mongodbLogPath
  1089. mongodbLogPath: { type: 'any' }
  1090. };
  1091. exports.DEFAULT_OPTIONS = new CaseInsensitiveMap(Object.entries(exports.OPTIONS)
  1092. .filter(([, descriptor]) => descriptor.default != null)
  1093. .map(([k, d]) => [k, d.default]));
  1094. /**
  1095. * Set of permitted feature flags
  1096. * @internal
  1097. */
  1098. exports.FEATURE_FLAGS = new Set([
  1099. Symbol.for('@@mdb.skipPingOnConnect'),
  1100. Symbol.for('@@mdb.enableMongoLogger'),
  1101. Symbol.for('@@mdb.internalLoggerConfig')
  1102. ]);
  1103. //# sourceMappingURL=connection_string.js.map