Skip to content

Commit 68adaf1

Browse files
feat(NODE-3924)!: read tls files async (#3776)
Co-authored-by: Bailey Pearson <[email protected]>
1 parent b16ef9e commit 68adaf1

8 files changed

+155
-66
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@
150150
"check:oidc-azure": "mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_oidc_azure.prose.test.ts",
151151
"check:ocsp": "mocha --config test/manual/mocharc.json test/manual/ocsp_support.test.js",
152152
"check:kerberos": "nyc mocha --config test/manual/mocharc.json test/manual/kerberos.test.ts",
153-
"check:tls": "mocha --config test/manual/mocharc.json test/manual/tls_support.test.js",
153+
"check:tls": "mocha --config test/manual/mocharc.json test/manual/tls_support.test.ts",
154154
"check:ldap": "nyc mocha --config test/manual/mocharc.json test/manual/ldap.test.js",
155155
"check:socks5": "mocha --config test/manual/mocharc.json test/manual/socks5.test.ts",
156156
"check:csfle": "mocha --config test/mocha_mongodb.json test/integration/client-side-encryption",

src/connection_string.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as dns from 'dns';
2-
import * as fs from 'fs';
32
import ConnectionString from 'mongodb-connection-string-url';
43
import { URLSearchParams } from 'url';
54

@@ -1097,16 +1096,10 @@ export const OPTIONS = {
10971096
}
10981097
},
10991098
tlsCAFile: {
1100-
target: 'ca',
1101-
transform({ values: [value] }) {
1102-
return fs.readFileSync(String(value), { encoding: 'ascii' });
1103-
}
1099+
type: 'string'
11041100
},
11051101
tlsCertificateKeyFile: {
1106-
target: 'key',
1107-
transform({ values: [value] }) {
1108-
return fs.readFileSync(String(value), { encoding: 'ascii' });
1109-
}
1102+
type: 'string'
11101103
},
11111104
tlsCertificateKeyFilePassword: {
11121105
target: 'passphrase',

src/mongo_client.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { promises as fs } from 'fs';
12
import type { TcpNetConnectOpts } from 'net';
23
import type { ConnectionOptions as TLSConnectionOptions, TLSSocketOptions } from 'tls';
34
import { promisify } from 'util';
@@ -433,6 +434,14 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> {
433434

434435
const options = this[kOptions];
435436

437+
if (options.tls) {
438+
if (typeof options.tlsCAFile === 'string') {
439+
options.ca ??= await fs.readFile(options.tlsCAFile, { encoding: 'utf8' });
440+
}
441+
if (typeof options.tlsCertificateKeyFile === 'string') {
442+
options.key ??= await fs.readFile(options.tlsCertificateKeyFile, { encoding: 'utf8' });
443+
}
444+
}
436445
if (typeof options.srvHost === 'string') {
437446
const hosts = await resolveSRVRecord(options);
438447

@@ -768,7 +777,7 @@ export interface MongoOptions
768777
*
769778
* ### Additional options:
770779
*
771-
* | nodejs native option | driver spec compliant option name | driver option type |
780+
* | nodejs native option | driver spec equivalent option name | driver option type |
772781
* |:----------------------|:----------------------------------------------|:-------------------|
773782
* | `ca` | `tlsCAFile` | `string` |
774783
* | `crl` | N/A | `string` |
@@ -784,9 +793,20 @@ export interface MongoOptions
784793
* If `tlsInsecure` is set to `false`, then it will set the node native options `checkServerIdentity`
785794
* to a no-op and `rejectUnauthorized` to the inverse value of `tlsAllowInvalidCertificates`. If
786795
* `tlsAllowInvalidCertificates` is not set, then `rejectUnauthorized` will be set to `true`.
796+
*
797+
* ### Note on `tlsCAFile` and `tlsCertificateKeyFile`
798+
*
799+
* The files specified by the paths passed in to the `tlsCAFile` and `tlsCertificateKeyFile` fields
800+
* are read lazily on the first call to `MongoClient.connect`. Once these files have been read and
801+
* the `ca` and `key` fields are populated, they will not be read again on subsequent calls to
802+
* `MongoClient.connect`. As a result, until the first call to `MongoClient.connect`, the `ca`
803+
* and `key` fields will be undefined.
787804
*/
788805
tls: boolean;
789806

807+
tlsCAFile?: string;
808+
tlsCertificateKeyFile?: string;
809+
790810
/** @internal */
791811
[featureFlag: symbol]: any;
792812

test/manual/mocharc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
"require": "ts-node/register",
33
"reporter": "test/tools/reporter/mongodb_reporter.js",
44
"failZero": true,
5-
"color": true
5+
"color": true,
6+
"timeout": 10000
67
}

test/manual/tls_support.test.js

Lines changed: 0 additions & 44 deletions
This file was deleted.

test/manual/tls_support.test.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { expect } from 'chai';
2+
import { promises as fs } from 'fs';
3+
4+
import { LEGACY_HELLO_COMMAND, MongoClient, type MongoClientOptions } from '../mongodb';
5+
6+
const REQUIRED_ENV = ['MONGODB_URI', 'SSL_KEY_FILE', 'SSL_CA_FILE'];
7+
8+
describe('TLS Support', function () {
9+
for (const key of REQUIRED_ENV) {
10+
if (process.env[key] == null) {
11+
throw new Error(`skipping SSL tests, ${key} environment variable is not defined`);
12+
}
13+
}
14+
15+
const CONNECTION_STRING = process.env.MONGODB_URI as string;
16+
const TLS_CERT_KEY_FILE = process.env.SSL_KEY_FILE as string;
17+
const TLS_CA_FILE = process.env.SSL_CA_FILE as string;
18+
const tlsSettings = {
19+
tls: true,
20+
tlsCertificateKeyFile: TLS_CERT_KEY_FILE,
21+
tlsCAFile: TLS_CA_FILE
22+
};
23+
24+
it(
25+
'should connect with tls via client options',
26+
makeConnectionTest(CONNECTION_STRING, tlsSettings)
27+
);
28+
29+
it(
30+
'should connect with tls via url options',
31+
makeConnectionTest(
32+
`${CONNECTION_STRING}?${Object.keys(tlsSettings)
33+
.map(key => `${key}=${tlsSettings[key]}`)
34+
.join('&')}`
35+
)
36+
);
37+
38+
context('when tls filepaths are provided', () => {
39+
let client: MongoClient;
40+
afterEach(async () => {
41+
if (client) await client.close();
42+
});
43+
44+
context('when tls filepaths have length > 0', () => {
45+
beforeEach(async () => {
46+
client = new MongoClient(CONNECTION_STRING, tlsSettings);
47+
});
48+
49+
it('should read in files async at connect time', async () => {
50+
expect(client.options).property('tlsCAFile', TLS_CA_FILE);
51+
expect(client.options).property('tlsCertificateKeyFile', TLS_CERT_KEY_FILE);
52+
expect(client.options).not.have.property('ca');
53+
expect(client.options).not.have.property('key');
54+
55+
await client.connect();
56+
57+
expect(client.options).property('ca').to.exist;
58+
expect(client.options).property('key').to.exist;
59+
});
60+
61+
context('when client has been opened and closed more than once', function () {
62+
it('should only read files once', async () => {
63+
await client.connect();
64+
await client.close();
65+
66+
const caFileAccessTime = (await fs.stat(TLS_CA_FILE)).atime;
67+
const certKeyFileAccessTime = (await fs.stat(TLS_CERT_KEY_FILE)).atime;
68+
69+
await client.connect();
70+
71+
expect((await fs.stat(TLS_CA_FILE)).atime).to.deep.equal(caFileAccessTime);
72+
expect((await fs.stat(TLS_CERT_KEY_FILE)).atime).to.deep.equal(certKeyFileAccessTime);
73+
});
74+
});
75+
});
76+
77+
context('when tlsCAFile has length === 0', () => {
78+
beforeEach(() => {
79+
client = new MongoClient(CONNECTION_STRING, {
80+
tls: true,
81+
tlsCAFile: '',
82+
tlsCertificateKeyFile: TLS_CERT_KEY_FILE
83+
});
84+
});
85+
86+
it('should throw an error at connect time', async () => {
87+
const err = await client.connect().catch(e => e);
88+
89+
expect(err).to.be.instanceof(Error);
90+
});
91+
});
92+
93+
context('when tlsCertificateKeyFile has length === 0', () => {
94+
beforeEach(() => {
95+
client = new MongoClient(CONNECTION_STRING, {
96+
tls: true,
97+
tlsCAFile: TLS_CA_FILE,
98+
tlsCertificateKeyFile: ''
99+
});
100+
});
101+
102+
it('should throw an error at connect time', async () => {
103+
const err = await client.connect().catch(e => e);
104+
105+
expect(err).to.be.instanceof(Error);
106+
});
107+
});
108+
});
109+
});
110+
111+
function makeConnectionTest(connectionString: string, clientOptions?: MongoClientOptions) {
112+
return async function () {
113+
const client = new MongoClient(connectionString, clientOptions);
114+
115+
await client.connect();
116+
await client.db('admin').command({ [LEGACY_HELLO_COMMAND]: 1 });
117+
await client.db('test').collection('test').findOne({});
118+
return await client.close();
119+
};
120+
}

test/tools/uri_spec_runner.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,13 +314,13 @@ export function executeUriValidationTest(
314314
.equal(optionValue);
315315
break;
316316
case 'tlsCertificateKeyFile':
317-
expectedProp = 'key';
317+
expectedProp = 'tlsCertificateKeyFile';
318318
expect(options, `${errorMessage} ${optionKey} -> ${expectedProp}`)
319319
.to.have.property(expectedProp)
320320
.equal(optionValue);
321321
break;
322322
case 'tlsCAFile':
323-
expectedProp = 'ca';
323+
expectedProp = 'tlsCAFile';
324324
expect(options, `${errorMessage} ${optionKey} -> ${expectedProp}`)
325325
.to.have.property(expectedProp)
326326
.equal(optionValue);

test/unit/mongo_client.test.js

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ describe('MongoOptions', function () {
4444
*
4545
* ### Additional options:
4646
*
47-
* | nodejs native option | driver spec compliant option name | driver option type |
47+
* | nodejs native option | driver spec equivalent option name | driver option type |
4848
* |:----------------------|:----------------------------------------------|:-------------------|
4949
* | `ca` | `tlsCAFile` | `string` |
5050
* | `crl` | N/A | `string` |
@@ -55,12 +55,11 @@ describe('MongoOptions', function () {
5555
* | see note below | `tlsInsecure` | `boolean` |
5656
*
5757
*/
58-
expect(options).to.not.have.property('tlsCertificateKeyFile');
59-
expect(options).to.not.have.property('tlsCAFile');
6058
expect(options).to.not.have.property('tlsCertificateKeyFilePassword');
61-
expect(options).has.property('ca', '');
62-
expect(options).has.property('key');
63-
expect(options.key).has.length(0);
59+
expect(options).to.not.have.property('key');
60+
expect(options).to.not.have.property('ca');
61+
expect(options).to.have.property('tlsCertificateKeyFile', filename);
62+
expect(options).to.have.property('tlsCAFile', filename);
6463
expect(options).has.property('passphrase', 'tlsCertificateKeyFilePassword');
6564
expect(options).has.property('tls', true);
6665
});
@@ -394,10 +393,10 @@ describe('MongoOptions', function () {
394393
const optsFromObject = parseOptions('mongodb://localhost/', {
395394
tlsCertificateKeyFile: 'testCertKey.pem'
396395
});
397-
expect(optsFromObject).to.have.property('key', 'cert key');
396+
expect(optsFromObject).to.have.property('tlsCertificateKeyFile', 'testCertKey.pem');
398397

399398
const optsFromUri = parseOptions('mongodb://localhost?tlsCertificateKeyFile=testCertKey.pem');
400-
expect(optsFromUri).to.have.property('key', 'cert key');
399+
expect(optsFromUri).to.have.property('tlsCertificateKeyFile', 'testCertKey.pem');
401400
});
402401
});
403402

0 commit comments

Comments
 (0)