Skip to content

Commit 107f190

Browse files
committed
test fixes
1 parent 534fa75 commit 107f190

File tree

4 files changed

+221
-87
lines changed

4 files changed

+221
-87
lines changed

src/connection_string.ts

Lines changed: 209 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as url from 'url';
22
import * as qs from 'querystring';
33
import * as dns from 'dns';
44
import { URL } from 'url';
5+
import { AuthMechanismEnum } from './cmap/auth/defaultAuthProviders';
56
import { ReadPreference, ReadPreferenceMode } from './read_preference';
67
import { ReadConcern, ReadConcernLevel } from './read_concern';
78
import { W, WriteConcern } from './write_concern';
@@ -13,7 +14,8 @@ import type { CompressorName } from './cmap/wire_protocol/compression';
1314
import type { DriverInfo, MongoClientOptions, MongoOptions, PkFactory } from './mongo_client';
1415
import { MongoCredentials } from './cmap/auth/mongo_credentials';
1516
import type { TagSet } from './sdam/server_description';
16-
import { Logger, LoggerLevel } from '.';
17+
import { Logger, LoggerLevel } from './logger';
18+
import { ObjectId } from 'bson';
1719

1820
/**
1921
* The following regular expression validates a connection string and breaks the
@@ -506,23 +508,6 @@ function checkTLSQueryString(queryString: any) {
506508
}
507509
}
508510

509-
/**
510-
* Checks options object if both options are present (any value) will throw an error.
511-
*
512-
* @param options - The options used for options parsing
513-
* @param optionKeyA - A options key
514-
* @param optionKeyB - B options key
515-
* @throws MongoParseError if two provided options are mutually exclusive.
516-
*/
517-
function assertRepelOptions(options: AnyOptions, optionKeyA: string, optionKeyB: string) {
518-
if (
519-
Object.prototype.hasOwnProperty.call(options, optionKeyA) &&
520-
Object.prototype.hasOwnProperty.call(options, optionKeyB)
521-
) {
522-
throw new MongoParseError(`The \`${optionKeyA}\` option cannot be used with \`${optionKeyB}\``);
523-
}
524-
}
525-
526511
/**
527512
* Checks if TLS options are valid
528513
*
@@ -775,7 +760,7 @@ export function parseConnectionString(
775760
// NEW PARSER WORK...
776761

777762
const HOSTS_REGEX = new RegExp(
778-
'(?<protocol>mongodb(?:\\+srv|)):\\/\\/(?:(?<username>[^:]*)(?::(?<password>[^@]*))?@)?(?<hosts>[^\\/?]*)(?<rest>.*)'
763+
String.raw`(?<protocol>mongodb(?:\+srv|)):\/\/(?:(?<username>[^:]*)(?::(?<password>[^@]*))?@)?(?<hosts>(?!:)[^\/?@]+)(?<rest>.*)`
779764
);
780765

781766
function parseURI(uri: string): { srv: boolean; url: URL; hosts: string[] } {
@@ -794,10 +779,39 @@ function parseURI(uri: string): { srv: boolean; url: URL; hosts: string[] } {
794779
throw new MongoParseError('Invalid connection string, protocol and host(s) required');
795780
}
796781

782+
decodeURIComponent(username ?? '');
783+
decodeURIComponent(password ?? '');
784+
785+
const illegalCharacters = new RegExp('[@/:]', 'gi');
786+
if (username?.match(illegalCharacters)) {
787+
throw new MongoParseError(`Username contains unescaped characters ${username}`);
788+
}
789+
if (!username || !password) {
790+
const uriWithoutProtocol = uri.replace(`${protocol}://`, '');
791+
if (uriWithoutProtocol.startsWith('@') || uriWithoutProtocol.startsWith(':')) {
792+
throw new MongoParseError('URI contained empty userinfo section');
793+
}
794+
}
795+
796+
if (password?.match(illegalCharacters)) {
797+
throw new MongoParseError('Password contains unescaped characters');
798+
}
799+
797800
const authString = username ? (password ? `${username}:${password}` : `${username}`) : '';
801+
const srv = protocol.includes('srv');
802+
const hostList = hosts.split(',');
803+
const url = new URL(`${protocol.toLowerCase()}://${authString}@dummyHostname${rest}`);
804+
805+
if (srv && hostList.length !== 1) {
806+
throw new MongoParseError('mongodb+srv URI cannot have multiple service names');
807+
}
808+
if (srv && hostList[0].includes(':')) {
809+
throw new MongoParseError('mongodb+srv URI cannot have port number');
810+
}
811+
798812
return {
799-
srv: protocol.includes('srv'),
800-
url: new URL(`${protocol.toLowerCase()}://${authString}@dummyHostname${rest}`),
813+
srv,
814+
url,
801815
hosts: hosts.split(',')
802816
};
803817
}
@@ -841,12 +855,60 @@ function toRecord(value: string): Record<string, any> {
841855
return record;
842856
}
843857

844-
const defaultOptions = new Map<string, unknown>([
845-
['dbName', 'test'],
846-
['socketTimeoutMS', 0],
847-
['readPreference', ReadPreference.primary]
848-
// TODO: add more
849-
]);
858+
const defaultOptions = new Map<string, unknown>(
859+
([
860+
['readPreference', ReadPreference.primary],
861+
['autoEncryption', {}],
862+
['compression', 'none'],
863+
['compressors', 'none'],
864+
['connectTimeoutMS', 30000],
865+
['dbName', 'test'],
866+
['directConnection', false],
867+
['driverInfo', {}],
868+
['forceServerObjectId', false],
869+
['gssapiServiceName', undefined],
870+
['heartbeatFrequencyMS', 10000],
871+
['keepAlive', true],
872+
['keepAliveInitialDelay', 120000],
873+
['localThresholdMS', 0],
874+
['logger', new Logger('MongoClient')],
875+
['maxIdleTimeMS', 0],
876+
['maxPoolSize', 100],
877+
['minPoolSize', 0],
878+
['monitorCommands', false],
879+
['noDelay', true],
880+
['numberOfRetries', 5],
881+
[
882+
'pkFactory',
883+
{
884+
createPk() {
885+
// We prefer not to rely on ObjectId having a createPk method
886+
return new ObjectId();
887+
}
888+
}
889+
],
890+
[
891+
'createPk',
892+
function createPk() {
893+
// We prefer not to rely on ObjectId having a createPk method
894+
return new ObjectId();
895+
}
896+
],
897+
['promiseLibrary', undefined],
898+
['raw', false],
899+
['reconnectInterval', 0],
900+
['reconnectTries', 0],
901+
['replicaSet', undefined],
902+
['retryReads', true],
903+
['retryWrites', true],
904+
['serverSelectionTimeoutMS', 30000],
905+
['serverSelectionTryOnce', false],
906+
['socketTimeoutMS', 0],
907+
['waitQueueMultiple', 1],
908+
['waitQueueTimeoutMS', 0],
909+
['zlibCompressionLevel', 0]
910+
] as [string, any][]).map(([k, v]) => [k.toLowerCase(), v])
911+
);
850912

851913
export function parseOptions(
852914
uri: string,
@@ -855,16 +917,29 @@ export function parseOptions(
855917
const { url, hosts, srv } = parseURI(uri);
856918

857919
const mongoOptions = Object.create(null);
858-
mongoOptions.hosts = hosts;
920+
mongoOptions.hosts = srv ? [{ host: hosts[0], type: 'srv' }] : hosts.map(toHostArray);
859921
mongoOptions.srv = srv;
922+
mongoOptions.dbName = decodeURIComponent(
923+
url.pathname[0] === '/' ? url.pathname.slice(1) : url.pathname
924+
);
860925

861926
const urlOptions = new Map();
927+
928+
if (url.username) urlOptions.set('username', [url.username]);
929+
if (url.password) urlOptions.set('password', [url.password]);
930+
862931
for (const key of url.searchParams.keys()) {
863932
const loweredKey = key.toLowerCase();
933+
const values = [...url.searchParams.getAll(key)];
934+
935+
if (values.includes('')) {
936+
throw new MongoParseError('URI cannot contain options with no value');
937+
}
938+
864939
if (urlOptions.has(loweredKey)) {
865-
urlOptions.set(loweredKey, [...urlOptions.get(loweredKey), ...url.searchParams.getAll(key)]);
940+
urlOptions.get(loweredKey)?.push(...values);
866941
} else {
867-
urlOptions.set(loweredKey, url.searchParams.getAll(key));
942+
urlOptions.set(loweredKey, values);
868943
}
869944
}
870945

@@ -895,51 +970,88 @@ export function parseOptions(
895970
}
896971

897972
for (const [loweredKey, values] of allOptions.entries()) {
898-
const descriptor = descriptorFor(loweredKey);
899-
const {
900-
descriptor: { rename, type, transform, deprecated },
901-
key
902-
} = descriptor;
903-
const name = rename ?? key;
904-
905-
if (deprecated) {
906-
console.warn(`${key} is a deprecated option`);
907-
}
973+
setOption(mongoOptions, loweredKey, values);
974+
}
908975

909-
switch (type) {
910-
case 'boolean':
911-
mongoOptions[name] = getBoolean(name, values[0]);
912-
break;
913-
case 'int':
914-
mongoOptions[name] = getInt(name, values[0]);
915-
break;
916-
case 'uint':
917-
mongoOptions[name] = getUint(name, values[0]);
918-
break;
919-
case 'string':
920-
mongoOptions[name] = String(values[0]);
921-
break;
922-
case 'record':
923-
if (!isRecord(values[0])) {
924-
throw new TypeError(`${name} must be an object`);
925-
}
926-
mongoOptions[name] = values[0];
927-
break;
928-
case 'asIs':
929-
mongoOptions[name] = values[0];
930-
break;
931-
default: {
932-
if (!transform) {
933-
throw new MongoParseError('Descriptors missing a type must define a transform');
934-
}
935-
const transformValue = transform({ name, options: mongoOptions, values });
936-
mongoOptions[name] = transformValue;
976+
mongoOptions.credentials?.validate();
977+
checkTLSOptions(mongoOptions);
978+
979+
return Object.freeze(mongoOptions) as Readonly<MongoOptions>;
980+
}
981+
982+
function setOption(mongoOptions: any, loweredKey: string, values: unknown[]) {
983+
const descriptor = descriptorFor(loweredKey);
984+
const {
985+
descriptor: { rename, type, transform, deprecated },
986+
key
987+
} = descriptor;
988+
const name = rename ?? key;
989+
990+
if (deprecated) {
991+
console.warn(`${key} is a deprecated option`);
992+
}
993+
994+
switch (type) {
995+
case 'boolean':
996+
mongoOptions[name] = getBoolean(name, values[0]);
997+
break;
998+
case 'int':
999+
mongoOptions[name] = getInt(name, values[0]);
1000+
break;
1001+
case 'uint':
1002+
mongoOptions[name] = getUint(name, values[0]);
1003+
break;
1004+
case 'string':
1005+
if (values[0] === undefined) {
9371006
break;
9381007
}
1008+
mongoOptions[name] = String(values[0]);
1009+
break;
1010+
case 'record':
1011+
if (!isRecord(values[0])) {
1012+
throw new TypeError(`${name} must be an object`);
1013+
}
1014+
mongoOptions[name] = values[0];
1015+
break;
1016+
case 'asIs':
1017+
mongoOptions[name] = values[0];
1018+
break;
1019+
default: {
1020+
if (!transform) {
1021+
throw new MongoParseError('Descriptors missing a type must define a transform');
1022+
}
1023+
const transformValue = transform({ name, options: mongoOptions, values });
1024+
mongoOptions[name] = transformValue;
1025+
break;
9391026
}
9401027
}
1028+
}
9411029

942-
return Object.freeze(mongoOptions) as Readonly<MongoOptions>;
1030+
function toHostArray(hostString: string) {
1031+
const parsedHost = new URL(`mongodb://${hostString.split(' ').join('%20')}`);
1032+
1033+
let socketPath;
1034+
if (hostString.match(/\.sock/)) {
1035+
// heuristically determine if we're working with a domain socket
1036+
socketPath = decodeURIComponent(hostString);
1037+
}
1038+
1039+
const result: Host = socketPath
1040+
? {
1041+
host: socketPath,
1042+
type: 'unix'
1043+
}
1044+
: {
1045+
host: parsedHost.hostname,
1046+
port: parsedHost.port ? parseInt(parsedHost.port) : 27017,
1047+
type: 'tcp'
1048+
};
1049+
1050+
if (result.type === 'tcp' && result.port === 0) {
1051+
throw new MongoParseError('Invalid port (zero) with hostname');
1052+
}
1053+
1054+
return result;
9431055
}
9441056

9451057
interface OptionDescriptor {
@@ -983,7 +1095,16 @@ export const OPTIONS: Record<keyof MongoClientOptions, OptionDescriptor> = {
9831095
if (!mechanism) {
9841096
throw new TypeError(`authMechanism one of ${mechanisms}, got ${value}`);
9851097
}
986-
return new MongoCredentials({ ...options.credentials, mechanism });
1098+
let db; // some mechanisms have '$external' as the Auth Source
1099+
if (
1100+
mechanism === 'PLAIN' ||
1101+
mechanism === 'GSSAPI' ||
1102+
mechanism === 'MONGODB-AWS' ||
1103+
mechanism === 'MONGODB-X509'
1104+
) {
1105+
db = '$external';
1106+
}
1107+
return new MongoCredentials({ ...options.credentials, mechanism, db });
9871108
}
9881109
},
9891110
authMechanismProperties: {
@@ -1181,9 +1302,8 @@ export const OPTIONS: Record<keyof MongoClientOptions, OptionDescriptor> = {
11811302
minPoolSize: {
11821303
type: 'uint'
11831304
},
1184-
minSize: {
1185-
type: 'uint',
1186-
rename: 'minPoolSize'
1305+
minHeartbeatFrequencyMS: {
1306+
type: 'uint'
11871307
},
11881308
monitorCommands: {
11891309
type: 'boolean'
@@ -1209,6 +1329,15 @@ export const OPTIONS: Record<keyof MongoClientOptions, OptionDescriptor> = {
12091329
return new MongoCredentials({ ...options.credentials, password });
12101330
}
12111331
} as OptionDescriptor,
1332+
password: {
1333+
rename: 'credentials',
1334+
transform({ values: [password], options }) {
1335+
if (typeof password !== 'string') {
1336+
throw new TypeError('pass must be a string');
1337+
}
1338+
return new MongoCredentials({ ...options.credentials, password });
1339+
}
1340+
},
12121341
pkFactory: {
12131342
rename: 'createPk',
12141343
transform({ values: [value] }): PkFactory {
@@ -1226,10 +1355,6 @@ export const OPTIONS: Record<keyof MongoClientOptions, OptionDescriptor> = {
12261355
return { ...options.driverInfo, platform: String(value) };
12271356
}
12281357
} as OptionDescriptor,
1229-
poolSize: {
1230-
rename: 'maxPoolSize',
1231-
type: 'uint'
1232-
},
12331358
promiseLibrary: {
12341359
type: 'asIs'
12351360
},
@@ -1395,10 +1520,17 @@ export const OPTIONS: Record<keyof MongoClientOptions, OptionDescriptor> = {
13951520
type: 'boolean'
13961521
},
13971522
user: {
1523+
rename: 'credentials',
13981524
transform({ values: [value], options }) {
13991525
return new MongoCredentials({ ...options.credentials, username: String(value) });
14001526
}
14011527
} as OptionDescriptor,
1528+
username: {
1529+
rename: 'credentials',
1530+
transform({ values: [value], options }) {
1531+
return new MongoCredentials({ ...options.credentials, username: String(value) });
1532+
}
1533+
},
14021534
validateOptions: {
14031535
type: 'boolean'
14041536
},

0 commit comments

Comments
 (0)