'use strict'; const EventEmitter = require('events'); const MongoError = require('../error').MongoError; const Pool = require('../connection/pool'); const relayEvents = require('../utils').relayEvents; const calculateDurationInMs = require('../utils').calculateDurationInMs; const Query = require('../connection/commands').Query; const TwoSixWireProtocolSupport = require('../wireprotocol/2_6_support'); const ThreeTwoWireProtocolSupport = require('../wireprotocol/3_2_support'); const BSON = require('../connection/utils').retrieveBSON(); const createClientInfo = require('../topologies/shared').createClientInfo; const Logger = require('../connection/logger'); const ServerDescription = require('./server_description').ServerDescription; const ReadPreference = require('../topologies/read_preference'); const monitorServer = require('./monitoring').monitorServer; /** * * @fires Server#serverHeartbeatStarted * @fires Server#serverHeartbeatSucceeded * @fires Server#serverHeartbeatFailed */ class Server extends EventEmitter { /** * Create a server * * @param {ServerDescription} description * @param {Object} options */ constructor(description, options) { super(); this.s = { // the server description description, // a saved copy of the incoming options options, // the server logger logger: Logger('Server', options), // the bson parser bson: options.bson || new BSON(), // client metadata for the initial handshake clientInfo: createClientInfo(options), // state variable to determine if there is an active server check in progress monitoring: false, // the connection pool pool: null }; } get description() { return this.s.description; } get name() { return this.s.description.address; } /** * Initiate server connect * * @param {Array} [options.auth] Array of auth options to apply on connect */ connect(options) { options = options || {}; // do not allow connect to be called on anything that's not disconnected if (this.s.pool && !this.s.pool.isDisconnected() && !this.s.pool.isDestroyed()) { throw new MongoError(`Server instance in invalid state ${this.s.pool.state}`); } // create a pool this.s.pool = new Pool(this, Object.assign(this.s.options, options, { bson: this.s.bson })); // Set up listeners this.s.pool.on('connect', connectEventHandler(this)); this.s.pool.on('close', closeEventHandler(this)); // this.s.pool.on('error', errorEventHandler(this)); // this.s.pool.on('timeout', timeoutEventHandler(this)); // this.s.pool.on('parseError', errorEventHandler(this)); // this.s.pool.on('reconnect', reconnectEventHandler(this)); // this.s.pool.on('reconnectFailed', errorEventHandler(this)); // relay all command monitoring events relayEvents(this.s.pool, this, ['commandStarted', 'commandSucceeded', 'commandFailed']); // If auth settings have been provided, use them if (options.auth) { this.s.pool.connect.apply(this.s.pool, options.auth); return; } this.s.pool.connect(); } /** * Destroy the server connection * * @param {Boolean} [options.emitClose=false] Emit close event on destroy * @param {Boolean} [options.emitDestroy=false] Emit destroy event on destroy * @param {Boolean} [options.force=false] Force destroy the pool */ destroy(callback) { if (typeof callback === 'function') { callback(null, null); } } /** * Immediately schedule monitoring of this server. If there already an attempt being made * this will be a no-op. */ monitor() { if (this.s.monitoring) return; if (this.s.monitorId) clearTimeout(this.s.monitorId); monitorServer(this); } /** * Execute a command * * @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1) * @param {object} cmd The command hash * @param {ReadPreference} [options.readPreference] Specify read preference if command supports it * @param {Boolean} [options.serializeFunctions=false] Specify if functions on an object should be serialized. * @param {Boolean} [options.checkKeys=false] Specify if the bson parser should validate keys. * @param {Boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields. * @param {Boolean} [options.fullResult=false] Return the full envelope instead of just the result document. * @param {ClientSession} [options.session=null] Session to use for the operation * @param {opResultCallback} callback A callback function */ command(ns, cmd, options, callback) { if (typeof options === 'function') { (callback = options), (options = {}), (options = options || {}); } const error = basicReadValidations(this, options); if (error) { return callback(error, null); } // Clone the options options = Object.assign({}, options, { wireProtocolCommand: false }); // Debug log if (this.s.logger.isDebug()) { this.s.logger.debug( `executing command [${JSON.stringify({ ns, cmd, options })}] against ${this.name}` ); } // Check if we have collation support if (this.description.maxWireVersion < 5 && cmd.collation) { callback(new MongoError(`server ${this.name} does not support collation`)); return; } // Create the query object const query = this.s.wireProtocolHandler.command(this, ns, cmd, {}, options); // Set slave OK of the query query.slaveOk = options.readPreference ? options.readPreference.slaveOk() : false; // write options const writeOptions = { raw: typeof options.raw === 'boolean' ? options.raw : false, promoteLongs: typeof options.promoteLongs === 'boolean' ? options.promoteLongs : true, promoteValues: typeof options.promoteValues === 'boolean' ? options.promoteValues : true, promoteBuffers: typeof options.promoteBuffers === 'boolean' ? options.promoteBuffers : false, command: true, monitoring: typeof options.monitoring === 'boolean' ? options.monitoring : false, fullResult: typeof options.fullResult === 'boolean' ? options.fullResult : false, requestId: query.requestId, socketTimeout: typeof options.socketTimeout === 'number' ? options.socketTimeout : null, session: options.session || null }; // write the operation to the pool this.s.pool.write(query, writeOptions, callback); } /** * Insert one or more documents * @method * @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1) * @param {array} ops An array of documents to insert * @param {boolean} [options.ordered=true] Execute in order or out of order * @param {object} [options.writeConcern={}] Write concern for the operation * @param {Boolean} [options.serializeFunctions=false] Specify if functions on an object should be serialized. * @param {Boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields. * @param {ClientSession} [options.session=null] Session to use for the operation * @param {opResultCallback} callback A callback function */ insert(ns, ops, options, callback) { executeWriteOperation({ server: this, op: 'insert', ns, ops }, options, callback); } /** * Perform one or more update operations * @method * @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1) * @param {array} ops An array of updates * @param {boolean} [options.ordered=true] Execute in order or out of order * @param {object} [options.writeConcern={}] Write concern for the operation * @param {Boolean} [options.serializeFunctions=false] Specify if functions on an object should be serialized. * @param {Boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields. * @param {ClientSession} [options.session=null] Session to use for the operation * @param {opResultCallback} callback A callback function */ update(ns, ops, options, callback) { executeWriteOperation({ server: this, op: 'update', ns, ops }, options, callback); } /** * Perform one or more remove operations * @method * @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1) * @param {array} ops An array of removes * @param {boolean} [options.ordered=true] Execute in order or out of order * @param {object} [options.writeConcern={}] Write concern for the operation * @param {Boolean} [options.serializeFunctions=false] Specify if functions on an object should be serialized. * @param {Boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields. * @param {ClientSession} [options.session=null] Session to use for the operation * @param {opResultCallback} callback A callback function */ remove(ns, ops, options, callback) { executeWriteOperation({ server: this, op: 'remove', ns, ops }, options, callback); } } function basicWriteValidations(server) { if (!server.s.pool) { return new MongoError('server instance is not connected'); } if (server.s.pool.isDestroyed()) { return new MongoError('server instance pool was destroyed'); } return null; } function basicReadValidations(server, options) { const error = basicWriteValidations(server, options); if (error) { return error; } if (options.readPreference && !(options.readPreference instanceof ReadPreference)) { return new MongoError('readPreference must be an instance of ReadPreference'); } } function executeWriteOperation(args, options, callback) { if (typeof options === 'function') (callback = options), (options = {}); options = options || {}; // TODO: once we drop Node 4, use destructuring either here or in arguments. const server = args.server; const op = args.op; const ns = args.ns; const ops = Array.isArray(args.ops) ? args.ops : [args.ops]; const error = basicWriteValidations(server, options); if (error) { callback(error, null); return; } // Check if we have collation support if (server.description.maxWireVersion < 5 && options.collation) { callback(new MongoError(`server ${this.name} does not support collation`)); return; } // Execute write return server.s.wireProtocolHandler[op](server.s.pool, ns, server.s.bson, ops, options, callback); } function saslSupportedMechs(options) { if (!options) { return {}; } const authArray = options.auth || []; const authMechanism = authArray[0] || options.authMechanism; const authSource = authArray[1] || options.authSource || options.dbName || 'admin'; const user = authArray[2] || options.user; if (typeof authMechanism === 'string' && authMechanism.toUpperCase() !== 'DEFAULT') { return {}; } if (!user) { return {}; } return { saslSupportedMechs: `${authSource}.${user}` }; } function extractIsMasterError(err, result) { if (err) return err; if (result && result.result && result.result.ok === 0) { return new MongoError(result.result); } } function executeServerHandshake(server, callback) { // construct an `ismaster` query const compressors = server.s.options.compression && server.s.options.compression.compressors ? server.s.options.compression.compressors : []; const queryOptions = { numberToSkip: 0, numberToReturn: -1, checkKeys: false, slaveOk: true }; const query = new Query( server.s.bson, 'admin.$cmd', Object.assign( { ismaster: true, client: server.s.clientInfo, compression: compressors }, saslSupportedMechs(server.s.options) ), queryOptions ); // execute the query server.s.pool.write( query, { socketTimeout: server.s.options.connectionTimeout || 2000 }, callback ); } function configureWireProtocolHandler(ismaster) { // 3.2 wire protocol handler if (ismaster.maxWireVersion >= 4) { return new ThreeTwoWireProtocolSupport(); } // default to 2.6 wire protocol handler return new TwoSixWireProtocolSupport(); } function connectEventHandler(server) { return function() { // log information of received information if in info mode // if (server.s.logger.isInfo()) { // var object = err instanceof MongoError ? JSON.stringify(err) : {}; // server.s.logger.info(`server ${server.name} fired event ${event} out with message ${object}`); // } // begin initial server handshake const start = process.hrtime(); executeServerHandshake(server, (err, response) => { // Set initial lastIsMasterMS - is this needed? server.s.lastIsMasterMS = calculateDurationInMs(start); const serverError = extractIsMasterError(err, response); if (serverError) { server.emit('error', serverError); return; } // extract the ismaster from the server response const isMaster = response.result; // compression negotation if (isMaster && isMaster.compression) { const localCompressionInfo = server.s.options.compression; const localCompressors = localCompressionInfo.compressors; for (var i = 0; i < localCompressors.length; i++) { if (isMaster.compression.indexOf(localCompressors[i]) > -1) { server.s.pool.options.agreedCompressor = localCompressors[i]; break; } } if (localCompressionInfo.zlibCompressionLevel) { server.s.pool.options.zlibCompressionLevel = localCompressionInfo.zlibCompressionLevel; } } // configure the wire protocol handler server.s.wireProtocolHandler = configureWireProtocolHandler(isMaster); // log the connection event if requested if (server.s.logger.isInfo()) { server.s.logger.info( `server ${server.name} connected with ismaster [${JSON.stringify(isMaster)}]` ); } // emit an event indicating that our description has changed server.emit( 'descriptionReceived', new ServerDescription(server.description.address, isMaster) ); // emit a connect event server.emit('connect', isMaster); }); }; } function closeEventHandler(server) { return function() { server.emit('close'); }; } module.exports = Server;