connect.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.LEGAL_TCP_SOCKET_OPTIONS = exports.LEGAL_TLS_SOCKET_OPTIONS = void 0;
  4. exports.connect = connect;
  5. exports.makeConnection = makeConnection;
  6. exports.performInitialHandshake = performInitialHandshake;
  7. exports.prepareHandshakeDocument = prepareHandshakeDocument;
  8. exports.makeSocket = makeSocket;
  9. const net = require("net");
  10. const tls = require("tls");
  11. const constants_1 = require("../constants");
  12. const deps_1 = require("../deps");
  13. const error_1 = require("../error");
  14. const utils_1 = require("../utils");
  15. const auth_provider_1 = require("./auth/auth_provider");
  16. const providers_1 = require("./auth/providers");
  17. const connection_1 = require("./connection");
  18. const constants_2 = require("./wire_protocol/constants");
  19. async function connect(options) {
  20. let connection = null;
  21. try {
  22. const socket = await makeSocket(options);
  23. connection = makeConnection(options, socket);
  24. await performInitialHandshake(connection, options);
  25. return connection;
  26. }
  27. catch (error) {
  28. connection?.destroy();
  29. throw error;
  30. }
  31. }
  32. function makeConnection(options, socket) {
  33. let ConnectionType = options.connectionType ?? connection_1.Connection;
  34. if (options.autoEncrypter) {
  35. ConnectionType = connection_1.CryptoConnection;
  36. }
  37. return new ConnectionType(socket, options);
  38. }
  39. function checkSupportedServer(hello, options) {
  40. const maxWireVersion = Number(hello.maxWireVersion);
  41. const minWireVersion = Number(hello.minWireVersion);
  42. const serverVersionHighEnough = !Number.isNaN(maxWireVersion) && maxWireVersion >= constants_2.MIN_SUPPORTED_WIRE_VERSION;
  43. const serverVersionLowEnough = !Number.isNaN(minWireVersion) && minWireVersion <= constants_2.MAX_SUPPORTED_WIRE_VERSION;
  44. if (serverVersionHighEnough) {
  45. if (serverVersionLowEnough) {
  46. return null;
  47. }
  48. const message = `Server at ${options.hostAddress} reports minimum wire version ${JSON.stringify(hello.minWireVersion)}, but this version of the Node.js Driver requires at most ${constants_2.MAX_SUPPORTED_WIRE_VERSION} (MongoDB ${constants_2.MAX_SUPPORTED_SERVER_VERSION})`;
  49. return new error_1.MongoCompatibilityError(message);
  50. }
  51. const message = `Server at ${options.hostAddress} reports maximum wire version ${JSON.stringify(hello.maxWireVersion) ?? 0}, but this version of the Node.js Driver requires at least ${constants_2.MIN_SUPPORTED_WIRE_VERSION} (MongoDB ${constants_2.MIN_SUPPORTED_SERVER_VERSION})`;
  52. return new error_1.MongoCompatibilityError(message);
  53. }
  54. async function performInitialHandshake(conn, options) {
  55. const credentials = options.credentials;
  56. if (credentials) {
  57. if (!(credentials.mechanism === providers_1.AuthMechanism.MONGODB_DEFAULT) &&
  58. !options.authProviders.getOrCreateProvider(credentials.mechanism, credentials.mechanismProperties)) {
  59. throw new error_1.MongoInvalidArgumentError(`AuthMechanism '${credentials.mechanism}' not supported`);
  60. }
  61. }
  62. const authContext = new auth_provider_1.AuthContext(conn, credentials, options);
  63. conn.authContext = authContext;
  64. const handshakeDoc = await prepareHandshakeDocument(authContext);
  65. // @ts-expect-error: TODO(NODE-5141): The options need to be filtered properly, Connection options differ from Command options
  66. const handshakeOptions = { ...options, raw: false };
  67. if (typeof options.connectTimeoutMS === 'number') {
  68. // The handshake technically is a monitoring check, so its socket timeout should be connectTimeoutMS
  69. handshakeOptions.socketTimeoutMS = options.connectTimeoutMS;
  70. }
  71. const start = new Date().getTime();
  72. const response = await executeHandshake(handshakeDoc, handshakeOptions);
  73. if (!('isWritablePrimary' in response)) {
  74. // Provide hello-style response document.
  75. response.isWritablePrimary = response[constants_1.LEGACY_HELLO_COMMAND];
  76. }
  77. if (response.helloOk) {
  78. conn.helloOk = true;
  79. }
  80. const supportedServerErr = checkSupportedServer(response, options);
  81. if (supportedServerErr) {
  82. throw supportedServerErr;
  83. }
  84. if (options.loadBalanced) {
  85. if (!response.serviceId) {
  86. throw new error_1.MongoCompatibilityError('Driver attempted to initialize in load balancing mode, ' +
  87. 'but the server does not support this mode.');
  88. }
  89. }
  90. // NOTE: This is metadata attached to the connection while porting away from
  91. // handshake being done in the `Server` class. Likely, it should be
  92. // relocated, or at very least restructured.
  93. conn.hello = response;
  94. conn.lastHelloMS = new Date().getTime() - start;
  95. if (!response.arbiterOnly && credentials) {
  96. // store the response on auth context
  97. authContext.response = response;
  98. const resolvedCredentials = credentials.resolveAuthMechanism(response);
  99. const provider = options.authProviders.getOrCreateProvider(resolvedCredentials.mechanism, resolvedCredentials.mechanismProperties);
  100. if (!provider) {
  101. throw new error_1.MongoInvalidArgumentError(`No AuthProvider for ${resolvedCredentials.mechanism} defined.`);
  102. }
  103. try {
  104. await provider.auth(authContext);
  105. }
  106. catch (error) {
  107. if (error instanceof error_1.MongoError) {
  108. error.addErrorLabel(error_1.MongoErrorLabel.HandshakeError);
  109. if ((0, error_1.needsRetryableWriteLabel)(error, response.maxWireVersion, conn.description.type)) {
  110. error.addErrorLabel(error_1.MongoErrorLabel.RetryableWriteError);
  111. }
  112. }
  113. throw error;
  114. }
  115. }
  116. // Connection establishment is socket creation (tcp handshake, tls handshake, MongoDB handshake (saslStart, saslContinue))
  117. // Once connection is established, command logging can log events (if enabled)
  118. conn.established = true;
  119. async function executeHandshake(handshakeDoc, handshakeOptions) {
  120. try {
  121. const handshakeResponse = await conn.command((0, utils_1.ns)('admin.$cmd'), handshakeDoc, handshakeOptions);
  122. return handshakeResponse;
  123. }
  124. catch (error) {
  125. if (error instanceof error_1.MongoError) {
  126. error.addErrorLabel(error_1.MongoErrorLabel.HandshakeError);
  127. }
  128. throw error;
  129. }
  130. }
  131. }
  132. /**
  133. * @internal
  134. *
  135. * This function is only exposed for testing purposes.
  136. */
  137. async function prepareHandshakeDocument(authContext) {
  138. const options = authContext.options;
  139. const compressors = options.compressors ? options.compressors : [];
  140. const { serverApi } = authContext.connection;
  141. const clientMetadata = await options.metadata;
  142. const handshakeDoc = {
  143. [serverApi?.version || options.loadBalanced === true ? 'hello' : constants_1.LEGACY_HELLO_COMMAND]: 1,
  144. helloOk: true,
  145. client: clientMetadata,
  146. compression: compressors
  147. };
  148. if (options.loadBalanced === true) {
  149. handshakeDoc.loadBalanced = true;
  150. }
  151. const credentials = authContext.credentials;
  152. if (credentials) {
  153. if (credentials.mechanism === providers_1.AuthMechanism.MONGODB_DEFAULT && credentials.username) {
  154. handshakeDoc.saslSupportedMechs = `${credentials.source}.${credentials.username}`;
  155. const provider = authContext.options.authProviders.getOrCreateProvider(providers_1.AuthMechanism.MONGODB_SCRAM_SHA256, credentials.mechanismProperties);
  156. if (!provider) {
  157. // This auth mechanism is always present.
  158. throw new error_1.MongoInvalidArgumentError(`No AuthProvider for ${providers_1.AuthMechanism.MONGODB_SCRAM_SHA256} defined.`);
  159. }
  160. return await provider.prepare(handshakeDoc, authContext);
  161. }
  162. const provider = authContext.options.authProviders.getOrCreateProvider(credentials.mechanism, credentials.mechanismProperties);
  163. if (!provider) {
  164. throw new error_1.MongoInvalidArgumentError(`No AuthProvider for ${credentials.mechanism} defined.`);
  165. }
  166. return await provider.prepare(handshakeDoc, authContext);
  167. }
  168. return handshakeDoc;
  169. }
  170. /** @public */
  171. exports.LEGAL_TLS_SOCKET_OPTIONS = [
  172. 'allowPartialTrustChain',
  173. 'ALPNProtocols',
  174. 'ca',
  175. 'cert',
  176. 'checkServerIdentity',
  177. 'ciphers',
  178. 'crl',
  179. 'ecdhCurve',
  180. 'key',
  181. 'minDHSize',
  182. 'passphrase',
  183. 'pfx',
  184. 'rejectUnauthorized',
  185. 'secureContext',
  186. 'secureProtocol',
  187. 'servername',
  188. 'session'
  189. ];
  190. /** @public */
  191. exports.LEGAL_TCP_SOCKET_OPTIONS = [
  192. 'autoSelectFamily',
  193. 'autoSelectFamilyAttemptTimeout',
  194. 'keepAliveInitialDelay',
  195. 'family',
  196. 'hints',
  197. 'localAddress',
  198. 'localPort',
  199. 'lookup'
  200. ];
  201. function parseConnectOptions(options) {
  202. const hostAddress = options.hostAddress;
  203. if (!hostAddress)
  204. throw new error_1.MongoInvalidArgumentError('Option "hostAddress" is required');
  205. const result = {};
  206. for (const name of exports.LEGAL_TCP_SOCKET_OPTIONS) {
  207. if (options[name] != null) {
  208. result[name] = options[name];
  209. }
  210. }
  211. result.keepAliveInitialDelay ??= 120000;
  212. result.keepAlive = true;
  213. result.noDelay = options.noDelay ?? true;
  214. if (typeof hostAddress.socketPath === 'string') {
  215. result.path = hostAddress.socketPath;
  216. return result;
  217. }
  218. else if (typeof hostAddress.host === 'string') {
  219. result.host = hostAddress.host;
  220. result.port = hostAddress.port;
  221. return result;
  222. }
  223. else {
  224. // This should never happen since we set up HostAddresses
  225. // But if we don't throw here the socket could hang until timeout
  226. // TODO(NODE-3483)
  227. throw new error_1.MongoRuntimeError(`Unexpected HostAddress ${JSON.stringify(hostAddress)}`);
  228. }
  229. }
  230. function parseSslOptions(options) {
  231. const result = parseConnectOptions(options);
  232. // Merge in valid SSL options
  233. for (const name of exports.LEGAL_TLS_SOCKET_OPTIONS) {
  234. if (options[name] != null) {
  235. result[name] = options[name];
  236. }
  237. }
  238. if (options.existingSocket) {
  239. result.socket = options.existingSocket;
  240. }
  241. // Set default sni servername to be the same as host
  242. if (result.servername == null && result.host && !net.isIP(result.host)) {
  243. result.servername = result.host;
  244. }
  245. return result;
  246. }
  247. async function makeSocket(options) {
  248. const useTLS = options.tls ?? false;
  249. const connectTimeoutMS = options.connectTimeoutMS ?? 30000;
  250. const existingSocket = options.existingSocket;
  251. let socket;
  252. if (options.proxyHost != null) {
  253. // Currently, only Socks5 is supported.
  254. return await makeSocks5Connection({
  255. ...options,
  256. connectTimeoutMS // Should always be present for Socks5
  257. });
  258. }
  259. if (useTLS) {
  260. const tlsSocket = tls.connect(parseSslOptions(options));
  261. if (typeof tlsSocket.disableRenegotiation === 'function') {
  262. tlsSocket.disableRenegotiation();
  263. }
  264. socket = tlsSocket;
  265. }
  266. else if (existingSocket) {
  267. // In the TLS case, parseSslOptions() sets options.socket to existingSocket,
  268. // so we only need to handle the non-TLS case here (where existingSocket
  269. // gives us all we need out of the box).
  270. socket = existingSocket;
  271. }
  272. else {
  273. socket = net.createConnection(parseConnectOptions(options));
  274. }
  275. socket.setTimeout(connectTimeoutMS);
  276. let cancellationHandler = null;
  277. const { promise: connectedSocket, resolve, reject } = (0, utils_1.promiseWithResolvers)();
  278. if (existingSocket) {
  279. resolve(socket);
  280. }
  281. else {
  282. const start = performance.now();
  283. const connectEvent = useTLS ? 'secureConnect' : 'connect';
  284. socket
  285. .once(connectEvent, () => resolve(socket))
  286. .once('error', cause => reject(new error_1.MongoNetworkError(error_1.MongoError.buildErrorMessage(cause), { cause })))
  287. .once('timeout', () => {
  288. reject(new error_1.MongoNetworkTimeoutError(`Socket '${connectEvent}' timed out after ${(performance.now() - start) | 0}ms (connectTimeoutMS: ${connectTimeoutMS})`));
  289. })
  290. .once('close', () => reject(new error_1.MongoNetworkError(`Socket closed after ${(performance.now() - start) | 0} during connection establishment`)));
  291. if (options.cancellationToken != null) {
  292. cancellationHandler = () => reject(new error_1.MongoNetworkError(`Socket connection establishment was cancelled after ${(performance.now() - start) | 0}`));
  293. options.cancellationToken.once('cancel', cancellationHandler);
  294. }
  295. }
  296. try {
  297. socket = await connectedSocket;
  298. return socket;
  299. }
  300. catch (error) {
  301. socket.destroy();
  302. throw error;
  303. }
  304. finally {
  305. socket.setTimeout(0);
  306. if (cancellationHandler != null) {
  307. options.cancellationToken?.removeListener('cancel', cancellationHandler);
  308. }
  309. }
  310. }
  311. let socks = null;
  312. function loadSocks() {
  313. if (socks == null) {
  314. const socksImport = (0, deps_1.getSocks)();
  315. if ('kModuleError' in socksImport) {
  316. throw socksImport.kModuleError;
  317. }
  318. socks = socksImport;
  319. }
  320. return socks;
  321. }
  322. async function makeSocks5Connection(options) {
  323. const hostAddress = utils_1.HostAddress.fromHostPort(options.proxyHost ?? '', // proxyHost is guaranteed to set here
  324. options.proxyPort ?? 1080);
  325. // First, connect to the proxy server itself:
  326. const rawSocket = await makeSocket({
  327. ...options,
  328. hostAddress,
  329. tls: false,
  330. proxyHost: undefined
  331. });
  332. const destination = parseConnectOptions(options);
  333. if (typeof destination.host !== 'string' || typeof destination.port !== 'number') {
  334. throw new error_1.MongoInvalidArgumentError('Can only make Socks5 connections to TCP hosts');
  335. }
  336. socks ??= loadSocks();
  337. let existingSocket;
  338. try {
  339. // Then, establish the Socks5 proxy connection:
  340. const connection = await socks.SocksClient.createConnection({
  341. existing_socket: rawSocket,
  342. timeout: options.connectTimeoutMS,
  343. command: 'connect',
  344. destination: {
  345. host: destination.host,
  346. port: destination.port
  347. },
  348. proxy: {
  349. // host and port are ignored because we pass existing_socket
  350. host: 'iLoveJavaScript',
  351. port: 0,
  352. type: 5,
  353. userId: options.proxyUsername || undefined,
  354. password: options.proxyPassword || undefined
  355. }
  356. });
  357. existingSocket = connection.socket;
  358. }
  359. catch (cause) {
  360. throw new error_1.MongoNetworkError(error_1.MongoError.buildErrorMessage(cause), { cause });
  361. }
  362. // Finally, now treat the resulting duplex stream as the
  363. // socket over which we send and receive wire protocol messages:
  364. return await makeSocket({ ...options, existingSocket, proxyHost: undefined });
  365. }
  366. //# sourceMappingURL=connect.js.map