Skip to content

Commit

Permalink
feat(NODE-3467): implement srvMaxHosts, srvServiceName options (#3031)
Browse files Browse the repository at this point in the history
  • Loading branch information
nbbeeken committed Nov 16, 2021
1 parent 0709ef2 commit 1f8b539
Show file tree
Hide file tree
Showing 18 changed files with 896 additions and 235 deletions.
2 changes: 1 addition & 1 deletion .mocharc.json
Expand Up @@ -5,8 +5,8 @@
"ts"
],
"require": [
"ts-node/register",
"source-map-support/register",
"ts-node/register",
"test/tools/runner/chai-addons",
"test/tools/runner/circular-dep-hack"
],
Expand Down
57 changes: 48 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -47,6 +47,7 @@
"@types/node": "^16.10.3",
"@types/saslprep": "^1.0.1",
"@types/semver": "^7.3.8",
"@types/sinon": "^10.0.6",
"@types/whatwg-url": "^8.2.1",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
Expand All @@ -69,7 +70,7 @@
"prettier": "^2.4.1",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"sinon": "^11.1.2",
"sinon": "^12.0.1",
"sinon-chai": "^3.7.0",
"source-map-support": "^0.5.20",
"standard-version": "^9.3.1",
Expand Down
116 changes: 80 additions & 36 deletions src/connection_string.ts
Expand Up @@ -75,7 +75,7 @@ export function resolveSRVRecord(options: MongoOptions, callback: Callback<HostA

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

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

const lbError = validateLoadBalancedOptions(hostAddresses, options);
const lbError = validateLoadBalancedOptions(hostAddresses, options, true);
if (lbError) {
return callback(lbError);
}
Expand All @@ -116,14 +116,14 @@ export function resolveSRVRecord(options: MongoOptions, callback: Callback<HostA
);
}

if (VALID_TXT_RECORDS.some(option => txtRecordOptions.get(option) === '')) {
return callback(new MongoParseError('Cannot have empty URI params in DNS TXT Record'));
}

const source = txtRecordOptions.get('authSource') ?? undefined;
const replicaSet = txtRecordOptions.get('replicaSet') ?? undefined;
const loadBalanced = txtRecordOptions.get('loadBalanced') ?? undefined;

if (source === '' || replicaSet === '') {
return callback(new MongoParseError('Cannot have empty URI params in DNS TXT Record'));
}

if (!options.userSpecifiedAuthSource && source) {
options.credentials = MongoCredentials.merge(options.credentials, { source });
}
Expand All @@ -136,7 +136,11 @@ export function resolveSRVRecord(options: MongoOptions, callback: Callback<HostA
options.loadBalanced = true;
}

const lbError = validateLoadBalancedOptions(hostAddresses, options);
if (options.replicaSet && options.srvMaxHosts > 0) {
return callback(new MongoParseError('Cannot combine replicaSet option with srvMaxHosts'));
}

const lbError = validateLoadBalancedOptions(hostAddresses, options, true);
if (lbError) {
return callback(lbError);
}
Expand Down Expand Up @@ -251,13 +255,6 @@ export function parseOptions(

const mongoOptions = Object.create(null);
mongoOptions.hosts = isSRV ? [] : hosts.map(HostAddress.fromString);
if (isSRV) {
// SRV Record is resolved upon connecting
mongoOptions.srvHost = hosts[0];
if (!url.searchParams.has('tls') && !url.searchParams.has('ssl')) {
options.tls = true;
}
}

const urlOptions = new CaseInsensitiveMap();

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

if (key.toLowerCase() === 'serverapi') {
throw new MongoParseError(
'URI cannot contain `serverApi`, it can only be passed to the client'
);
}

if (key.toLowerCase() === 'authsource' && urlOptions.has('authSource')) {
// If authSource is an explicit key in the urlOptions we need to remove the implicit dbName
urlOptions.delete('authSource');
}

if (!urlOptions.has(key)) {
urlOptions.set(key, values);
}
}

if (urlOptions.has('authSource')) {
// If authSource is an explicit key in the urlOptions we need to remove the dbName
urlOptions.delete('dbName');
}

const objectOptions = new CaseInsensitiveMap(
Object.entries(options).filter(([, v]) => v != null)
);

// Validate options that can only be provided by one of uri or object

if (urlOptions.has('serverApi')) {
throw new MongoParseError(
'URI cannot contain `serverApi`, it can only be passed to the client'
);
}

if (objectOptions.has('loadBalanced')) {
throw new MongoParseError('loadBalanced is only a valid option in the URI');
}

// All option collection

const allOptions = new CaseInsensitiveMap();

const allKeys = new Set<string>([
Expand Down Expand Up @@ -360,6 +361,8 @@ export function parseOptions(
);
}

// Option parsing and setting

for (const [key, descriptor] of Object.entries(OPTIONS)) {
const values = allOptions.get(key);
if (!values || values.length === 0) continue;
Expand Down Expand Up @@ -401,33 +404,62 @@ export function parseOptions(

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

if (mongoOptions.directConnection && typeof mongoOptions.srvHost === 'string') {
throw new MongoAPIError('SRV URI does not support directConnection');
}

const lbError = validateLoadBalancedOptions(hosts, mongoOptions);
const lbError = validateLoadBalancedOptions(hosts, mongoOptions, isSRV);
if (lbError) {
throw lbError;
}
if (mongoClient && mongoOptions.autoEncryption) {
Encrypter.checkForMongoCrypt();
mongoOptions.encrypter = new Encrypter(mongoClient, uri, options);
mongoOptions.autoEncrypter = mongoOptions.encrypter.autoEncrypter;
}

// Potential SRV Overrides and SRV connection string validations

// Potential SRV Overrides
mongoOptions.userSpecifiedAuthSource =
objectOptions.has('authSource') || urlOptions.has('authSource');
mongoOptions.userSpecifiedReplicaSet =
objectOptions.has('replicaSet') || urlOptions.has('replicaSet');

if (mongoClient && mongoOptions.autoEncryption) {
Encrypter.checkForMongoCrypt();
mongoOptions.encrypter = new Encrypter(mongoClient, uri, options);
mongoOptions.autoEncrypter = mongoOptions.encrypter.autoEncrypter;
if (isSRV) {
// SRV Record is resolved upon connecting
mongoOptions.srvHost = hosts[0];

if (mongoOptions.directConnection) {
throw new MongoAPIError('SRV URI does not support directConnection');
}

if (mongoOptions.srvMaxHosts > 0 && typeof mongoOptions.replicaSet === 'string') {
throw new MongoParseError('Cannot use srvMaxHosts option with replicaSet');
}

// SRV turns on TLS by default, but users can override and turn it off
const noUserSpecifiedTLS = !objectOptions.has('tls') && !urlOptions.has('tls');
const noUserSpecifiedSSL = !objectOptions.has('ssl') && !urlOptions.has('ssl');
if (noUserSpecifiedTLS && noUserSpecifiedSSL) {
mongoOptions.tls = true;
}
} else {
const userSpecifiedSrvOptions =
urlOptions.has('srvMaxHosts') ||
objectOptions.has('srvMaxHosts') ||
urlOptions.has('srvServiceName') ||
objectOptions.has('srvServiceName');

if (userSpecifiedSrvOptions) {
throw new MongoParseError(
'Cannot use srvMaxHosts or srvServiceName with a non-srv connection string'
);
}
}

return mongoOptions;
}

function validateLoadBalancedOptions(
hosts: HostAddress[] | string[],
mongoOptions: MongoOptions
mongoOptions: MongoOptions,
isSrv: boolean
): MongoParseError | undefined {
if (mongoOptions.loadBalanced) {
if (hosts.length > 1) {
Expand All @@ -439,6 +471,10 @@ function validateLoadBalancedOptions(
if (mongoOptions.directConnection) {
return new MongoParseError(LB_DIRECT_CONNECTION_ERROR);
}

if (isSrv && mongoOptions.srvMaxHosts > 0) {
return new MongoParseError('Cannot limit srv hosts with loadBalanced enabled');
}
}
}

Expand Down Expand Up @@ -924,6 +960,14 @@ export const OPTIONS = {
default: 0,
type: 'uint'
},
srvMaxHosts: {
type: 'uint',
default: 0
},
srvServiceName: {
type: 'string',
default: 'mongodb'
},
ssl: {
target: 'tls',
type: 'boolean'
Expand Down
12 changes: 12 additions & 0 deletions src/mongo_client.ts
Expand Up @@ -132,6 +132,16 @@ export interface MongoClientOptions extends BSONSerializeOptions, SupportedNodeC
compressors?: CompressorName[] | string;
/** An integer that specifies the compression level if using zlib for network compression. */
zlibCompressionLevel?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | undefined;
/** The maximum number of hosts to connect to when using an srv connection string, a setting of `0` means unlimited hosts */
srvMaxHosts?: number;
/**
* Modifies the srv URI to look like:
*
* `_{srvServiceName}._tcp.{hostname}.{domainname}`
*
* Querying this DNS URI is expected to respond with SRV records
*/
srvServiceName?: string;
/** The maximum number of connections in the connection pool. */
maxPoolSize?: number;
/** The minimum number of connections in the connection pool. */
Expand Down Expand Up @@ -643,6 +653,8 @@ export interface MongoOptions
| 'retryWrites'
| 'serverSelectionTimeoutMS'
| 'socketTimeoutMS'
| 'srvMaxHosts'
| 'srvServiceName'
| 'tlsAllowInvalidCertificates'
| 'tlsAllowInvalidHostnames'
| 'tlsInsecure'
Expand Down

0 comments on commit 1f8b539

Please sign in to comment.