From ac4aabc9cf49c78ca5854b0185e2fc5ae983d3cf Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Fri, 25 Jun 2021 16:37:46 +0200 Subject: [PATCH] chore(NODE-2998): move connection-string parsing into separate package (#2848) --- package-lock.json | 37 +++++++++++++-- package.json | 3 +- src/connection_string.ts | 68 ++-------------------------- test/functional/mongo_client.test.js | 2 +- test/tools/runner/config.js | 5 +- 5 files changed, 43 insertions(+), 72 deletions(-) diff --git a/package-lock.json b/package-lock.json index 51c70a3503..2ae622d34a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4556,8 +4556,7 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.camelcase": { "version": "4.3.0", @@ -5243,6 +5242,14 @@ } } }, + "mongodb-connection-string-url": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-1.0.0.tgz", + "integrity": "sha512-s2kSyqM/PgvLHKzc67eK/syC4n2eXu4Q2XWA4gJOgMvOk1/bsZ8jW04EBFabfNHgw66gYSovx9F623eKEkpfGA==", + "requires": { + "whatwg-url": "^8.4.0" + } + }, "mongodb-mock-server": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mongodb-mock-server/-/mongodb-mock-server-2.0.1.tgz", @@ -6179,8 +6186,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "pupa": { "version": "2.1.1", @@ -7554,6 +7560,14 @@ "punycode": "^2.1.1" } }, + "tr46": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "requires": { + "punycode": "^2.1.1" + } + }, "trim-newlines": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.0.tgz", @@ -8074,6 +8088,21 @@ "defaults": "^1.0.3" } }, + "webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==" + }, + "whatwg-url": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.6.0.tgz", + "integrity": "sha512-os0KkeeqUOl7ccdDT1qqUcS4KH4tcBTSKK5Nl5WKb2lyxInIZ/CpjkqKa1Ss12mjfdcRX9mHmPPs7/SxG1Hbdw==", + "requires": { + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 388ee12323..e5e6909396 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ }, "dependencies": { "bson": "^4.4.0", - "denque": "^1.5.0" + "denque": "^1.5.0", + "mongodb-connection-string-url": "^1.0.0" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.1", diff --git a/src/connection_string.ts b/src/connection_string.ts index c3e6978a82..831e19dd26 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -1,6 +1,7 @@ import * as dns from 'dns'; import * as fs from 'fs'; -import { URL, URLSearchParams } from 'url'; +import ConnectionString from 'mongodb-connection-string-url'; +import { URLSearchParams } from 'url'; import { AuthMechanism } from './cmap/auth/defaultAuthProviders'; import { ReadPreference, ReadPreferenceMode } from './read_preference'; import { ReadConcern, ReadConcernLevel } from './read_concern'; @@ -146,68 +147,6 @@ export function checkTLSOptions(options: AnyOptions): void { check('tlsDisableCertificateRevocationCheck', 'tlsDisableOCSPEndpointCheck'); } -const HOSTS_REGEX = new RegExp( - String.raw`(?mongodb(?:\+srv|)):\/\/(?:(?[^:]*)(?::(?[^@]*))?@)?(?(?!:)[^\/?@]+)(?.*)` -); - -/** @internal */ -export function parseURI(uri: string): { isSRV: boolean; url: URL; hosts: string[] } { - const match = uri.match(HOSTS_REGEX); - if (!match) { - throw new MongoParseError(`Invalid connection string ${uri}`); - } - - const protocol = match.groups?.protocol; - const username = match.groups?.username; - const password = match.groups?.password; - const hosts = match.groups?.hosts; - const rest = match.groups?.rest; - - if (!protocol || !hosts) { - throw new MongoParseError('Invalid connection string, protocol and host(s) required'); - } - - decodeURIComponent(username ?? ''); - decodeURIComponent(password ?? ''); - - // characters not permitted in username nor password Set([':', '/', '?', '#', '[', ']', '@']) - const illegalCharacters = new RegExp(String.raw`[:/?#\[\]@]`, 'gi'); - if (username?.match(illegalCharacters)) { - throw new MongoParseError(`Username contains unescaped characters ${username}`); - } - if (!username || !password) { - const uriWithoutProtocol = uri.replace(`${protocol}://`, ''); - if (uriWithoutProtocol.startsWith('@') || uriWithoutProtocol.startsWith(':')) { - throw new MongoParseError('URI contained empty userinfo section'); - } - } - - if (password?.match(illegalCharacters)) { - throw new MongoParseError('Password contains unescaped characters'); - } - - let authString = ''; - if (typeof username === 'string') authString += username; - if (typeof password === 'string') authString += `:${password}`; - - const isSRV = protocol.includes('srv'); - const hostList = hosts.split(','); - const url = new URL(`${protocol.toLowerCase()}://${authString}@dummyHostname${rest}`); - - if (isSRV && hostList.length !== 1) { - throw new MongoParseError('mongodb+srv URI cannot have multiple service names'); - } - if (isSRV && hostList[0].includes(':')) { - throw new MongoParseError('mongodb+srv URI cannot have port number'); - } - - return { - isSRV, - url, - hosts: hosts.split(',') - }; -} - const TRUTHS = new Set(['true', 't', '1', 'y', 'yes']); const FALSEHOODS = new Set(['false', 'f', '0', 'n', 'no', '-1']); function getBoolean(name: string, value: unknown): boolean { @@ -285,7 +224,8 @@ export function parseOptions( mongoClient = undefined; } - const { url, hosts, isSRV } = parseURI(uri); + const url = new ConnectionString(uri); + const { hosts, isSRV } = url; const mongoOptions = Object.create(null); mongoOptions.hosts = isSRV ? [] : hosts.map(HostAddress.fromString); diff --git a/test/functional/mongo_client.test.js b/test/functional/mongo_client.test.js index 250df655cd..c1575c1355 100644 --- a/test/functional/mongo_client.test.js +++ b/test/functional/mongo_client.test.js @@ -66,7 +66,7 @@ describe('MongoClient', function () { }, test() { expect(() => this.configuration.newClient('user:password@localhost:27017/test')).to.throw( - 'Invalid connection string user:password@localhost:27017/test' + 'Invalid connection string "user:password@localhost:27017/test"' ); } }); diff --git a/test/tools/runner/config.js b/test/tools/runner/config.js index 34b2435fe9..4943a2da1b 100644 --- a/test/tools/runner/config.js +++ b/test/tools/runner/config.js @@ -1,4 +1,5 @@ 'use strict'; +const ConnectionString = require('mongodb-connection-string-url').default; const url = require('url'); const qs = require('querystring'); const { expect } = require('chai'); @@ -6,7 +7,6 @@ const { expect } = require('chai'); const { MongoClient } = require('../../../src/mongo_client'); const { Topology } = require('../../../src/sdam/topology'); const { TopologyType } = require('../../../src/sdam/common'); -const { parseURI } = require('../../../src/connection_string'); const { HostAddress } = require('../../../src/utils'); /** @@ -35,7 +35,8 @@ function convertToConnStringMap(obj) { class TestConfiguration { constructor(uri, context) { - const { url, hosts } = parseURI(uri); + const url = new ConnectionString(uri); + const { hosts } = url; const hostAddresses = hosts.map(HostAddress.fromString); this.topologyType = context.topologyType; this.version = context.version;