Skip to content

Commit 1f8b539

Browse files
authored
feat(NODE-3467): implement srvMaxHosts, srvServiceName options (#3031)
1 parent 0709ef2 commit 1f8b539

18 files changed

+896
-235
lines changed

.mocharc.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
"ts"
66
],
77
"require": [
8-
"ts-node/register",
98
"source-map-support/register",
9+
"ts-node/register",
1010
"test/tools/runner/chai-addons",
1111
"test/tools/runner/circular-dep-hack"
1212
],

package-lock.json

+48-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"@types/node": "^16.10.3",
4848
"@types/saslprep": "^1.0.1",
4949
"@types/semver": "^7.3.8",
50+
"@types/sinon": "^10.0.6",
5051
"@types/whatwg-url": "^8.2.1",
5152
"@typescript-eslint/eslint-plugin": "^4.33.0",
5253
"@typescript-eslint/parser": "^4.33.0",
@@ -69,7 +70,7 @@
6970
"prettier": "^2.4.1",
7071
"rimraf": "^3.0.2",
7172
"semver": "^7.3.5",
72-
"sinon": "^11.1.2",
73+
"sinon": "^12.0.1",
7374
"sinon-chai": "^3.7.0",
7475
"source-map-support": "^0.5.20",
7576
"standard-version": "^9.3.1",

src/connection_string.ts

+80-36
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export function resolveSRVRecord(options: MongoOptions, callback: Callback<HostA
7575

7676
// Resolve the SRV record and use the result as the list of hosts to connect to.
7777
const lookupAddress = options.srvHost;
78-
dns.resolveSrv(`_mongodb._tcp.${lookupAddress}`, (err, addresses) => {
78+
dns.resolveSrv(`_${options.srvServiceName}._tcp.${lookupAddress}`, (err, addresses) => {
7979
if (err) return callback(err);
8080

8181
if (addresses.length === 0) {
@@ -92,7 +92,7 @@ export function resolveSRVRecord(options: MongoOptions, callback: Callback<HostA
9292
HostAddress.fromString(`${r.name}:${r.port ?? 27017}`)
9393
);
9494

95-
const lbError = validateLoadBalancedOptions(hostAddresses, options);
95+
const lbError = validateLoadBalancedOptions(hostAddresses, options, true);
9696
if (lbError) {
9797
return callback(lbError);
9898
}
@@ -116,14 +116,14 @@ export function resolveSRVRecord(options: MongoOptions, callback: Callback<HostA
116116
);
117117
}
118118

119+
if (VALID_TXT_RECORDS.some(option => txtRecordOptions.get(option) === '')) {
120+
return callback(new MongoParseError('Cannot have empty URI params in DNS TXT Record'));
121+
}
122+
119123
const source = txtRecordOptions.get('authSource') ?? undefined;
120124
const replicaSet = txtRecordOptions.get('replicaSet') ?? undefined;
121125
const loadBalanced = txtRecordOptions.get('loadBalanced') ?? undefined;
122126

123-
if (source === '' || replicaSet === '') {
124-
return callback(new MongoParseError('Cannot have empty URI params in DNS TXT Record'));
125-
}
126-
127127
if (!options.userSpecifiedAuthSource && source) {
128128
options.credentials = MongoCredentials.merge(options.credentials, { source });
129129
}
@@ -136,7 +136,11 @@ export function resolveSRVRecord(options: MongoOptions, callback: Callback<HostA
136136
options.loadBalanced = true;
137137
}
138138

139-
const lbError = validateLoadBalancedOptions(hostAddresses, options);
139+
if (options.replicaSet && options.srvMaxHosts > 0) {
140+
return callback(new MongoParseError('Cannot combine replicaSet option with srvMaxHosts'));
141+
}
142+
143+
const lbError = validateLoadBalancedOptions(hostAddresses, options, true);
140144
if (lbError) {
141145
return callback(lbError);
142146
}
@@ -251,13 +255,6 @@ export function parseOptions(
251255

252256
const mongoOptions = Object.create(null);
253257
mongoOptions.hosts = isSRV ? [] : hosts.map(HostAddress.fromString);
254-
if (isSRV) {
255-
// SRV Record is resolved upon connecting
256-
mongoOptions.srvHost = hosts[0];
257-
if (!url.searchParams.has('tls') && !url.searchParams.has('ssl')) {
258-
options.tls = true;
259-
}
260-
}
261258

262259
const urlOptions = new CaseInsensitiveMap();
263260

@@ -289,30 +286,34 @@ export function parseOptions(
289286
throw new MongoAPIError('URI cannot contain options with no value');
290287
}
291288

292-
if (key.toLowerCase() === 'serverapi') {
293-
throw new MongoParseError(
294-
'URI cannot contain `serverApi`, it can only be passed to the client'
295-
);
296-
}
297-
298-
if (key.toLowerCase() === 'authsource' && urlOptions.has('authSource')) {
299-
// If authSource is an explicit key in the urlOptions we need to remove the implicit dbName
300-
urlOptions.delete('authSource');
301-
}
302-
303289
if (!urlOptions.has(key)) {
304290
urlOptions.set(key, values);
305291
}
306292
}
307293

294+
if (urlOptions.has('authSource')) {
295+
// If authSource is an explicit key in the urlOptions we need to remove the dbName
296+
urlOptions.delete('dbName');
297+
}
298+
308299
const objectOptions = new CaseInsensitiveMap(
309300
Object.entries(options).filter(([, v]) => v != null)
310301
);
311302

303+
// Validate options that can only be provided by one of uri or object
304+
305+
if (urlOptions.has('serverApi')) {
306+
throw new MongoParseError(
307+
'URI cannot contain `serverApi`, it can only be passed to the client'
308+
);
309+
}
310+
312311
if (objectOptions.has('loadBalanced')) {
313312
throw new MongoParseError('loadBalanced is only a valid option in the URI');
314313
}
315314

315+
// All option collection
316+
316317
const allOptions = new CaseInsensitiveMap();
317318

318319
const allKeys = new Set<string>([
@@ -360,6 +361,8 @@ export function parseOptions(
360361
);
361362
}
362363

364+
// Option parsing and setting
365+
363366
for (const [key, descriptor] of Object.entries(OPTIONS)) {
364367
const values = allOptions.get(key);
365368
if (!values || values.length === 0) continue;
@@ -401,33 +404,62 @@ export function parseOptions(
401404

402405
if (options.promiseLibrary) PromiseProvider.set(options.promiseLibrary);
403406

404-
if (mongoOptions.directConnection && typeof mongoOptions.srvHost === 'string') {
405-
throw new MongoAPIError('SRV URI does not support directConnection');
406-
}
407-
408-
const lbError = validateLoadBalancedOptions(hosts, mongoOptions);
407+
const lbError = validateLoadBalancedOptions(hosts, mongoOptions, isSRV);
409408
if (lbError) {
410409
throw lbError;
411410
}
411+
if (mongoClient && mongoOptions.autoEncryption) {
412+
Encrypter.checkForMongoCrypt();
413+
mongoOptions.encrypter = new Encrypter(mongoClient, uri, options);
414+
mongoOptions.autoEncrypter = mongoOptions.encrypter.autoEncrypter;
415+
}
416+
417+
// Potential SRV Overrides and SRV connection string validations
412418

413-
// Potential SRV Overrides
414419
mongoOptions.userSpecifiedAuthSource =
415420
objectOptions.has('authSource') || urlOptions.has('authSource');
416421
mongoOptions.userSpecifiedReplicaSet =
417422
objectOptions.has('replicaSet') || urlOptions.has('replicaSet');
418423

419-
if (mongoClient && mongoOptions.autoEncryption) {
420-
Encrypter.checkForMongoCrypt();
421-
mongoOptions.encrypter = new Encrypter(mongoClient, uri, options);
422-
mongoOptions.autoEncrypter = mongoOptions.encrypter.autoEncrypter;
424+
if (isSRV) {
425+
// SRV Record is resolved upon connecting
426+
mongoOptions.srvHost = hosts[0];
427+
428+
if (mongoOptions.directConnection) {
429+
throw new MongoAPIError('SRV URI does not support directConnection');
430+
}
431+
432+
if (mongoOptions.srvMaxHosts > 0 && typeof mongoOptions.replicaSet === 'string') {
433+
throw new MongoParseError('Cannot use srvMaxHosts option with replicaSet');
434+
}
435+
436+
// SRV turns on TLS by default, but users can override and turn it off
437+
const noUserSpecifiedTLS = !objectOptions.has('tls') && !urlOptions.has('tls');
438+
const noUserSpecifiedSSL = !objectOptions.has('ssl') && !urlOptions.has('ssl');
439+
if (noUserSpecifiedTLS && noUserSpecifiedSSL) {
440+
mongoOptions.tls = true;
441+
}
442+
} else {
443+
const userSpecifiedSrvOptions =
444+
urlOptions.has('srvMaxHosts') ||
445+
objectOptions.has('srvMaxHosts') ||
446+
urlOptions.has('srvServiceName') ||
447+
objectOptions.has('srvServiceName');
448+
449+
if (userSpecifiedSrvOptions) {
450+
throw new MongoParseError(
451+
'Cannot use srvMaxHosts or srvServiceName with a non-srv connection string'
452+
);
453+
}
423454
}
424455

425456
return mongoOptions;
426457
}
427458

428459
function validateLoadBalancedOptions(
429460
hosts: HostAddress[] | string[],
430-
mongoOptions: MongoOptions
461+
mongoOptions: MongoOptions,
462+
isSrv: boolean
431463
): MongoParseError | undefined {
432464
if (mongoOptions.loadBalanced) {
433465
if (hosts.length > 1) {
@@ -439,6 +471,10 @@ function validateLoadBalancedOptions(
439471
if (mongoOptions.directConnection) {
440472
return new MongoParseError(LB_DIRECT_CONNECTION_ERROR);
441473
}
474+
475+
if (isSrv && mongoOptions.srvMaxHosts > 0) {
476+
return new MongoParseError('Cannot limit srv hosts with loadBalanced enabled');
477+
}
442478
}
443479
}
444480

@@ -924,6 +960,14 @@ export const OPTIONS = {
924960
default: 0,
925961
type: 'uint'
926962
},
963+
srvMaxHosts: {
964+
type: 'uint',
965+
default: 0
966+
},
967+
srvServiceName: {
968+
type: 'string',
969+
default: 'mongodb'
970+
},
927971
ssl: {
928972
target: 'tls',
929973
type: 'boolean'

src/mongo_client.ts

+12
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,16 @@ export interface MongoClientOptions extends BSONSerializeOptions, SupportedNodeC
132132
compressors?: CompressorName[] | string;
133133
/** An integer that specifies the compression level if using zlib for network compression. */
134134
zlibCompressionLevel?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | undefined;
135+
/** The maximum number of hosts to connect to when using an srv connection string, a setting of `0` means unlimited hosts */
136+
srvMaxHosts?: number;
137+
/**
138+
* Modifies the srv URI to look like:
139+
*
140+
* `_{srvServiceName}._tcp.{hostname}.{domainname}`
141+
*
142+
* Querying this DNS URI is expected to respond with SRV records
143+
*/
144+
srvServiceName?: string;
135145
/** The maximum number of connections in the connection pool. */
136146
maxPoolSize?: number;
137147
/** The minimum number of connections in the connection pool. */
@@ -643,6 +653,8 @@ export interface MongoOptions
643653
| 'retryWrites'
644654
| 'serverSelectionTimeoutMS'
645655
| 'socketTimeoutMS'
656+
| 'srvMaxHosts'
657+
| 'srvServiceName'
646658
| 'tlsAllowInvalidCertificates'
647659
| 'tlsAllowInvalidHostnames'
648660
| 'tlsInsecure'

0 commit comments

Comments
 (0)