mongodb 7.1.0 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/lib/bson.js +26 -5
- package/lib/bson.js.map +1 -1
- package/lib/change_stream.js +4 -0
- package/lib/change_stream.js.map +1 -1
- package/lib/client-side-encryption/auto_encrypter.js +19 -10
- package/lib/client-side-encryption/auto_encrypter.js.map +1 -1
- package/lib/client-side-encryption/client_encryption.js +1 -3
- package/lib/client-side-encryption/client_encryption.js.map +1 -1
- package/lib/cmap/auth/aws4.js +4 -4
- package/lib/cmap/auth/aws4.js.map +1 -1
- package/lib/cmap/auth/gssapi.js +3 -6
- package/lib/cmap/auth/gssapi.js.map +1 -1
- package/lib/cmap/auth/mongodb_aws.js +3 -2
- package/lib/cmap/auth/mongodb_aws.js.map +1 -1
- package/lib/cmap/auth/mongodb_oidc/azure_machine_workflow.js +3 -3
- package/lib/cmap/auth/mongodb_oidc/azure_machine_workflow.js.map +1 -1
- package/lib/cmap/auth/mongodb_oidc/gcp_machine_workflow.js +3 -3
- package/lib/cmap/auth/mongodb_oidc/gcp_machine_workflow.js.map +1 -1
- package/lib/cmap/auth/mongodb_oidc/k8s_machine_workflow.js +3 -3
- package/lib/cmap/auth/mongodb_oidc/k8s_machine_workflow.js.map +1 -1
- package/lib/cmap/auth/mongodb_oidc/token_machine_workflow.js +3 -3
- package/lib/cmap/auth/mongodb_oidc/token_machine_workflow.js.map +1 -1
- package/lib/cmap/auth/mongodb_oidc.js +4 -4
- package/lib/cmap/auth/mongodb_oidc.js.map +1 -1
- package/lib/cmap/auth/plain.js +1 -1
- package/lib/cmap/auth/plain.js.map +1 -1
- package/lib/cmap/auth/scram.js +53 -40
- package/lib/cmap/auth/scram.js.map +1 -1
- package/lib/cmap/commands.js +46 -39
- package/lib/cmap/commands.js.map +1 -1
- package/lib/cmap/connect.js +19 -2
- package/lib/cmap/connect.js.map +1 -1
- package/lib/cmap/connection.js +5 -2
- package/lib/cmap/connection.js.map +1 -1
- package/lib/cmap/handshake/client_metadata.js +3 -4
- package/lib/cmap/handshake/client_metadata.js.map +1 -1
- package/lib/cmap/wire_protocol/compression.js +8 -7
- package/lib/cmap/wire_protocol/compression.js.map +1 -1
- package/lib/cmap/wire_protocol/on_data.js.map +1 -1
- package/lib/cmap/wire_protocol/on_demand/document.js +9 -9
- package/lib/cmap/wire_protocol/on_demand/document.js.map +1 -1
- package/lib/connection_string.js +21 -5
- package/lib/connection_string.js.map +1 -1
- package/lib/gridfs/download.js +2 -1
- package/lib/gridfs/download.js.map +1 -1
- package/lib/gridfs/upload.js +7 -7
- package/lib/gridfs/upload.js.map +1 -1
- package/lib/mongo_client.js.map +1 -1
- package/lib/operations/execute_operation.js +114 -41
- package/lib/operations/execute_operation.js.map +1 -1
- package/lib/operations/operation.js +1 -0
- package/lib/operations/operation.js.map +1 -1
- package/lib/runtime_adapters.js +32 -0
- package/lib/runtime_adapters.js.map +1 -0
- package/lib/sdam/srv_polling.js +1 -1
- package/lib/sdam/srv_polling.js.map +1 -1
- package/lib/sdam/topology.js +4 -2
- package/lib/sdam/topology.js.map +1 -1
- package/lib/sessions.js +124 -79
- package/lib/sessions.js.map +1 -1
- package/lib/utils.js +28 -36
- package/lib/utils.js.map +1 -1
- package/mongodb.d.ts +45 -2
- package/package.json +30 -21
- package/src/bson.ts +28 -5
- package/src/change_stream.ts +5 -0
- package/src/client-side-encryption/auto_encrypter.ts +17 -11
- package/src/client-side-encryption/client_encryption.ts +1 -3
- package/src/cmap/auth/auth_provider.ts +1 -1
- package/src/cmap/auth/aws4.ts +5 -5
- package/src/cmap/auth/gssapi.ts +9 -6
- package/src/cmap/auth/mongodb_aws.ts +2 -2
- package/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts +1 -1
- package/src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts +1 -1
- package/src/cmap/auth/mongodb_oidc/k8s_machine_workflow.ts +1 -1
- package/src/cmap/auth/mongodb_oidc/token_machine_workflow.ts +1 -1
- package/src/cmap/auth/mongodb_oidc.ts +4 -4
- package/src/cmap/auth/plain.ts +2 -2
- package/src/cmap/auth/scram.ts +82 -55
- package/src/cmap/commands.ts +70 -51
- package/src/cmap/connect.ts +21 -1
- package/src/cmap/connection.ts +11 -4
- package/src/cmap/handshake/client_metadata.ts +6 -6
- package/src/cmap/wire_protocol/compression.ts +18 -14
- package/src/cmap/wire_protocol/on_data.ts +5 -5
- package/src/cmap/wire_protocol/on_demand/document.ts +12 -14
- package/src/connection_string.ts +26 -8
- package/src/deps.ts +4 -4
- package/src/gridfs/download.ts +2 -2
- package/src/gridfs/upload.ts +13 -12
- package/src/index.ts +1 -0
- package/src/mongo_client.ts +24 -0
- package/src/operations/client_bulk_write/command_builder.ts +1 -1
- package/src/operations/execute_operation.ts +146 -45
- package/src/operations/operation.ts +8 -0
- package/src/runtime_adapters.ts +64 -0
- package/src/sdam/srv_polling.ts +1 -1
- package/src/sdam/topology.ts +10 -7
- package/src/sessions.ts +140 -96
- package/src/utils.ts +40 -45
- package/tsconfig.json +1 -1
package/src/cmap/auth/plain.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Binary } from '../../bson';
|
|
1
|
+
import { Binary, ByteUtils } from '../../bson';
|
|
2
2
|
import { MongoMissingCredentialsError } from '../../error';
|
|
3
3
|
import { ns } from '../../utils';
|
|
4
4
|
import { type AuthContext, AuthProvider } from './auth_provider';
|
|
@@ -12,7 +12,7 @@ export class Plain extends AuthProvider {
|
|
|
12
12
|
|
|
13
13
|
const { username, password } = credentials;
|
|
14
14
|
|
|
15
|
-
const payload = new Binary(
|
|
15
|
+
const payload = new Binary(ByteUtils.fromUTF8(`\x00${username}\x00${password}`));
|
|
16
16
|
const command = {
|
|
17
17
|
saslStart: 1,
|
|
18
18
|
mechanism: 'PLAIN',
|
package/src/cmap/auth/scram.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { saslprep } from '@mongodb-js/saslprep';
|
|
2
|
-
import * as crypto from 'crypto';
|
|
3
2
|
|
|
4
|
-
import { Binary, type Document } from '../../bson';
|
|
3
|
+
import { Binary, ByteUtils, type Document } from '../../bson';
|
|
5
4
|
import {
|
|
6
5
|
MongoInvalidArgumentError,
|
|
7
6
|
MongoMissingCredentialsError,
|
|
@@ -65,21 +64,21 @@ function cleanUsername(username: string) {
|
|
|
65
64
|
return username.replace('=', '=3D').replace(',', '=2C');
|
|
66
65
|
}
|
|
67
66
|
|
|
68
|
-
function clientFirstMessageBare(username: string, nonce:
|
|
67
|
+
function clientFirstMessageBare(username: string, nonce: Uint8Array) {
|
|
69
68
|
// NOTE: This is done b/c Javascript uses UTF-16, but the server is hashing in UTF-8.
|
|
70
69
|
// Since the username is not sasl-prep-d, we need to do this here.
|
|
71
|
-
return
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
return ByteUtils.concat([
|
|
71
|
+
ByteUtils.fromUTF8('n='),
|
|
72
|
+
ByteUtils.fromUTF8(username),
|
|
73
|
+
ByteUtils.fromUTF8(',r='),
|
|
74
|
+
ByteUtils.fromUTF8(ByteUtils.toBase64(nonce))
|
|
76
75
|
]);
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
function makeFirstMessage(
|
|
80
79
|
cryptoMethod: CryptoMethod,
|
|
81
80
|
credentials: MongoCredentials,
|
|
82
|
-
nonce:
|
|
81
|
+
nonce: Uint8Array
|
|
83
82
|
) {
|
|
84
83
|
const username = cleanUsername(credentials.username);
|
|
85
84
|
const mechanism =
|
|
@@ -91,7 +90,7 @@ function makeFirstMessage(
|
|
|
91
90
|
saslStart: 1,
|
|
92
91
|
mechanism,
|
|
93
92
|
payload: new Binary(
|
|
94
|
-
|
|
93
|
+
ByteUtils.concat([ByteUtils.fromUTF8('n,,'), clientFirstMessageBare(username, nonce)])
|
|
95
94
|
),
|
|
96
95
|
autoAuthorize: 1,
|
|
97
96
|
options: { skipEmptyExchange: true }
|
|
@@ -136,7 +135,7 @@ async function continueScramConversation(
|
|
|
136
135
|
const processedPassword =
|
|
137
136
|
cryptoMethod === 'sha256' ? saslprep(password) : passwordDigest(username, password);
|
|
138
137
|
|
|
139
|
-
const payload: Binary =
|
|
138
|
+
const payload: Binary = ByteUtils.isUint8Array(response.payload)
|
|
140
139
|
? new Binary(response.payload)
|
|
141
140
|
: response.payload;
|
|
142
141
|
|
|
@@ -157,37 +156,37 @@ async function continueScramConversation(
|
|
|
157
156
|
|
|
158
157
|
// Set up start of proof
|
|
159
158
|
const withoutProof = `c=biws,r=${rnonce}`;
|
|
160
|
-
const saltedPassword = HI(
|
|
159
|
+
const saltedPassword = await HI(
|
|
161
160
|
processedPassword,
|
|
162
|
-
|
|
161
|
+
ByteUtils.fromBase64(salt),
|
|
163
162
|
iterations,
|
|
164
163
|
cryptoMethod
|
|
165
164
|
);
|
|
166
165
|
|
|
167
|
-
const clientKey = HMAC(cryptoMethod, saltedPassword, 'Client Key');
|
|
168
|
-
const serverKey = HMAC(cryptoMethod, saltedPassword, 'Server Key');
|
|
169
|
-
const storedKey = H(cryptoMethod, clientKey);
|
|
166
|
+
const clientKey = await HMAC(cryptoMethod, saltedPassword, 'Client Key');
|
|
167
|
+
const serverKey = await HMAC(cryptoMethod, saltedPassword, 'Server Key');
|
|
168
|
+
const storedKey = await H(cryptoMethod, clientKey);
|
|
170
169
|
const authMessage = [
|
|
171
170
|
clientFirstMessageBare(username, nonce),
|
|
172
171
|
payload.toString('utf8'),
|
|
173
172
|
withoutProof
|
|
174
173
|
].join(',');
|
|
175
174
|
|
|
176
|
-
const clientSignature = HMAC(cryptoMethod, storedKey, authMessage);
|
|
175
|
+
const clientSignature = await HMAC(cryptoMethod, storedKey, authMessage);
|
|
177
176
|
const clientProof = `p=${xor(clientKey, clientSignature)}`;
|
|
178
177
|
const clientFinal = [withoutProof, clientProof].join(',');
|
|
179
178
|
|
|
180
|
-
const serverSignature = HMAC(cryptoMethod, serverKey, authMessage);
|
|
179
|
+
const serverSignature = await HMAC(cryptoMethod, serverKey, authMessage);
|
|
181
180
|
const saslContinueCmd = {
|
|
182
181
|
saslContinue: 1,
|
|
183
182
|
conversationId: response.conversationId,
|
|
184
|
-
payload: new Binary(
|
|
183
|
+
payload: new Binary(ByteUtils.fromUTF8(clientFinal))
|
|
185
184
|
};
|
|
186
185
|
|
|
187
186
|
const r = await connection.command(ns(`${db}.$cmd`), saslContinueCmd, undefined);
|
|
188
187
|
const parsedResponse = parsePayload(r.payload);
|
|
189
188
|
|
|
190
|
-
if (!compareDigest(
|
|
189
|
+
if (!compareDigest(ByteUtils.fromBase64(parsedResponse.v), serverSignature)) {
|
|
191
190
|
throw new MongoRuntimeError('Server returned an invalid signature');
|
|
192
191
|
}
|
|
193
192
|
|
|
@@ -199,7 +198,7 @@ async function continueScramConversation(
|
|
|
199
198
|
const retrySaslContinueCmd = {
|
|
200
199
|
saslContinue: 1,
|
|
201
200
|
conversationId: r.conversationId,
|
|
202
|
-
payload:
|
|
201
|
+
payload: ByteUtils.allocate(0)
|
|
203
202
|
};
|
|
204
203
|
|
|
205
204
|
await connection.command(ns(`${db}.$cmd`), retrySaslContinueCmd, undefined);
|
|
@@ -229,31 +228,36 @@ function passwordDigest(username: string, password: string) {
|
|
|
229
228
|
throw new MongoInvalidArgumentError('Password cannot be empty');
|
|
230
229
|
}
|
|
231
230
|
|
|
232
|
-
let
|
|
231
|
+
let nodeCrypto;
|
|
233
232
|
try {
|
|
234
|
-
|
|
233
|
+
// TODO: NODE-7424 - remove dependency on 'crypto' for SCRAM-SHA-1 authentication
|
|
234
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
235
|
+
nodeCrypto = require('crypto');
|
|
236
|
+
} catch (e) {
|
|
237
|
+
throw new MongoRuntimeError(
|
|
238
|
+
'Node.js crypto module is required for SCRAM-SHA-1 authentication',
|
|
239
|
+
{
|
|
240
|
+
cause: e
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const md5 = nodeCrypto.createHash('md5');
|
|
247
|
+
md5.update(`${username}:mongo:${password}`, 'utf8');
|
|
248
|
+
return md5.digest('hex');
|
|
235
249
|
} catch (err) {
|
|
236
|
-
if (
|
|
250
|
+
if (nodeCrypto.getFips()) {
|
|
237
251
|
// This error is (slightly) more helpful than what comes from OpenSSL directly, e.g.
|
|
238
252
|
// 'Error: error:060800C8:digital envelope routines:EVP_DigestInit_ex:disabled for FIPS'
|
|
239
253
|
throw new Error('Auth mechanism SCRAM-SHA-1 is not supported in FIPS mode');
|
|
240
254
|
}
|
|
241
255
|
throw err;
|
|
242
256
|
}
|
|
243
|
-
md5.update(`${username}:mongo:${password}`, 'utf8');
|
|
244
|
-
return md5.digest('hex');
|
|
245
257
|
}
|
|
246
258
|
|
|
247
259
|
// XOR two buffers
|
|
248
|
-
function xor(a:
|
|
249
|
-
if (!Buffer.isBuffer(a)) {
|
|
250
|
-
a = Buffer.from(a);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (!Buffer.isBuffer(b)) {
|
|
254
|
-
b = Buffer.from(b);
|
|
255
|
-
}
|
|
256
|
-
|
|
260
|
+
function xor(a: Uint8Array, b: Uint8Array) {
|
|
257
261
|
const length = Math.max(a.length, b.length);
|
|
258
262
|
const res = [];
|
|
259
263
|
|
|
@@ -261,19 +265,35 @@ function xor(a: Buffer, b: Buffer) {
|
|
|
261
265
|
res.push(a[i] ^ b[i]);
|
|
262
266
|
}
|
|
263
267
|
|
|
264
|
-
return
|
|
268
|
+
return ByteUtils.toBase64(ByteUtils.fromNumberArray(res));
|
|
265
269
|
}
|
|
266
270
|
|
|
267
|
-
function H(method: CryptoMethod, text:
|
|
268
|
-
|
|
271
|
+
async function H(method: CryptoMethod, text: Uint8Array): Promise<Uint8Array> {
|
|
272
|
+
const buffer = await crypto.subtle.digest(method === 'sha256' ? 'SHA-256' : 'SHA-1', text);
|
|
273
|
+
return new Uint8Array(buffer);
|
|
269
274
|
}
|
|
270
275
|
|
|
271
|
-
function HMAC(
|
|
272
|
-
|
|
276
|
+
async function HMAC(
|
|
277
|
+
method: CryptoMethod,
|
|
278
|
+
key: Uint8Array,
|
|
279
|
+
text: Uint8Array | string
|
|
280
|
+
): Promise<Uint8Array> {
|
|
281
|
+
const keyBuffer = ByteUtils.toLocalBufferType(key);
|
|
282
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
283
|
+
'raw',
|
|
284
|
+
keyBuffer,
|
|
285
|
+
{ name: 'HMAC', hash: { name: method === 'sha256' ? 'SHA-256' : 'SHA-1' } },
|
|
286
|
+
false,
|
|
287
|
+
['sign', 'verify']
|
|
288
|
+
);
|
|
289
|
+
const textData: Uint8Array = typeof text === 'string' ? new TextEncoder().encode(text) : text;
|
|
290
|
+
const textBuffer = ByteUtils.toLocalBufferType(textData);
|
|
291
|
+
const signature = await crypto.subtle.sign('HMAC', cryptoKey, textBuffer);
|
|
292
|
+
return new Uint8Array(signature);
|
|
273
293
|
}
|
|
274
294
|
|
|
275
295
|
interface HICache {
|
|
276
|
-
[key: string]:
|
|
296
|
+
[key: string]: Uint8Array;
|
|
277
297
|
}
|
|
278
298
|
|
|
279
299
|
let _hiCache: HICache = {};
|
|
@@ -288,21 +308,32 @@ const hiLengthMap = {
|
|
|
288
308
|
sha1: 20
|
|
289
309
|
};
|
|
290
310
|
|
|
291
|
-
function HI(data: string, salt:
|
|
311
|
+
async function HI(data: string, salt: Uint8Array, iterations: number, cryptoMethod: CryptoMethod) {
|
|
292
312
|
// omit the work if already generated
|
|
293
|
-
const key = [data,
|
|
313
|
+
const key = [data, ByteUtils.toBase64(salt), iterations].join('_');
|
|
294
314
|
if (_hiCache[key] != null) {
|
|
295
315
|
return _hiCache[key];
|
|
296
316
|
}
|
|
297
317
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
data,
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
cryptoMethod
|
|
318
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
319
|
+
'raw',
|
|
320
|
+
new TextEncoder().encode(data),
|
|
321
|
+
{ name: 'PBKDF2' },
|
|
322
|
+
false,
|
|
323
|
+
['deriveBits']
|
|
305
324
|
);
|
|
325
|
+
const params = {
|
|
326
|
+
name: 'PBKDF2',
|
|
327
|
+
salt: salt,
|
|
328
|
+
iterations: iterations,
|
|
329
|
+
hash: { name: cryptoMethod === 'sha256' ? 'SHA-256' : 'SHA-1' }
|
|
330
|
+
};
|
|
331
|
+
const derivedBits = await crypto.subtle.deriveBits(
|
|
332
|
+
params,
|
|
333
|
+
keyMaterial,
|
|
334
|
+
hiLengthMap[cryptoMethod] * 8
|
|
335
|
+
);
|
|
336
|
+
const saltedData = new Uint8Array(derivedBits);
|
|
306
337
|
|
|
307
338
|
// cache a copy to speed up the next lookup, but prevent unbounded cache growth
|
|
308
339
|
if (_hiCacheCount >= 200) {
|
|
@@ -314,15 +345,11 @@ function HI(data: string, salt: Buffer, iterations: number, cryptoMethod: Crypto
|
|
|
314
345
|
return saltedData;
|
|
315
346
|
}
|
|
316
347
|
|
|
317
|
-
function compareDigest(lhs:
|
|
348
|
+
function compareDigest(lhs: Uint8Array, rhs: Uint8Array) {
|
|
318
349
|
if (lhs.length !== rhs.length) {
|
|
319
350
|
return false;
|
|
320
351
|
}
|
|
321
352
|
|
|
322
|
-
if (typeof crypto.timingSafeEqual === 'function') {
|
|
323
|
-
return crypto.timingSafeEqual(lhs, rhs);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
353
|
let result = 0;
|
|
327
354
|
for (let i = 0; i < lhs.length; i++) {
|
|
328
355
|
result |= lhs[i] ^ rhs[i];
|
package/src/cmap/commands.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
BSON,
|
|
3
|
+
type BSONSerializeOptions,
|
|
4
|
+
ByteUtils,
|
|
5
|
+
type Document,
|
|
6
|
+
type Long,
|
|
7
|
+
NumberUtils,
|
|
8
|
+
readInt32LE,
|
|
9
|
+
setUint32LE
|
|
10
|
+
} from '../bson';
|
|
3
11
|
import { MongoInvalidArgumentError, MongoRuntimeError } from '../error';
|
|
4
12
|
import { type ReadPreference } from '../read_preference';
|
|
5
13
|
import type { ClientSession } from '../sessions';
|
|
@@ -30,7 +38,7 @@ const QUERY_FAILURE = 2;
|
|
|
30
38
|
const SHARD_CONFIG_STALE = 4;
|
|
31
39
|
const AWAIT_CAPABLE = 8;
|
|
32
40
|
|
|
33
|
-
const encodeUTF8Into =
|
|
41
|
+
const encodeUTF8Into = ByteUtils.encodeUTF8Into;
|
|
34
42
|
|
|
35
43
|
/** @internal */
|
|
36
44
|
export type WriteProtocolMessageType = OpQueryRequest | OpMsgRequest;
|
|
@@ -182,10 +190,10 @@ export class OpQueryRequest {
|
|
|
182
190
|
if (this.batchSize !== this.numberToReturn) this.numberToReturn = this.batchSize;
|
|
183
191
|
|
|
184
192
|
// Allocate write protocol header buffer
|
|
185
|
-
const header =
|
|
193
|
+
const header = ByteUtils.allocate(
|
|
186
194
|
4 * 4 + // Header
|
|
187
195
|
4 + // Flags
|
|
188
|
-
|
|
196
|
+
ByteUtils.utf8ByteLength(this.ns) +
|
|
189
197
|
1 + // namespace
|
|
190
198
|
4 + // numberToSkip
|
|
191
199
|
4 // numberToReturn
|
|
@@ -256,7 +264,7 @@ export class OpQueryRequest {
|
|
|
256
264
|
index = index + 4;
|
|
257
265
|
|
|
258
266
|
// Write collection name
|
|
259
|
-
index = index + header
|
|
267
|
+
index = index + encodeUTF8Into(header, this.ns, index) + 1;
|
|
260
268
|
header[index - 1] = 0;
|
|
261
269
|
|
|
262
270
|
// Write header information flags numberToSkip
|
|
@@ -290,8 +298,8 @@ export interface MessageHeader {
|
|
|
290
298
|
/** @internal */
|
|
291
299
|
export class OpReply {
|
|
292
300
|
parsed: boolean;
|
|
293
|
-
raw:
|
|
294
|
-
data:
|
|
301
|
+
raw: Uint8Array;
|
|
302
|
+
data: Uint8Array;
|
|
295
303
|
opts: BSONSerializeOptions;
|
|
296
304
|
length: number;
|
|
297
305
|
requestId: number;
|
|
@@ -318,9 +326,9 @@ export class OpReply {
|
|
|
318
326
|
moreToCome = false;
|
|
319
327
|
|
|
320
328
|
constructor(
|
|
321
|
-
message:
|
|
329
|
+
message: Uint8Array,
|
|
322
330
|
msgHeader: MessageHeader,
|
|
323
|
-
msgBody:
|
|
331
|
+
msgBody: Uint8Array,
|
|
324
332
|
opts?: BSONSerializeOptions
|
|
325
333
|
) {
|
|
326
334
|
this.parsed = false;
|
|
@@ -364,10 +372,10 @@ export class OpReply {
|
|
|
364
372
|
this.index = 20;
|
|
365
373
|
|
|
366
374
|
// Read the message body
|
|
367
|
-
this.responseFlags = this.data
|
|
368
|
-
this.cursorId = new BSON.Long(this.data
|
|
369
|
-
this.startingFrom = this.data
|
|
370
|
-
this.numberReturned = this.data
|
|
375
|
+
this.responseFlags = readInt32LE(this.data, 0);
|
|
376
|
+
this.cursorId = new BSON.Long(readInt32LE(this.data, 4), readInt32LE(this.data, 8));
|
|
377
|
+
this.startingFrom = readInt32LE(this.data, 12);
|
|
378
|
+
this.numberReturned = readInt32LE(this.data, 16);
|
|
371
379
|
|
|
372
380
|
if (this.numberReturned < 0 || this.numberReturned > 2 ** 32 - 1) {
|
|
373
381
|
throw new RangeError(
|
|
@@ -433,7 +441,7 @@ export class DocumentSequence {
|
|
|
433
441
|
documents: Document[];
|
|
434
442
|
serializedDocumentsLength: number;
|
|
435
443
|
private chunks: Uint8Array[];
|
|
436
|
-
private header:
|
|
444
|
+
private header: Uint8Array;
|
|
437
445
|
|
|
438
446
|
/**
|
|
439
447
|
* Create a new document sequence for the provided field.
|
|
@@ -446,7 +454,7 @@ export class DocumentSequence {
|
|
|
446
454
|
this.serializedDocumentsLength = 0;
|
|
447
455
|
// Document sequences starts with type 1 at the first byte.
|
|
448
456
|
// Field strings must always be UTF-8.
|
|
449
|
-
const buffer =
|
|
457
|
+
const buffer = ByteUtils.allocateUnsafe(1 + 4 + this.field.length + 1);
|
|
450
458
|
buffer[0] = 1;
|
|
451
459
|
// Third part is the field name at offset 5 with trailing null byte.
|
|
452
460
|
encodeUTF8Into(buffer, `${this.field}\0`, 5);
|
|
@@ -473,7 +481,13 @@ export class DocumentSequence {
|
|
|
473
481
|
// Push the document raw bson.
|
|
474
482
|
this.chunks.push(buffer);
|
|
475
483
|
// Write the new length.
|
|
476
|
-
this.header
|
|
484
|
+
if (this.header) {
|
|
485
|
+
NumberUtils.setInt32LE(
|
|
486
|
+
this.header,
|
|
487
|
+
1,
|
|
488
|
+
4 + this.field.length + 1 + this.serializedDocumentsLength
|
|
489
|
+
);
|
|
490
|
+
}
|
|
477
491
|
return this.serializedDocumentsLength + this.header.length;
|
|
478
492
|
}
|
|
479
493
|
|
|
@@ -482,7 +496,7 @@ export class DocumentSequence {
|
|
|
482
496
|
* @returns The section bytes.
|
|
483
497
|
*/
|
|
484
498
|
toBin(): Uint8Array {
|
|
485
|
-
return
|
|
499
|
+
return ByteUtils.concat(this.chunks);
|
|
486
500
|
}
|
|
487
501
|
}
|
|
488
502
|
|
|
@@ -531,8 +545,8 @@ export class OpMsgRequest {
|
|
|
531
545
|
typeof options.exhaustAllowed === 'boolean' ? options.exhaustAllowed : false;
|
|
532
546
|
}
|
|
533
547
|
|
|
534
|
-
toBin():
|
|
535
|
-
const buffers:
|
|
548
|
+
toBin(): Uint8Array[] {
|
|
549
|
+
const buffers: Uint8Array[] = [];
|
|
536
550
|
let flags = 0;
|
|
537
551
|
|
|
538
552
|
if (this.checksumPresent) {
|
|
@@ -547,7 +561,7 @@ export class OpMsgRequest {
|
|
|
547
561
|
flags |= OPTS_EXHAUST_ALLOWED;
|
|
548
562
|
}
|
|
549
563
|
|
|
550
|
-
const header =
|
|
564
|
+
const header = ByteUtils.allocate(
|
|
551
565
|
4 * 4 + // Header
|
|
552
566
|
4 // Flags
|
|
553
567
|
);
|
|
@@ -558,11 +572,13 @@ export class OpMsgRequest {
|
|
|
558
572
|
const command = this.command;
|
|
559
573
|
totalLength += this.makeSections(buffers, command);
|
|
560
574
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
575
|
+
NumberUtils.setInt32LE(header, 0, totalLength); // messageLength
|
|
576
|
+
NumberUtils.setInt32LE(header, 4, this.requestId); // requestID
|
|
577
|
+
NumberUtils.setInt32LE(header, 8, 0); // responseTo
|
|
578
|
+
NumberUtils.setInt32LE(header, 12, OP_MSG); // opCode
|
|
579
|
+
// The OP_MSG spec calls out that flags is uint32:
|
|
580
|
+
// https://github.com/mongodb/specifications/blob/master/source/message/OP_MSG.md#op_msg-1
|
|
581
|
+
setUint32LE(header, 16, flags); // flags
|
|
566
582
|
return buffers;
|
|
567
583
|
}
|
|
568
584
|
|
|
@@ -571,7 +587,7 @@ export class OpMsgRequest {
|
|
|
571
587
|
*/
|
|
572
588
|
makeSections(buffers: Uint8Array[], document: Document): number {
|
|
573
589
|
const sequencesBuffer = this.extractDocumentSequences(document);
|
|
574
|
-
const payloadTypeBuffer =
|
|
590
|
+
const payloadTypeBuffer = ByteUtils.allocateUnsafe(1);
|
|
575
591
|
payloadTypeBuffer[0] = 0;
|
|
576
592
|
|
|
577
593
|
const documentBuffer = this.serializeBson(document);
|
|
@@ -606,11 +622,11 @@ export class OpMsgRequest {
|
|
|
606
622
|
}
|
|
607
623
|
}
|
|
608
624
|
if (chunks.length > 0) {
|
|
609
|
-
return
|
|
625
|
+
return ByteUtils.concat(chunks);
|
|
610
626
|
}
|
|
611
627
|
// If we have no document sequences we return an empty buffer for nothing to add
|
|
612
628
|
// to the payload.
|
|
613
|
-
return
|
|
629
|
+
return ByteUtils.allocate(0);
|
|
614
630
|
}
|
|
615
631
|
|
|
616
632
|
serializeBson(document: Document): Uint8Array {
|
|
@@ -630,8 +646,8 @@ export class OpMsgRequest {
|
|
|
630
646
|
/** @internal */
|
|
631
647
|
export class OpMsgResponse {
|
|
632
648
|
parsed: boolean;
|
|
633
|
-
raw:
|
|
634
|
-
data:
|
|
649
|
+
raw: Uint8Array;
|
|
650
|
+
data: Uint8Array;
|
|
635
651
|
opts: BSONSerializeOptions;
|
|
636
652
|
length: number;
|
|
637
653
|
requestId: number;
|
|
@@ -652,9 +668,9 @@ export class OpMsgResponse {
|
|
|
652
668
|
sections: Uint8Array[] = [];
|
|
653
669
|
|
|
654
670
|
constructor(
|
|
655
|
-
message:
|
|
671
|
+
message: Uint8Array,
|
|
656
672
|
msgHeader: MessageHeader,
|
|
657
|
-
msgBody:
|
|
673
|
+
msgBody: Uint8Array,
|
|
658
674
|
opts?: BSONSerializeOptions
|
|
659
675
|
) {
|
|
660
676
|
this.parsed = false;
|
|
@@ -676,7 +692,7 @@ export class OpMsgResponse {
|
|
|
676
692
|
this.fromCompressed = msgHeader.fromCompressed;
|
|
677
693
|
|
|
678
694
|
// Read response flags
|
|
679
|
-
this.responseFlags =
|
|
695
|
+
this.responseFlags = readInt32LE(msgBody, 0);
|
|
680
696
|
this.checksumPresent = (this.responseFlags & OPTS_CHECKSUM_PRESENT) !== 0;
|
|
681
697
|
this.moreToCome = (this.responseFlags & OPTS_MORE_TO_COME) !== 0;
|
|
682
698
|
this.exhaustAllowed = (this.responseFlags & OPTS_EXHAUST_ALLOWED) !== 0;
|
|
@@ -700,9 +716,12 @@ export class OpMsgResponse {
|
|
|
700
716
|
this.index = 4;
|
|
701
717
|
|
|
702
718
|
while (this.index < this.data.length) {
|
|
703
|
-
const payloadType = this.data
|
|
719
|
+
const payloadType = this.data[this.index++];
|
|
704
720
|
if (payloadType === 0) {
|
|
705
|
-
|
|
721
|
+
// BSON spec specifies that this is a 32-bit signed integer: https://bsonspec.org/spec.html#:~:text=%3A%3A%3D-,int32,-e_list%20unsigned_byte(0
|
|
722
|
+
// While allowing negative sizes seems odd, in practice we never expect a negative size. Also, the server's 16mb limit for BSON documents leaves plenty
|
|
723
|
+
// of room in an int32 to store a document of the max BSON size that the server supports
|
|
724
|
+
const bsonSize = readInt32LE(this.data, this.index);
|
|
706
725
|
const bin = this.data.subarray(this.index, this.index + bsonSize);
|
|
707
726
|
|
|
708
727
|
this.sections.push(bin);
|
|
@@ -758,31 +777,31 @@ export class OpCompressedRequest {
|
|
|
758
777
|
return !uncompressibleCommands.has(commandName);
|
|
759
778
|
}
|
|
760
779
|
|
|
761
|
-
async toBin(): Promise<
|
|
762
|
-
const concatenatedOriginalCommandBuffer =
|
|
780
|
+
async toBin(): Promise<Uint8Array[]> {
|
|
781
|
+
const concatenatedOriginalCommandBuffer = ByteUtils.concat(this.command.toBin());
|
|
763
782
|
// otherwise, compress the message
|
|
764
783
|
const messageToBeCompressed = concatenatedOriginalCommandBuffer.slice(MESSAGE_HEADER_SIZE);
|
|
765
784
|
|
|
766
785
|
// Extract information needed for OP_COMPRESSED from the uncompressed message
|
|
767
|
-
const originalCommandOpCode =
|
|
786
|
+
const originalCommandOpCode = readInt32LE(concatenatedOriginalCommandBuffer, 12);
|
|
768
787
|
|
|
769
788
|
// Compress the message body
|
|
770
789
|
const compressedMessage = await compress(this.options, messageToBeCompressed);
|
|
771
790
|
// Create the msgHeader of OP_COMPRESSED
|
|
772
|
-
const msgHeader =
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
0
|
|
791
|
+
const msgHeader = ByteUtils.allocate(MESSAGE_HEADER_SIZE);
|
|
792
|
+
NumberUtils.setInt32LE(
|
|
793
|
+
msgHeader,
|
|
794
|
+
0,
|
|
795
|
+
MESSAGE_HEADER_SIZE + COMPRESSION_DETAILS_SIZE + compressedMessage.length
|
|
776
796
|
); // messageLength
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
797
|
+
NumberUtils.setInt32LE(msgHeader, 4, this.command.requestId); // requestID
|
|
798
|
+
NumberUtils.setInt32LE(msgHeader, 8, 0); // responseTo (zero)
|
|
799
|
+
NumberUtils.setInt32LE(msgHeader, 12, OP_COMPRESSED); // opCode
|
|
781
800
|
// Create the compression details of OP_COMPRESSED
|
|
782
|
-
const compressionDetails =
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
compressionDetails
|
|
801
|
+
const compressionDetails = ByteUtils.allocate(COMPRESSION_DETAILS_SIZE);
|
|
802
|
+
NumberUtils.setInt32LE(compressionDetails, 0, originalCommandOpCode); // originalOpcode
|
|
803
|
+
NumberUtils.setInt32LE(compressionDetails, 4, messageToBeCompressed.length); // Size of the uncompressed compressedMessage, excluding the MsgHeader
|
|
804
|
+
compressionDetails[8] = Compressor[this.options.agreedCompressor]; // compressorID
|
|
786
805
|
return [msgHeader, compressionDetails, compressedMessage];
|
|
787
806
|
}
|
|
788
807
|
}
|
package/src/cmap/connect.ts
CHANGED
|
@@ -224,6 +224,7 @@ export interface HandshakeDocument extends Document {
|
|
|
224
224
|
compression: string[];
|
|
225
225
|
saslSupportedMechs?: string;
|
|
226
226
|
loadBalanced?: boolean;
|
|
227
|
+
backpressure: true;
|
|
227
228
|
}
|
|
228
229
|
|
|
229
230
|
/**
|
|
@@ -241,6 +242,7 @@ export async function prepareHandshakeDocument(
|
|
|
241
242
|
|
|
242
243
|
const handshakeDoc: HandshakeDocument = {
|
|
243
244
|
[serverApi?.version || options.loadBalanced === true ? 'hello' : LEGACY_HELLO_COMMAND]: 1,
|
|
245
|
+
backpressure: true,
|
|
244
246
|
helloOk: true,
|
|
245
247
|
client: clientMetadata,
|
|
246
248
|
compression: compressors
|
|
@@ -279,6 +281,15 @@ export async function prepareHandshakeDocument(
|
|
|
279
281
|
return handshakeDoc;
|
|
280
282
|
}
|
|
281
283
|
|
|
284
|
+
/**
|
|
285
|
+
* @internal
|
|
286
|
+
* Default TCP keepAlive initial delay in milliseconds.
|
|
287
|
+
* Set to half the Azure load balancer idle timeout (240s) to ensure
|
|
288
|
+
* probes fire well before cloud LBs (Azure, AWS PrivateLink/NLB)
|
|
289
|
+
* drop idle connections.
|
|
290
|
+
*/
|
|
291
|
+
export const DEFAULT_KEEP_ALIVE_INITIAL_DELAY_MS = 120_000;
|
|
292
|
+
|
|
282
293
|
/** @public */
|
|
283
294
|
export const LEGAL_TLS_SOCKET_OPTIONS = [
|
|
284
295
|
'allowPartialTrustChain',
|
|
@@ -322,7 +333,7 @@ function parseConnectOptions(options: ConnectionOptions): SocketConnectOpts {
|
|
|
322
333
|
(result as Document)[name] = options[name];
|
|
323
334
|
}
|
|
324
335
|
}
|
|
325
|
-
result.keepAliveInitialDelay ??=
|
|
336
|
+
result.keepAliveInitialDelay ??= DEFAULT_KEEP_ALIVE_INITIAL_DELAY_MS;
|
|
326
337
|
result.keepAlive = true;
|
|
327
338
|
result.noDelay = options.noDelay ?? true;
|
|
328
339
|
|
|
@@ -368,6 +379,9 @@ export async function makeSocket(options: MakeConnectionOptions): Promise<Stream
|
|
|
368
379
|
const useTLS = options.tls ?? false;
|
|
369
380
|
const connectTimeoutMS = options.connectTimeoutMS ?? 30000;
|
|
370
381
|
const existingSocket = options.existingSocket;
|
|
382
|
+
const keepAliveInitialDelay =
|
|
383
|
+
options.keepAliveInitialDelay ?? DEFAULT_KEEP_ALIVE_INITIAL_DELAY_MS;
|
|
384
|
+
const noDelay = options.noDelay ?? true;
|
|
371
385
|
|
|
372
386
|
let socket: Stream;
|
|
373
387
|
|
|
@@ -394,6 +408,12 @@ export async function makeSocket(options: MakeConnectionOptions): Promise<Stream
|
|
|
394
408
|
socket = net.createConnection(parseConnectOptions(options));
|
|
395
409
|
}
|
|
396
410
|
|
|
411
|
+
// Explicit setKeepAlive/setNoDelay are required because tls.connect() silently
|
|
412
|
+
// ignores these constructor options due to a Node.js bug.
|
|
413
|
+
// See: https://github.com/nodejs/node/issues/62003
|
|
414
|
+
// TODO(NODE-7474): remove this fix once the underlying Node.js issue is resolved.
|
|
415
|
+
socket.setKeepAlive(true, keepAliveInitialDelay);
|
|
416
|
+
socket.setNoDelay(noDelay);
|
|
397
417
|
socket.setTimeout(connectTimeoutMS);
|
|
398
418
|
|
|
399
419
|
let cancellationHandler: ((err: Error) => void) | null = null;
|