Skip to content

Commit c305532

Browse files
authored
fix(service-provider-core): allow non-hostname chars in DB name MONGOSH-792 (#906)
This also means that e.g. `host:port?option=value` is not allowed, and instead any optional part after the port in the non-URL variant needs to be separated by a slash (which was also the case in the legacy shell).
1 parent 5b86471 commit c305532

File tree

4 files changed

+90
-63
lines changed

4 files changed

+90
-63
lines changed

packages/cli-repl/test/e2e-direct.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ describe('e2e direct connection', () => {
185185
});
186186
it('when specifying multiple seeds through --host with a wrong replsetid', async() => {
187187
const hostlist = 'wrongreplset/' + await rs2.hostport() + ',' + await rs1.hostport() + ',' + await rs0.hostport();
188-
const shell = TestShell.start({ args: ['--host', hostlist, 'admin?serverSelectionTimeoutMS=2000'] });
188+
const shell = TestShell.start({ args: ['--host', hostlist, 'admin'] });
189189
await shell.waitForExit();
190190
shell.assertContainsOutput('MongoServerSelectionError');
191191
});

packages/cli-repl/test/e2e.spec.ts

Lines changed: 40 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -77,57 +77,51 @@ describe('e2e', function() {
7777
expect(await onExit).to.equal(0);
7878
});
7979
});
80-
8180
describe('set db', () => {
82-
describe('via host:port/test', () => {
83-
let shell;
84-
beforeEach(async() => {
85-
shell = TestShell.start({ args: [`${await testServer.hostport()}/testdb1`] });
86-
await shell.waitForPrompt();
87-
shell.assertNoErrors();
88-
});
89-
it('db set correctly', async() => {
90-
await shell.executeLine('db');
91-
shell.assertNoErrors();
92-
93-
await eventually(() => {
94-
shell.assertContainsOutput('testdb1');
81+
for (const { mode, dbname, dbnameUri } of [
82+
{ mode: 'no special characetrs', dbname: 'testdb1', dbnameUri: 'testdb1' },
83+
{ mode: 'special characters', dbname: 'ä:-,🐈_\'[!?%', dbnameUri: 'ä:-,🐈_\'[!%3F%25' }
84+
]) {
85+
context(mode, () => {
86+
describe('via host:port/test', () => {
87+
let shell;
88+
beforeEach(async() => {
89+
shell = TestShell.start({ args: [`${await testServer.hostport()}/${dbname}`] });
90+
await shell.waitForPrompt();
91+
shell.assertNoErrors();
92+
});
93+
it('db set correctly', async() => {
94+
expect(await shell.executeLine('db')).to.include(dbname);
95+
shell.assertNoErrors();
96+
});
9597
});
96-
});
97-
});
98-
describe('via mongodb://uri', () => {
99-
let shell;
100-
beforeEach(async() => {
101-
shell = TestShell.start({ args: [`mongodb://${await testServer.hostport()}/testdb2`] });
102-
await shell.waitForPrompt();
103-
shell.assertNoErrors();
104-
});
105-
it('db set correctly', async() => {
106-
await shell.executeLine('db');
107-
shell.assertNoErrors();
108-
109-
await eventually(() => {
110-
shell.assertContainsOutput('testdb2');
98+
describe('via mongodb://uri', () => {
99+
let shell;
100+
beforeEach(async() => {
101+
shell = TestShell.start({ args: [`mongodb://${await testServer.hostport()}/${dbnameUri}`] });
102+
await shell.waitForPrompt();
103+
shell.assertNoErrors();
104+
});
105+
it('db set correctly', async() => {
106+
expect(await shell.executeLine('db')).to.include(dbname);
107+
shell.assertNoErrors();
108+
});
111109
});
112-
});
113-
});
114-
describe('legacy db only', () => {
115-
let shell;
116-
beforeEach(async() => {
117-
const port = await testServer.port();
118-
shell = TestShell.start({ args: ['testdb3', `--port=${port}`] });
119-
await shell.waitForPrompt();
120-
shell.assertNoErrors();
121-
});
122-
it('db set correctly', async() => {
123-
await shell.executeLine('db');
124-
shell.assertNoErrors();
125-
126-
await eventually(() => {
127-
shell.assertContainsOutput('testdb3');
110+
describe('legacy db only', () => {
111+
let shell;
112+
beforeEach(async() => {
113+
const port = await testServer.port();
114+
shell = TestShell.start({ args: [dbname, `--port=${port}`] });
115+
await shell.waitForPrompt();
116+
shell.assertNoErrors();
117+
});
118+
it('db set correctly', async() => {
119+
expect(await shell.executeLine('db')).to.include(dbname);
120+
shell.assertNoErrors();
121+
});
128122
});
129123
});
130-
});
124+
}
131125
});
132126

133127
describe('with connection string', () => {

packages/service-provider-core/src/uri-generator.spec.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,30 @@ describe('uri-generator.generate-uri', () => {
183183
});
184184
});
185185

186+
context('when no additional options are provided with db with special characters', () => {
187+
const uri = '192.0.0.1:27018/föö-:?%ab💙,\'_.c';
188+
const options = { connectionSpecifier: uri };
189+
190+
it('returns the uri with the scheme', () => {
191+
expect(generateUri(options)).to.equal('mongodb://192.0.0.1:27018/f%C3%B6%C3%B6-%3A%3F%25ab%F0%9F%92%99%2C\'_.c?directConnection=true');
192+
});
193+
});
194+
195+
context('when the db part does not start with a slash', () => {
196+
const uri = '192.0.0.1:27018?foo=bar';
197+
const options = { connectionSpecifier: uri };
198+
199+
it('throws an exception', () => {
200+
try {
201+
generateUri(options);
202+
expect.fail('expected error');
203+
} catch (e) {
204+
expect(e).to.be.instanceOf(MongoshInvalidInputError);
205+
expect(e.code).to.equal(CommonErrors.InvalidArgument);
206+
}
207+
});
208+
});
209+
186210
context('when additional options are provided', () => {
187211
context('when providing host with URI', () => {
188212
const uri = '192.0.0.1:27018/foo';
@@ -277,23 +301,23 @@ describe('uri-generator.generate-uri', () => {
277301

278302
context('when providing a URI with query parameters', () => {
279303
context('that do not conflict with directConnection', () => {
280-
const uri = '192.0.0.1:27018?readPreference=primary';
304+
const uri = 'mongodb://192.0.0.1:27018/?readPreference=primary';
281305
const options = { connectionSpecifier: uri };
282306
it('still includes directConnection', () => {
283307
expect(generateUri(options)).to.equal('mongodb://192.0.0.1:27018/?readPreference=primary&directConnection=true');
284308
});
285309
});
286310

287311
context('including replicaSet', () => {
288-
const uri = '192.0.0.1:27018/db?replicaSet=replicaset';
312+
const uri = 'mongodb://192.0.0.1:27018/db?replicaSet=replicaset';
289313
const options = { connectionSpecifier: uri };
290314
it('does not add the directConnection parameter', () => {
291-
expect(generateUri(options)).to.equal(`mongodb://${uri}`);
315+
expect(generateUri(options)).to.equal(uri);
292316
});
293317
});
294318

295319
context('including explicit directConnection', () => {
296-
const uri = '192.0.0.1:27018?directConnection=false';
320+
const uri = 'mongodb://192.0.0.1:27018/?directConnection=false';
297321
const options = { connectionSpecifier: uri };
298322
it('does not change the directConnection parameter', () => {
299323
expect(generateUri(options)).to.equal('mongodb://192.0.0.1:27018/?directConnection=false');
@@ -346,5 +370,10 @@ describe('uri-generator.generate-uri', () => {
346370
const options = { host: 'replsetname/host1:123,host2,host3:456', connectionSpecifier: 'admin' };
347371
expect(generateUri(options)).to.equal('mongodb://host1:123,host2,host3:456/admin?replicaSet=replsetname');
348372
});
373+
374+
it('returns a URI for the hosts and ports specified in --host and database name with escaped chars', () => {
375+
const options = { host: 'replsetname/host1:123,host2,host3:456', connectionSpecifier: 'admin?foo=bar' };
376+
expect(generateUri(options)).to.equal('mongodb://host1:123,host2,host3:456/admin%3Ffoo%3Dbar?replicaSet=replsetname');
377+
});
349378
});
350379
});

packages/service-provider-core/src/uri-generator.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ function generateUri(options: CliOptions): string {
129129
return '';
130130
}
131131
const connectionString = generateUriNormalized(options);
132-
if (connectionString.hosts.length === 1 &&
133-
['localhost', '127.0.0.1'].includes(connectionString.hosts[0].split(':')[0])) {
132+
if (connectionString.hosts.every(host =>
133+
['localhost', '127.0.0.1'].includes(host.split(':')[0]))) {
134134
const params = connectionString.searchParams;
135135
if (!params.has('serverSelectionTimeoutMS')) {
136136
params.set('serverSelectionTimeoutMS', '2000');
@@ -147,7 +147,7 @@ function generateUriNormalized(options: CliOptions): ConnectionString {
147147
/^(?<replSetName>[^/]+)\/(?<hosts>([A-Za-z0-9.-]+(:\d+)?,?)+)$/);
148148
if (replSetHostMatch) {
149149
const { replSetName, hosts } = replSetHostMatch.groups as { replSetName: string, hosts: string };
150-
const connectionString = new ConnectionString(`${Scheme.Mongo}replacemeHost/${uri ?? DEFAULT_DB}`);
150+
const connectionString = new ConnectionString(`${Scheme.Mongo}replacemeHost/${encodeURIComponent(uri ?? DEFAULT_DB)}`);
151151
connectionString.hosts = hosts.split(',').filter(host => host.trim());
152152
connectionString.searchParams.set('replicaSet', replSetName);
153153
return addShellConnectionStringParameters(connectionString);
@@ -169,17 +169,22 @@ function generateUriNormalized(options: CliOptions): ConnectionString {
169169
}
170170

171171
// Capture host, port and db from the string and generate a URI from
172-
// the parts.
173-
const uriMatch = /^([A-Za-z0-9][A-Za-z0-9.-]+):?(\d+)?[\/]?(\S+)?$/gi;
174-
const parts = uriMatch.exec(uri);
172+
// the parts. If there is a db part, it *must* start with /.
173+
const uriMatch = /^([A-Za-z0-9][A-Za-z0-9.-]+):?(\d+)?(?:\/(\S*))?$/gi;
174+
let parts: string[] | null = uriMatch.exec(uri);
175175

176176
if (parts === null) {
177-
throw new MongoshInvalidInputError(`Invalid URI: ${uri}`, CommonErrors.InvalidArgument);
177+
if (/[/\\. "$]/.test(uri)) {
178+
// This cannot be a database name because 'uri' contains characters invalid in a database.
179+
throw new MongoshInvalidInputError(`Invalid URI: ${uri}`, CommonErrors.InvalidArgument);
180+
} else {
181+
parts = [ uri, uri ];
182+
}
178183
}
179184

180-
let host: string | undefined = parts[1];
181-
const port = parts[2];
182-
let dbAndQueryString = parts[3];
185+
let host: string | undefined = parts?.[1];
186+
const port = parts?.[2];
187+
let dbAndQueryString = parts?.[3];
183188

184189
// If there is no port and db, host becomes db if there is no
185190
// '.' in the string. (legacy shell behaviour)
@@ -193,9 +198,8 @@ function generateUriNormalized(options: CliOptions): ConnectionString {
193198
if (host || port) {
194199
validateConflicts(options);
195200
}
196-
197201
return addShellConnectionStringParameters(new ConnectionString(
198-
`${Scheme.Mongo}${host || generateHost(options)}:${port || generatePort(options)}/${dbAndQueryString ?? DEFAULT_DB}`));
202+
`${Scheme.Mongo}${host || generateHost(options)}:${port || generatePort(options)}/${encodeURIComponent(dbAndQueryString || DEFAULT_DB)}`));
199203
}
200204

201205
/**

0 commit comments

Comments
 (0)