Skip to content

Commit 847965c

Browse files
authored
fix(shell-api,shell-bson): increase runtime independence (#2544)
Make the `shell-bson` and `shell-api` packages more runtime-independent (as they are supposed to be) by using `util` and `crypto` as progressive enhancement enablers rather than required packages. This specifically makes `shell-bson` fully usable in a bare-bones JS environment.
1 parent 1024a09 commit 847965c

File tree

9 files changed

+128
-55
lines changed

9 files changed

+128
-55
lines changed

packages/service-provider-core/src/admin.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ export default interface Admin {
6666
*/
6767
bsonLibrary: BSON;
6868

69+
/**
70+
* Compute a hex MD5 hash from a string. Used for legacy auth mechanisms such as
71+
* SCRAM-SHA-1.
72+
*/
73+
computeLegacyHexMD5?(str: string): Promise<string>;
74+
6975
/**
7076
* list databases.
7177
*

packages/service-provider-node-driver/src/node-driver-service-provider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ const bsonlib = () => {
109109
Decimal128,
110110
BSONSymbol,
111111
BSONRegExp,
112+
UUID,
112113
BSON,
113114
} = driver;
114115
return {
@@ -126,6 +127,7 @@ const bsonlib = () => {
126127
BSONSymbol,
127128
calculateObjectSize: BSON.calculateObjectSize,
128129
EJSON: BSON.EJSON,
130+
UUID,
129131
BSONRegExp,
130132
};
131133
};

packages/shell-api/src/database.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -581,10 +581,11 @@ export class Database<
581581
if (writeConcern) {
582582
command.writeConcern = writeConcern;
583583
}
584-
const digestPwd = processDigestPassword(
584+
const digestPwd = await processDigestPassword(
585585
user.user,
586586
user.passwordDigestor,
587-
command
587+
command,
588+
this._instanceState.currentServiceProvider
588589
);
589590
const orderedCmd = {
590591
createUser: command.createUser,
@@ -627,10 +628,11 @@ export class Database<
627628
if (writeConcern) {
628629
command.writeConcern = writeConcern;
629630
}
630-
const digestPwd = processDigestPassword(
631+
const digestPwd = await processDigestPassword(
631632
username,
632633
userDoc.passwordDigestor,
633-
command
634+
command,
635+
this._instanceState.currentServiceProvider
634636
);
635637
const orderedCmd = {
636638
updateUser: command.updateUser,

packages/shell-api/src/helpers.ts

Lines changed: 72 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ import type {
99
DeleteOptions,
1010
MapReduceOptions,
1111
ExplainOptions,
12+
ServiceProvider,
1213
} from '@mongosh/service-provider-core';
1314
import {
1415
CommonErrors,
1516
MongoshInvalidInputError,
1617
MongoshUnimplementedError,
1718
} from '@mongosh/errors';
18-
import crypto from 'crypto';
1919
import type { Database } from './database';
2020
import type { Collection } from './collection';
2121
import type { CursorIterationResult } from './result';
@@ -27,8 +27,12 @@ import { shellApiType } from './enums';
2727
import type { AbstractFiniteCursor } from './abstract-cursor';
2828
import type ChangeStreamCursor from './change-stream-cursor';
2929
import type { BSON, ShellBson } from '@mongosh/shell-bson';
30-
import { inspect } from 'util';
3130
import type { MQLPipeline, MQLQuery } from './mql-types';
31+
import type { InspectOptions } from 'util';
32+
33+
let coreUtilInspect: ((obj: any, options: InspectOptions) => string) & {
34+
defaultOptions: InspectOptions;
35+
};
3236

3337
/**
3438
* Helper method to adapt aggregation pipeline options.
@@ -173,6 +177,35 @@ export function adaptOptions(
173177
}, additions);
174178
}
175179

180+
async function computeLegacyHexMD5(
181+
sp: ServiceProvider,
182+
str: string
183+
): Promise<string> {
184+
const digested = await sp.computeLegacyHexMD5?.(str);
185+
if (digested) return digested;
186+
187+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
188+
let crypto: typeof import('crypto');
189+
try {
190+
// Try to dynamically import crypto so that we don't break plain-JS-runtime builds.
191+
// The Web Crypto API does not provide MD5, which is reasonable for a modern API
192+
// but means that we cannot use it as a fallback.
193+
crypto = require('crypto');
194+
} catch {
195+
throw new MongoshUnimplementedError(
196+
'Legacy password digest algorithms like SCRAM-SHA-1 are not supported by this instance of mongosh',
197+
CommonErrors.Deprecated
198+
);
199+
}
200+
// NOTE: this code has raised a code scanning alert about the "use of a broken or weak cryptographic algorithm":
201+
// we inherited this code from `mongo`, and we cannot replace MD5 with a different algorithm, since MD5 is part of the SCRAM-SHA-1 protocol,
202+
// and the purpose of `passwordDigestor=client` is to improve the security of SCRAM-SHA-1, allowing the creation of new users
203+
// without the need to communicate their password to the server.
204+
const hash = crypto.createHash('md5');
205+
hash.update(str);
206+
return hash.digest('hex');
207+
}
208+
176209
/**
177210
* Optionally digest password if passwordDigestor field set to 'client'. If it's false,
178211
* then hash the password.
@@ -181,11 +214,12 @@ export function adaptOptions(
181214
* @param passwordDigestor
182215
* @param {Object} command
183216
*/
184-
export function processDigestPassword(
217+
export async function processDigestPassword(
185218
username: string,
186219
passwordDigestor: 'server' | 'client',
187-
command: { pwd: string }
188-
): { digestPassword?: boolean; pwd?: string } {
220+
command: { pwd: string },
221+
sp: ServiceProvider
222+
): Promise<{ digestPassword?: boolean; pwd?: string }> {
189223
if (passwordDigestor === undefined) {
190224
return {};
191225
}
@@ -202,14 +236,10 @@ export function processDigestPassword(
202236
CommonErrors.InvalidArgument
203237
);
204238
}
205-
// NOTE: this code has raised a code scanning alert about the "use of a broken or weak cryptographic algorithm":
206-
// we inherited this code from `mongo`, and we cannot replace MD5 with a different algorithm, since MD5 is part of the SCRAM-SHA-1 protocol,
207-
// and the purpose of `passwordDigestor=client` is to improve the security of SCRAM-SHA-1, allowing the creation of new users
208-
// without the need to communicate their password to the server.
209-
const hash = crypto.createHash('md5');
210-
hash.update(`${username}:mongo:${command.pwd}`);
211-
const digested = hash.digest('hex');
212-
return { digestPassword: false, pwd: digested };
239+
return {
240+
digestPassword: false,
241+
pwd: await computeLegacyHexMD5(sp, `${username}:mongo:${command.pwd}`),
242+
};
213243
}
214244
return { digestPassword: true };
215245
}
@@ -630,24 +660,35 @@ export async function getPrintableShardStatus(
630660
'on shard': chunk.shard,
631661
'last modified': chunk.lastmod,
632662
} as any;
633-
// Displaying a full, multi-line output for each chunk is a bit verbose,
634-
// even if there are only a few chunks. Where supported, we use a custom
635-
// inspection function to inspect a copy of this object with an unlimited
636-
// line break length (i.e. all objects on a single line).
637-
Object.defineProperty(
638-
c,
639-
Symbol.for('nodejs.util.inspect.custom'),
640-
{
641-
value: function (depth: number, options: any): string {
642-
return inspect(
643-
{ ...this },
644-
{ ...options, breakLength: Infinity }
645-
);
646-
},
647-
writable: true,
648-
configurable: true,
649-
}
650-
);
663+
try {
664+
// eslint-disable-next-line @typescript-eslint/no-var-requires
665+
coreUtilInspect ??= require('util').inspect;
666+
} catch {
667+
// No util.inspect available, e.g. in browser builds.
668+
}
669+
if (coreUtilInspect) {
670+
// Displaying a full, multi-line output for each chunk is a bit verbose,
671+
// even if there are only a few chunks. Where supported, we use a custom
672+
// inspection function to inspect a copy of this object with an unlimited
673+
// line break length (i.e. all objects on a single line).
674+
Object.defineProperty(
675+
c,
676+
Symbol.for('nodejs.util.inspect.custom'),
677+
{
678+
value: function (
679+
depth: number,
680+
options: InspectOptions
681+
): string {
682+
return coreUtilInspect(
683+
{ ...this },
684+
{ ...options, breakLength: Infinity }
685+
);
686+
},
687+
writable: true,
688+
configurable: true,
689+
}
690+
);
691+
}
651692
if (chunk.jumbo) c.jumbo = 'yes';
652693
chunksRes.push(c);
653694
} else if (chunksRes.length === 20 && !verbose) {

packages/shell-api/src/runtime-independence.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe('Runtime independence', function () {
2020
// for other environments, but which we should still ideally remove in the
2121
// long run (and definitely not add anything here).
2222
// Guaranteed bonusly for anyone who removes a package from this list!
23-
const allowedNodeBuiltins = ['crypto', 'util', 'events', 'path'];
23+
const allowedNodeBuiltins = ['events', 'path'];
2424
// Our TextDecoder/TextEncoder polyfills require this, unfortunately.
2525
context.Buffer = Buffer;
2626

packages/shell-api/src/shell-api.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import {
3131
MongoshInternalError,
3232
} from '@mongosh/errors';
3333
import { DBQuery } from './dbquery';
34-
import { promisify } from 'util';
3534
import type { ClientSideFieldLevelEncryptionOptions } from './field-level-encryption';
3635
import { dirname } from 'path';
3736
import { ShellUserConfig } from '@mongosh/types';
@@ -422,7 +421,7 @@ export default class ShellApi extends ShellApiClass {
422421

423422
@returnsPromise
424423
async sleep(ms: number): Promise<void> {
425-
return await promisify(setTimeout)(ms);
424+
return await new Promise<void>((resolve) => setTimeout(resolve, ms));
426425
}
427426

428427
private async _print(

packages/shell-bson/src/bson-export.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
calculateObjectSize,
1414
Double,
1515
EJSON,
16+
UUID,
1617
BSONRegExp,
1718
} from 'bson';
1819
export type {
@@ -29,6 +30,7 @@ export type {
2930
Binary,
3031
Double,
3132
EJSON,
33+
UUID,
3234
BSONRegExp,
3335
calculateObjectSize,
3436
};
@@ -45,6 +47,7 @@ export type BSON = {
4547
Binary: typeof Binary;
4648
Double: typeof Double;
4749
EJSON: typeof EJSON;
50+
UUID: typeof UUID;
4851
BSONRegExp: typeof BSONRegExp;
4952
BSONSymbol: typeof BSONSymbol;
5053
calculateObjectSize: typeof calculateObjectSize;

packages/shell-bson/src/printable-bson.ts

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,40 @@
11
import type { BSON } from './';
2-
import type { InspectOptionsStylized, CustomInspectFunction } from 'util';
3-
import { inspect as utilInspect } from 'util';
2+
import type {
3+
InspectOptionsStylized,
4+
CustomInspectFunction,
5+
InspectOptions,
6+
} from 'util';
47
const inspectCustom = Symbol.for('nodejs.util.inspect.custom');
58
type BSONClassKey = BSON[Exclude<
69
keyof BSON,
710
'EJSON' | 'calculateObjectSize'
811
>]['prototype']['_bsontype'];
912

13+
let coreUtilInspect: ((obj: any, options: InspectOptions) => string) & {
14+
defaultOptions: InspectOptions;
15+
};
16+
function inspectTypedArray(
17+
obj: Iterable<number>,
18+
options: InspectOptions
19+
): string {
20+
try {
21+
coreUtilInspect ??= require('util').inspect;
22+
return coreUtilInspect(obj, {
23+
...options,
24+
// These arrays can be very large, so would prefer to use the default options instead.
25+
maxArrayLength: coreUtilInspect.defaultOptions.maxArrayLength,
26+
});
27+
} catch {
28+
const arr = Array.from(obj);
29+
if (arr.length > 100) {
30+
return `[${arr.slice(0, 100).join(', ')}, ... ${
31+
arr.length - 100
32+
} more items]`;
33+
}
34+
return `[${arr.join(', ')}]`;
35+
}
36+
}
37+
1038
// Turn e.g. 'new Double(...)' into 'Double(...)' but preserve possible leading whitespace
1139
function removeNewFromInspectResult(str: string): string {
1240
return str.replace(/^(\s*)(new )/, '$1');
@@ -43,30 +71,24 @@ const makeBinaryVectorInspect = (bsonLibrary: BSON): CustomInspectFunction => {
4371
switch (this.buffer[0]) {
4472
case bsonLibrary.Binary.VECTOR_TYPE.Int8:
4573
return `Binary.fromInt8Array(new Int8Array(${removeTypedArrayPrefixFromInspectResult(
46-
utilInspect(this.toInt8Array(), {
74+
inspectTypedArray(this.toInt8Array(), {
4775
depth,
4876
...options,
49-
// These arrays can be very large, so would prefer to use the default options instead.
50-
maxArrayLength: utilInspect.defaultOptions.maxArrayLength,
5177
})
5278
)}))`;
5379
case bsonLibrary.Binary.VECTOR_TYPE.Float32:
5480
return `Binary.fromFloat32Array(new Float32Array(${removeTypedArrayPrefixFromInspectResult(
55-
utilInspect(this.toFloat32Array(), {
81+
inspectTypedArray(this.toFloat32Array(), {
5682
depth,
5783
...options,
58-
// These arrays can be very large, so would prefer to use the default options instead.
59-
maxArrayLength: utilInspect.defaultOptions.maxArrayLength,
6084
})
6185
)}))`;
6286
case bsonLibrary.Binary.VECTOR_TYPE.PackedBit: {
6387
const paddingInfo = this.buffer[1] === 0 ? '' : `, ${this.buffer[1]}`;
6488
return `Binary.fromPackedBits(new Uint8Array(${removeTypedArrayPrefixFromInspectResult(
65-
utilInspect(this.toPackedBits(), {
89+
inspectTypedArray(this.toPackedBits(), {
6690
depth,
6791
...options,
68-
// These arrays can be very large, so would prefer to use the default options instead.
69-
maxArrayLength: utilInspect.defaultOptions.maxArrayLength,
7092
})
7193
)})${paddingInfo})`;
7294
}

packages/shell-bson/src/shell-bson.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
assignAll,
1111
pickWithExactKeyMatch,
1212
} from './helpers';
13-
import { randomBytes } from 'crypto';
1413

1514
type LongWithoutAccidentallyExposedMethods = Omit<
1615
typeof Long,
@@ -327,11 +326,10 @@ export function constructShellBson<
327326
UUID: assignAll(
328327
function UUID(hexstr?: string): BinaryType {
329328
if (hexstr === undefined) {
330-
// Generate a version 4, variant 1 UUID, like the old shell did.
331-
const uuid = randomBytes(16);
332-
uuid[6] = (uuid[6] & 0x0f) | 0x40;
333-
uuid[8] = (uuid[8] & 0x3f) | 0x80;
334-
hexstr = uuid.toString('hex');
329+
// TODO(MONGOSH-2710): Actually use UUID instances from `bson`
330+
// (but then also be consistent about that when we e.g. receive
331+
// them from the server).
332+
hexstr = new bson.UUID().toString();
335333
}
336334
assertArgsDefinedType([hexstr], ['string'], 'UUID');
337335
// Strip any dashes, as they occur in the standard UUID formatting

0 commit comments

Comments
 (0)