diff --git a/package-lock.json b/package-lock.json index e601bf5af99..c7db725d21d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,7 @@ "license": "Apache-2.0", "dependencies": { "bson": "^5.4.0", - "mongodb-connection-string-url": "^2.6.0", - "socks": "^2.7.1" + "mongodb-connection-string-url": "^2.6.0" }, "devDependencies": { "@iarna/toml": "^2.2.5", @@ -55,6 +54,7 @@ "sinon": "^15.0.4", "sinon-chai": "^3.7.0", "snappy": "^7.2.2", + "socks": "^2.7.1", "source-map-support": "^0.5.21", "ts-node": "^10.9.1", "tsd": "^0.28.1", @@ -75,7 +75,8 @@ "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0-alpha.0 <7", - "snappy": "^7.2.2" + "snappy": "^7.2.2", + "socks": "^2.7.1" }, "peerDependenciesMeta": { "@aws-sdk/credential-providers": { @@ -95,6 +96,9 @@ }, "snappy": { "optional": true + }, + "socks": { + "optional": true } } }, @@ -5593,7 +5597,8 @@ "node_modules/ip": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", + "dev": true }, "node_modules/ipaddr.js": { "version": "1.9.1", @@ -8099,6 +8104,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" @@ -8136,6 +8142,7 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "dev": true, "dependencies": { "ip": "^2.0.0", "smart-buffer": "^4.2.0" diff --git a/package.json b/package.json index 12003ba28a2..ac7ffaa6d93 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,7 @@ }, "dependencies": { "bson": "^5.4.0", - "mongodb-connection-string-url": "^2.6.0", - "socks": "^2.7.1" + "mongodb-connection-string-url": "^2.6.0" }, "optionalDependencies": { "saslprep": "^1.0.3" @@ -38,7 +37,8 @@ "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0-alpha.0 <7", - "snappy": "^7.2.2" + "snappy": "^7.2.2", + "socks": "^2.7.1" }, "peerDependenciesMeta": { "@aws-sdk/credential-providers": { @@ -58,6 +58,9 @@ }, "gcp-metadata": { "optional": true + }, + "socks": { + "optional": true } }, "devDependencies": { @@ -102,6 +105,7 @@ "sinon": "^15.0.4", "sinon-chai": "^3.7.0", "snappy": "^7.2.2", + "socks": "^2.7.1", "source-map-support": "^0.5.21", "ts-node": "^10.9.1", "tsd": "^0.28.1", diff --git a/src/client-side-encryption/stateMachine.js b/src/client-side-encryption/stateMachine.js index c08fb259b2d..931437ee472 100644 --- a/src/client-side-encryption/stateMachine.js +++ b/src/client-side-encryption/stateMachine.js @@ -4,12 +4,25 @@ import * as tls from 'tls'; import * as net from 'net'; import * as fs from 'fs'; import { once } from 'events'; -import { SocksClient } from 'socks'; import { MongoNetworkTimeoutError } from '../error'; import { debug, databaseNamespace, collectionNamespace } from './common'; import { MongoCryptError } from './errors'; import { BufferPool } from './buffer_pool'; import { serialize, deserialize } from '../bson'; +import { getSocks } from '../deps'; + +/** @type {import('../deps').SocksLib | null} */ +let socks = null; +function loadSocks() { + if (socks == null) { + const socksImport = getSocks(); + if ('kModuleError' in socksImport) { + throw socksImport.kModuleError; + } + socks = socksImport; + } + return socks; +} // libmongocrypt states const MONGOCRYPT_CTX_ERROR = 0; @@ -289,8 +302,9 @@ class StateMachine { rawSocket.on('error', onerror); try { await once(rawSocket, 'connect'); + socks ??= loadSocks(); options.socket = ( - await SocksClient.createConnection({ + await socks.SocksClient.createConnection({ existing_socket: rawSocket, command: 'connect', destination: { host: options.host, port: options.port }, diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index dd77c26685d..0ea49e939cd 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -1,11 +1,11 @@ import type { Socket, SocketConnectOpts } from 'net'; import * as net from 'net'; -import { SocksClient } from 'socks'; import type { ConnectionOptions as TLSConnectionOpts, TLSSocket } from 'tls'; import * as tls from 'tls'; import type { Document } from '../bson'; import { LEGACY_HELLO_COMMAND } from '../constants'; +import { getSocks, type SocksLib } from '../deps'; import { MongoCompatibilityError, MongoError, @@ -419,6 +419,18 @@ function makeConnection(options: MakeConnectionOptions, _callback: Callback) { const hostAddress = HostAddress.fromHostPort( options.proxyHost ?? '', // proxyHost is guaranteed to set here @@ -434,7 +446,7 @@ function makeSocks5Connection(options: MakeConnectionOptions, callback: Callback proxyHost: undefined }, (err, rawSocket) => { - if (err) { + if (err || !rawSocket) { return callback(err); } @@ -445,8 +457,14 @@ function makeSocks5Connection(options: MakeConnectionOptions, callback: Callback ); } + try { + socks ??= loadSocks(); + } catch (error) { + return callback(error); + } + // Then, establish the Socks5 proxy connection: - SocksClient.createConnection({ + socks.SocksClient.createConnection({ existing_socket: rawSocket, timeout: options.connectTimeoutMS, command: 'connect', diff --git a/src/deps.ts b/src/deps.ts index f3c8965685b..319dbe1fc0c 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-var-requires */ import type { Document } from './bson'; +import { type Stream } from './cmap/connect'; import type { ProxyOptions } from './cmap/connection'; import { MongoMissingDependencyError } from './error'; import type { MongoClient } from './mongo_client'; @@ -157,6 +158,40 @@ export function getSnappy(): SnappyLib | { kModuleError: MongoMissingDependencyE } } +export type SocksLib = { + SocksClient: { + createConnection(options: { + command: 'connect'; + destination: { host: string; port: number }; + proxy: { + /** host and port are ignored because we pass existing_socket */ + host: 'iLoveJavaScript'; + port: 0; + type: 5; + userId?: string; + password?: string; + }; + timeout?: number; + /** We always create our own socket, and pass it to this API for proxy negotiation */ + existing_socket: Stream; + }): Promise<{ socket: Stream }>; + }; +}; + +export function getSocks(): SocksLib | { kModuleError: MongoMissingDependencyError } { + try { + // Ensure you always wrap an optional require in the try block NODE-3199 + const value = require('socks'); + return value; + } catch (cause) { + const kModuleError = new MongoMissingDependencyError( + 'Optional module `socks` not found. Please install it to connections over a SOCKS5 proxy', + { cause } + ); + return { kModuleError }; + } +} + export let saslprep: typeof import('saslprep') | { kModuleError: MongoMissingDependencyError } = makeErrorModule( new MongoMissingDependencyError( diff --git a/test/action/dependency.test.ts b/test/action/dependency.test.ts index c04a3f8cbc7..93fd400f234 100644 --- a/test/action/dependency.test.ts +++ b/test/action/dependency.test.ts @@ -7,14 +7,15 @@ import { expect } from 'chai'; import { dependencies, peerDependencies, peerDependenciesMeta } from '../../package.json'; import { itInNodeProcess } from '../tools/utils'; -const EXPECTED_DEPENDENCIES = ['bson', 'mongodb-connection-string-url', 'socks']; +const EXPECTED_DEPENDENCIES = ['bson', 'mongodb-connection-string-url']; const EXPECTED_PEER_DEPENDENCIES = [ '@aws-sdk/credential-providers', '@mongodb-js/zstd', 'kerberos', 'snappy', 'mongodb-client-encryption', - 'gcp-metadata' + 'gcp-metadata', + 'socks' ]; describe('package.json', function () { @@ -119,10 +120,7 @@ describe('package.json', function () { 'mongodb-connection-string-url', 'whatwg-url', 'webidl-conversions', - 'tr46', - 'socks', - 'ip', - 'smart-buffer' + 'tr46' ]; describe('mongodb imports', () => {