From 72a0620016cf5a21816b2d20f1528b329b35d385 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Wed, 10 Jan 2024 18:40:17 +0100 Subject: [PATCH 01/23] fix(NODE-5127): reject kmsRequest on server close --- src/client-side-encryption/state_machine.ts | 9 ++ .../kms-request.test.ts | 104 ++++++++++++++++++ .../client-side-encryption/ssl/ca.pem | 21 ++++ .../client-side-encryption/ssl/client.pem | 48 ++++++++ .../client-side-encryption/ssl/server-crt.pem | 22 ++++ .../client-side-encryption/ssl/server-key.pem | 27 +++++ 6 files changed, 231 insertions(+) create mode 100644 test/integration/client-side-encryption/kms-request.test.ts create mode 100644 test/integration/client-side-encryption/ssl/ca.pem create mode 100644 test/integration/client-side-encryption/ssl/client.pem create mode 100644 test/integration/client-side-encryption/ssl/server-crt.pem create mode 100644 test/integration/client-side-encryption/ssl/server-key.pem diff --git a/src/client-side-encryption/state_machine.ts b/src/client-side-encryption/state_machine.ts index 7d5dc23bf8..8f76acb0b9 100644 --- a/src/client-side-encryption/state_machine.ts +++ b/src/client-side-encryption/state_machine.ts @@ -321,6 +321,12 @@ export class StateMachine { reject(mcError); } + function onclose() { + destroySockets(); + const mcError = new MongoCryptError('KMS request closed'); + reject(mcError); + } + if (this.options.proxyOptions && this.options.proxyOptions.proxyHost) { rawSocket = net.connect({ host: this.options.proxyOptions.proxyHost, @@ -329,6 +335,8 @@ export class StateMachine { rawSocket.on('timeout', ontimeout); rawSocket.on('error', onerror); + rawSocket.on('close', onclose); + try { // eslint-disable-next-line @typescript-eslint/no-var-requires const events = require('events') as typeof import('events'); @@ -374,6 +382,7 @@ export class StateMachine { socket.once('timeout', ontimeout); socket.once('error', onerror); + socket.once('close', onclose); socket.on('data', data => { buffer.append(data); diff --git a/test/integration/client-side-encryption/kms-request.test.ts b/test/integration/client-side-encryption/kms-request.test.ts new file mode 100644 index 0000000000..551969fedc --- /dev/null +++ b/test/integration/client-side-encryption/kms-request.test.ts @@ -0,0 +1,104 @@ +import { expect } from 'chai'; +const fs = require('fs'); +import * as tls from 'tls'; +import { once } from 'events'; + +const { ClientEncryption } = require('../../../src/client-side-encryption/client_encryption'); + +describe.only('kmsRequest', function () { + context('when server closes connection without an error', function () { + const endpoint = 'https://localhost:5699'; + const provider = 'kmip'; + const kmsProviders = { kmip: { endpoint } }; + const keyVaultNamespace = 'encryption._keyVault'; + let tlsOptions; + let serverThatClosesOnHandshake; + let client; + let clientEncrypted; + let db; + + beforeEach(async function () { + const serverKey = fs.readFileSync(`${__dirname}/ssl/server-key.pem`); + const serverCrt = fs.readFileSync(`${__dirname}/ssl/server-crt.pem`); + const ca = fs.readFileSync(`${__dirname}/ssl/ca.pem`); + const clientKey = fs.readFileSync(`${__dirname}/ssl/client.pem`); + + tlsOptions = { + tlsCAFile: ca, + tlsCertificateKeyFile: clientKey + }; + + const autoEncryption = { + keyVaultNamespace, + kmsProviders, + tlsOptions: { [provider]: tlsOptions }, + explicitEncryptionOnly: true, + }; + + // Start fake server. + serverThatClosesOnHandshake = tls.createServer({ + key: serverKey, + cert: serverCrt, + ca: [ca] + }, (socket) => { + socket.end(); + }); + serverThatClosesOnHandshake.listen(5699); + await once(serverThatClosesOnHandshake, 'listening'); + + client = this.configuration.newClient(); + await client.connect(); + + db = client.db('automatic_data_encryption_keys'); + await db.dropDatabase().catch(() => null); + + clientEncrypted = this.configuration.newClient({}, { autoEncryption }); + await clientEncrypted.connect(); + }); + + afterEach(() => { + serverThatClosesOnHandshake.close(); + client.close(); + clientEncrypted.close(); + }); + + it('kmsRequest rejects with kms request closed error', async function () { + try { + console.log('tlsOptions----------------------'); + console.log(tlsOptions); + console.log('----------------------'); + const clientEncryption = new ClientEncryption(client, { + keyVaultNamespace, + kmsProviders, + tlsOptions + }); + const createCollectionOptions = { + encryptedFields: { fields: [{ path: 'ssn', bsonType: 'string', keyId: null }] } + }; + const masterKey = {}; + const { encryptedFields } = await clientEncryption.createEncryptedCollection( + db, + 'testing1', + { + provider, + createCollectionOptions, + masterKey + } + ); + console.log('encryptedFields----------------------'); + console.log(encryptedFields); + console.log('----------------------'); + } catch (err) { + console.log('err----------------------'); + console.log(err); + console.log('----------------------'); + expect(err.name).to.equal('MongoCryptCreateDataKeyError'); + expect(err.message).to.include('KMS request closed'); + return; + } finally { + client.close(); + clientEncrypted.close(); + } + }); + }); +}); \ No newline at end of file diff --git a/test/integration/client-side-encryption/ssl/ca.pem b/test/integration/client-side-encryption/ssl/ca.pem new file mode 100644 index 0000000000..6ac86cfcc1 --- /dev/null +++ b/test/integration/client-side-encryption/ssl/ca.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDfzCCAmegAwIBAgIDB1MGMA0GCSqGSIb3DQEBCwUAMHkxGzAZBgNVBAMTEkRy +aXZlcnMgVGVzdGluZyBDQTEQMA4GA1UECxMHRHJpdmVyczEQMA4GA1UEChMHTW9u +Z29EQjEWMBQGA1UEBxMNTmV3IFlvcmsgQ2l0eTERMA8GA1UECBMITmV3IFlvcmsx +CzAJBgNVBAYTAlVTMB4XDTE5MDUyMjIwMjMxMVoXDTM5MDUyMjIwMjMxMVoweTEb +MBkGA1UEAxMSRHJpdmVycyBUZXN0aW5nIENBMRAwDgYDVQQLEwdEcml2ZXJzMRAw +DgYDVQQKEwdNb25nb0RCMRYwFAYDVQQHEw1OZXcgWW9yayBDaXR5MREwDwYDVQQI +EwhOZXcgWW9yazELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCl7VN+WsQfHlwapcOpTLZVoeMAl1LTbWTFuXSAavIyy0W1Ytky1UP/ +bxCSW0mSWwCgqoJ5aXbAvrNRp6ArWu3LsTQIEcD3pEdrFIVQhYzWUs9fXqPyI9k+ +QNNQ+MRFKeGteTPYwF2eVEtPzUHU5ws3+OKp1m6MCLkwAG3RBFUAfddUnLvGoZiT +pd8/eNabhgHvdrCw+tYFCWvSjz7SluEVievpQehrSEPKe8DxJq/IM3tSl3tdylzT +zeiKNO7c7LuQrgjAfrZl7n2SriHIlNmqiDR/kdd8+TxBuxjFlcf2WyHCO3lIcIgH +KXTlhUCg50KfHaxHu05Qw0x8869yIzqbAgMBAAGjEDAOMAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQELBQADggEBAEHuhTL8KQZcKCTSJbYA9MgZj7U32arMGBbc1hiq +VBREwvdVz4+9tIyWMzN9R/YCKmUTnCq8z3wTlC8kBtxYn/l4Tj8nJYcgLJjQ0Fwe +gT564CmvkUat8uXPz6olOCdwkMpJ9Sj62i0mpgXJdBfxKQ6TZ9yGz6m3jannjZpN +LchB7xSAEWtqUgvNusq0dApJsf4n7jZ+oBZVaQw2+tzaMfaLqHgMwcu1FzA8UKCD +sxCgIsZUs8DdxaD418Ot6nPfheOTqe24n+TTa+Z6O0W0QtnofJBx7tmAo1aEc57i +77s89pfwIJetpIlhzNSMKurCAocFCJMJLAASJFuu6dyDvPo= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/test/integration/client-side-encryption/ssl/client.pem b/test/integration/client-side-encryption/ssl/client.pem new file mode 100644 index 0000000000..5b07001092 --- /dev/null +++ b/test/integration/client-side-encryption/ssl/client.pem @@ -0,0 +1,48 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAsNS8UEuin7/K29jXfIOLpIoh1jEyWVqxiie2Onx7uJJKcoKo +khA3XeUnVN0k6X5MwYWcN52xcns7LYtyt06nRpTG2/emoV44w9uKTuHsvUbiOwSV +m/ToKQQ4FUFZoqorXH+ZmJuIpJNfoW+3CkE1vEDCIecIq6BNg5ySsPtvSuSJHGjp +mc7/5ZUDvFE2aJ8QbJU3Ws0HXiEb6ymi048LlzEL2VKX3w6mqqh+7dcZGAy7qYk2 +5FZ9ktKvCeQau7mTyU1hsPrKFiKtMN8Q2ZAItX13asw5/IeSTq2LgLFHlbj5Kpq4 +GmLdNCshzH5X7Ew3IYM8EHmsX8dmD6mhv7vpVwIDAQABAoIBABOdpb4qhcG+3twA +c/cGCKmaASLnljQ/UU6IFTjrsjXJVKTbRaPeVKX/05sgZQXZ0t3s2mV5AsQ2U1w8 +Cd+3w+qaemzQThW8hAOGCROzEDX29QWi/o2sX0ydgTMqaq0Wv3SlWv6I0mGfT45y +/BURIsrdTCvCmz2erLqa1dL4MWJXRFjT9UTs5twlecIOM2IHKoGGagFhymRK4kDe +wTRC9fpfoAgyfus3pCO/wi/F8yKGPDEwY+zgkhrJQ+kSeki7oKdGD1H540vB8gRt +EIqssE0Y6rEYf97WssQlxJgvoJBDSftOijS6mwvoasDUwfFqyyPiirawXWWhHXkc +DjIi/XECgYEA5xfjilw9YyM2UGQNESbNNunPcj7gDZbN347xJwmYmi9AUdPLt9xN +3XaMqqR22k1DUOxC/5hH0uiXir7mDfqmC+XS/ic/VOsa3CDWejkEnyGLiwSHY502 +wD/xWgHwUiGVAG9HY64vnDGm6L3KGXA2oqxanL4V0+0+Ht49pZ16i8sCgYEAw+Ox +CHGtpkzjCP/z8xr+1VTSdpc/4CP2HONnYopcn48KfQnf7Nale69/1kZpypJlvQSG +eeA3jMGigNJEkb8/kaVoRLCisXcwLc0XIfCTeiK6FS0Ka30D/84Qm8UsHxRdpGkM +kYITAa2r64tgRL8as4/ukeXBKE+oOhX43LeEfyUCgYBkf7IX2Ndlhsm3GlvIarxy +NipeP9PGdR/hKlPbq0OvQf9R1q7QrcE7H7Q6/b0mYNV2mtjkOQB7S2WkFDMOP0P5 +BqDEoKLdNkV/F9TOYH+PCNKbyYNrodJOt0Ap6Y/u1+Xpw3sjcXwJDFrO+sKqX2+T +PStG4S+y84jBedsLbDoAEwKBgQCTz7/KC11o2yOFqv09N+WKvBKDgeWlD/2qFr3w +UU9K5viXGVhqshz0k5z25vL09Drowf1nAZVpFMO2SPOMtq8VC6b+Dfr1xmYIaXVH +Gu1tf77CM9Zk/VSDNc66e7GrUgbHBK2DLo+A+Ld9aRIfTcSsMbNnS+LQtCrQibvb +cG7+MQKBgQCY11oMT2dUekoZEyW4no7W5D74lR8ztMjp/fWWTDo/AZGPBY6cZoZF +IICrzYtDT/5BzB0Jh1f4O9ZQkm5+OvlFbmoZoSbMzHL3oJCBOY5K0/kdGXL46WWh +IRJSYakNU6VIS7SjDpKgm9D8befQqZeoSggSjIIULIiAtYgS80vmGA== +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDgzCCAmugAwIBAgIDAxOUMA0GCSqGSIb3DQEBCwUAMHkxGzAZBgNVBAMTEkRy +aXZlcnMgVGVzdGluZyBDQTEQMA4GA1UECxMHRHJpdmVyczEQMA4GA1UEChMHTW9u +Z29EQjEWMBQGA1UEBxMNTmV3IFlvcmsgQ2l0eTERMA8GA1UECBMITmV3IFlvcmsx +CzAJBgNVBAYTAlVTMB4XDTE5MDUyMjIzNTU1NFoXDTM5MDUyMjIzNTU1NFowaTEP +MA0GA1UEAxMGY2xpZW50MRAwDgYDVQQLEwdEcml2ZXJzMQwwCgYDVQQKEwNNREIx +FjAUBgNVBAcTDU5ldyBZb3JrIENpdHkxETAPBgNVBAgTCE5ldyBZb3JrMQswCQYD +VQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALDUvFBLop+/ +ytvY13yDi6SKIdYxMllasYontjp8e7iSSnKCqJIQN13lJ1TdJOl+TMGFnDedsXJ7 +Oy2LcrdOp0aUxtv3pqFeOMPbik7h7L1G4jsElZv06CkEOBVBWaKqK1x/mZibiKST +X6FvtwpBNbxAwiHnCKugTYOckrD7b0rkiRxo6ZnO/+WVA7xRNmifEGyVN1rNB14h +G+spotOPC5cxC9lSl98Opqqofu3XGRgMu6mJNuRWfZLSrwnkGru5k8lNYbD6yhYi +rTDfENmQCLV9d2rMOfyHkk6ti4CxR5W4+SqauBpi3TQrIcx+V+xMNyGDPBB5rF/H +Zg+pob+76VcCAwEAAaMkMCIwCwYDVR0PBAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUF +BwMCMA0GCSqGSIb3DQEBCwUAA4IBAQAqRcLAGvYMaGYOV4HJTzNotT2qE0I9THNQ +wOV1fBg69x6SrUQTQLjJEptpOA288Wue6Jt3H+p5qAGV5GbXjzN/yjCoItggSKxG +Xg7279nz6/C5faoIKRjpS9R+MsJGlttP9nUzdSxrHvvqm62OuSVFjjETxD39DupE +YPFQoHOxdFTtBQlc/zIKxVdd20rs1xJeeU2/L7jtRBSPuR/Sk8zot7G2/dQHX49y +kHrq8qz12kj1T6XDXf8KZawFywXaz0/Ur+fUYKmkVk1T0JZaNtF4sKqDeNE4zcns +p3xLVDSl1Q5Gwj7bgph9o4Hxs9izPwiqjmNaSjPimGYZ399zcurY +-----END CERTIFICATE----- diff --git a/test/integration/client-side-encryption/ssl/server-crt.pem b/test/integration/client-side-encryption/ssl/server-crt.pem new file mode 100644 index 0000000000..26c34b2c4d --- /dev/null +++ b/test/integration/client-side-encryption/ssl/server-crt.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDlTCCAn2gAwIBAgICdxUwDQYJKoZIhvcNAQELBQAweTEbMBkGA1UEAxMSRHJp +dmVycyBUZXN0aW5nIENBMRAwDgYDVQQLEwdEcml2ZXJzMRAwDgYDVQQKEwdNb25n +b0RCMRYwFAYDVQQHEw1OZXcgWW9yayBDaXR5MREwDwYDVQQIEwhOZXcgWW9yazEL +MAkGA1UEBhMCVVMwHhcNMTkwNTIyMjIzMjU2WhcNMzkwNTIyMjIzMjU2WjBwMRIw +EAYDVQQDEwlsb2NhbGhvc3QxEDAOBgNVBAsTB0RyaXZlcnMxEDAOBgNVBAoTB01v +bmdvREIxFjAUBgNVBAcTDU5ldyBZb3JrIENpdHkxETAPBgNVBAgTCE5ldyBZb3Jr +MQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAITa +wdBOhmP5BUnfP7zabv7fedrW5zrA+Xa1dFz75rsu0oCg2Gv0vsW/4z55WSinoXQZ +x+6j/4GG79+GcpM3AxWbHIzy4hnNnERj/jUAkhStHdv+3ctec0g984Y9eBvPjJKQ +Ho01PtP6hTWTHOZ3g7xmp03tPvGlmtA17LDNDLk8SL+4+grbDFnqiy6Hze2tSsNl +L5ongh3xzLgP9PKScEZ1f/uU/VjjQTyY2tmUEaxtQY6SOPNc0j1GXrCKovMg5RMX +b4DWkWSBB4v540RKsFYCTmaJlGOe85isqYjM28+PHQ07FYFErcaxKm2j/gRI0GD1 ++fkEJOBnTI5C3+di0ekCAwEAAaMwMC4wLAYDVR0RBCUwI4IJbG9jYWxob3N0hwR/ +AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEBCwUAA4IBAQBol8+YH7MA +HwnIh7KcJ8h87GkCWsjOJCDJWiYBJArQ0MmgDO0qdx+QEtvLMn3XNtP05ZfK0WyX +or4cWllAkMFYaFbyB2hYazlD1UAAG+22Rku0UP6pJMLbWe6pnqzx+RL68FYdbZhN +fCW2xiiKsdPoo2VEY7eeZKrNr/0RFE5EKXgzmobpTBQT1Dl3Ve4aWLoTy9INlQ/g +z40qS7oq1PjjPLgxINhf4ncJqfmRXugYTOnyFiVXLZTys5Pb9SMKdToGl3NTYWLL +2AZdjr6bKtT+WtXyHqO0cQ8CkAW0M6VOlMluACllcJxfrtdlQS2S4lUIj76QKBdZ +khBHXq/b8MFX +-----END CERTIFICATE----- \ No newline at end of file diff --git a/test/integration/client-side-encryption/ssl/server-key.pem b/test/integration/client-side-encryption/ssl/server-key.pem new file mode 100644 index 0000000000..2c60d046e8 --- /dev/null +++ b/test/integration/client-side-encryption/ssl/server-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAhNrB0E6GY/kFSd8/vNpu/t952tbnOsD5drV0XPvmuy7SgKDY +a/S+xb/jPnlZKKehdBnH7qP/gYbv34ZykzcDFZscjPLiGc2cRGP+NQCSFK0d2/7d +y15zSD3zhj14G8+MkpAejTU+0/qFNZMc5neDvGanTe0+8aWa0DXssM0MuTxIv7j6 +CtsMWeqLLofN7a1Kw2UvmieCHfHMuA/08pJwRnV/+5T9WONBPJja2ZQRrG1BjpI4 +81zSPUZesIqi8yDlExdvgNaRZIEHi/njREqwVgJOZomUY57zmKypiMzbz48dDTsV +gUStxrEqbaP+BEjQYPX5+QQk4GdMjkLf52LR6QIDAQABAoIBAHSs+hHLJNOf2zkp +S3y8CUblVMsQeTpsR6otaehPgi9Zy50TpX4KD5D0GMrBH8BIl86y5Zd7h+VlcDzK +gs0vPxI2izhuBovKuzaE6rf5rFFkSBjxGDCG3o/PeJOoYFdsS3RcBbjVzju0hFCs +xnDQ/Wz0anJRrTnjyraY5SnQqx/xuhLXkj/lwWoWjP2bUqDprnuLOj16soNu60Um +JziWbmWx9ty0wohkI/8DPBl9FjSniEEUi9pnZXPElFN6kwPkgdfT5rY/TkMH4lsu +ozOUc5xgwlkT6kVjXHcs3fleuT/mOfVXLPgNms85JKLucfd6KiV7jYZkT/bXIjQ+ +7CZEn0ECgYEA5QiKZgsfJjWvZpt21V/i7dPje2xdwHtZ8F9NjX7ZUFA7mUPxUlwe +GiXxmy6RGzNdnLOto4SF0/7ebuF3koO77oLup5a2etL+y/AnNAufbu4S5D72sbiz +wdLzr3d5JQ12xeaEH6kQNk2SD5/ShctdS6GmTgQPiJIgH0MIdi9F3v0CgYEAlH84 +hMWcC+5b4hHUEexeNkT8kCXwHVcUjGRaYFdSHgovvWllApZDHSWZ+vRcMBdlhNPu +09Btxo99cjOZwGYJyt20QQLGc/ZyiOF4ximQzabTeFgLkTH3Ox6Mh2Rx9yIruYoX +nE3UfMDkYELanEJUv0zenKpZHw7tTt5yXXSlEF0CgYBSsEOvVcKYO/eoluZPYQAA +F2jgzZ4HeUFebDoGpM52lZD+463Dq2hezmYtPaG77U6V3bUJ/TWH9VN/Or290vvN +v83ECcC2FWlSXdD5lFyqYx/E8gqE3YdgqfW62uqM+xBvoKsA9zvYLydVpsEN9v8m +6CSvs/2btA4O21e5u5WBTQKBgGtAb6vFpe0gHRDs24SOeYUs0lWycPhf+qFjobrP +lqnHpa9iPeheat7UV6BfeW3qmBIVl/s4IPE2ld4z0qqZiB0Tf6ssu/TpXNPsNXS6 +dLFz+myC+ufFdNEoQUtQitd5wKbjTCZCOGRaVRgJcSdG6Tq55Fa22mOKPm+mTmed +ZdKpAoGAFsTYBAHPxs8nzkCJCl7KLa4/zgbgywO6EcQgA7tfelB8bc8vcAMG5o+8 +YqAfwxrzhVSVbJx0fibTARXROmbh2pn010l2wj3+qUajM8NiskCPFbSjGy7HSUze +P8Kt1uMDJdj55gATzn44au31QBioZY2zXleorxF21cr+BZCJgfA= +-----END RSA PRIVATE KEY----- \ No newline at end of file From bfbd8b9f46c37214230026e23c09899cb12ba01b Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Thu, 11 Jan 2024 15:51:02 +0100 Subject: [PATCH 02/23] test: add unit test --- .../kms-request.test.ts | 35 +++++++------------ .../state_machine.test.ts | 35 +++++++++++++++++++ 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/test/integration/client-side-encryption/kms-request.test.ts b/test/integration/client-side-encryption/kms-request.test.ts index 551969fedc..fa987cd569 100644 --- a/test/integration/client-side-encryption/kms-request.test.ts +++ b/test/integration/client-side-encryption/kms-request.test.ts @@ -5,7 +5,7 @@ import { once } from 'events'; const { ClientEncryption } = require('../../../src/client-side-encryption/client_encryption'); -describe.only('kmsRequest', function () { +describe('kmsRequest', function () { context('when server closes connection without an error', function () { const endpoint = 'https://localhost:5699'; const provider = 'kmip'; @@ -17,15 +17,17 @@ describe.only('kmsRequest', function () { let clientEncrypted; let db; - beforeEach(async function () { + before(async function () { const serverKey = fs.readFileSync(`${__dirname}/ssl/server-key.pem`); const serverCrt = fs.readFileSync(`${__dirname}/ssl/server-crt.pem`); const ca = fs.readFileSync(`${__dirname}/ssl/ca.pem`); const clientKey = fs.readFileSync(`${__dirname}/ssl/client.pem`); - + tlsOptions = { tlsCAFile: ca, - tlsCertificateKeyFile: clientKey + tlsCertificateKeyFile: clientKey, + requestCert: true, + rejectUnauthorized: false }; const autoEncryption = { @@ -64,35 +66,24 @@ describe.only('kmsRequest', function () { it('kmsRequest rejects with kms request closed error', async function () { try { - console.log('tlsOptions----------------------'); - console.log(tlsOptions); - console.log('----------------------'); const clientEncryption = new ClientEncryption(client, { keyVaultNamespace, kmsProviders, tlsOptions }); - const createCollectionOptions = { - encryptedFields: { fields: [{ path: 'ssn', bsonType: 'string', keyId: null }] } - }; const masterKey = {}; - const { encryptedFields } = await clientEncryption.createEncryptedCollection( - db, - 'testing1', - { - provider, - createCollectionOptions, - masterKey - } - ); - console.log('encryptedFields----------------------'); - console.log(encryptedFields); + const dataKeyId = await clientEncryption.createDataKey(provider, { + masterKey, + keyAltNames: ['0703-dataKe6'] + }); + console.log('dataKeyId----------------------'); + console.log(dataKeyId); console.log('----------------------'); } catch (err) { console.log('err----------------------'); console.log(err); console.log('----------------------'); - expect(err.name).to.equal('MongoCryptCreateDataKeyError'); + expect(err.name).to.equal('MongoCryptError'); expect(err.message).to.include('KMS request closed'); return; } finally { diff --git a/test/unit/client-side-encryption/state_machine.test.ts b/test/unit/client-side-encryption/state_machine.test.ts index e84b7c1f18..a6c1901bd5 100644 --- a/test/unit/client-side-encryption/state_machine.test.ts +++ b/test/unit/client-side-encryption/state_machine.test.ts @@ -251,6 +251,41 @@ describe('StateMachine', function () { }); }); + context('when server closed the socket', function () { + let server; + + beforeEach(async () => { + server = net.createServer(async socket => { + socket.end(); + }); + server.listen(0); + await once(server, 'listening'); + }); + + afterEach(() => { + server.close(); + }); + + it('rejects with the kms request closed error', async function () { + const stateMachine = new StateMachine({ + proxyOptions: { + proxyHost: 'localhost', + proxyPort: server.address().port + } + } as any); + + const request = new MockRequest(Buffer.from('foobar'), 500); + try { + await stateMachine.kmsRequest(request); + } catch (err) { + expect(err.name).to.equal('MongoCryptError'); + expect(err.message).to.equal('KMS request closed'); + return; + } + expect.fail('missed exception'); + }); + }); + afterEach(function () { this.sinon.restore(); }); From 05bf2b3ba4a2061312885465eed38b4dbbd79cb3 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Fri, 12 Jan 2024 16:36:55 +0100 Subject: [PATCH 03/23] refactor: kmsRequest to async/await --- src/client-side-encryption/state_machine.ts | 194 +++++++++--------- .../kms-request.test.ts | 95 --------- .../client-side-encryption/ssl/ca.pem | 21 -- .../client-side-encryption/ssl/client.pem | 48 ----- .../client-side-encryption/ssl/server-crt.pem | 22 -- .../client-side-encryption/ssl/server-key.pem | 27 --- .../state_machine.test.ts | 12 +- 7 files changed, 108 insertions(+), 311 deletions(-) delete mode 100644 test/integration/client-side-encryption/kms-request.test.ts delete mode 100644 test/integration/client-side-encryption/ssl/ca.pem delete mode 100644 test/integration/client-side-encryption/ssl/client.pem delete mode 100644 test/integration/client-side-encryption/ssl/server-crt.pem delete mode 100644 test/integration/client-side-encryption/ssl/server-key.pem diff --git a/src/client-side-encryption/state_machine.ts b/src/client-side-encryption/state_machine.ts index 8f76acb0b9..1ab8fb9a5c 100644 --- a/src/client-side-encryption/state_machine.ts +++ b/src/client-side-encryption/state_machine.ts @@ -1,3 +1,4 @@ +import { once } from 'events'; import * as fs from 'fs/promises'; import { type MongoCryptContext, type MongoCryptKMSRequest } from 'mongodb-client-encryption'; import * as net from 'net'; @@ -282,7 +283,7 @@ export class StateMachine { * @param kmsContext - A C++ KMS context returned from the bindings * @returns A promise that resolves when the KMS reply has be fully parsed */ - kmsRequest(request: MongoCryptKMSRequest): Promise { + 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 } = { @@ -291,113 +292,122 @@ export class StateMachine { port }; const message = request.message; + const buffer = new BufferPool(); - // TODO(NODE-3959): We can adopt `for-await on(socket, 'data')` with logic to control abort - // eslint-disable-next-line @typescript-eslint/no-misused-promises, no-async-promise-executor - return new Promise(async (resolve, reject) => { - const buffer = new BufferPool(); + // eslint-disable-next-line prefer-const + let socket: net.Socket; + let rawSocket: net.Socket; - // eslint-disable-next-line prefer-const - let socket: net.Socket; - let rawSocket: net.Socket; - - function destroySockets() { - for (const sock of [socket, rawSocket]) { - if (sock) { - sock.removeAllListeners(); - sock.destroy(); - } + function destroySockets() { + for (const sock of [socket, rawSocket]) { + if (sock) { + sock.removeAllListeners(); + sock.destroy(); } } + } - function ontimeout() { - destroySockets(); - reject(new MongoCryptError('KMS request timed out')); - } - - function onerror(err: Error) { - destroySockets(); - const mcError = new MongoCryptError('KMS request failed', { cause: err }); - reject(mcError); - } + function ontimeout() { + destroySockets(); + throw new MongoCryptError('KMS request timed out'); + } - function onclose() { - destroySockets(); - const mcError = new MongoCryptError('KMS request closed'); - reject(mcError); - } + function onerror(err: Error) { + destroySockets(); + throw new MongoCryptError('KMS request failed', { cause: err }); + } - if (this.options.proxyOptions && this.options.proxyOptions.proxyHost) { - rawSocket = net.connect({ - host: this.options.proxyOptions.proxyHost, - port: this.options.proxyOptions.proxyPort || 1080 - }); + function onclose() { + destroySockets(); + throw new MongoCryptError('KMS request closed'); + } - rawSocket.on('timeout', ontimeout); - rawSocket.on('error', onerror); - rawSocket.on('close', onclose); + if (this.options.proxyOptions && this.options.proxyOptions.proxyHost) { + rawSocket = net.connect({ + host: this.options.proxyOptions.proxyHost, + port: this.options.proxyOptions.proxyPort || 1080 + }); - try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const events = require('events') as typeof import('events'); - await events.once(rawSocket, 'connect'); - socks ??= loadSocks(); - options.socket = ( - await socks.SocksClient.createConnection({ - existing_socket: rawSocket, - command: 'connect', - destination: { host: options.host, port: options.port }, - proxy: { - // host and port are ignored because we pass existing_socket - host: 'iLoveJavaScript', - port: 0, - type: 5, - userId: this.options.proxyOptions.proxyUsername, - password: this.options.proxyOptions.proxyPassword - } - }) - ).socket; - } catch (err) { - return onerror(err); - } + await Promise.race([ + new Promise((resolve, reject) => { + rawSocket.once('timeout', reject); + }).catch(ontimeout), + new Promise((resolve, reject) => { + rawSocket.once('error', reject); + }).catch(onerror), + new Promise((resolve, reject) => { + rawSocket.once('close', reject); + }).catch(onclose), + once(rawSocket, 'connect') + ]); + + try { + socks ??= loadSocks(); + options.socket = ( + await socks.SocksClient.createConnection({ + existing_socket: rawSocket, + command: 'connect', + destination: { host: options.host, port: options.port }, + proxy: { + // host and port are ignored because we pass existing_socket + host: 'iLoveJavaScript', + port: 0, + type: 5, + userId: this.options.proxyOptions.proxyUsername, + password: this.options.proxyOptions.proxyPassword + } + }) + ).socket; + } catch (err) { + return onerror(err); } + } - const tlsOptions = this.options.tlsOptions; - if (tlsOptions) { - const kmsProvider = request.kmsProvider as ClientEncryptionDataKeyProvider; - const providerTlsOptions = tlsOptions[kmsProvider]; - if (providerTlsOptions) { - const error = this.validateTlsOptions(kmsProvider, providerTlsOptions); - if (error) reject(error); - try { - await this.setTlsOptions(providerTlsOptions, options); - } catch (error) { - return onerror(error); - } + const tlsOptions = this.options.tlsOptions; + if (tlsOptions) { + const kmsProvider = request.kmsProvider as ClientEncryptionDataKeyProvider; + const providerTlsOptions = tlsOptions[kmsProvider]; + if (providerTlsOptions) { + const error = this.validateTlsOptions(kmsProvider, providerTlsOptions); + if (error) throw error; + try { + await this.setTlsOptions(providerTlsOptions, options); + } catch (error) { + return onerror(error); } } - socket = tls.connect(options, () => { - socket.write(message); - }); + } - socket.once('timeout', ontimeout); - socket.once('error', onerror); - socket.once('close', onclose); + socket = tls.connect(options, () => { + socket.write(message); + }); - socket.on('data', data => { - buffer.append(data); - while (request.bytesNeeded > 0 && buffer.length) { - const bytesNeeded = Math.min(request.bytesNeeded, buffer.length); - request.addResponse(buffer.read(bytesNeeded)); - } + await Promise.race([ + new Promise((resolve, reject) => { + socket.once('timeout', reject); + }).catch(ontimeout), + new Promise((resolve, reject) => { + socket.once('error', reject); + }).catch(onerror), + new Promise((resolve, reject) => { + socket.once('close', reject); + }).catch(onclose), + new Promise(resolve => { + socket.on('data', data => { + buffer.append(data); + while (request.bytesNeeded > 0 && buffer.length) { + const bytesNeeded = Math.min(request.bytesNeeded, buffer.length); + request.addResponse(buffer.read(bytesNeeded)); + } - if (request.bytesNeeded <= 0) { - // There's no need for any more activity on this socket at this point. - destroySockets(); - resolve(); - } - }); - }); + if (request.bytesNeeded <= 0) { + // There's no need for any more activity on this socket at this point. + destroySockets(); + resolve(); + } + }); + }) + ]); } *requests(context: MongoCryptContext) { diff --git a/test/integration/client-side-encryption/kms-request.test.ts b/test/integration/client-side-encryption/kms-request.test.ts deleted file mode 100644 index fa987cd569..0000000000 --- a/test/integration/client-side-encryption/kms-request.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { expect } from 'chai'; -const fs = require('fs'); -import * as tls from 'tls'; -import { once } from 'events'; - -const { ClientEncryption } = require('../../../src/client-side-encryption/client_encryption'); - -describe('kmsRequest', function () { - context('when server closes connection without an error', function () { - const endpoint = 'https://localhost:5699'; - const provider = 'kmip'; - const kmsProviders = { kmip: { endpoint } }; - const keyVaultNamespace = 'encryption._keyVault'; - let tlsOptions; - let serverThatClosesOnHandshake; - let client; - let clientEncrypted; - let db; - - before(async function () { - const serverKey = fs.readFileSync(`${__dirname}/ssl/server-key.pem`); - const serverCrt = fs.readFileSync(`${__dirname}/ssl/server-crt.pem`); - const ca = fs.readFileSync(`${__dirname}/ssl/ca.pem`); - const clientKey = fs.readFileSync(`${__dirname}/ssl/client.pem`); - - tlsOptions = { - tlsCAFile: ca, - tlsCertificateKeyFile: clientKey, - requestCert: true, - rejectUnauthorized: false - }; - - const autoEncryption = { - keyVaultNamespace, - kmsProviders, - tlsOptions: { [provider]: tlsOptions }, - explicitEncryptionOnly: true, - }; - - // Start fake server. - serverThatClosesOnHandshake = tls.createServer({ - key: serverKey, - cert: serverCrt, - ca: [ca] - }, (socket) => { - socket.end(); - }); - serverThatClosesOnHandshake.listen(5699); - await once(serverThatClosesOnHandshake, 'listening'); - - client = this.configuration.newClient(); - await client.connect(); - - db = client.db('automatic_data_encryption_keys'); - await db.dropDatabase().catch(() => null); - - clientEncrypted = this.configuration.newClient({}, { autoEncryption }); - await clientEncrypted.connect(); - }); - - afterEach(() => { - serverThatClosesOnHandshake.close(); - client.close(); - clientEncrypted.close(); - }); - - it('kmsRequest rejects with kms request closed error', async function () { - try { - const clientEncryption = new ClientEncryption(client, { - keyVaultNamespace, - kmsProviders, - tlsOptions - }); - const masterKey = {}; - const dataKeyId = await clientEncryption.createDataKey(provider, { - masterKey, - keyAltNames: ['0703-dataKe6'] - }); - console.log('dataKeyId----------------------'); - console.log(dataKeyId); - console.log('----------------------'); - } catch (err) { - console.log('err----------------------'); - console.log(err); - console.log('----------------------'); - expect(err.name).to.equal('MongoCryptError'); - expect(err.message).to.include('KMS request closed'); - return; - } finally { - client.close(); - clientEncrypted.close(); - } - }); - }); -}); \ No newline at end of file diff --git a/test/integration/client-side-encryption/ssl/ca.pem b/test/integration/client-side-encryption/ssl/ca.pem deleted file mode 100644 index 6ac86cfcc1..0000000000 --- a/test/integration/client-side-encryption/ssl/ca.pem +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDfzCCAmegAwIBAgIDB1MGMA0GCSqGSIb3DQEBCwUAMHkxGzAZBgNVBAMTEkRy -aXZlcnMgVGVzdGluZyBDQTEQMA4GA1UECxMHRHJpdmVyczEQMA4GA1UEChMHTW9u -Z29EQjEWMBQGA1UEBxMNTmV3IFlvcmsgQ2l0eTERMA8GA1UECBMITmV3IFlvcmsx -CzAJBgNVBAYTAlVTMB4XDTE5MDUyMjIwMjMxMVoXDTM5MDUyMjIwMjMxMVoweTEb -MBkGA1UEAxMSRHJpdmVycyBUZXN0aW5nIENBMRAwDgYDVQQLEwdEcml2ZXJzMRAw -DgYDVQQKEwdNb25nb0RCMRYwFAYDVQQHEw1OZXcgWW9yayBDaXR5MREwDwYDVQQI -EwhOZXcgWW9yazELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw -ggEKAoIBAQCl7VN+WsQfHlwapcOpTLZVoeMAl1LTbWTFuXSAavIyy0W1Ytky1UP/ -bxCSW0mSWwCgqoJ5aXbAvrNRp6ArWu3LsTQIEcD3pEdrFIVQhYzWUs9fXqPyI9k+ -QNNQ+MRFKeGteTPYwF2eVEtPzUHU5ws3+OKp1m6MCLkwAG3RBFUAfddUnLvGoZiT -pd8/eNabhgHvdrCw+tYFCWvSjz7SluEVievpQehrSEPKe8DxJq/IM3tSl3tdylzT -zeiKNO7c7LuQrgjAfrZl7n2SriHIlNmqiDR/kdd8+TxBuxjFlcf2WyHCO3lIcIgH -KXTlhUCg50KfHaxHu05Qw0x8869yIzqbAgMBAAGjEDAOMAwGA1UdEwQFMAMBAf8w -DQYJKoZIhvcNAQELBQADggEBAEHuhTL8KQZcKCTSJbYA9MgZj7U32arMGBbc1hiq -VBREwvdVz4+9tIyWMzN9R/YCKmUTnCq8z3wTlC8kBtxYn/l4Tj8nJYcgLJjQ0Fwe -gT564CmvkUat8uXPz6olOCdwkMpJ9Sj62i0mpgXJdBfxKQ6TZ9yGz6m3jannjZpN -LchB7xSAEWtqUgvNusq0dApJsf4n7jZ+oBZVaQw2+tzaMfaLqHgMwcu1FzA8UKCD -sxCgIsZUs8DdxaD418Ot6nPfheOTqe24n+TTa+Z6O0W0QtnofJBx7tmAo1aEc57i -77s89pfwIJetpIlhzNSMKurCAocFCJMJLAASJFuu6dyDvPo= ------END CERTIFICATE----- \ No newline at end of file diff --git a/test/integration/client-side-encryption/ssl/client.pem b/test/integration/client-side-encryption/ssl/client.pem deleted file mode 100644 index 5b07001092..0000000000 --- a/test/integration/client-side-encryption/ssl/client.pem +++ /dev/null @@ -1,48 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAsNS8UEuin7/K29jXfIOLpIoh1jEyWVqxiie2Onx7uJJKcoKo -khA3XeUnVN0k6X5MwYWcN52xcns7LYtyt06nRpTG2/emoV44w9uKTuHsvUbiOwSV -m/ToKQQ4FUFZoqorXH+ZmJuIpJNfoW+3CkE1vEDCIecIq6BNg5ySsPtvSuSJHGjp -mc7/5ZUDvFE2aJ8QbJU3Ws0HXiEb6ymi048LlzEL2VKX3w6mqqh+7dcZGAy7qYk2 -5FZ9ktKvCeQau7mTyU1hsPrKFiKtMN8Q2ZAItX13asw5/IeSTq2LgLFHlbj5Kpq4 -GmLdNCshzH5X7Ew3IYM8EHmsX8dmD6mhv7vpVwIDAQABAoIBABOdpb4qhcG+3twA -c/cGCKmaASLnljQ/UU6IFTjrsjXJVKTbRaPeVKX/05sgZQXZ0t3s2mV5AsQ2U1w8 -Cd+3w+qaemzQThW8hAOGCROzEDX29QWi/o2sX0ydgTMqaq0Wv3SlWv6I0mGfT45y -/BURIsrdTCvCmz2erLqa1dL4MWJXRFjT9UTs5twlecIOM2IHKoGGagFhymRK4kDe -wTRC9fpfoAgyfus3pCO/wi/F8yKGPDEwY+zgkhrJQ+kSeki7oKdGD1H540vB8gRt -EIqssE0Y6rEYf97WssQlxJgvoJBDSftOijS6mwvoasDUwfFqyyPiirawXWWhHXkc -DjIi/XECgYEA5xfjilw9YyM2UGQNESbNNunPcj7gDZbN347xJwmYmi9AUdPLt9xN -3XaMqqR22k1DUOxC/5hH0uiXir7mDfqmC+XS/ic/VOsa3CDWejkEnyGLiwSHY502 -wD/xWgHwUiGVAG9HY64vnDGm6L3KGXA2oqxanL4V0+0+Ht49pZ16i8sCgYEAw+Ox -CHGtpkzjCP/z8xr+1VTSdpc/4CP2HONnYopcn48KfQnf7Nale69/1kZpypJlvQSG -eeA3jMGigNJEkb8/kaVoRLCisXcwLc0XIfCTeiK6FS0Ka30D/84Qm8UsHxRdpGkM -kYITAa2r64tgRL8as4/ukeXBKE+oOhX43LeEfyUCgYBkf7IX2Ndlhsm3GlvIarxy -NipeP9PGdR/hKlPbq0OvQf9R1q7QrcE7H7Q6/b0mYNV2mtjkOQB7S2WkFDMOP0P5 -BqDEoKLdNkV/F9TOYH+PCNKbyYNrodJOt0Ap6Y/u1+Xpw3sjcXwJDFrO+sKqX2+T -PStG4S+y84jBedsLbDoAEwKBgQCTz7/KC11o2yOFqv09N+WKvBKDgeWlD/2qFr3w -UU9K5viXGVhqshz0k5z25vL09Drowf1nAZVpFMO2SPOMtq8VC6b+Dfr1xmYIaXVH -Gu1tf77CM9Zk/VSDNc66e7GrUgbHBK2DLo+A+Ld9aRIfTcSsMbNnS+LQtCrQibvb -cG7+MQKBgQCY11oMT2dUekoZEyW4no7W5D74lR8ztMjp/fWWTDo/AZGPBY6cZoZF -IICrzYtDT/5BzB0Jh1f4O9ZQkm5+OvlFbmoZoSbMzHL3oJCBOY5K0/kdGXL46WWh -IRJSYakNU6VIS7SjDpKgm9D8befQqZeoSggSjIIULIiAtYgS80vmGA== ------END RSA PRIVATE KEY----- ------BEGIN CERTIFICATE----- -MIIDgzCCAmugAwIBAgIDAxOUMA0GCSqGSIb3DQEBCwUAMHkxGzAZBgNVBAMTEkRy -aXZlcnMgVGVzdGluZyBDQTEQMA4GA1UECxMHRHJpdmVyczEQMA4GA1UEChMHTW9u -Z29EQjEWMBQGA1UEBxMNTmV3IFlvcmsgQ2l0eTERMA8GA1UECBMITmV3IFlvcmsx -CzAJBgNVBAYTAlVTMB4XDTE5MDUyMjIzNTU1NFoXDTM5MDUyMjIzNTU1NFowaTEP -MA0GA1UEAxMGY2xpZW50MRAwDgYDVQQLEwdEcml2ZXJzMQwwCgYDVQQKEwNNREIx -FjAUBgNVBAcTDU5ldyBZb3JrIENpdHkxETAPBgNVBAgTCE5ldyBZb3JrMQswCQYD -VQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALDUvFBLop+/ -ytvY13yDi6SKIdYxMllasYontjp8e7iSSnKCqJIQN13lJ1TdJOl+TMGFnDedsXJ7 -Oy2LcrdOp0aUxtv3pqFeOMPbik7h7L1G4jsElZv06CkEOBVBWaKqK1x/mZibiKST -X6FvtwpBNbxAwiHnCKugTYOckrD7b0rkiRxo6ZnO/+WVA7xRNmifEGyVN1rNB14h -G+spotOPC5cxC9lSl98Opqqofu3XGRgMu6mJNuRWfZLSrwnkGru5k8lNYbD6yhYi -rTDfENmQCLV9d2rMOfyHkk6ti4CxR5W4+SqauBpi3TQrIcx+V+xMNyGDPBB5rF/H -Zg+pob+76VcCAwEAAaMkMCIwCwYDVR0PBAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUF -BwMCMA0GCSqGSIb3DQEBCwUAA4IBAQAqRcLAGvYMaGYOV4HJTzNotT2qE0I9THNQ -wOV1fBg69x6SrUQTQLjJEptpOA288Wue6Jt3H+p5qAGV5GbXjzN/yjCoItggSKxG -Xg7279nz6/C5faoIKRjpS9R+MsJGlttP9nUzdSxrHvvqm62OuSVFjjETxD39DupE -YPFQoHOxdFTtBQlc/zIKxVdd20rs1xJeeU2/L7jtRBSPuR/Sk8zot7G2/dQHX49y -kHrq8qz12kj1T6XDXf8KZawFywXaz0/Ur+fUYKmkVk1T0JZaNtF4sKqDeNE4zcns -p3xLVDSl1Q5Gwj7bgph9o4Hxs9izPwiqjmNaSjPimGYZ399zcurY ------END CERTIFICATE----- diff --git a/test/integration/client-side-encryption/ssl/server-crt.pem b/test/integration/client-side-encryption/ssl/server-crt.pem deleted file mode 100644 index 26c34b2c4d..0000000000 --- a/test/integration/client-side-encryption/ssl/server-crt.pem +++ /dev/null @@ -1,22 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDlTCCAn2gAwIBAgICdxUwDQYJKoZIhvcNAQELBQAweTEbMBkGA1UEAxMSRHJp -dmVycyBUZXN0aW5nIENBMRAwDgYDVQQLEwdEcml2ZXJzMRAwDgYDVQQKEwdNb25n -b0RCMRYwFAYDVQQHEw1OZXcgWW9yayBDaXR5MREwDwYDVQQIEwhOZXcgWW9yazEL -MAkGA1UEBhMCVVMwHhcNMTkwNTIyMjIzMjU2WhcNMzkwNTIyMjIzMjU2WjBwMRIw -EAYDVQQDEwlsb2NhbGhvc3QxEDAOBgNVBAsTB0RyaXZlcnMxEDAOBgNVBAoTB01v -bmdvREIxFjAUBgNVBAcTDU5ldyBZb3JrIENpdHkxETAPBgNVBAgTCE5ldyBZb3Jr -MQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAITa -wdBOhmP5BUnfP7zabv7fedrW5zrA+Xa1dFz75rsu0oCg2Gv0vsW/4z55WSinoXQZ -x+6j/4GG79+GcpM3AxWbHIzy4hnNnERj/jUAkhStHdv+3ctec0g984Y9eBvPjJKQ -Ho01PtP6hTWTHOZ3g7xmp03tPvGlmtA17LDNDLk8SL+4+grbDFnqiy6Hze2tSsNl -L5ongh3xzLgP9PKScEZ1f/uU/VjjQTyY2tmUEaxtQY6SOPNc0j1GXrCKovMg5RMX -b4DWkWSBB4v540RKsFYCTmaJlGOe85isqYjM28+PHQ07FYFErcaxKm2j/gRI0GD1 -+fkEJOBnTI5C3+di0ekCAwEAAaMwMC4wLAYDVR0RBCUwI4IJbG9jYWxob3N0hwR/ -AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEBCwUAA4IBAQBol8+YH7MA -HwnIh7KcJ8h87GkCWsjOJCDJWiYBJArQ0MmgDO0qdx+QEtvLMn3XNtP05ZfK0WyX -or4cWllAkMFYaFbyB2hYazlD1UAAG+22Rku0UP6pJMLbWe6pnqzx+RL68FYdbZhN -fCW2xiiKsdPoo2VEY7eeZKrNr/0RFE5EKXgzmobpTBQT1Dl3Ve4aWLoTy9INlQ/g -z40qS7oq1PjjPLgxINhf4ncJqfmRXugYTOnyFiVXLZTys5Pb9SMKdToGl3NTYWLL -2AZdjr6bKtT+WtXyHqO0cQ8CkAW0M6VOlMluACllcJxfrtdlQS2S4lUIj76QKBdZ -khBHXq/b8MFX ------END CERTIFICATE----- \ No newline at end of file diff --git a/test/integration/client-side-encryption/ssl/server-key.pem b/test/integration/client-side-encryption/ssl/server-key.pem deleted file mode 100644 index 2c60d046e8..0000000000 --- a/test/integration/client-side-encryption/ssl/server-key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEAhNrB0E6GY/kFSd8/vNpu/t952tbnOsD5drV0XPvmuy7SgKDY -a/S+xb/jPnlZKKehdBnH7qP/gYbv34ZykzcDFZscjPLiGc2cRGP+NQCSFK0d2/7d -y15zSD3zhj14G8+MkpAejTU+0/qFNZMc5neDvGanTe0+8aWa0DXssM0MuTxIv7j6 -CtsMWeqLLofN7a1Kw2UvmieCHfHMuA/08pJwRnV/+5T9WONBPJja2ZQRrG1BjpI4 -81zSPUZesIqi8yDlExdvgNaRZIEHi/njREqwVgJOZomUY57zmKypiMzbz48dDTsV -gUStxrEqbaP+BEjQYPX5+QQk4GdMjkLf52LR6QIDAQABAoIBAHSs+hHLJNOf2zkp -S3y8CUblVMsQeTpsR6otaehPgi9Zy50TpX4KD5D0GMrBH8BIl86y5Zd7h+VlcDzK -gs0vPxI2izhuBovKuzaE6rf5rFFkSBjxGDCG3o/PeJOoYFdsS3RcBbjVzju0hFCs -xnDQ/Wz0anJRrTnjyraY5SnQqx/xuhLXkj/lwWoWjP2bUqDprnuLOj16soNu60Um -JziWbmWx9ty0wohkI/8DPBl9FjSniEEUi9pnZXPElFN6kwPkgdfT5rY/TkMH4lsu -ozOUc5xgwlkT6kVjXHcs3fleuT/mOfVXLPgNms85JKLucfd6KiV7jYZkT/bXIjQ+ -7CZEn0ECgYEA5QiKZgsfJjWvZpt21V/i7dPje2xdwHtZ8F9NjX7ZUFA7mUPxUlwe -GiXxmy6RGzNdnLOto4SF0/7ebuF3koO77oLup5a2etL+y/AnNAufbu4S5D72sbiz -wdLzr3d5JQ12xeaEH6kQNk2SD5/ShctdS6GmTgQPiJIgH0MIdi9F3v0CgYEAlH84 -hMWcC+5b4hHUEexeNkT8kCXwHVcUjGRaYFdSHgovvWllApZDHSWZ+vRcMBdlhNPu -09Btxo99cjOZwGYJyt20QQLGc/ZyiOF4ximQzabTeFgLkTH3Ox6Mh2Rx9yIruYoX -nE3UfMDkYELanEJUv0zenKpZHw7tTt5yXXSlEF0CgYBSsEOvVcKYO/eoluZPYQAA -F2jgzZ4HeUFebDoGpM52lZD+463Dq2hezmYtPaG77U6V3bUJ/TWH9VN/Or290vvN -v83ECcC2FWlSXdD5lFyqYx/E8gqE3YdgqfW62uqM+xBvoKsA9zvYLydVpsEN9v8m -6CSvs/2btA4O21e5u5WBTQKBgGtAb6vFpe0gHRDs24SOeYUs0lWycPhf+qFjobrP -lqnHpa9iPeheat7UV6BfeW3qmBIVl/s4IPE2ld4z0qqZiB0Tf6ssu/TpXNPsNXS6 -dLFz+myC+ufFdNEoQUtQitd5wKbjTCZCOGRaVRgJcSdG6Tq55Fa22mOKPm+mTmed -ZdKpAoGAFsTYBAHPxs8nzkCJCl7KLa4/zgbgywO6EcQgA7tfelB8bc8vcAMG5o+8 -YqAfwxrzhVSVbJx0fibTARXROmbh2pn010l2wj3+qUajM8NiskCPFbSjGy7HSUze -P8Kt1uMDJdj55gATzn44au31QBioZY2zXleorxF21cr+BZCJgfA= ------END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/test/unit/client-side-encryption/state_machine.test.ts b/test/unit/client-side-encryption/state_machine.test.ts index a6c1901bd5..28a7969764 100644 --- a/test/unit/client-side-encryption/state_machine.test.ts +++ b/test/unit/client-side-encryption/state_machine.test.ts @@ -253,7 +253,7 @@ describe('StateMachine', function () { context('when server closed the socket', function () { let server; - + beforeEach(async () => { server = net.createServer(async socket => { socket.end(); @@ -261,25 +261,25 @@ describe('StateMachine', function () { server.listen(0); await once(server, 'listening'); }); - + afterEach(() => { server.close(); }); - - it('rejects with the kms request closed error', async function () { + + it('throws a MongoCryptError error', async function () { const stateMachine = new StateMachine({ proxyOptions: { proxyHost: 'localhost', proxyPort: server.address().port } } as any); - + const request = new MockRequest(Buffer.from('foobar'), 500); try { await stateMachine.kmsRequest(request); } catch (err) { expect(err.name).to.equal('MongoCryptError'); - expect(err.message).to.equal('KMS request closed'); + expect(err.message).to.equal('KMS request failed'); return; } expect.fail('missed exception'); From 08ad041093d40386229445e04d7fdc001c4e452e Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Fri, 12 Jan 2024 18:17:32 +0100 Subject: [PATCH 04/23] refactor: use promiseWithResolvers --- src/client-side-encryption/state_machine.ts | 79 +++++++++------------ 1 file changed, 35 insertions(+), 44 deletions(-) diff --git a/src/client-side-encryption/state_machine.ts b/src/client-side-encryption/state_machine.ts index 1ab8fb9a5c..ce640d6682 100644 --- a/src/client-side-encryption/state_machine.ts +++ b/src/client-side-encryption/state_machine.ts @@ -14,7 +14,7 @@ import { import { type ProxyOptions } from '../cmap/connection'; import { getSocks, type SocksLib } from '../deps'; import { type MongoClient, type MongoClientOptions } from '../mongo_client'; -import { BufferPool, MongoDBCollectionNamespace } from '../utils'; +import { BufferPool, MongoDBCollectionNamespace, promiseWithResolvers } from '../utils'; import { type DataKey } from './client_encryption'; import { MongoCryptError } from './errors'; import { type MongocryptdManager } from './mongocryptd_manager'; @@ -309,37 +309,33 @@ export class StateMachine { function ontimeout() { destroySockets(); - throw new MongoCryptError('KMS request timed out'); + return new MongoCryptError('KMS request timed out'); } function onerror(err: Error) { destroySockets(); - throw new MongoCryptError('KMS request failed', { cause: err }); + return new MongoCryptError('KMS request failed', { cause: err }); } function onclose() { destroySockets(); - throw new MongoCryptError('KMS request closed'); + return new MongoCryptError('KMS request closed'); } + const { promise: willError, reject } = promiseWithResolvers(); + if (this.options.proxyOptions && this.options.proxyOptions.proxyHost) { rawSocket = net.connect({ host: this.options.proxyOptions.proxyHost, port: this.options.proxyOptions.proxyPort || 1080 }); - await Promise.race([ - new Promise((resolve, reject) => { - rawSocket.once('timeout', reject); - }).catch(ontimeout), - new Promise((resolve, reject) => { - rawSocket.once('error', reject); - }).catch(onerror), - new Promise((resolve, reject) => { - rawSocket.once('close', reject); - }).catch(onclose), - once(rawSocket, 'connect') - ]); + rawSocket + .once('timeout', () => reject(ontimeout())) + .once('error', err => reject(onerror(err))) + .once('close', () => reject(onclose())); + + await Promise.race([willError, once(rawSocket, 'connect')]); try { socks ??= loadSocks(); @@ -359,7 +355,7 @@ export class StateMachine { }) ).socket; } catch (err) { - return onerror(err); + throw onerror(err); } } @@ -372,8 +368,8 @@ export class StateMachine { if (error) throw error; try { await this.setTlsOptions(providerTlsOptions, options); - } catch (error) { - return onerror(error); + } catch (err) { + throw onerror(err); } } } @@ -382,32 +378,27 @@ export class StateMachine { socket.write(message); }); - await Promise.race([ - new Promise((resolve, reject) => { - socket.once('timeout', reject); - }).catch(ontimeout), - new Promise((resolve, reject) => { - socket.once('error', reject); - }).catch(onerror), - new Promise((resolve, reject) => { - socket.once('close', reject); - }).catch(onclose), - new Promise(resolve => { - socket.on('data', data => { - buffer.append(data); - while (request.bytesNeeded > 0 && buffer.length) { - const bytesNeeded = Math.min(request.bytesNeeded, buffer.length); - request.addResponse(buffer.read(bytesNeeded)); - } + const { promise: willSuccseed, resolve } = promiseWithResolvers(); + + socket + .once('timeout', () => reject(ontimeout())) + .once('error', err => reject(onerror(err))) + .once('close', () => reject(onclose())) + .on('data', data => { + buffer.append(data); + while (request.bytesNeeded > 0 && buffer.length) { + const bytesNeeded = Math.min(request.bytesNeeded, buffer.length); + request.addResponse(buffer.read(bytesNeeded)); + } - if (request.bytesNeeded <= 0) { - // There's no need for any more activity on this socket at this point. - destroySockets(); - resolve(); - } - }); - }) - ]); + if (request.bytesNeeded <= 0) { + // There's no need for any more activity on this socket at this point. + destroySockets(); + resolve(undefined); + } + }); + + await Promise.race([willError, willSuccseed]); } *requests(context: MongoCryptContext) { From 29beb702d7683526349fa25560b63b062664b4cb Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Sat, 13 Jan 2024 00:07:30 +0100 Subject: [PATCH 05/23] refactor: destroy sockets at the very end --- src/client-side-encryption/state_machine.ts | 136 ++++++++++---------- 1 file changed, 68 insertions(+), 68 deletions(-) diff --git a/src/client-side-encryption/state_machine.ts b/src/client-side-encryption/state_machine.ts index ce640d6682..277dc7b50f 100644 --- a/src/client-side-encryption/state_machine.ts +++ b/src/client-side-encryption/state_machine.ts @@ -294,7 +294,6 @@ export class StateMachine { const message = request.message; const buffer = new BufferPool(); - // eslint-disable-next-line prefer-const let socket: net.Socket; let rawSocket: net.Socket; @@ -308,97 +307,98 @@ export class StateMachine { } function ontimeout() { - destroySockets(); return new MongoCryptError('KMS request timed out'); } function onerror(err: Error) { - destroySockets(); return new MongoCryptError('KMS request failed', { cause: err }); } function onclose() { - destroySockets(); return new MongoCryptError('KMS request closed'); } - const { promise: willError, reject } = promiseWithResolvers(); + try { + const { promise: willError, reject } = promiseWithResolvers(); - if (this.options.proxyOptions && this.options.proxyOptions.proxyHost) { - rawSocket = net.connect({ - host: this.options.proxyOptions.proxyHost, - port: this.options.proxyOptions.proxyPort || 1080 - }); + if (this.options.proxyOptions && this.options.proxyOptions.proxyHost) { + rawSocket = net.connect({ + host: this.options.proxyOptions.proxyHost, + port: this.options.proxyOptions.proxyPort || 1080 + }); - rawSocket - .once('timeout', () => reject(ontimeout())) - .once('error', err => reject(onerror(err))) - .once('close', () => reject(onclose())); - - await Promise.race([willError, once(rawSocket, 'connect')]); - - try { - socks ??= loadSocks(); - options.socket = ( - await socks.SocksClient.createConnection({ - existing_socket: rawSocket, - command: 'connect', - destination: { host: options.host, port: options.port }, - proxy: { - // host and port are ignored because we pass existing_socket - host: 'iLoveJavaScript', - port: 0, - type: 5, - userId: this.options.proxyOptions.proxyUsername, - password: this.options.proxyOptions.proxyPassword - } - }) - ).socket; - } catch (err) { - throw onerror(err); - } - } + rawSocket + .once('timeout', () => reject(ontimeout())) + .once('error', err => reject(onerror(err))) + .once('close', () => reject(onclose())); + + await Promise.race([willError, once(rawSocket, 'connect')]); - const tlsOptions = this.options.tlsOptions; - if (tlsOptions) { - const kmsProvider = request.kmsProvider as ClientEncryptionDataKeyProvider; - const providerTlsOptions = tlsOptions[kmsProvider]; - if (providerTlsOptions) { - const error = this.validateTlsOptions(kmsProvider, providerTlsOptions); - if (error) throw error; try { - await this.setTlsOptions(providerTlsOptions, options); + socks ??= loadSocks(); + options.socket = ( + await socks.SocksClient.createConnection({ + existing_socket: rawSocket, + command: 'connect', + destination: { host: options.host, port: options.port }, + proxy: { + // host and port are ignored because we pass existing_socket + host: 'iLoveJavaScript', + port: 0, + type: 5, + userId: this.options.proxyOptions.proxyUsername, + password: this.options.proxyOptions.proxyPassword + } + }) + ).socket; } catch (err) { throw onerror(err); } } - } - socket = tls.connect(options, () => { - socket.write(message); - }); - - const { promise: willSuccseed, resolve } = promiseWithResolvers(); - - socket - .once('timeout', () => reject(ontimeout())) - .once('error', err => reject(onerror(err))) - .once('close', () => reject(onclose())) - .on('data', data => { - buffer.append(data); - while (request.bytesNeeded > 0 && buffer.length) { - const bytesNeeded = Math.min(request.bytesNeeded, buffer.length); - request.addResponse(buffer.read(bytesNeeded)); + const tlsOptions = this.options.tlsOptions; + if (tlsOptions) { + const kmsProvider = request.kmsProvider as ClientEncryptionDataKeyProvider; + const providerTlsOptions = tlsOptions[kmsProvider]; + if (providerTlsOptions) { + const error = this.validateTlsOptions(kmsProvider, providerTlsOptions); + if (error) throw error; + try { + await this.setTlsOptions(providerTlsOptions, options); + } catch (err) { + throw onerror(err); + } } + } - if (request.bytesNeeded <= 0) { - // There's no need for any more activity on this socket at this point. - destroySockets(); - resolve(undefined); - } + socket = tls.connect(options, () => { + socket.write(message); }); - await Promise.race([willError, willSuccseed]); + const { promise: willSuccseed, resolve } = promiseWithResolvers(); + + socket + .once('timeout', () => reject(ontimeout())) + .once('error', err => reject(onerror(err))) + .once('close', () => reject(onclose())) + .on('data', data => { + buffer.append(data); + while (request.bytesNeeded > 0 && buffer.length) { + const bytesNeeded = Math.min(request.bytesNeeded, buffer.length); + request.addResponse(buffer.read(bytesNeeded)); + } + + if (request.bytesNeeded <= 0) { + resolve(undefined); + } + }); + + await Promise.race([willError, willSuccseed]); + } catch (err) { + // There's no need for any more activity on this socket at this point. + destroySockets(); + throw err; + } } *requests(context: MongoCryptContext) { From 12356a2439f825cfb163ebaa29b839efe9e7d881 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Mon, 15 Jan 2024 12:59:29 +0100 Subject: [PATCH 06/23] refactor: split rejecters --- src/client-side-encryption/state_machine.ts | 23 +++++++++++---------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/client-side-encryption/state_machine.ts b/src/client-side-encryption/state_machine.ts index 277dc7b50f..2f66190b2e 100644 --- a/src/client-side-encryption/state_machine.ts +++ b/src/client-side-encryption/state_machine.ts @@ -319,7 +319,7 @@ export class StateMachine { } try { - const { promise: willError, reject } = promiseWithResolvers(); + const { promise: onceRawSocketError, reject: rawSocketReject } = promiseWithResolvers(); if (this.options.proxyOptions && this.options.proxyOptions.proxyHost) { rawSocket = net.connect({ @@ -328,11 +328,11 @@ export class StateMachine { }); rawSocket - .once('timeout', () => reject(ontimeout())) - .once('error', err => reject(onerror(err))) - .once('close', () => reject(onclose())); + .once('timeout', () => rawSocketReject(ontimeout())) + .once('error', err => rawSocketReject(onerror(err))) + .once('close', () => rawSocketReject(onclose())); - await Promise.race([willError, once(rawSocket, 'connect')]); + await Promise.race([onceRawSocketError, once(rawSocket, 'connect')]); try { socks ??= loadSocks(); @@ -375,12 +375,13 @@ export class StateMachine { socket.write(message); }); - const { promise: willSuccseed, resolve } = promiseWithResolvers(); + const { promise: onceSocketError, reject: socketReject } = promiseWithResolvers(); + const { promise: onDataFullyRead, resolve: socketResolve } = promiseWithResolvers(); socket - .once('timeout', () => reject(ontimeout())) - .once('error', err => reject(onerror(err))) - .once('close', () => reject(onclose())) + .once('timeout', () => socketReject(ontimeout())) + .once('error', err => socketReject(onerror(err))) + .once('close', () => socketReject(onclose())) .on('data', data => { buffer.append(data); while (request.bytesNeeded > 0 && buffer.length) { @@ -389,11 +390,11 @@ export class StateMachine { } if (request.bytesNeeded <= 0) { - resolve(undefined); + socketResolve(undefined); } }); - await Promise.race([willError, willSuccseed]); + await Promise.race([onceSocketError, onDataFullyRead]); } catch (err) { // There's no need for any more activity on this socket at this point. destroySockets(); From ac17114abe393fb6c47b0af652a57c0191860e2f Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Mon, 15 Jan 2024 13:16:49 +0100 Subject: [PATCH 07/23] refactor: listen for error events on tls socket only --- src/client-side-encryption/state_machine.ts | 22 +++++++-------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/client-side-encryption/state_machine.ts b/src/client-side-encryption/state_machine.ts index 2f66190b2e..d4cbc447f3 100644 --- a/src/client-side-encryption/state_machine.ts +++ b/src/client-side-encryption/state_machine.ts @@ -319,20 +319,13 @@ export class StateMachine { } try { - const { promise: onceRawSocketError, reject: rawSocketReject } = promiseWithResolvers(); - if (this.options.proxyOptions && this.options.proxyOptions.proxyHost) { rawSocket = net.connect({ host: this.options.proxyOptions.proxyHost, port: this.options.proxyOptions.proxyPort || 1080 }); - rawSocket - .once('timeout', () => rawSocketReject(ontimeout())) - .once('error', err => rawSocketReject(onerror(err))) - .once('close', () => rawSocketReject(onclose())); - - await Promise.race([onceRawSocketError, once(rawSocket, 'connect')]); + await once(rawSocket, 'connect'); try { socks ??= loadSocks(); @@ -375,13 +368,12 @@ export class StateMachine { socket.write(message); }); - const { promise: onceSocketError, reject: socketReject } = promiseWithResolvers(); - const { promise: onDataFullyRead, resolve: socketResolve } = promiseWithResolvers(); + const { promise, reject, resolve } = promiseWithResolvers(); socket - .once('timeout', () => socketReject(ontimeout())) - .once('error', err => socketReject(onerror(err))) - .once('close', () => socketReject(onclose())) + .once('timeout', () => reject(ontimeout())) + .once('error', err => reject(onerror(err))) + .once('close', () => reject(onclose())) .on('data', data => { buffer.append(data); while (request.bytesNeeded > 0 && buffer.length) { @@ -390,11 +382,11 @@ export class StateMachine { } if (request.bytesNeeded <= 0) { - socketResolve(undefined); + resolve(undefined); } }); - await Promise.race([onceSocketError, onDataFullyRead]); + await promise; } catch (err) { // There's no need for any more activity on this socket at this point. destroySockets(); From bdfb5e314891cbf359b7208d190fa320fd9f6ba0 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Mon, 15 Jan 2024 20:09:28 +0100 Subject: [PATCH 08/23] fix: try listen for errors again on rawSocket --- src/client-side-encryption/state_machine.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/client-side-encryption/state_machine.ts b/src/client-side-encryption/state_machine.ts index d4cbc447f3..8156526f4f 100644 --- a/src/client-side-encryption/state_machine.ts +++ b/src/client-side-encryption/state_machine.ts @@ -325,7 +325,12 @@ export class StateMachine { port: this.options.proxyOptions.proxyPort || 1080 }); - await once(rawSocket, 'connect'); + const { promise: onRawSocketError, reject } = promiseWithResolvers(); + rawSocket + .on('timeout', () => reject(ontimeout())) + .on('error', err => reject(onerror(err))) + .on('close', () => reject(onclose())); + await Promise.race([onRawSocketError, once(rawSocket, 'connect')]); try { socks ??= loadSocks(); @@ -369,7 +374,6 @@ export class StateMachine { }); const { promise, reject, resolve } = promiseWithResolvers(); - socket .once('timeout', () => reject(ontimeout())) .once('error', err => reject(onerror(err))) From f3b9c62c101627b1e1a44f381831ae99b8f8776c Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Mon, 15 Jan 2024 20:58:16 +0100 Subject: [PATCH 09/23] fix: add back destroy socket on resolve --- src/client-side-encryption/state_machine.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/client-side-encryption/state_machine.ts b/src/client-side-encryption/state_machine.ts index 8156526f4f..dd18fb3ab4 100644 --- a/src/client-side-encryption/state_machine.ts +++ b/src/client-side-encryption/state_machine.ts @@ -325,12 +325,13 @@ export class StateMachine { port: this.options.proxyOptions.proxyPort || 1080 }); - const { promise: onRawSocketError, reject } = promiseWithResolvers(); + const { promise: onRawSocketConnect, reject, resolve } = promiseWithResolvers(); rawSocket .on('timeout', () => reject(ontimeout())) .on('error', err => reject(onerror(err))) - .on('close', () => reject(onclose())); - await Promise.race([onRawSocketError, once(rawSocket, 'connect')]); + .on('close', () => reject(onclose())) + .once('connect', () => resolve(undefined)); + await onRawSocketConnect; try { socks ??= loadSocks(); @@ -373,7 +374,7 @@ export class StateMachine { socket.write(message); }); - const { promise, reject, resolve } = promiseWithResolvers(); + const { promise: onSocketDataFullyRead, reject, resolve } = promiseWithResolvers(); socket .once('timeout', () => reject(ontimeout())) .once('error', err => reject(onerror(err))) @@ -386,13 +387,15 @@ export class StateMachine { } if (request.bytesNeeded <= 0) { + // There's no need for any more activity on this socket at this point. + destroySockets(); resolve(undefined); } }); - await promise; + await onSocketDataFullyRead; } catch (err) { - // There's no need for any more activity on this socket at this point. + // Destroy sockets on error. destroySockets(); throw err; } From 4536da2381bcb522d6e17f5f1d7a6b819939d9cf Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Mon, 15 Jan 2024 21:24:24 +0100 Subject: [PATCH 10/23] refactor: try again without error handlers on rawSocket --- src/client-side-encryption/state_machine.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/client-side-encryption/state_machine.ts b/src/client-side-encryption/state_machine.ts index dd18fb3ab4..99b108e882 100644 --- a/src/client-side-encryption/state_machine.ts +++ b/src/client-side-encryption/state_machine.ts @@ -325,13 +325,7 @@ export class StateMachine { port: this.options.proxyOptions.proxyPort || 1080 }); - const { promise: onRawSocketConnect, reject, resolve } = promiseWithResolvers(); - rawSocket - .on('timeout', () => reject(ontimeout())) - .on('error', err => reject(onerror(err))) - .on('close', () => reject(onclose())) - .once('connect', () => resolve(undefined)); - await onRawSocketConnect; + await once(rawSocket, 'connect'); try { socks ??= loadSocks(); From 70f279ac92898723b38c517318d40bc0f4a6803b Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Tue, 16 Jan 2024 19:26:06 +0100 Subject: [PATCH 11/23] refactor: destroy sockets on finally --- src/client-side-encryption/state_machine.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/client-side-encryption/state_machine.ts b/src/client-side-encryption/state_machine.ts index 99b108e882..daca88d821 100644 --- a/src/client-side-encryption/state_machine.ts +++ b/src/client-side-encryption/state_machine.ts @@ -294,7 +294,7 @@ export class StateMachine { const message = request.message; const buffer = new BufferPool(); - let socket: net.Socket; + let socket: tls.TLSSocket; let rawSocket: net.Socket; function destroySockets() { @@ -310,8 +310,8 @@ export class StateMachine { return new MongoCryptError('KMS request timed out'); } - function onerror(err: Error) { - return new MongoCryptError('KMS request failed', { cause: err }); + function onerror(cause: Error) { + return new MongoCryptError('KMS request failed', { cause }); } function onclose() { @@ -368,7 +368,7 @@ export class StateMachine { socket.write(message); }); - const { promise: onSocketDataFullyRead, reject, resolve } = promiseWithResolvers(); + const { promise: onSocketDataFullyRead, reject, resolve } = promiseWithResolvers(); socket .once('timeout', () => reject(ontimeout())) .once('error', err => reject(onerror(err))) @@ -381,17 +381,14 @@ export class StateMachine { } if (request.bytesNeeded <= 0) { - // There's no need for any more activity on this socket at this point. - destroySockets(); - resolve(undefined); + resolve(); } }); await onSocketDataFullyRead; - } catch (err) { - // Destroy sockets on error. + } finally { + // There's no need for any more activity on this socket at this point. destroySockets(); - throw err; } } From bd7723f134a46371ce11f7002bcb3cc9e2aae9cd Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Tue, 16 Jan 2024 21:30:50 +0100 Subject: [PATCH 12/23] fix: add error listeners to raw socket --- src/client-side-encryption/state_machine.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/client-side-encryption/state_machine.ts b/src/client-side-encryption/state_machine.ts index daca88d821..83e8f8ed28 100644 --- a/src/client-side-encryption/state_machine.ts +++ b/src/client-side-encryption/state_machine.ts @@ -325,7 +325,12 @@ export class StateMachine { port: this.options.proxyOptions.proxyPort || 1080 }); - await once(rawSocket, 'connect'); + const { promise: onceRawSocketError, reject } = promiseWithResolvers(); + rawSocket + .once('timeout', () => reject(ontimeout())) + .once('error', err => reject(onerror(err))) + .once('close', () => reject(onclose())); + await Promise.race([onceRawSocketError, once(rawSocket, 'connect')]); try { socks ??= loadSocks(); @@ -384,7 +389,6 @@ export class StateMachine { resolve(); } }); - await onSocketDataFullyRead; } finally { // There's no need for any more activity on this socket at this point. From 36c10fbe41b8a191478a9d377fdc04549904cf71 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Tue, 16 Jan 2024 21:33:29 +0100 Subject: [PATCH 13/23] test: add test for a plain endpoint --- .../state_machine.test.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/test/unit/client-side-encryption/state_machine.test.ts b/test/unit/client-side-encryption/state_machine.test.ts index 28a7969764..e2cf7fefc4 100644 --- a/test/unit/client-side-encryption/state_machine.test.ts +++ b/test/unit/client-side-encryption/state_machine.test.ts @@ -266,7 +266,7 @@ describe('StateMachine', function () { server.close(); }); - it('throws a MongoCryptError error', async function () { + it('throws a MongoCryptError error with proxyOptions', async function () { const stateMachine = new StateMachine({ proxyOptions: { proxyHost: 'localhost', @@ -284,6 +284,23 @@ describe('StateMachine', function () { } expect.fail('missed exception'); }); + + it('throws a MongoCryptError error with host and port', async function () { + const stateMachine = new StateMachine({ + host: 'localhost', + port: server.address().port + } as any); + + const request = new MockRequest(Buffer.from('foobar'), 500); + try { + await stateMachine.kmsRequest(request); + } catch (err) { + expect(err.name).to.equal('MongoCryptError'); + expect(err.message).to.equal('KMS request failed'); + return; + } + expect.fail('missed exception'); + }); }); afterEach(function () { From e744436cc1487abe47a93500d293bbe798e682c0 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Tue, 16 Jan 2024 22:43:26 +0100 Subject: [PATCH 14/23] refactor: try with a single socket --- src/client-side-encryption/state_machine.ts | 31 +++++++----------- .../state_machine.test.ts | 32 +++++++++++++------ 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/client-side-encryption/state_machine.ts b/src/client-side-encryption/state_machine.ts index 83e8f8ed28..61d9c207bc 100644 --- a/src/client-side-encryption/state_machine.ts +++ b/src/client-side-encryption/state_machine.ts @@ -293,18 +293,7 @@ export class StateMachine { }; const message = request.message; const buffer = new BufferPool(); - - let socket: tls.TLSSocket; - let rawSocket: net.Socket; - - function destroySockets() { - for (const sock of [socket, rawSocket]) { - if (sock) { - sock.removeAllListeners(); - sock.destroy(); - } - } - } + const rawSocket = new net.Socket(); function ontimeout() { return new MongoCryptError('KMS request timed out'); @@ -319,17 +308,18 @@ export class StateMachine { } try { + const { promise: onceRawSocketError, reject: rejectOnRawSocket } = + promiseWithResolvers(); + if (this.options.proxyOptions && this.options.proxyOptions.proxyHost) { - rawSocket = net.connect({ + rawSocket.connect({ host: this.options.proxyOptions.proxyHost, port: this.options.proxyOptions.proxyPort || 1080 }); - - const { promise: onceRawSocketError, reject } = promiseWithResolvers(); rawSocket - .once('timeout', () => reject(ontimeout())) - .once('error', err => reject(onerror(err))) - .once('close', () => reject(onclose())); + .once('timeout', () => rejectOnRawSocket(ontimeout())) + .once('error', err => rejectOnRawSocket(onerror(err))) + .once('close', () => rejectOnRawSocket(onclose())); await Promise.race([onceRawSocketError, once(rawSocket, 'connect')]); try { @@ -369,7 +359,7 @@ export class StateMachine { } } - socket = tls.connect(options, () => { + const socket = tls.connect(options, () => { socket.write(message); }); @@ -392,7 +382,8 @@ export class StateMachine { await onSocketDataFullyRead; } finally { // There's no need for any more activity on this socket at this point. - destroySockets(); + rawSocket.removeAllListeners(); + rawSocket.destroy(); } } diff --git a/test/unit/client-side-encryption/state_machine.test.ts b/test/unit/client-side-encryption/state_machine.test.ts index e2cf7fefc4..6be6e08172 100644 --- a/test/unit/client-side-encryption/state_machine.test.ts +++ b/test/unit/client-side-encryption/state_machine.test.ts @@ -254,7 +254,7 @@ describe('StateMachine', function () { context('when server closed the socket', function () { let server; - beforeEach(async () => { + beforeEach(async function () { server = net.createServer(async socket => { socket.end(); }); @@ -262,8 +262,9 @@ describe('StateMachine', function () { await once(server, 'listening'); }); - afterEach(() => { + afterEach(function () { server.close(); + this.sinon.restore(); }); it('throws a MongoCryptError error with proxyOptions', async function () { @@ -273,7 +274,6 @@ describe('StateMachine', function () { proxyPort: server.address().port } } as any); - const request = new MockRequest(Buffer.from('foobar'), 500); try { await stateMachine.kmsRequest(request); @@ -285,18 +285,32 @@ describe('StateMachine', function () { expect.fail('missed exception'); }); - it('throws a MongoCryptError error with host and port', async function () { + it('throws a MongoCryptError error with a tls endpoint', async function () { const stateMachine = new StateMachine({ - host: 'localhost', - port: server.address().port + tlsOptions: { aws: { tlsCertificateKeyFile: 'test.pem' } } } as any); + const request = new MockRequest(Buffer.from('foobar'), -1); + const buffer = Buffer.from('foobar'); + + this.sinon.stub(fs, 'readFile').callsFake(fileName => { + expect(fileName).to.equal('test.pem'); + return Promise.resolve(buffer); + }); + this.sinon.stub(tls, 'connect').callsFake((options, callback) => { + this.fakeSocket = new MockSocket(callback); + return this.fakeSocket; + }); - const request = new MockRequest(Buffer.from('foobar'), 500); try { - await stateMachine.kmsRequest(request); + const kmsRequestPromise = stateMachine.kmsRequest(request); + + await promisify(setTimeout)(0); + this.fakeSocket.emit('close'); + + await kmsRequestPromise; } catch (err) { expect(err.name).to.equal('MongoCryptError'); - expect(err.message).to.equal('KMS request failed'); + expect(err.message).to.equal('KMS request closed'); return; } expect.fail('missed exception'); From e12e84e2cf27deb0ad8123943725cfe8bffcd202 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Tue, 16 Jan 2024 23:32:44 +0100 Subject: [PATCH 15/23] fix: destroy tls socket --- src/client-side-encryption/state_machine.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/client-side-encryption/state_machine.ts b/src/client-side-encryption/state_machine.ts index 61d9c207bc..6e92555841 100644 --- a/src/client-side-encryption/state_machine.ts +++ b/src/client-side-encryption/state_machine.ts @@ -294,6 +294,16 @@ export class StateMachine { const message = request.message; const buffer = new BufferPool(); const rawSocket = new net.Socket(); + let socket: tls.TLSSocket; + + function destroySockets() { + for (const sock of [socket, rawSocket]) { + if (sock) { + sock.removeAllListeners(); + sock.destroy(); + } + } + } function ontimeout() { return new MongoCryptError('KMS request timed out'); @@ -382,8 +392,7 @@ export class StateMachine { await onSocketDataFullyRead; } finally { // There's no need for any more activity on this socket at this point. - rawSocket.removeAllListeners(); - rawSocket.destroy(); + destroySockets(); } } From 48a985c13f4c3e7d8cffbc8d774cc1408bddd957 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Wed, 17 Jan 2024 00:12:04 +0100 Subject: [PATCH 16/23] refactor: revert --- src/client-side-encryption/state_machine.ts | 17 ++++++++--------- .../state_machine.test.ts | 3 ++- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/client-side-encryption/state_machine.ts b/src/client-side-encryption/state_machine.ts index 6e92555841..2b66d18b86 100644 --- a/src/client-side-encryption/state_machine.ts +++ b/src/client-side-encryption/state_machine.ts @@ -293,8 +293,9 @@ export class StateMachine { }; const message = request.message; const buffer = new BufferPool(); - const rawSocket = new net.Socket(); + let socket: tls.TLSSocket; + let rawSocket: net.Socket; function destroySockets() { for (const sock of [socket, rawSocket]) { @@ -318,18 +319,16 @@ export class StateMachine { } try { - const { promise: onceRawSocketError, reject: rejectOnRawSocket } = - promiseWithResolvers(); - if (this.options.proxyOptions && this.options.proxyOptions.proxyHost) { - rawSocket.connect({ + rawSocket = net.connect({ host: this.options.proxyOptions.proxyHost, port: this.options.proxyOptions.proxyPort || 1080 }); + const { promise: onceRawSocketError, reject } = promiseWithResolvers(); rawSocket - .once('timeout', () => rejectOnRawSocket(ontimeout())) - .once('error', err => rejectOnRawSocket(onerror(err))) - .once('close', () => rejectOnRawSocket(onclose())); + .once('timeout', () => reject(ontimeout())) + .once('error', err => reject(onerror(err))) + .once('close', () => reject(onclose())); await Promise.race([onceRawSocketError, once(rawSocket, 'connect')]); try { @@ -369,7 +368,7 @@ export class StateMachine { } } - const socket = tls.connect(options, () => { + socket = tls.connect(options, () => { socket.write(message); }); diff --git a/test/unit/client-side-encryption/state_machine.test.ts b/test/unit/client-side-encryption/state_machine.test.ts index 6be6e08172..b6b7f1885a 100644 --- a/test/unit/client-side-encryption/state_machine.test.ts +++ b/test/unit/client-side-encryption/state_machine.test.ts @@ -16,7 +16,7 @@ import { Db } from '../../../src/db'; import { MongoClient } from '../../../src/mongo_client'; import { Int32, Long, serialize } from '../../mongodb'; -describe('StateMachine', function () { +describe.only('StateMachine', function () { class MockRequest implements MongoCryptKMSRequest { _bytesNeeded: number; endpoint = 'some.fake.host.com'; @@ -275,6 +275,7 @@ describe('StateMachine', function () { } } as any); const request = new MockRequest(Buffer.from('foobar'), 500); + try { await stateMachine.kmsRequest(request); } catch (err) { From d9fa8c83e768fae6773e8e2d927404dcfa9f2c8b Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Wed, 17 Jan 2024 00:12:26 +0100 Subject: [PATCH 17/23] refactor: remove only --- test/unit/client-side-encryption/state_machine.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/client-side-encryption/state_machine.test.ts b/test/unit/client-side-encryption/state_machine.test.ts index b6b7f1885a..1bd261f352 100644 --- a/test/unit/client-side-encryption/state_machine.test.ts +++ b/test/unit/client-side-encryption/state_machine.test.ts @@ -16,7 +16,7 @@ import { Db } from '../../../src/db'; import { MongoClient } from '../../../src/mongo_client'; import { Int32, Long, serialize } from '../../mongodb'; -describe.only('StateMachine', function () { +describe('StateMachine', function () { class MockRequest implements MongoCryptKMSRequest { _bytesNeeded: number; endpoint = 'some.fake.host.com'; From 96b0cc8eb336dcbc815587cfb6fbc813f270b440 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Wed, 17 Jan 2024 09:34:33 +0100 Subject: [PATCH 18/23] refactor: try one more way --- src/client-side-encryption/state_machine.ts | 63 +++++++++++---------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/src/client-side-encryption/state_machine.ts b/src/client-side-encryption/state_machine.ts index 2b66d18b86..7a3a5d3e4a 100644 --- a/src/client-side-encryption/state_machine.ts +++ b/src/client-side-encryption/state_machine.ts @@ -286,19 +286,19 @@ 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; socket: net.Socket } = { host: parsedUrl[0], servername: parsedUrl[0], - port + port, + socket: new net.Socket() }; const message = request.message; const buffer = new BufferPool(); let socket: tls.TLSSocket; - let rawSocket: net.Socket; function destroySockets() { - for (const sock of [socket, rawSocket]) { + for (const sock of [socket, options.socket]) { if (sock) { sock.removeAllListeners(); sock.destroy(); @@ -318,36 +318,35 @@ export class StateMachine { return new MongoCryptError('KMS request closed'); } + const { promise: onceNetSocketError, reject: rejectOnNetSocketError } = promiseWithResolvers(); + options.socket + .once('timeout', () => rejectOnNetSocketError(ontimeout())) + .once('error', err => rejectOnNetSocketError(onerror(err))) + .once('close', () => rejectOnNetSocketError(onclose())); + try { if (this.options.proxyOptions && this.options.proxyOptions.proxyHost) { - rawSocket = net.connect({ + options.socket.connect({ host: this.options.proxyOptions.proxyHost, port: this.options.proxyOptions.proxyPort || 1080 }); - const { promise: onceRawSocketError, reject } = promiseWithResolvers(); - rawSocket - .once('timeout', () => reject(ontimeout())) - .once('error', err => reject(onerror(err))) - .once('close', () => reject(onclose())); - await Promise.race([onceRawSocketError, once(rawSocket, 'connect')]); + await Promise.race([onceNetSocketError, once(options.socket, 'connect')]); try { socks ??= loadSocks(); - options.socket = ( - await socks.SocksClient.createConnection({ - existing_socket: rawSocket, - command: 'connect', - destination: { host: options.host, port: options.port }, - proxy: { - // host and port are ignored because we pass existing_socket - host: 'iLoveJavaScript', - port: 0, - type: 5, - userId: this.options.proxyOptions.proxyUsername, - password: this.options.proxyOptions.proxyPassword - } - }) - ).socket; + await socks.SocksClient.createConnection({ + existing_socket: options.socket, + command: 'connect', + destination: { host: options.host, port: options.port }, + proxy: { + // host and port are ignored because we pass existing_socket + host: 'iLoveJavaScript', + port: 0, + type: 5, + userId: this.options.proxyOptions.proxyUsername, + password: this.options.proxyOptions.proxyPassword + } + }); } catch (err) { throw onerror(err); } @@ -372,11 +371,15 @@ export class StateMachine { socket.write(message); }); - const { promise: onSocketDataFullyRead, reject, resolve } = promiseWithResolvers(); + const { + promise: onSocketDataFullyRead, + reject: rejectOnTlsSocketError, + resolve + } = promiseWithResolvers(); socket - .once('timeout', () => reject(ontimeout())) - .once('error', err => reject(onerror(err))) - .once('close', () => reject(onclose())) + .once('timeout', () => rejectOnTlsSocketError(ontimeout())) + .once('error', err => rejectOnTlsSocketError(onerror(err))) + .once('close', () => rejectOnTlsSocketError(onclose())) .on('data', data => { buffer.append(data); while (request.bytesNeeded > 0 && buffer.length) { From a3128fb1f0b39fd328b6c25e9467f2153d55e87f Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Wed, 17 Jan 2024 10:52:26 +0100 Subject: [PATCH 19/23] refactor: revert --- src/client-side-encryption/state_machine.ts | 49 +++++++++++---------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/src/client-side-encryption/state_machine.ts b/src/client-side-encryption/state_machine.ts index 7a3a5d3e4a..1cfbebf726 100644 --- a/src/client-side-encryption/state_machine.ts +++ b/src/client-side-encryption/state_machine.ts @@ -286,7 +286,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 options: tls.ConnectionOptions & { host: string; port: number; socket: net.Socket } = { + const options: tls.ConnectionOptions & { host: string; port: number } = { host: parsedUrl[0], servername: parsedUrl[0], port, @@ -295,10 +295,11 @@ export class StateMachine { const message = request.message; const buffer = new BufferPool(); + let rawSocket: net.Socket; let socket: tls.TLSSocket; function destroySockets() { - for (const sock of [socket, options.socket]) { + for (const sock of [socket, rawSocket]) { if (sock) { sock.removeAllListeners(); sock.destroy(); @@ -318,35 +319,37 @@ export class StateMachine { return new MongoCryptError('KMS request closed'); } - const { promise: onceNetSocketError, reject: rejectOnNetSocketError } = promiseWithResolvers(); - options.socket - .once('timeout', () => rejectOnNetSocketError(ontimeout())) - .once('error', err => rejectOnNetSocketError(onerror(err))) - .once('close', () => rejectOnNetSocketError(onclose())); - try { if (this.options.proxyOptions && this.options.proxyOptions.proxyHost) { - options.socket.connect({ + rawSocket = net.connect({ host: this.options.proxyOptions.proxyHost, port: this.options.proxyOptions.proxyPort || 1080 }); - await Promise.race([onceNetSocketError, once(options.socket, 'connect')]); + + const { promise: onceNetSocketError, reject: rejectOnNetSocketError } = promiseWithResolvers(); + rawSocket + .once('timeout', () => rejectOnNetSocketError(ontimeout())) + .once('error', err => rejectOnNetSocketError(onerror(err))) + .once('close', () => rejectOnNetSocketError(onclose())); + await Promise.race([onceNetSocketError, once(rawSocket, 'connect')]); try { socks ??= loadSocks(); - await socks.SocksClient.createConnection({ - existing_socket: options.socket, - command: 'connect', - destination: { host: options.host, port: options.port }, - proxy: { - // host and port are ignored because we pass existing_socket - host: 'iLoveJavaScript', - port: 0, - type: 5, - userId: this.options.proxyOptions.proxyUsername, - password: this.options.proxyOptions.proxyPassword - } - }); + options.socket = ( + await socks.SocksClient.createConnection({ + existing_socket: rawSocket, + command: 'connect', + destination: { host: options.host, port: options.port }, + proxy: { + // host and port are ignored because we pass existing_socket + host: 'iLoveJavaScript', + port: 0, + type: 5, + userId: this.options.proxyOptions.proxyUsername, + password: this.options.proxyOptions.proxyPassword + } + }) + ).socket; } catch (err) { throw onerror(err); } From 10c8b3d7c570917155718b7c119eb383b0d8959a Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Wed, 17 Jan 2024 11:27:33 +0100 Subject: [PATCH 20/23] refactor: and one more round --- src/client-side-encryption/state_machine.ts | 56 +++++++++++---------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/src/client-side-encryption/state_machine.ts b/src/client-side-encryption/state_machine.ts index 1cfbebf726..26c3b7e512 100644 --- a/src/client-side-encryption/state_machine.ts +++ b/src/client-side-encryption/state_machine.ts @@ -289,17 +289,16 @@ export class StateMachine { const options: tls.ConnectionOptions & { host: string; port: number } = { host: parsedUrl[0], servername: parsedUrl[0], - port, - socket: new net.Socket() + port }; const message = request.message; const buffer = new BufferPool(); - let rawSocket: net.Socket; + const netSocket: net.Socket = new net.Socket(); let socket: tls.TLSSocket; function destroySockets() { - for (const sock of [socket, rawSocket]) { + for (const sock of [socket, netSocket]) { if (sock) { sock.removeAllListeners(); sock.destroy(); @@ -319,25 +318,43 @@ export class StateMachine { return new MongoCryptError('KMS request closed'); } + const tlsOptions = this.options.tlsOptions; + if (tlsOptions) { + const kmsProvider = request.kmsProvider as ClientEncryptionDataKeyProvider; + const providerTlsOptions = tlsOptions[kmsProvider]; + if (providerTlsOptions) { + const error = this.validateTlsOptions(kmsProvider, providerTlsOptions); + if (error) { + throw error; + } + try { + await this.setTlsOptions(providerTlsOptions, options); + } catch (err) { + throw onerror(err); + } + } + } + + const { promise: onceNetSocketError, reject: rejectOnNetSocketError } = promiseWithResolvers(); + netSocket + .once('timeout', () => rejectOnNetSocketError(ontimeout())) + .once('error', err => rejectOnNetSocketError(onerror(err))) + .once('close', () => rejectOnNetSocketError(onclose())); + try { if (this.options.proxyOptions && this.options.proxyOptions.proxyHost) { - rawSocket = net.connect({ + netSocket.connect({ host: this.options.proxyOptions.proxyHost, port: this.options.proxyOptions.proxyPort || 1080 }); - const { promise: onceNetSocketError, reject: rejectOnNetSocketError } = promiseWithResolvers(); - rawSocket - .once('timeout', () => rejectOnNetSocketError(ontimeout())) - .once('error', err => rejectOnNetSocketError(onerror(err))) - .once('close', () => rejectOnNetSocketError(onclose())); - await Promise.race([onceNetSocketError, once(rawSocket, 'connect')]); + await Promise.race([onceNetSocketError, once(netSocket, 'connect')]); try { socks ??= loadSocks(); options.socket = ( await socks.SocksClient.createConnection({ - existing_socket: rawSocket, + existing_socket: netSocket, command: 'connect', destination: { host: options.host, port: options.port }, proxy: { @@ -355,21 +372,6 @@ export class StateMachine { } } - const tlsOptions = this.options.tlsOptions; - if (tlsOptions) { - const kmsProvider = request.kmsProvider as ClientEncryptionDataKeyProvider; - const providerTlsOptions = tlsOptions[kmsProvider]; - if (providerTlsOptions) { - const error = this.validateTlsOptions(kmsProvider, providerTlsOptions); - if (error) throw error; - try { - await this.setTlsOptions(providerTlsOptions, options); - } catch (err) { - throw onerror(err); - } - } - } - socket = tls.connect(options, () => { socket.write(message); }); From 788eeb5e6c2b40826fb80611348a1dade991b616 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Wed, 17 Jan 2024 17:00:54 +0100 Subject: [PATCH 21/23] test: close tls socket on server --- .../state_machine.test.ts | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/test/unit/client-side-encryption/state_machine.test.ts b/test/unit/client-side-encryption/state_machine.test.ts index 1bd261f352..38b385cbef 100644 --- a/test/unit/client-side-encryption/state_machine.test.ts +++ b/test/unit/client-side-encryption/state_machine.test.ts @@ -288,30 +288,17 @@ describe('StateMachine', function () { it('throws a MongoCryptError error with a tls endpoint', async function () { const stateMachine = new StateMachine({ + host: 'localhost', + post: server.address().port, tlsOptions: { aws: { tlsCertificateKeyFile: 'test.pem' } } } as any); - const request = new MockRequest(Buffer.from('foobar'), -1); - const buffer = Buffer.from('foobar'); - - this.sinon.stub(fs, 'readFile').callsFake(fileName => { - expect(fileName).to.equal('test.pem'); - return Promise.resolve(buffer); - }); - this.sinon.stub(tls, 'connect').callsFake((options, callback) => { - this.fakeSocket = new MockSocket(callback); - return this.fakeSocket; - }); + const request = new MockRequest(Buffer.from('foobar'), 500); try { - const kmsRequestPromise = stateMachine.kmsRequest(request); - - await promisify(setTimeout)(0); - this.fakeSocket.emit('close'); - - await kmsRequestPromise; + await stateMachine.kmsRequest(request); } catch (err) { expect(err.name).to.equal('MongoCryptError'); - expect(err.message).to.equal('KMS request closed'); + expect(err.message).to.equal('KMS request failed'); return; } expect.fail('missed exception'); From 1aec2be69241d6edb2afbe4b20513c6fd893d8fd Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Wed, 17 Jan 2024 18:45:05 +0100 Subject: [PATCH 22/23] refactor: clean up --- src/client-side-encryption/state_machine.ts | 17 ++-- .../state_machine.test.ts | 88 ++++++++++++------- 2 files changed, 66 insertions(+), 39 deletions(-) diff --git a/src/client-side-encryption/state_machine.ts b/src/client-side-encryption/state_machine.ts index 26c3b7e512..a4b2379fb5 100644 --- a/src/client-side-encryption/state_machine.ts +++ b/src/client-side-encryption/state_machine.ts @@ -1,4 +1,3 @@ -import { once } from 'events'; import * as fs from 'fs/promises'; import { type MongoCryptContext, type MongoCryptKMSRequest } from 'mongodb-client-encryption'; import * as net from 'net'; @@ -335,11 +334,16 @@ export class StateMachine { } } - const { promise: onceNetSocketError, reject: rejectOnNetSocketError } = promiseWithResolvers(); + const { + promise: willConnect, + reject: rejectOnNetSocketError, + resolve: resolveOnNetSocketConnect + } = promiseWithResolvers(); netSocket .once('timeout', () => rejectOnNetSocketError(ontimeout())) .once('error', err => rejectOnNetSocketError(onerror(err))) - .once('close', () => rejectOnNetSocketError(onclose())); + .once('close', () => rejectOnNetSocketError(onclose())) + .once('connect', () => resolveOnNetSocketConnect()); try { if (this.options.proxyOptions && this.options.proxyOptions.proxyHost) { @@ -347,8 +351,7 @@ export class StateMachine { host: this.options.proxyOptions.proxyHost, port: this.options.proxyOptions.proxyPort || 1080 }); - - await Promise.race([onceNetSocketError, once(netSocket, 'connect')]); + await willConnect; try { socks ??= loadSocks(); @@ -377,7 +380,7 @@ export class StateMachine { }); const { - promise: onSocketDataFullyRead, + promise: willResolveKmsRequest, reject: rejectOnTlsSocketError, resolve } = promiseWithResolvers(); @@ -396,7 +399,7 @@ export class StateMachine { resolve(); } }); - await onSocketDataFullyRead; + await willResolveKmsRequest; } finally { // There's no need for any more activity on this socket at this point. destroySockets(); diff --git a/test/unit/client-side-encryption/state_machine.test.ts b/test/unit/client-side-encryption/state_machine.test.ts index 38b385cbef..931881e7aa 100644 --- a/test/unit/client-side-encryption/state_machine.test.ts +++ b/test/unit/client-side-encryption/state_machine.test.ts @@ -264,44 +264,68 @@ describe('StateMachine', function () { afterEach(function () { server.close(); - this.sinon.restore(); }); - it('throws a MongoCryptError error with proxyOptions', async function () { - const stateMachine = new StateMachine({ - proxyOptions: { - proxyHost: 'localhost', - proxyPort: server.address().port + context('Socks5', function () { + it('throws a MongoCryptError with SocksClientError cause', async function () { + const stateMachine = new StateMachine({ + proxyOptions: { + proxyHost: 'localhost', + proxyPort: server.address().port + } + } as any); + const request = new MockRequest(Buffer.from('foobar'), 500); + + try { + await stateMachine.kmsRequest(request); + } catch (err) { + expect(err.name).to.equal('MongoCryptError'); + expect(err.message).to.equal('KMS request failed'); + expect(err.cause.constructor.name).to.equal('SocksClientError'); + return; } - } as any); - const request = new MockRequest(Buffer.from('foobar'), 500); - - try { - await stateMachine.kmsRequest(request); - } catch (err) { - expect(err.name).to.equal('MongoCryptError'); - expect(err.message).to.equal('KMS request failed'); - return; - } - expect.fail('missed exception'); + expect.fail('missed exception'); + }); }); - it('throws a MongoCryptError error with a tls endpoint', async function () { - const stateMachine = new StateMachine({ - host: 'localhost', - post: server.address().port, - tlsOptions: { aws: { tlsCertificateKeyFile: 'test.pem' } } - } as any); - const request = new MockRequest(Buffer.from('foobar'), 500); + context('endpoint', function () { + const netSocket = new net.Socket(); - try { - await stateMachine.kmsRequest(request); - } catch (err) { - expect(err.name).to.equal('MongoCryptError'); - expect(err.message).to.equal('KMS request failed'); - return; - } - expect.fail('missed exception'); + beforeEach(async function () { + netSocket.connect({ + port: server.address().port + }); + await once(netSocket, 'connect'); + + this.sinon.stub(tls, 'connect').returns(netSocket); + }); + + afterEach(function () { + server.close(); + this.sinon.restore(); + }); + + it('throws a MongoCryptError error', async function () { + const stateMachine = new StateMachine({ + host: 'localhost', + port: server.address().port + } as any); + const request = new MockRequest(Buffer.from('foobar'), 500); + + try { + const kmsRequestPromise = stateMachine.kmsRequest(request); + + await promisify(setTimeout)(0); + netSocket.end(); + + await kmsRequestPromise; + } catch (err) { + expect(err.name).to.equal('MongoCryptError'); + expect(err.message).to.equal('KMS request closed'); + return; + } + expect.fail('missed exception'); + }); }); }); From 16448caae71622d88f541490d175dd00bfcfe9af Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Wed, 17 Jan 2024 19:20:36 +0100 Subject: [PATCH 23/23] refactor: this time close really on server --- .../state_machine.test.ts | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/test/unit/client-side-encryption/state_machine.test.ts b/test/unit/client-side-encryption/state_machine.test.ts index 931881e7aa..eda8669b3e 100644 --- a/test/unit/client-side-encryption/state_machine.test.ts +++ b/test/unit/client-side-encryption/state_machine.test.ts @@ -252,21 +252,21 @@ describe('StateMachine', function () { }); context('when server closed the socket', function () { - let server; + context('Socks5', function () { + let server; - beforeEach(async function () { - server = net.createServer(async socket => { - socket.end(); + beforeEach(async function () { + server = net.createServer(async socket => { + socket.end(); + }); + server.listen(0); + await once(server, 'listening'); }); - server.listen(0); - await once(server, 'listening'); - }); - afterEach(function () { - server.close(); - }); + afterEach(function () { + server.close(); + }); - context('Socks5', function () { it('throws a MongoCryptError with SocksClientError cause', async function () { const stateMachine = new StateMachine({ proxyOptions: { @@ -288,15 +288,27 @@ describe('StateMachine', function () { }); }); - context('endpoint', function () { - const netSocket = new net.Socket(); + context('endpoint with host and port', function () { + let server; + let serverSocket; beforeEach(async function () { - netSocket.connect({ + server = net.createServer(async socket => { + serverSocket = socket; + }); + server.listen(0); + await once(server, 'listening'); + }); + + afterEach(function () { + server.close(); + }); + + beforeEach(async function () { + const netSocket = net.connect({ port: server.address().port }); await once(netSocket, 'connect'); - this.sinon.stub(tls, 'connect').returns(netSocket); }); @@ -316,7 +328,7 @@ describe('StateMachine', function () { const kmsRequestPromise = stateMachine.kmsRequest(request); await promisify(setTimeout)(0); - netSocket.end(); + serverSocket.end(); await kmsRequestPromise; } catch (err) {