Skip to content

Commit 1135789

Browse files
committed
fix: normalize domains to ensure FQDN equality
1 parent 2c37c13 commit 1135789

File tree

1 file changed

+47
-41
lines changed

1 file changed

+47
-41
lines changed

src/index.ts

Lines changed: 47 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { promisify } from 'util';
2-
import { URL, URLSearchParams } from 'whatwg-url';
1+
import { promisify } from "util";
2+
import { URL, URLSearchParams } from "whatwg-url";
33

44
class MongoParseError extends Error {}
55

@@ -11,114 +11,120 @@ type Options = {
1111
};
1212
};
1313

14-
const ALLOWED_TXT_OPTIONS: Readonly<string[]> = ['authSource', 'replicaSet', 'loadBalanced'];
14+
const ALLOWED_TXT_OPTIONS: Readonly<string[]> = ["authSource", "replicaSet", "loadBalanced"];
1515

16-
function matchesParentDomain (srvAddress: string, parentDomain: string): boolean {
16+
function matchesParentDomain(srvAddress: string, parentDomain: string): boolean {
1717
const regex = /^.*?\./;
18-
const srv = `.${srvAddress.replace(regex, '')}`;
19-
const parent = `.${parentDomain.replace(regex, '')}`;
18+
const srv = `.${(srvAddress.endsWith(".") ? srvAddress.slice(0, -1) : srvAddress).replace(regex, "")}`;
19+
const parent = `.${(parentDomain.endsWith(".") ? parentDomain.slice(0, -1) : parentDomain).replace(regex, "")}`;
2020
return srv.endsWith(parent);
2121
}
2222

23-
async function resolveDnsSrvRecord (dns: NonNullable<Options['dns']>, lookupAddress: string, srvServiceName: string): Promise<string[]> {
23+
async function resolveDnsSrvRecord(
24+
dns: NonNullable<Options["dns"]>,
25+
lookupAddress: string,
26+
srvServiceName: string
27+
): Promise<string[]> {
2428
const addresses = await promisify(dns.resolveSrv)(`_${srvServiceName}._tcp.${lookupAddress}`);
2529
if (!addresses?.length) {
26-
throw new MongoParseError('No addresses found at host');
30+
throw new MongoParseError("No addresses found at host");
2731
}
2832

2933
for (const { name } of addresses) {
3034
if (!matchesParentDomain(name, lookupAddress)) {
31-
throw new MongoParseError('Server record does not share hostname with parent URI');
35+
throw new MongoParseError("Server record does not share hostname with parent URI");
3236
}
3337
}
3438

35-
return addresses.map(r => r.name + ((r.port ?? 27017) === 27017 ? '' : `:${r.port}`));
39+
return addresses.map((r) => r.name + ((r.port ?? 27017) === 27017 ? "" : `:${r.port}`));
3640
}
3741

38-
async function resolveDnsTxtRecord (dns: NonNullable<Options['dns']>, lookupAddress: string): Promise<URLSearchParams> {
42+
async function resolveDnsTxtRecord(dns: NonNullable<Options["dns"]>, lookupAddress: string): Promise<URLSearchParams> {
3943
let records: string[][] | undefined;
4044
try {
4145
records = await promisify(dns.resolveTxt)(lookupAddress);
4246
} catch (err: any) {
43-
if (err?.code && (err.code !== 'ENODATA' && err.code !== 'ENOTFOUND')) {
47+
if (err?.code && err.code !== "ENODATA" && err.code !== "ENOTFOUND") {
4448
throw err;
4549
}
4650
}
4751

4852
let txtRecord: string;
4953
if (records && records.length > 1) {
50-
throw new MongoParseError('Multiple text records not allowed');
54+
throw new MongoParseError("Multiple text records not allowed");
5155
} else {
52-
txtRecord = records?.[0]?.join('') ?? '';
56+
txtRecord = records?.[0]?.join("") ?? "";
5357
}
5458

5559
const txtRecordOptions = new URLSearchParams(txtRecord);
5660
const txtRecordOptionKeys = [...txtRecordOptions.keys()];
57-
if (txtRecordOptionKeys.some(key => !ALLOWED_TXT_OPTIONS.includes(key))) {
58-
throw new MongoParseError(`Text record must only set ${ALLOWED_TXT_OPTIONS.join(', ')}`);
61+
if (txtRecordOptionKeys.some((key) => !ALLOWED_TXT_OPTIONS.includes(key))) {
62+
throw new MongoParseError(`Text record must only set ${ALLOWED_TXT_OPTIONS.join(", ")}`);
5963
}
6064

61-
const source = txtRecordOptions.get('authSource') ?? undefined;
62-
const replicaSet = txtRecordOptions.get('replicaSet') ?? undefined;
63-
const loadBalanced = txtRecordOptions.get('loadBalanced') ?? undefined;
65+
const source = txtRecordOptions.get("authSource") ?? undefined;
66+
const replicaSet = txtRecordOptions.get("replicaSet") ?? undefined;
67+
const loadBalanced = txtRecordOptions.get("loadBalanced") ?? undefined;
6468

65-
if (source === '' || replicaSet === '' || loadBalanced === '') {
66-
throw new MongoParseError('Cannot have empty URI params in DNS TXT Record');
69+
if (source === "" || replicaSet === "" || loadBalanced === "") {
70+
throw new MongoParseError("Cannot have empty URI params in DNS TXT Record");
6771
}
6872

69-
if (loadBalanced !== undefined && loadBalanced !== 'true' && loadBalanced !== 'false') {
70-
throw new MongoParseError(`DNS TXT Record contains invalid value ${loadBalanced} for loadBalanced option (allowed: true, false)`);
73+
if (loadBalanced !== undefined && loadBalanced !== "true" && loadBalanced !== "false") {
74+
throw new MongoParseError(
75+
`DNS TXT Record contains invalid value ${loadBalanced} for loadBalanced option (allowed: true, false)`
76+
);
7177
}
7278

7379
return txtRecordOptions;
7480
}
7581

76-
async function resolveMongodbSrv (input: string, options?: Options): Promise<string> {
77-
const dns = options?.dns ?? require('dns');
82+
async function resolveMongodbSrv(input: string, options?: Options): Promise<string> {
83+
const dns = options?.dns ?? require("dns");
7884

79-
if (input.startsWith('mongodb://')) {
85+
if (input.startsWith("mongodb://")) {
8086
return input;
8187
}
82-
if (!input.startsWith('mongodb+srv://')) {
83-
throw new MongoParseError('Unknown URL scheme');
88+
if (!input.startsWith("mongodb+srv://")) {
89+
throw new MongoParseError("Unknown URL scheme");
8490
}
8591

8692
const url = new URL(input);
8793
if (url.port) {
88-
throw new Error('mongodb+srv:// URL cannot have port number');
94+
throw new Error("mongodb+srv:// URL cannot have port number");
8995
}
9096

9197
const lookupAddress = url.hostname;
92-
const srvServiceName = url.searchParams.get('srvServiceName') || 'mongodb';
93-
const srvMaxHosts = +(url.searchParams.get('srvMaxHosts') || '0');
98+
const srvServiceName = url.searchParams.get("srvServiceName") || "mongodb";
99+
const srvMaxHosts = +(url.searchParams.get("srvMaxHosts") || "0");
94100

95101
const [srvResult, txtResult] = await Promise.all([
96102
resolveDnsSrvRecord(dns, lookupAddress, srvServiceName),
97-
resolveDnsTxtRecord(dns, lookupAddress)
103+
resolveDnsTxtRecord(dns, lookupAddress),
98104
]);
99105

100106
if (srvMaxHosts && srvMaxHosts < srvResult.length) {
101107
// Replace srvResult with shuffled + limited srvResult
102108
srvResult.splice(0, srvResult.length, ...shuffle(srvResult, srvMaxHosts));
103109
}
104110

105-
url.protocol = 'mongodb:';
106-
url.hostname = '__DUMMY_HOSTNAME__';
111+
url.protocol = "mongodb:";
112+
url.hostname = "__DUMMY_HOSTNAME__";
107113
if (!url.pathname) {
108-
url.pathname = '/';
114+
url.pathname = "/";
109115
}
110116
for (const [key, value] of txtResult) {
111117
if (!url.searchParams.has(key)) {
112118
url.searchParams.set(key, value);
113119
}
114120
}
115-
if (!url.searchParams.has('tls') && !url.searchParams.has('ssl')) {
116-
url.searchParams.set('tls', 'true');
121+
if (!url.searchParams.has("tls") && !url.searchParams.has("ssl")) {
122+
url.searchParams.set("tls", "true");
117123
}
118-
url.searchParams.delete('srvServiceName');
119-
url.searchParams.delete('srvMaxHosts');
124+
url.searchParams.delete("srvServiceName");
125+
url.searchParams.delete("srvMaxHosts");
120126

121-
return url.toString().replace('__DUMMY_HOSTNAME__', srvResult.join(','));
127+
return url.toString().replace("__DUMMY_HOSTNAME__", srvResult.join(","));
122128
}
123129

124130
/**
@@ -129,7 +135,7 @@ async function resolveMongodbSrv (input: string, options?: Options): Promise<str
129135
* @param sequence - items to be shuffled
130136
* @param limit - Defaults to `0`. If nonzero shuffle will slice the randomized array e.g, `.slice(0, limit)` otherwise will return the entire randomized array.
131137
*/
132-
function shuffle<T> (sequence: Iterable<T>, limit = 0): Array<T> {
138+
function shuffle<T>(sequence: Iterable<T>, limit = 0): Array<T> {
133139
const items = Array.from(sequence); // shallow copy in order to never shuffle the input
134140

135141
limit = Math.min(limit, items.length);

0 commit comments

Comments
 (0)