connect.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. 'use strict';
  2. const net = require('net');
  3. const tls = require('tls');
  4. const Connection = require('./connection');
  5. const Query = require('./commands').Query;
  6. const createClientInfo = require('../topologies/shared').createClientInfo;
  7. const MongoError = require('../error').MongoError;
  8. const defaultAuthProviders = require('../auth/defaultAuthProviders').defaultAuthProviders;
  9. const WIRE_CONSTANTS = require('../wireprotocol/constants');
  10. const MAX_SUPPORTED_WIRE_VERSION = WIRE_CONSTANTS.MAX_SUPPORTED_WIRE_VERSION;
  11. const MAX_SUPPORTED_SERVER_VERSION = WIRE_CONSTANTS.MAX_SUPPORTED_SERVER_VERSION;
  12. const MIN_SUPPORTED_WIRE_VERSION = WIRE_CONSTANTS.MIN_SUPPORTED_WIRE_VERSION;
  13. const MIN_SUPPORTED_SERVER_VERSION = WIRE_CONSTANTS.MIN_SUPPORTED_SERVER_VERSION;
  14. let AUTH_PROVIDERS;
  15. function connect(options, callback) {
  16. if (AUTH_PROVIDERS == null) {
  17. AUTH_PROVIDERS = defaultAuthProviders(options.bson);
  18. }
  19. if (options.family !== void 0) {
  20. makeConnection(options.family, options, (err, socket) => {
  21. if (err) {
  22. callback(err, socket); // in the error case, `socket` is the originating error event name
  23. return;
  24. }
  25. performInitialHandshake(new Connection(socket, options), options, callback);
  26. });
  27. return;
  28. }
  29. return makeConnection(6, options, (err, ipv6Socket) => {
  30. if (err) {
  31. makeConnection(4, options, (err, ipv4Socket) => {
  32. if (err) {
  33. callback(err, ipv4Socket); // in the error case, `ipv4Socket` is the originating error event name
  34. return;
  35. }
  36. performInitialHandshake(new Connection(ipv4Socket, options), options, callback);
  37. });
  38. return;
  39. }
  40. performInitialHandshake(new Connection(ipv6Socket, options), options, callback);
  41. });
  42. }
  43. function getSaslSupportedMechs(options) {
  44. if (!(options && options.credentials)) {
  45. return {};
  46. }
  47. const credentials = options.credentials;
  48. // TODO: revisit whether or not items like `options.user` and `options.dbName` should be checked here
  49. const authMechanism = credentials.mechanism;
  50. const authSource = credentials.source || options.dbName || 'admin';
  51. const user = credentials.username || options.user;
  52. if (typeof authMechanism === 'string' && authMechanism.toUpperCase() !== 'DEFAULT') {
  53. return {};
  54. }
  55. if (!user) {
  56. return {};
  57. }
  58. return { saslSupportedMechs: `${authSource}.${user}` };
  59. }
  60. function checkSupportedServer(ismaster, options) {
  61. const serverVersionHighEnough =
  62. ismaster &&
  63. typeof ismaster.maxWireVersion === 'number' &&
  64. ismaster.maxWireVersion >= MIN_SUPPORTED_WIRE_VERSION;
  65. const serverVersionLowEnough =
  66. ismaster &&
  67. typeof ismaster.minWireVersion === 'number' &&
  68. ismaster.minWireVersion <= MAX_SUPPORTED_WIRE_VERSION;
  69. if (serverVersionHighEnough) {
  70. if (serverVersionLowEnough) {
  71. return null;
  72. }
  73. const message = `Server at ${options.host}:${options.port} reports minimum wire version ${
  74. ismaster.minWireVersion
  75. }, but this version of the Node.js Driver requires at most ${MAX_SUPPORTED_WIRE_VERSION} (MongoDB ${MAX_SUPPORTED_SERVER_VERSION})`;
  76. return new MongoError(message);
  77. }
  78. const message = `Server at ${options.host}:${
  79. options.port
  80. } reports maximum wire version ${ismaster.maxWireVersion ||
  81. 0}, but this version of the Node.js Driver requires at least ${MIN_SUPPORTED_WIRE_VERSION} (MongoDB ${MIN_SUPPORTED_SERVER_VERSION})`;
  82. return new MongoError(message);
  83. }
  84. function performInitialHandshake(conn, options, _callback) {
  85. const callback = function(err, ret) {
  86. if (err && conn) {
  87. conn.destroy();
  88. }
  89. _callback(err, ret);
  90. };
  91. let compressors = [];
  92. if (options.compression && options.compression.compressors) {
  93. compressors = options.compression.compressors;
  94. }
  95. const handshakeDoc = Object.assign(
  96. {
  97. ismaster: true,
  98. client: createClientInfo(options),
  99. compression: compressors
  100. },
  101. getSaslSupportedMechs(options)
  102. );
  103. const start = new Date().getTime();
  104. runCommand(conn, 'admin.$cmd', handshakeDoc, options, (err, ismaster) => {
  105. if (err) {
  106. callback(err, null);
  107. return;
  108. }
  109. if (ismaster.ok === 0) {
  110. callback(new MongoError(ismaster), null);
  111. return;
  112. }
  113. const supportedServerErr = checkSupportedServer(ismaster, options);
  114. if (supportedServerErr) {
  115. callback(supportedServerErr, null);
  116. return;
  117. }
  118. // resolve compression
  119. if (ismaster.compression) {
  120. const agreedCompressors = compressors.filter(
  121. compressor => ismaster.compression.indexOf(compressor) !== -1
  122. );
  123. if (agreedCompressors.length) {
  124. conn.agreedCompressor = agreedCompressors[0];
  125. }
  126. if (options.compression && options.compression.zlibCompressionLevel) {
  127. conn.zlibCompressionLevel = options.compression.zlibCompressionLevel;
  128. }
  129. }
  130. // NOTE: This is metadata attached to the connection while porting away from
  131. // handshake being done in the `Server` class. Likely, it should be
  132. // relocated, or at very least restructured.
  133. conn.ismaster = ismaster;
  134. conn.lastIsMasterMS = new Date().getTime() - start;
  135. const credentials = options.credentials;
  136. if (!ismaster.arbiterOnly && credentials) {
  137. credentials.resolveAuthMechanism(ismaster);
  138. authenticate(conn, credentials, callback);
  139. return;
  140. }
  141. callback(null, conn);
  142. });
  143. }
  144. const LEGAL_SSL_SOCKET_OPTIONS = [
  145. 'pfx',
  146. 'key',
  147. 'passphrase',
  148. 'cert',
  149. 'ca',
  150. 'ciphers',
  151. 'NPNProtocols',
  152. 'ALPNProtocols',
  153. 'servername',
  154. 'ecdhCurve',
  155. 'secureProtocol',
  156. 'secureContext',
  157. 'session',
  158. 'minDHSize',
  159. 'crl',
  160. 'rejectUnauthorized'
  161. ];
  162. function parseConnectOptions(family, options) {
  163. const host = typeof options.host === 'string' ? options.host : 'localhost';
  164. if (host.indexOf('/') !== -1) {
  165. return { path: host };
  166. }
  167. const result = {
  168. family,
  169. host,
  170. port: typeof options.port === 'number' ? options.port : 27017,
  171. rejectUnauthorized: false
  172. };
  173. return result;
  174. }
  175. function parseSslOptions(family, options) {
  176. const result = parseConnectOptions(family, options);
  177. // Merge in valid SSL options
  178. for (const name in options) {
  179. if (options[name] != null && LEGAL_SSL_SOCKET_OPTIONS.indexOf(name) !== -1) {
  180. result[name] = options[name];
  181. }
  182. }
  183. // Override checkServerIdentity behavior
  184. if (options.checkServerIdentity === false) {
  185. // Skip the identiy check by retuning undefined as per node documents
  186. // https://nodejs.org/api/tls.html#tls_tls_connect_options_callback
  187. result.checkServerIdentity = function() {
  188. return undefined;
  189. };
  190. } else if (typeof options.checkServerIdentity === 'function') {
  191. result.checkServerIdentity = options.checkServerIdentity;
  192. }
  193. // Set default sni servername to be the same as host
  194. if (result.servername == null) {
  195. result.servername = result.host;
  196. }
  197. return result;
  198. }
  199. function makeConnection(family, options, _callback) {
  200. const useSsl = typeof options.ssl === 'boolean' ? options.ssl : false;
  201. const keepAlive = typeof options.keepAlive === 'boolean' ? options.keepAlive : true;
  202. let keepAliveInitialDelay =
  203. typeof options.keepAliveInitialDelay === 'number' ? options.keepAliveInitialDelay : 300000;
  204. const noDelay = typeof options.noDelay === 'boolean' ? options.noDelay : true;
  205. const connectionTimeout =
  206. typeof options.connectionTimeout === 'number' ? options.connectionTimeout : 30000;
  207. const socketTimeout = typeof options.socketTimeout === 'number' ? options.socketTimeout : 360000;
  208. const rejectUnauthorized =
  209. typeof options.rejectUnauthorized === 'boolean' ? options.rejectUnauthorized : true;
  210. if (keepAliveInitialDelay > socketTimeout) {
  211. keepAliveInitialDelay = Math.round(socketTimeout / 2);
  212. }
  213. let socket;
  214. const callback = function(err, ret) {
  215. if (err && socket) {
  216. socket.destroy();
  217. }
  218. _callback(err, ret);
  219. };
  220. try {
  221. if (useSsl) {
  222. socket = tls.connect(parseSslOptions(family, options));
  223. if (typeof socket.disableRenegotiation === 'function') {
  224. socket.disableRenegotiation();
  225. }
  226. } else {
  227. socket = net.createConnection(parseConnectOptions(family, options));
  228. }
  229. } catch (err) {
  230. return callback(err);
  231. }
  232. socket.setKeepAlive(keepAlive, keepAliveInitialDelay);
  233. socket.setTimeout(connectionTimeout);
  234. socket.setNoDelay(noDelay);
  235. const errorEvents = ['error', 'close', 'timeout', 'parseError', 'connect'];
  236. function errorHandler(eventName) {
  237. return err => {
  238. if (err == null || err === false) err = true;
  239. errorEvents.forEach(event => socket.removeAllListeners(event));
  240. socket.removeListener('connect', connectHandler);
  241. callback(err, eventName);
  242. };
  243. }
  244. function connectHandler() {
  245. errorEvents.forEach(event => socket.removeAllListeners(event));
  246. if (socket.authorizationError && rejectUnauthorized) {
  247. return callback(socket.authorizationError);
  248. }
  249. socket.setTimeout(socketTimeout);
  250. callback(null, socket);
  251. }
  252. socket.once('error', errorHandler('error'));
  253. socket.once('close', errorHandler('close'));
  254. socket.once('timeout', errorHandler('timeout'));
  255. socket.once('parseError', errorHandler('parseError'));
  256. socket.once('connect', connectHandler);
  257. }
  258. const CONNECTION_ERROR_EVENTS = ['error', 'close', 'timeout', 'parseError'];
  259. function runCommand(conn, ns, command, options, callback) {
  260. if (typeof options === 'function') (callback = options), (options = {});
  261. const socketTimeout = typeof options.socketTimeout === 'number' ? options.socketTimeout : 360000;
  262. const bson = conn.options.bson;
  263. const query = new Query(bson, ns, command, {
  264. numberToSkip: 0,
  265. numberToReturn: 1
  266. });
  267. function errorHandler(err) {
  268. conn.resetSocketTimeout();
  269. CONNECTION_ERROR_EVENTS.forEach(eventName => conn.removeListener(eventName, errorHandler));
  270. conn.removeListener('message', messageHandler);
  271. callback(err, null);
  272. }
  273. function messageHandler(msg) {
  274. if (msg.responseTo !== query.requestId) {
  275. return;
  276. }
  277. conn.resetSocketTimeout();
  278. CONNECTION_ERROR_EVENTS.forEach(eventName => conn.removeListener(eventName, errorHandler));
  279. conn.removeListener('message', messageHandler);
  280. msg.parse({ promoteValues: true });
  281. callback(null, msg.documents[0]);
  282. }
  283. conn.setSocketTimeout(socketTimeout);
  284. CONNECTION_ERROR_EVENTS.forEach(eventName => conn.once(eventName, errorHandler));
  285. conn.on('message', messageHandler);
  286. conn.write(query.toBin());
  287. }
  288. function authenticate(conn, credentials, callback) {
  289. const mechanism = credentials.mechanism;
  290. if (!AUTH_PROVIDERS[mechanism]) {
  291. callback(new MongoError(`authMechanism '${mechanism}' not supported`));
  292. return;
  293. }
  294. const provider = AUTH_PROVIDERS[mechanism];
  295. provider.auth(runCommand, [conn], credentials, err => {
  296. if (err) return callback(err);
  297. callback(null, conn);
  298. });
  299. }
  300. module.exports = connect;