From 6321b2c413bdb817f69eb111089b5d3afdb4eabd Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 29 Jul 2024 14:51:24 +0200 Subject: [PATCH 1/3] feat(NODE-5754): allow auto select family options --- .evergreen/run-typescript.sh | 0 src/cmap/connect.ts | 6 ++ src/connection_string.ts | 6 ++ src/mongo_client.ts | 2 +- .../node-specific/mongo_client.test.ts | 53 +++++++++++++++++ test/manual/mocharc.json | 5 +- test/manual/tls_support.test.ts | 59 +++++++++++++++++++ 7 files changed, 129 insertions(+), 2 deletions(-) mode change 100644 => 100755 .evergreen/run-typescript.sh diff --git a/.evergreen/run-typescript.sh b/.evergreen/run-typescript.sh old mode 100644 new mode 100755 diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index 58c3519fed..9a4ec80dc0 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -269,6 +269,8 @@ export const LEGAL_TLS_SOCKET_OPTIONS = [ /** @public */ export const LEGAL_TCP_SOCKET_OPTIONS = [ + 'autoSelectFamily', + 'autoSelectFamilyAttemptTimeout', 'family', 'hints', 'localAddress', @@ -287,6 +289,10 @@ function parseConnectOptions(options: ConnectionOptions): SocketConnectOpts { } } + if (!('autoSelectFamily' in result)) { + result.autoSelectFamily = true; + } + if (typeof hostAddress.socketPath === 'string') { result.path = hostAddress.socketPath; return result as net.IpcNetConnectOpts; diff --git a/src/connection_string.ts b/src/connection_string.ts index b0becafac0..37ecd5c567 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -740,6 +740,12 @@ export const OPTIONS = { autoEncryption: { type: 'record' }, + autoSelectFamily: { + type: 'boolean' + }, + autoSelectFamilyAttemptTimeout: { + type: 'uint' + }, bsonRegExp: { type: 'boolean' }, diff --git a/src/mongo_client.ts b/src/mongo_client.ts index aee241076f..b5cf49deb5 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -104,7 +104,7 @@ export type SupportedTLSSocketOptions = Pick< /** @public */ export type SupportedSocketOptions = Pick< - TcpNetConnectOpts, + TcpNetConnectOpts & { autoSelectFamily?: boolean; autoSelectFamilyAttemptTimeout?: number }, (typeof LEGAL_TCP_SOCKET_OPTIONS)[number] >; diff --git a/test/integration/node-specific/mongo_client.test.ts b/test/integration/node-specific/mongo_client.test.ts index b0653a702a..634516f8f7 100644 --- a/test/integration/node-specific/mongo_client.test.ts +++ b/test/integration/node-specific/mongo_client.test.ts @@ -1,5 +1,6 @@ import { expect } from 'chai'; import { once } from 'events'; +import * as net from 'net'; import * as sinon from 'sinon'; import { @@ -721,4 +722,56 @@ describe('class MongoClient', function () { }); }); }); + + context('when connecting', function () { + let netSpy; + + beforeEach(function () { + netSpy = sinon.spy(net, 'createConnection'); + }); + + afterEach(function () { + sinon.restore(); + }); + + context('when auto select options are provided', function () { + beforeEach(function () { + client = this.configuration.newClient({ + autoSelectFamily: false, + autoSelectFamilyAttemptTimeout: 100 + }); + }); + + it('sets the provided options', { + metadata: { requires: { topology: ['single'] } }, + test: async function () { + await client.connect(); + expect(netSpy).to.have.been.calledWith({ + autoSelectFamily: false, + autoSelectFamilyAttemptTimeout: 100, + host: 'localhost', + port: 27017 + }); + } + }); + }); + + context('when auto select options are not provided', function () { + beforeEach(function () { + client = this.configuration.newClient(); + }); + + it('sets the default options', { + metadata: { requires: { topology: ['single'] } }, + test: async function () { + await client.connect(); + expect(netSpy).to.have.been.calledWith({ + autoSelectFamily: true, + host: 'localhost', + port: 27017 + }); + } + }); + }); + }); }); diff --git a/test/manual/mocharc.json b/test/manual/mocharc.json index b52cf660c2..08a1a88d8e 100644 --- a/test/manual/mocharc.json +++ b/test/manual/mocharc.json @@ -1,5 +1,8 @@ { - "require": "ts-node/register", + "require": [ + "ts-node/register", + "test/tools/runner/chai_addons.ts" + ], "reporter": "test/tools/reporter/mongodb_reporter.js", "failZero": true, "color": true, diff --git a/test/manual/tls_support.test.ts b/test/manual/tls_support.test.ts index da89c71046..5359708626 100644 --- a/test/manual/tls_support.test.ts +++ b/test/manual/tls_support.test.ts @@ -1,7 +1,9 @@ import * as process from 'node:process'; +import * as tls from 'node:tls'; import { expect } from 'chai'; import { promises as fs } from 'fs'; +import * as sinon from 'sinon'; import { LEGACY_HELLO_COMMAND, @@ -61,6 +63,63 @@ describe('TLS Support', function () { }); context('when tls filepaths have length > 0', () => { + context('when auto family options are not set', function () { + let tlsSpy; + + afterEach(function () { + sinon.restore(); + }); + + beforeEach(function () { + client = new MongoClient(CONNECTION_STRING, tlsSettings); + tlsSpy = sinon.spy(tls, 'connect'); + }); + + it('sets the default options', async function () { + await client.connect(); + expect(tlsSpy).to.have.been.calledWith({ + autoSelectFamily: true, + host: 'localhost', + port: 27017, + servername: 'localhost', + ca: sinon.match.defined, + cert: sinon.match.defined, + key: sinon.match.defined + }); + }); + }); + + context('when auto family options are set', function () { + let tlsSpy; + + afterEach(function () { + sinon.restore(); + }); + + beforeEach(function () { + client = new MongoClient(CONNECTION_STRING, { + ...tlsSettings, + autoSelectFamily: false, + autoSelectFamilyAttemptTimeout: 100 + }); + tlsSpy = sinon.spy(tls, 'connect'); + }); + + it('sets the provided options', async function () { + await client.connect(); + expect(tlsSpy).to.have.been.calledWith({ + autoSelectFamily: false, + autoSelectFamilyAttemptTimeout: 100, + host: 'localhost', + port: 27017, + servername: 'localhost', + ca: sinon.match.defined, + cert: sinon.match.defined, + key: sinon.match.defined + }); + }); + }); + context('when connection will succeed', () => { beforeEach(async () => { client = new MongoClient(CONNECTION_STRING, tlsSettings); From 7c92254dab90e4fbf64ac0ce0e9d7755f5b4fb40 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 31 Jul 2024 17:46:40 +0200 Subject: [PATCH 2/3] feat(NODE-5754): support auto select family in kms requests --- src/client-side-encryption/auto_encrypter.ts | 16 ++++++++-- .../client_encryption.ts | 29 +++++++++++++++---- src/client-side-encryption/state_machine.ts | 28 +++++++++++++++++- src/cmap/connect.ts | 4 --- src/connection_string.ts | 3 +- src/encrypter.ts | 10 +++++++ src/index.ts | 1 + .../client_encryption.test.ts | 5 ++++ .../state_machine.test.ts | 24 +++++++++++++++ 9 files changed, 106 insertions(+), 14 deletions(-) diff --git a/src/client-side-encryption/auto_encrypter.ts b/src/client-side-encryption/auto_encrypter.ts index 3066b8d1f0..2ca163390b 100644 --- a/src/client-side-encryption/auto_encrypter.ts +++ b/src/client-side-encryption/auto_encrypter.ts @@ -15,7 +15,11 @@ import * as cryptoCallbacks from './crypto_callbacks'; import { MongoCryptInvalidArgumentError } from './errors'; import { MongocryptdManager } from './mongocryptd_manager'; import { type KMSProviders, refreshKMSCredentials } from './providers'; -import { type CSFLEKMSTlsOptions, StateMachine } from './state_machine'; +import { + type ClientEncryptionSocketOptions, + type CSFLEKMSTlsOptions, + StateMachine +} from './state_machine'; /** @public */ export interface AutoEncryptionOptions { @@ -101,6 +105,8 @@ export interface AutoEncryptionOptions { proxyOptions?: ProxyOptions; /** The TLS options to use connecting to the KMS provider */ tlsOptions?: CSFLEKMSTlsOptions; + /** Options for KMS socket requests. */ + socketOptions?: ClientEncryptionSocketOptions; } /** @@ -150,6 +156,7 @@ export class AutoEncrypter { _kmsProviders: KMSProviders; _bypassMongocryptdAndCryptShared: boolean; _contextCounter: number; + _socketOptions: ClientEncryptionSocketOptions; _mongocryptdManager?: MongocryptdManager; _mongocryptdClient?: MongoClient; @@ -234,6 +241,7 @@ export class AutoEncrypter { this._proxyOptions = options.proxyOptions || {}; this._tlsOptions = options.tlsOptions || {}; this._kmsProviders = options.kmsProviders || {}; + this._socketOptions = options.socketOptions || {}; const mongoCryptOptions: MongoCryptOptions = { cryptoCallbacks @@ -379,7 +387,8 @@ export class AutoEncrypter { promoteValues: false, promoteLongs: false, proxyOptions: this._proxyOptions, - tlsOptions: this._tlsOptions + tlsOptions: this._tlsOptions, + socketOptions: this._socketOptions }); return deserialize(await stateMachine.execute(this, context), { @@ -399,7 +408,8 @@ export class AutoEncrypter { const stateMachine = new StateMachine({ ...options, proxyOptions: this._proxyOptions, - tlsOptions: this._tlsOptions + tlsOptions: this._tlsOptions, + socketOptions: this._socketOptions }); return await stateMachine.execute(this, context); diff --git a/src/client-side-encryption/client_encryption.ts b/src/client-side-encryption/client_encryption.ts index b78b354996..1ce33279ea 100644 --- a/src/client-side-encryption/client_encryption.ts +++ b/src/client-side-encryption/client_encryption.ts @@ -28,7 +28,11 @@ import { type KMSProviders, refreshKMSCredentials } from './providers/index'; -import { type CSFLEKMSTlsOptions, StateMachine } from './state_machine'; +import { + type ClientEncryptionSocketOptions, + type CSFLEKMSTlsOptions, + StateMachine +} from './state_machine'; /** * @public @@ -62,6 +66,8 @@ export class ClientEncryption { _tlsOptions: CSFLEKMSTlsOptions; /** @internal */ _kmsProviders: KMSProviders; + /** @internal */ + _socketOptions: ClientEncryptionSocketOptions; /** @internal */ _mongoCrypt: MongoCrypt; @@ -108,6 +114,15 @@ export class ClientEncryption { this._proxyOptions = options.proxyOptions ?? {}; this._tlsOptions = options.tlsOptions ?? {}; this._kmsProviders = options.kmsProviders || {}; + this._socketOptions = {}; + + if ('autoSelectFamily' in client.s.options) { + this._socketOptions.autoSelectFamily = client.s.options.autoSelectFamily; + } + if ('autoSelectFamilyAttemptTimeout' in client.s.options) { + this._socketOptions.autoSelectFamilyAttemptTimeout = + client.s.options.autoSelectFamilyAttemptTimeout; + } if (options.keyVaultNamespace == null) { throw new MongoCryptInvalidArgumentError('Missing required option `keyVaultNamespace`'); @@ -199,7 +214,8 @@ export class ClientEncryption { const stateMachine = new StateMachine({ proxyOptions: this._proxyOptions, - tlsOptions: this._tlsOptions + tlsOptions: this._tlsOptions, + socketOptions: this._socketOptions }); const dataKey = deserialize(await stateMachine.execute(this, context)) as DataKey; @@ -256,7 +272,8 @@ export class ClientEncryption { const context = this._mongoCrypt.makeRewrapManyDataKeyContext(filterBson, keyEncryptionKeyBson); const stateMachine = new StateMachine({ proxyOptions: this._proxyOptions, - tlsOptions: this._tlsOptions + tlsOptions: this._tlsOptions, + socketOptions: this._socketOptions }); const { v: dataKeys } = deserialize(await stateMachine.execute(this, context)); @@ -637,7 +654,8 @@ export class ClientEncryption { const stateMachine = new StateMachine({ proxyOptions: this._proxyOptions, - tlsOptions: this._tlsOptions + tlsOptions: this._tlsOptions, + socketOptions: this._socketOptions }); const { v } = deserialize(await stateMachine.execute(this, context)); @@ -715,7 +733,8 @@ export class ClientEncryption { const valueBuffer = serialize({ v: value }); const stateMachine = new StateMachine({ proxyOptions: this._proxyOptions, - tlsOptions: this._tlsOptions + tlsOptions: this._tlsOptions, + socketOptions: this._socketOptions }); const context = this._mongoCrypt.makeExplicitEncryptionContext(valueBuffer, contextOptions); diff --git a/src/client-side-encryption/state_machine.ts b/src/client-side-encryption/state_machine.ts index f0ae19546a..fd21fd4f3b 100644 --- a/src/client-side-encryption/state_machine.ts +++ b/src/client-side-encryption/state_machine.ts @@ -114,6 +114,16 @@ export type CSFLEKMSTlsOptions = { [key: string]: ClientEncryptionTlsOptions | undefined; }; +/** + * @public + * + * Socket options to use for KMS requests. + */ +export type ClientEncryptionSocketOptions = Pick< + MongoClientOptions, + 'autoSelectFamily' | 'autoSelectFamilyAttemptTimeout' +>; + /** * This is kind of a hack. For `rewrapManyDataKey`, we have tests that * guarantee that when there are no matching keys, `rewrapManyDataKey` returns @@ -153,6 +163,9 @@ export type StateMachineOptions = { /** TLS options for KMS requests, if set. */ tlsOptions: CSFLEKMSTlsOptions; + + /** Socket specific options we support. */ + socketOptions: ClientEncryptionSocketOptions; } & Pick; /** @@ -289,7 +302,12 @@ export class StateMachine { async kmsRequest(request: MongoCryptKMSRequest): Promise { const parsedUrl = request.endpoint.split(':'); const port = parsedUrl[1] != null ? Number.parseInt(parsedUrl[1], 10) : HTTPS_PORT; - const options: tls.ConnectionOptions & { host: string; port: number } = { + const options: tls.ConnectionOptions & { + host: string; + port: number; + autoSelectFamily?: boolean; + autoSelectFamilyAttemptTimeout?: number; + } = { host: parsedUrl[0], servername: parsedUrl[0], port @@ -297,6 +315,14 @@ export class StateMachine { const message = request.message; const buffer = new BufferPool(); + const socketOptions = this.options.socketOptions || {}; + if ('autoSelectFamily' in socketOptions) { + options.autoSelectFamily = socketOptions.autoSelectFamily; + } + if ('autoSelectFamilyAttemptTimeout' in socketOptions) { + options.autoSelectFamilyAttemptTimeout = socketOptions.autoSelectFamilyAttemptTimeout; + } + const netSocket: net.Socket = new net.Socket(); let socket: tls.TLSSocket; diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index 9a4ec80dc0..b2ec3caabf 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -289,10 +289,6 @@ function parseConnectOptions(options: ConnectionOptions): SocketConnectOpts { } } - if (!('autoSelectFamily' in result)) { - result.autoSelectFamily = true; - } - if (typeof hostAddress.socketPath === 'string') { result.path = hostAddress.socketPath; return result as net.IpcNetConnectOpts; diff --git a/src/connection_string.ts b/src/connection_string.ts index 37ecd5c567..bf9acf556c 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -741,7 +741,8 @@ export const OPTIONS = { type: 'record' }, autoSelectFamily: { - type: 'boolean' + type: 'boolean', + default: true }, autoSelectFamilyAttemptTimeout: { type: 'uint' diff --git a/src/encrypter.ts b/src/encrypter.ts index fbcf7c195d..7ebda9e61f 100644 --- a/src/encrypter.ts +++ b/src/encrypter.ts @@ -1,6 +1,7 @@ import { callbackify } from 'util'; import { AutoEncrypter, type AutoEncryptionOptions } from './client-side-encryption/auto_encrypter'; +import { type ClientEncryptionSocketOptions } from './client-side-encryption/state_machine'; import { MONGO_CLIENT_EVENTS } from './constants'; import { getMongoDBClientEncryption } from './deps'; import { MongoInvalidArgumentError, MongoMissingDependencyError } from './error'; @@ -56,6 +57,15 @@ export class Encrypter { }; } + const socketOptions: ClientEncryptionSocketOptions = {}; + if ('autoSelectFamily' in options) { + socketOptions.autoSelectFamily = options.autoSelectFamily; + } + if ('autoSelectFamilyAttemptTimeout' in options) { + socketOptions.autoSelectFamilyAttemptTimeout = options.autoSelectFamilyAttemptTimeout; + } + options.autoEncryption.socketOptions = socketOptions; + this.autoEncrypter = new AutoEncrypter(client, options.autoEncryption); } diff --git a/src/index.ts b/src/index.ts index 0ba8f82c01..efd0b9d055 100644 --- a/src/index.ts +++ b/src/index.ts @@ -248,6 +248,7 @@ export type { LocalKMSProviderConfiguration } from './client-side-encryption/providers/index'; export type { + ClientEncryptionSocketOptions, ClientEncryptionTlsOptions, CSFLEKMSTlsOptions, StateMachineExecutable diff --git a/test/unit/client-side-encryption/client_encryption.test.ts b/test/unit/client-side-encryption/client_encryption.test.ts index c83383d4e4..b9b56ff9e5 100644 --- a/test/unit/client-side-encryption/client_encryption.test.ts +++ b/test/unit/client-side-encryption/client_encryption.test.ts @@ -19,6 +19,11 @@ import { Binary, BSON, deserialize } from '../../mongodb'; const { EJSON } = BSON; class MockClient { + s: any; + + constructor(options?: any) { + this.s = { options: options || {} }; + } db(dbName) { return { async createCollection(name, options) { diff --git a/test/unit/client-side-encryption/state_machine.test.ts b/test/unit/client-side-encryption/state_machine.test.ts index 518e63a26d..baec0cbece 100644 --- a/test/unit/client-side-encryption/state_machine.test.ts +++ b/test/unit/client-side-encryption/state_machine.test.ts @@ -148,6 +148,30 @@ describe('StateMachine', function () { }); }); + context('when socket options are provided', function () { + const stateMachine = new StateMachine({ + socketOptions: { autoSelectFamily: true, autoSelectFamilyAttemptTimeout: 300 } + } as any); + const request = new MockRequest(Buffer.from('foobar'), -1); + let connectOptions; + + it('passes them through to the socket', async function () { + sandbox.stub(tls, 'connect').callsFake((options, callback) => { + connectOptions = options; + this.fakeSocket = new MockSocket(callback); + return this.fakeSocket; + }); + const kmsRequestPromise = stateMachine.kmsRequest(request); + + await setTimeoutAsync(0); + this.fakeSocket.emit('data', Buffer.alloc(0)); + + await kmsRequestPromise; + expect(connectOptions.autoSelectFamily).to.equal(true); + expect(connectOptions.autoSelectFamilyAttemptTimeout).to.equal(300); + }); + }); + context('when tls options are provided', function () { context('when the options are insecure', function () { [ From 991abcea471b38144c08d3fa07d7b031d4cae611 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 1 Aug 2024 16:10:29 +0200 Subject: [PATCH 3/3] refactor: setting auto select family options --- src/client-side-encryption/auto_encrypter.ts | 28 +++++++------ .../client_encryption.ts | 39 +++++++++++-------- src/client-side-encryption/state_machine.ts | 22 +++++------ src/encrypter.ts | 10 ----- .../auto_encrypter.test.ts | 21 +++++++--- .../client_encryption.test.ts | 4 +- 6 files changed, 66 insertions(+), 58 deletions(-) diff --git a/src/client-side-encryption/auto_encrypter.ts b/src/client-side-encryption/auto_encrypter.ts index 2ca163390b..5ac3945f5e 100644 --- a/src/client-side-encryption/auto_encrypter.ts +++ b/src/client-side-encryption/auto_encrypter.ts @@ -3,6 +3,7 @@ import { type MongoCryptConstructor, type MongoCryptOptions } from 'mongodb-client-encryption'; +import * as net from 'net'; import { deserialize, type Document, serialize } from '../bson'; import { type CommandOptions, type ProxyOptions } from '../cmap/connection'; @@ -11,15 +12,12 @@ import { getMongoDBClientEncryption } from '../deps'; import { MongoRuntimeError } from '../error'; import { MongoClient, type MongoClientOptions } from '../mongo_client'; import { MongoDBCollectionNamespace } from '../utils'; +import { autoSelectSocketOptions } from './client_encryption'; import * as cryptoCallbacks from './crypto_callbacks'; import { MongoCryptInvalidArgumentError } from './errors'; import { MongocryptdManager } from './mongocryptd_manager'; import { type KMSProviders, refreshKMSCredentials } from './providers'; -import { - type ClientEncryptionSocketOptions, - type CSFLEKMSTlsOptions, - StateMachine -} from './state_machine'; +import { type CSFLEKMSTlsOptions, StateMachine } from './state_machine'; /** @public */ export interface AutoEncryptionOptions { @@ -105,8 +103,6 @@ export interface AutoEncryptionOptions { proxyOptions?: ProxyOptions; /** The TLS options to use connecting to the KMS provider */ tlsOptions?: CSFLEKMSTlsOptions; - /** Options for KMS socket requests. */ - socketOptions?: ClientEncryptionSocketOptions; } /** @@ -156,7 +152,6 @@ export class AutoEncrypter { _kmsProviders: KMSProviders; _bypassMongocryptdAndCryptShared: boolean; _contextCounter: number; - _socketOptions: ClientEncryptionSocketOptions; _mongocryptdManager?: MongocryptdManager; _mongocryptdClient?: MongoClient; @@ -241,7 +236,6 @@ export class AutoEncrypter { this._proxyOptions = options.proxyOptions || {}; this._tlsOptions = options.tlsOptions || {}; this._kmsProviders = options.kmsProviders || {}; - this._socketOptions = options.socketOptions || {}; const mongoCryptOptions: MongoCryptOptions = { cryptoCallbacks @@ -305,10 +299,20 @@ export class AutoEncrypter { serverSelectionTimeoutMS: 10000 }; - if (options.extraOptions == null || typeof options.extraOptions.mongocryptdURI !== 'string') { + if ( + (options.extraOptions == null || typeof options.extraOptions.mongocryptdURI !== 'string') && + !net.getDefaultAutoSelectFamily + ) { + // Only set family if autoSelectFamily options are not supported. clientOptions.family = 4; } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: TS complains as this always returns true on versions where it is present. + if (net.getDefaultAutoSelectFamily) { + Object.assign(clientOptions, autoSelectSocketOptions(this._client.options)); + } + this._mongocryptdClient = new MongoClient(this._mongocryptdManager.uri, clientOptions); } } @@ -388,7 +392,7 @@ export class AutoEncrypter { promoteLongs: false, proxyOptions: this._proxyOptions, tlsOptions: this._tlsOptions, - socketOptions: this._socketOptions + socketOptions: autoSelectSocketOptions(this._client.options) }); return deserialize(await stateMachine.execute(this, context), { @@ -409,7 +413,7 @@ export class AutoEncrypter { ...options, proxyOptions: this._proxyOptions, tlsOptions: this._tlsOptions, - socketOptions: this._socketOptions + socketOptions: autoSelectSocketOptions(this._client.options) }); return await stateMachine.execute(this, context); diff --git a/src/client-side-encryption/client_encryption.ts b/src/client-side-encryption/client_encryption.ts index 1ce33279ea..03d2ff2de2 100644 --- a/src/client-side-encryption/client_encryption.ts +++ b/src/client-side-encryption/client_encryption.ts @@ -12,7 +12,7 @@ import { type Collection } from '../collection'; import { type FindCursor } from '../cursor/find_cursor'; import { type Db } from '../db'; import { getMongoDBClientEncryption } from '../deps'; -import { type MongoClient } from '../mongo_client'; +import { type MongoClient, type MongoClientOptions } from '../mongo_client'; import { type Filter, type WithId } from '../mongo_types'; import { type CreateCollectionOptions } from '../operations/create_collection'; import { type DeleteResult } from '../operations/delete'; @@ -66,8 +66,6 @@ export class ClientEncryption { _tlsOptions: CSFLEKMSTlsOptions; /** @internal */ _kmsProviders: KMSProviders; - /** @internal */ - _socketOptions: ClientEncryptionSocketOptions; /** @internal */ _mongoCrypt: MongoCrypt; @@ -114,15 +112,6 @@ export class ClientEncryption { this._proxyOptions = options.proxyOptions ?? {}; this._tlsOptions = options.tlsOptions ?? {}; this._kmsProviders = options.kmsProviders || {}; - this._socketOptions = {}; - - if ('autoSelectFamily' in client.s.options) { - this._socketOptions.autoSelectFamily = client.s.options.autoSelectFamily; - } - if ('autoSelectFamilyAttemptTimeout' in client.s.options) { - this._socketOptions.autoSelectFamilyAttemptTimeout = - client.s.options.autoSelectFamilyAttemptTimeout; - } if (options.keyVaultNamespace == null) { throw new MongoCryptInvalidArgumentError('Missing required option `keyVaultNamespace`'); @@ -215,7 +204,7 @@ export class ClientEncryption { const stateMachine = new StateMachine({ proxyOptions: this._proxyOptions, tlsOptions: this._tlsOptions, - socketOptions: this._socketOptions + socketOptions: autoSelectSocketOptions(this._client.options) }); const dataKey = deserialize(await stateMachine.execute(this, context)) as DataKey; @@ -273,7 +262,7 @@ export class ClientEncryption { const stateMachine = new StateMachine({ proxyOptions: this._proxyOptions, tlsOptions: this._tlsOptions, - socketOptions: this._socketOptions + socketOptions: autoSelectSocketOptions(this._client.options) }); const { v: dataKeys } = deserialize(await stateMachine.execute(this, context)); @@ -655,7 +644,7 @@ export class ClientEncryption { const stateMachine = new StateMachine({ proxyOptions: this._proxyOptions, tlsOptions: this._tlsOptions, - socketOptions: this._socketOptions + socketOptions: autoSelectSocketOptions(this._client.options) }); const { v } = deserialize(await stateMachine.execute(this, context)); @@ -734,7 +723,7 @@ export class ClientEncryption { const stateMachine = new StateMachine({ proxyOptions: this._proxyOptions, tlsOptions: this._tlsOptions, - socketOptions: this._socketOptions + socketOptions: autoSelectSocketOptions(this._client.options) }); const context = this._mongoCrypt.makeExplicitEncryptionContext(valueBuffer, contextOptions); @@ -976,3 +965,21 @@ export interface RangeOptions { sparsity: Long; precision?: number; } + +/** + * Get the socket options from the client. + * @param baseOptions - The mongo client options. + * @returns ClientEncryptionSocketOptions + */ +export function autoSelectSocketOptions( + baseOptions: MongoClientOptions +): ClientEncryptionSocketOptions { + const options: ClientEncryptionSocketOptions = { autoSelectFamily: true }; + if ('autoSelectFamily' in baseOptions) { + options.autoSelectFamily = baseOptions.autoSelectFamily; + } + if ('autoSelectFamilyAttemptTimeout' in baseOptions) { + options.autoSelectFamilyAttemptTimeout = baseOptions.autoSelectFamilyAttemptTimeout; + } + return options; +} diff --git a/src/client-side-encryption/state_machine.ts b/src/client-side-encryption/state_machine.ts index fd21fd4f3b..af3ea4c215 100644 --- a/src/client-side-encryption/state_machine.ts +++ b/src/client-side-encryption/state_machine.ts @@ -14,7 +14,7 @@ import { type ProxyOptions } from '../cmap/connection'; import { getSocks, type SocksLib } from '../deps'; import { type MongoClient, type MongoClientOptions } from '../mongo_client'; import { BufferPool, MongoDBCollectionNamespace, promiseWithResolvers } from '../utils'; -import { type DataKey } from './client_encryption'; +import { autoSelectSocketOptions, type DataKey } from './client_encryption'; import { MongoCryptError } from './errors'; import { type MongocryptdManager } from './mongocryptd_manager'; import { type KMSProviders } from './providers'; @@ -302,6 +302,7 @@ export class StateMachine { async kmsRequest(request: MongoCryptKMSRequest): Promise { const parsedUrl = request.endpoint.split(':'); const port = parsedUrl[1] != null ? Number.parseInt(parsedUrl[1], 10) : HTTPS_PORT; + const socketOptions = autoSelectSocketOptions(this.options.socketOptions || {}); const options: tls.ConnectionOptions & { host: string; port: number; @@ -310,19 +311,12 @@ export class StateMachine { } = { host: parsedUrl[0], servername: parsedUrl[0], - port + port, + ...socketOptions }; const message = request.message; const buffer = new BufferPool(); - const socketOptions = this.options.socketOptions || {}; - if ('autoSelectFamily' in socketOptions) { - options.autoSelectFamily = socketOptions.autoSelectFamily; - } - if ('autoSelectFamilyAttemptTimeout' in socketOptions) { - options.autoSelectFamilyAttemptTimeout = socketOptions.autoSelectFamilyAttemptTimeout; - } - const netSocket: net.Socket = new net.Socket(); let socket: tls.TLSSocket; @@ -377,10 +371,12 @@ export class StateMachine { try { if (this.options.proxyOptions && this.options.proxyOptions.proxyHost) { - netSocket.connect({ + const netSocketOptions = { host: this.options.proxyOptions.proxyHost, - port: this.options.proxyOptions.proxyPort || 1080 - }); + port: this.options.proxyOptions.proxyPort || 1080, + ...socketOptions + }; + netSocket.connect(netSocketOptions); await willConnect; try { diff --git a/src/encrypter.ts b/src/encrypter.ts index 7ebda9e61f..fbcf7c195d 100644 --- a/src/encrypter.ts +++ b/src/encrypter.ts @@ -1,7 +1,6 @@ import { callbackify } from 'util'; import { AutoEncrypter, type AutoEncryptionOptions } from './client-side-encryption/auto_encrypter'; -import { type ClientEncryptionSocketOptions } from './client-side-encryption/state_machine'; import { MONGO_CLIENT_EVENTS } from './constants'; import { getMongoDBClientEncryption } from './deps'; import { MongoInvalidArgumentError, MongoMissingDependencyError } from './error'; @@ -57,15 +56,6 @@ export class Encrypter { }; } - const socketOptions: ClientEncryptionSocketOptions = {}; - if ('autoSelectFamily' in options) { - socketOptions.autoSelectFamily = options.autoSelectFamily; - } - if ('autoSelectFamilyAttemptTimeout' in options) { - socketOptions.autoSelectFamilyAttemptTimeout = options.autoSelectFamilyAttemptTimeout; - } - options.autoEncryption.socketOptions = socketOptions; - this.autoEncrypter = new AutoEncrypter(client, options.autoEncryption); } diff --git a/test/unit/client-side-encryption/auto_encrypter.test.ts b/test/unit/client-side-encryption/auto_encrypter.test.ts index 08072512c7..1e13c0b07c 100644 --- a/test/unit/client-side-encryption/auto_encrypter.test.ts +++ b/test/unit/client-side-encryption/auto_encrypter.test.ts @@ -1,5 +1,6 @@ import { expect } from 'chai'; import * as fs from 'fs'; +import * as net from 'net'; import * as sinon from 'sinon'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports @@ -37,7 +38,13 @@ const MOCK_MONGOCRYPTD_RESPONSE = readExtendedJsonToBuffer( const MOCK_KEYDOCUMENT_RESPONSE = readExtendedJsonToBuffer(`${__dirname}/data/key-document.json`); const MOCK_KMS_DECRYPT_REPLY = readHttpResponse(`${__dirname}/data/kms-decrypt-reply.txt`); -class MockClient {} +class MockClient { + options: any; + + constructor(options?: any) { + this.options = { options: options || {} }; + } +} const originalAccessKeyId = process.env.AWS_ACCESS_KEY_ID; const originalSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; @@ -105,15 +112,19 @@ describe('AutoEncrypter', function () { }); context('when mongocryptdURI is not specified', () => { - it('sets the ip address family to ipv4', function () { + it('sets family options', function () { expect(autoEncrypter).to.have.nested.property('_mongocryptdClient.s.options'); const options = autoEncrypter._mongocryptdClient?.s.options; - expect(options).to.have.property('family', 4); + if (net.getDefaultAutoSelectFamily) { + expect(options).to.have.property('autoSelectFamily', true); + } else { + expect(options).to.have.property('family', 4); + } }); }); context('when mongocryptdURI is specified', () => { - it('does not set the ip address family to ipv4', function () { + it('sets autoSelectFamily options', function () { const autoEncrypter = new AutoEncrypter(client, { ...autoEncrypterOptions, extraOptions: { mongocryptdURI: MongocryptdManager.DEFAULT_MONGOCRYPTD_URI } @@ -121,7 +132,7 @@ describe('AutoEncrypter', function () { expect(autoEncrypter).to.have.nested.property('_mongocryptdClient.s.options'); const options = autoEncrypter._mongocryptdClient?.s.options; - expect(options).not.to.have.property('family', 4); + expect(options).to.have.property('autoSelectFamily', true); }); }); }); diff --git a/test/unit/client-side-encryption/client_encryption.test.ts b/test/unit/client-side-encryption/client_encryption.test.ts index b9b56ff9e5..2ecf634771 100644 --- a/test/unit/client-side-encryption/client_encryption.test.ts +++ b/test/unit/client-side-encryption/client_encryption.test.ts @@ -19,10 +19,10 @@ import { Binary, BSON, deserialize } from '../../mongodb'; const { EJSON } = BSON; class MockClient { - s: any; + options: any; constructor(options?: any) { - this.s = { options: options || {} }; + this.options = { options: options || {} }; } db(dbName) { return {