diff options
Diffstat (limited to 'node_modules/mongodb-core/lib')
26 files changed, 10763 insertions, 0 deletions
diff --git a/node_modules/mongodb-core/lib/auth/gssapi.js b/node_modules/mongodb-core/lib/auth/gssapi.js new file mode 100644 index 0000000..c3bd6bd --- /dev/null +++ b/node_modules/mongodb-core/lib/auth/gssapi.js @@ -0,0 +1,277 @@ +"use strict"; + +var f = require('util').format + , crypto = require('crypto') + , require_optional = require('require_optional') + , Query = require('../connection/commands').Query + , MongoError = require('../error'); + +var AuthSession = function(db, username, password, options) { + this.db = db; + this.username = username; + this.password = password; + this.options = options; +} + +AuthSession.prototype.equal = function(session) { + return session.db == this.db + && session.username == this.username + && session.password == this.password; +} + +// Kerberos class +var Kerberos = null; +var MongoAuthProcess = null; + +// Try to grab the Kerberos class +try { + Kerberos = require_optional('kerberos').Kerberos; + // Authentication process for Mongo + MongoAuthProcess = require_optional('kerberos').processes.MongoAuthProcess +} catch(err) {} + +/** + * Creates a new GSSAPI authentication mechanism + * @class + * @return {GSSAPI} A cursor instance + */ +var GSSAPI = function(bson) { + this.bson = bson; + this.authStore = []; +} + +/** + * Authenticate + * @method + * @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on + * @param {[]Connections} connections Connections to authenticate using this authenticator + * @param {string} db Name of the database + * @param {string} username Username + * @param {string} password Password + * @param {authResultCallback} callback The callback to return the result from the authentication + * @return {object} + */ +GSSAPI.prototype.auth = function(server, connections, db, username, password, options, callback) { + var self = this; + // We don't have the Kerberos library + if(Kerberos == null) return callback(new Error("Kerberos library is not installed")); + var gssapiServiceName = options['gssapiServiceName'] || 'mongodb'; + // Total connections + var count = connections.length; + if(count == 0) return callback(null, null); + + // Valid connections + var numberOfValidConnections = 0; + var credentialsValid = false; + var errorObject = null; + + // For each connection we need to authenticate + while(connections.length > 0) { + // Execute MongoCR + var execute = function(connection) { + // Start Auth process for a connection + GSSAPIInitialize(self, db, username, password, db, gssapiServiceName, server, connection, options, function(err, r) { + // Adjust count + count = count - 1; + + // If we have an error + if(err) { + errorObject = err; + } else if(r.result['$err']) { + errorObject = r.result; + } else if(r.result['errmsg']) { + errorObject = r.result; + } else { + credentialsValid = true; + numberOfValidConnections = numberOfValidConnections + 1; + } + + // We have authenticated all connections + if(count == 0 && numberOfValidConnections > 0) { + // Store the auth details + addAuthSession(self.authStore, new AuthSession(db, username, password, options)); + // Return correct authentication + callback(null, true); + } else if(count == 0) { + if(errorObject == null) errorObject = new MongoError(f("failed to authenticate using mongocr")); + callback(errorObject, false); + } + }); + } + + var _execute = function(_connection) { + process.nextTick(function() { + execute(_connection); + }); + } + + _execute(connections.shift()); + } +} + +// +// Initialize step +var GSSAPIInitialize = function(self, db, username, password, authdb, gssapiServiceName, server, connection, options, callback) { + // Create authenticator + var mongo_auth_process = new MongoAuthProcess(connection.host, connection.port, gssapiServiceName, options); + + // Perform initialization + mongo_auth_process.init(username, password, function(err, context) { + if(err) return callback(err, false); + + // Perform the first step + mongo_auth_process.transition('', function(err, payload) { + if(err) return callback(err, false); + + // Call the next db step + MongoDBGSSAPIFirstStep(self, mongo_auth_process, payload, db, username, password, authdb, server, connection, callback); + }); + }); +} + +// +// Perform first step against mongodb +var MongoDBGSSAPIFirstStep = function(self, mongo_auth_process, payload, db, username, password, authdb, server, connection, callback) { + // Build the sasl start command + var command = { + saslStart: 1 + , mechanism: 'GSSAPI' + , payload: payload + , autoAuthorize: 1 + }; + + // Write the commmand on the connection + server(connection, new Query(self.bson, "$external.$cmd", command, { + numberToSkip: 0, numberToReturn: 1 + }).toBin(), function(err, r) { + // // Execute first sasl step + // server.command("$external.$cmd" + // , command + // , { connection: connection }, function(err, r) { + if(err) return callback(err, false); + var doc = r.result; + // Execute mongodb transition + mongo_auth_process.transition(r.result.payload, function(err, payload) { + if(err) return callback(err, false); + + // MongoDB API Second Step + MongoDBGSSAPISecondStep(self, mongo_auth_process, payload, doc, db, username, password, authdb, server, connection, callback); + }); + }); +} + +// +// Perform first step against mongodb +var MongoDBGSSAPISecondStep = function(self, mongo_auth_process, payload, doc, db, username, password, authdb, server, connection, callback) { + // Build Authentication command to send to MongoDB + var command = { + saslContinue: 1 + , conversationId: doc.conversationId + , payload: payload + }; + + // Execute the command + // Write the commmand on the connection + server(connection, new Query(self.bson, "$external.$cmd", command, { + numberToSkip: 0, numberToReturn: 1 + }).toBin(), function(err, r) { + // server.command("$external.$cmd" + // , command + // , { connection: connection }, function(err, r) { + if(err) return callback(err, false); + var doc = r.result; + // Call next transition for kerberos + mongo_auth_process.transition(doc.payload, function(err, payload) { + if(err) return callback(err, false); + + // Call the last and third step + MongoDBGSSAPIThirdStep(self, mongo_auth_process, payload, doc, db, username, password, authdb, server, connection, callback); + }); + }); +} + +var MongoDBGSSAPIThirdStep = function(self, mongo_auth_process, payload, doc, db, username, password, authdb, server, connection, callback) { + // Build final command + var command = { + saslContinue: 1 + , conversationId: doc.conversationId + , payload: payload + }; + + // Execute the command + // server.command("$external.$cmd" + // , command + // , { connection: connection }, function(err, r) { + // Write the commmand on the connection + server(connection, new Query(self.bson, "$external.$cmd", command, { + numberToSkip: 0, numberToReturn: 1 + }).toBin(), function(err, r) { + if(err) return callback(err, false); + mongo_auth_process.transition(null, function(err, payload) { + if(err) return callback(err, null); + callback(null, r); + }); + }); +} + +// Add to store only if it does not exist +var addAuthSession = function(authStore, session) { + var found = false; + + for(var i = 0; i < authStore.length; i++) { + if(authStore[i].equal(session)) { + found = true; + break; + } + } + + if(!found) authStore.push(session); +} + +/** + * Remove authStore credentials + * @method + * @param {string} db Name of database we are removing authStore details about + * @return {object} + */ +GSSAPI.prototype.logout = function(dbName) { + this.authStore = this.authStore.filter(function(x) { + return x.db != dbName; + }); +} + +/** + * Re authenticate pool + * @method + * @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on + * @param {[]Connections} connections Connections to authenticate using this authenticator + * @param {authResultCallback} callback The callback to return the result from the authentication + * @return {object} + */ +GSSAPI.prototype.reauthenticate = function(server, connections, callback) { + var authStore = this.authStore.slice(0); + var err = null; + var count = authStore.length; + if(count == 0) return callback(null, null); + // Iterate over all the auth details stored + for(var i = 0; i < authStore.length; i++) { + this.auth(server, connections, authStore[i].db, authStore[i].username, authStore[i].password, authStore[i].options, function(err, r) { + if(err) err = err; + count = count - 1; + // Done re-authenticating + if(count == 0) { + callback(err, null); + } + }); + } +} + +/** + * This is a result from a authentication strategy + * + * @callback authResultCallback + * @param {error} error An error object. Set to null if no error present + * @param {boolean} result The result of the authentication process + */ + +module.exports = GSSAPI; diff --git a/node_modules/mongodb-core/lib/auth/mongocr.js b/node_modules/mongodb-core/lib/auth/mongocr.js new file mode 100644 index 0000000..1f5a670 --- /dev/null +++ b/node_modules/mongodb-core/lib/auth/mongocr.js @@ -0,0 +1,192 @@ +"use strict"; + +var f = require('util').format + , crypto = require('crypto') + , Query = require('../connection/commands').Query + , MongoError = require('../error'); + +var AuthSession = function(db, username, password) { + this.db = db; + this.username = username; + this.password = password; +} + +AuthSession.prototype.equal = function(session) { + return session.db == this.db + && session.username == this.username + && session.password == this.password; +} + +/** + * Creates a new MongoCR authentication mechanism + * @class + * @return {MongoCR} A cursor instance + */ +var MongoCR = function(bson) { + this.bson = bson; + this.authStore = []; +} + +// Add to store only if it does not exist +var addAuthSession = function(authStore, session) { + var found = false; + + for(var i = 0; i < authStore.length; i++) { + if(authStore[i].equal(session)) { + found = true; + break; + } + } + + if(!found) authStore.push(session); +} + +/** + * Authenticate + * @method + * @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on + * @param {[]Connections} connections Connections to authenticate using this authenticator + * @param {string} db Name of the database + * @param {string} username Username + * @param {string} password Password + * @param {authResultCallback} callback The callback to return the result from the authentication + * @return {object} + */ +MongoCR.prototype.auth = function(server, connections, db, username, password, callback) { + var self = this; + // Total connections + var count = connections.length; + if(count == 0) return callback(null, null); + + // Valid connections + var numberOfValidConnections = 0; + var credentialsValid = false; + var errorObject = null; + + // For each connection we need to authenticate + while(connections.length > 0) { + // Execute MongoCR + var executeMongoCR = function(connection) { + // Write the commmand on the connection + server(connection, new Query(self.bson, f("%s.$cmd", db), { + getnonce:1 + }, { + numberToSkip: 0, numberToReturn: 1 + }).toBin(), function(err, r) { + // // Let's start the process + // server.command(f("%s.$cmd", db) + // , { getnonce: 1 } + // , { connection: connection }, function(err, r) { + var nonce = null; + var key = null; + + // Adjust the number of connections left + // Get nonce + if(err == null) { + nonce = r.result.nonce; + // Use node md5 generator + var md5 = crypto.createHash('md5'); + // Generate keys used for authentication + md5.update(username + ":mongo:" + password, 'utf8'); + var hash_password = md5.digest('hex'); + // Final key + md5 = crypto.createHash('md5'); + md5.update(nonce + username + hash_password, 'utf8'); + key = md5.digest('hex'); + } + + // Execute command + // Write the commmand on the connection + server(connection, new Query(self.bson, f("%s.$cmd", db), { + authenticate: 1, user: username, nonce: nonce, key:key + }, { + numberToSkip: 0, numberToReturn: 1 + }).toBin(), function(err, r) { + // server.command(f("%s.$cmd", db) + // , { authenticate: 1, user: username, nonce: nonce, key:key} + // , { connection: connection }, function(err, r) { + count = count - 1; + + // If we have an error + if(err) { + errorObject = err; + } else if(r.result['$err']) { + errorObject = r.result; + } else if(r.result['errmsg']) { + errorObject = r.result; + } else { + credentialsValid = true; + numberOfValidConnections = numberOfValidConnections + 1; + } + + // We have authenticated all connections + if(count == 0 && numberOfValidConnections > 0) { + // Store the auth details + addAuthSession(self.authStore, new AuthSession(db, username, password)); + // Return correct authentication + callback(null, true); + } else if(count == 0) { + if(errorObject == null) errorObject = new MongoError(f("failed to authenticate using mongocr")); + callback(errorObject, false); + } + }); + }); + } + + var _execute = function(_connection) { + process.nextTick(function() { + executeMongoCR(_connection); + }); + } + + _execute(connections.shift()); + } +} + +/** + * Remove authStore credentials + * @method + * @param {string} db Name of database we are removing authStore details about + * @return {object} + */ +MongoCR.prototype.logout = function(dbName) { + this.authStore = this.authStore.filter(function(x) { + return x.db != dbName; + }); +} + +/** + * Re authenticate pool + * @method + * @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on + * @param {[]Connections} connections Connections to authenticate using this authenticator + * @param {authResultCallback} callback The callback to return the result from the authentication + * @return {object} + */ +MongoCR.prototype.reauthenticate = function(server, connections, callback) { + var authStore = this.authStore.slice(0); + var err = null; + var count = authStore.length; + if(count == 0) return callback(null, null); + // Iterate over all the auth details stored + for(var i = 0; i < authStore.length; i++) { + this.auth(server, connections, authStore[i].db, authStore[i].username, authStore[i].password, function(err, r) { + if(err) err = err; + count = count - 1; + // Done re-authenticating + if(count == 0) { + callback(err, null); + } + }); + } +} + +/** + * This is a result from a authentication strategy + * + * @callback authResultCallback + * @param {error} error An error object. Set to null if no error present + * @param {boolean} result The result of the authentication process + */ + +module.exports = MongoCR; diff --git a/node_modules/mongodb-core/lib/auth/plain.js b/node_modules/mongodb-core/lib/auth/plain.js new file mode 100644 index 0000000..70210cb --- /dev/null +++ b/node_modules/mongodb-core/lib/auth/plain.js @@ -0,0 +1,173 @@ +"use strict"; + +var f = require('util').format + , crypto = require('crypto') + , Binary = require('bson').Binary + , Query = require('../connection/commands').Query + , MongoError = require('../error'); + +var AuthSession = function(db, username, password) { + this.db = db; + this.username = username; + this.password = password; +} + +AuthSession.prototype.equal = function(session) { + return session.db == this.db + && session.username == this.username + && session.password == this.password; +} + +/** + * Creates a new Plain authentication mechanism + * @class + * @return {Plain} A cursor instance + */ +var Plain = function(bson) { + this.bson = bson; + this.authStore = []; +} + +/** + * Authenticate + * @method + * @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on + * @param {[]Connections} connections Connections to authenticate using this authenticator + * @param {string} db Name of the database + * @param {string} username Username + * @param {string} password Password + * @param {authResultCallback} callback The callback to return the result from the authentication + * @return {object} + */ +Plain.prototype.auth = function(server, connections, db, username, password, callback) { + var self = this; + // Total connections + var count = connections.length; + if(count == 0) return callback(null, null); + + // Valid connections + var numberOfValidConnections = 0; + var credentialsValid = false; + var errorObject = null; + + // For each connection we need to authenticate + while(connections.length > 0) { + // Execute MongoCR + var execute = function(connection) { + // Create payload + var payload = new Binary(f("\x00%s\x00%s", username, password)); + + // Let's start the sasl process + var command = { + saslStart: 1 + , mechanism: 'PLAIN' + , payload: payload + , autoAuthorize: 1 + }; + + // Let's start the process + server(connection, new Query(self.bson, "$external.$cmd", command, { + numberToSkip: 0, numberToReturn: 1 + }).toBin(), function(err, r) { + // server.command("$external.$cmd" + // , command + // , { connection: connection }, function(err, r) { + // Adjust count + count = count - 1; + + // If we have an error + if(err) { + errorObject = err; + } else if(r.result['$err']) { + errorObject = r.result; + } else if(r.result['errmsg']) { + errorObject = r.result; + } else { + credentialsValid = true; + numberOfValidConnections = numberOfValidConnections + 1; + } + + // We have authenticated all connections + if(count == 0 && numberOfValidConnections > 0) { + // Store the auth details + addAuthSession(self.authStore, new AuthSession(db, username, password)); + // Return correct authentication + callback(null, true); + } else if(count == 0) { + if(errorObject == null) errorObject = new MongoError(f("failed to authenticate using mongocr")); + callback(errorObject, false); + } + }); + } + + var _execute = function(_connection) { + process.nextTick(function() { + execute(_connection); + }); + } + + _execute(connections.shift()); + } +} + +// Add to store only if it does not exist +var addAuthSession = function(authStore, session) { + var found = false; + + for(var i = 0; i < authStore.length; i++) { + if(authStore[i].equal(session)) { + found = true; + break; + } + } + + if(!found) authStore.push(session); +} + +/** + * Remove authStore credentials + * @method + * @param {string} db Name of database we are removing authStore details about + * @return {object} + */ +Plain.prototype.logout = function(dbName) { + this.authStore = this.authStore.filter(function(x) { + return x.db != dbName; + }); +} + +/** + * Re authenticate pool + * @method + * @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on + * @param {[]Connections} connections Connections to authenticate using this authenticator + * @param {authResultCallback} callback The callback to return the result from the authentication + * @return {object} + */ +Plain.prototype.reauthenticate = function(server, connections, callback) { + var authStore = this.authStore.slice(0); + var err = null; + var count = authStore.length; + if(count == 0) return callback(null, null); + // Iterate over all the auth details stored + for(var i = 0; i < authStore.length; i++) { + this.auth(server, connections, authStore[i].db, authStore[i].username, authStore[i].password, function(err, r) { + if(err) err = err; + count = count - 1; + // Done re-authenticating + if(count == 0) { + callback(err, null); + } + }); + } +} + +/** + * This is a result from a authentication strategy + * + * @callback authResultCallback + * @param {error} error An error object. Set to null if no error present + * @param {boolean} result The result of the authentication process + */ + +module.exports = Plain; diff --git a/node_modules/mongodb-core/lib/auth/scram.js b/node_modules/mongodb-core/lib/auth/scram.js new file mode 100644 index 0000000..cdb6507 --- /dev/null +++ b/node_modules/mongodb-core/lib/auth/scram.js @@ -0,0 +1,345 @@ +"use strict"; + +var f = require('util').format + , crypto = require('crypto') + , Query = require('../connection/commands').Query + , Binary = require('bson').Binary + , MongoError = require('../error'); + +var AuthSession = function(db, username, password) { + this.db = db; + this.username = username; + this.password = password; +} + +AuthSession.prototype.equal = function(session) { + return session.db == this.db + && session.username == this.username + && session.password == this.password; +} + +var id = 0; + +/** + * Creates a new ScramSHA1 authentication mechanism + * @class + * @return {ScramSHA1} A cursor instance + */ +var ScramSHA1 = function(bson) { + this.bson = bson; + this.authStore = []; + this.id = id++; +} + +var parsePayload = function(payload) { + var dict = {}; + var parts = payload.split(','); + + for(var i = 0; i < parts.length; i++) { + var valueParts = parts[i].split('='); + dict[valueParts[0]] = valueParts[1]; + } + + return dict; +} + +var passwordDigest = function(username, password) { + if(typeof username != 'string') throw new MongoError("username must be a string"); + if(typeof password != 'string') throw new MongoError("password must be a string"); + if(password.length == 0) throw new MongoError("password cannot be empty"); + // Use node md5 generator + var md5 = crypto.createHash('md5'); + // Generate keys used for authentication + md5.update(username + ":mongo:" + password, 'utf8'); + return md5.digest('hex'); +} + +// XOR two buffers +var xor = function(a, b) { + if (!Buffer.isBuffer(a)) a = new Buffer(a) + if (!Buffer.isBuffer(b)) b = new Buffer(b) + var res = [] + if (a.length > b.length) { + for (var i = 0; i < b.length; i++) { + res.push(a[i] ^ b[i]) + } + } else { + for (var i = 0; i < a.length; i++) { + res.push(a[i] ^ b[i]) + } + } + return new Buffer(res); +} + +// Create a final digest +var hi = function(data, salt, iterations) { + // Create digest + var digest = function(msg) { + var hmac = crypto.createHmac('sha1', data); + hmac.update(msg); + return new Buffer(hmac.digest('base64'), 'base64'); + } + + // Create variables + salt = Buffer.concat([salt, new Buffer('\x00\x00\x00\x01')]) + var ui = digest(salt); + var u1 = ui; + + for(var i = 0; i < iterations - 1; i++) { + u1 = digest(u1); + ui = xor(ui, u1); + } + + return ui; +} + +/** + * Authenticate + * @method + * @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on + * @param {[]Connections} connections Connections to authenticate using this authenticator + * @param {string} db Name of the database + * @param {string} username Username + * @param {string} password Password + * @param {authResultCallback} callback The callback to return the result from the authentication + * @return {object} + */ +ScramSHA1.prototype.auth = function(server, connections, db, username, password, callback) { + var self = this; + // Total connections + var count = connections.length; + if(count == 0) return callback(null, null); + + // Valid connections + var numberOfValidConnections = 0; + var credentialsValid = false; + var errorObject = null; + + // Execute MongoCR + var executeScram = function(connection) { + // Clean up the user + username = username.replace('=', "=3D").replace(',', '=2C'); + + // Create a random nonce + var nonce = crypto.randomBytes(24).toString('base64'); + // var nonce = 'MsQUY9iw0T9fx2MUEz6LZPwGuhVvWAhc' + var firstBare = f("n=%s,r=%s", username, nonce); + + // Build command structure + var cmd = { + saslStart: 1 + , mechanism: 'SCRAM-SHA-1' + , payload: new Binary(f("n,,%s", firstBare)) + , autoAuthorize: 1 + } + + // Handle the error + var handleError = function(err, r) { + if(err) { + numberOfValidConnections = numberOfValidConnections - 1; + errorObject = err; return false; + } else if(r.result['$err']) { + errorObject = r.result; return false; + } else if(r.result['errmsg']) { + errorObject = r.result; return false; + } else { + credentialsValid = true; + numberOfValidConnections = numberOfValidConnections + 1; + } + + return true + } + + // Finish up + var finish = function(_count, _numberOfValidConnections) { + if(_count == 0 && _numberOfValidConnections > 0) { + // Store the auth details + addAuthSession(self.authStore, new AuthSession(db, username, password)); + // Return correct authentication + return callback(null, true); + } else if(_count == 0) { + if(errorObject == null) errorObject = new MongoError(f("failed to authenticate using scram")); + return callback(errorObject, false); + } + } + + var handleEnd = function(_err, _r) { + // Handle any error + handleError(_err, _r) + // Adjust the number of connections + count = count - 1; + // Execute the finish + finish(count, numberOfValidConnections); + } + + // Write the commmand on the connection + server(connection, new Query(self.bson, f("%s.$cmd", db), cmd, { + numberToSkip: 0, numberToReturn: 1 + }).toBin(), function(err, r) { + // Do we have an error, handle it + if(handleError(err, r) == false) { + count = count - 1; + + if(count == 0 && numberOfValidConnections > 0) { + // Store the auth details + addAuthSession(self.authStore, new AuthSession(db, username, password)); + // Return correct authentication + return callback(null, true); + } else if(count == 0) { + if(errorObject == null) errorObject = new MongoError(f("failed to authenticate using scram")); + return callback(errorObject, false); + } + + return; + } + + // Get the dictionary + var dict = parsePayload(r.result.payload.value()) + + // Unpack dictionary + var iterations = parseInt(dict.i, 10); + var salt = dict.s; + var rnonce = dict.r; + + // Set up start of proof + var withoutProof = f("c=biws,r=%s", rnonce); + var passwordDig = passwordDigest(username, password); + var saltedPassword = hi(passwordDig + , new Buffer(salt, 'base64') + , iterations); + + // Create the client key + var hmac = crypto.createHmac('sha1', saltedPassword); + hmac.update(new Buffer("Client Key")); + var clientKey = new Buffer(hmac.digest('base64'), 'base64'); + + // Create the stored key + var hash = crypto.createHash('sha1'); + hash.update(clientKey); + var storedKey = new Buffer(hash.digest('base64'), 'base64'); + + // Create the authentication message + var authMsg = [firstBare, r.result.payload.value().toString('base64'), withoutProof].join(','); + + // Create client signature + var hmac = crypto.createHmac('sha1', storedKey); + hmac.update(new Buffer(authMsg)); + var clientSig = new Buffer(hmac.digest('base64'), 'base64'); + + // Create client proof + var clientProof = f("p=%s", new Buffer(xor(clientKey, clientSig)).toString('base64')); + + // Create client final + var clientFinal = [withoutProof, clientProof].join(','); + + // Generate server key + var hmac = crypto.createHmac('sha1', saltedPassword); + hmac.update(new Buffer('Server Key')) + var serverKey = new Buffer(hmac.digest('base64'), 'base64'); + + // Generate server signature + var hmac = crypto.createHmac('sha1', serverKey); + hmac.update(new Buffer(authMsg)) + var serverSig = new Buffer(hmac.digest('base64'), 'base64'); + + // + // Create continue message + var cmd = { + saslContinue: 1 + , conversationId: r.result.conversationId + , payload: new Binary(new Buffer(clientFinal)) + } + + // + // Execute sasl continue + // Write the commmand on the connection + server(connection, new Query(self.bson, f("%s.$cmd", db), cmd, { + numberToSkip: 0, numberToReturn: 1 + }).toBin(), function(err, r) { + if(r && r.result.done == false) { + var cmd = { + saslContinue: 1 + , conversationId: r.result.conversationId + , payload: new Buffer(0) + } + + // Write the commmand on the connection + server(connection, new Query(self.bson, f("%s.$cmd", db), cmd, { + numberToSkip: 0, numberToReturn: 1 + }).toBin(), function(err, r) { + handleEnd(err, r); + }); + } else { + handleEnd(err, r); + } + }); + }); + } + + var _execute = function(_connection) { + process.nextTick(function() { + executeScram(_connection); + }); + } + + // For each connection we need to authenticate + while(connections.length > 0) { + _execute(connections.shift()); + } +} + +// Add to store only if it does not exist +var addAuthSession = function(authStore, session) { + var found = false; + + for(var i = 0; i < authStore.length; i++) { + if(authStore[i].equal(session)) { + found = true; + break; + } + } + + if(!found) authStore.push(session); +} + +/** + * Remove authStore credentials + * @method + * @param {string} db Name of database we are removing authStore details about + * @return {object} + */ +ScramSHA1.prototype.logout = function(dbName) { + this.authStore = this.authStore.filter(function(x) { + return x.db != dbName; + }); +} + +/** + * Re authenticate pool + * @method + * @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on + * @param {[]Connections} connections Connections to authenticate using this authenticator + * @param {authResultCallback} callback The callback to return the result from the authentication + * @return {object} + */ +ScramSHA1.prototype.reauthenticate = function(server, connections, callback) { + var authStore = this.authStore.slice(0); + var count = authStore.length; + var err = null; + // No connections + if(count == 0) return callback(null, null); + // Iterate over all the auth details stored + for(var i = 0; i < authStore.length; i++) { + this.auth(server, connections, authStore[i].db, authStore[i].username, authStore[i].password, function(err, r) { + if(err) err = err; + count = count - 1; + // Done re-authenticating + if(count == 0) { + callback(err, null); + } + }); + } +} + + +module.exports = ScramSHA1; diff --git a/node_modules/mongodb-core/lib/auth/sspi.js b/node_modules/mongodb-core/lib/auth/sspi.js new file mode 100644 index 0000000..60d744f --- /dev/null +++ b/node_modules/mongodb-core/lib/auth/sspi.js @@ -0,0 +1,267 @@ +"use strict"; + +var f = require('util').format + , crypto = require('crypto') + , require_optional = require('require_optional') + , Query = require('../connection/commands').Query + , MongoError = require('../error'); + +var AuthSession = function(db, username, password, options) { + this.db = db; + this.username = username; + this.password = password; + this.options = options; +} + +AuthSession.prototype.equal = function(session) { + return session.db == this.db + && session.username == this.username + && session.password == this.password; +} + +// Kerberos class +var Kerberos = null; +var MongoAuthProcess = null; + +// Try to grab the Kerberos class +try { + Kerberos = require_optional('kerberos').Kerberos + // Authentication process for Mongo + MongoAuthProcess = require_optional('kerberos').processes.MongoAuthProcess +} catch(err) {} + +/** + * Creates a new SSPI authentication mechanism + * @class + * @return {SSPI} A cursor instance + */ +var SSPI = function(bson) { + this.bson = bson; + this.authStore = []; +} + +/** + * Authenticate + * @method + * @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on + * @param {[]Connections} connections Connections to authenticate using this authenticator + * @param {string} db Name of the database + * @param {string} username Username + * @param {string} password Password + * @param {authResultCallback} callback The callback to return the result from the authentication + * @return {object} + */ +SSPI.prototype.auth = function(server, connections, db, username, password, options, callback) { + var self = this; + // We don't have the Kerberos library + if(Kerberos == null) return callback(new Error("Kerberos library is not installed")); + var gssapiServiceName = options['gssapiServiceName'] || 'mongodb'; + // Total connections + var count = connections.length; + if(count == 0) return callback(null, null); + + // Valid connections + var numberOfValidConnections = 0; + var credentialsValid = false; + var errorObject = null; + + // For each connection we need to authenticate + while(connections.length > 0) { + // Execute MongoCR + var execute = function(connection) { + // Start Auth process for a connection + SSIPAuthenticate(self, username, password, gssapiServiceName, server, connection, options, function(err, r) { + // Adjust count + count = count - 1; + + // If we have an error + if(err) { + errorObject = err; + } else if(r && typeof r == 'object' && r.result['$err']) { + errorObject = r.result; + } else if(r && typeof r == 'object' && r.result['errmsg']) { + errorObject = r.result; + } else { + credentialsValid = true; + numberOfValidConnections = numberOfValidConnections + 1; + } + + // We have authenticated all connections + if(count == 0 && numberOfValidConnections > 0) { + // Store the auth details + addAuthSession(self.authStore, new AuthSession(db, username, password, options)); + // Return correct authentication + callback(null, true); + } else if(count == 0) { + if(errorObject == null) errorObject = new MongoError(f("failed to authenticate using mongocr")); + callback(errorObject, false); + } + }); + } + + var _execute = function(_connection) { + process.nextTick(function() { + execute(_connection); + }); + } + + _execute(connections.shift()); + } +} + +var SSIPAuthenticate = function(self, username, password, gssapiServiceName, server, connection, options, callback) { + // Build Authentication command to send to MongoDB + var command = { + saslStart: 1 + , mechanism: 'GSSAPI' + , payload: '' + , autoAuthorize: 1 + }; + + // Create authenticator + var mongo_auth_process = new MongoAuthProcess(connection.host, connection.port, gssapiServiceName, options); + + // Execute first sasl step + server(connection, new Query(self.bson, "$external.$cmd", command, { + numberToSkip: 0, numberToReturn: 1 + }).toBin(), function(err, r) { + // server.command("$external.$cmd" + // , command + // , { connection: connection }, function(err, r) { + if(err) return callback(err, false); + var doc = r.result; + + mongo_auth_process.init(username, password, function(err) { + if(err) return callback(err); + + mongo_auth_process.transition(doc.payload, function(err, payload) { + if(err) return callback(err); + + // Perform the next step against mongod + var command = { + saslContinue: 1 + , conversationId: doc.conversationId + , payload: payload + }; + + // Execute the command + server(connection, new Query(self.bson, "$external.$cmd", command, { + numberToSkip: 0, numberToReturn: 1 + }).toBin(), function(err, r) { + // server.command("$external.$cmd" + // , command + // , { connection: connection }, function(err, r) { + if(err) return callback(err, false); + var doc = r.result; + + mongo_auth_process.transition(doc.payload, function(err, payload) { + if(err) return callback(err); + + // Perform the next step against mongod + var command = { + saslContinue: 1 + , conversationId: doc.conversationId + , payload: payload + }; + + // Execute the command + server(connection, new Query(self.bson, "$external.$cmd", command, { + numberToSkip: 0, numberToReturn: 1 + }).toBin(), function(err, r) { + // server.command("$external.$cmd" + // , command + // , { connection: connection }, function(err, r) { + if(err) return callback(err, false); + var doc = r.result; + + mongo_auth_process.transition(doc.payload, function(err, payload) { + // Perform the next step against mongod + var command = { + saslContinue: 1 + , conversationId: doc.conversationId + , payload: payload + }; + + // Execute the command + server(connection, new Query(self.bson, "$external.$cmd", command, { + numberToSkip: 0, numberToReturn: 1 + }).toBin(), function(err, r) { + // server.command("$external.$cmd" + // , command + // , { connection: connection }, function(err, r) { + if(err) return callback(err, false); + var doc = r.result; + + if(doc.done) return callback(null, true); + callback(new Error("Authentication failed"), false); + }); + }); + }); + }); + }); + }); + }); + }); +} + +// Add to store only if it does not exist +var addAuthSession = function(authStore, session) { + var found = false; + + for(var i = 0; i < authStore.length; i++) { + if(authStore[i].equal(session)) { + found = true; + break; + } + } + + if(!found) authStore.push(session); +} + +/** + * Remove authStore credentials + * @method + * @param {string} db Name of database we are removing authStore details about + * @return {object} + */ +SSPI.prototype.logout = function(dbName) { + this.authStore = this.authStore.filter(function(x) { + return x.db != dbName; + }); +} + +/** + * Re authenticate pool + * @method + * @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on + * @param {[]Connections} connections Connections to authenticate using this authenticator + * @param {authResultCallback} callback The callback to return the result from the authentication + * @return {object} + */ +SSPI.prototype.reauthenticate = function(server, connections, callback) { + var authStore = this.authStore.slice(0); + var err = null; + var count = authStore.length; + if(count == 0) return callback(null, null); + // Iterate over all the auth details stored + for(var i = 0; i < authStore.length; i++) { + this.auth(server, connections, authStore[i].db, authStore[i].username, authStore[i].password, authStore[i].options, function(err, r) { + if(err) err = err; + count = count - 1; + // Done re-authenticating + if(count == 0) { + callback(err, null); + } + }); + } +} + +/** + * This is a result from a authentication strategy + * + * @callback authResultCallback + * @param {error} error An error object. Set to null if no error present + * @param {boolean} result The result of the authentication process + */ + +module.exports = SSPI; diff --git a/node_modules/mongodb-core/lib/auth/x509.js b/node_modules/mongodb-core/lib/auth/x509.js new file mode 100644 index 0000000..b1b8287 --- /dev/null +++ b/node_modules/mongodb-core/lib/auth/x509.js @@ -0,0 +1,168 @@ +"use strict"; + +var f = require('util').format + , crypto = require('crypto') + , Query = require('../connection/commands').Query + , MongoError = require('../error'); + +var AuthSession = function(db, username, password) { + this.db = db; + this.username = username; + this.password = password; +} + +AuthSession.prototype.equal = function(session) { + return session.db == this.db + && session.username == this.username + && session.password == this.password; +} + +/** + * Creates a new X509 authentication mechanism + * @class + * @return {X509} A cursor instance + */ +var X509 = function(bson) { + this.bson = bson; + this.authStore = []; +} + +/** + * Authenticate + * @method + * @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on + * @param {[]Connections} connections Connections to authenticate using this authenticator + * @param {string} db Name of the database + * @param {string} username Username + * @param {string} password Password + * @param {authResultCallback} callback The callback to return the result from the authentication + * @return {object} + */ +X509.prototype.auth = function(server, connections, db, username, password, callback) { + var self = this; + // Total connections + var count = connections.length; + if(count == 0) return callback(null, null); + + // Valid connections + var numberOfValidConnections = 0; + var credentialsValid = false; + var errorObject = null; + + // For each connection we need to authenticate + while(connections.length > 0) { + // Execute MongoCR + var execute = function(connection) { + // Let's start the sasl process + var command = { + authenticate: 1 + , mechanism: 'MONGODB-X509' + , user: username + }; + + // Let's start the process + server(connection, new Query(self.bson, "$external.$cmd", command, { + numberToSkip: 0, numberToReturn: 1 + }).toBin(), function(err, r) { + // server.command("$external.$cmd" + // , command + // , { connection: connection }, function(err, r) { + // Adjust count + count = count - 1; + + // If we have an error + if(err) { + errorObject = err; + } else if(r.result['$err']) { + errorObject = r.result; + } else if(r.result['errmsg']) { + errorObject = r.result; + } else { + credentialsValid = true; + numberOfValidConnections = numberOfValidConnections + 1; + } + + // We have authenticated all connections + if(count == 0 && numberOfValidConnections > 0) { + // Store the auth details + addAuthSession(self.authStore, new AuthSession(db, username, password)); + // Return correct authentication + callback(null, true); + } else if(count == 0) { + if(errorObject == null) errorObject = new MongoError(f("failed to authenticate using mongocr")); + callback(errorObject, false); + } + }); + } + + var _execute = function(_connection) { + process.nextTick(function() { + execute(_connection); + }); + } + + _execute(connections.shift()); + } +} + +// Add to store only if it does not exist +var addAuthSession = function(authStore, session) { + var found = false; + + for(var i = 0; i < authStore.length; i++) { + if(authStore[i].equal(session)) { + found = true; + break; + } + } + + if(!found) authStore.push(session); +} + +/** + * Remove authStore credentials + * @method + * @param {string} db Name of database we are removing authStore details about + * @return {object} + */ +X509.prototype.logout = function(dbName) { + this.authStore = this.authStore.filter(function(x) { + return x.db != dbName; + }); +} + +/** + * Re authenticate pool + * @method + * @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on + * @param {[]Connections} connections Connections to authenticate using this authenticator + * @param {authResultCallback} callback The callback to return the result from the authentication + * @return {object} + */ +X509.prototype.reauthenticate = function(server, connections, callback) { + var authStore = this.authStore.slice(0); + var err = null; + var count = authStore.length; + if(count == 0) return callback(null, null); + // Iterate over all the auth details stored + for(var i = 0; i < authStore.length; i++) { + this.auth(server, connections, authStore[i].db, authStore[i].username, authStore[i].password, function(err, r) { + if(err) err = err; + count = count - 1; + // Done re-authenticating + if(count == 0) { + callback(err, null); + } + }); + } +} + +/** + * This is a result from a authentication strategy + * + * @callback authResultCallback + * @param {error} error An error object. Set to null if no error present + * @param {boolean} result The result of the authentication process + */ + +module.exports = X509; diff --git a/node_modules/mongodb-core/lib/connection/command_result.js b/node_modules/mongodb-core/lib/connection/command_result.js new file mode 100644 index 0000000..eb7b27a --- /dev/null +++ b/node_modules/mongodb-core/lib/connection/command_result.js @@ -0,0 +1,38 @@ +"use strict"; + +var setProperty = require('../connection/utils').setProperty + , getProperty = require('../connection/utils').getProperty + , getSingleProperty = require('../connection/utils').getSingleProperty; + +/** + * Creates a new CommandResult instance + * @class + * @param {object} result CommandResult object + * @param {Connection} connection A connection instance associated with this result + * @return {CommandResult} A cursor instance + */ +var CommandResult = function(result, connection, message) { + this.result = result; + this.connection = connection; + this.message = message; +} + +/** + * Convert CommandResult to JSON + * @method + * @return {object} + */ +CommandResult.prototype.toJSON = function() { + return this.result; +} + +/** + * Convert CommandResult to String representation + * @method + * @return {string} + */ +CommandResult.prototype.toString = function() { + return JSON.stringify(this.toJSON()); +} + +module.exports = CommandResult; diff --git a/node_modules/mongodb-core/lib/connection/commands.js b/node_modules/mongodb-core/lib/connection/commands.js new file mode 100644 index 0000000..dba0d1e --- /dev/null +++ b/node_modules/mongodb-core/lib/connection/commands.js @@ -0,0 +1,539 @@ +"use strict"; + +var f = require('util').format + , Long = require('bson').Long + , setProperty = require('./utils').setProperty + , getProperty = require('./utils').getProperty + , getSingleProperty = require('./utils').getSingleProperty; + +// Incrementing request id +var _requestId = 0; + +// Wire command operation ids +var OP_QUERY = 2004; +var OP_GETMORE = 2005; +var OP_KILL_CURSORS = 2007; + +// Query flags +var OPTS_NONE = 0; +var OPTS_TAILABLE_CURSOR = 2; +var OPTS_SLAVE = 4; +var OPTS_OPLOG_REPLAY = 8; +var OPTS_NO_CURSOR_TIMEOUT = 16; +var OPTS_AWAIT_DATA = 32; +var OPTS_EXHAUST = 64; +var OPTS_PARTIAL = 128; + +// Response flags +var CURSOR_NOT_FOUND = 0; +var QUERY_FAILURE = 2; +var SHARD_CONFIG_STALE = 4; +var AWAIT_CAPABLE = 8; + +/************************************************************** + * QUERY + **************************************************************/ +var Query = function(bson, ns, query, options) { + var self = this; + // Basic options needed to be passed in + if(ns == null) throw new Error("ns must be specified for query"); + if(query == null) throw new Error("query must be specified for query"); + + // Validate that we are not passing 0x00 in the colletion name + if(!!~ns.indexOf("\x00")) { + throw new Error("namespace cannot contain a null character"); + } + + // Basic options + this.bson = bson; + this.ns = ns; + this.query = query; + + // Ensure empty options + this.options = options || {}; + + // Additional options + this.numberToSkip = options.numberToSkip || 0; + this.numberToReturn = options.numberToReturn || 0; + this.returnFieldSelector = options.returnFieldSelector || null; + this.requestId = _requestId++; + + // Serialization option + this.serializeFunctions = typeof options.serializeFunctions == 'boolean' ? options.serializeFunctions : false; + this.ignoreUndefined = typeof options.ignoreUndefined == 'boolean' ? options.ignoreUndefined : false; + this.maxBsonSize = options.maxBsonSize || 1024 * 1024 * 16; + this.checkKeys = typeof options.checkKeys == 'boolean' ? options.checkKeys : true; + this.batchSize = self.numberToReturn; + + // Flags + this.tailable = false; + this.slaveOk = typeof options.slaveOk == 'boolean'? options.slaveOk : false; + this.oplogReplay = false; + this.noCursorTimeout = false; + this.awaitData = false; + this.exhaust = false; + this.partial = false; +} + +// +// Assign a new request Id +Query.prototype.incRequestId = function() { + this.requestId = _requestId++; +} + +// +// Assign a new request Id +Query.nextRequestId = function() { + return _requestId + 1; +} + +// +// Uses a single allocated buffer for the process, avoiding multiple memory allocations +Query.prototype.toBin = function() { + var self = this; + var buffers = []; + var projection = null; + + // Set up the flags + var flags = 0; + if(this.tailable) { + flags |= OPTS_TAILABLE_CURSOR; + } + + if(this.slaveOk) { + flags |= OPTS_SLAVE; + } + + if(this.oplogReplay) { + flags |= OPTS_OPLOG_REPLAY; + } + + if(this.noCursorTimeout) { + flags |= OPTS_NO_CURSOR_TIMEOUT; + } + + if(this.awaitData) { + flags |= OPTS_AWAIT_DATA; + } + + if(this.exhaust) { + flags |= OPTS_EXHAUST; + } + + if(this.partial) { + flags |= OPTS_PARTIAL; + } + + // If batchSize is different to self.numberToReturn + if(self.batchSize != self.numberToReturn) self.numberToReturn = self.batchSize; + + // Allocate write protocol header buffer + var header = new Buffer( + 4 * 4 // Header + + 4 // Flags + + Buffer.byteLength(self.ns) + 1 // namespace + + 4 // numberToSkip + + 4 // numberToReturn + ); + + // Add header to buffers + buffers.push(header); + + // Serialize the query + var query = self.bson.serialize(this.query + , this.checkKeys + , true + , this.serializeFunctions + , 0, this.ignoreUndefined); + + // Add query document + buffers.push(query); + + if(self.returnFieldSelector && Object.keys(self.returnFieldSelector).length > 0) { + // Serialize the projection document + projection = self.bson.serialize(this.returnFieldSelector, this.checkKeys, true, this.serializeFunctions, this.ignoreUndefined); + // Add projection document + buffers.push(projection); + } + + // Total message size + var totalLength = header.length + query.length + (projection ? projection.length : 0); + + // Set up the index + var index = 4; + + // Write total document length + header[3] = (totalLength >> 24) & 0xff; + header[2] = (totalLength >> 16) & 0xff; + header[1] = (totalLength >> 8) & 0xff; + header[0] = (totalLength) & 0xff; + + // Write header information requestId + header[index + 3] = (this.requestId >> 24) & 0xff; + header[index + 2] = (this.requestId >> 16) & 0xff; + header[index + 1] = (this.requestId >> 8) & 0xff; + header[index] = (this.requestId) & 0xff; + index = index + 4; + + // Write header information responseTo + header[index + 3] = (0 >> 24) & 0xff; + header[index + 2] = (0 >> 16) & 0xff; + header[index + 1] = (0 >> 8) & 0xff; + header[index] = (0) & 0xff; + index = index + 4; + + // Write header information OP_QUERY + header[index + 3] = (OP_QUERY >> 24) & 0xff; + header[index + 2] = (OP_QUERY >> 16) & 0xff; + header[index + 1] = (OP_QUERY >> 8) & 0xff; + header[index] = (OP_QUERY) & 0xff; + index = index + 4; + + // Write header information flags + header[index + 3] = (flags >> 24) & 0xff; + header[index + 2] = (flags >> 16) & 0xff; + header[index + 1] = (flags >> 8) & 0xff; + header[index] = (flags) & 0xff; + index = index + 4; + + // Write collection name + index = index + header.write(this.ns, index, 'utf8') + 1; + header[index - 1] = 0; + + // Write header information flags numberToSkip + header[index + 3] = (this.numberToSkip >> 24) & 0xff; + header[index + 2] = (this.numberToSkip >> 16) & 0xff; + header[index + 1] = (this.numberToSkip >> 8) & 0xff; + header[index] = (this.numberToSkip) & 0xff; + index = index + 4; + + // Write header information flags numberToReturn + header[index + 3] = (this.numberToReturn >> 24) & 0xff; + header[index + 2] = (this.numberToReturn >> 16) & 0xff; + header[index + 1] = (this.numberToReturn >> 8) & 0xff; + header[index] = (this.numberToReturn) & 0xff; + index = index + 4; + + // Return the buffers + return buffers; +} + +Query.getRequestId = function() { + return ++_requestId; +} + +/************************************************************** + * GETMORE + **************************************************************/ +var GetMore = function(bson, ns, cursorId, opts) { + opts = opts || {}; + this.numberToReturn = opts.numberToReturn || 0; + this.requestId = _requestId++; + this.bson = bson; + this.ns = ns; + this.cursorId = cursorId; +} + +// +// Uses a single allocated buffer for the process, avoiding multiple memory allocations +GetMore.prototype.toBin = function() { + var length = 4 + Buffer.byteLength(this.ns) + 1 + 4 + 8 + (4 * 4); + // Create command buffer + var index = 0; + // Allocate buffer + var _buffer = new Buffer(length); + + // Write header information + // index = write32bit(index, _buffer, length); + _buffer[index + 3] = (length >> 24) & 0xff; + _buffer[index + 2] = (length >> 16) & 0xff; + _buffer[index + 1] = (length >> 8) & 0xff; + _buffer[index] = (length) & 0xff; + index = index + 4; + + // index = write32bit(index, _buffer, requestId); + _buffer[index + 3] = (this.requestId >> 24) & 0xff; + _buffer[index + 2] = (this.requestId >> 16) & 0xff; + _buffer[index + 1] = (this.requestId >> 8) & 0xff; + _buffer[index] = (this.requestId) & 0xff; + index = index + 4; + + // index = write32bit(index, _buffer, 0); + _buffer[index + 3] = (0 >> 24) & 0xff; + _buffer[index + 2] = (0 >> 16) & 0xff; + _buffer[index + 1] = (0 >> 8) & 0xff; + _buffer[index] = (0) & 0xff; + index = index + 4; + + // index = write32bit(index, _buffer, OP_GETMORE); + _buffer[index + 3] = (OP_GETMORE >> 24) & 0xff; + _buffer[index + 2] = (OP_GETMORE >> 16) & 0xff; + _buffer[index + 1] = (OP_GETMORE >> 8) & 0xff; + _buffer[index] = (OP_GETMORE) & 0xff; + index = index + 4; + + // index = write32bit(index, _buffer, 0); + _buffer[index + 3] = (0 >> 24) & 0xff; + _buffer[index + 2] = (0 >> 16) & 0xff; + _buffer[index + 1] = (0 >> 8) & 0xff; + _buffer[index] = (0) & 0xff; + index = index + 4; + + // Write collection name + index = index + _buffer.write(this.ns, index, 'utf8') + 1; + _buffer[index - 1] = 0; + + // Write batch size + // index = write32bit(index, _buffer, numberToReturn); + _buffer[index + 3] = (this.numberToReturn >> 24) & 0xff; + _buffer[index + 2] = (this.numberToReturn >> 16) & 0xff; + _buffer[index + 1] = (this.numberToReturn >> 8) & 0xff; + _buffer[index] = (this.numberToReturn) & 0xff; + index = index + 4; + + // Write cursor id + // index = write32bit(index, _buffer, cursorId.getLowBits()); + _buffer[index + 3] = (this.cursorId.getLowBits() >> 24) & 0xff; + _buffer[index + 2] = (this.cursorId.getLowBits() >> 16) & 0xff; + _buffer[index + 1] = (this.cursorId.getLowBits() >> 8) & 0xff; + _buffer[index] = (this.cursorId.getLowBits()) & 0xff; + index = index + 4; + + // index = write32bit(index, _buffer, cursorId.getHighBits()); + _buffer[index + 3] = (this.cursorId.getHighBits() >> 24) & 0xff; + _buffer[index + 2] = (this.cursorId.getHighBits() >> 16) & 0xff; + _buffer[index + 1] = (this.cursorId.getHighBits() >> 8) & 0xff; + _buffer[index] = (this.cursorId.getHighBits()) & 0xff; + index = index + 4; + + // Return buffer + return _buffer; +} + +/************************************************************** + * KILLCURSOR + **************************************************************/ +var KillCursor = function(bson, cursorIds) { + this.requestId = _requestId++; + this.cursorIds = cursorIds; +} + +// +// Uses a single allocated buffer for the process, avoiding multiple memory allocations +KillCursor.prototype.toBin = function() { + var length = 4 + 4 + (4 * 4) + (this.cursorIds.length * 8); + + // Create command buffer + var index = 0; + var _buffer = new Buffer(length); + + // Write header information + // index = write32bit(index, _buffer, length); + _buffer[index + 3] = (length >> 24) & 0xff; + _buffer[index + 2] = (length >> 16) & 0xff; + _buffer[index + 1] = (length >> 8) & 0xff; + _buffer[index] = (length) & 0xff; + index = index + 4; + + // index = write32bit(index, _buffer, requestId); + _buffer[index + 3] = (this.requestId >> 24) & 0xff; + _buffer[index + 2] = (this.requestId >> 16) & 0xff; + _buffer[index + 1] = (this.requestId >> 8) & 0xff; + _buffer[index] = (this.requestId) & 0xff; + index = index + 4; + + // index = write32bit(index, _buffer, 0); + _buffer[index + 3] = (0 >> 24) & 0xff; + _buffer[index + 2] = (0 >> 16) & 0xff; + _buffer[index + 1] = (0 >> 8) & 0xff; + _buffer[index] = (0) & 0xff; + index = index + 4; + + // index = write32bit(index, _buffer, OP_KILL_CURSORS); + _buffer[index + 3] = (OP_KILL_CURSORS >> 24) & 0xff; + _buffer[index + 2] = (OP_KILL_CURSORS >> 16) & 0xff; + _buffer[index + 1] = (OP_KILL_CURSORS >> 8) & 0xff; + _buffer[index] = (OP_KILL_CURSORS) & 0xff; + index = index + 4; + + // index = write32bit(index, _buffer, 0); + _buffer[index + 3] = (0 >> 24) & 0xff; + _buffer[index + 2] = (0 >> 16) & 0xff; + _buffer[index + 1] = (0 >> 8) & 0xff; + _buffer[index] = (0) & 0xff; + index = index + 4; + + // Write batch size + // index = write32bit(index, _buffer, this.cursorIds.length); + _buffer[index + 3] = (this.cursorIds.length >> 24) & 0xff; + _buffer[index + 2] = (this.cursorIds.length >> 16) & 0xff; + _buffer[index + 1] = (this.cursorIds.length >> 8) & 0xff; + _buffer[index] = (this.cursorIds.length) & 0xff; + index = index + 4; + + // Write all the cursor ids into the array + for(var i = 0; i < this.cursorIds.length; i++) { + // Write cursor id + // index = write32bit(index, _buffer, cursorIds[i].getLowBits()); + _buffer[index + 3] = (this.cursorIds[i].getLowBits() >> 24) & 0xff; + _buffer[index + 2] = (this.cursorIds[i].getLowBits() >> 16) & 0xff; + _buffer[index + 1] = (this.cursorIds[i].getLowBits() >> 8) & 0xff; + _buffer[index] = (this.cursorIds[i].getLowBits()) & 0xff; + index = index + 4; + + // index = write32bit(index, _buffer, cursorIds[i].getHighBits()); + _buffer[index + 3] = (this.cursorIds[i].getHighBits() >> 24) & 0xff; + _buffer[index + 2] = (this.cursorIds[i].getHighBits() >> 16) & 0xff; + _buffer[index + 1] = (this.cursorIds[i].getHighBits() >> 8) & 0xff; + _buffer[index] = (this.cursorIds[i].getHighBits()) & 0xff; + index = index + 4; + } + + // Return buffer + return _buffer; +} + +var Response = function(bson, data, opts) { + opts = opts || {promoteLongs: true}; + this.parsed = false; + + // + // Parse Header + // + this.index = 0; + this.raw = data; + this.data = data; + this.bson = bson; + this.opts = opts; + + // Read the message length + this.length = data[this.index] | data[this.index + 1] << 8 | data[this.index + 2] << 16 | data[this.index + 3] << 24; + this.index = this.index + 4; + + // Fetch the request id for this reply + this.requestId = data[this.index] | data[this.index + 1] << 8 | data[this.index + 2] << 16 | data[this.index + 3] << 24; + this.index = this.index + 4; + + // Fetch the id of the request that triggered the response + this.responseTo = data[this.index] | data[this.index + 1] << 8 | data[this.index + 2] << 16 | data[this.index + 3] << 24; + this.index = this.index + 4; + + // Skip op-code field + this.index = this.index + 4; + + // Unpack flags + this.responseFlags = data[this.index] | data[this.index + 1] << 8 | data[this.index + 2] << 16 | data[this.index + 3] << 24; + this.index = this.index + 4; + + // Unpack the cursor + var lowBits = data[this.index] | data[this.index + 1] << 8 | data[this.index + 2] << 16 | data[this.index + 3] << 24; + this.index = this.index + 4; + var highBits = data[this.index] | data[this.index + 1] << 8 | data[this.index + 2] << 16 | data[this.index + 3] << 24; + this.index = this.index + 4; + // Create long object + this.cursorId = new Long(lowBits, highBits); + + // Unpack the starting from + this.startingFrom = data[this.index] | data[this.index + 1] << 8 | data[this.index + 2] << 16 | data[this.index + 3] << 24; + this.index = this.index + 4; + + // Unpack the number of objects returned + this.numberReturned = data[this.index] | data[this.index + 1] << 8 | data[this.index + 2] << 16 | data[this.index + 3] << 24; + this.index = this.index + 4; + + // Preallocate document array + this.documents = new Array(this.numberReturned); + + // Flag values + this.cursorNotFound = (this.responseFlags & CURSOR_NOT_FOUND) != 0; + this.queryFailure = (this.responseFlags & QUERY_FAILURE) != 0; + this.shardConfigStale = (this.responseFlags & SHARD_CONFIG_STALE) != 0; + this.awaitCapable = (this.responseFlags & AWAIT_CAPABLE) != 0; + this.promoteLongs = typeof opts.promoteLongs == 'boolean' ? opts.promoteLongs : true; +} + +Response.prototype.isParsed = function() { + return this.parsed; +} + +// Validation buffers +var firstBatch = new Buffer('firstBatch', 'utf8'); +var nextBatch = new Buffer('nextBatch', 'utf8'); +var cursorId = new Buffer('id', 'utf8').toString('hex'); + +var documentBuffers = { + firstBatch: firstBatch.toString('hex'), + nextBatch: nextBatch.toString('hex') +}; + +Response.prototype.parse = function(options) { + // Don't parse again if not needed + if(this.parsed) return; + options = options || {}; + + // Allow the return of raw documents instead of parsing + var raw = options.raw || false; + var documentsReturnedIn = options.documentsReturnedIn || null; + + // + // Single document and documentsReturnedIn set + // + if(this.numberReturned == 1 && documentsReturnedIn != null && raw) { + // Calculate the bson size + var bsonSize = this.data[this.index] | this.data[this.index + 1] << 8 | this.data[this.index + 2] << 16 | this.data[this.index + 3] << 24; + // Slice out the buffer containing the command result document + var document = this.data.slice(this.index, this.index + bsonSize); + // Set up field we wish to keep as raw + var fieldsAsRaw = {} + fieldsAsRaw[documentsReturnedIn] = true; + // Set up the options + var _options = {promoteLongs: this.opts.promoteLongs, fieldsAsRaw: fieldsAsRaw}; + + // Deserialize but keep the array of documents in non-parsed form + var doc = this.bson.deserialize(document, _options); + + // Get the documents + this.documents = doc.cursor[documentsReturnedIn]; + this.numberReturned = this.documents.length; + // Ensure we have a Long valie cursor id + this.cursorId = typeof doc.cursor.id == 'number' + ? Long.fromNumber(doc.cursor.id) + : doc.cursor.id; + + // Adjust the index + this.index = this.index + bsonSize; + + // Set as parsed + this.parsed = true + return; + } + + // + // Parse Body + // + for(var i = 0; i < this.numberReturned; i++) { + var bsonSize = this.data[this.index] | this.data[this.index + 1] << 8 | this.data[this.index + 2] << 16 | this.data[this.index + 3] << 24; + // Parse options + var _options = {promoteLongs: this.opts.promoteLongs}; + + // If we have raw results specified slice the return document + if(raw) { + this.documents[i] = this.data.slice(this.index, this.index + bsonSize); + } else { + this.documents[i] = this.bson.deserialize(this.data.slice(this.index, this.index + bsonSize), _options); + } + + // Adjust the index + this.index = this.index + bsonSize; + } + + // Set parsed + this.parsed = true; +} + +module.exports = { + Query: Query + , GetMore: GetMore + , Response: Response + , KillCursor: KillCursor +} diff --git a/node_modules/mongodb-core/lib/connection/connection.js b/node_modules/mongodb-core/lib/connection/connection.js new file mode 100644 index 0000000..b44c4e0 --- /dev/null +++ b/node_modules/mongodb-core/lib/connection/connection.js @@ -0,0 +1,531 @@ +"use strict"; + +var inherits = require('util').inherits + , EventEmitter = require('events').EventEmitter + , net = require('net') + , tls = require('tls') + , f = require('util').format + , debugOptions = require('./utils').debugOptions + , Response = require('./commands').Response + , MongoError = require('../error') + , Logger = require('./logger'); + +var _id = 0; +var debugFields = ['host', 'port', 'size', 'keepAlive', 'keepAliveInitialDelay', 'noDelay' + , 'connectionTimeout', 'socketTimeout', 'singleBufferSerializtion', 'ssl', 'ca', 'cert' + , 'rejectUnauthorized', 'promoteLongs', 'checkServerIdentity']; +var connectionAccounting = false; +var connections = {}; + +/** + * Creates a new Connection instance + * @class + * @param {string} options.host The server host + * @param {number} options.port The server port + * @param {boolean} [options.keepAlive=true] TCP Connection keep alive enabled + * @param {number} [options.keepAliveInitialDelay=0] Initial delay before TCP keep alive enabled + * @param {boolean} [options.noDelay=true] TCP Connection no delay + * @param {number} [options.connectionTimeout=0] TCP Connection timeout setting + * @param {number} [options.socketTimeout=0] TCP Socket timeout setting + * @param {boolean} [options.singleBufferSerializtion=true] Serialize into single buffer, trade of peak memory for serialization speed + * @param {boolean} [options.ssl=false] Use SSL for connection + * @param {boolean|function} [options.checkServerIdentity=true] Ensure we check server identify during SSL, set to false to disable checking. Only works for Node 0.12.x or higher. You can pass in a boolean or your own checkServerIdentity override function. + * @param {Buffer} [options.ca] SSL Certificate store binary buffer + * @param {Buffer} [options.cert] SSL Certificate binary buffer + * @param {Buffer} [options.key] SSL Key file binary buffer + * @param {string} [options.passphrase] SSL Certificate pass phrase + * @param {boolean} [options.rejectUnauthorized=true] Reject unauthorized server certificates + * @param {boolean} [options.promoteLongs=true] Convert Long values from the db into Numbers if they fit into 53 bits + * @fires Connection#connect + * @fires Connection#close + * @fires Connection#error + * @fires Connection#timeout + * @fires Connection#parseError + * @return {Connection} A cursor instance + */ +var Connection = function(messageHandler, options) { + // Add event listener + EventEmitter.call(this); + // Set empty if no options passed + this.options = options || {}; + // Identification information + this.id = _id++; + // Logger instance + this.logger = Logger('Connection', options); + // No bson parser passed in + if(!options.bson) throw new Error("must pass in valid bson parser"); + // Get bson parser + this.bson = options.bson; + // Grouping tag used for debugging purposes + this.tag = options.tag; + // Message handler + this.messageHandler = messageHandler; + + // Max BSON message size + this.maxBsonMessageSize = options.maxBsonMessageSize || (1024 * 1024 * 16 * 4); + // Debug information + if(this.logger.isDebug()) this.logger.debug(f('creating connection %s with options [%s]', this.id, JSON.stringify(debugOptions(debugFields, options)))); + + // Default options + this.port = options.port || 27017; + this.host = options.host || 'localhost'; + this.keepAlive = typeof options.keepAlive == 'boolean' ? options.keepAlive : true; + this.keepAliveInitialDelay = options.keepAliveInitialDelay || 0; + this.noDelay = typeof options.noDelay == 'boolean' ? options.noDelay : true; + this.connectionTimeout = options.connectionTimeout || 0; + this.socketTimeout = options.socketTimeout || 0; + + // If connection was destroyed + this.destroyed = false; + + // Check if we have a domain socket + this.domainSocket = this.host.indexOf('\/') != -1; + + // Serialize commands using function + this.singleBufferSerializtion = typeof options.singleBufferSerializtion == 'boolean' ? options.singleBufferSerializtion : true; + this.serializationFunction = this.singleBufferSerializtion ? 'toBinUnified' : 'toBin'; + + // SSL options + this.ca = options.ca || null; + this.cert = options.cert || null; + this.key = options.key || null; + this.passphrase = options.passphrase || null; + this.ssl = typeof options.ssl == 'boolean' ? options.ssl : false; + this.rejectUnauthorized = typeof options.rejectUnauthorized == 'boolean' ? options.rejectUnauthorized : true; + this.checkServerIdentity = typeof options.checkServerIdentity == 'boolean' + || typeof options.checkServerIdentity == 'function' ? options.checkServerIdentity : true; + + // If ssl not enabled + if(!this.ssl) this.rejectUnauthorized = false; + + // Response options + this.responseOptions = { + promoteLongs: typeof options.promoteLongs == 'boolean' ? options.promoteLongs : true + } + + // Flushing + this.flushing = false; + this.queue = []; + + // Internal state + this.connection = null; + this.writeStream = null; +} + +inherits(Connection, EventEmitter); + +Connection.prototype.setSocketTimeout = function(value) { + if(this.connection) { + this.connection.setTimeout(value); + } +} + +Connection.prototype.resetSocketTimeout = function(value) { + if(this.connection) { + this.connection.setTimeout(this.socketTimeout);; + } +} + +Connection.enableConnectionAccounting = function() { + connectionAccounting = true; + connections = {}; +} + +Connection.disableConnectionAccounting = function() { + connectionAccounting = false; +} + +Connection.connections = function() { + return connections; +} + +// +// Connection handlers +var errorHandler = function(self) { + return function(err) { + if(connectionAccounting) delete connections[self.id]; + // Debug information + if(self.logger.isDebug()) self.logger.debug(f('connection %s for [%s:%s] errored out with [%s]', self.id, self.host, self.port, JSON.stringify(err))); + // Emit the error + if(self.listeners('error').length > 0) self.emit("error", MongoError.create(err), self); + } +} + +var timeoutHandler = function(self) { + return function(err) { + if(connectionAccounting) delete connections[self.id]; + // Debug information + if(self.logger.isDebug()) self.logger.debug(f('connection %s for [%s:%s] timed out', self.id, self.host, self.port)); + // Emit timeout error + self.emit("timeout" + , MongoError.create(f("connection %s to %s:%s timed out", self.id, self.host, self.port)) + , self); + } +} + +var closeHandler = function(self) { + return function(hadError) { + if(connectionAccounting) delete connections[self.id]; + // Debug information + if(self.logger.isDebug()) self.logger.debug(f('connection %s with for [%s:%s] closed', self.id, self.host, self.port)); + + // Emit close event + if(!hadError) { + self.emit("close" + , MongoError.create(f("connection %s to %s:%s closed", self.id, self.host, self.port)) + , self); + } + } +} + +var dataHandler = function(self) { + return function(data) { + // Parse until we are done with the data + while(data.length > 0) { + // If we still have bytes to read on the current message + if(self.bytesRead > 0 && self.sizeOfMessage > 0) { + // Calculate the amount of remaining bytes + var remainingBytesToRead = self.sizeOfMessage - self.bytesRead; + // Check if the current chunk contains the rest of the message + if(remainingBytesToRead > data.length) { + // Copy the new data into the exiting buffer (should have been allocated when we know the message size) + data.copy(self.buffer, self.bytesRead); + // Adjust the number of bytes read so it point to the correct index in the buffer + self.bytesRead = self.bytesRead + data.length; + + // Reset state of buffer + data = new Buffer(0); + } else { + // Copy the missing part of the data into our current buffer + data.copy(self.buffer, self.bytesRead, 0, remainingBytesToRead); + // Slice the overflow into a new buffer that we will then re-parse + data = data.slice(remainingBytesToRead); + + // Emit current complete message + try { + var emitBuffer = self.buffer; + // Reset state of buffer + self.buffer = null; + self.sizeOfMessage = 0; + self.bytesRead = 0; + self.stubBuffer = null; + // Emit the buffer + self.messageHandler(new Response(self.bson, emitBuffer, self.responseOptions), self); + } catch(err) { + var errorObject = {err:"socketHandler", trace:err, bin:self.buffer, parseState:{ + sizeOfMessage:self.sizeOfMessage, + bytesRead:self.bytesRead, + stubBuffer:self.stubBuffer}}; + // We got a parse Error fire it off then keep going + self.emit("parseError", errorObject, self); + } + } + } else { + // Stub buffer is kept in case we don't get enough bytes to determine the + // size of the message (< 4 bytes) + if(self.stubBuffer != null && self.stubBuffer.length > 0) { + // If we have enough bytes to determine the message size let's do it + if(self.stubBuffer.length + data.length > 4) { + // Prepad the data + var newData = new Buffer(self.stubBuffer.length + data.length); + self.stubBuffer.copy(newData, 0); + data.copy(newData, self.stubBuffer.length); + // Reassign for parsing + data = newData; + + // Reset state of buffer + self.buffer = null; + self.sizeOfMessage = 0; + self.bytesRead = 0; + self.stubBuffer = null; + + } else { + + // Add the the bytes to the stub buffer + var newStubBuffer = new Buffer(self.stubBuffer.length + data.length); + // Copy existing stub buffer + self.stubBuffer.copy(newStubBuffer, 0); + // Copy missing part of the data + data.copy(newStubBuffer, self.stubBuffer.length); + // Exit parsing loop + data = new Buffer(0); + } + } else { + if(data.length > 4) { + // Retrieve the message size + // var sizeOfMessage = data.readUInt32LE(0); + var sizeOfMessage = data[0] | data[1] << 8 | data[2] << 16 | data[3] << 24; + // If we have a negative sizeOfMessage emit error and return + if(sizeOfMessage < 0 || sizeOfMessage > self.maxBsonMessageSize) { + var errorObject = {err:"socketHandler", trace:'', bin:self.buffer, parseState:{ + sizeOfMessage: sizeOfMessage, + bytesRead: self.bytesRead, + stubBuffer: self.stubBuffer}}; + // We got a parse Error fire it off then keep going + self.emit("parseError", errorObject, self); + return; + } + + // Ensure that the size of message is larger than 0 and less than the max allowed + if(sizeOfMessage > 4 && sizeOfMessage < self.maxBsonMessageSize && sizeOfMessage > data.length) { + self.buffer = new Buffer(sizeOfMessage); + // Copy all the data into the buffer + data.copy(self.buffer, 0); + // Update bytes read + self.bytesRead = data.length; + // Update sizeOfMessage + self.sizeOfMessage = sizeOfMessage; + // Ensure stub buffer is null + self.stubBuffer = null; + // Exit parsing loop + data = new Buffer(0); + + } else if(sizeOfMessage > 4 && sizeOfMessage < self.maxBsonMessageSize && sizeOfMessage == data.length) { + try { + var emitBuffer = data; + // Reset state of buffer + self.buffer = null; + self.sizeOfMessage = 0; + self.bytesRead = 0; + self.stubBuffer = null; + // Exit parsing loop + data = new Buffer(0); + // Emit the message + self.messageHandler(new Response(self.bson, emitBuffer, self.responseOptions), self); + } catch (err) { + self.emit("parseError", err, self); + } + } else if(sizeOfMessage <= 4 || sizeOfMessage > self.maxBsonMessageSize) { + var errorObject = {err:"socketHandler", trace:null, bin:data, parseState:{ + sizeOfMessage:sizeOfMessage, + bytesRead:0, + buffer:null, + stubBuffer:null}}; + // We got a parse Error fire it off then keep going + self.emit("parseError", errorObject, self); + + // Clear out the state of the parser + self.buffer = null; + self.sizeOfMessage = 0; + self.bytesRead = 0; + self.stubBuffer = null; + // Exit parsing loop + data = new Buffer(0); + } else { + var emitBuffer = data.slice(0, sizeOfMessage); + // Reset state of buffer + self.buffer = null; + self.sizeOfMessage = 0; + self.bytesRead = 0; + self.stubBuffer = null; + // Copy rest of message + data = data.slice(sizeOfMessage); + // Emit the message + self.messageHandler(new Response(self.bson, emitBuffer, self.responseOptions), self); + } + } else { + // Create a buffer that contains the space for the non-complete message + self.stubBuffer = new Buffer(data.length) + // Copy the data to the stub buffer + data.copy(self.stubBuffer, 0); + // Exit parsing loop + data = new Buffer(0); + } + } + } + } + } +} + +/** + * Connect + * @method + */ +Connection.prototype.connect = function(_options) { + var self = this; + _options = _options || {}; + // Set the connections + if(connectionAccounting) connections[this.id] = this; + // Check if we are overriding the promoteLongs + if(typeof _options.promoteLongs == 'boolean') { + self.responseOptions.promoteLongs = _options.promoteLongs; + } + + // Create new connection instance + self.connection = self.domainSocket + ? net.createConnection(self.host) + : net.createConnection(self.port, self.host); + + // Set the options for the connection + self.connection.setKeepAlive(self.keepAlive, self.keepAliveInitialDelay); + self.connection.setTimeout(self.connectionTimeout); + self.connection.setNoDelay(self.noDelay); + + // If we have ssl enabled + if(self.ssl) { + var sslOptions = { + socket: self.connection + , rejectUnauthorized: self.rejectUnauthorized + } + + if(self.ca) sslOptions.ca = self.ca; + if(self.cert) sslOptions.cert = self.cert; + if(self.key) sslOptions.key = self.key; + if(self.passphrase) sslOptions.passphrase = self.passphrase; + + // Override checkServerIdentity behavior + if(self.checkServerIdentity == false) { + // Skip the identiy check by retuning undefined as per node documents + // https://nodejs.org/api/tls.html#tls_tls_connect_options_callback + sslOptions.checkServerIdentity = function(servername, cert) { + return undefined; + } + } else if(typeof self.checkServerIdentity == 'function') { + sslOptions.checkServerIdentity = self.checkServerIdentity; + } + + // Attempt SSL connection + self.connection = tls.connect(self.port, self.host, sslOptions, function() { + // Error on auth or skip + if(self.connection.authorizationError && self.rejectUnauthorized) { + return self.emit("error", self.connection.authorizationError, self, {ssl:true}); + } + + // Set socket timeout instead of connection timeout + self.connection.setTimeout(self.socketTimeout); + // We are done emit connect + self.emit('connect', self); + }); + self.connection.setTimeout(self.connectionTimeout); + } else { + self.connection.on('connect', function() { + // Set socket timeout instead of connection timeout + self.connection.setTimeout(self.socketTimeout); + // Emit connect event + self.emit('connect', self); + }); + } + + // Add handlers for events + self.connection.once('error', errorHandler(self)); + self.connection.once('timeout', timeoutHandler(self)); + self.connection.once('close', closeHandler(self)); + self.connection.on('data', dataHandler(self)); +} + +/** + * Unref this connection + * @method + * @return {boolean} + */ +Connection.prototype.unref = function() { + if (this.connection) this.connection.unref(); + else { + var self = this; + this.once('connect', function() { + self.connection.unref(); + }); + } +} + +/** + * Destroy connection + * @method + */ +Connection.prototype.destroy = function() { + // Set the connections + if(connectionAccounting) delete connections[this.id]; + if(this.connection) { + this.connection.end(); + this.connection.destroy(); + } + + this.destroyed = true; +} + +/** + * Write to connection + * @method + * @param {Command} command Command to write out need to implement toBin and toBinUnified + */ +Connection.prototype.write = function(buffer) { + // Debug Log + if(this.logger.isDebug()) { + if(!Array.isArray(buffer)) { + this.logger.debug(f('writing buffer [%s] to %s:%s', buffer.toString('hex'), this.host, this.port)); + } else { + for(var i = 0; i < buffer.length; i++) + this.logger.debug(f('writing buffer [%s] to %s:%s', buffer[i].toString('hex'), this.host, this.port)); + } + } + + // Write out the command + if(!Array.isArray(buffer)) return this.connection.write(buffer, 'binary'); + // Iterate over all buffers and write them in order to the socket + for(var i = 0; i < buffer.length; i++) this.connection.write(buffer[i], 'binary'); +} + +/** + * Return id of connection as a string + * @method + * @return {string} + */ +Connection.prototype.toString = function() { + return "" + this.id; +} + +/** + * Return json object of connection + * @method + * @return {object} + */ +Connection.prototype.toJSON = function() { + return {id: this.id, host: this.host, port: this.port}; +} + +/** + * Is the connection connected + * @method + * @return {boolean} + */ +Connection.prototype.isConnected = function() { + if(this.destroyed) return false; + return !this.connection.destroyed && this.connection.writable; +} + +/** + * A server connect event, used to verify that the connection is up and running + * + * @event Connection#connect + * @type {Connection} + */ + +/** + * The server connection closed, all pool connections closed + * + * @event Connection#close + * @type {Connection} + */ + +/** + * The server connection caused an error, all pool connections closed + * + * @event Connection#error + * @type {Connection} + */ + +/** + * The server connection timed out, all pool connections closed + * + * @event Connection#timeout + * @type {Connection} + */ + +/** + * The driver experienced an invalid message, all pool connections closed + * + * @event Connection#parseError + * @type {Connection} + */ + +module.exports = Connection; diff --git a/node_modules/mongodb-core/lib/connection/logger.js b/node_modules/mongodb-core/lib/connection/logger.js new file mode 100644 index 0000000..cba8954 --- /dev/null +++ b/node_modules/mongodb-core/lib/connection/logger.js @@ -0,0 +1,229 @@ +"use strict"; + +var f = require('util').format + , MongoError = require('../error'); + +// Filters for classes +var classFilters = {}; +var filteredClasses = {}; +var level = null; +// Save the process id +var pid = process.pid; +// current logger +var currentLogger = null; + +/** + * Creates a new Logger instance + * @class + * @param {string} className The Class name associated with the logging instance + * @param {object} [options=null] Optional settings. + * @param {Function} [options.logger=null] Custom logger function; + * @param {string} [options.loggerLevel=error] Override default global log level. + * @return {Logger} a Logger instance. + */ +var Logger = function(className, options) { + if(!(this instanceof Logger)) return new Logger(className, options); + options = options || {}; + + // Current reference + var self = this; + this.className = className; + + // Current logger + if(options.logger) { + currentLogger = options.logger; + } else if(currentLogger == null) { + currentLogger = console.log; + } + + // Set level of logging, default is error + if(options.loggerLevel) { + level = options.loggerLevel || 'error'; + } + + // Add all class names + if(filteredClasses[this.className] == null) classFilters[this.className] = true; +} + +/** + * Log a message at the debug level + * @method + * @param {string} message The message to log + * @param {object} object additional meta data to log + * @return {null} + */ +Logger.prototype.debug = function(message, object) { + if(this.isDebug() + && ((Object.keys(filteredClasses).length > 0 && filteredClasses[this.className]) + || (Object.keys(filteredClasses).length == 0 && classFilters[this.className]))) { + var dateTime = new Date().getTime(); + var msg = f("[%s-%s:%s] %s %s", 'DEBUG', this.className, pid, dateTime, message); + var state = { + type: 'debug', message: message, className: this.className, pid: pid, date: dateTime + }; + if(object) state.meta = object; + currentLogger(msg, state); + } +} + +/** + * Log a message at the warn level + * @method + * @param {string} message The message to log + * @param {object} object additional meta data to log + * @return {null} + */ +Logger.prototype.warn = function(message, object) { + if(this.isWarn() + && ((Object.keys(filteredClasses).length > 0 && filteredClasses[this.className]) + || (Object.keys(filteredClasses).length == 0 && classFilters[this.className]))) { + var dateTime = new Date().getTime(); + var msg = f("[%s-%s:%s] %s %s", 'WARN', this.className, pid, dateTime, message); + var state = { + type: 'warn', message: message, className: this.className, pid: pid, date: dateTime + }; + if(object) state.meta = object; + currentLogger(msg, state); + } +}, + +/** + * Log a message at the info level + * @method + * @param {string} message The message to log + * @param {object} object additional meta data to log + * @return {null} + */ +Logger.prototype.info = function(message, object) { + if(this.isInfo() + && ((Object.keys(filteredClasses).length > 0 && filteredClasses[this.className]) + || (Object.keys(filteredClasses).length == 0 && classFilters[this.className]))) { + var dateTime = new Date().getTime(); + var msg = f("[%s-%s:%s] %s %s", 'INFO', this.className, pid, dateTime, message); + var state = { + type: 'info', message: message, className: this.className, pid: pid, date: dateTime + }; + if(object) state.meta = object; + currentLogger(msg, state); + } +}, + +/** + * Log a message at the error level + * @method + * @param {string} message The message to log + * @param {object} object additional meta data to log + * @return {null} + */ +Logger.prototype.error = function(message, object) { + if(this.isError() + && ((Object.keys(filteredClasses).length > 0 && filteredClasses[this.className]) + || (Object.keys(filteredClasses).length == 0 && classFilters[this.className]))) { + var dateTime = new Date().getTime(); + var msg = f("[%s-%s:%s] %s %s", 'ERROR', this.className, pid, dateTime, message); + var state = { + type: 'error', message: message, className: this.className, pid: pid, date: dateTime + }; + if(object) state.meta = object; + currentLogger(msg, state); + } +}, + +/** + * Is the logger set at info level + * @method + * @return {boolean} + */ +Logger.prototype.isInfo = function() { + return level == 'info' || level == 'debug'; +}, + +/** + * Is the logger set at error level + * @method + * @return {boolean} + */ +Logger.prototype.isError = function() { + return level == 'error' || level == 'info' || level == 'debug'; +}, + +/** + * Is the logger set at error level + * @method + * @return {boolean} + */ +Logger.prototype.isWarn = function() { + return level == 'error' || level == 'warn' || level == 'info' || level == 'debug'; +}, + +/** + * Is the logger set at debug level + * @method + * @return {boolean} + */ +Logger.prototype.isDebug = function() { + return level == 'debug'; +} + +/** + * Resets the logger to default settings, error and no filtered classes + * @method + * @return {null} + */ +Logger.reset = function() { + level = 'error'; + filteredClasses = {}; +} + +/** + * Get the current logger function + * @method + * @return {function} + */ +Logger.currentLogger = function() { + return currentLogger; +} + +/** + * Set the current logger function + * @method + * @param {function} logger Logger function. + * @return {null} + */ +Logger.setCurrentLogger = function(logger) { + if(typeof logger != 'function') throw new MongoError("current logger must be a function"); + currentLogger = logger; +} + +/** + * Set what classes to log. + * @method + * @param {string} type The type of filter (currently only class) + * @param {string[]} values The filters to apply + * @return {null} + */ +Logger.filter = function(type, values) { + if(type == 'class' && Array.isArray(values)) { + filteredClasses = {}; + + values.forEach(function(x) { + filteredClasses[x] = true; + }); + } +} + +/** + * Set the current log level + * @method + * @param {string} level Set current log level (debug, info, error) + * @return {null} + */ +Logger.setLevel = function(_level) { + if(_level != 'info' && _level != 'error' && _level != 'debug' && _level != 'warn') { + throw new Error(f("%s is an illegal logging level", _level)); + } + + level = _level; +} + +module.exports = Logger; diff --git a/node_modules/mongodb-core/lib/connection/pool.js b/node_modules/mongodb-core/lib/connection/pool.js new file mode 100644 index 0000000..4f1c83b --- /dev/null +++ b/node_modules/mongodb-core/lib/connection/pool.js @@ -0,0 +1,1122 @@ +"use strict"; + +var inherits = require('util').inherits, + EventEmitter = require('events').EventEmitter, + Connection = require('./connection'), + MongoError = require('../error'), + Logger = require('./logger'), + f = require('util').format, + Query = require('./commands').Query, + CommandResult = require('./command_result'), + assign = require('../topologies/shared').assign; + +var MongoCR = require('../auth/mongocr') + , X509 = require('../auth/x509') + , Plain = require('../auth/plain') + , GSSAPI = require('../auth/gssapi') + , SSPI = require('../auth/sspi') + , ScramSHA1 = require('../auth/scram'); + +var DISCONNECTED = 'disconnected'; +var CONNECTING = 'connecting'; +var CONNECTED = 'connected'; +var DESTROYING = 'destroying'; +var DESTROYED = 'destroyed'; + +var _id = 0; + +/** + * Creates a new Pool instance + * @class + * @param {string} options.host The server host + * @param {number} options.port The server port + * @param {number} [options.size=1] Max server connection pool size + * @param {boolean} [options.reconnect=true] Server will attempt to reconnect on loss of connection + * @param {number} [options.reconnectTries=30] Server attempt to reconnect #times + * @param {number} [options.reconnectInterval=1000] Server will wait # milliseconds between retries + * @param {boolean} [options.keepAlive=true] TCP Connection keep alive enabled + * @param {number} [options.keepAliveInitialDelay=0] Initial delay before TCP keep alive enabled + * @param {boolean} [options.noDelay=true] TCP Connection no delay + * @param {number} [options.connectionTimeout=0] TCP Connection timeout setting + * @param {number} [options.socketTimeout=0] TCP Socket timeout setting + * @param {number} [options.monitoringSocketTimeout=30000] TCP Socket timeout setting for replicaset monitoring socket + * @param {boolean} [options.ssl=false] Use SSL for connection + * @param {boolean|function} [options.checkServerIdentity=true] Ensure we check server identify during SSL, set to false to disable checking. Only works for Node 0.12.x or higher. You can pass in a boolean or your own checkServerIdentity override function. + * @param {Buffer} [options.ca] SSL Certificate store binary buffer + * @param {Buffer} [options.cert] SSL Certificate binary buffer + * @param {Buffer} [options.key] SSL Key file binary buffer + * @param {string} [options.passPhrase] SSL Certificate pass phrase + * @param {boolean} [options.rejectUnauthorized=false] Reject unauthorized server certificates + * @param {boolean} [options.promoteLongs=true] Convert Long values from the db into Numbers if they fit into 53 bits + * @fires Pool#connect + * @fires Pool#close + * @fires Pool#error + * @fires Pool#timeout + * @fires Pool#parseError + * @return {Pool} A cursor instance + */ +var Pool = function(options) { + var self = this; + // Add event listener + EventEmitter.call(this); + // Add the options + this.options = assign({ + // Host and port settings + host: 'localhost', + port: 27017, + // Pool default max size + size: 5, + // socket settings + connectionTimeout: 30000, + socketTimeout: 30000, + keepAlive: true, + keepAliveInitialDelay: 0, + noDelay: true, + // SSL Settings + ssl: false, checkServerIdentity: false, + ca: null, cert: null, key: null, passPhrase: null, + rejectUnauthorized: false, + promoteLongs: true, + // Reconnection options + reconnect: true, + reconnectInterval: 1000, + reconnectTries: 30 + }, options); + + // Identification information + this.id = _id++; + // Current reconnect retries + this.retriesLeft = this.options.reconnectTries; + this.reconnectId = null; + // No bson parser passed in + if(!options.bson || (options.bson + && (typeof options.bson.serialize != 'function' || typeof options.bson.deserialize != 'function'))) throw new Error("must pass in valid bson parser"); + // Logger instance + this.logger = Logger('Pool', options); + // Pool state + this.state = DISCONNECTED; + // Connections + this.availableConnections = []; + this.inUseConnections = []; + this.connectingConnections = []; + // Currently executing + this.executing = false; + // Operation work queue + this.queue = []; + + // All the authProviders + this.authProviders = options.authProviders || { + 'mongocr': new MongoCR(options.bson), 'x509': new X509(options.bson) + , 'plain': new Plain(options.bson), 'gssapi': new GSSAPI(options.bson) + , 'sspi': new SSPI(options.bson), 'scram-sha-1': new ScramSHA1(options.bson) + } + + // Are we currently authenticating + this.authenticating = false; + this.loggingout = false; + this.nonAuthenticatedConnections = []; + this.authenticatingTimestamp = null; + // Number of consecutive timeouts caught + this.numberOfConsecutiveTimeouts = 0; +} + +inherits(Pool, EventEmitter); + +Object.defineProperty(Pool.prototype, 'size', { + enumerable:true, + get: function() { return this.options.size; } +}); + +Object.defineProperty(Pool.prototype, 'connectionTimeout', { + enumerable:true, + get: function() { return this.options.connectionTimeout; } +}); + +Object.defineProperty(Pool.prototype, 'socketTimeout', { + enumerable:true, + get: function() { return this.options.socketTimeout; } +}); + +function stateTransition(self, newState) { + var legalTransitions = { + 'disconnected': [CONNECTING, DESTROYING, DISCONNECTED], + 'connecting': [CONNECTING, DESTROYING, CONNECTED, DISCONNECTED], + 'connected': [CONNECTED, DISCONNECTED, DESTROYING], + 'destroying': [DESTROYING, DESTROYED], + 'destroyed': [DESTROYED] + } + + // Get current state + var legalStates = legalTransitions[self.state]; + if(legalStates && legalStates.indexOf(newState) != -1) { + self.state = newState; + } else { + self.logger.error(f('Pool with id [%s] failed attempted illegal state transition from [%s] to [%s] only following state allowed [%s]' + , self.id, self.state, newState, legalStates)); + } +} + +function authenticate(pool, auth, connection, cb) { + if(auth[0] === undefined) return cb(null); + // We need to authenticate the server + var mechanism = auth[0]; + var db = auth[1]; + // Validate if the mechanism exists + if(!pool.authProviders[mechanism]) { + throw new MongoError(f('authMechanism %s not supported', mechanism)); + } + + // Get the provider + var provider = pool.authProviders[mechanism]; + + // Authenticate using the provided mechanism + provider.auth.apply(provider, [write(pool), [connection], db].concat(auth.slice(2)).concat([cb])); +} + +// The write function used by the authentication mechanism (bypasses external) +function write(self) { + return function(connection, buffer, callback) { + // Ensure we stop auth if pool was destroyed + if(self.state == DESTROYED || self.state == DESTROYING) { + return callback(new MongoError('pool destroyed')); + } + // Set the connection workItem callback + connection.workItem = {cb: callback, command: true}; + // Write the buffer out to the connection + connection.write(buffer); + }; +} + + +function reauthenticate(pool, connection, cb) { + // Authenticate + function authenticateAgainstProvider(pool, connection, providers, cb) { + // Finished re-authenticating against providers + if(providers.length == 0) return cb(); + // Get the provider name + var provider = pool.authProviders[providers.pop()]; + + // Auth provider + provider.reauthenticate(write(pool), [connection], function(err, r) { + // We got an error return immediately + if(err) return cb(err); + // Continue authenticating the connection + authenticateAgainstProvider(pool, connection, providers, cb); + }); + } + + // Start re-authenticating process + authenticateAgainstProvider(pool, connection, Object.keys(pool.authProviders), cb); +} + +function connectionFailureHandler(self, event) { + return function(err) { + removeConnection(self, this); + + // Flush out the callback if there is one + if(this.workItem && this.workItem.cb) { + var workItem = this.workItem; + this.workItem = null; + workItem.cb(err); + } + + // Did we catch a timeout, increment the numberOfConsecutiveTimeouts + if(event == 'timeout') { + self.numberOfConsecutiveTimeouts = self.numberOfConsecutiveTimeouts + 1; + + // Have we timed out more than reconnectTries in a row ? + // Force close the pool as we are trying to connect to tcp sink hole + if(self.numberOfConsecutiveTimeouts > self.options.reconnectTries) { + self.numberOfConsecutiveTimeouts = 0; + // Destroy all connections and pool + self.destroy(true); + // Emit close event + return self.emit('close', self); + } + } + + // No more socket available propegate the event + if(self.socketCount() == 0) { + if(self.state != DESTROYED && self.state != DESTROYING) { + stateTransition(self, DISCONNECTED); + } + + // Do not emit error events, they are always close events + // do not trigger the low level error handler in node + event = event == 'error' ? 'close' : event; + self.emit(event, err); + } + + // Start reconnection attempts + if(!self.reconnectId && self.options.reconnect) { + self.reconnectId = setTimeout(attemptReconnect(self), self.options.reconnectInterval); + } + }; +} + +function attemptReconnect(self) { + return function() { + self.emit('attemptReconnect', self); + if(self.state == DESTROYED || self.state == DESTROYING) return; + + // We are connected do not try again + if(self.isConnected()) { + self.reconnectId = null; + return; + } + + // If we have failure schedule a retry + function _connectionFailureHandler(self, event) { + return function() { + self.retriesLeft = self.retriesLeft - 1; + // How many retries are left + if(self.retriesLeft == 0) { + // Destroy the instance + self.destroy(); + // Emit close event + self.emit('reconnectFailed' + , new MongoError(f('failed to reconnect after %s attempts with interval %s ms', self.options.reconnectTries, self.options.reconnectInterval))); + } else { + self.reconnectId = setTimeout(attemptReconnect(self), self.options.reconnectInterval); + } + } + } + + // Got a connect handler + function _connectHandler(self) { + return function() { + // Assign + var connection = this; + + // Pool destroyed stop the connection + if(self.state == DESTROYED || self.state == DESTROYING) { + return connection.destroy(); + } + + // Clear out all handlers + handlers.forEach(function(event) { + connection.removeAllListeners(event); + }); + + // Reset reconnect id + self.reconnectId = null; + + // Apply pool connection handlers + connection.on('error', connectionFailureHandler(self, 'error')); + connection.on('close', connectionFailureHandler(self, 'close')); + connection.on('timeout', connectionFailureHandler(self, 'timeout')); + connection.on('parseError', connectionFailureHandler(self, 'parseError')); + + // Apply any auth to the connection + reauthenticate(self, this, function(err) { + // Reset retries + self.retriesLeft = self.options.reconnectTries; + // Push to available connections + self.availableConnections.push(connection); + // Emit reconnect event + self.emit('reconnect', self); + // Trigger execute to start everything up again + _execute(self)(); + }); + } + } + + // Create a connection + var connection = new Connection(messageHandler(self), self.options); + // Add handlers + connection.on('close', _connectionFailureHandler(self, 'close')); + connection.on('error', _connectionFailureHandler(self, 'error')); + connection.on('timeout', _connectionFailureHandler(self, 'timeout')); + connection.on('parseError', _connectionFailureHandler(self, 'parseError')); + // On connection + connection.on('connect', _connectHandler(self)); + // Attempt connection + connection.connect(); + } +} + +function moveConnectionBetween(connection, from, to) { + var index = from.indexOf(connection); + // Move the connection from connecting to available + if(index != -1) { + from.splice(index, 1); + to.push(connection); + } +} + +function messageHandler(self) { + return function(message, connection) { + // Get the callback + var workItem = connection.workItem; + // Reset timeout counter + self.numberOfConsecutiveTimeouts = 0; + + // Reset the connection timeout if we modified it for + // this operation + if(workItem.socketTimeout) { + connection.resetSocketTimeout(); + } + + // Log if debug enabled + if(self.logger.isDebug()) { + self.logger.debug(f('message [%s] received from %s:%s' + , message.raw.toString('hex'), self.options.host, self.options.port)); + } + + // Authenticate any straggler connections + function authenticateStragglers(self, connection, callback) { + // Get any non authenticated connections + var connections = self.nonAuthenticatedConnections.slice(0); + var nonAuthenticatedConnections = self.nonAuthenticatedConnections; + self.nonAuthenticatedConnections = []; + + // Establish if the connection need to be authenticated + // Add to authentication list if + // 1. we were in an authentication process when the operation was executed + // 2. our current authentication timestamp is from the workItem one, meaning an auth has happened + if(connection.workItem.authenticating == true + || (typeof connection.workItem.authenticatingTimestamp == 'number' + && connection.workItem.authenticatingTimestamp != self.authenticatingTimestamp)) { + // Add connection to the list + connections.push(connection); + } + + // Clear out workItem + connection.workItem = null; + + // No connections need to be re-authenticated + if(connections.length == 0) { + // Release the connection back to the pool + moveConnectionBetween(connection, self.inUseConnections, self.availableConnections); + // Finish + return callback(); + } + + // Apply re-authentication to all connections before releasing back to pool + var connectionCount = connections.length; + // Authenticate all connections + for(var i = 0; i < connectionCount; i++) { + reauthenticate(self, connections[i], function(err) { + connectionCount = connectionCount - 1; + + if(connectionCount == 0) { + // Put non authenticated connections in available connections + self.availableConnections = self.availableConnections.concat(nonAuthenticatedConnections); + // Release the connection back to the pool + moveConnectionBetween(connection, self.inUseConnections, self.availableConnections); + // Return + callback(); + } + }); + } + } + + authenticateStragglers(self, connection, function(err) { + // Keep executing, ensure current message handler does not stop execution + process.nextTick(function() { + _execute(self)(); + }); + + // Time to dispatch the message if we have a callback + if(!workItem.immediateRelease) { + try { + // Parse the message according to the provided options + message.parse(workItem); + } catch(err) { + return workItem.cb(MongoError.create(err)); + } + + // Establish if we have an error + if(workItem.command && message.documents[0] && (message.documents[0].ok == 0 || message.documents[0]['$err'] + || message.documents[0]['errmsg'] || message.documents[0]['code'])) { + return workItem.cb(MongoError.create(message.documents[0])); + } + + // Return the documents + workItem.cb(null, new CommandResult(message.documents[0], connection, message)); + } + }); + } +} + +/** + * Return the total socket count in the pool. + * @method + * @return {Number} The number of socket available. + */ +Pool.prototype.socketCount = function() { + return this.availableConnections.length + + this.inUseConnections.length + + this.connectingConnections.length; +} + +/** + * Return all pool connections + * @method + * @return {Connectio[]} The pool connections + */ +Pool.prototype.allConnections = function() { + return this.availableConnections + .concat(this.inUseConnections) + .concat(this.connectingConnections); +} + +/** + * Get a pool connection (round-robin) + * @method + * @return {Connection} + */ +Pool.prototype.get = function() { + return this.allConnections()[0]; +} + +/** + * Is the pool connected + * @method + * @return {boolean} + */ +Pool.prototype.isConnected = function() { + // We are in a destroyed state + if(this.state == DESTROYED || this.state == DESTROYING) { + return false; + } + + // Get connections + var connections = this.availableConnections + .concat(this.inUseConnections) + .concat(this.connectingConnections); + for(var i = 0; i < connections.length; i++) { + if(connections[i].isConnected()) return true; + } + + // Might be authenticating, but we are still connected + if(connections.length == 0 && this.authenticating) { + return true + } + + // Not connected + return false; +} + +/** + * Was the pool destroyed + * @method + * @return {boolean} + */ +Pool.prototype.isDestroyed = function() { + return this.state == DESTROYED || this.state == DESTROYING; +} + +/** + * Is the pool in a disconnected state + * @method + * @return {boolean} + */ +Pool.prototype.isDisconnected = function() { + return this.state == DISCONNECTED; +} + +/** + * Connect pool + * @method + */ +Pool.prototype.connect = function(auth) { + if(this.state != DISCONNECTED) throw new MongoError('connection in unlawful state ' + this.state); + var self = this; + // Transition to connecting state + stateTransition(this, CONNECTING); + // Create an array of the arguments + var args = Array.prototype.slice.call(arguments, 0); + // Create a connection + var connection = new Connection(messageHandler(self), this.options); + // Add to list of connections + this.connectingConnections.push(connection); + // Add listeners to the connection + connection.once('connect', function(connection) { + if(self.state == DESTROYED || self.state == DESTROYING) return self.destroy(); + + // Apply any store credentials + reauthenticate(self, connection, function(err) { + if(self.state == DESTROYED || self.state == DESTROYING) return self.destroy(); + + // We have an error emit it + if(err) { + // Destroy the pool + self.destroy(); + // Emit the error + return self.emit('error', err); + } + + // Authenticate + authenticate(self, args, connection, function(err) { + if(self.state == DESTROYED || self.state == DESTROYING) return self.destroy(); + + // We have an error emit it + if(err) { + // Destroy the pool + self.destroy(); + // Emit the error + return self.emit('error', err); + } + // Set connected mode + stateTransition(self, CONNECTED); + + // Move the active connection + moveConnectionBetween(connection, self.connectingConnections, self.availableConnections); + + // Emit the connect event + self.emit('connect', self); + }); + }); + }); + + // Add error handlers + connection.once('error', connectionFailureHandler(this, 'error')); + connection.once('close', connectionFailureHandler(this, 'close')); + connection.once('timeout', connectionFailureHandler(this, 'timeout')); + connection.once('parseError', connectionFailureHandler(this, 'parseError')); + + try { + connection.connect(); + } catch(err) { + // SSL or something threw on connect + self.emit('error', err); + } +} + +/** + * Authenticate using a specified mechanism + * @method + * @param {string} mechanism The Auth mechanism we are invoking + * @param {string} db The db we are invoking the mechanism against + * @param {...object} param Parameters for the specific mechanism + * @param {authResultCallback} callback A callback function + */ +Pool.prototype.auth = function(mechanism, db) { + var self = this; + var args = Array.prototype.slice.call(arguments, 0); + var callback = args.pop(); + // If we are not connected don't allow additonal authentications to happen + // if(this.state != CONNECTED) throw new MongoError('connection in unlawful state ' + this.state); + + // If we don't have the mechanism fail + if(self.authProviders[mechanism] == null && mechanism != 'default') { + throw new MongoError(f("auth provider %s does not exist", mechanism)); + } + + // Signal that we are authenticating a new set of credentials + this.authenticating = true; + this.authenticatingTimestamp = new Date().getTime(); + + // Authenticate all live connections + function authenticateLiveConnections(self, args, cb) { + // Get the current viable connections + var connections = self.availableConnections; + // Allow nothing else to use the connections while we authenticate them + self.availableConnections = []; + + var connectionsCount = connections.length; + var error = null; + // No connections available, return + if(connectionsCount == 0) return callback(null); + // Authenticate the connections + for(var i = 0; i < connections.length; i++) { + authenticate(self, args, connections[i], function(err) { + connectionsCount = connectionsCount - 1; + + // Store the error + if(err) error = err; + + // Processed all connections + if(connectionsCount == 0) { + // Auth finished + self.authenticating = false; + // Add the connections back to available connections + self.availableConnections = self.availableConnections.concat(connections); + // We had an error, return it + if(error) { + // Log the error + if(self.logger.isError()) { + self.logger.error(f('[%s] failed to authenticate against server %s:%s' + , self.id, self.options.host, self.options.port)); + } + + return cb(error); + } + cb(null); + } + }); + } + } + + // Wait for a logout in process to happen + function waitForLogout(self, cb) { + if(!self.loggingout) return cb(); + setTimeout(function() { + waitForLogout(self, cb); + }, 1) + } + + // Wait for loggout to finish + waitForLogout(self, function() { + // Authenticate all live connections + authenticateLiveConnections(self, args, function(err) { + // Credentials correctly stored in auth provider if successful + // Any new connections will now reauthenticate correctly + self.authenticating = false; + // Return after authentication connections + callback(err); + }); + }); +} + +/** + * Logout all users against a database + * @method + * @param {string} dbName The database name + * @param {authResultCallback} callback A callback function + */ +Pool.prototype.logout = function(dbName, callback) { + var self = this; + if(typeof dbName != 'string') throw new MongoError('logout method requires a db name as first argument'); + if(typeof callback != 'function') throw new MongoError('logout method requires a callback'); + + // Indicate logout in process + this.loggingout = true; + + // Get all relevant connections + var connections = self.availableConnections.concat(self.inUseConnections); + var count = connections.length; + // Store any error + var error = null; + + // Send logout command over all the connections + for(var i = 0; i < connections.length; i++) { + var query = new Query(this.options.bson + , f('%s.$cmd', dbName) + , {logout:1}, {numberToSkip: 0, numberToReturn: 1}); + write(self)(connections[i], query.toBin(), function(err, r) { + count = count - 1; + if(err) error = err; + + if(count == 0) { + self.loggingout = false; + callback(error); + }; + }); + } +} + +/** + * Unref the pool + * @method + */ +Pool.prototype.unref = function() { + // Get all the known connections + var connections = this.availableConnections + .concat(this.inUseConnections) + .concat(this.connectingConnections); + connections.forEach(function(c) { + c.unref(); + }); +} + +// Events +var events = ['error', 'close', 'timeout', 'parseError', 'connect']; + +// Destroy the connections +function destroy(self, connections) { + // Destroy all connections + connections.forEach(function(c) { + // Remove all listeners + for(var i = 0; i < events.length; i++) { + c.removeAllListeners(events[i]); + } + // Destroy connection + c.destroy(); + }); + + // Zero out all connections + self.inUseConnections = []; + self.availableConnections = []; + self.nonAuthenticatedConnections = []; + self.connectingConnections = []; + + // Set state to destroyed + stateTransition(self, DESTROYED); +} + +/** + * Destroy pool + * @method + */ +Pool.prototype.destroy = function(force) { + var self = this; + // Do not try again if the pool is already dead + if(this.state == DESTROYED || self.state == DESTROYING) return; + // Set state to destroyed + stateTransition(this, DESTROYING); + + // Are we force closing + if(force) { + // Get all the known connections + var connections = self.availableConnections + .concat(self.inUseConnections) + .concat(self.nonAuthenticatedConnections) + .concat(self.connectingConnections); + return destroy(self, connections); + } + + // Wait for the operations to drain before we close the pool + function checkStatus() { + if(self.queue.length == 0) { + // Get all the known connections + var connections = self.availableConnections + .concat(self.inUseConnections) + .concat(self.nonAuthenticatedConnections) + .concat(self.connectingConnections); + + // Check if we have any in flight operations + for(var i = 0; i < connections.length; i++) { + // There is an operation still in flight, reschedule a + // check waiting for it to drain + if(connections[i].workItem) { + return setTimeout(checkStatus, 1); + } + } + + destroy(self, connections); + } else { + setTimeout(checkStatus, 1); + } + } + + // Initiate drain of operations + checkStatus(); +} + +/** + * Write a message to MongoDB + * @method + * @return {Connection} + */ +Pool.prototype.write = function(buffer, options, cb) { + // Ensure we have a callback + if(typeof options == 'function') { + cb = options; + } + + // Always have options + options = options || {}; + + // Pool was destroyed error out + if(this.state == DESTROYED || this.state == DESTROYING) { + // Callback with an error + if(cb) cb(new MongoError('pool destroyed')); + return; + } + + // Do we have an operation + var operation = { + buffer:buffer, cb: cb, raw: false, promoteLongs: true + }; + + // Set the options for the parsing + operation.promoteLongs = typeof options.promoteLongs == 'boolean' ? options.promoteLongs : true; + operation.raw = typeof options.raw == 'boolean' ? options.raw : false; + operation.immediateRelease = typeof options.immediateRelease == 'boolean' ? options.immediateRelease : false; + operation.documentsReturnedIn = options.documentsReturnedIn; + operation.command = typeof options.command == 'boolean' ? options.command : false; + // Optional per operation socketTimeout + operation.socketTimeout = options.socketTimeout; + operation.monitoring = options.monitoring; + // // debug + // operation.cmd = options.cmd; + + // We need to have a callback function unless the message returns no response + if(!(typeof cb == 'function') && !options.noResponse) { + throw new MongoError('write method must provide a callback'); + } + + // If we have a monitoring operation schedule as the very first operation + // Otherwise add to back of queue + if(options.monitoring) { + this.queue.unshift(operation); + } else { + this.queue.push(operation); + } + + // Attempt to execute the operation + _execute(this)(); +} + +// Remove connection method +function remove(connection, connections) { + for(var i = 0; i < connections.length; i++) { + if(connections[i] === connection) { + connections.splice(i, 1); + return true; + } + } +} + +function removeConnection(self, connection) { + if(remove(connection, self.availableConnections)) return; + if(remove(connection, self.inUseConnections)) return; + if(remove(connection, self.connectingConnections)) return; + if(remove(connection, self.nonAuthenticatedConnections)) return; +} + +// All event handlers +var handlers = ["close", "message", "error", "timeout", "parseError", "connect"]; + +function _createConnection(self) { + var connection = new Connection(messageHandler(self), self.options); + + // Push the connection + self.connectingConnections.push(connection); + + // Handle any errors + var tempErrorHandler = function(_connection) { + return function(err) { + // Destroy the connection + _connection.destroy(); + // Remove the connection from the connectingConnections list + removeConnection(self, _connection); + // Start reconnection attempts + if(!self.reconnectId && self.options.reconnect) { + self.reconnectId = setTimeout(attemptReconnect(self), self.options.reconnectInterval); + } + } + } + + // Handle successful connection + var tempConnectHandler = function(_connection) { + return function() { + // Destroyed state return + if(self.state == DESTROYED || self.state == DESTROYING) { + // Remove the connection from the list + removeConnection(self, _connection); + return _connection.destroy(); + } + + // Destroy all event emitters + handlers.forEach(function(e) { + _connection.removeAllListeners(e); + }); + + // Add the final handlers + _connection.once('close', connectionFailureHandler(self, 'close')); + _connection.once('error', connectionFailureHandler(self, 'error')); + _connection.once('timeout', connectionFailureHandler(self, 'timeout')); + _connection.once('parseError', connectionFailureHandler(self, 'parseError')); + + // Signal + reauthenticate(self, _connection, function(err) { + if(self.state == DESTROYED || self.state == DESTROYING) { + return _connection.destroy(); + } + // Remove the connection from the connectingConnections list + removeConnection(self, _connection); + + // Handle error + if(err) { + return _connection.destroy(); + } + + // If we are authenticating at the moment + // Do not automatially put in available connections + // As we need to apply the credentials first + if(self.authenticating) { + self.nonAuthenticatedConnections.push(_connection); + } else { + // Push to available + self.availableConnections.push(_connection); + // Execute any work waiting + _execute(self)(); + } + }); + } + } + + // Add all handlers + connection.once('close', tempErrorHandler(connection)); + connection.once('error', tempErrorHandler(connection)); + connection.once('timeout', tempErrorHandler(connection)); + connection.once('parseError', tempErrorHandler(connection)); + connection.once('connect', tempConnectHandler(connection)); + + // Start connection + connection.connect(); +} + +function flushMonitoringOperations(queue) { + for(var i = 0; i < queue.length; i++) { + if(queue[i].monitoring) { + var workItem = queue[i]; + queue.splice(i, 1); + workItem.cb(new MongoError('no connection available for monitoring')); + } + } +} + +function _execute(self) { + return function() { + if(self.state == DESTROYED) return; + // Already executing, skip + if(self.executing) return; + // Set pool as executing + self.executing = true; + + // Wait for auth to clear before continuing + function waitForAuth(cb) { + if(!self.authenticating) return cb(); + // Wait for a milisecond and try again + setTimeout(function() { + waitForAuth(cb); + }, 1); + } + + // Block on any auth in process + waitForAuth(function() { + // As long as we have available connections + while(true) { + // Total availble connections + var totalConnections = self.availableConnections.length + + self.connectingConnections.length + + self.inUseConnections.length; + + // Have we not reached the max connection size yet + if(self.availableConnections.length == 0 + && self.connectingConnections.length == 0 + && totalConnections < self.options.size + && self.queue.length > 0) { + // // Flush any monitoring operations + // flushMonitoringOperations(self.queue); + // Create a new connection + _createConnection(self); + // Attempt to execute again + self.executing = false; + return; + } + + // No available connections available, flush any monitoring ops + if(self.availableConnections.length == 0) { + // Flush any monitoring operations + flushMonitoringOperations(self.queue); + break; + } + + // No queue break + if(self.queue.length == 0) { + break; + } + + // Get a connection + var connection = self.availableConnections.pop(); + if(connection.isConnected()) { + // Get the next work item + var workItem = self.queue.shift(); + + // Get actual binary commands + var buffer = workItem.buffer; + + // Add connection to workers in flight + self.inUseConnections.push(connection); + + // Set current status of authentication process + workItem.authenticating = self.authenticating; + workItem.authenticatingTimestamp = self.authenticatingTimestamp; + + // Add current associated callback to the connection + connection.workItem = workItem + + // We have a custom socketTimeout + if(!workItem.immediateRelease && typeof workItem.socketTimeout == 'number') { + connection.setSocketTimeout(workItem.socketTimeout); + } + + // Put operation on the wire + if(Array.isArray(buffer)) { + for(var i = 0; i < buffer.length; i++) { + connection.write(buffer[i]) + } + } else { + connection.write(buffer); + } + + // Fire and forgot message, release the socket + if(workItem.immediateRelease && !self.authenticating) { + self.inUseConnections.pop(); + self.availableConnections.push(connection); + } else if(workItem.immediateRelease && self.authenticating) { + self.inUseConnections.pop(); + self.nonAuthenticatedConnections.push(connection); + } + } else { + flushMonitoringOperations(self.queue); + } + } + }); + + self.executing = false; + } +} + +/** + * A server connect event, used to verify that the connection is up and running + * + * @event Pool#connect + * @type {Pool} + */ + +/** + * A server reconnect event, used to verify that pool reconnected. + * + * @event Pool#reconnect + * @type {Pool} + */ + +/** + * The server connection closed, all pool connections closed + * + * @event Pool#close + * @type {Pool} + */ + +/** + * The server connection caused an error, all pool connections closed + * + * @event Pool#error + * @type {Pool} + */ + +/** + * The server connection timed out, all pool connections closed + * + * @event Pool#timeout + * @type {Pool} + */ + +/** + * The driver experienced an invalid message, all pool connections closed + * + * @event Pool#parseError + * @type {Pool} + */ + +/** + * The driver attempted to reconnect + * + * @event Pool#attemptReconnect + * @type {Pool} + */ + +/** + * The driver exhausted all reconnect attempts + * + * @event Pool#reconnectFailed + * @type {Pool} + */ + +module.exports = Pool; diff --git a/node_modules/mongodb-core/lib/connection/utils.js b/node_modules/mongodb-core/lib/connection/utils.js new file mode 100644 index 0000000..019ef19 --- /dev/null +++ b/node_modules/mongodb-core/lib/connection/utils.js @@ -0,0 +1,67 @@ +"use strict"; + +// Set property function +var setProperty = function(obj, prop, flag, values) { + Object.defineProperty(obj, prop.name, { + enumerable:true, + set: function(value) { + if(typeof value != 'boolean') throw new Error(f("%s required a boolean", prop.name)); + // Flip the bit to 1 + if(value == true) values.flags |= flag; + // Flip the bit to 0 if it's set, otherwise ignore + if(value == false && (values.flags & flag) == flag) values.flags ^= flag; + prop.value = value; + } + , get: function() { return prop.value; } + }); +} + +// Set property function +var getProperty = function(obj, propName, fieldName, values, func) { + Object.defineProperty(obj, propName, { + enumerable:true, + get: function() { + // Not parsed yet, parse it + if(values[fieldName] == null && obj.isParsed && !obj.isParsed()) { + obj.parse(); + } + + // Do we have a post processing function + if(typeof func == 'function') return func(values[fieldName]); + // Return raw value + return values[fieldName]; + } + }); +} + +// Set simple property +var getSingleProperty = function(obj, name, value) { + Object.defineProperty(obj, name, { + enumerable:true, + get: function() { + return value + } + }); +} + +// Shallow copy +var copy = function(fObj, tObj) { + tObj = tObj || {}; + for(var name in fObj) tObj[name] = fObj[name]; + return tObj; +} + +var debugOptions = function(debugFields, options) { + var finaloptions = {}; + debugFields.forEach(function(n) { + finaloptions[n] = options[n]; + }); + + return finaloptions; +} + +exports.setProperty = setProperty; +exports.getProperty = getProperty; +exports.getSingleProperty = getSingleProperty; +exports.copy = copy; +exports.debugOptions = debugOptions; diff --git a/node_modules/mongodb-core/lib/cursor.js b/node_modules/mongodb-core/lib/cursor.js new file mode 100644 index 0000000..0ba6d80 --- /dev/null +++ b/node_modules/mongodb-core/lib/cursor.js @@ -0,0 +1,677 @@ +"use strict"; + +var Long = require('bson').Long + , Logger = require('./connection/logger') + , MongoError = require('./error') + , f = require('util').format; + +/** + * This is a cursor results callback + * + * @callback resultCallback + * @param {error} error An error object. Set to null if no error present + * @param {object} document + */ + +/** + * @fileOverview The **Cursor** class is an internal class that embodies a cursor on MongoDB + * allowing for iteration over the results returned from the underlying query. + * + * **CURSORS Cannot directly be instantiated** + * @example + * var Server = require('mongodb-core').Server + * , ReadPreference = require('mongodb-core').ReadPreference + * , assert = require('assert'); + * + * var server = new Server({host: 'localhost', port: 27017}); + * // Wait for the connection event + * server.on('connect', function(server) { + * assert.equal(null, err); + * + * // Execute the write + * var cursor = _server.cursor('integration_tests.inserts_example4', { + * find: 'integration_tests.example4' + * , query: {a:1} + * }, { + * readPreference: new ReadPreference('secondary'); + * }); + * + * // Get the first document + * cursor.next(function(err, doc) { + * assert.equal(null, err); + * server.destroy(); + * }); + * }); + * + * // Start connecting + * server.connect(); + */ + +/** + * Creates a new Cursor, not to be used directly + * @class + * @param {object} bson An instance of the BSON parser + * @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1) + * @param {{object}|Long} cmd The selector (can be a command or a cursorId) + * @param {object} [options=null] Optional settings. + * @param {object} [options.batchSize=1000] Batchsize for the operation + * @param {array} [options.documents=[]] Initial documents list for cursor + * @param {object} [options.transforms=null] Transform methods for the cursor results + * @param {function} [options.transforms.query] Transform the value returned from the initial query + * @param {function} [options.transforms.doc] Transform each document returned from Cursor.prototype.next + * @param {object} topology The server topology instance. + * @param {object} topologyOptions The server topology options. + * @return {Cursor} A cursor instance + * @property {number} cursorBatchSize The current cursorBatchSize for the cursor + * @property {number} cursorLimit The current cursorLimit for the cursor + * @property {number} cursorSkip The current cursorSkip for the cursor + */ +var Cursor = function(bson, ns, cmd, options, topology, topologyOptions) { + options = options || {}; + // Cursor reference + var self = this; + // Initial query + var query = null; + + // Cursor pool + this.pool = null; + // Cursor server + this.server = null; + + // Do we have a not connected handler + this.disconnectHandler = options.disconnectHandler; + + // Set local values + this.bson = bson; + this.ns = ns; + this.cmd = cmd; + this.options = options; + this.topology = topology; + + // All internal state + this.cursorState = { + cursorId: null + , cmd: cmd + , documents: options.documents || [] + , cursorIndex: 0 + , dead: false + , killed: false + , init: false + , notified: false + , limit: options.limit || cmd.limit || 0 + , skip: options.skip || cmd.skip || 0 + , batchSize: options.batchSize || cmd.batchSize || 1000 + , currentLimit: 0 + // Result field name if not a cursor (contains the array of results) + , transforms: options.transforms + } + + // Add promoteLong to cursor state + if(typeof topologyOptions.promoteLongs == 'boolean') { + this.cursorState.promoteLongs = topologyOptions.promoteLongs; + } + + // Logger + this.logger = Logger('Cursor', topologyOptions); + + // + // Did we pass in a cursor id + if(typeof cmd == 'number') { + this.cursorState.cursorId = Long.fromNumber(cmd); + this.cursorState.lastCursorId = this.cursorState.cursorId; + } else if(cmd instanceof Long) { + this.cursorState.cursorId = cmd; + this.cursorState.lastCursorId = cmd; + } +} + +Cursor.prototype.setCursorBatchSize = function(value) { + this.cursorState.batchSize = value; +} + +Cursor.prototype.cursorBatchSize = function() { + return this.cursorState.batchSize; +} + +Cursor.prototype.setCursorLimit = function(value) { + this.cursorState.limit = value; +} + +Cursor.prototype.cursorLimit = function() { + return this.cursorState.limit; +} + +Cursor.prototype.setCursorSkip = function(value) { + this.cursorState.skip = value; +} + +Cursor.prototype.cursorSkip = function() { + return this.cursorState.skip; +} + +// +// Handle callback (including any exceptions thrown) +var handleCallback = function(callback, err, result) { + try { + callback(err, result); + } catch(err) { + process.nextTick(function() { + throw err; + }); + } +} + +// Internal methods +Cursor.prototype._find = function(callback) { + var self = this; + + if(self.logger.isDebug()) { + self.logger.debug(f("issue initial query [%s] with flags [%s]" + , JSON.stringify(self.cmd) + , JSON.stringify(self.query))); + } + + var queryCallback = function(err, r) { + if(err) return callback(err); + + // Get the raw message + var result = r.message; + + // Query failure bit set + if(result.queryFailure) { + return callback(MongoError.create(result.documents[0]), null); + } + + // Check if we have a command cursor + if(Array.isArray(result.documents) && result.documents.length == 1 + && (!self.cmd.find || (self.cmd.find && self.cmd.virtual == false)) + && (result.documents[0].cursor != 'string' + || result.documents[0]['$err'] + || result.documents[0]['errmsg'] + || Array.isArray(result.documents[0].result)) + ) { + + // We have a an error document return the error + if(result.documents[0]['$err'] + || result.documents[0]['errmsg']) { + return callback(MongoError.create(result.documents[0]), null); + } + + // We have a cursor document + if(result.documents[0].cursor != null + && typeof result.documents[0].cursor != 'string') { + var id = result.documents[0].cursor.id; + // If we have a namespace change set the new namespace for getmores + if(result.documents[0].cursor.ns) { + self.ns = result.documents[0].cursor.ns; + } + // Promote id to long if needed + self.cursorState.cursorId = typeof id == 'number' ? Long.fromNumber(id) : id; + self.cursorState.lastCursorId = self.cursorState.cursorId; + // If we have a firstBatch set it + if(Array.isArray(result.documents[0].cursor.firstBatch)) { + self.cursorState.documents = result.documents[0].cursor.firstBatch;//.reverse(); + } + + // Return after processing command cursor + return callback(null, null); + } + + if(Array.isArray(result.documents[0].result)) { + self.cursorState.documents = result.documents[0].result; + self.cursorState.cursorId = Long.ZERO; + return callback(null, null); + } + } + + // Otherwise fall back to regular find path + self.cursorState.cursorId = result.cursorId; + self.cursorState.documents = result.documents; + self.cursorState.lastCursorId = result.cursorId; + + // Transform the results with passed in transformation method if provided + if(self.cursorState.transforms && typeof self.cursorState.transforms.query == 'function') { + self.cursorState.documents = self.cursorState.transforms.query(result); + } + + // Return callback + callback(null, null); + } + + // Options passed to the pool + var queryOptions = {}; + + // If we have a raw query decorate the function + if(self.options.raw || self.cmd.raw) { + // queryCallback.raw = self.options.raw || self.cmd.raw; + queryOptions.raw = self.options.raw || self.cmd.raw; + } + + // Do we have documentsReturnedIn set on the query + if(typeof self.query.documentsReturnedIn == 'string') { + // queryCallback.documentsReturnedIn = self.query.documentsReturnedIn; + queryOptions.documentsReturnedIn = self.query.documentsReturnedIn; + } + + // Add promote Long value if defined + if(typeof self.cursorState.promoteLongs == 'boolean') { + // queryCallback.promoteLongs = self.cursorState.promoteLongs; + queryOptions.promoteLongs = self.cursorState.promoteLongs; + } + + // Write the initial command out + self.server.s.pool.write(self.query.toBin(), queryOptions, queryCallback); +} + +Cursor.prototype._getmore = function(callback) { + if(this.logger.isDebug()) this.logger.debug(f("schedule getMore call for query [%s]", JSON.stringify(this.query))) + // Determine if it's a raw query + var raw = this.options.raw || this.cmd.raw; + + // Set the current batchSize + var batchSize = this.cursorState.batchSize; + if(this.cursorState.limit > 0 + && ((this.cursorState.currentLimit + batchSize) > this.cursorState.limit)) { + batchSize = this.cursorState.limit - this.cursorState.currentLimit; + } + + // Default pool + var pool = this.server.s.pool; + + // We have a wire protocol handler + this.server.wireProtocolHandler.getMore(this.bson, this.ns, this.cursorState, batchSize, raw, pool, this.options, callback); +} + +Cursor.prototype._killcursor = function(callback) { + // Set cursor to dead + this.cursorState.dead = true; + this.cursorState.killed = true; + // Remove documents + this.cursorState.documents = []; + + // If no cursor id just return + if(this.cursorState.cursorId == null || this.cursorState.cursorId.isZero() || this.cursorState.init == false) { + if(callback) callback(null, null); + return; + } + + // // Default pool + // var pool = this.pool; + // // If we have a connection get the corresponding pool + // if(this.connection && this.connection.isConnected()) { + // // Get the server + // var server = this.topology.getServerFrom(this.connection); + // // Get the pool + // if(server && server.s.pool) { + // pool = server.s.pool; + // } + // } + + // Default pool + var pool = this.server.s.pool; + // // Set cursorId to ZERO + // this.cursorState.cursorId = Long.fromNumber(0); + // Execute command + this.server.wireProtocolHandler.killCursor(this.bson, this.ns, this.cursorState.cursorId, pool, callback); +} + +/** + * Clone the cursor + * @method + * @return {Cursor} + */ +Cursor.prototype.clone = function() { + return this.topology.cursor(this.ns, this.cmd, this.options); +} + +/** + * Checks if the cursor is dead + * @method + * @return {boolean} A boolean signifying if the cursor is dead or not + */ +Cursor.prototype.isDead = function() { + return this.cursorState.dead == true; +} + +/** + * Checks if the cursor was killed by the application + * @method + * @return {boolean} A boolean signifying if the cursor was killed by the application + */ +Cursor.prototype.isKilled = function() { + return this.cursorState.killed == true; +} + +/** + * Checks if the cursor notified it's caller about it's death + * @method + * @return {boolean} A boolean signifying if the cursor notified the callback + */ +Cursor.prototype.isNotified = function() { + return this.cursorState.notified == true; +} + +/** + * Returns current buffered documents length + * @method + * @return {number} The number of items in the buffered documents + */ +Cursor.prototype.bufferedCount = function() { + return this.cursorState.documents.length - this.cursorState.cursorIndex; +} + +/** + * Returns current buffered documents + * @method + * @return {Array} An array of buffered documents + */ +Cursor.prototype.readBufferedDocuments = function(number) { + var unreadDocumentsLength = this.cursorState.documents.length - this.cursorState.cursorIndex; + var length = number < unreadDocumentsLength ? number : unreadDocumentsLength; + var elements = this.cursorState.documents.slice(this.cursorState.cursorIndex, this.cursorState.cursorIndex + length); + + // Transform the doc with passed in transformation method if provided + if(this.cursorState.transforms && typeof this.cursorState.transforms.doc == 'function') { + // Transform all the elements + for(var i = 0; i < elements.length; i++) { + elements[i] = this.cursorState.transforms.doc(elements[i]); + } + } + + // Ensure we do not return any more documents than the limit imposed + // Just return the number of elements up to the limit + if(this.cursorState.limit > 0 && (this.cursorState.currentLimit + elements.length) > this.cursorState.limit) { + elements = elements.slice(0, (this.cursorState.limit - this.cursorState.currentLimit)); + this.kill(); + } + + // Adjust current limit + this.cursorState.currentLimit = this.cursorState.currentLimit + elements.length; + this.cursorState.cursorIndex = this.cursorState.cursorIndex + elements.length; + + // Return elements + return elements; +} + +/** + * Kill the cursor + * @method + * @param {resultCallback} callback A callback function + */ +Cursor.prototype.kill = function(callback) { + this._killcursor(callback); +} + +/** + * Resets the cursor + * @method + * @return {null} + */ +Cursor.prototype.rewind = function() { + if(this.cursorState.init) { + if(!this.cursorState.dead) { + this.kill(); + } + + this.cursorState.currentLimit = 0; + this.cursorState.init = false; + this.cursorState.dead = false; + this.cursorState.killed = false; + this.cursorState.notified = false; + this.cursorState.documents = []; + this.cursorState.cursorId = null; + this.cursorState.cursorIndex = 0; + } +} + +/** + * Validate if the pool is dead and return error + */ +var isConnectionDead = function(self, callback) { + if(self.pool + && self.pool.isDestroyed()) { + self.cursorState.notified = true; + self.cursorState.killed = true; + self.cursorState.documents = []; + self.cursorState.cursorIndex = 0; + callback(MongoError.create(f('connection to host %s:%s was destroyed', self.pool.host, self.pool.port))) + return true; + } + + return false; +} + +/** + * Validate if the cursor is dead but was not explicitly killed by user + */ +var isCursorDeadButNotkilled = function(self, callback) { + // Cursor is dead but not marked killed, return null + if(self.cursorState.dead && !self.cursorState.killed) { + self.cursorState.notified = true; + self.cursorState.killed = true; + self.cursorState.documents = []; + self.cursorState.cursorIndex = 0; + handleCallback(callback, null, null); + return true; + } + + return false; +} + +/** + * Validate if the cursor is dead and was killed by user + */ +var isCursorDeadAndKilled = function(self, callback) { + if(self.cursorState.dead && self.cursorState.killed) { + handleCallback(callback, MongoError.create("cursor is dead")); + return true; + } + + return false; +} + +/** + * Validate if the cursor was killed by the user + */ +var isCursorKilled = function(self, callback) { + if(self.cursorState.killed) { + self.cursorState.notified = true; + self.cursorState.documents = []; + self.cursorState.cursorIndex = 0; + handleCallback(callback, null, null); + return true; + } + + return false; +} + +/** + * Mark cursor as being dead and notified + */ +var setCursorDeadAndNotified = function(self, callback) { + self.cursorState.dead = true; + self.cursorState.notified = true; + self.cursorState.documents = []; + self.cursorState.cursorIndex = 0; + handleCallback(callback, null, null); +} + +/** + * Mark cursor as being notified + */ +var setCursorNotified = function(self, callback) { + self.cursorState.notified = true; + self.cursorState.documents = []; + self.cursorState.cursorIndex = 0; + handleCallback(callback, null, null); +} + +var push = Array.prototype.push; + +var nextFunction = function(self, callback) { + // We have notified about it + if(self.cursorState.notified) { + return callback(new Error('cursor is exhausted')); + } + + // Cursor is killed return null + if(isCursorKilled(self, callback)) return; + + // Cursor is dead but not marked killed, return null + if(isCursorDeadButNotkilled(self, callback)) return; + + // We have a dead and killed cursor, attempting to call next should error + if(isCursorDeadAndKilled(self, callback)) return; + + // We have just started the cursor + if(!self.cursorState.init) { + // Topology is not connected, save the call in the provided store to be + // Executed at some point when the handler deems it's reconnected + if(!self.topology.isConnected(self.options) && self.disconnectHandler != null) { + return self.disconnectHandler.addObjectAndMethod('cursor', self, 'next', [callback], callback); + } + + try { + self.server = self.topology.getServer(self.options); + } catch(err) { + // Handle the error and add object to next method call + if(self.disconnectHandler != null) { + return self.disconnectHandler.addObjectAndMethod('cursor', self, 'next', [callback], callback); + } + + // Otherwise return the error + return callback(err); + } + + // Set as init + self.cursorState.init = true; + + try { + self.query = self.server.wireProtocolHandler.command(self.bson, self.ns, self.cmd, self.cursorState, self.topology, self.options); + } catch(err) { + return callback(err); + } + } + + // If we don't have a cursorId execute the first query + if(self.cursorState.cursorId == null) { + // Check if pool is dead and return if not possible to + // execute the query against the db + if(isConnectionDead(self, callback)) return; + + // Check if topology is destroyed + if(self.topology.isDestroyed()) return callback(new MongoError(f('connection destroyed, not possible to instantiate cursor'))); + + // query, cmd, options, cursorState, callback + self._find(function(err, r) { + if(err) return handleCallback(callback, err, null); + + if(self.cursorState.documents.length == 0 + && self.cursorState.cursorId && self.cursorState.cursorId.isZero() + && !self.cmd.tailable && !self.cmd.awaitData) { + return setCursorNotified(self, callback); + } + + nextFunction(self, callback); + }); + } else if(self.cursorState.limit > 0 && self.cursorState.currentLimit >= self.cursorState.limit) { + // Ensure we kill the cursor on the server + self.kill(); + // Set cursor in dead and notified state + return setCursorDeadAndNotified(self, callback); + } else if(self.cursorState.cursorIndex == self.cursorState.documents.length + && !Long.ZERO.equals(self.cursorState.cursorId)) { + // Ensure an empty cursor state + self.cursorState.documents = []; + self.cursorState.cursorIndex = 0; + + // Check if topology is destroyed + if(self.topology.isDestroyed()) return callback(new MongoError(f('connection destroyed, not possible to instantiate cursor'))); + + // Check if connection is dead and return if not possible to + // execute a getmore on this connection + if(isConnectionDead(self, callback)) return; + + // Execute the next get more + self._getmore(function(err, doc, connection) { + if(err) return handleCallback(callback, err); + + // Save the returned connection to ensure all getMore's fire over the same connection + self.connection = connection; + + // Tailable cursor getMore result, notify owner about it + // No attempt is made here to retry, this is left to the user of the + // core module to handle to keep core simple + if(self.cursorState.documents.length == 0 + && self.cmd.tailable && Long.ZERO.equals(self.cursorState.cursorId)) { + // No more documents in the tailed cursor + return handleCallback(callback, MongoError.create({ + message: "No more documents in tailed cursor" + , tailable: self.cmd.tailable + , awaitData: self.cmd.awaitData + })); + } else if(self.cursorState.documents.length == 0 + && self.cmd.tailable && !Long.ZERO.equals(self.cursorState.cursorId)) { + return nextFunction(self, callback); + } + + if(self.cursorState.limit > 0 && self.cursorState.currentLimit >= self.cursorState.limit) { + return setCursorDeadAndNotified(self, callback); + } + + nextFunction(self, callback); + }); + } else if(self.cursorState.documents.length == self.cursorState.cursorIndex + && self.cmd.tailable && Long.ZERO.equals(self.cursorState.cursorId)) { + return handleCallback(callback, MongoError.create({ + message: "No more documents in tailed cursor" + , tailable: self.cmd.tailable + , awaitData: self.cmd.awaitData + })); + } else if(self.cursorState.documents.length == self.cursorState.cursorIndex + && Long.ZERO.equals(self.cursorState.cursorId)) { + setCursorDeadAndNotified(self, callback); + } else { + if(self.cursorState.limit > 0 && self.cursorState.currentLimit >= self.cursorState.limit) { + // Ensure we kill the cursor on the server + self.kill(); + // Set cursor in dead and notified state + return setCursorDeadAndNotified(self, callback); + } + + // Increment the current cursor limit + self.cursorState.currentLimit += 1; + + // Get the document + var doc = self.cursorState.documents[self.cursorState.cursorIndex++]; + + // Doc overflow + if(doc.$err) { + // Ensure we kill the cursor on the server + self.kill(); + // Set cursor in dead and notified state + return setCursorDeadAndNotified(self, function() { + handleCallback(callback, new MongoError(doc.$err)); + }); + } + + // Transform the doc with passed in transformation method if provided + if(self.cursorState.transforms && typeof self.cursorState.transforms.doc == 'function') { + doc = self.cursorState.transforms.doc(doc); + } + + // Return the document + handleCallback(callback, null, doc); + } +} + +/** + * Retrieve the next document from the cursor + * @method + * @param {resultCallback} callback A callback function + */ +Cursor.prototype.next = function(callback) { + nextFunction(this, callback); +} + +module.exports = Cursor; diff --git a/node_modules/mongodb-core/lib/error.js b/node_modules/mongodb-core/lib/error.js new file mode 100644 index 0000000..31ede94 --- /dev/null +++ b/node_modules/mongodb-core/lib/error.js @@ -0,0 +1,44 @@ +"use strict"; + +/** + * Creates a new MongoError + * @class + * @augments Error + * @param {string} message The error message + * @return {MongoError} A MongoError instance + */ +function MongoError(message) { + this.name = 'MongoError'; + this.message = message; + Error.captureStackTrace(this, MongoError); +} + +/** + * Creates a new MongoError object + * @method + * @param {object} options The error options + * @return {MongoError} A MongoError instance + */ +MongoError.create = function(options) { + var err = null; + + if(options instanceof Error) { + err = new MongoError(options.message); + err.stack = options.stack; + } else if(typeof options == 'string') { + err = new MongoError(options); + } else { + err = new MongoError(options.message || options.errmsg || options.$err || "n/a"); + // Other options + for(var name in options) { + err[name] = options[name]; + } + } + + return err; +} + +// Extend JavaScript error +MongoError.prototype = new Error; + +module.exports = MongoError; diff --git a/node_modules/mongodb-core/lib/tools/smoke_plugin.js b/node_modules/mongodb-core/lib/tools/smoke_plugin.js new file mode 100644 index 0000000..dcceda4 --- /dev/null +++ b/node_modules/mongodb-core/lib/tools/smoke_plugin.js @@ -0,0 +1,59 @@ +var fs = require('fs'); + +/* Note: because this plugin uses process.on('uncaughtException'), only one + * of these can exist at any given time. This plugin and anything else that + * uses process.on('uncaughtException') will conflict. */ +exports.attachToRunner = function(runner, outputFile) { + var smokeOutput = { results : [] }; + var runningTests = {}; + + var integraPlugin = { + beforeTest: function(test, callback) { + test.startTime = Date.now(); + runningTests[test.name] = test; + callback(); + }, + afterTest: function(test, callback) { + smokeOutput.results.push({ + status: test.status, + start: test.startTime, + end: Date.now(), + test_file: test.name, + exit_code: 0, + url: "" + }); + delete runningTests[test.name]; + callback(); + }, + beforeExit: function(obj, callback) { + fs.writeFile(outputFile, JSON.stringify(smokeOutput), function() { + callback(); + }); + } + }; + + // In case of exception, make sure we write file + process.on('uncaughtException', function(err) { + // Mark all currently running tests as failed + for (var testName in runningTests) { + smokeOutput.results.push({ + status: "fail", + start: runningTests[testName].startTime, + end: Date.now(), + test_file: testName, + exit_code: 0, + url: "" + }); + } + + // write file + fs.writeFileSync(outputFile, JSON.stringify(smokeOutput)); + + // Standard NodeJS uncaught exception handler + console.error(err.stack); + process.exit(1); + }); + + runner.plugin(integraPlugin); + return integraPlugin; +}; diff --git a/node_modules/mongodb-core/lib/topologies/mongos.js b/node_modules/mongodb-core/lib/topologies/mongos.js new file mode 100644 index 0000000..98987f2 --- /dev/null +++ b/node_modules/mongodb-core/lib/topologies/mongos.js @@ -0,0 +1,1079 @@ +"use strict" + +var inherits = require('util').inherits, + f = require('util').format, + EventEmitter = require('events').EventEmitter, + BSON = require('bson').native().BSON, + ReadPreference = require('./read_preference'), + BasicCursor = require('../cursor'), + Logger = require('../connection/logger'), + debugOptions = require('../connection/utils').debugOptions, + MongoError = require('../error'), + Server = require('./server'), + ReplSetState = require('./replset_state'), + assign = require('./shared').assign; + +/** + * @fileOverview The **Mongos** class is a class that represents a Mongos Proxy topology and is + * used to construct connections. + * + * @example + * var Mongos = require('mongodb-core').Mongos + * , ReadPreference = require('mongodb-core').ReadPreference + * , assert = require('assert'); + * + * var server = new Mongos([{host: 'localhost', port: 30000}]); + * // Wait for the connection event + * server.on('connect', function(server) { + * server.destroy(); + * }); + * + * // Start connecting + * server.connect(); + */ + +var MongoCR = require('../auth/mongocr') + , X509 = require('../auth/x509') + , Plain = require('../auth/plain') + , GSSAPI = require('../auth/gssapi') + , SSPI = require('../auth/sspi') + , ScramSHA1 = require('../auth/scram'); + +// +// States +var DISCONNECTED = 'disconnected'; +var CONNECTING = 'connecting'; +var CONNECTED = 'connected'; +var DESTROYED = 'destroyed'; + +function stateTransition(self, newState) { + var legalTransitions = { + 'disconnected': [CONNECTING, DESTROYED, DISCONNECTED], + 'connecting': [CONNECTING, DESTROYED, CONNECTED, DISCONNECTED], + 'connected': [CONNECTED, DISCONNECTED, DESTROYED], + 'destroyed': [DESTROYED] + } + + // Get current state + var legalStates = legalTransitions[self.state]; + if(legalStates && legalStates.indexOf(newState) != -1) { + self.state = newState; + } else { + self.logger.error(f('Pool with id [%s] failed attempted illegal state transition from [%s] to [%s] only following state allowed [%s]' + , self.id, self.state, newState, legalStates)); + } +} + +// +// ReplSet instance id +var id = 1; +var handlers = ['connect', 'close', 'error', 'timeout', 'parseError']; + +/** + * Creates a new Mongos instance + * @class + * @param {array} seedlist A list of seeds for the replicaset + * @param {number} [options.haInterval=5000] The High availability period for replicaset inquiry + * @param {Cursor} [options.cursorFactory=Cursor] The cursor factory class used for all query cursors + * @param {number} [options.size=5] Server connection pool size + * @param {boolean} [options.keepAlive=true] TCP Connection keep alive enabled + * @param {number} [options.keepAliveInitialDelay=0] Initial delay before TCP keep alive enabled + * @param {number} [options.localThresholdMS=15] Cutoff latency point in MS for MongoS proxy selection + * @param {boolean} [options.noDelay=true] TCP Connection no delay + * @param {number} [options.connectionTimeout=1000] TCP Connection timeout setting + * @param {number} [options.socketTimeout=0] TCP Socket timeout setting + * @param {boolean} [options.singleBufferSerializtion=true] Serialize into single buffer, trade of peak memory for serialization speed + * @param {boolean} [options.ssl=false] Use SSL for connection + * @param {boolean|function} [options.checkServerIdentity=true] Ensure we check server identify during SSL, set to false to disable checking. Only works for Node 0.12.x or higher. You can pass in a boolean or your own checkServerIdentity override function. + * @param {Buffer} [options.ca] SSL Certificate store binary buffer + * @param {Buffer} [options.cert] SSL Certificate binary buffer + * @param {Buffer} [options.key] SSL Key file binary buffer + * @param {string} [options.passphrase] SSL Certificate pass phrase + * @param {boolean} [options.rejectUnauthorized=true] Reject unauthorized server certificates + * @param {boolean} [options.promoteLongs=true] Convert Long values from the db into Numbers if they fit into 53 bits + * @return {Mongos} A cursor instance + * @fires Mongos#connect + * @fires Mongos#reconnect + * @fires Mongos#joined + * @fires Mongos#left + * @fires Mongos#failed + * @fires Mongos#fullsetup + * @fires Mongos#all + * @fires Mongos#serverHeartbeatStarted + * @fires Mongos#serverHeartbeatSucceeded + * @fires Mongos#serverHeartbeatFailed + * @fires Mongos#topologyOpening + * @fires Mongos#topologyClosed + * @fires Mongos#topologyDescriptionChanged + */ +var Mongos = function(seedlist, options) { + var self = this; + options = options || {}; + + // Get replSet Id + this.id = id++; + + // Internal state + this.s = { + options: assign({}, options), + // BSON instance + bson: options.bson || new BSON(), + // Factory overrides + Cursor: options.cursorFactory || BasicCursor, + // Logger instance + logger: Logger('Mongos', options), + // Seedlist + seedlist: seedlist, + // Ha interval + haInterval: options.haInterval ? options.haInterval : 10000, + // Disconnect handler + disconnectHandler: options.disconnectHandler, + // Server selection index + index: 0, + // Connect function options passed in + connectOptions: {}, + // Are we running in debug mode + debug: typeof options.debug == 'boolean' ? options.debug : false, + // localThresholdMS + localThresholdMS: options.localThresholdMS || 15, + } + + // Log info warning if the socketTimeout < haInterval as it will cause + // a lot of recycled connections to happen. + if(this.s.logger.isWarn() + && this.s.options.socketTimeout != 0 + && this.s.options.socketTimeout < this.s.haInterval) { + this.s.logger.warn(f('warning socketTimeout %s is less than haInterval %s. This might cause unnecessary server reconnections due to socket timeouts' + , this.s.options.socketTimeout, this.s.haInterval)); + } + + // All the authProviders + this.authProviders = options.authProviders || { + 'mongocr': new MongoCR(this.s.bson), 'x509': new X509(this.s.bson) + , 'plain': new Plain(this.s.bson), 'gssapi': new GSSAPI(this.s.bson) + , 'sspi': new SSPI(this.s.bson), 'scram-sha-1': new ScramSHA1(this.s.bson) + } + + // Disconnected state + this.state = DISCONNECTED; + + // Current proxies we are connecting to + this.connectingProxies = []; + // Currently connected proxies + this.connectedProxies = []; + // Disconnected proxies + this.disconnectedProxies = []; + // Are we authenticating + this.authenticating = false; + // Index of proxy to run operations against + this.index = 0; + // High availability timeout id + this.haTimeoutId = null; + // Last ismaster + this.ismaster = null; + // Lower bound latency + this.lowerBoundLatency = Number.MAX_VALUE; + + // Add event listener + EventEmitter.call(this); +} + +inherits(Mongos, EventEmitter); + +Object.defineProperty(Mongos.prototype, 'type', { + enumerable:true, get: function() { return 'mongos'; } +}); + +/** + * Emit event if it exists + * @method + */ +function emitSDAMEvent(self, event, description) { + if(self.listeners(event).length > 0) { + self.emit(event, description); + } +} + +/** + * Initiate server connect + * @method + * @param {array} [options.auth=null] Array of auth options to apply on connect + */ +Mongos.prototype.connect = function(options) { + var self = this; + // Add any connect level options to the internal state + this.s.connectOptions = options || {}; + // Set connecting state + stateTransition(this, CONNECTING); + // Create server instances + var servers = this.s.seedlist.map(function(x) { + return new Server(assign({}, self.s.options, x, { + authProviders: self.authProviders, reconnect:false, monitoring:false, inTopology: true + })); + }); + + // Emit the topology opening event + emitSDAMEvent(this, 'topologyOpening', { topologyId: this.id }); + + // Start all server connections + connectProxies(self, servers); +} + +function handleEvent(self, event) { + return function(err) { + if(self.state == DESTROYED) return; + // Move to list of disconnectedProxies + moveServerFrom(self.connectedProxies, self.disconnectedProxies, this); + // Emit the left signal + self.emit('left', 'mongos', this); + } +} + +function handleInitialConnectEvent(self, event) { + return function(err) { + // Destroy the instance + if(self.state == DESTROYED) { + return this.destroy(); + } + + // Check the type of server + if(event == 'connect') { + // Get last known ismaster + self.ismaster = this.lastIsMaster(); + + // Is this not a proxy, remove t + if(self.ismaster.msg == 'isdbgrid') { + // Add to the connectd list + for(var i = 0; i < self.connectedProxies.length; i++) { + if(self.connectedProxies[i].name == this.name) { + this.destroy(); + return self.emit('failed', this); + } + } + + // Remove the handlers + for(var i = 0; i < handlers.length; i++) { + this.removeAllListeners(handlers[i]); + } + + // Add stable state handlers + this.on('error', handleEvent(self, 'error')); + this.on('close', handleEvent(self, 'close')); + this.on('timeout', handleEvent(self, 'timeout')); + this.on('parseError', handleEvent(self, 'parseError')); + + // Move from connecting proxies connected + moveServerFrom(self.connectingProxies, self.connectedProxies, this); + // Emit the joined event + self.emit('joined', 'mongos', this); + } else { + // This is not a mongos proxy, remove it completely + removeProxyFrom(self.connectingProxies, this); + // Emit the left event + self.emit('left', 'server', this); + // Emit failed event + self.emit('failed', this); + } + } else { + moveServerFrom(self.connectingProxies, self.disconnectedProxies, this); + // Emit the left event + self.emit('left', 'mongos', this); + // Emit failed event + self.emit('failed', this); + } + + // Trigger topologyMonitor + if(self.connectingProxies.length == 0) { + // Emit connected if we are connected + if(self.connectedProxies.length > 0) { + // Set initial lowerBoundLatency + for(var i = 0; i < self.connectedProxies.length; i++) { + // Adjust lower bound + if(self.lowerBoundLatency > self.connectedProxies[i].lastIsMasterMS) { + self.lowerBoundLatency = self.connectedProxies[i].lastIsMasterMS; + } + } + + // Set the state to connected + stateTransition(self, CONNECTED); + // Emit the connect event + self.emit('connect', self); + self.emit('fullsetup', self); + self.emit('all', self); + } else if(self.disconnectedProxies.length == 0) { + return self.emit('error', new MongoError('no mongos proxies found in seed list')); + } + + // Topology monitor + topologyMonitor(self, {firstConnect:true}); + } + }; +} + +function connectProxies(self, servers) { + // Update connectingProxies + self.connectingProxies = self.connectingProxies.concat(servers); + + // Index used to interleaf the server connects, avoiding + // runtime issues on io constrained vm's + var timeoutInterval = 0; + + function connect(server, timeoutInterval) { + setTimeout(function() { + // Add event handlers + server.once('close', handleInitialConnectEvent(self, 'close')); + server.once('timeout', handleInitialConnectEvent(self, 'timeout')); + server.once('parseError', handleInitialConnectEvent(self, 'parseError')); + server.once('error', handleInitialConnectEvent(self, 'error')); + server.once('connect', handleInitialConnectEvent(self, 'connect')); + // SDAM Monitoring events + server.on('serverOpening', function(e) { self.emit('serverOpening', e); }); + server.on('serverDescriptionChanged', function(e) { self.emit('serverDescriptionChanged', e); }); + server.on('serverClosed', function(e) { self.emit('serverClosed', e); }); + // Start connection + server.connect(self.s.connectOptions); + }, timeoutInterval); + } + // Start all the servers + while(servers.length > 0) { + connect(servers.shift(), timeoutInterval++); + } +} + +function pickProxy(self) { + // Get the currently connected Proxies + var connectedProxies = self.connectedProxies.slice(0); + + // Filter out the possible servers + connectedProxies = connectedProxies.filter(function(server) { + if((server.lastIsMasterMS <= (self.lowerBoundLatency + self.s.localThresholdMS)) + && server.isConnected()) { + return true; + } + }); + + // We have no connectedProxies pick first of the connected ones + if(connectedProxies.length == 0) { + return self.connectedProxies[0]; + } + + // Get proxy + var proxy = connectedProxies[self.index % connectedProxies.length]; + // Update the index + self.index = (self.index + 1) % connectedProxies.length; + // Return the proxy + return proxy; +} + +function moveServerFrom(from, to, proxy) { + for(var i = 0; i < from.length; i++) { + if(from[i].name == proxy.name) { + from.splice(i, 1); + } + } + + for(var i = 0; i < to.length; i++) { + if(to[i].name == proxy.name) { + to.splice(i, 1); + } + } + + to.push(proxy); +} + +function removeProxyFrom(from, proxy) { + for(var i = 0; i < from.length; i++) { + if(from[i].name == proxy.name) { + from.splice(i, 1); + } + } +} + +function reconnectProxies(self, proxies, callback) { + // Count lefts + var count = proxies.length; + + // Handle events + var _handleEvent = function(self, event) { + return function(err, r) { + var _self = this; + count = count - 1; + + // Destroyed + if(self.state == DESTROYED) { + return this.destroy(); + } + + if(event == 'connect' && !self.authenticating) { + // Destroyed + if(self.state == DESTROYED) { + return _self.destroy(); + } + + // Remove the handlers + for(var i = 0; i < handlers.length; i++) { + _self.removeAllListeners(handlers[i]); + } + + // Add stable state handlers + _self.on('error', handleEvent(self, 'error')); + _self.on('close', handleEvent(self, 'close')); + _self.on('timeout', handleEvent(self, 'timeout')); + _self.on('parseError', handleEvent(self, 'parseError')); + + // Move to the connected servers + moveServerFrom(self.disconnectedProxies, self.connectedProxies, _self); + // Emit joined event + self.emit('joined', 'mongos', _self); + } else if(event == 'connect' && self.authenticating) { + this.destroy(); + } + + // Are we done finish up callback + if(count == 0) { + callback(); + } + } + } + + // No new servers + if(count == 0) { + return callback(); + } + + // Execute method + function execute(_server, i) { + setTimeout(function() { + // Destroyed + if(self.state == DESTROYED) { + return; + } + + // Create a new server instance + var server = new Server(assign({}, self.s.options, { + host: _server.name.split(':')[0], + port: parseInt(_server.name.split(':')[1], 10) + }, { + authProviders: self.authProviders, reconnect:false, monitoring: false, inTopology: true + })); + + // Add temp handlers + server.once('connect', _handleEvent(self, 'connect')); + server.once('close', _handleEvent(self, 'close')); + server.once('timeout', _handleEvent(self, 'timeout')); + server.once('error', _handleEvent(self, 'error')); + server.once('parseError', _handleEvent(self, 'parseError')); + + // SDAM Monitoring events + server.on('serverOpening', function(e) { self.emit('serverOpening', e); }); + server.on('serverDescriptionChanged', function(e) { self.emit('serverDescriptionChanged', e); }); + server.on('serverClosed', function(e) { self.emit('serverClosed', e); }); + server.connect(self.s.connectOptions); + }, i); + } + + // Create new instances + for(var i = 0; i < proxies.length; i++) { + execute(proxies[i], i); + } +} + +function topologyMonitor(self, options) { + options = options || {}; + + // Set momitoring timeout + self.haTimeoutId = setTimeout(function() { + if(self.state == DESTROYED) return; + // If we have a primary and a disconnect handler, execute + // buffered operations + if(self.isConnected() && self.s.disconnectHandler) { + self.s.disconnectHandler.execute(); + } + + // Get the connectingServers + var proxies = self.connectedProxies.slice(0); + // Get the count + var count = proxies.length; + + // If the count is zero schedule a new fast + function pingServer(_self, _server, cb) { + // Measure running time + var start = new Date().getTime(); + + // Emit the server heartbeat start + emitSDAMEvent(self, 'serverHeartbeatStarted', { connectionId: _server.name }); + + // Execute ismaster + _server.command('admin.$cmd', {ismaster:true}, {monitoring: true}, function(err, r) { + if(self.state == DESTROYED) { + _server.destroy(); + return cb(err, r); + } + + // Calculate latency + var latencyMS = new Date().getTime() - start; + + // Adjust lower bound + if(self.lowerBoundLatency > _server.lastIsMasterMS) { + self.lowerBoundLatency = _server.lastIsMasterMS; + } + + // We had an error, remove it from the state + if(err) { + // Emit the server heartbeat failure + emitSDAMEvent(self, 'serverHearbeatFailed', { durationMS: latencyMS, failure: err, connectionId: _server.name }); + // Move from connected proxies to disconnected proxies + moveServerFrom(self.connectedProxies, self.disconnectedProxies, _server); + // Emit left event + self.emit('left', 'mongos', _server); + _server.destroy(); + } else { + // Update the server ismaster + _server.ismaster = r.result; + _server.lastIsMasterMS = latencyMS; + + // Server heart beat event + emitSDAMEvent(self, 'serverHeartbeatSucceeded', { durationMS: latencyMS, reply: r.result, connectionId: _server.name }); + } + + cb(err, r); + }); + } + + // No proxies initiate monitor again + if(proxies.length == 0) { + // Emit close event if any listeners registered + if(self.listeners("close").length > 0) { + self.emit('close', self); + } + + // Attempt to connect to any unknown servers + return reconnectProxies(self, self.disconnectedProxies, function(err, cb) { + if(self.state == DESTROYED) return; + + // Are we connected ? emit connect event + if(self.state == CONNECTING && options.firstConnect) { + self.emit('connect', self); + self.emit('fullsetup', self); + self.emit('all', self); + } else if(self.isConnected()) { + self.emit('reconnect', self); + } else if(!self.isConnected() && self.listeners("close").length > 0) { + self.emit('close', self); + } + + // Perform topology monitor + topologyMonitor(self); + }); + } + + // Ping all servers + for(var i = 0; i < proxies.length; i++) { + pingServer(self, proxies[i], function(err, r) { + count = count - 1; + + if(count == 0) { + if(self.state == DESTROYED) return; + + // Attempt to connect to any unknown servers + reconnectProxies(self, self.disconnectedProxies, function(err, cb) { + if(self.state == DESTROYED) return; + // Perform topology monitor + topologyMonitor(self); + }); + } + }); + } + }, self.s.haInterval); +} + +/** + * Returns the last known ismaster document for this server + * @method + * @return {object} + */ +Mongos.prototype.lastIsMaster = function() { + return this.ismaster; +} + +/** + * Unref all connections belong to this server + * @method + */ +Mongos.prototype.unref = function(emitClose) { + // Transition state + stateTransition(this, DISCONNECTED); + // Get all proxies + var proxies = self.connectedProxies.concat(self.connectingProxies); + proxies.forEach(function(x) { + x.unref(); + }); + + clearTimeout(this.haTimeoutId); +} + +/** + * Destroy the server connection + * @method + */ +Mongos.prototype.destroy = function(emitClose) { + // Transition state + stateTransition(this, DESTROYED); + // Get all proxies + var proxies = this.connectedProxies.concat(this.connectingProxies); + // Clear out any monitoring process + if(this.haTimeoutId) clearTimeout(this.haTimeoutId); + // Destroy all connecting servers + proxies.forEach(function(x) { + x.destroy(); + }); + + // Emit toplogy closing event + emitSDAMEvent(this, 'topologyClosed', { topologyId: this.id }); + +} + +/** + * Figure out if the server is connected + * @method + * @return {boolean} + */ +Mongos.prototype.isConnected = function(options) { + return this.connectedProxies.length > 0; +} + +/** + * Figure out if the server instance was destroyed by calling destroy + * @method + * @return {boolean} + */ +Mongos.prototype.isDestroyed = function() { + return this.state == DESTROYED; +} + +// +// Operations +// + +// Execute write operation +var executeWriteOperation = function(self, op, ns, ops, options, callback) { + if(typeof options == 'function') callback = options, options = {}, options = options || {}; + // Ensure we have no options + options = options || {}; + // Pick a server + var server = pickProxy(self); + // No server found error out + if(!server) return callback(new MongoError('no mongos proxy available')); + // Execute the command + server[op](ns, ops, options, 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 {opResultCallback} callback A callback function + */ +Mongos.prototype.insert = function(ns, ops, options, callback) { + if(typeof options == 'function') callback = options, options = {}, options = options || {}; + if(this.state == DESTROYED) return callback(new MongoError(f('topology was destroyed'))); + + // Not connected but we have a disconnecthandler + if(!this.isConnected() && this.s.disconnectHandler != null) { + return this.s.disconnectHandler.add('insert', ns, ops, options, callback); + } + + // No mongos proxy available + if(!this.isConnected()) { + return callback(new MongoError('no mongos proxy available')); + } + + // Execute write operation + executeWriteOperation(this, '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 {opResultCallback} callback A callback function + */ +Mongos.prototype.update = function(ns, ops, options, callback) { + if(typeof options == 'function') callback = options, options = {}, options = options || {}; + if(this.state == DESTROYED) return callback(new MongoError(f('topology was destroyed'))); + + // Not connected but we have a disconnecthandler + if(!this.isConnected() && this.s.disconnectHandler != null) { + return this.s.disconnectHandler.add('insert', ns, ops, options, callback); + } + + // No mongos proxy available + if(!this.isConnected()) { + return callback(new MongoError('no mongos proxy available')); + } + + // Execute write operation + executeWriteOperation(this, '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 {opResultCallback} callback A callback function + */ +Mongos.prototype.remove = function(ns, ops, options, callback) { + if(typeof options == 'function') callback = options, options = {}, options = options || {}; + if(this.state == DESTROYED) return callback(new MongoError(f('topology was destroyed'))); + + // Not connected but we have a disconnecthandler + if(!this.isConnected() && this.s.disconnectHandler != null) { + return this.s.disconnectHandler.add('insert', ns, ops, options, callback); + } + + // No mongos proxy available + if(!this.isConnected()) { + return callback(new MongoError('no mongos proxy available')); + } + + // Execute write operation + executeWriteOperation(this, 'remove', ns, ops, options, callback); +} + +/** + * Execute a command + * @method + * @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 {Connection} [options.connection] Specify connection object to execute command against + * @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 {opResultCallback} callback A callback function + */ +Mongos.prototype.command = function(ns, cmd, options, callback) { + if(typeof options == 'function') callback = options, options = {}, options = options || {}; + if(this.state == DESTROYED) return callback(new MongoError(f('topology was destroyed'))); + var self = this; + + // Establish readPreference + var readPreference = options.readPreference ? options.readPreference : ReadPreference.primary; + + // Pick a proxy + var server = pickProxy(self); + + // No server returned we had an error + if(server == null) { + return callback(new MongoError('no mongos proxy available')); + } + + // Topology is not connected, save the call in the provided store to be + // Executed at some point when the handler deems it's reconnected + if((server == null || !server.isConnected()) && this.s.disconnectHandler != null) { + return this.s.disconnectHandler.add('command', ns, cmd, options, callback); + } + + // Execute the command + server.command(ns, cmd, options, callback); +} + +/** + * Perform one or more remove operations + * @method + * @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1) + * @param {{object}|{Long}} cmd Can be either a command returning a cursor or a cursorId + * @param {object} [options.batchSize=0] Batchsize for the operation + * @param {array} [options.documents=[]] Initial documents list for cursor + * @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.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields. + * @param {opResultCallback} callback A callback function + */ +Mongos.prototype.cursor = function(ns, cmd, cursorOptions) { + cursorOptions = cursorOptions || {}; + var FinalCursor = cursorOptions.cursorFactory || this.s.Cursor; + return new FinalCursor(this.s.bson, ns, cmd, cursorOptions, this, this.s.options); +} + +/** + * Authenticate using a specified mechanism + * @method + * @param {string} mechanism The Auth mechanism we are invoking + * @param {string} db The db we are invoking the mechanism against + * @param {...object} param Parameters for the specific mechanism + * @param {authResultCallback} callback A callback function + */ +Mongos.prototype.auth = function(mechanism, db) { + var allArgs = Array.prototype.slice.call(arguments, 0).slice(0); + var self = this; + var args = Array.prototype.slice.call(arguments, 2); + var callback = args.pop(); + + // If we don't have the mechanism fail + if(this.authProviders[mechanism] == null && mechanism != 'default') { + return callback(new MongoError(f("auth provider %s does not exist", mechanism))); + } + + // Are we already authenticating, throw + if(this.authenticating) { + return callback(new MongoError('authentication or logout allready in process')); + } + + // Topology is not connected, save the call in the provided store to be + // Executed at some point when the handler deems it's reconnected + if(!self.isConnected() && self.s.disconnectHandler != null) { + return self.s.disconnectHandler.add('auth', db, allArgs, {}, callback); + } + + // Set to authenticating + this.authenticating = true; + // All errors + var errors = []; + + // Get all the servers + var servers = this.connectedProxies.slice(0); + // No servers return + if(servers.length == 0) { + this.authenticating = false; + callback(null, true); + } + + // Authenticate + function auth(server) { + // Arguments without a callback + var argsWithoutCallback = [mechanism, db].concat(args.slice(0)); + // Create arguments + var finalArguments = argsWithoutCallback.concat([function(err, r) { + count = count - 1; + // Save all the errors + if(err) errors.push({name: server.name, err: err}); + // We are done + if(count == 0) { + // Auth is done + self.authenticating = false; + + // Return the auth error + if(errors.length) return callback(MongoError.create({ + message: 'authentication fail', errors: errors + }), false); + + // Successfully authenticated session + callback(null, self); + } + }]); + + // Execute the auth only against non arbiter servers + if(!server.lastIsMaster().arbiterOnly) { + server.auth.apply(server, finalArguments); + } + } + + // Get total count + var count = servers.length; + // Authenticate against all servers + while(servers.length > 0) { + auth(servers.shift()); + } +} + +/** + * Logout from a database + * @method + * @param {string} db The db we are logging out from + * @param {authResultCallback} callback A callback function + */ +Mongos.prototype.logout = function(dbName, callback) { + var self = this; + // Are we authenticating or logging out, throw + if(this.authenticating) { + throw new MongoError('authentication or logout allready in process'); + } + + // Ensure no new members are processed while logging out + this.authenticating = true; + + // Remove from all auth providers (avoid any reaplication of the auth details) + var providers = Object.keys(this.authProviders); + for(var i = 0; i < providers.length; i++) { + this.authProviders[providers[i]].logout(dbName); + } + + // Now logout all the servers + var servers = this.connectedProxies.slice(0); + var count = servers.length; + if(count == 0) return callback(); + var errors = []; + + // Execute logout on all server instances + for(var i = 0; i < servers.length; i++) { + servers[i].logout(dbName, function(err) { + count = count - 1; + if(err) errors.push({name: server.name, err: err}); + + if(count == 0) { + // Do not block new operations + self.authenticating = false; + // If we have one or more errors + if(errors.length) return callback(MongoError.create({ + message: f('logout failed against db %s', dbName), errors: errors + }), false); + + // No errors + callback(); + } + }); + } +} + +/** + * Get server + * @method + * @param {ReadPreference} [options.readPreference] Specify read preference if command supports it + * @return {Server} + */ +Mongos.prototype.getServer = function() { + var server = pickProxy(this); + if(this.s.debug) this.emit('pickedServer', null, server); + return server; +} + +/** + * All raw connections + * @method + * @return {Connection[]} + */ +Mongos.prototype.connections = function() { + var connections = []; + + for(var i = 0; i < this.connectedProxies.length; i++) { + connections = connections.concat(this.connectedProxies[i].connections()); + } + + return connections; +} + +/** + * A mongos connect event, used to verify that the connection is up and running + * + * @event Mongos#connect + * @type {Mongos} + */ + +/** + * A mongos reconnect event, used to verify that the mongos topology has reconnected + * + * @event Mongos#reconnect + * @type {Mongos} + */ + +/** + * A mongos fullsetup event, used to signal that all topology members have been contacted. + * + * @event Mongos#fullsetup + * @type {Mongos} + */ + +/** + * A mongos all event, used to signal that all topology members have been contacted. + * + * @event Mongos#all + * @type {Mongos} + */ + +/** + * A server member left the mongos list + * + * @event Mongos#left + * @type {Mongos} + * @param {string} type The type of member that left (mongos) + * @param {Server} server The server object that left + */ + +/** + * A server member joined the mongos list + * + * @event Mongos#joined + * @type {Mongos} + * @param {string} type The type of member that left (mongos) + * @param {Server} server The server object that joined + */ + +/** + * A server opening SDAM monitoring event + * + * @event Mongos#serverOpening + * @type {object} + */ + +/** + * A server closed SDAM monitoring event + * + * @event Mongos#serverClosed + * @type {object} + */ + +/** + * A server description SDAM change monitoring event + * + * @event Mongos#serverDescriptionChanged + * @type {object} + */ + +/** + * A topology open SDAM event + * + * @event Mongos#topologyOpening + * @type {object} + */ + +/** + * A topology closed SDAM event + * + * @event Mongos#topologyClosed + * @type {object} + */ + +/** + * A topology structure SDAM change event + * + * @event Mongos#topologyDescriptionChanged + * @type {object} + */ + +/** + * A topology serverHeartbeatStarted SDAM event + * + * @event Mongos#serverHeartbeatStarted + * @type {object} + */ + +/** + * A topology serverHearbeatFailed SDAM event + * + * @event Mongos#serverHearbeatFailed + * @type {object} + */ + +/** + * A topology serverHeartbeatSucceeded SDAM change event + * + * @event Mongos#serverHeartbeatSucceeded + * @type {object} + */ + +module.exports = Mongos; diff --git a/node_modules/mongodb-core/lib/topologies/read_preference.js b/node_modules/mongodb-core/lib/topologies/read_preference.js new file mode 100644 index 0000000..257b957 --- /dev/null +++ b/node_modules/mongodb-core/lib/topologies/read_preference.js @@ -0,0 +1,106 @@ +"use strict"; + +var needSlaveOk = ['primaryPreferred', 'secondary', 'secondaryPreferred', 'nearest']; + +/** + * @fileOverview The **ReadPreference** class is a class that represents a MongoDB ReadPreference and is + * used to construct connections. + * + * @example + * var ReplSet = require('mongodb-core').ReplSet + * , ReadPreference = require('mongodb-core').ReadPreference + * , assert = require('assert'); + * + * var server = new ReplSet([{host: 'localhost', port: 30000}], {setName: 'rs'}); + * // Wait for the connection event + * server.on('connect', function(server) { + * var cursor = server.cursor('db.test' + * , {find: 'db.test', query: {}} + * , {readPreference: new ReadPreference('secondary')}); + * cursor.next(function(err, doc) { + * server.destroy(); + * }); + * }); + * + * // Start connecting + * server.connect(); + */ + +/** + * Creates a new Pool instance + * @class + * @param {string} preference A string describing the preference (primary|primaryPreferred|secondary|secondaryPreferred|nearest) + * @param {object} tags The tags object + * @param {object} [options] Additional read preference options + * @property {string} preference The preference string (primary|primaryPreferred|secondary|secondaryPreferred|nearest) + * @property {object} tags The tags object + * @property {object} options Additional read preference options + * @return {ReadPreference} + */ +var ReadPreference = function(preference, tags, options) { + this.preference = preference; + this.tags = tags; + this.options = options; +} + +/** + * This needs slaveOk bit set + * @method + * @return {boolean} + */ +ReadPreference.prototype.slaveOk = function() { + return needSlaveOk.indexOf(this.preference) != -1; +} + +/** + * Are the two read preference equal + * @method + * @return {boolean} + */ +ReadPreference.prototype.equals = function(readPreference) { + return readPreference.preference == this.preference; +} + +/** + * Return JSON representation + * @method + * @return {Object} + */ +ReadPreference.prototype.toJSON = function() { + var readPreference = {mode: this.preference}; + if(Array.isArray(this.tags)) readPreference.tags = this.tags; + return readPreference; +} + +/** + * Primary read preference + * @method + * @return {ReadPreference} + */ +ReadPreference.primary = new ReadPreference('primary'); +/** + * Primary Preferred read preference + * @method + * @return {ReadPreference} + */ +ReadPreference.primaryPreferred = new ReadPreference('primaryPreferred'); +/** + * Secondary read preference + * @method + * @return {ReadPreference} + */ +ReadPreference.secondary = new ReadPreference('secondary'); +/** + * Secondary Preferred read preference + * @method + * @return {ReadPreference} + */ +ReadPreference.secondaryPreferred = new ReadPreference('secondaryPreferred'); +/** + * Nearest read preference + * @method + * @return {ReadPreference} + */ +ReadPreference.nearest = new ReadPreference('nearest'); + +module.exports = ReadPreference; diff --git a/node_modules/mongodb-core/lib/topologies/replset.js b/node_modules/mongodb-core/lib/topologies/replset.js new file mode 100644 index 0000000..dc73d59 --- /dev/null +++ b/node_modules/mongodb-core/lib/topologies/replset.js @@ -0,0 +1,1278 @@ +"use strict" + +var inherits = require('util').inherits, + f = require('util').format, + EventEmitter = require('events').EventEmitter, + BSON = require('bson').native().BSON, + ReadPreference = require('./read_preference'), + BasicCursor = require('../cursor'), + Logger = require('../connection/logger'), + debugOptions = require('../connection/utils').debugOptions, + MongoError = require('../error'), + Server = require('./server'), + ReplSetState = require('./replset_state'), + assign = require('./shared').assign; + +var MongoCR = require('../auth/mongocr') + , X509 = require('../auth/x509') + , Plain = require('../auth/plain') + , GSSAPI = require('../auth/gssapi') + , SSPI = require('../auth/sspi') + , ScramSHA1 = require('../auth/scram'); + +// +// States +var DISCONNECTED = 'disconnected'; +var CONNECTING = 'connecting'; +var CONNECTED = 'connected'; +var DESTROYED = 'destroyed'; + +function stateTransition(self, newState) { + var legalTransitions = { + 'disconnected': [CONNECTING, DESTROYED, DISCONNECTED], + 'connecting': [CONNECTING, DESTROYED, CONNECTED, DISCONNECTED], + 'connected': [CONNECTED, DISCONNECTED, DESTROYED], + 'destroyed': [DESTROYED] + } + + // Get current state + var legalStates = legalTransitions[self.state]; + if(legalStates && legalStates.indexOf(newState) != -1) { + self.state = newState; + } else { + self.logger.error(f('Pool with id [%s] failed attempted illegal state transition from [%s] to [%s] only following state allowed [%s]' + , self.id, self.state, newState, legalStates)); + } +} + +// +// ReplSet instance id +var id = 1; +var handlers = ['connect', 'close', 'error', 'timeout', 'parseError']; + +/** + * Creates a new Replset instance + * @class + * @param {array} seedlist A list of seeds for the replicaset + * @param {boolean} options.setName The Replicaset set name + * @param {boolean} [options.secondaryOnlyConnectionAllowed=false] Allow connection to a secondary only replicaset + * @param {number} [options.haInterval=10000] The High availability period for replicaset inquiry + * @param {boolean} [options.emitError=false] Server will emit errors events + * @param {Cursor} [options.cursorFactory=Cursor] The cursor factory class used for all query cursors + * @param {number} [options.size=5] Server connection pool size + * @param {boolean} [options.keepAlive=true] TCP Connection keep alive enabled + * @param {number} [options.keepAliveInitialDelay=0] Initial delay before TCP keep alive enabled + * @param {boolean} [options.noDelay=true] TCP Connection no delay + * @param {number} [options.connectionTimeout=10000] TCP Connection timeout setting + * @param {number} [options.socketTimeout=0] TCP Socket timeout setting + * @param {boolean} [options.singleBufferSerializtion=true] Serialize into single buffer, trade of peak memory for serialization speed + * @param {boolean} [options.ssl=false] Use SSL for connection + * @param {boolean|function} [options.checkServerIdentity=true] Ensure we check server identify during SSL, set to false to disable checking. Only works for Node 0.12.x or higher. You can pass in a boolean or your own checkServerIdentity override function. + * @param {Buffer} [options.ca] SSL Certificate store binary buffer + * @param {Buffer} [options.cert] SSL Certificate binary buffer + * @param {Buffer} [options.key] SSL Key file binary buffer + * @param {string} [options.passphrase] SSL Certificate pass phrase + * @param {boolean} [options.rejectUnauthorized=true] Reject unauthorized server certificates + * @param {boolean} [options.promoteLongs=true] Convert Long values from the db into Numbers if they fit into 53 bits + * @param {number} [options.pingInterval=5000] Ping interval to check the response time to the different servers + * @param {number} [options.localThresholdMS=15] Cutoff latency point in MS for MongoS proxy selection + * @return {ReplSet} A cursor instance + * @fires ReplSet#connect + * @fires ReplSet#ha + * @fires ReplSet#joined + * @fires ReplSet#left + * @fires ReplSet#failed + * @fires ReplSet#fullsetup + * @fires ReplSet#all + * @fires ReplSet#error + * @fires ReplSet#serverHeartbeatStarted + * @fires ReplSet#serverHeartbeatSucceeded + * @fires ReplSet#serverHeartbeatFailed + * @fires ReplSet#topologyOpening + * @fires ReplSet#topologyClosed + * @fires ReplSet#topologyDescriptionChanged + */ +var ReplSet = function(seedlist, options) { + var self = this; + options = options || {}; + + // Validate seedlist + if(!Array.isArray(seedlist)) throw new MongoError("seedlist must be an array"); + // Validate list + if(seedlist.length == 0) throw new MongoError("seedlist must contain at least one entry"); + // Validate entries + seedlist.forEach(function(e) { + if(typeof e.host != 'string' || typeof e.port != 'number') + throw new MongoError("seedlist entry must contain a host and port"); + }); + + // Add event listener + EventEmitter.call(this); + + // Get replSet Id + this.id = id++; + + // Get the localThresholdMS + var localThresholdMS = options.localThresholdMS || 15; + // Backward compatibility + if(options.acceptableLatency) localThresholdMS = options.acceptableLatency; + + // Create a logger + var logger = Logger('ReplSet', options); + + // Internal state + this.s = { + options: assign({}, options), + // BSON instance + bson: options.bson || new BSON(), + // Factory overrides + Cursor: options.cursorFactory || BasicCursor, + // Logger instance + logger: logger, + // Seedlist + seedlist: seedlist, + // Replicaset state + replicaSetState: new ReplSetState({ + id: this.id, setName: options.setName, + acceptableLatency: localThresholdMS, + logger: logger + }), + // Current servers we are connecting to + connectingServers: [], + // Ha interval + haInterval: options.haInterval ? options.haInterval : 10000, + // Minimum heartbeat frequency used if we detect a server close + minHeartbeatFrequencyMS: 500, + // Disconnect handler + disconnectHandler: options.disconnectHandler, + // Server selection index + index: 0, + // Connect function options passed in + connectOptions: {}, + // Are we running in debug mode + debug: typeof options.debug == 'boolean' ? options.debug : false + } + + // Add handler for topology change + this.s.replicaSetState.on('topologyDescriptionChanged', function(r) { self.emit('topologyDescriptionChanged', r); }); + + // Log info warning if the socketTimeout < haInterval as it will cause + // a lot of recycled connections to happen. + if(this.s.logger.isWarn() + && this.s.options.socketTimeout != 0 + && this.s.options.socketTimeout < this.s.haInterval) { + this.s.logger.warn(f('warning socketTimeout %s is less than haInterval %s. This might cause unnecessary server reconnections due to socket timeouts' + , this.s.options.socketTimeout, this.s.haInterval)); + } + + // All the authProviders + this.authProviders = options.authProviders || { + 'mongocr': new MongoCR(this.s.bson), 'x509': new X509(this.s.bson) + , 'plain': new Plain(this.s.bson), 'gssapi': new GSSAPI(this.s.bson) + , 'sspi': new SSPI(this.s.bson), 'scram-sha-1': new ScramSHA1(this.s.bson) + } + + // Add forwarding of events from state handler + var types = ['joined', 'left']; + types.forEach(function(x) { + self.s.replicaSetState.on(x, function(t, s) { + self.emit(x, t, s); + }); + }); + + // Connect stat + this.initialConnectState = { + connect: false, fullsetup: false, all: false + } + + // Disconnected state + this.state = DISCONNECTED; + this.haTimeoutId = null; + // Are we authenticating + this.authenticating = false; +} + +inherits(ReplSet, EventEmitter); + +Object.defineProperty(ReplSet.prototype, 'type', { + enumerable:true, get: function() { return 'replset'; } +}); + +function attemptReconnect(self) { + self.haTimeoutId = setTimeout(function() { + if(self.state == DESTROYED) return; + + // Debug log + if(self.s.logger.isDebug()) { + self.s.logger.debug(f('attemptReconnect for replset with id %s', self.id)); + } + + // Get all known hosts + var keys = Object.keys(self.s.replicaSetState.set); + var servers = keys.map(function(x) { + return new Server(assign({}, self.s.options, { + host: x.split(':')[0], port: parseInt(x.split(':')[1], 10) + }, { + authProviders: self.authProviders, reconnect:false, monitoring: false, inTopology: true + })); + }); + + // Create the list of servers + self.s.connectingServers = servers.slice(0); + + // Handle all events coming from servers + function _handleEvent(self, event) { + return function(err) { + // Destroy the instance + if(self.state == DESTROYED) { + return this.destroy(); + } + + // Debug log + if(self.s.logger.isDebug()) { + self.s.logger.debug(f('attemptReconnect for replset with id %s using server %s ended with event %s', self.id, this.name, event)); + } + + // Check if we are done + function done() { + // Done with the reconnection attempt + if(self.s.connectingServers.length == 0) { + if(self.state == DESTROYED) return; + + // Do we have a primary + if(self.s.replicaSetState.hasPrimary()) { + // If we have a primary and a disconnect handler, execute + // buffered operations + if(self.s.replicaSetState.hasPrimaryAndSecondary() && self.s.disconnectHandler) { + self.s.disconnectHandler.execute(); + } + + // Connect any missing servers + connectNewServers(self, self.s.replicaSetState.unknownServers, function(err, cb) { + // Debug log + if(self.s.logger.isDebug()) { + self.s.logger.debug(f('attemptReconnect for replset with id successful resuming topologyMonitor', self.id)); + } + // Go back to normal topology monitoring + topologyMonitor(self); + }); + } else { + if(self.listeners("close").length > 0) { + self.emit('close', self); + } + + attemptReconnect(self); + } + } + } + + // Remove the server from our list + for(var i = 0; i < self.s.connectingServers.length; i++) { + if(self.s.connectingServers[i].equals(this)) { + self.s.connectingServers.splice(i, 1); + } + } + + // Keep reference to server + var _self = this; + + // Debug log + if(self.s.logger.isDebug()) { + self.s.logger.debug(f('attemptReconnect in replset with id %s for', self.id)); + } + + // Connect and not authenticating + if(event == 'connect' && !self.authenticating) { + if(self.state == DESTROYED) { + return _self.destroy(); + } + + // Update the replicaset state + if(self.s.replicaSetState.update(_self)) { + // Remove the handlers + for(var i = 0; i < handlers.length; i++) { + _self.removeAllListeners(handlers[i]); + } + + // Add stable state handlers + _self.on('error', handleEvent(self, 'error')); + _self.on('close', handleEvent(self, 'close')); + _self.on('timeout', handleEvent(self, 'timeout')); + _self.on('parseError', handleEvent(self, 'parseError')); + } else { + _self.destroy(); + } + + done(); + } else if(event == 'connect' && self.authenticating) { + this.destroy(); + } + + done(); + } + } + + // Index used to interleaf the server connects, avoiding + // runtime issues on io constrained vm's + var timeoutInterval = 0; + + function connect(server, timeoutInterval) { + setTimeout(function() { + server.once('connect', _handleEvent(self, 'connect')); + server.once('close', _handleEvent(self, 'close')); + server.once('timeout', _handleEvent(self, 'timeout')); + server.once('error', _handleEvent(self, 'error')); + server.once('parseError', _handleEvent(self, 'parseError')); + + // SDAM Monitoring events + server.on('serverOpening', function(e) { self.emit('serverOpening', e); }); + server.on('serverDescriptionChanged', function(e) { self.emit('serverDescriptionChanged', e); }); + server.on('serverClosed', function(e) { self.emit('serverClosed', e); }); + + server.connect(self.s.connectOptions); + }, timeoutInterval); + } + + // Connect all servers + while(servers.length > 0) { + connect(servers.shift(), timeoutInterval++); + } + }, self.s.minHeartbeatFrequencyMS); +} + +function connectNewServers(self, servers, callback) { + // Count lefts + var count = servers.length; + + // Handle events + var _handleEvent = function(self, event) { + return function(err, r) { + var _self = this; + count = count - 1; + + // Destroyed + if(self.state == DESTROYED) { + return this.destroy(); + } + + if(event == 'connect' && !self.authenticating) { + // Destroyed + if(self.state == DESTROYED) { + return _self.destroy(); + } + + var result = self.s.replicaSetState.update(_self); + // Update the state with the new server + if(result) { + // Remove the handlers + for(var i = 0; i < handlers.length; i++) { + _self.removeAllListeners(handlers[i]); + } + + // Add stable state handlers + _self.on('error', handleEvent(self, 'error')); + _self.on('close', handleEvent(self, 'close')); + _self.on('timeout', handleEvent(self, 'timeout')); + _self.on('parseError', handleEvent(self, 'parseError')); + } else { + _self.destroy(); + } + } else if(event == 'connect' && self.authenticating) { + this.destroy(); + } + + // Are we done finish up callback + if(count == 0) { callback(); } + } + } + + // No new servers + if(count == 0) return callback(); + + // Execute method + function execute(_server, i) { + setTimeout(function() { + // Destroyed + if(self.state == DESTROYED) { + return; + } + + // Create a new server instance + var server = new Server(assign({}, self.s.options, { + host: _server.split(':')[0], + port: parseInt(_server.split(':')[1], 10) + }, { + authProviders: self.authProviders, reconnect:false, monitoring: false, inTopology: true + })); + + // Add temp handlers + server.once('connect', _handleEvent(self, 'connect')); + server.once('close', _handleEvent(self, 'close')); + server.once('timeout', _handleEvent(self, 'timeout')); + server.once('error', _handleEvent(self, 'error')); + server.once('parseError', _handleEvent(self, 'parseError')); + + // SDAM Monitoring events + server.on('serverOpening', function(e) { self.emit('serverOpening', e); }); + server.on('serverDescriptionChanged', function(e) { self.emit('serverDescriptionChanged', e); }); + server.on('serverClosed', function(e) { self.emit('serverClosed', e); }); + server.connect(self.s.connectOptions); + }, i); + } + + // Create new instances + for(var i = 0; i < servers.length; i++) { + execute(servers[i], i); + } +} + +function topologyMonitor(self, options) { + options = options || {}; + + // Set momitoring timeout + self.haTimeoutId = setTimeout(function() { + if(self.state == DESTROYED) return; + + // If we have a primary and a disconnect handler, execute + // buffered operations + if(self.s.replicaSetState.hasPrimaryAndSecondary() && self.s.disconnectHandler) { + self.s.disconnectHandler.execute(); + } + + // Get the connectingServers + var connectingServers = self.s.replicaSetState.allServers(); + // Debug log + if(self.s.logger.isDebug()) { + self.s.logger.debug(f('topologyMonitor in replset with id %s connected servers [%s]' + , self.id + , connectingServers.map(function(x) { + return x.name; + }))); + } + // Get the count + var count = connectingServers.length; + // If we have no servers connected + if(count == 0 && !options.haInterval) { + if(self.listeners("close").length > 0) { + self.emit('close', self); + } + + return attemptReconnect(self); + } + + // If the count is zero schedule a new fast + function pingServer(_self, _server, cb) { + // Measure running time + var start = new Date().getTime(); + + // Emit the server heartbeat start + emitSDAMEvent(self, 'serverHeartbeatStarted', { connectionId: _server.name }); + // Execute ismaster + _server.command('admin.$cmd', {ismaster:true}, {monitoring: true}, function(err, r) { + if(self.state == DESTROYED) { + _server.destroy(); + return cb(err, r); + } + + // Calculate latency + var latencyMS = new Date().getTime() - start; + + // We had an error, remove it from the state + if(err) { + // Emit the server heartbeat failure + emitSDAMEvent(self, 'serverHearbeatFailed', { durationMS: latencyMS, failure: err, connectionId: _server.name }); + // Remove the server from the state + _self.s.replicaSetState.remove(_server); + } else { + // Update the server ismaster + _server.ismaster = r.result; + if(_server.lastIsMasterMS == -1) { + _server.lastIsMasterMS = latencyMS; + } else if(_server.lastIsMasterMS) { + // After the first measurement, average RTT MUST be computed using an + // exponentially-weighted moving average formula, with a weighting factor (alpha) of 0.2. + // If the prior average is denoted old_rtt, then the new average (new_rtt) is + // computed from a new RTT measurement (x) using the following formula: + // alpha = 0.2 + // new_rtt = alpha * x + (1 - alpha) * old_rtt + _server.lastIsMasterMS = 0.2 * latencyMS + (1 - 0.2) * _server.lastIsMasterMS; + } + + // _server.lastIsMasterMS = !latencyMS; + _self.s.replicaSetState.update(_server); + + // Server heart beat event + emitSDAMEvent(self, 'serverHeartbeatSucceeded', { durationMS: latencyMS, reply: r.result, connectionId: _server.name }); + } + + cb(err, r); + }); + } + + // Connect any missing servers + function connectMissingServers() { + if(self.state == DESTROYED) return; + + // Attempt to connect to any unknown servers + connectNewServers(self, self.s.replicaSetState.unknownServers, function(err, cb) { + if(self.state == DESTROYED) return; + // Check if we have an options.haInterval (meaning it was triggered from connect) + if(options.haInterval) { + // Do we have a primary and secondary + if(self.state == CONNECTING + && self.s.replicaSetState.hasPrimaryAndSecondary()) { + // Transition to connected + stateTransition(self, CONNECTED); + // Update initial state + self.initialConnectState.connect = true; + self.initialConnectState.fullsetup = true; + self.initialConnectState.all = true; + // Emit fullsetup and all events + process.nextTick(function() { + self.emit('connect', self); + self.emit('fullsetup', self); + self.emit('all', self); + }); + } else if(self.state == CONNECTING + && self.s.replicaSetState.hasPrimary()) { + // Transition to connected + stateTransition(self, CONNECTED); + // Update initial state + self.initialConnectState.connect = true; + // Emit connected sign + process.nextTick(function() { + self.emit('connect', self); + }); + } else if(self.state == CONNECTING + && self.s.replicaSetState.hasSecondary() + && self.s.options.secondaryOnlyConnectionAllowed) { + // Transition to connected + stateTransition(self, CONNECTED); + // Update initial state + self.initialConnectState.connect = true; + // Emit connected sign + process.nextTick(function() { + self.emit('connect', self); + }); + } else if(self.state == CONNECTING) { + self.emit('error', new MongoError('no primary found in replicaset')); + // Destroy the topology + return self.destroy(); + } else if(self.state == CONNECTED + && self.s.replicaSetState.hasPrimaryAndSecondary() + && !self.initialConnectState.fullsetup) { + self.initialConnectState.fullsetup = true; + // Emit fullsetup and all events + process.nextTick(function() { + self.emit('fullsetup', self); + self.emit('all', self); + }); + } + } + + topologyMonitor(self); + }); + } + + // No connectingServers but unknown servers + if(connectingServers.length == 0 + && self.s.replicaSetState.unknownServers.length > 0 && options.haInterval) { + return connectMissingServers(); + } else if(connectingServers.length == 0 && options.haInterval) { + self.destroy(); + return self.emit('error', new MongoError('no valid replicaset members found')); + } + + // Ping all servers + for(var i = 0; i < connectingServers.length; i++) { + pingServer(self, connectingServers[i], function(err, r) { + count = count - 1; + + if(count == 0) { + connectMissingServers(); + } + }); + } + }, options.haInterval || self.s.haInterval) +} + +function handleEvent(self, event) { + return function(err) { + if(self.state == DESTROYED) return; + // Debug log + if(self.s.logger.isDebug()) { + self.s.logger.debug(f('handleEvent %s from server %s in replset with id %s', event, self.name, self.id)); + } + + self.s.replicaSetState.remove(this); + } +} + +function handleInitialConnectEvent(self, event) { + return function(err) { + // Debug log + if(self.s.logger.isDebug()) { + self.s.logger.debug(f('handleInitialConnectEvent %s from server %s in replset with id %s', event, this.name, self.id)); + } + + // Destroy the instance + if(self.state == DESTROYED) { + return this.destroy(); + } + + // Check the type of server + if(event == 'connect') { + // Update the state + var result = self.s.replicaSetState.update(this); + if(result == true) { + // Debug log + if(self.s.logger.isDebug()) { + self.s.logger.debug(f('handleInitialConnectEvent %s from server %s in replset with id %s has state [%s]', event, this.name, self.id, JSON.stringify(self.s.replicaSetState.set))); + } + + // Remove the handlers + for(var i = 0; i < handlers.length; i++) { + this.removeAllListeners(handlers[i]); + } + + // Add stable state handlers + this.on('error', handleEvent(self, 'error')); + this.on('close', handleEvent(self, 'close')); + this.on('timeout', handleEvent(self, 'timeout')); + this.on('parseError', handleEvent(self, 'parseError')); + } else if(result instanceof MongoError) { + this.destroy(); + self.destroy(); + return self.emit('error', result); + } else { + this.destroy(); + } + } else { + // Emit failure to connect + self.emit('failed', this); + // Remove from the state + self.s.replicaSetState.remove(this); + } + + // Remove from the list from connectingServers + for(var i = 0; i < self.s.connectingServers.length; i++) { + if(self.s.connectingServers[i].equals(this)) { + self.s.connectingServers.splice(i, 1); + } + } + + // Trigger topologyMonitor + if(self.s.connectingServers.length == 0) { + topologyMonitor(self, {haInterval: 1}); + } + }; +} + +function connectServers(self, servers) { + // Update connectingServers + self.s.connectingServers = self.s.connectingServers.concat(servers); + + // Index used to interleaf the server connects, avoiding + // runtime issues on io constrained vm's + var timeoutInterval = 0; + + function connect(server, timeoutInterval) { + setTimeout(function() { + // Add the server to the state + self.s.replicaSetState.update(server); + // Add event handlers + server.once('close', handleInitialConnectEvent(self, 'close')); + server.once('timeout', handleInitialConnectEvent(self, 'timeout')); + server.once('parseError', handleInitialConnectEvent(self, 'parseError')); + server.once('error', handleInitialConnectEvent(self, 'error')); + server.once('connect', handleInitialConnectEvent(self, 'connect')); + // SDAM Monitoring events + server.on('serverOpening', function(e) { self.emit('serverOpening', e); }); + server.on('serverDescriptionChanged', function(e) { self.emit('serverDescriptionChanged', e); }); + server.on('serverClosed', function(e) { self.emit('serverClosed', e); }); + // Start connection + server.connect(self.s.connectOptions); + }, timeoutInterval); + } + + // Start all the servers + while(servers.length > 0) { + connect(servers.shift(), timeoutInterval++); + } +} + +/** + * Emit event if it exists + * @method + */ +function emitSDAMEvent(self, event, description) { + if(self.listeners(event).length > 0) { + self.emit(event, description); + } +} + +/** + * Initiate server connect + * @method + * @param {array} [options.auth=null] Array of auth options to apply on connect + */ +ReplSet.prototype.connect = function(options) { + var self = this; + // Add any connect level options to the internal state + this.s.connectOptions = options || {}; + // Set connecting state + stateTransition(this, CONNECTING); + // Create server instances + var servers = this.s.seedlist.map(function(x) { + return new Server(assign({}, self.s.options, x, { + authProviders: self.authProviders, reconnect:false, monitoring:false, inTopology: true + })); + }); + + // Emit the topology opening event + emitSDAMEvent(this, 'topologyOpening', { topologyId: this.id }); + + // Start all server connections + connectServers(self, servers); +} + +/** + * Destroy the server connection + * @method + */ +ReplSet.prototype.destroy = function() { + // Transition state + stateTransition(this, DESTROYED); + // Clear out any monitoring process + if(this.haTimeoutId) clearTimeout(this.haTimeoutId); + // Destroy the replicaset + this.s.replicaSetState.destroy(); + + // Destroy all connecting servers + this.s.connectingServers.forEach(function(x) { + x.destroy(); + }); + + // Emit toplogy closing event + emitSDAMEvent(this, 'topologyClosed', { topologyId: this.id }); +} + +/** + * Unref all connections belong to this server + * @method + */ +ReplSet.prototype.unref = function() { + // Transition state + stateTransition(this, DISCONNECTED); + + this.s.replicaSetState.allServers().forEach(function(x) { + x.unref(); + }); + + clearTimeout(this.haTimeoutId); +} + +/** + * Returns the last known ismaster document for this server + * @method + * @return {object} + */ +ReplSet.prototype.lastIsMaster = function() { + return this.s.replicaSetState.primary + ? this.s.replicaSetState.primary.lastIsMaster() : null; +} + +/** + * All raw connections + * @method + * @return {Connection[]} + */ +ReplSet.prototype.connections = function() { + var servers = this.s.replicaSetState.allServers(); + var connections = []; + for(var i = 0; i < servers.length; i++) { + connections = connections.concat(servers[i].connections()); + } + + return connections; +} + +/** + * Figure out if the server is connected + * @method + * @param {ReadPreference} [options.readPreference] Specify read preference if command supports it + * @return {boolean} + */ +ReplSet.prototype.isConnected = function(options) { + options = options || {}; + + // If we are authenticating signal not connected + // To avoid interleaving of operations + if(this.authenticating) return false; + + // If we specified a read preference check if we are connected to something + // than can satisfy this + if(options.readPreference + && options.readPreference.equals(ReadPreference.secondary)) { + return this.s.replicaSetState.hasSecondary(); + } + + if(options.readPreference + && options.readPreference.equals(ReadPreference.primary)) { + return this.s.replicaSetState.hasPrimary(); + } + + if(options.readPreference + && options.readPreference.equals(ReadPreference.primaryPreferred)) { + return this.s.replicaSetState.hasSecondary() || this.s.replicaSetState.hasPrimary(); + } + + if(options.readPreference + && options.readPreference.equals(ReadPreference.secondaryPreferred)) { + return this.s.replicaSetState.hasSecondary() || this.s.replicaSetState.hasPrimary(); + } + + if(this.s.secondaryOnlyConnectionAllowed + && this.s.replicaSetState.hasSecondary()) { + return true; + } + + return this.s.replicaSetState.hasPrimary(); +} + +/** + * Figure out if the replicaset instance was destroyed by calling destroy + * @method + * @return {boolean} + */ +ReplSet.prototype.isDestroyed = function() { + return this.state == DESTROYED; +} + +/** + * Get server + * @method + * @param {ReadPreference} [options.readPreference] Specify read preference if command supports it + * @return {Server} + */ +ReplSet.prototype.getServer = function(options) { + // Ensure we have no options + options = options || {}; + // Pick the right server baspickServerd on readPreference + // var server = pickServer(this, this.s, options.readPreference); + var server = this.s.replicaSetState.pickServer(options.readPreference); + if(this.s.debug) this.emit('pickedServer', options.readPreference, server); + return server; +} + +/** + * Get all connected servers + * @method + * @return {Server[]} + */ +ReplSet.prototype.getServers = function() { + return this.s.replicaSetState.allServers(); +} + +function basicReadPreferenceValidation(self, options) { + if(options.readPreference && !(options.readPreference instanceof ReadPreference)) { + throw new Error("readPreference must be an instance of ReadPreference"); + } +} + +// +// Execute write operation +var executeWriteOperation = function(self, op, ns, ops, options, callback) { + if(typeof options == 'function') callback = options, options = {}, options = options || {}; + // Ensure we have no options + options = options || {}; + + // No server returned we had an error + if(self.s.replicaSetState.primary == null) { + return callback(new MongoError("no primary server found")); + } + + // Execute the command + self.s.replicaSetState.primary[op](ns, ops, options, 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 {opResultCallback} callback A callback function + */ +ReplSet.prototype.insert = function(ns, ops, options, callback) { + if(typeof options == 'function') callback = options, options = {}, options = options || {}; + if(this.state == DESTROYED) return callback(new MongoError(f('topology was destroyed'))); + + // Not connected but we have a disconnecthandler + if(!this.s.replicaSetState.hasPrimary() && this.s.disconnectHandler != null) { + return this.s.disconnectHandler.add('insert', ns, ops, options, callback); + } + + // Execute write operation + executeWriteOperation(this, '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 {opResultCallback} callback A callback function + */ +ReplSet.prototype.update = function(ns, ops, options, callback) { + if(typeof options == 'function') callback = options, options = {}, options = options || {}; + if(this.state == DESTROYED) return callback(new MongoError(f('topology was destroyed'))); + + // Not connected but we have a disconnecthandler + if(!this.s.replicaSetState.hasPrimary() && this.s.disconnectHandler != null) { + return this.s.disconnectHandler.add('insert', ns, ops, options, callback); + } + + // Execute write operation + executeWriteOperation(this, '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 {opResultCallback} callback A callback function + */ +ReplSet.prototype.remove = function(ns, ops, options, callback) { + if(typeof options == 'function') callback = options, options = {}, options = options || {}; + if(this.state == DESTROYED) return callback(new MongoError(f('topology was destroyed'))); + + // Not connected but we have a disconnecthandler + if(!this.s.replicaSetState.hasPrimary() && this.s.disconnectHandler != null) { + return this.s.disconnectHandler.add('insert', ns, ops, options, callback); + } + + // Execute write operation + executeWriteOperation(this, 'remove', ns, ops, options, callback); +} + +/** + * Execute a command + * @method + * @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 {Connection} [options.connection] Specify connection object to execute command against + * @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 {opResultCallback} callback A callback function + */ +ReplSet.prototype.command = function(ns, cmd, options, callback) { + if(typeof options == 'function') callback = options, options = {}, options = options || {}; + if(this.state == DESTROYED) return callback(new MongoError(f('topology was destroyed'))); + var self = this; + + // Establish readPreference + var readPreference = options.readPreference ? options.readPreference : ReadPreference.primary; + // Pick a server + // var server = pickServer(self, self.s, readPreference); + // var server = self.s.replicaSetState.pickServer(readPreference); + var server = this.s.replicaSetState.pickServer(readPreference); + if(!(server instanceof Server)) return callback(server); + if(self.s.debug) self.emit('pickedServer', ReadPreference.primary, server); + + // Topology is not connected, save the call in the provided store to be + // Executed at some point when the handler deems it's reconnected + if(!server && this.s.disconnectHandler != null) { + return this.s.disconnectHandler.add('command', ns, cmd, options, callback); + } + + // No server returned we had an error + if(server == null) { + return callback(new MongoError(f("no server found that matches the provided readPreference %s", readPreference))); + } + + // Execute the command + server.command(ns, cmd, options, callback); +} + +/** + * Authenticate using a specified mechanism + * @method + * @param {string} mechanism The Auth mechanism we are invoking + * @param {string} db The db we are invoking the mechanism against + * @param {...object} param Parameters for the specific mechanism + * @param {authResultCallback} callback A callback function + */ +ReplSet.prototype.auth = function(mechanism, db) { + var allArgs = Array.prototype.slice.call(arguments, 0).slice(0); + var self = this; + var args = Array.prototype.slice.call(arguments, 2); + var callback = args.pop(); + + // If we don't have the mechanism fail + if(this.authProviders[mechanism] == null && mechanism != 'default') { + return callback(new MongoError(f("auth provider %s does not exist", mechanism))); + } + + // Are we already authenticating, throw + if(this.authenticating) { + return callback(new MongoError('authentication or logout allready in process')); + } + + // Topology is not connected, save the call in the provided store to be + // Executed at some point when the handler deems it's reconnected + if(!self.s.replicaSetState.hasPrimary() && self.s.disconnectHandler != null) { + return self.s.disconnectHandler.add('auth', db, allArgs, {}, callback); + } + + // Set to authenticating + this.authenticating = true; + // All errors + var errors = []; + + // Get all the servers + var servers = this.s.replicaSetState.allServers(); + // No servers return + if(servers.length == 0) { + this.authenticating = false; + callback(null, true); + } + + // Authenticate + function auth(server) { + // Arguments without a callback + var argsWithoutCallback = [mechanism, db].concat(args.slice(0)); + // Create arguments + var finalArguments = argsWithoutCallback.concat([function(err, r) { + count = count - 1; + // Save all the errors + if(err) errors.push({name: server.name, err: err}); + // We are done + if(count == 0) { + // Auth is done + self.authenticating = false; + + // Return the auth error + if(errors.length) return callback(MongoError.create({ + message: 'authentication fail', errors: errors + }), false); + + // Successfully authenticated session + callback(null, self); + } + }]); + + if(!server.lastIsMaster().arbiterOnly) { + // Execute the auth only against non arbiter servers + server.auth.apply(server, finalArguments); + } else { + // If we are authenticating against an arbiter just ignore it + finalArguments.pop()(null); + } + } + + // Get total count + var count = servers.length; + // Authenticate against all servers + while(servers.length > 0) { + auth(servers.shift()); + } +} + +/** + * Logout from a database + * @method + * @param {string} db The db we are logging out from + * @param {authResultCallback} callback A callback function + */ +ReplSet.prototype.logout = function(dbName, callback) { + var self = this; + // Are we authenticating or logging out, throw + if(this.authenticating) { + throw new MongoError('authentication or logout allready in process'); + } + + // Ensure no new members are processed while logging out + this.authenticating = true; + + // Remove from all auth providers (avoid any reaplication of the auth details) + var providers = Object.keys(this.authProviders); + for(var i = 0; i < providers.length; i++) { + this.authProviders[providers[i]].logout(dbName); + } + + // Now logout all the servers + var servers = this.s.replicaSetState.allServers(); + var count = servers.length; + if(count == 0) return callback(); + var errors = []; + + // Execute logout on all server instances + for(var i = 0; i < servers.length; i++) { + servers[i].logout(dbName, function(err) { + count = count - 1; + if(err) errors.push({name: server.name, err: err}); + + if(count == 0) { + // Do not block new operations + self.authenticating = false; + // If we have one or more errors + if(errors.length) return callback(MongoError.create({ + message: f('logout failed against db %s', dbName), errors: errors + }), false); + + // No errors + callback(); + } + }); + } +} + +/** + * Perform one or more remove operations + * @method + * @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1) + * @param {{object}|{Long}} cmd Can be either a command returning a cursor or a cursorId + * @param {object} [options.batchSize=0] Batchsize for the operation + * @param {array} [options.documents=[]] Initial documents list for cursor + * @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.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields. + * @param {opResultCallback} callback A callback function + */ +ReplSet.prototype.cursor = function(ns, cmd, cursorOptions) { + cursorOptions = cursorOptions || {}; + var FinalCursor = cursorOptions.cursorFactory || this.s.Cursor; + return new FinalCursor(this.s.bson, ns, cmd, cursorOptions, this, this.s.options); +} + +/** + * A replset connect event, used to verify that the connection is up and running + * + * @event ReplSet#connect + * @type {ReplSet} + */ + +/** + * A replset reconnect event, used to verify that the topology reconnected + * + * @event ReplSet#reconnect + * @type {ReplSet} + */ + +/** + * A replset fullsetup event, used to signal that all topology members have been contacted. + * + * @event ReplSet#fullsetup + * @type {ReplSet} + */ + +/** + * A replset all event, used to signal that all topology members have been contacted. + * + * @event ReplSet#all + * @type {ReplSet} + */ + +/** + * A replset failed event, used to signal that initial replset connection failed. + * + * @event ReplSet#failed + * @type {ReplSet} + */ + +/** + * A server member left the replicaset + * + * @event ReplSet#left + * @type {function} + * @param {string} type The type of member that left (primary|secondary|arbiter) + * @param {Server} server The server object that left + */ + +/** + * A server member joined the replicaset + * + * @event ReplSet#joined + * @type {function} + * @param {string} type The type of member that joined (primary|secondary|arbiter) + * @param {Server} server The server object that joined + */ + +/** + * A server opening SDAM monitoring event + * + * @event ReplSet#serverOpening + * @type {object} + */ + +/** + * A server closed SDAM monitoring event + * + * @event ReplSet#serverClosed + * @type {object} + */ + +/** + * A server description SDAM change monitoring event + * + * @event ReplSet#serverDescriptionChanged + * @type {object} + */ + +/** + * A topology open SDAM event + * + * @event ReplSet#topologyOpening + * @type {object} + */ + +/** + * A topology closed SDAM event + * + * @event ReplSet#topologyClosed + * @type {object} + */ + +/** + * A topology structure SDAM change event + * + * @event ReplSet#topologyDescriptionChanged + * @type {object} + */ + +/** + * A topology serverHeartbeatStarted SDAM event + * + * @event ReplSet#serverHeartbeatStarted + * @type {object} + */ + +/** + * A topology serverHearbeatFailed SDAM event + * + * @event ReplSet#serverHearbeatFailed + * @type {object} + */ + +/** + * A topology serverHeartbeatSucceeded SDAM change event + * + * @event ReplSet#serverHeartbeatSucceeded + * @type {object} + */ + +module.exports = ReplSet; diff --git a/node_modules/mongodb-core/lib/topologies/replset_state.js b/node_modules/mongodb-core/lib/topologies/replset_state.js new file mode 100644 index 0000000..96249eb --- /dev/null +++ b/node_modules/mongodb-core/lib/topologies/replset_state.js @@ -0,0 +1,812 @@ +"use strict" + +var inherits = require('util').inherits, + f = require('util').format, + EventEmitter = require('events').EventEmitter, + Logger = require('../connection/logger'), + ObjectId = require('bson').ObjectId, + ReadPreference = require('./read_preference'), + MongoError = require('../error'); + +var TopologyType = { + 'Single': 'Single', 'ReplicaSetNoPrimary': 'ReplicaSetNoPrimary', + 'ReplicaSetWithPrimary': 'ReplicaSetWithPrimary', 'Sharded': 'Sharded', + 'Unknown': 'Unknown' +}; + +var ServerType = { + 'Standalone': 'Standalone', 'Mongos': 'Mongos', 'PossiblePrimary': 'PossiblePrimary', + 'RSPrimary': 'RSPrimary', 'RSSecondary': 'RSSecondary', 'RSArbiter': 'RSArbiter', + 'RSOther': 'RSOther', 'RSGhost': 'RSGhost', 'Unknown': 'Unknown' +}; + +var ReplSetState = function(options) { + options = options || {}; + // Add event listener + EventEmitter.call(this); + // Topology state + this.topologyType = TopologyType.ReplicaSetNoPrimary; + this.setName = options.setName; + + // Server set + this.set = {}; + + // Unpacked options + this.id = options.id; + this.setName = options.setName; + + // Replicaset logger + this.logger = options.logger || Logger('ReplSet', options); + + // Server selection index + this.index = 0; + // Acceptable latency + this.acceptableLatency = options.acceptableLatency || 15; + + // Server side + this.primary = null; + this.secondaries = []; + this.arbiters = []; + this.passives = []; + this.ghosts = []; + // Current unknown hosts + this.unknownServers = []; + // In set status + this.set = {}; + // Status + this.maxElectionId = null; + this.maxSetVersion = 0; + // Description of the Replicaset + this.replicasetDescription = { + "topologyType": "Unknown", "servers": [] + }; +} + +inherits(ReplSetState, EventEmitter); + +ReplSetState.prototype.hasPrimaryAndSecondary = function(server) { + return this.primary && this.secondaries.length > 0; +} + +ReplSetState.prototype.hasPrimary = function(server) { + return this.primary != null; +} + +ReplSetState.prototype.hasSecondary = function(server) { + return this.secondaries.length > 0; +} + +ReplSetState.prototype.allServers = function(options) { + options = options || {}; + var servers = this.primary ? [this.primary] : []; + servers = servers.concat(this.secondaries); + if(!options.ignoreArbiters) servers = servers.concat(this.arbiters); + servers = servers.concat(this.passives); + return servers; +} + +ReplSetState.prototype.destroy = function() { + // Destroy all sockets + if(this.primary) this.primary.destroy(); + this.secondaries.forEach(function(x) { x.destroy(); }); + this.arbiters.forEach(function(x) { x.destroy(); }); + this.passives.forEach(function(x) { x.destroy(); }); + this.ghosts.forEach(function(x) { x.destroy(); }); + // Clear out the complete state + this.secondaries = []; + this.arbiters = []; + this.passives = []; + this.ghosts = []; + this.unknownServers = []; + this.set = {}; +} + +ReplSetState.prototype.remove = function(server, options) { + options = options || {}; + + // Only remove if the current server is not connected + var servers = this.primary ? [this.primary] : []; + servers = servers.concat(this.secondaries); + servers = servers.concat(this.arbiters); + servers = servers.concat(this.passives); + + // Check if it's active and this is just a failed connection attempt + for(var i = 0; i < servers.length; i++) { + if(!options.force && servers[i].equals(server) && servers[i].isConnected && servers[i].isConnected()) { + return; + } + } + + // If we have it in the set remove it + if(this.set[server.name]) { + this.set[server.name].type = ServerType.Unknown; + this.set[server.name].electionId = null; + this.set[server.name].setName = null; + this.set[server.name].setVersion = null; + } + + // Remove type + var removeType = null; + + // Remove from any lists + if(this.primary && this.primary.equals(server)) { + this.primary = null; + this.topologyType = TopologyType.ReplicaSetNoPrimary; + removeType = 'primary'; + } + + // Remove from any other server lists + removeType = removeFrom(server, this.secondaries) ? 'secondary' : removeType; + removeType = removeFrom(server, this.arbiters) ? 'arbiter' : removeType; + removeType = removeFrom(server, this.passives) ? 'secondary' : removeType; + removeFrom(server, this.ghosts); + removeFrom(server, this.unknownServers); + + // Do we have a removeType + if(removeType) { + this.emit('left', removeType, server); + } +} + +ReplSetState.prototype.update = function(server) { + var self = this; + // Get the current ismaster + var ismaster = server.lastIsMaster(); + + // + // Add any hosts + // + if(ismaster) { + // Join all the possible new hosts + var hosts = Array.isArray(ismaster.hosts) ? ismaster.hosts : []; + hosts = hosts.concat(Array.isArray(ismaster.arbiters) ? ismaster.arbiters : []); + hosts = hosts.concat(Array.isArray(ismaster.passives) ? ismaster.passives : []); + + // Add all hosts as unknownServers + for(var i = 0; i < hosts.length; i++) { + // Add to the list of unknown server + if(this.unknownServers.indexOf(hosts[i]) == -1 + && (!this.set[hosts[i]] || this.set[hosts[i]].type == ServerType.Unknown)) { + this.unknownServers.push(hosts[i]); + } + + if(!this.set[hosts[i]]) { + this.set[hosts[i]] = { + type: ServerType.Unknown, + electionId: null, + setName: null, + setVersion: null + } + } + } + } + + // + // Unknown server + // + if(!ismaster && !inList(ismaster, server, this.unknownServers)) { + self.set[server.name] = { + type: ServerType.Unknown, setVersion: null, electionId: null, setName: null + } + // Update set information about the server instance + self.set[server.name].type = ServerType.Unknown; + self.set[server.name].electionId = ismaster ? ismaster.electionId : ismaster; + self.set[server.name].setName = ismaster ? ismaster.setName : ismaster; + self.set[server.name].setVersion = ismaster ? ismaster.setVersion : ismaster; + + if(self.unknownServers.indexOf(server.name) == -1) { + self.unknownServers.push(server.name); + } + + // Set the topology + return false; + } + + // + // Is this a mongos + // + if(ismaster && ismaster.msg == 'isdbgrid') { + return false; + } + + // A RSOther instance + if((ismaster.setName && ismaster.hidden) + || (ismaster.setName && !ismaster.ismaster && !ismaster.secondary && !ismaster.arbiterOnly && !ismaster.passive)) { + self.set[server.name] = { + type: ServerType.RSOther, setVersion: null, + electionId: null, setName: ismaster.setName + } + // Set the topology + this.topologyType = this.primary ? TopologyType.ReplicaSetWithPrimary : TopologyType.ReplicaSetNoPrimary; + if(ismaster.setName) this.setName = ismaster.setName; + return false; + } + + // A RSGhost instance + if(ismaster.isreplicaset) { + self.set[server.name] = { + type: ServerType.RSGhost, setVersion: null, + electionId: null, setName: null + } + + // Set the topology + this.topologyType = this.primary ? TopologyType.ReplicaSetWithPrimary : TopologyType.ReplicaSetNoPrimary; + if(ismaster.setName) this.setName = ismaster.setName; + + // Set the topology + return false; + } + + // + // Standalone server, destroy and return + // + if(ismaster && ismaster.ismaster && !ismaster.setName) { + this.topologyType = this.primary ? TopologyType.ReplicaSetWithPrimary : TopologyType.Unknown; + this.remove(server, {force:true}); + return false; + } + + // + // Server in maintanance mode + // + if(ismaster && !ismaster.ismaster && !ismaster.secondary && !ismaster.arbiterOnly) { + this.remove(server, {force:true}); + return false; + } + + // + // If the .me field does not match the passed in server + // + if(ismaster.me && ismaster.me != server.name) { + if(this.logger.isWarn()) { + this.logger.warn(f('the seedlist server was removed due to its address %s not matching its ismaster.me address %s', server.name, ismaster.me)); + } + + // Set the type of topology we have + if(this.primary && !this.primary.equals(server)) { + this.topologyType = TopologyType.ReplicaSetWithPrimary; + } else { + this.topologyType = TopologyType.ReplicaSetNoPrimary; + } + + return false; + } + + // + // Primary handling + // + if(!this.primary && ismaster.ismaster && ismaster.setName) { + var ismasterElectionId = server.lastIsMaster().electionId; + if(this.setName && this.setName != ismaster.setName) { + this.topologyType = TopologyType.ReplicaSetNoPrimary; + return new MongoError(f('setName from ismaster does not match provided connection setName [%s] != [%s]', ismaster.setName, this.setName)); + } + + if(!this.maxElectionId && ismasterElectionId) { + this.maxElectionId = ismasterElectionId; + } else if(this.maxElectionId && ismasterElectionId) { + var result = compareObjectIds(this.maxElectionId, ismasterElectionId); + // Get the electionIds + var ismasterSetVersion = server.lastIsMaster().setVersion; + + // if(result == 1 || result == 0) { + if(result == 1) { + this.topologyType = TopologyType.ReplicaSetNoPrimary; + return false; + } else if(result == 0 && ismasterSetVersion) { + if(ismasterSetVersion < this.maxSetVersion) { + this.topologyType = TopologyType.ReplicaSetNoPrimary; + return false; + } + } + + this.maxSetVersion = ismasterSetVersion; + this.maxElectionId = ismasterElectionId; + } + + self.primary = server; + self.set[server.name] = { + type: ServerType.RSPrimary, + setVersion: ismaster.setVersion, + electionId: ismaster.electionId, + setName: ismaster.setName + } + + // Set the topology + this.topologyType = TopologyType.ReplicaSetWithPrimary; + if(ismaster.setName) this.setName = ismaster.setName; + removeFrom(server, self.unknownServers); + removeFrom(server, self.secondaries); + removeFrom(server, self.passives); + self.emit('joined', 'primary', server); + emitTopologyDescriptionChanged(self); + return true; + } else if(ismaster.ismaster && ismaster.setName) { + // Get the electionIds + var currentElectionId = self.set[self.primary.name].electionId; + var currentSetVersion = self.set[self.primary.name].setVersion; + var currentSetName = self.set[self.primary.name].setName; + var ismasterElectionId = server.lastIsMaster().electionId; + var ismasterSetVersion = server.lastIsMaster().setVersion; + var ismasterSetName = server.lastIsMaster().setName; + + // Is it the same server instance + if(this.primary.equals(server) + && currentSetName == ismasterSetName) { + return false; + } + + // If we do not have the same rs name + if(currentSetName && currentSetName != ismasterSetName) { + if(!this.primary.equals(server)) { + this.topologyType = TopologyType.ReplicaSetWithPrimary; + } else { + this.topologyType = TopologyType.ReplicaSetNoPrimary; + } + + return false; + } + + // Check if we need to replace the server + if(currentElectionId && ismasterElectionId) { + var result = compareObjectIds(currentElectionId, ismasterElectionId); + + if(result == 1) { + return false; + } else if(result == 0 && (currentSetVersion > ismasterSetVersion)) { + return false; + } + } else if(!currentElectionId && ismasterElectionId + && ismasterSetVersion) { + if(ismasterSetVersion < this.maxSetVersion) { + return false; + } + } + + if(!this.maxElectionId && ismasterElectionId) { + this.maxElectionId = ismasterElectionId; + } else if(this.maxElectionId && ismasterElectionId) { + var result = compareObjectIds(this.maxElectionId, ismasterElectionId); + + if(result == 1) { + return false; + } else if(result == 0 && currentSetVersion && ismasterSetVersion) { + if(ismasterSetVersion < this.maxSetVersion) { + return false; + } + } else { + if(ismasterSetVersion < this.maxSetVersion) { + return false; + } + } + + this.maxElectionId = ismasterElectionId; + this.maxSetVersion = ismasterSetVersion; + } else { + this.maxSetVersion = ismasterSetVersion; + } + + // Modify the entry to unknown + self.set[self.primary.name] = { + type: ServerType.Unknown, setVersion: null, + electionId: null, setName: null + } + + // Signal primary left + self.emit('left', 'primary', this.primary); + // Destroy the instance + self.primary.destroy(); + // Set the new instance + self.primary = server; + // Set the set information + self.set[server.name] = { + type: ServerType.RSPrimary, setVersion: ismaster.setVersion, + electionId: ismaster.electionId, setName: ismaster.setName + } + + // Set the topology + this.topologyType = TopologyType.ReplicaSetWithPrimary; + if(ismaster.setName) this.setName = ismaster.setName; + removeFrom(server, self.unknownServers); + removeFrom(server, self.secondaries); + removeFrom(server, self.passives); + self.emit('joined', 'primary', server); + emitTopologyDescriptionChanged(self); + return true; + } + + // A possible instance + if(!this.primary && ismaster.primary) { + self.set[ismaster.primary] = { + type: ServerType.PossiblePrimary, setVersion: null, + electionId: null, setName: null + } + } + + // + // Secondary handling + // + if(ismaster.secondary && ismaster.setName + && !inList(ismaster, server, this.secondaries) + && this.setName && this.setName == ismaster.setName) { + addToList(self, ServerType.RSSecondary, ismaster, server, this.secondaries); + // Set the topology + this.topologyType = this.primary ? TopologyType.ReplicaSetWithPrimary : TopologyType.ReplicaSetNoPrimary; + if(ismaster.setName) this.setName = ismaster.setName; + removeFrom(server, self.unknownServers); + + // Remove primary + if(this.primary && this.primary.name == server.name) { + server.destroy(); + this.primary = null; + self.emit('left', 'primary', server); + } + + self.emit('joined', 'secondary', server); + emitTopologyDescriptionChanged(self); + return true; + } + + // + // Arbiter handling + // + if(ismaster.arbiterOnly && ismaster.setName + && !inList(ismaster, server, this.arbiters) + && this.setName && this.setName == ismaster.setName) { + addToList(self, ServerType.RSArbiter, ismaster, server, this.arbiters); + // Set the topology + this.topologyType = this.primary ? TopologyType.ReplicaSetWithPrimary : TopologyType.ReplicaSetNoPrimary; + if(ismaster.setName) this.setName = ismaster.setName; + removeFrom(server, self.unknownServers); + self.emit('joined', 'arbiter', server); + emitTopologyDescriptionChanged(self); + return true; + } + + // + // Passive handling + // + if(ismaster.passive && ismaster.setName + && !inList(ismaster, server, this.passives) + && this.setName && this.setName == ismaster.setName) { + addToList(self, ServerType.RSSecondary, ismaster, server, this.passives); + // Set the topology + this.topologyType = this.primary ? TopologyType.ReplicaSetWithPrimary : TopologyType.ReplicaSetNoPrimary; + if(ismaster.setName) this.setName = ismaster.setName; + removeFrom(server, self.unknownServers); + + // Remove primary + if(this.primary && this.primary.name == server.name) { + server.destroy(); + this.primary = null; + self.emit('left', 'primary', server); + } + + self.emit('joined', 'secondary', server); + emitTopologyDescriptionChanged(self); + return true; + } + + // + // Remove the primary + // + if(this.set[server.name] && this.set[server.name].type == ServerType.RSPrimary) { + self.emit('left', 'primary', this.primary); + this.primary.destroy(); + this.primary = null; + this.topologyType = TopologyType.ReplicaSetNoPrimary; + return false; + } + + this.topologyType = this.primary ? TopologyType.ReplicaSetWithPrimary : TopologyType.ReplicaSetNoPrimary; + return false; +} + +ReplSetState.prototype.pickServer = function(readPreference) { + // If no read Preference set to primary by default + readPreference = readPreference || ReadPreference.primary; + + // Do we have the nearest readPreference + if(readPreference.preference == 'nearest') { + return pickNearest(this, readPreference); + } + + // Get all the secondaries + var secondaries = this.secondaries; + + // Check if we can satisfy and of the basic read Preferences + if(readPreference.equals(ReadPreference.secondary) + && secondaries.length == 0) { + return new MongoError("no secondary server available"); + } + + if(readPreference.equals(ReadPreference.secondaryPreferred) + && secondaries.length == 0 + && this.primary == null) { + return new MongoError("no secondary or primary server available"); + } + + if(readPreference.equals(ReadPreference.primary) + && this.primary == null) { + return new MongoError("no primary server available"); + } + + // Secondary preferred or just secondaries + if(readPreference.equals(ReadPreference.secondaryPreferred) + || readPreference.equals(ReadPreference.secondary)) { + + if(secondaries.length > 0) { + // Apply tags if present + var servers = filterByTags(readPreference, secondaries); + // Pick nearest of any other servers available + var server = pickNearest(this, readPreference); + // No server in the window return primary + if(!server && readPreference.equals(ReadPreference.secondaryPreferred)) { + return this.primary; + } else { + return server; + } + } + + return null; + } + + // Primary preferred + if(readPreference.equals(ReadPreference.primaryPreferred)) { + if(this.primary) return this.primary; + + if(secondaries.length > 0) { + // Apply tags if present + var servers = filterByTags(readPreference, secondaries); + // Pick nearest of any other servers available + return pickNearest(this, readPreference); + } + } + + // Return the primary + return this.primary; +} + +// +// Filter serves by tags +var filterByTags = function(readPreference, servers) { + if(readPreference.tags == null) return servers; + var filteredServers = []; + var tagsArray = Array.isArray(readPreference.tags) ? readPreference.tags : [readPreference.tags]; + + // Iterate over the tags + for(var j = 0; j < tagsArray.length; j++) { + var tags = tagsArray[j]; + + // Iterate over all the servers + for(var i = 0; i < servers.length; i++) { + var serverTag = servers[i].lastIsMaster().tags || {}; + // Did we find the a matching server + var found = true; + // Check if the server is valid + for(var name in tags) { + if(serverTag[name] != tags[name]) found = false; + } + + // Add to candidate list + if(found) { + filteredServers.push(servers[i]); + } + } + + // We found servers by the highest priority + if(found) break; + } + + // Returned filtered servers + return filteredServers; +} + +function pickNearest(self, readPreference) { + // Only get primary and secondaries as seeds + var seeds = {}; + var servers = []; + + // Add primary to list if not a secondary read preference + if(self.primary && readPreference.preference != 'secondary') { + servers.push(self.primary); + } + + // Add all the secondaries + for(var i = 0; i < self.secondaries.length; i++) { + servers.push(self.secondaries[i]); + } + + // Filter by tags + servers = filterByTags(readPreference, servers); + + // Sort by time + servers.sort(function(a, b) { + // return a.time > b.time; + return a.lastIsMasterMS > b.lastIsMasterMS + }); + + // Locate lowest time (picked servers are lowest time + acceptable Latency margin) + var lowest = servers.length > 0 ? servers[0].lastIsMasterMS : 0; + + // Filter by latency + servers = servers.filter(function(s) { + return s.lastIsMasterMS <= lowest + self.acceptableLatency; + }); + + // No servers, default to primary + if(servers.length == 0) { + return null + } + + // Add to the index + self.index = self.index + 1; + // Select the index + self.index = self.index % servers.length; + + // Return the first server of the sorted and filtered list + return servers[self.index]; +} + +function inList(ismaster, server, list) { + for(var i = 0; i < list.length; i++) { + if(list[i].name == server.name) return true; + } + + return false; +} + +function addToList(self, type, ismaster, server, list) { + // Update set information about the server instance + self.set[server.name].type = type; + self.set[server.name].electionId = ismaster ? ismaster.electionId : ismaster; + self.set[server.name].setName = ismaster ? ismaster.setName : ismaster; + self.set[server.name].setVersion = ismaster ? ismaster.setVersion : ismaster; + // Add to the list + list.push(server); +} + +function compareObjectIds(id1, id2) { + var a = new Buffer(id1.toHexString(), 'hex'); + var b = new Buffer(id2.toHexString(), 'hex'); + + if(a === b) { + return 0; + } + + if(typeof Buffer.compare === 'function') { + return Buffer.compare(a, b); + } + + var x = a.length; + var y = b.length; + var len = Math.min(x, y); + + for (var i = 0; i < len; i++) { + if (a[i] !== b[i]) { + break; + } + } + + if (i !== len) { + x = a[i]; + y = b[i]; + } + + return x < y ? -1 : y < x ? 1 : 0; +} + +function removeFrom(server, list) { + for(var i = 0; i < list.length; i++) { + if(list[i].equals && list[i].equals(server)) { + list.splice(i, 1); + return true; + } else if(typeof list[i] == 'string' && list[i] == server.name) { + list.splice(i, 1); + return true; + } + } + + return false; +} + +function emitTopologyDescriptionChanged(self) { + if(self.listeners('topologyDescriptionChanged').length > 0) { + var topology = 'Unknown'; + var setName = self.setName; + + if(self.hasPrimaryAndSecondary()) { + topology = 'ReplicaSetWithPrimary'; + } else if(!self.hasPrimary() && self.hasSecondary()) { + topology = 'ReplicaSetNoPrimary'; + } + + // Generate description + var description = { + topologyType: topology, + setName: setName, + servers: [] + } + + // Add the primary to the list + if(self.hasPrimary()) { + var desc = self.primary.getDescription(); + desc.type = 'RSPrimary'; + description.servers.push(desc); + } + + // Add all the secondaries + description.servers = description.servers.concat(self.secondaries.map(function(x) { + var description = x.getDescription(); + description.type = 'RSSecondary'; + return description; + })); + + // Add all the arbiters + description.servers = description.servers.concat(self.arbiters.map(function(x) { + var description = x.getDescription(); + description.type = 'RSArbiter'; + return description; + })); + + // Add all the passives + description.servers = description.servers.concat(self.passives.map(function(x) { + var description = x.getDescription(); + description.type = 'RSSecondary'; + return description; + })); + + // Create the result + var result = { + topologyId: self.id, + previousDescription: self.replicasetDescription, + newDescription: description, + diff: diff(self.replicasetDescription, description) + }; + + // Emit the topologyDescription change + self.emit('topologyDescriptionChanged', result); + + // Set the new description + self.replicasetDescription = description; + } +} + +function diff(previous, current) { + // Difference document + var diff = { + servers: [] + } + + // Previous entry + if(!previous) { + previous = { servers: [] }; + } + + // Got through all the servers + for(var i = 0; i < previous.servers.length; i++) { + var prevServer = previous.servers[i]; + + // Go through all current servers + for(var j = 0; j < current.servers.length; j++) { + var currServer = current.servers[j]; + + // Matching server + if(prevServer.address === currServer.address) { + // We had a change in state + if(prevServer.type != currServer.type) { + diff.servers.push({ + address: prevServer.address, + from: prevServer.type, + to: currServer.type + }); + } + } + } + } + + // Return difference + return diff; +} + +module.exports = ReplSetState; diff --git a/node_modules/mongodb-core/lib/topologies/server.js b/node_modules/mongodb-core/lib/topologies/server.js new file mode 100644 index 0000000..6304274 --- /dev/null +++ b/node_modules/mongodb-core/lib/topologies/server.js @@ -0,0 +1,805 @@ +"use strict" + +var inherits = require('util').inherits, + f = require('util').format, + EventEmitter = require('events').EventEmitter, + BSON = require('bson').native().BSON, + ReadPreference = require('./read_preference'), + Logger = require('../connection/logger'), + debugOptions = require('../connection/utils').debugOptions, + Pool = require('../connection/pool'), + Query = require('../connection/commands').Query, + MongoError = require('../error'), + PreTwoSixWireProtocolSupport = require('../wireprotocol/2_4_support'), + TwoSixWireProtocolSupport = require('../wireprotocol/2_6_support'), + ThreeTwoWireProtocolSupport = require('../wireprotocol/3_2_support'), + BasicCursor = require('../cursor'), + sdam = require('./shared'), + assign = require('./shared').assign; + +// Used for filtering out fields for loggin +var debugFields = ['reconnect', 'reconnectTries', 'reconnectInterval', 'emitError', 'cursorFactory', 'host' + , 'port', 'size', 'keepAlive', 'keepAliveInitialDelay', 'noDelay', 'connectionTimeout', 'checkServerIdentity' + , 'socketTimeout', 'singleBufferSerializtion', 'ssl', 'ca', 'cert', 'key', 'rejectUnauthorized', 'promoteLongs']; + +// Server instance id +var id = 0; +var serverAccounting = false; +var servers = {}; + +/** + * Creates a new Server instance + * @class + * @param {boolean} [options.reconnect=true] Server will attempt to reconnect on loss of connection + * @param {number} [options.reconnectTries=30] Server attempt to reconnect #times + * @param {number} [options.reconnectInterval=1000] Server will wait # milliseconds between retries + * @param {number} [options.monitoring=true] Enable the server state monitoring (calling ismaster at monitoringInterval) + * @param {number} [options.monitoringInterval=5000] The interval of calling ismaster when monitoring is enabled. + * @param {Cursor} [options.cursorFactory=Cursor] The cursor factory class used for all query cursors + * @param {string} options.host The server host + * @param {number} options.port The server port + * @param {number} [options.size=5] Server connection pool size + * @param {boolean} [options.keepAlive=true] TCP Connection keep alive enabled + * @param {number} [options.keepAliveInitialDelay=0] Initial delay before TCP keep alive enabled + * @param {boolean} [options.noDelay=true] TCP Connection no delay + * @param {number} [options.connectionTimeout=0] TCP Connection timeout setting + * @param {number} [options.socketTimeout=0] TCP Socket timeout setting + * @param {boolean} [options.ssl=false] Use SSL for connection + * @param {boolean|function} [options.checkServerIdentity=true] Ensure we check server identify during SSL, set to false to disable checking. Only works for Node 0.12.x or higher. You can pass in a boolean or your own checkServerIdentity override function. + * @param {Buffer} [options.ca] SSL Certificate store binary buffer + * @param {Buffer} [options.cert] SSL Certificate binary buffer + * @param {Buffer} [options.key] SSL Key file binary buffer + * @param {string} [options.passphrase] SSL Certificate pass phrase + * @param {boolean} [options.rejectUnauthorized=true] Reject unauthorized server certificates + * @param {boolean} [options.promoteLongs=true] Convert Long values from the db into Numbers if they fit into 53 bits + * @return {Server} A cursor instance + * @fires Server#connect + * @fires Server#close + * @fires Server#error + * @fires Server#timeout + * @fires Server#parseError + * @fires Server#reconnect + * @fires Server#reconnectFailed + * @fires Server#serverHeartbeatStarted + * @fires Server#serverHeartbeatSucceeded + * @fires Server#serverHeartbeatFailed + * @fires Server#topologyOpening + * @fires Server#topologyClosed + * @fires Server#topologyDescriptionChanged + */ +var Server = function(options) { + options = options || {}; + + // Add event listener + EventEmitter.call(this); + + // Server instance id + this.id = id++; + + // Reconnect retries + var reconnectTries = options.reconnectTries || 30; + + // Internal state + this.s = { + // Options + options: options, + // Logger + logger: Logger('Server', options), + // Factory overrides + Cursor: options.cursorFactory || BasicCursor, + // BSON instance + bson: options.bson || new BSON(), + // Pool + pool: null, + // Disconnect handler + disconnectHandler: options.disconnectHandler, + // Monitor thread (keeps the connection alive) + monitoring: typeof options.monitoring == 'boolean' ? options.monitoring : true, + // Is the server in a topology + inTopology: typeof options.inTopology == 'boolean' ? options.inTopology : false, + // Monitoring timeout + monitoringInterval: typeof options.monitoringInterval == 'number' + ? options.monitoringInterval + : 5000, + // Topology id + topologyId: -1 + } + + // Curent ismaster + this.ismaster = null; + // Current ping time + this.lastIsMasterMS = -1; + // The monitoringProcessId + this.monitoringProcessId = null; + // Initial connection + this.initalConnect = true; + // Wire protocol handler, default to oldest known protocol handler + // this gets changed when the first ismaster is called. + this.wireProtocolHandler = new PreTwoSixWireProtocolSupport(); + // Default type + this._type = 'server'; +} + +inherits(Server, EventEmitter); + +Object.defineProperty(Server.prototype, 'type', { + enumerable:true, get: function() { return this._type; } +}); + +Server.enableServerAccounting = function() { + serverAccounting = true; + servers = {}; +} + +Server.disableServerAccounting = function() { + serverAccounting = false; +} + +Server.servers = function() { + return servers; +} + +Object.defineProperty(Server.prototype, 'name', { + enumerable:true, + get: function() { return this.s.options.host + ":" + this.s.options.port; } +}); + +function configureWireProtocolHandler(self, ismaster) { + // 3.2 wire protocol handler + if(ismaster.maxWireVersion >= 4) { + return new ThreeTwoWireProtocolSupport(new TwoSixWireProtocolSupport()); + } + + // 2.6 wire protocol handler + if(ismaster.maxWireVersion >= 2) { + return new TwoSixWireProtocolSupport(); + } + + // 2.4 or earlier wire protocol handler + return new PreTwoSixWireProtocolSupport(); +} + +function disconnectHandler(self, type, ns, cmd, options, callback) { + // Topology is not connected, save the call in the provided store to be + // Executed at some point when the handler deems it's reconnected + if(!self.s.pool.isConnected() && self.s.disconnectHandler != null && !options.monitoring) { + self.s.disconnectHandler.add(type, ns, cmd, options, callback); + return true; + } + + // If we have no connection error + if(!self.s.pool.isConnected()) { + callback(MongoError.create(f("no connection available to server %s", self.name))); + return true; + } +} + +function monitoringProcess(self) { + return function() { + // Pool was destroyed do not continue process + if(self.s.pool.isDestroyed()) return; + // Emit monitoring Process event + self.emit('monitoring', self); + // Perform ismaster call + // Query options + var queryOptions = { numberToSkip: 0, numberToReturn: -1, checkKeys: false, slaveOk: true }; + // Create a query instance + var query = new Query(self.s.bson, 'admin.$cmd', {ismaster:true}, queryOptions); + // Get start time + var start = new Date().getTime(); + // Execute the ismaster query + self.s.pool.write(query.toBin(), {}, function(err, result) { + // Set initial lastIsMasterMS + self.lastIsMasterMS = new Date().getTime() - start; + if(self.s.pool.isDestroyed()) return; + // Update the ismaster view if we have a result + if(result) { + self.ismaster = result.result; + // It's a proxy change the type so + // the wireprotocol will send $readPreference + if(self.ismaster.msg == 'isdbgrid') { + self._type = 'mongos'; + } + } + // Re-schedule the monitoring process + self.monitoringProcessId = setTimeout(monitoringProcess(self), self.s.monitoringInterval); + }); + } +} + +var eventHandler = function(self, event) { + return function(err) { + // Log information of received information if in info mode + if(self.s.logger.isInfo()) { + var object = err instanceof MongoError ? JSON.stringify(err) : {} + self.s.logger.info(f('server %s fired event %s out with message %s' + , self.name, event, object)); + } + + // Handle connect event + if(event == 'connect') { + // Issue an ismaster command at connect + // Query options + var queryOptions = { numberToSkip: 0, numberToReturn: -1, checkKeys: false, slaveOk: true }; + // Create a query instance + var query = new Query(self.s.bson, 'admin.$cmd', {ismaster:true}, queryOptions); + // Get start time + var start = new Date().getTime(); + // Execute the ismaster query + self.s.pool.write(query.toBin(), {}, function(err, result) { + // Set initial lastIsMasterMS + self.lastIsMasterMS = new Date().getTime() - start; + if(err) { + self.destroy(); + if(self.listeners('error').length > 0) self.emit('error', err); + return; + } + + // Ensure no error emitted after initial connect when reconnecting + self.initalConnect = false; + // Save the ismaster + self.ismaster = result.result; + // Add the correct wire protocol handler + self.wireProtocolHandler = configureWireProtocolHandler(self, self.ismaster); + // Have we defined self monitoring + if(self.s.monitoring) { + self.monitoringProcessId = setTimeout(monitoringProcess(self), self.s.monitoringInterval); + } + + // Emit server description changed if something listening + sdam.emitServerDescriptionChanged(self, { + address: self.name, arbiters: [], hosts: [], passives: [], type: !self.s.inTopology ? 'Standalone' : sdam.getTopologyType(self) + }); + + // Emit topology description changed if something listening + sdam.emitTopologyDescriptionChanged(self, { + topologyType: 'Single', servers: [{address: self.name, arbiters: [], hosts: [], passives: [], type: 'Standalone'}] + }); + + // Log the ismaster if available + if(self.s.logger.isInfo()) { + self.s.logger.info(f('server %s connected with ismaster [%s]', self.name, JSON.stringify(self.ismaster))); + } + + // Emit connect + self.emit('connect', self); + }); + } else if(event == 'error' || event == 'parseError' + || event == 'close' || event == 'timeout' || event == 'reconnect' + || event == 'attemptReconnect' || 'reconnectFailed') { + + // Remove server instance from accounting + if(serverAccounting && ['close', 'timeout', 'error', 'parseError', 'reconnectFailed'].indexOf(event) != -1) { + // Emit toplogy opening event if not in topology + if(!self.s.inTopology) { + self.emit('topologyOpening', { topologyId: self.id }); + } + + delete servers[self.id]; + } + + // Reconnect failed return error + if(event == 'reconnectFailed') { + self.emit('reconnectFailed', err); + // Emit error if any listeners + if(self.listeners('error').length > 0) { + self.emit('error', err); + } + // Terminate + return; + } + + // On first connect fail + if(self.s.pool.state == 'disconnected' && self.initalConnect && ['close', 'timeout', 'error', 'parseError'].indexOf(event) != -1) { + self.initalConnect = false; + return self.emit('error', new MongoError(f('failed to connect to server [%s] on first connect', self.name))); + } + + // Reconnect event, emit the server + if(event == 'reconnect') { + return self.emit(event, self); + } + + // Emit the event + self.emit(event, err); + } + } +} + +/** + * Initiate server connect + * @method + * @param {array} [options.auth=null] Array of auth options to apply on connect + */ +Server.prototype.connect = function(options) { + var self = this; + options = options || {}; + + // Set the connections + if(serverAccounting) servers[this.id] = this; + + // Do not allow connect to be called on anything that's not disconnected + if(self.s.pool && !self.s.pool.isDisconnected() && !self.s.pool.isDestroyed()) { + throw MongoError.create(f('server instance in invalid state %s', self.s.state)); + } + + // Create a pool + self.s.pool = new Pool(assign(self.s.options, options, {bson: this.s.bson})); + + // Set up listeners + self.s.pool.on('close', eventHandler(self, 'close')); + self.s.pool.on('error', eventHandler(self, 'error')); + self.s.pool.on('timeout', eventHandler(self, 'timeout')); + self.s.pool.on('parseError', eventHandler(self, 'parseError')); + self.s.pool.on('connect', eventHandler(self, 'connect')); + self.s.pool.on('reconnect', eventHandler(self, 'reconnect')); + self.s.pool.on('reconnectFailed', eventHandler(self, 'reconnectFailed')); + + // Emit toplogy opening event if not in topology + if(!self.s.inTopology) { + this.emit('topologyOpening', { topologyId: self.id }); + } + + // Emit opening server event + self.emit('serverOpening', { + topologyId: self.s.topologyId != -1 ? self.s.topologyId : self.id, + address: self.name + }); + + // Connect with optional auth settings + if(options.auth) { + self.s.pool.connect.apply(self.s.pool, options.auth); + } else { + self.s.pool.connect(); + } +} + +/** + * Get the server description + * @method + * @return {object} +*/ +Server.prototype.getDescription = function() { + var ismaster = this.ismaster || {}; + var description = { + type: sdam.getTopologyType(this), + address: this.name, + }; + + // Add fields if available + if(ismaster.hosts) description.hosts = ismaster.hosts; + if(ismaster.arbiters) description.arbiters = ismaster.arbiters; + if(ismaster.passives) description.passives = ismaster.passives; + if(ismaster.setName) description.setName = ismaster.setName; + return description; +} + +/** + * Returns the last known ismaster document for this server + * @method + * @return {object} + */ +Server.prototype.lastIsMaster = function() { + return this.ismaster; +} + +/** + * Unref all connections belong to this server + * @method + */ +Server.prototype.unref = function() { + this.s.pool.unref(); +} + +/** + * Figure out if the server is connected + * @method + * @return {boolean} + */ +Server.prototype.isConnected = function() { + if(!this.s.pool) return false; + return this.s.pool.isConnected(); +} + +/** + * Figure out if the server instance was destroyed by calling destroy + * @method + * @return {boolean} + */ +Server.prototype.isDestroyed = function() { + if(!this.s.pool) return false; + return this.s.pool.isDestroyed(); +} + +function basicWriteValidations(self, options) { + if(!self.s.pool) return MongoError.create('server instance is not connected'); + if(self.s.pool.isDestroyed()) return MongoError.create('server instance pool was destroyed'); +} + +function basicReadValidations(self, options) { + basicWriteValidations(self, options); + + if(options.readPreference && !(options.readPreference instanceof ReadPreference)) { + throw new Error("readPreference must be an instance of ReadPreference"); + } +} + +/** + * Execute a command + * @method + * @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.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 {opResultCallback} callback A callback function + */ +Server.prototype.command = function(ns, cmd, options, callback) { + var self = this; + if(typeof options == 'function') callback = options, options = {}, options = options || {}; + var result = basicReadValidations(self, options); + if(result) return callback(result); + + // Debug log + if(self.s.logger.isDebug()) self.s.logger.debug(f('executing command [%s] against %s', JSON.stringify({ + ns: ns, cmd: cmd, options: debugOptions(debugFields, options) + }), self.name)); + + // If we are not connected or have a disconnectHandler specified + if(disconnectHandler(self, 'command', ns, cmd, options, callback)) return; + + // Query options + var queryOptions = { + numberToSkip: 0, + numberToReturn: -1, + checkKeys: typeof options.checkKeys == 'boolean' ? options.checkKeys: false, + serializeFunctions: typeof options.serializeFunctions == 'boolean' ? options.serializeFunctions : false, + ignoreUndefined: typeof options.ignoreUndefined == 'boolean' ? options.ignoreUndefined : false + }; + + // Create a query instance + var query = new Query(self.s.bson, ns, cmd, queryOptions); + // Set slave OK of the query + query.slaveOk = options.readPreference ? options.readPreference.slaveOk() : false; + + // Write options + var writeOptions = { + raw: typeof options.raw == 'boolean' ? options.raw : false, + promoteLongs: typeof options.promoteLongs == 'boolean' ? options.promoteLongs : true, + command: true, + monitoring: typeof options.monitoring == 'boolean' ? options.monitoring : false, + }; + + // Write the operation to the pool + self.s.pool.write(query.toBin(), 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 {opResultCallback} callback A callback function + */ +Server.prototype.insert = function(ns, ops, options, callback) { + var self = this; + if(typeof options == 'function') callback = options, options = {}, options = options || {}; + var result = basicWriteValidations(self, options); + if(result) return callback(result); + + // If we are not connected or have a disconnectHandler specified + if(disconnectHandler(self, 'insert', ns, ops, options, callback)) return; + + // Setup the docs as an array + ops = Array.isArray(ops) ? ops : [ops]; + + // Execute write + return self.wireProtocolHandler.insert(self.s.pool, self.ismaster, ns, self.s.bson, 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 {opResultCallback} callback A callback function + */ +Server.prototype.update = function(ns, ops, options, callback) { + var self = this; + if(typeof options == 'function') callback = options, options = {}, options = options || {}; + var result = basicWriteValidations(self, options); + if(result) return callback(result); + + // If we are not connected or have a disconnectHandler specified + if(disconnectHandler(self, 'update', ns, ops, options, callback)) return; + + // Setup the docs as an array + ops = Array.isArray(ops) ? ops : [ops]; + // Execute write + return self.wireProtocolHandler.update(self.s.pool, self.ismaster, ns, self.s.bson, 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 {opResultCallback} callback A callback function + */ +Server.prototype.remove = function(ns, ops, options, callback) { + var self = this; + if(typeof options == 'function') callback = options, options = {}, options = options || {}; + var result = basicWriteValidations(self, options); + if(result) return callback(result); + + // If we are not connected or have a disconnectHandler specified + if(disconnectHandler(self, 'remove', ns, ops, options, callback)) return; + + // Setup the docs as an array + ops = Array.isArray(ops) ? ops : [ops]; + // Execute write + return self.wireProtocolHandler.remove(self.s.pool, self.ismaster, ns, self.s.bson, ops, options, callback); +} + +/** + * Get a new cursor + * @method + * @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1) + * @param {{object}|{Long}} cmd Can be either a command returning a cursor or a cursorId + * @param {object} [options.batchSize=0] Batchsize for the operation + * @param {array} [options.documents=[]] Initial documents list for cursor + * @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.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields. + * @param {opResultCallback} callback A callback function + */ +Server.prototype.cursor = function(ns, cmd, cursorOptions) { + var s = this.s; + cursorOptions = cursorOptions || {}; + // Set up final cursor type + var FinalCursor = cursorOptions.cursorFactory || s.Cursor; + // Return the cursor + return new FinalCursor(s.bson, ns, cmd, cursorOptions, this, s.options); +} + +/** + * Logout from a database + * @method + * @param {string} db The db we are logging out from + * @param {authResultCallback} callback A callback function + */ +Server.prototype.logout = function(dbName, callback) { + this.s.pool.logout(dbName, callback); +} + +/** + * Authenticate using a specified mechanism + * @method + * @param {string} mechanism The Auth mechanism we are invoking + * @param {string} db The db we are invoking the mechanism against + * @param {...object} param Parameters for the specific mechanism + * @param {authResultCallback} callback A callback function + */ +Server.prototype.auth = function(mechanism, db) { + var self = this; + + // If we have the default mechanism we pick mechanism based on the wire + // protocol max version. If it's >= 3 then scram-sha1 otherwise mongodb-cr + if(mechanism == 'default' && self.ismaster && self.ismaster.maxWireVersion >= 3) { + mechanism = 'scram-sha-1'; + } else if(mechanism == 'default') { + mechanism = 'mongocr'; + } + + // Slice all the arguments off + var args = Array.prototype.slice.call(arguments, 0); + // Set the mechanism + args[0] = mechanism; + // Get the callback + var callback = args[args.length - 1]; + + // If we are not connected or have a disconnectHandler specified + //function disconnectHandler(self, type, ns, cmd, options, callback) { + if(disconnectHandler(self, 'auth', db, args, {}, callback)) { + return; + } + + // Do not authenticate if we are an arbiter + if(this.lastIsMaster() && this.lastIsMaster().arbiterOnly) { + return callback(null, true); + } + + // Apply the arguments to the pool + self.s.pool.auth.apply(self.s.pool, args); +} + +/** + * Compare two server instances + * @method + * @param {Server} server Server to compare equality against + * @return {boolean} + */ +Server.prototype.equals = function(server) { + if(typeof server == 'string') return this.name == server; + if(server.name) return this.name == server.name; + return false; +} + +/** + * All raw connections + * @method + * @return {Connection[]} + */ +Server.prototype.connections = function() { + return this.s.pool.allConnections(); +} + +/** + * Get server + * @method + * @return {Server} + */ +Server.prototype.getServer = function() { + return this; +} + +/** + * Get connection + * @method + * @return {Connection} + */ +Server.prototype.getConnection = function() { + return this.s.pool.get(); +} + +var listeners = ['close', 'error', 'timeout', 'parseError', 'connect']; + +/** + * Destroy the server connection + * @method + * @param {boolean} [options.emitClose=false] Emit close event on destroy + * @param {boolean} [options.emitDestroy=false] Emit destroy event on destroy + */ +Server.prototype.destroy = function(options) { + options = options || {}; + var self = this; + + // Set the connections + if(serverAccounting) delete servers[this.id]; + + // Destroy the monitoring process if any + if(this.monitoringProcessId) { + clearTimeout(this.monitoringProcessId); + } + + // Emit close event + if(options.emitClose) { + self.emit('close', self); + } + + // Emit destroy event + if(options.emitDestroy) { + self.emit('destroy', self); + } + + // Remove all listeners + listeners.forEach(function(event) { + self.s.pool.removeAllListeners(event); + }); + + // Emit opening server event + if(self.listeners('serverClosed').length > 0) self.emit('serverClosed', { + topologyId: self.s.topologyId != -1 ? self.s.topologyId : self.id, address: self.name + }); + + // Emit toplogy opening event if not in topology + if(self.listeners('topologyClosed').length > 0 && !self.s.inTopology) { + self.emit('topologyClosed', { topologyId: self.id }); + } + + if(self.s.logger.isDebug()) { + self.s.logger.debug(f('destroy called on server %s', self.name)); + } + + // Destroy the pool + this.s.pool.destroy(); +} + +/** + * A server connect event, used to verify that the connection is up and running + * + * @event Server#connect + * @type {Server} + */ + +/** + * A server reconnect event, used to verify that the server topology has reconnected + * + * @event Server#reconnect + * @type {Server} + */ + +/** + * A server opening SDAM monitoring event + * + * @event Server#serverOpening + * @type {object} + */ + +/** + * A server closed SDAM monitoring event + * + * @event Server#serverClosed + * @type {object} + */ + +/** + * A server description SDAM change monitoring event + * + * @event Server#serverDescriptionChanged + * @type {object} + */ + +/** + * A topology open SDAM event + * + * @event Server#topologyOpening + * @type {object} + */ + +/** + * A topology closed SDAM event + * + * @event Server#topologyClosed + * @type {object} + */ + +/** + * A topology structure SDAM change event + * + * @event Server#topologyDescriptionChanged + * @type {object} + */ + +/** + * Server reconnect failed + * + * @event Server#reconnectFailed + * @type {Error} + */ + +/** + * Server connection pool closed + * + * @event Server#close + * @type {object} + */ + +/** + * Server connection pool caused an error + * + * @event Server#error + * @type {Error} + */ + +/** + * Server destroyed was called + * + * @event Server#destroy + * @type {Server} + */ + +module.exports = Server; diff --git a/node_modules/mongodb-core/lib/topologies/shared.js b/node_modules/mongodb-core/lib/topologies/shared.js new file mode 100644 index 0000000..902ac98 --- /dev/null +++ b/node_modules/mongodb-core/lib/topologies/shared.js @@ -0,0 +1,173 @@ +"use strict" + +/** + * Emit event if it exists + * @method + */ +function emitSDAMEvent(self, event, description) { + if(self.listeners(event).length > 0) { + self.emit(event, description); + } +} + +var getPreviousDescription = function(self) { + if(!self.s.serverDescription) { + self.s.serverDescription = { + address: self.name, + arbiters: [], hosts: [], passives: [], type: 'Unknown' + } + } + + return self.s.serverDescription; +} + +var emitServerDescriptionChanged = function(self, description) { + if(self.listeners('serverDescriptionChanged').length > 0) { + // Emit the server description changed events + self.emit('serverDescriptionChanged', { + topologyId: self.s.topologyId != -1 ? self.s.topologyId : self.id, address: self.name, + previousDescription: getPreviousDescription(self), + newDescription: description + }); + + self.s.serverDescription = description; + } +} + +var getPreviousTopologyDescription = function(self) { + if(!self.s.topologyDescription) { + self.s.topologyDescription = { + topologyType: 'Unknown', + servers: [{ + address: self.name, arbiters: [], hosts: [], passives: [], type: 'Unknown' + }] + } + } + + return self.s.topologyDescription; +} + +var emitTopologyDescriptionChanged = function(self, description) { + if(self.listeners('topologyDescriptionChanged').length > 0) { + // Emit the server description changed events + self.emit('topologyDescriptionChanged', { + topologyId: self.s.topologyId != -1 ? self.s.topologyId : self.id, address: self.name, + previousDescription: getPreviousTopologyDescription(self), + newDescription: description + }); + + self.s.serverDescription = description; + } +} + +var changedIsMaster = function(self, currentIsmaster, ismaster) { + var currentType = getTopologyType(self, currentIsmaster); + var newType = getTopologyType(self, ismaster); + if(newType != currentType) return true; + return false; +} + +var getTopologyType = function(self, ismaster) { + if(!ismaster) { + ismaster = self.ismaster; + } + + if(!ismaster) return 'Unknown'; + if(ismaster.ismaster && !ismaster.hosts) return 'Standalone'; + if(ismaster.ismaster && ismaster.msg == 'isdbgrid') return 'Mongos'; + if(ismaster.ismaster) return 'RSPrimary'; + if(ismaster.secondary) return 'RSSecondary'; + if(ismaster.arbiterOnly) return 'RSArbiter'; + return 'Unknown'; +} + +var inquireServerState = function(self) { + return function(callback) { + if(self.s.state == 'destroyed') return; + // Record response time + var start = new Date().getTime(); + + // emitSDAMEvent + emitSDAMEvent(self, 'serverHeartbeatStarted', { connectionId: self.name }); + + // Attempt to execute ismaster command + self.command('admin.$cmd', { ismaster:true }, { monitoring:true }, function(err, r) { + if(!err) { + // Legacy event sender + self.emit('ismaster', r, self); + + // Calculate latencyMS + var latencyMS = new Date().getTime() - start; + + // Server heart beat event + emitSDAMEvent(self, 'serverHeartbeatSucceeded', { durationMS: latencyMS, reply: r.result, connectionId: self.name }); + + // Did the server change + if(changedIsMaster(self, self.s.ismaster, r.result)) { + // Emit server description changed if something listening + emitServerDescriptionChanged(self, { + address: self.name, arbiters: [], hosts: [], passives: [], type: !self.s.inTopology ? 'Standalone' : getTopologyType(self) + }); + } + + // Updat ismaster view + self.s.ismaster = r.result; + + // Set server response time + self.s.isMasterLatencyMS = latencyMS; + } else { + emitSDAMEvent(self, 'serverHearbeatFailed', { durationMS: latencyMS, failure: err, connectionId: self.name }); + } + + // Peforming an ismaster monitoring callback operation + if(typeof callback == 'function') { + return callback(err, r); + } + + // Perform another sweep + self.s.inquireServerStateTimeout = setTimeout(inquireServerState(self), self.s.haInterval); + }); + }; +} + +// Object.assign method or polyfille +var assign = Object.assign ? Object.assign : function assign(target, firstSource) { + if (target === undefined || target === null) { + throw new TypeError('Cannot convert first argument to object'); + } + + var to = Object(target); + for (var i = 1; i < arguments.length; i++) { + var nextSource = arguments[i]; + if (nextSource === undefined || nextSource === null) { + continue; + } + + var keysArray = Object.keys(Object(nextSource)); + for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) { + var nextKey = keysArray[nextIndex]; + var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey); + if (desc !== undefined && desc.enumerable) { + to[nextKey] = nextSource[nextKey]; + } + } + } + return to; +} + +// +// Clone the options +var cloneOptions = function(options) { + var opts = {}; + for(var name in options) { + opts[name] = options[name]; + } + return opts; +} + +module.exports.inquireServerState = inquireServerState +module.exports.getTopologyType = getTopologyType; +module.exports.emitServerDescriptionChanged = emitServerDescriptionChanged; +module.exports.emitTopologyDescriptionChanged = emitTopologyDescriptionChanged; +module.exports.cloneOptions = cloneOptions; +module.exports.assign = assign; diff --git a/node_modules/mongodb-core/lib/wireprotocol/2_4_support.js b/node_modules/mongodb-core/lib/wireprotocol/2_4_support.js new file mode 100644 index 0000000..88c758c --- /dev/null +++ b/node_modules/mongodb-core/lib/wireprotocol/2_4_support.js @@ -0,0 +1,555 @@ +"use strict"; + +var Insert = require('./commands').Insert + , Update = require('./commands').Update + , Remove = require('./commands').Remove + , Query = require('../connection/commands').Query + , copy = require('../connection/utils').copy + , KillCursor = require('../connection/commands').KillCursor + , GetMore = require('../connection/commands').GetMore + , Query = require('../connection/commands').Query + , ReadPreference = require('../topologies/read_preference') + , f = require('util').format + , CommandResult = require('../connection/command_result') + , MongoError = require('../error') + , Long = require('bson').Long + , getReadPreference = require('./shared').getReadPreference; + +// Write concern fields +var writeConcernFields = ['w', 'wtimeout', 'j', 'fsync']; + +var WireProtocol = function() {} + +// +// Needs to support legacy mass insert as well as ordered/unordered legacy +// emulation +// +WireProtocol.prototype.insert = function(pool, ismaster, ns, bson, ops, options, callback) { + options = options || {}; + // Default is ordered execution + var ordered = typeof options.ordered == 'boolean' ? options.ordered : true; + var legacy = typeof options.legacy == 'boolean' ? options.legacy : false; + ops = Array.isArray(ops) ? ops :[ops]; + + // If we have more than a 1000 ops fails + if(ops.length > 1000) return callback(new MongoError("exceeded maximum write batch size of 1000")); + + // Write concern + var writeConcern = options.writeConcern || {w:1}; + + // We are unordered + if(!ordered || writeConcern.w == 0) { + return executeUnordered('insert', Insert, ismaster, ns, bson, pool, ops, options, callback); + } + + return executeOrdered('insert', Insert, ismaster, ns, bson, pool, ops, options, callback); +} + +WireProtocol.prototype.update = function(pool, ismaster, ns, bson, ops, options, callback) { + options = options || {}; + // Default is ordered execution + var ordered = typeof options.ordered == 'boolean' ? options.ordered : true; + ops = Array.isArray(ops) ? ops :[ops]; + + // Write concern + var writeConcern = options.writeConcern || {w:1}; + + // We are unordered + if(!ordered || writeConcern.w == 0) { + return executeUnordered('update', Update, ismaster, ns, bson, pool, ops, options, callback); + } + + return executeOrdered('update', Update, ismaster, ns, bson, pool, ops, options, callback); +} + +WireProtocol.prototype.remove = function(pool, ismaster, ns, bson, ops, options, callback) { + options = options || {}; + // Default is ordered execution + var ordered = typeof options.ordered == 'boolean' ? options.ordered : true; + ops = Array.isArray(ops) ? ops :[ops]; + + // Write concern + var writeConcern = options.writeConcern || {w:1}; + + // We are unordered + if(!ordered || writeConcern.w == 0) { + return executeUnordered('remove', Remove, ismaster, ns, bson, pool, ops, options, callback); + } + + return executeOrdered('remove', Remove, ismaster, ns, bson, pool, ops, options, callback); +} + +WireProtocol.prototype.killCursor = function(bson, ns, cursorId, pool, callback) { + // Create a kill cursor command + var killCursor = new KillCursor(bson, [cursorId]); + // Execute the kill cursor command + if(pool && pool.isConnected()) { + pool.write(killCursor.toBin(), { + immediateRelease:true, noResponse: true + }); + } + + // Callback + if(typeof callback == 'function') callback(null, null); +} + +WireProtocol.prototype.getMore = function(bson, ns, cursorState, batchSize, raw, connection, options, callback) { + // Create getMore command + var getMore = new GetMore(bson, ns, cursorState.cursorId, {numberToReturn: batchSize}); + + // Query callback + var queryCallback = function(err, result) { + if(err) return callback(err); + // Get the raw message + var r = result.message; + + // If we have a timed out query or a cursor that was killed + if((r.responseFlags & (1 << 0)) != 0) { + return callback(new MongoError("cursor does not exist, was killed or timed out"), null); + } + + // Ensure we have a Long valie cursor id + var cursorId = typeof r.cursorId == 'number' + ? Long.fromNumber(r.cursorId) + : r.cursorId; + + // Set all the values + cursorState.documents = r.documents; + cursorState.cursorId = cursorId; + + // Return + callback(null, null, r.connection); + } + + // If we have a raw query decorate the function + if(raw) { + queryCallback.raw = raw; + } + + // Check if we need to promote longs + if(typeof cursorState.promoteLongs == 'boolean') { + queryCallback.promoteLongs = cursorState.promoteLongs; + } + + // Write out the getMore command + connection.write(getMore.toBin(), queryCallback); +} + +WireProtocol.prototype.command = function(bson, ns, cmd, cursorState, topology, options) { + // Establish type of command + if(cmd.find) { + return setupClassicFind(bson, ns, cmd, cursorState, topology, options) + } else if(cursorState.cursorId != null) { + } else if(cmd) { + return setupCommand(bson, ns, cmd, cursorState, topology, options); + } else { + throw new MongoError(f("command %s does not return a cursor", JSON.stringify(cmd))); + } +} + +// +// Execute a find command +var setupClassicFind = function(bson, ns, cmd, cursorState, topology, options) { + // Ensure we have at least some options + options = options || {}; + // Get the readPreference + var readPreference = getReadPreference(cmd, options); + // Set the optional batchSize + cursorState.batchSize = cmd.batchSize || cursorState.batchSize; + var numberToReturn = 0; + + // Unpack the limit and batchSize values + if(cursorState.limit == 0) { + numberToReturn = cursorState.batchSize; + } else if(cursorState.limit < 0 || cursorState.limit < cursorState.batchSize || (cursorState.limit > 0 && cursorState.batchSize == 0)) { + numberToReturn = cursorState.limit; + } else { + numberToReturn = cursorState.batchSize; + } + + var numberToSkip = cursorState.skip || 0; + // Build actual find command + var findCmd = {}; + // Using special modifier + var usesSpecialModifier = false; + + // We have a Mongos topology, check if we need to add a readPreference + if(topology.type == 'mongos' && readPreference) { + findCmd['$readPreference'] = readPreference.toJSON(); + usesSpecialModifier = true; + } + + // Add special modifiers to the query + if(cmd.sort) findCmd['orderby'] = cmd.sort, usesSpecialModifier = true; + if(cmd.hint) findCmd['$hint'] = cmd.hint, usesSpecialModifier = true; + if(cmd.snapshot) findCmd['$snapshot'] = cmd.snapshot, usesSpecialModifier = true; + if(cmd.returnKey) findCmd['$returnKey'] = cmd.returnKey, usesSpecialModifier = true; + if(cmd.maxScan) findCmd['$maxScan'] = cmd.maxScan, usesSpecialModifier = true; + if(cmd.min) findCmd['$min'] = cmd.min, usesSpecialModifier = true; + if(cmd.max) findCmd['$max'] = cmd.max, usesSpecialModifier = true; + if(cmd.showDiskLoc) findCmd['$showDiskLoc'] = cmd.showDiskLoc, usesSpecialModifier = true; + if(cmd.comment) findCmd['$comment'] = cmd.comment, usesSpecialModifier = true; + if(cmd.maxTimeMS) findCmd['$maxTimeMS'] = cmd.maxTimeMS, usesSpecialModifier = true; + + if(cmd.explain) { + // nToReturn must be 0 (match all) or negative (match N and close cursor) + // nToReturn > 0 will give explain results equivalent to limit(0) + numberToReturn = -Math.abs(cmd.limit || 0); + usesSpecialModifier = true; + findCmd['$explain'] = true; + } + + // If we have a special modifier + if(usesSpecialModifier) { + findCmd['$query'] = cmd.query; + } else { + findCmd = cmd.query; + } + + // Throw on majority readConcern passed in + if(cmd.readConcern && cmd.readConcern.level != 'local') { + throw new MongoError(f('server find command does not support a readConcern level of %s', cmd.readConcern.level)); + } + + // Remove readConcern, ensure no failing commands + if(cmd.readConcern) { + cmd = copy(cmd); + delete cmd['readConcern']; + } + + // Set up the serialize and ignoreUndefined fields + var serializeFunctions = typeof options.serializeFunctions == 'boolean' + ? options.serializeFunctions : false; + var ignoreUndefined = typeof options.ignoreUndefined == 'boolean' + ? options.ignoreUndefined : false; + + // Build Query object + var query = new Query(bson, ns, findCmd, { + numberToSkip: numberToSkip, numberToReturn: numberToReturn + , checkKeys: false, returnFieldSelector: cmd.fields + , serializeFunctions: serializeFunctions, ignoreUndefined: ignoreUndefined + }); + + // Set query flags + query.slaveOk = readPreference.slaveOk(); + + // Set up the option bits for wire protocol + if(typeof cmd.tailable == 'boolean') query.tailable = cmd.tailable; + if(typeof cmd.oplogReplay == 'boolean') query.oplogReplay = cmd.oplogReplay; + if(typeof cmd.noCursorTimeout == 'boolean') query.noCursorTimeout = cmd.noCursorTimeout; + if(typeof cmd.awaitData == 'boolean') query.awaitData = cmd.awaitData; + if(typeof cmd.partial == 'boolean') query.partial = cmd.partial; + // Return the query + return query; +} + +// +// Set up a command cursor +var setupCommand = function(bson, ns, cmd, cursorState, topology, options) { + // Set empty options object + options = options || {} + // Get the readPreference + var readPreference = getReadPreference(cmd, options); + // Final query + var finalCmd = {}; + for(var name in cmd) { + finalCmd[name] = cmd[name]; + } + + // Build command namespace + var parts = ns.split(/\./); + + // Throw on majority readConcern passed in + if(cmd.readConcern && cmd.readConcern.level != 'local') { + throw new MongoError(f('server %s command does not support a readConcern level of %s', JSON.stringify(cmd), cmd.readConcern.level)); + } + + // Remove readConcern, ensure no failing commands + if(cmd.readConcern) delete cmd['readConcern']; + + // Serialize functions + var serializeFunctions = typeof options.serializeFunctions == 'boolean' + ? options.serializeFunctions : false; + + // Set up the serialize and ignoreUndefined fields + var ignoreUndefined = typeof options.ignoreUndefined == 'boolean' + ? options.ignoreUndefined : false; + + // We have a Mongos topology, check if we need to add a readPreference + if(topology.type == 'mongos' + && readPreference + && readPreference.preference != 'primary') { + finalCmd = { + '$query': finalCmd, + '$readPreference': readPreference.toJSON() + }; + } + + // Build Query object + var query = new Query(bson, f('%s.$cmd', parts.shift()), finalCmd, { + numberToSkip: 0, numberToReturn: -1 + , checkKeys: false, serializeFunctions: serializeFunctions + , ignoreUndefined: ignoreUndefined + }); + + // Set query flags + query.slaveOk = readPreference.slaveOk(); + + // Return the query + return query; +} + +var hasWriteConcern = function(writeConcern) { + if(writeConcern.w + || writeConcern.wtimeout + || writeConcern.j == true + || writeConcern.fsync == true + || Object.keys(writeConcern).length == 0) { + return true; + } + return false; +} + +var cloneWriteConcern = function(writeConcern) { + var wc = {}; + if(writeConcern.w != null) wc.w = writeConcern.w; + if(writeConcern.wtimeout != null) wc.wtimeout = writeConcern.wtimeout; + if(writeConcern.j != null) wc.j = writeConcern.j; + if(writeConcern.fsync != null) wc.fsync = writeConcern.fsync; + return wc; +} + +// +// Aggregate up all the results +// +var aggregateWriteOperationResults = function(opType, ops, results, connection) { + var finalResult = { ok: 1, n: 0 } + + // Map all the results coming back + for(var i = 0; i < results.length; i++) { + var result = results[i]; + var op = ops[i]; + + if((result.upserted || (result.updatedExisting == false)) && finalResult.upserted == null) { + finalResult.upserted = []; + } + + // Push the upserted document to the list of upserted values + if(result.upserted) { + finalResult.upserted.push({index: i, _id: result.upserted}); + } + + // We have an upsert where we passed in a _id + if(result.updatedExisting == false && result.n == 1 && result.upserted == null) { + finalResult.upserted.push({index: i, _id: op.q._id}); + } + + // We have an insert command + if(result.ok == 1 && opType == 'insert' && result.err == null) { + finalResult.n = finalResult.n + 1; + } + + // We have a command error + if(result != null && result.ok == 0 || result.err || result.errmsg) { + if(result.ok == 0) finalResult.ok = 0; + finalResult.code = result.code; + finalResult.errmsg = result.errmsg || result.err || result.errMsg; + + // Check if we have a write error + if(result.code == 11000 + || result.code == 11001 + || result.code == 12582 + || result.code == 16544 + || result.code == 16538 + || result.code == 16542 + || result.code == 14 + || result.code == 13511) { + if(finalResult.writeErrors == null) finalResult.writeErrors = []; + finalResult.writeErrors.push({ + index: i + , code: result.code + , errmsg: result.errmsg || result.err || result.errMsg + }); + } else { + finalResult.writeConcernError = { + code: result.code + , errmsg: result.errmsg || result.err || result.errMsg + } + } + } else if(typeof result.n == 'number') { + finalResult.n += result.n; + } else { + finalResult.n += 1; + } + + // Result as expected + if(result != null && result.lastOp) finalResult.lastOp = result.lastOp; + } + + // Return finalResult aggregated results + return new CommandResult(finalResult, connection); +} + +// +// Execute all inserts in an ordered manner +// +var executeOrdered = function(opType ,command, ismaster, ns, bson, pool, ops, options, callback) { + var _ops = ops.slice(0); + // Collect all the getLastErrors + var getLastErrors = []; + // Execute an operation + var executeOp = function(list, _callback) { + // No more items in the list + if(list.length == 0) { + return process.nextTick(function() { + _callback(null, aggregateWriteOperationResults(opType, ops, getLastErrors, null)); + }); + } + + // Get the first operation + var doc = list.shift(); + // Create an insert command + var op = new command(Query.getRequestId(), ismaster, bson, ns, [doc], options); + // Write concern + var optionWriteConcern = options.writeConcern || {w:1}; + // Final write concern + var writeConcern = cloneWriteConcern(optionWriteConcern); + + // Get the db name + var db = ns.split('.').shift(); + + try { + // Add binary message to list of commands to execute + var commands = [op.toBin()]; + + // Add getLastOrdered + var getLastErrorCmd = {getlasterror: 1}; + // Merge all the fields + for(var i = 0; i < writeConcernFields.length; i++) { + if(writeConcern[writeConcernFields[i]] != null) { + getLastErrorCmd[writeConcernFields[i]] = writeConcern[writeConcernFields[i]]; + } + } + + // Create a getLastError command + var getLastErrorOp = new Query(bson, f("%s.$cmd", db), getLastErrorCmd, {numberToReturn: -1}); + // Add getLastError command to list of ops to execute + commands.push(getLastErrorOp.toBin()); + + // getLastError callback + var getLastErrorCallback = function(err, result) { + if(err) return callback(err); + // Get the document + var doc = result.result; + // Save the getLastError document + getLastErrors.push(doc); + + // If we have an error terminate + if(doc.ok == 0 || doc.err || doc.errmsg) { + return callback(null, aggregateWriteOperationResults(opType, ops, getLastErrors, result.connection)); + } + + // Execute the next op in the list + executeOp(list, callback); + } + + // Write both commands out at the same time + pool.write(commands, getLastErrorCallback); + } catch(err) { + if(typeof err == 'string') err = new MongoError(err); + // We have a serialization error, rewrite as a write error to have same behavior as modern + // write commands + getLastErrors.push({ ok: 1, errmsg: err.message, code: 14 }); + // Return due to an error + process.nextTick(function() { + _callback(null, aggregateWriteOperationResults(opType, ops, getLastErrors, null)); + }); + } + } + + // Execute the operations + executeOp(_ops, callback); +} + +var executeUnordered = function(opType, command, ismaster, ns, bson, pool, ops, options, callback) { + // Total operations to write + var totalOps = ops.length; + // Collect all the getLastErrors + var getLastErrors = []; + // Write concern + var optionWriteConcern = options.writeConcern || {w:1}; + // Final write concern + var writeConcern = cloneWriteConcern(optionWriteConcern); + // Driver level error + var error; + + // Execute all the operations + for(var i = 0; i < ops.length; i++) { + // Create an insert command + var op = new command(Query.getRequestId(), ismaster, bson, ns, [ops[i]], options); + // Get db name + var db = ns.split('.').shift(); + + try { + // Add binary message to list of commands to execute + var commands = [op.toBin()]; + + // If write concern 0 don't fire getLastError + if(hasWriteConcern(writeConcern)) { + var getLastErrorCmd = {getlasterror: 1}; + // Merge all the fields + for(var j = 0; j < writeConcernFields.length; j++) { + if(writeConcern[writeConcernFields[j]] != null) + getLastErrorCmd[writeConcernFields[j]] = writeConcern[writeConcernFields[j]]; + } + + // Create a getLastError command + var getLastErrorOp = new Query(bson, f("%s.$cmd", db), getLastErrorCmd, {numberToReturn: -1}); + // Add getLastError command to list of ops to execute + commands.push(getLastErrorOp.toBin()); + + // Give the result from getLastError the right index + var callbackOp = function(_index) { + return function(err, result) { + if(err) error = err; + // Update the number of operations executed + totalOps = totalOps - 1; + // Save the getLastError document + if(!err) getLastErrors[_index] = result.result; + // Check if we are done + if(totalOps == 0) { + process.nextTick(function() { + if(error) return callback(error); + callback(null, aggregateWriteOperationResults(opType, ops, getLastErrors, result.connection)); + }); + } + } + } + + // Write both commands out at the same time + pool.write(commands, callbackOp(i)); + } else { + pool.write(commands, {immediateRelease:true, noResponse:true}); + } + } catch(err) { + if(typeof err == 'string') err = new MongoError(err); + // Update the number of operations executed + totalOps = totalOps - 1; + // We have a serialization error, rewrite as a write error to have same behavior as modern + // write commands + getLastErrors[i] = { ok: 1, errmsg: err.message, code: 14 }; + // Check if we are done + if(totalOps == 0) { + callback(null, aggregateWriteOperationResults(opType, ops, getLastErrors, null)); + } + } + } + + // Empty w:0 return + if(writeConcern + && writeConcern.w == 0 && callback) { + callback(null, new CommandResult({ok:1}, null)); + } +} + +module.exports = WireProtocol; diff --git a/node_modules/mongodb-core/lib/wireprotocol/2_6_support.js b/node_modules/mongodb-core/lib/wireprotocol/2_6_support.js new file mode 100644 index 0000000..0349dc6 --- /dev/null +++ b/node_modules/mongodb-core/lib/wireprotocol/2_6_support.js @@ -0,0 +1,324 @@ +"use strict"; + +var Insert = require('./commands').Insert + , Update = require('./commands').Update + , Remove = require('./commands').Remove + , Query = require('../connection/commands').Query + , copy = require('../connection/utils').copy + , KillCursor = require('../connection/commands').KillCursor + , GetMore = require('../connection/commands').GetMore + , Query = require('../connection/commands').Query + , ReadPreference = require('../topologies/read_preference') + , f = require('util').format + , CommandResult = require('../connection/command_result') + , MongoError = require('../error') + , Long = require('bson').Long + , getReadPreference = require('./shared').getReadPreference; + +var WireProtocol = function() {} + +// +// Execute a write operation +var executeWrite = function(pool, bson, type, opsField, ns, ops, options, callback) { + if(ops.length == 0) throw new MongoError("insert must contain at least one document"); + if(typeof options == 'function') { + callback = options; + options = {}; + options = options || {}; + } + + // Split the ns up to get db and collection + var p = ns.split("."); + var d = p.shift(); + // Options + var ordered = typeof options.ordered == 'boolean' ? options.ordered : true; + var writeConcern = options.writeConcern; + + // return skeleton + var writeCommand = {}; + writeCommand[type] = p.join('.'); + writeCommand[opsField] = ops; + writeCommand.ordered = ordered; + + // Did we specify a write concern + if(writeConcern && Object.keys(writeConcern).length > 0) { + writeCommand.writeConcern = writeConcern; + } + + // Do we have bypassDocumentValidation set, then enable it on the write command + if(typeof options.bypassDocumentValidation == 'boolean') { + writeCommand.bypassDocumentValidation = options.bypassDocumentValidation; + } + + // Options object + var opts = { command: true }; + var queryOptions = { checkKeys : false, numberToSkip: 0, numberToReturn: 1 }; + if(type == 'insert') queryOptions.checkKeys = true; + // Ensure we support serialization of functions + if(options.serializeFunctions) queryOptions.serializeFunctions = options.serializeFunctions; + // Do not serialize the undefined fields + if(options.ignoreUndefined) queryOptions.ignoreUndefined = options.ignoreUndefined; + + try { + // Create write command + var cmd = new Query(bson, f("%s.$cmd", d), writeCommand, queryOptions); + // Execute command + pool.write(cmd.toBin(), opts, callback); + } catch(err) { + callback(err); + } +} + +// +// Needs to support legacy mass insert as well as ordered/unordered legacy +// emulation +// +WireProtocol.prototype.insert = function(pool, ismaster, ns, bson, ops, options, callback) { + executeWrite(pool, bson, 'insert', 'documents', ns, ops, options, callback); +} + +WireProtocol.prototype.update = function(pool, ismaster, ns, bson, ops, options, callback) { + executeWrite(pool, bson, 'update', 'updates', ns, ops, options, callback); +} + +WireProtocol.prototype.remove = function(pool, ismaster, ns, bson, ops, options, callback) { + executeWrite(pool, bson, 'delete', 'deletes', ns, ops, options, callback); +} + +WireProtocol.prototype.killCursor = function(bson, ns, cursorId, pool, callback) { + // Create a kill cursor command + var killCursor = new KillCursor(bson, [cursorId]); + // Execute the kill cursor command + if(pool && pool.isConnected()) { + pool.write(killCursor.toBin(), { + immediateRelease:true, noResponse: true + }); + } + + // Callback + if(typeof callback == 'function') callback(null, null); +} + +WireProtocol.prototype.getMore = function(bson, ns, cursorState, batchSize, raw, connection, options, callback) { + // Create getMore command + var getMore = new GetMore(bson, ns, cursorState.cursorId, {numberToReturn: batchSize}); + + // Query callback + var queryCallback = function(err, result) { + if(err) return callback(err); + // Get the raw message + var r = result.message; + + // If we have a timed out query or a cursor that was killed + if((r.responseFlags & (1 << 0)) != 0) { + return callback(new MongoError("cursor does not exist, was killed or timed out"), null); + } + + // Ensure we have a Long valie cursor id + var cursorId = typeof r.cursorId == 'number' + ? Long.fromNumber(r.cursorId) + : r.cursorId; + + // Set all the values + cursorState.documents = r.documents; + cursorState.cursorId = cursorId; + + // Return + callback(null, null, r.connection); + } + + // If we have a raw query decorate the function + if(raw) { + queryCallback.raw = raw; + } + + // Check if we need to promote longs + if(typeof cursorState.promoteLongs == 'boolean') { + queryCallback.promoteLongs = cursorState.promoteLongs; + } + + // Write out the getMore command + connection.write(getMore.toBin(), queryCallback); +} + +WireProtocol.prototype.command = function(bson, ns, cmd, cursorState, topology, options) { + // Establish type of command + if(cmd.find) { + return setupClassicFind(bson, ns, cmd, cursorState, topology, options) + } else if(cursorState.cursorId != null) { + } else if(cmd) { + return setupCommand(bson, ns, cmd, cursorState, topology, options); + } else { + throw new MongoError(f("command %s does not return a cursor", JSON.stringify(cmd))); + } +} + +// +// Execute a find command +var setupClassicFind = function(bson, ns, cmd, cursorState, topology, options) { + // Ensure we have at least some options + options = options || {}; + // Get the readPreference + var readPreference = getReadPreference(cmd, options); + // Set the optional batchSize + cursorState.batchSize = cmd.batchSize || cursorState.batchSize; + var numberToReturn = 0; + + // Unpack the limit and batchSize values + if(cursorState.limit == 0) { + numberToReturn = cursorState.batchSize; + } else if(cursorState.limit < 0 || cursorState.limit < cursorState.batchSize || (cursorState.limit > 0 && cursorState.batchSize == 0)) { + numberToReturn = cursorState.limit; + } else { + numberToReturn = cursorState.batchSize; + } + + var numberToSkip = cursorState.skip || 0; + // Build actual find command + var findCmd = {}; + // Using special modifier + var usesSpecialModifier = false; + + // We have a Mongos topology, check if we need to add a readPreference + if(topology.type == 'mongos' && readPreference) { + findCmd['$readPreference'] = readPreference.toJSON(); + usesSpecialModifier = true; + } + + // Add special modifiers to the query + if(cmd.sort) findCmd['orderby'] = cmd.sort, usesSpecialModifier = true; + if(cmd.hint) findCmd['$hint'] = cmd.hint, usesSpecialModifier = true; + if(cmd.snapshot) findCmd['$snapshot'] = cmd.snapshot, usesSpecialModifier = true; + if(cmd.returnKey) findCmd['$returnKey'] = cmd.returnKey, usesSpecialModifier = true; + if(cmd.maxScan) findCmd['$maxScan'] = cmd.maxScan, usesSpecialModifier = true; + if(cmd.min) findCmd['$min'] = cmd.min, usesSpecialModifier = true; + if(cmd.max) findCmd['$max'] = cmd.max, usesSpecialModifier = true; + if(cmd.showDiskLoc) findCmd['$showDiskLoc'] = cmd.showDiskLoc, usesSpecialModifier = true; + if(cmd.comment) findCmd['$comment'] = cmd.comment, usesSpecialModifier = true; + if(cmd.maxTimeMS) findCmd['$maxTimeMS'] = cmd.maxTimeMS, usesSpecialModifier = true; + + if(cmd.explain) { + // nToReturn must be 0 (match all) or negative (match N and close cursor) + // nToReturn > 0 will give explain results equivalent to limit(0) + numberToReturn = -Math.abs(cmd.limit || 0); + usesSpecialModifier = true; + findCmd['$explain'] = true; + } + + // If we have a special modifier + if(usesSpecialModifier) { + findCmd['$query'] = cmd.query; + } else { + findCmd = cmd.query; + } + + // Throw on majority readConcern passed in + if(cmd.readConcern && cmd.readConcern.level != 'local') { + throw new MongoError(f('server find command does not support a readConcern level of %s', cmd.readConcern.level)); + } + + // Remove readConcern, ensure no failing commands + if(cmd.readConcern) { + cmd = copy(cmd); + delete cmd['readConcern']; + } + + // Serialize functions + var serializeFunctions = typeof options.serializeFunctions == 'boolean' + ? options.serializeFunctions : false; + var ignoreUndefined = typeof options.ignoreUndefined == 'boolean' + ? options.ignoreUndefined : false; + + // Build Query object + var query = new Query(bson, ns, findCmd, { + numberToSkip: numberToSkip, numberToReturn: numberToReturn + , checkKeys: false, returnFieldSelector: cmd.fields + , serializeFunctions: serializeFunctions + , ignoreUndefined: ignoreUndefined + }); + + // Set query flags + query.slaveOk = readPreference.slaveOk(); + + // Set up the option bits for wire protocol + if(typeof cmd.tailable == 'boolean') { + query.tailable = cmd.tailable; + } + + if(typeof cmd.oplogReplay == 'boolean') { + query.oplogReplay = cmd.oplogReplay; + } + + if(typeof cmd.noCursorTimeout == 'boolean') { + query.noCursorTimeout = cmd.noCursorTimeout; + } + + if(typeof cmd.awaitData == 'boolean') { + query.awaitData = cmd.awaitData; + } + + if(typeof cmd.partial == 'boolean') { + query.partial = cmd.partial; + } + + // Return the query + return query; +} + +// +// Set up a command cursor +var setupCommand = function(bson, ns, cmd, cursorState, topology, options) { + // Set empty options object + options = options || {} + // Get the readPreference + var readPreference = getReadPreference(cmd, options); + + // Final query + var finalCmd = {}; + for(var name in cmd) { + finalCmd[name] = cmd[name]; + } + + // Build command namespace + var parts = ns.split(/\./); + + // Serialize functions + var serializeFunctions = typeof options.serializeFunctions == 'boolean' + ? options.serializeFunctions : false; + + var ignoreUndefined = typeof options.ignoreUndefined == 'boolean' + ? options.ignoreUndefined : false; + + // Throw on majority readConcern passed in + if(cmd.readConcern && cmd.readConcern.level != 'local') { + throw new MongoError(f('server %s command does not support a readConcern level of %s', JSON.stringify(cmd), cmd.readConcern.level)); + } + + // Remove readConcern, ensure no failing commands + if(cmd.readConcern) delete cmd['readConcern']; + + // We have a Mongos topology, check if we need to add a readPreference + if(topology.type == 'mongos' + && readPreference + && readPreference.preference != 'primary') { + finalCmd = { + '$query': finalCmd, + '$readPreference': readPreference.toJSON() + }; + } + + // Build Query object + var query = new Query(bson, f('%s.$cmd', parts.shift()), finalCmd, { + numberToSkip: 0, numberToReturn: -1 + , checkKeys: false, serializeFunctions: serializeFunctions + , ignoreUndefined: ignoreUndefined + }); + + // Set query flags + query.slaveOk = readPreference.slaveOk(); + + // Return the query + return query; +} + +module.exports = WireProtocol; diff --git a/node_modules/mongodb-core/lib/wireprotocol/3_2_support.js b/node_modules/mongodb-core/lib/wireprotocol/3_2_support.js new file mode 100644 index 0000000..dc919e1 --- /dev/null +++ b/node_modules/mongodb-core/lib/wireprotocol/3_2_support.js @@ -0,0 +1,520 @@ +"use strict"; + +var Insert = require('./commands').Insert + , Update = require('./commands').Update + , Remove = require('./commands').Remove + , Query = require('../connection/commands').Query + , copy = require('../connection/utils').copy + , KillCursor = require('../connection/commands').KillCursor + , GetMore = require('../connection/commands').GetMore + , Query = require('../connection/commands').Query + , ReadPreference = require('../topologies/read_preference') + , f = require('util').format + , CommandResult = require('../connection/command_result') + , MongoError = require('../error') + , Long = require('bson').Long + , getReadPreference = require('./shared').getReadPreference; + +var WireProtocol = function(legacyWireProtocol) { + this.legacyWireProtocol = legacyWireProtocol; +} + +// +// Execute a write operation +var executeWrite = function(pool, bson, type, opsField, ns, ops, options, callback) { + if(ops.length == 0) throw new MongoError("insert must contain at least one document"); + if(typeof options == 'function') { + callback = options; + options = {}; + options = options || {}; + } + + // Split the ns up to get db and collection + var p = ns.split("."); + var d = p.shift(); + // Options + var ordered = typeof options.ordered == 'boolean' ? options.ordered : true; + var writeConcern = options.writeConcern; + + // return skeleton + var writeCommand = {}; + writeCommand[type] = p.join('.'); + writeCommand[opsField] = ops; + writeCommand.ordered = ordered; + + // Did we specify a write concern + if(writeConcern && Object.keys(writeConcern).length > 0) { + writeCommand.writeConcern = writeConcern; + } + + // Do we have bypassDocumentValidation set, then enable it on the write command + if(typeof options.bypassDocumentValidation == 'boolean') { + writeCommand.bypassDocumentValidation = options.bypassDocumentValidation; + } + + // Options object + var opts = { command: true }; + var queryOptions = { checkKeys : false, numberToSkip: 0, numberToReturn: 1 }; + if(type == 'insert') queryOptions.checkKeys = true; + + // Ensure we support serialization of functions + if(options.serializeFunctions) queryOptions.serializeFunctions = options.serializeFunctions; + // Do not serialize the undefined fields + if(options.ignoreUndefined) queryOptions.ignoreUndefined = options.ignoreUndefined; + + try { + // Create write command + var cmd = new Query(bson, f("%s.$cmd", d), writeCommand, queryOptions); + // Execute command + pool.write(cmd.toBin(), opts, callback); + } catch(err) { + callback(err); + } +} + +// +// Needs to support legacy mass insert as well as ordered/unordered legacy +// emulation +// +WireProtocol.prototype.insert = function(pool, ismaster, ns, bson, ops, options, callback) { + executeWrite(pool, bson, 'insert', 'documents', ns, ops, options, callback); +} + +WireProtocol.prototype.update = function(pool, ismaster, ns, bson, ops, options, callback) { + executeWrite(pool, bson, 'update', 'updates', ns, ops, options, callback); +} + +WireProtocol.prototype.remove = function(pool, ismaster, ns, bson, ops, options, callback) { + executeWrite(pool, bson, 'delete', 'deletes', ns, ops, options, callback); +} + +WireProtocol.prototype.killCursor = function(bson, ns, cursorId, pool, callback) { + // Build command namespace + var parts = ns.split(/\./); + // Command namespace + var commandns = f('%s.$cmd', parts.shift()); + // Create getMore command + var killcursorCmd = { + killCursors: parts.join('.'), + cursors: [cursorId] + } + + // Build Query object + var query = new Query(bson, commandns, killcursorCmd, { + numberToSkip: 0, numberToReturn: -1 + , checkKeys: false, returnFieldSelector: null + }); + + // Set query flags + query.slaveOk = true; + + // Kill cursor callback + var killCursorCallback = function(err, result) { + if(err) { + if(typeof callback != 'function') return; + return callback(err); + } + + // Result + var r = result.message; + // If we have a timed out query or a cursor that was killed + if((r.responseFlags & (1 << 0)) != 0) { + if(typeof callback != 'function') return; + return callback(new MongoError("cursor killed or timed out"), null); + } + + if(!Array.isArray(r.documents) || r.documents.length == 0) { + if(typeof callback != 'function') return; + return callback(new MongoError(f('invalid killCursors result returned for cursor id %s', cursorState.cursorId))); + } + + // Return the result + if(typeof callback == 'function') { + callback(null, r.documents[0]); + } + } + + // Execute the kill cursor command + if(pool && pool.isConnected()) { + pool.write(query.toBin(), { command: true }, killCursorCallback); + } +} + +WireProtocol.prototype.getMore = function(bson, ns, cursorState, batchSize, raw, connection, options, callback) { + options = options || {}; + // Build command namespace + var parts = ns.split(/\./); + // Command namespace + var commandns = f('%s.$cmd', parts.shift()); + + // Check if we have an maxTimeMS set + var maxTimeMS = typeof cursorState.cmd.maxTimeMS == 'number' ? cursorState.cmd.maxTimeMS : 3000; + + // Create getMore command + var getMoreCmd = { + getMore: cursorState.cursorId, + collection: parts.join('.'), + batchSize: Math.abs(batchSize) + } + + if(cursorState.cmd.tailable + && typeof cursorState.cmd.maxAwaitTimeMS == 'number') { + getMoreCmd.maxTimeMS = cursorState.cmd.maxAwaitTimeMS; + } + + // Build Query object + var query = new Query(bson, commandns, getMoreCmd, { + numberToSkip: 0, numberToReturn: -1 + , checkKeys: false, returnFieldSelector: null + }); + + // Set query flags + query.slaveOk = true; + + // Query callback + var queryCallback = function(err, result) { + if(err) return callback(err); + // Get the raw message + var r = result.message; + + // If we have a timed out query or a cursor that was killed + if((r.responseFlags & (1 << 0)) != 0) { + return callback(new MongoError("cursor killed or timed out"), null); + } + + // Raw, return all the extracted documents + if(raw) { + cursorState.documents = r.documents; + cursorState.cursorId = r.cursorId; + return callback(null, r.documents); + } + + // We have an error detected + if(r.documents[0].ok == 0) { + return callback(MongoError.create(r.documents[0])); + } + + // Ensure we have a Long valid cursor id + var cursorId = typeof r.documents[0].cursor.id == 'number' + ? Long.fromNumber(r.documents[0].cursor.id) + : r.documents[0].cursor.id; + + // Set all the values + cursorState.documents = r.documents[0].cursor.nextBatch; + cursorState.cursorId = cursorId; + + // Return the result + callback(null, r.documents[0], r.connection); + } + + // Query options + var queryOptions = { command: true }; + + // If we have a raw query decorate the function + if(raw) { + queryOptions.raw = raw; + } + + // Add the result field needed + queryOptions.documentsReturnedIn = 'nextBatch'; + + // Check if we need to promote longs + if(typeof cursorState.promoteLongs == 'boolean') { + queryOptions.promoteLongs = cursorState.promoteLongs; + } + + // Write out the getMore command + connection.write(query.toBin(), queryOptions, queryCallback); + global.debug = true +} + +WireProtocol.prototype.command = function(bson, ns, cmd, cursorState, topology, options) { + // Establish type of command + if(cmd.find) { + // Create the find command + var query = executeFindCommand(bson, ns, cmd, cursorState, topology, options) + // Mark the cmd as virtual + cmd.virtual = false; + // Signal the documents are in the firstBatch value + query.documentsReturnedIn = 'firstBatch'; + // Return the query + return query; + } else if(cursorState.cursorId != null) { + } else if(cmd) { + return setupCommand(bson, ns, cmd, cursorState, topology, options); + } else { + throw new MongoError(f("command %s does not return a cursor", JSON.stringify(cmd))); + } +} + +// // Command +// { +// find: ns +// , query: <object> +// , limit: <n> +// , fields: <object> +// , skip: <n> +// , hint: <string> +// , explain: <boolean> +// , snapshot: <boolean> +// , batchSize: <n> +// , returnKey: <boolean> +// , maxScan: <n> +// , min: <n> +// , max: <n> +// , showDiskLoc: <boolean> +// , comment: <string> +// , maxTimeMS: <n> +// , raw: <boolean> +// , readPreference: <ReadPreference> +// , tailable: <boolean> +// , oplogReplay: <boolean> +// , noCursorTimeout: <boolean> +// , awaitdata: <boolean> +// , exhaust: <boolean> +// , partial: <boolean> +// } + +// FIND/GETMORE SPEC +// { +// “find”: <string>, +// “filter”: { ... }, +// “sort”: { ... }, +// “projection”: { ... }, +// “hint”: { ... }, +// “skip”: <int>, +// “limit”: <int>, +// “batchSize”: <int>, +// “singleBatch”: <bool>, +// “comment”: <string>, +// “maxScan”: <int>, +// “maxTimeMS”: <int>, +// “max”: { ... }, +// “min”: { ... }, +// “returnKey”: <bool>, +// “showRecordId”: <bool>, +// “snapshot”: <bool>, +// “tailable”: <bool>, +// “oplogReplay”: <bool>, +// “noCursorTimeout”: <bool>, +// “awaitData”: <bool>, +// “partial”: <bool>, +// “$readPreference”: { ... } +// } + +// +// Execute a find command +var executeFindCommand = function(bson, ns, cmd, cursorState, topology, options) { + // Ensure we have at least some options + options = options || {}; + // Get the readPreference + var readPreference = getReadPreference(cmd, options); + // Set the optional batchSize + cursorState.batchSize = cmd.batchSize || cursorState.batchSize; + + // Build command namespace + var parts = ns.split(/\./); + // Command namespace + var commandns = f('%s.$cmd', parts.shift()); + + // Build actual find command + var findCmd = { + find: parts.join('.') + }; + + // I we provided a filter + if(cmd.query) { + // Check if the user is passing in the $query parameter + if(cmd.query['$query']) { + findCmd.filter = cmd.query['$query']; + } else { + findCmd.filter = cmd.query; + } + } + + // Sort value + var sortValue = cmd.sort; + + // Handle issue of sort being an Array + if(Array.isArray(sortValue)) { + var sortObject = {}; + + if(sortValue.length > 0 && !Array.isArray(sortValue[0])) { + var sortDirection = sortValue[1]; + // Translate the sort order text + if(sortDirection == 'asc') { + sortDirection = 1; + } else if(sortDirection == 'desc') { + sortDirection = -1; + } + + // Set the sort order + sortObject[sortValue[0]] = sortDirection; + } else { + for(var i = 0; i < sortValue.length; i++) { + var sortDirection = sortValue[i][1]; + // Translate the sort order text + if(sortDirection == 'asc') { + sortDirection = 1; + } else if(sortDirection == 'desc') { + sortDirection = -1; + } + + // Set the sort order + sortObject[sortValue[i][0]] = sortDirection; + } + } + + sortValue = sortObject; + }; + + // Add sort to command + if(cmd.sort) findCmd.sort = sortValue; + // Add a projection to the command + if(cmd.fields) findCmd.projection = cmd.fields; + // Add a hint to the command + if(cmd.hint) findCmd.hint = cmd.hint; + // Add a skip + if(cmd.skip) findCmd.skip = cmd.skip; + // Add a limit + if(cmd.limit) findCmd.limit = cmd.limit; + // Add a batchSize + if(typeof cmd.batchSize == 'number') findCmd.batchSize = Math.abs(cmd.batchSize); + + // Check if we wish to have a singleBatch + if(cmd.limit < 0) { + findCmd.limit = Math.abs(cmd.limit); + findCmd.singleBatch = true; + } + + // If we have comment set + if(cmd.comment) findCmd.comment = cmd.comment; + + // If we have maxScan + if(cmd.maxScan) findCmd.maxScan = cmd.maxScan; + + // If we have maxTimeMS set + if(cmd.maxTimeMS) findCmd.maxTimeMS = cmd.maxTimeMS; + + // If we have min + if(cmd.min) findCmd.min = cmd.min; + + // If we have max + if(cmd.max) findCmd.max = cmd.max; + + // If we have returnKey set + if(cmd.returnKey) findCmd.returnKey = cmd.returnKey; + + // If we have showDiskLoc set + if(cmd.showDiskLoc) findCmd.showRecordId = cmd.showDiskLoc; + + // If we have snapshot set + if(cmd.snapshot) findCmd.snapshot = cmd.snapshot; + + // If we have tailable set + if(cmd.tailable) findCmd.tailable = cmd.tailable; + + // If we have oplogReplay set + if(cmd.oplogReplay) findCmd.oplogReplay = cmd.oplogReplay; + + // If we have noCursorTimeout set + if(cmd.noCursorTimeout) findCmd.noCursorTimeout = cmd.noCursorTimeout; + + // If we have awaitData set + if(cmd.awaitData) findCmd.awaitData = cmd.awaitData; + if(cmd.awaitdata) findCmd.awaitData = cmd.awaitdata; + + // If we have partial set + if(cmd.partial) findCmd.partial = cmd.partial; + + // If we have explain, we need to rewrite the find command + // to wrap it in the explain command + if(cmd.explain) { + findCmd = { + explain: findCmd + } + } + + // Did we provide a readConcern + if(cmd.readConcern) findCmd.readConcern = cmd.readConcern; + + // Set up the serialize and ignoreUndefined fields + var serializeFunctions = typeof options.serializeFunctions == 'boolean' + ? options.serializeFunctions : false; + var ignoreUndefined = typeof options.ignoreUndefined == 'boolean' + ? options.ignoreUndefined : false; + + // We have a Mongos topology, check if we need to add a readPreference + if(topology.type == 'mongos' + && readPreference + && readPreference.preference != 'primary') { + findCmd = { + '$query': findCmd, + '$readPreference': readPreference.toJSON() + }; + } + + // Build Query object + var query = new Query(bson, commandns, findCmd, { + numberToSkip: 0, numberToReturn: 1 + , checkKeys: false, returnFieldSelector: null + , serializeFunctions: serializeFunctions, ignoreUndefined: ignoreUndefined + }); + + // Set query flags + query.slaveOk = readPreference.slaveOk(); + + // Return the query + return query; +} + +// +// Set up a command cursor +var setupCommand = function(bson, ns, cmd, cursorState, topology, options) { + // Set empty options object + options = options || {} + // Get the readPreference + var readPreference = getReadPreference(cmd, options); + + // Final query + var finalCmd = {}; + for(var name in cmd) { + finalCmd[name] = cmd[name]; + } + + // Build command namespace + var parts = ns.split(/\./); + + // Serialize functions + var serializeFunctions = typeof options.serializeFunctions == 'boolean' + ? options.serializeFunctions : false; + + // Set up the serialize and ignoreUndefined fields + var ignoreUndefined = typeof options.ignoreUndefined == 'boolean' + ? options.ignoreUndefined : false; + + // We have a Mongos topology, check if we need to add a readPreference + if(topology.type == 'mongos' + && readPreference + && readPreference.preference != 'primary') { + finalCmd = { + '$query': finalCmd, + '$readPreference': readPreference.toJSON() + }; + } + + // Build Query object + var query = new Query(bson, f('%s.$cmd', parts.shift()), finalCmd, { + numberToSkip: 0, numberToReturn: -1 + , checkKeys: false, serializeFunctions: serializeFunctions + , ignoreUndefined: ignoreUndefined + }); + + // Set query flags + query.slaveOk = readPreference.slaveOk(); + + // Return the query + return query; +} + +module.exports = WireProtocol; diff --git a/node_modules/mongodb-core/lib/wireprotocol/commands.js b/node_modules/mongodb-core/lib/wireprotocol/commands.js new file mode 100644 index 0000000..9c665ee --- /dev/null +++ b/node_modules/mongodb-core/lib/wireprotocol/commands.js @@ -0,0 +1,357 @@ +"use strict"; + +var MongoError = require('../error'); + +// Wire command operation ids +var OP_UPDATE = 2001; +var OP_INSERT = 2002; +var OP_DELETE = 2006; + +var Insert = function(requestId, ismaster, bson, ns, documents, options) { + // Basic options needed to be passed in + if(ns == null) throw new MongoError("ns must be specified for query"); + if(!Array.isArray(documents) || documents.length == 0) throw new MongoError("documents array must contain at least one document to insert"); + + // Validate that we are not passing 0x00 in the colletion name + if(!!~ns.indexOf("\x00")) { + throw new MongoError("namespace cannot contain a null character"); + } + + // Set internal + this.requestId = requestId; + this.bson = bson; + this.ns = ns; + this.documents = documents; + this.ismaster = ismaster; + + // Ensure empty options + options = options || {}; + + // Unpack options + this.serializeFunctions = typeof options.serializeFunctions == 'boolean' ? options.serializeFunctions : false; + this.ignoreUndefined = typeof options.ignoreUndefined == 'boolean' ? options.ignoreUndefined : false; + this.checkKeys = typeof options.checkKeys == 'boolean' ? options.checkKeys : true; + this.continueOnError = typeof options.continueOnError == 'boolean' ? options.continueOnError : false; + // Set flags + this.flags = this.continueOnError ? 1 : 0; +} + +// To Binary +Insert.prototype.toBin = function() { + // Contains all the buffers to be written + var buffers = []; + + // Header buffer + var header = new Buffer( + 4 * 4 // Header + + 4 // Flags + + Buffer.byteLength(this.ns) + 1 // namespace + ); + + // Add header to buffers + buffers.push(header); + + // Total length of the message + var totalLength = header.length; + + // Serialize all the documents + for(var i = 0; i < this.documents.length; i++) { + var buffer = this.bson.serialize(this.documents[i] + , this.checkKeys + , true + , this.serializeFunctions + , 0, this.ignoreUndefined); + + // Document is larger than maxBsonObjectSize, terminate serialization + if(buffer.length > this.ismaster.maxBsonObjectSize) { + throw new MongoError("Document exceeds maximum allowed bson size of " + this.ismaster.maxBsonObjectSize + " bytes"); + } + + // Add to total length of wire protocol message + totalLength = totalLength + buffer.length; + // Add to buffer + buffers.push(buffer); + } + + // Command is larger than maxMessageSizeBytes terminate serialization + if(totalLength > this.ismaster.maxMessageSizeBytes) { + throw new MongoError("Command exceeds maximum message size of " + this.ismaster.maxMessageSizeBytes + " bytes"); + } + + // Add all the metadata + var index = 0; + + // Write header length + header[index + 3] = (totalLength >> 24) & 0xff; + header[index + 2] = (totalLength >> 16) & 0xff; + header[index + 1] = (totalLength >> 8) & 0xff; + header[index] = (totalLength) & 0xff; + index = index + 4; + + // Write header requestId + header[index + 3] = (this.requestId >> 24) & 0xff; + header[index + 2] = (this.requestId >> 16) & 0xff; + header[index + 1] = (this.requestId >> 8) & 0xff; + header[index] = (this.requestId) & 0xff; + index = index + 4; + + // No flags + header[index + 3] = (0 >> 24) & 0xff; + header[index + 2] = (0 >> 16) & 0xff; + header[index + 1] = (0 >> 8) & 0xff; + header[index] = (0) & 0xff; + index = index + 4; + + // Operation + header[index + 3] = (OP_INSERT >> 24) & 0xff; + header[index + 2] = (OP_INSERT >> 16) & 0xff; + header[index + 1] = (OP_INSERT >> 8) & 0xff; + header[index] = (OP_INSERT) & 0xff; + index = index + 4; + + // Flags + header[index + 3] = (this.flags >> 24) & 0xff; + header[index + 2] = (this.flags >> 16) & 0xff; + header[index + 1] = (this.flags >> 8) & 0xff; + header[index] = (this.flags) & 0xff; + index = index + 4; + + // Write collection name + index = index + header.write(this.ns, index, 'utf8') + 1; + header[index - 1] = 0; + + // Return the buffers + return buffers; +} + +var Update = function(requestId, ismaster, bson, ns, update, options) { + // Basic options needed to be passed in + if(ns == null) throw new MongoError("ns must be specified for query"); + + // Ensure empty options + options = options || {}; + + // Set internal + this.requestId = requestId; + this.bson = bson; + this.ns = ns; + this.ismaster = ismaster; + + // Unpack options + this.serializeFunctions = typeof options.serializeFunctions == 'boolean' ? options.serializeFunctions : false; + this.ignoreUndefined = typeof options.ignoreUndefined == 'boolean' ? options.ignoreUndefined : false; + this.checkKeys = typeof options.checkKeys == 'boolean' ? options.checkKeys : false; + + // Unpack the update document + this.upsert = typeof update[0].upsert == 'boolean' ? update[0].upsert : false; + this.multi = typeof update[0].multi == 'boolean' ? update[0].multi : false; + this.q = update[0].q; + this.u = update[0].u; + + // Create flag value + this.flags = this.upsert ? 1 : 0; + this.flags = this.multi ? this.flags | 2 : this.flags; +} + +// To Binary +Update.prototype.toBin = function() { + // Contains all the buffers to be written + var buffers = []; + + // Header buffer + var header = new Buffer( + 4 * 4 // Header + + 4 // ZERO + + Buffer.byteLength(this.ns) + 1 // namespace + + 4 // Flags + ); + + // Add header to buffers + buffers.push(header); + + // Total length of the message + var totalLength = header.length; + + // Serialize the selector + var selector = this.bson.serialize(this.q + , this.checkKeys + , true + , this.serializeFunctions + , 0, this.ignoreUndefined); + buffers.push(selector); + totalLength = totalLength + selector.length; + + // Serialize the update + var update = this.bson.serialize(this.u + , this.checkKeys + , true + , this.serializeFunctions + , 0, this.ignoreUndefined); + buffers.push(update); + totalLength = totalLength + update.length; + + // Index in header buffer + var index = 0; + + // Write header length + header[index + 3] = (totalLength >> 24) & 0xff; + header[index + 2] = (totalLength >> 16) & 0xff; + header[index + 1] = (totalLength >> 8) & 0xff; + header[index] = (totalLength) & 0xff; + index = index + 4; + + // Write header requestId + header[index + 3] = (this.requestId >> 24) & 0xff; + header[index + 2] = (this.requestId >> 16) & 0xff; + header[index + 1] = (this.requestId >> 8) & 0xff; + header[index] = (this.requestId) & 0xff; + index = index + 4; + + // No flags + header[index + 3] = (0 >> 24) & 0xff; + header[index + 2] = (0 >> 16) & 0xff; + header[index + 1] = (0 >> 8) & 0xff; + header[index] = (0) & 0xff; + index = index + 4; + + // Operation + header[index + 3] = (OP_UPDATE >> 24) & 0xff; + header[index + 2] = (OP_UPDATE >> 16) & 0xff; + header[index + 1] = (OP_UPDATE >> 8) & 0xff; + header[index] = (OP_UPDATE) & 0xff; + index = index + 4; + + // Write ZERO + header[index + 3] = (0 >> 24) & 0xff; + header[index + 2] = (0 >> 16) & 0xff; + header[index + 1] = (0 >> 8) & 0xff; + header[index] = (0) & 0xff; + index = index + 4; + + // Write collection name + index = index + header.write(this.ns, index, 'utf8') + 1; + header[index - 1] = 0; + + // Flags + header[index + 3] = (this.flags >> 24) & 0xff; + header[index + 2] = (this.flags >> 16) & 0xff; + header[index + 1] = (this.flags >> 8) & 0xff; + header[index] = (this.flags) & 0xff; + index = index + 4; + + // Return the buffers + return buffers; +} + +var Remove = function(requestId, ismaster, bson, ns, remove, options) { + // Basic options needed to be passed in + if(ns == null) throw new MongoError("ns must be specified for query"); + + // Ensure empty options + options = options || {}; + + // Set internal + this.requestId = requestId; + this.bson = bson; + this.ns = ns; + this.ismaster = ismaster; + + // Unpack options + this.serializeFunctions = typeof options.serializeFunctions == 'boolean' ? options.serializeFunctions : false; + this.ignoreUndefined = typeof options.ignoreUndefined == 'boolean' ? options.ignoreUndefined : false; + this.checkKeys = typeof options.checkKeys == 'boolean' ? options.checkKeys : false; + + // Unpack the update document + this.limit = typeof remove[0].limit == 'number' ? remove[0].limit : 1; + this.q = remove[0].q; + + // Create flag value + this.flags = this.limit == 1 ? 1 : 0; +} + +// To Binary +Remove.prototype.toBin = function() { + // Contains all the buffers to be written + var buffers = []; + + // Header buffer + var header = new Buffer( + 4 * 4 // Header + + 4 // ZERO + + Buffer.byteLength(this.ns) + 1 // namespace + + 4 // Flags + ); + + // Add header to buffers + buffers.push(header); + + // Total length of the message + var totalLength = header.length; + + // Serialize the selector + var selector = this.bson.serialize(this.q + , this.checkKeys + , true + , this.serializeFunctions + , 0, this.ignoreUndefined); + buffers.push(selector); + totalLength = totalLength + selector.length; + + // Index in header buffer + var index = 0; + + // Write header length + header[index + 3] = (totalLength >> 24) & 0xff; + header[index + 2] = (totalLength >> 16) & 0xff; + header[index + 1] = (totalLength >> 8) & 0xff; + header[index] = (totalLength) & 0xff; + index = index + 4; + + // Write header requestId + header[index + 3] = (this.requestId >> 24) & 0xff; + header[index + 2] = (this.requestId >> 16) & 0xff; + header[index + 1] = (this.requestId >> 8) & 0xff; + header[index] = (this.requestId) & 0xff; + index = index + 4; + + // No flags + header[index + 3] = (0 >> 24) & 0xff; + header[index + 2] = (0 >> 16) & 0xff; + header[index + 1] = (0 >> 8) & 0xff; + header[index] = (0) & 0xff; + index = index + 4; + + // Operation + header[index + 3] = (OP_DELETE >> 24) & 0xff; + header[index + 2] = (OP_DELETE >> 16) & 0xff; + header[index + 1] = (OP_DELETE >> 8) & 0xff; + header[index] = (OP_DELETE) & 0xff; + index = index + 4; + + // Write ZERO + header[index + 3] = (0 >> 24) & 0xff; + header[index + 2] = (0 >> 16) & 0xff; + header[index + 1] = (0 >> 8) & 0xff; + header[index] = (0) & 0xff; + index = index + 4; + + // Write collection name + index = index + header.write(this.ns, index, 'utf8') + 1; + header[index - 1] = 0; + + // Write ZERO + header[index + 3] = (this.flags >> 24) & 0xff; + header[index + 2] = (this.flags >> 16) & 0xff; + header[index + 1] = (this.flags >> 8) & 0xff; + header[index] = (this.flags) & 0xff; + index = index + 4; + + // Return the buffers + return buffers; +} + +module.exports = { + Insert: Insert + , Update: Update + , Remove: Remove +} diff --git a/node_modules/mongodb-core/lib/wireprotocol/shared.js b/node_modules/mongodb-core/lib/wireprotocol/shared.js new file mode 100644 index 0000000..3a20f51 --- /dev/null +++ b/node_modules/mongodb-core/lib/wireprotocol/shared.js @@ -0,0 +1,26 @@ +"use strict" + +var ReadPreference = require('../topologies/read_preference'); + +var getReadPreference = function(cmd, options) { + // Default to command version of the readPreference + var readPreference = cmd.readPreference || new ReadPreference('primary'); + // If we have an option readPreference override the command one + if(options.readPreference) { + readPreference = options.readPreference; + } + + if(typeof readPreference == 'string') { + readPreference = new ReadPreference(readPreference); + } + + if(!(readPreference instanceof ReadPreference)) { + throw new MongoError('readPreference must be a ReadPreference instance'); + } + + return readPreference; +} + +module.exports = { + getReadPreference: getReadPreference +} |
