This commit is contained in:
root
2019-11-28 20:40:02 +00:00
commit 1c78566f5b
2275 changed files with 272351 additions and 0 deletions
+262
View File
@@ -0,0 +1,262 @@
"use strict";
var f = require('util').format
, 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 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 {
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) {
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
}), 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
}), 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(connection, new Query(self.bson, "$external.$cmd", command, {
numberToSkip: 0, numberToReturn: 1
}), function(err, r) {
if(err) return callback(err, false);
mongo_auth_process.transition(null, function(err) {
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 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) {
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;
+181
View File
@@ -0,0 +1,181 @@
"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 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
}), 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
}), 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 {
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 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) {
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;
+168
View File
@@ -0,0 +1,168 @@
"use strict";
var f = require('util').format
, retrieveBSON = require('../connection/utils').retrieveBSON
, Query = require('../connection/commands').Query
, MongoError = require('../error');
var BSON = retrieveBSON()
, Binary = BSON.Binary;
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 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
}), 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 {
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 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) {
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;
+339
View File
@@ -0,0 +1,339 @@
"use strict";
var f = require('util').format
, crypto = require('crypto')
, retrieveBSON = require('../connection/utils').retrieveBSON
, Query = require('../connection/commands').Query
, MongoError = require('../error');
var BSON = retrieveBSON(),
Binary = BSON.Binary;
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 (i = 0; i < a.length; i++) {
res.push(a[i] ^ b[i])
}
}
return new Buffer(res);
}
var _hiCache = {};
var _hiCacheCount = 0;
var _hiCachePurge = function() {
_hiCache = {};
_hiCacheCount = 0;
};
var hi = function(data, salt, iterations) {
// omit the work if already generated
var key = [data, salt.toString('base64'), iterations].join('_');
if (_hiCache[key] !== undefined) {
return _hiCache[key];
}
// generate the salt
var saltedData = crypto.pbkdf2Sync(data, salt, iterations, 20, "sha1");
// cache a copy to speed up the next lookup, but prevent unbounded cache growth
if (_hiCacheCount >= 200) {
_hiCachePurge();
}
_hiCache[key] = saltedData;
_hiCacheCount += 1;
return saltedData;
};
/**
* 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 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 {
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
}), 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
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(',');
//
// 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
}), 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
}), 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;
// 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) {
count = count - 1;
// Done re-authenticating
if(count == 0) {
callback(err, null);
}
});
}
}
module.exports = ScramSHA1;
+250
View File
@@ -0,0 +1,250 @@
"use strict";
var f = require('util').format
, 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 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 {
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
}), 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
}), 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
}), 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
}), 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 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) {
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;
+164
View File
@@ -0,0 +1,164 @@
"use strict";
var f = require('util').format
, 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 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'
};
// Add username if specified
if(username) {
command.user = username;
}
// Let's start the process
server(connection, new Query(self.bson, "$external.$cmd", command, {
numberToSkip: 0, numberToReturn: 1
}), 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 {
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 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) {
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;
@@ -0,0 +1,34 @@
"use strict";
/**
* 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;
+546
View File
@@ -0,0 +1,546 @@
"use strict";
var retrieveBSON = require('../connection/utils').retrieveBSON;
var BSON = retrieveBSON();
var Long = BSON.Long;
// 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_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 collection 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 = Query.getRequestId();
// 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, {
checkKeys: this.checkKeys,
serializeFunctions: this.serializeFunctions,
ignoreUndefined: 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, {
checkKeys: this.checkKeys,
serializeFunctions: this.serializeFunctions,
ignoreUndefined: 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, promoteValues: true, promoteBuffers: false};
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;
this.promoteValues = typeof opts.promoteValues == 'boolean' ? opts.promoteValues : true;
this.promoteBuffers = typeof opts.promoteBuffers == 'boolean' ? opts.promoteBuffers : false;
}
Response.prototype.isParsed = function() {
return this.parsed;
}
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;
var promoteLongs = typeof options.promoteLongs == 'boolean'
? options.promoteLongs
: this.opts.promoteLongs;
var promoteValues = typeof options.promoteValues == 'boolean'
? options.promoteValues
: this.opts.promoteValues;
var promoteBuffers = typeof options.promoteBuffers == 'boolean'
? options.promoteBuffers
: this.opts.promoteBuffers
var bsonSize, _options;
// Set up the options
_options = {
promoteLongs: promoteLongs,
promoteValues: promoteValues,
promoteBuffers: promoteBuffers
};
//
// Single document and documentsReturnedIn set
//
if(this.numberReturned == 1 && documentsReturnedIn != null && raw) {
// Calculate the bson size
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;
_options.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++) {
bsonSize = this.data[this.index] | this.data[this.index + 1] << 8 | this.data[this.index + 2] << 16 | this.data[this.index + 3] << 24;
// 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
}
+619
View File
@@ -0,0 +1,619 @@
"use strict";
var inherits = require('util').inherits
, EventEmitter = require('events').EventEmitter
, net = require('net')
, tls = require('tls')
, crypto = require('crypto')
, 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', 'crl', 'cert'
, 'rejectUnauthorized', 'promoteLongs', 'promoteValues', 'promoteBuffers', '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 {number} [options.family=null] IP version for DNS lookup, passed down to Node's [`dns.lookup()` function](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback). If set to `6`, will only look for ipv6 addresses.
* @param {boolean} [options.keepAlive=true] TCP Connection keep alive enabled
* @param {number} [options.keepAliveInitialDelay=300000] Initial delay before TCP keep alive enabled
* @param {boolean} [options.noDelay=true] TCP Connection no delay
* @param {number} [options.connectionTimeout=30000] TCP Connection timeout setting
* @param {number} [options.socketTimeout=360000] 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.crl] SSL Certificate revocation 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 {boolean} [options.promoteValues=true] Promotes BSON values to native types where possible, set to false to only receive wrapper types.
* @param {boolean} [options.promoteBuffers=false] Promotes Binary BSON values to native Node Buffers.
* @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.family = typeof options.family == 'number' ? options.family : void 0;
this.keepAlive = typeof options.keepAlive == 'boolean' ? options.keepAlive : true;
this.keepAliveInitialDelay = typeof options.keepAliveInitialDelay == 'number'
? options.keepAliveInitialDelay : 300000;
this.noDelay = typeof options.noDelay == 'boolean' ? options.noDelay : true;
this.connectionTimeout = typeof options.connectionTimeout == 'number'
? options.connectionTimeout : 30000;
this.socketTimeout = typeof options.socketTimeout == 'number'
? options.socketTimeout : 360000;
// Is the keepAliveInitialDelay > socketTimeout set it to half of socketTimeout
if(this.keepAliveInitialDelay > this.socketTimeout) {
this.keepAliveInitialDelay = Math.round(this.socketTimeout/2);
}
// 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.crl = options.crl || null;
this.cert = options.cert || null;
this.key = options.key || null;
this.passphrase = options.passphrase || null;
this.ciphers = options.ciphers || null;
this.ecdhCurve = options.ecdhCurve || 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,
promoteValues: typeof options.promoteValues == 'boolean' ? options.promoteValues : true,
promoteBuffers: typeof options.promoteBuffers == 'boolean' ? options.promoteBuffers: false
}
// Flushing
this.flushing = false;
this.queue = [];
// Internal state
this.connection = null;
this.writeStream = null;
// Create hash method
var hash = crypto.createHash('sha1');
hash.update(f('%s:%s', this.host, this.port));
// Create a hash name
this.hashedName = hash.digest('hex');
// All operations in flight on the connection
this.workItems = [];
}
inherits(Connection, EventEmitter);
Connection.prototype.setSocketTimeout = function(value) {
if(this.connection) {
this.connection.setTimeout(value);
}
}
Connection.prototype.resetSocketTimeout = function() {
if(this.connection) {
this.connection.setTimeout(this.socketTimeout);
}
}
Connection.enableConnectionAccounting = function() {
connectionAccounting = true;
connections = {};
}
Connection.disableConnectionAccounting = function() {
connectionAccounting = false;
}
Connection.connections = function() {
return connections;
}
function deleteConnection(id) {
// console.log("=== deleted connection " + id + " :: " + (connections[id] ? connections[id].port : ''))
delete connections[id];
}
function addConnection(id, connection) {
// console.log("=== added connection " + id + " :: " + connection.port)
connections[id] = connection;
}
//
// Connection handlers
var errorHandler = function(self) {
return function(err) {
if(connectionAccounting) deleteConnection(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() {
if(connectionAccounting) deleteConnection(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) deleteConnection(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) {
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 {
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) {
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 {
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);
}
}
}
}
}
}
// List of socket level valid ssl options
var legalSslSocketOptions = ['pfx', 'key', 'passphrase', 'cert', 'ca', 'ciphers'
, 'NPNProtocols', 'ALPNProtocols', 'servername', 'ecdhCurve'
, 'secureProtocol', 'secureContext', 'session'
, 'minDHSize'];
function merge(options1, options2) {
// Merge in any allowed ssl options
for(var name in options2) {
if(options2[name] != null && legalSslSocketOptions.indexOf(name) != -1) {
options1[name] = options2[name];
}
}
}
/**
* Connect
* @method
*/
Connection.prototype.connect = function(_options) {
var self = this;
_options = _options || {};
// Set the connections
if(connectionAccounting) addConnection(this.id, this);
// Check if we are overriding the promoteLongs
if(typeof _options.promoteLongs == 'boolean') {
self.responseOptions.promoteLongs = _options.promoteLongs;
self.responseOptions.promoteValues = _options.promoteValues;
self.responseOptions.promoteBuffers = _options.promoteBuffers;
}
// Create new connection instance
var connection_options;
if (self.domainSocket) {
connection_options = {path: self.host};
} else {
connection_options = {port: self.port, host: self.host};
if (self.family !== void 0) {
connection_options.family = self.family;
}
}
self.connection = net.createConnection(connection_options);
// 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
}
// Merge in options
merge(sslOptions, this.options);
merge(sslOptions, _options);
// Set options for ssl
if(self.ca) sslOptions.ca = self.ca;
if(self.crl) sslOptions.crl = self.crl;
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() {
return undefined;
}
} else if(typeof self.checkServerIdentity == 'function') {
sslOptions.checkServerIdentity = self.checkServerIdentity;
}
// Set default sni servername to be the same as host
if(sslOptions.servername == null) {
sslOptions.servername = self.host;
}
// 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.once('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) deleteConnection(this.id);
if(this.connection) {
// Catch posssible exception thrown by node 0.10.x
try { this.connection.end(); } catch (err) {}
// Destroy connection
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) {
var i;
// 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(i = 0; i < buffer.length; i++)
this.logger.debug(f('writing buffer [%s] to %s:%s', buffer[i].toString('hex'), this.host, this.port));
}
}
// Double check that the connection is not destroyed
if(this.connection.destroyed === false) {
// Write out the command
if(!Array.isArray(buffer)) {
this.connection.write(buffer, 'binary');
return true;
}
// Iterate over all buffers and write them in order to the socket
for(i = 0; i < buffer.length; i++) this.connection.write(buffer[i], 'binary');
return true;
}
// Connection is destroyed return write failed
return false;
}
/**
* 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;
+228
View File
@@ -0,0 +1,228 @@
"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
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;
File diff suppressed because it is too large Load Diff
+86
View File
@@ -0,0 +1,86 @@
"use strict";
var f = require('util').format,
require_optional = require('require_optional');
// 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;
}
var retrieveBSON = function() {
var BSON = require('bson');
BSON.native = false;
try {
var optionalBSON = require_optional('bson-ext');
if(optionalBSON) {
optionalBSON.native = true;
return optionalBSON;
}
} catch(err) {}
return BSON;
}
exports.setProperty = setProperty;
exports.getProperty = getProperty;
exports.getSingleProperty = getSingleProperty;
exports.copy = copy;
exports.debugOptions = debugOptions;
exports.retrieveBSON = retrieveBSON;
+704
View File
@@ -0,0 +1,704 @@
"use strict";
var Logger = require('./connection/logger')
, retrieveBSON = require('./connection/utils').retrieveBSON
, MongoError = require('./error')
, f = require('util').format;
var BSON = retrieveBSON(),
Long = BSON.Long;
/**
* 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 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;
} else if(typeof options.promoteLongs == 'boolean') {
this.cursorState.promoteLongs = options.promoteLongs;
}
// Add promoteValues to cursor state
if(typeof topologyOptions.promoteValues == 'boolean') {
this.cursorState.promoteValues = topologyOptions.promoteValues;
} else if(typeof options.promoteValues == 'boolean') {
this.cursorState.promoteValues = options.promoteValues;
}
// Add promoteBuffers to cursor state
if(typeof topologyOptions.promoteBuffers == 'boolean') {
this.cursorState.promoteBuffers = topologyOptions.promoteBuffers;
} else if(typeof options.promoteBuffers == 'boolean') {
this.cursorState.promoteBuffers = options.promoteBuffers;
}
// 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') {
queryOptions.promoteLongs = self.cursorState.promoteLongs;
}
// Add promote values if defined
if(typeof self.cursorState.promoteValues == 'boolean') {
queryOptions.promoteValues = self.cursorState.promoteValues;
}
// Add promote values if defined
if(typeof self.cursorState.promoteBuffers == 'boolean') {
queryOptions.promoteBuffers = self.cursorState.promoteBuffers;
}
// Write the initial command out
self.server.s.pool.write(self.query, 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.server.s.pool;
// 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 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)) {
// Only need this for single server, because repl sets and mongos
// will always continue trying to reconnect
if (self.topology._type === 'server' && !self.topology.s.options.reconnect) {
// Reconnect is disabled, so we'll never reconnect
return callback(new MongoError('no connection available'));
}
if (self.disconnectHandler != null) {
if (self.topology.isDestroyed()) {
// Topology was destroyed, so don't try to wait for it to reconnect
return callback(new MongoError('Topology was destroyed'));
}
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;
// Server does not support server
if(self.cmd
&& self.cmd.collation
&& self.server.ismaster.maxWireVersion < 5) {
return callback(new MongoError(f('server %s does not support collation', self.server.name)));
}
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('connection destroyed, not possible to instantiate cursor'));
// query, cmd, options, cursorState, callback
self._find(function(err) {
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('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 || 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 ? doc.$err : undefined));
});
}
// 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;
+44
View File
@@ -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;
+59
View File
@@ -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;
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,118 @@
"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 {array} tags The tags object
* @param {object} [options] Additional read preference options
* @param {number} [options.maxStalenessSeconds] Max Secondary Read Stalleness in Seconds, Minimum value is 90 seconds.
* @property {string} preference The preference string (primary|primaryPreferred|secondary|secondaryPreferred|nearest)
* @property {array} tags The tags object
* @property {object} options Additional read preference options
* @property {number} maxStalenessSeconds MaxStalenessSeconds value for the read preference
* @return {ReadPreference}
*/
var ReadPreference = function(preference, tags, options) {
this.preference = preference;
this.tags = tags;
this.options = options;
// Add the maxStalenessSeconds value to the read Preference
if(this.options && this.options.maxStalenessSeconds != null) {
this.options = options;
this.maxStalenessSeconds = this.options.maxStalenessSeconds >= 0
? this.options.maxStalenessSeconds : null;
} else if(tags && typeof tags == 'object') {
this.options = tags, tags = null;
}
}
/**
* 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;
if(this.maxStalenessSeconds) readPreference.maxStalenessSeconds = this.maxStalenessSeconds;
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;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+880
View File
@@ -0,0 +1,880 @@
"use strict"
var inherits = require('util').inherits,
f = require('util').format,
EventEmitter = require('events').EventEmitter,
ReadPreference = require('./read_preference'),
Logger = require('../connection/logger'),
debugOptions = require('../connection/utils').debugOptions,
retrieveBSON = require('../connection/utils').retrieveBSON,
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('../utils').assign,
createClientInfo = require('./shared').createClientInfo;
// 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', 'crl', 'cert', 'key', 'rejectUnauthorized', 'promoteLongs', 'promoteValues'
, 'promoteBuffers', 'servername'];
// Server instance id
var id = 0;
var serverAccounting = false;
var servers = {};
var BSON = retrieveBSON();
/**
* 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=300000] Initial delay before TCP keep alive enabled
* @param {boolean} [options.noDelay=true] TCP Connection no delay
* @param {number} [options.connectionTimeout=30000] TCP Connection timeout setting
* @param {number} [options.socketTimeout=360000] 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.crl] SSL Certificate revocation 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 {string} [options.servername=null] String containing the server name requested via TLS SNI.
* @param {boolean} [options.promoteLongs=true] Convert Long values from the db into Numbers if they fit into 53 bits
* @param {boolean} [options.promoteValues=true] Promotes BSON values to native types where possible, set to false to only receive wrapper types.
* @param {boolean} [options.promoteBuffers=false] Promotes Binary BSON values to native Node Buffers.
* @param {string} [options.appname=null] Application name, passed in on ismaster call and logged in mongod server logs. Maximum size 128 bytes.
* @param {boolean} [options.domainsEnabled=false] Enable the wrapping of the callback in the current domain, disabled by default to avoid perf hit.
* @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
* @property {string} type the topology type.
* @property {string} parserType the parser type used (c++ or js).
*/
var Server = function(options) {
options = options || {};
// Add event listener
EventEmitter.call(this);
// Server instance id
this.id = id++;
// 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([BSON.Binary, BSON.Code, BSON.DBRef, BSON.Decimal128,
BSON.Double, BSON.Int32, BSON.Long, BSON.Map, BSON.MaxKey, BSON.MinKey,
BSON.ObjectId, BSON.BSONRegExp, BSON.Symbol, BSON.Timestamp]),
// 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';
// Set the client info
this.clientInfo = createClientInfo(options);
// Max Stalleness values
// last time we updated the ismaster state
this.lastUpdateTime = 0;
// Last write time
this.lastWriteDate = 0;
// Stalleness
this.staleness = 0;
}
inherits(Server, EventEmitter);
Object.defineProperty(Server.prototype, 'type', {
enumerable:true, get: function() { return this._type; }
});
Object.defineProperty(Server.prototype, 'parserType', {
enumerable:true, get: function() {
return BSON.native ? "c++" : "js";
}
});
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.options.reconnect && 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, {
socketTimeout: (typeof self.s.options.connectionTimeout !== 'number') ? 2000 : self.s.options.connectionTimeout,
monitoring: true,
}, 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;
}
// 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, client: self.clientInfo}, queryOptions);
// Get start time
var start = new Date().getTime();
// Execute the ismaster query
self.s.pool.write(query, {
socketTimeout: self.s.options.connectionTimeout || 2000,
}, 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;
// It's a proxy change the type so
// the wireprotocol will send $readPreference
if(self.ismaster.msg == 'isdbgrid') {
self._type = 'mongos';
}
// 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: sdam.getTopologyType(self)
});
if(!self.s.inTopology) {
// Emit topology description changed if something listening
sdam.emitTopologyDescriptionChanged(self, {
topologyType: 'Single', servers: [{address: self.name, arbiters: [], hosts: [], passives: [], type: sdam.getTopologyType(self)}]
});
}
// 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];
}
if (event === 'close') {
// Closing emits a server description changed event going to unknown.
sdam.emitServerDescriptionChanged(self, {
address: self.name, arbiters: [], hosts: [], passives: [], type: 'Unknown'
});
}
// 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 [%s]', self.name, err)));
}
// Reconnect event, emit the server
if(event == 'reconnect') {
// Reconnecting emits a server description changed event going from unknown to the
// current server type.
sdam.emitServerDescriptionChanged(self, {
address: self.name, arbiters: [], hosts: [], passives: [], type: sdam.getTopologyType(self)
});
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.pool.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) {
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.checkKeys=false] Specify if the bson parser should validate keys.
* @param {Boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
* @param {Boolean} [options.fullResult=false] Return the full envelope instead of just the result document.
* @param {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);
// Clone the options
options = assign({}, options, { wireProtocolCommand: false });
// 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;
// Check if we have collation support
if(this.ismaster && this.ismaster.maxWireVersion < 5 && cmd.collation) {
return callback(new MongoError(f('server %s does not support collation', this.name)));
}
// 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
};
// Are we executing against a specific topology
var topology = options.topology || {};
// Create the query object
var query = self.wireProtocolHandler.command(self.s.bson, ns, cmd, {}, topology, options);
// 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,
promoteValues: typeof options.promoteValues == 'boolean' ? options.promoteValues : true,
promoteBuffers: typeof options.promoteBuffers == 'boolean' ? options.promoteBuffers : false,
command: true,
monitoring: typeof options.monitoring == 'boolean' ? options.monitoring : false,
fullResult: typeof options.fullResult == 'boolean' ? options.fullResult : false,
requestId: query.requestId,
socketTimeout: typeof options.socketTimeout == 'number' ? options.socketTimeout : null,
};
// Write the operation to the pool
self.s.pool.write(query, writeOptions, callback);
}
/**
* Insert one or more documents
* @method
* @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1)
* @param {array} ops An array of documents to insert
* @param {boolean} [options.ordered=true] Execute in order or out of order
* @param {object} [options.writeConcern={}] Write concern for the operation
* @param {Boolean} [options.serializeFunctions=false] Specify if functions on an object should be serialized.
* @param {Boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
* @param {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;
// Check if we have collation support
if(this.ismaster && this.ismaster.maxWireVersion < 5 && options.collation) {
return callback(new MongoError(f('server %s does not support collation', this.name)));
}
// 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;
// Check if we have collation support
if(this.ismaster && this.ismaster.maxWireVersion < 5 && options.collation) {
return callback(new MongoError(f('server %s does not support collation', this.name)));
}
// 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
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.toLowerCase() == server.toLowerCase();
if(server.name) return this.name.toLowerCase() == server.name.toLowerCase();
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
* @param {boolean} [options.force=false] Force destroy the pool
*/
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);
}
// No pool, return
if(!self.s.pool) return;
// 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(options.force);
}
/**
* 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;
+334
View File
@@ -0,0 +1,334 @@
"use strict"
var os = require('os'),
f = require('util').format;
/**
* Emit event if it exists
* @method
*/
function emitSDAMEvent(self, event, description) {
if(self.listeners(event).length > 0) {
self.emit(event, description);
}
}
// Get package.json variable
var driverVersion = require('../../package.json').version;
var nodejsversion = f('Node.js %s, %s', process.version, os.endianness());
var type = os.type();
var name = process.platform;
var architecture = process.arch;
var release = os.release();
function createClientInfo(options) {
// Build default client information
var clientInfo = options.clientInfo ? clone(options.clientInfo) : {
driver: {
name: "nodejs-core",
version: driverVersion
},
os: {
type: type,
name: name,
architecture: architecture,
version: release
}
}
// Is platform specified
if(clientInfo.platform && clientInfo.platform.indexOf('mongodb-core') == -1) {
clientInfo.platform = f('%s, mongodb-core: %s', clientInfo.platform, driverVersion);
} else if(!clientInfo.platform){
clientInfo.platform = nodejsversion;
}
// Do we have an application specific string
if(options.appname) {
// Cut at 128 bytes
var buffer = new Buffer(options.appname);
// Return the truncated appname
var appname = buffer.length > 128 ? buffer.slice(0, 128).toString('utf8') : options.appname;
// Add to the clientInfo
clientInfo.application = { name: appname };
}
return clientInfo;
}
function clone(object) {
return JSON.parse(JSON.stringify(object));
}
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.msg == 'isdbgrid') return 'Mongos';
if(ismaster.ismaster && !ismaster.hosts) return 'Standalone';
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, 'serverHeartbeatFailed', { 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);
});
};
}
//
// Clone the options
var cloneOptions = function(options) {
var opts = {};
for(var name in options) {
opts[name] = options[name];
}
return opts;
}
function Interval(fn, time) {
var timer = false;
this.start = function () {
if (!this.isRunning()) {
timer = setInterval(fn, time);
}
return this;
};
this.stop = function () {
clearInterval(timer);
timer = false;
return this;
};
this.isRunning = function () {
return timer !== false;
};
}
function Timeout(fn, time) {
var timer = false;
this.start = function () {
if (!this.isRunning()) {
timer = setTimeout(function() {
fn();
if (timer && timer._called === undefined) {
// The artificial _called is set here for compatibility with node.js 0.10.x/0.12.x versions
timer._called = true;
}
}, time);
}
return this;
};
this.stop = function () {
clearTimeout(timer);
timer = false;
return this;
};
this.isRunning = function () {
if(timer && timer._called) return false;
return timer !== false;
};
}
function diff(previous, current) {
// Difference document
var diff = {
servers: []
}
// Previous entry
if(!previous) {
previous = { servers: [] };
}
// Check if we have any previous servers missing in the current ones
for(var i = 0; i < previous.servers.length; i++) {
var found = false;
for(var j = 0; j < current.servers.length; j++) {
if(current.servers[j].address.toLowerCase()
=== previous.servers[i].address.toLowerCase()) {
found = true;
break;
}
}
if(!found) {
// Add to the diff
diff.servers.push({
address: previous.servers[i].address,
from: previous.servers[i].type,
to: 'Unknown',
});
}
}
// Check if there are any severs that don't exist
for(var j = 0; j < current.servers.length; j++) {
var found = false;
// Go over all the previous servers
for(var i = 0; i < previous.servers.length; i++) {
if(previous.servers[i].address.toLowerCase()
=== current.servers[j].address.toLowerCase()) {
found = true;
break;
}
}
// Add the server to the diff
if(!found) {
diff.servers.push({
address: current.servers[j].address,
from: 'Unknown',
to: current.servers[j].type,
});
}
}
// 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.toLowerCase() === currServer.address.toLowerCase()) {
// 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.inquireServerState = inquireServerState
module.exports.getTopologyType = getTopologyType;
module.exports.emitServerDescriptionChanged = emitServerDescriptionChanged;
module.exports.emitTopologyDescriptionChanged = emitTopologyDescriptionChanged;
module.exports.cloneOptions = cloneOptions;
module.exports.createClientInfo = createClientInfo;
module.exports.clone = clone;
module.exports.diff = diff;
module.exports.Interval = Interval;
module.exports.Timeout = Timeout;
+32
View File
@@ -0,0 +1,32 @@
/**
* Copy the values of all enumerable own properties from one or more
* source objects to a target object. It will return the target object.
*/
var assign = Object.assign ? Object.assign : function assign(target) {
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;
}
module.exports = {
assign: assign
};
+570
View File
@@ -0,0 +1,570 @@
"use strict";
var Insert = require('./commands').Insert
, Update = require('./commands').Update
, Remove = require('./commands').Remove
, copy = require('../connection/utils').copy
, retrieveBSON = require('../connection/utils').retrieveBSON
, KillCursor = require('../connection/commands').KillCursor
, GetMore = require('../connection/commands').GetMore
, Query = require('../connection/commands').Query
, f = require('util').format
, CommandResult = require('../connection/command_result')
, MongoError = require('../error')
, getReadPreference = require('./shared').getReadPreference;
var BSON = retrieveBSON(),
Long = BSON.Long;
// 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;
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, {
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);
}
// Contains any query options
var queryOptions = {};
// If we have a raw query decorate the function
if(raw) {
queryOptions.raw = raw;
}
// Check if we need to promote longs
if(typeof cursorState.promoteLongs == 'boolean') {
queryOptions.promoteLongs = cursorState.promoteLongs;
}
if(typeof cursorState.promoteValues == 'boolean') {
queryOptions.promoteValues = cursorState.promoteValues;
}
if(typeof cursorState.promoteBuffers == 'boolean') {
queryOptions.promoteBuffers = cursorState.promoteBuffers;
}
// Write out the getMore command
connection.write(getMore, queryOptions, 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) {
return;
} 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 }
if(opType == 'update') {
finalResult.nModified = 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});
} else if(result.updatedExisting == true) {
finalResult.nModified += result.n;
}
// 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];
// 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);
// 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) {
// We have a serialization error, rewrite as a write error to have same behavior as modern
// write commands
getLastErrors.push({ ok: 1, errmsg: typeof err == 'string' ? err : 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];
// 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);
// 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) {
// 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: typeof err == 'string' ? err : 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;
+334
View File
@@ -0,0 +1,334 @@
"use strict";
var copy = require('../connection/utils').copy
, retrieveBSON = require('../connection/utils').retrieveBSON
, KillCursor = require('../connection/commands').KillCursor
, GetMore = require('../connection/commands').GetMore
, Query = require('../connection/commands').Query
, f = require('util').format
, MongoError = require('../error')
, getReadPreference = require('./shared').getReadPreference;
var BSON = retrieveBSON(),
Long = BSON.Long;
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;
if(typeof options.checkKeys == 'boolean') queryOptions.checkKeys = options.checkKeys;
// 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, 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, {
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);
}
// Contains any query options
var queryOptions = {};
// If we have a raw query decorate the function
if(raw) {
queryOptions.raw = raw;
}
// Check if we need to promote longs
if(typeof cursorState.promoteLongs == 'boolean') {
queryOptions.promoteLongs = cursorState.promoteLongs;
}
if(typeof cursorState.promoteValues == 'boolean') {
queryOptions.promoteValues = cursorState.promoteValues;
}
if(typeof cursorState.promoteBuffers == 'boolean') {
queryOptions.promoteBuffers = cursorState.promoteBuffers;
}
// Write out the getMore command
connection.write(getMore, queryOptions, 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) {
return;
} 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;
+549
View File
@@ -0,0 +1,549 @@
"use strict";
var Query = require('../connection/commands').Query
, retrieveBSON = require('../connection/utils').retrieveBSON
, f = require('util').format
, MongoError = require('../error')
, getReadPreference = require('./shared').getReadPreference;
var BSON = retrieveBSON(),
Long = BSON.Long;
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;
}
// If we have collation passed in
if(options.collation) {
for(var i = 0; i < writeCommand[opsField].length; i++) {
if(!writeCommand[opsField][i].collation) {
writeCommand[opsField][i].collation = options.collation;
}
}
}
// 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;
if(typeof options.checkKeys == 'boolean') queryOptions.checkKeys = options.checkKeys;
// 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, 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', 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, {
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());
// 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;
}
if(typeof cursorState.promoteValues == 'boolean') {
queryOptions.promoteValues = cursorState.promoteValues;
}
if(typeof cursorState.promoteBuffers == 'boolean') {
queryOptions.promoteBuffers = cursorState.promoteBuffers;
}
// Write out the getMore command
connection.write(query, queryOptions, queryCallback);
}
WireProtocol.prototype.command = function(bson, ns, cmd, cursorState, topology, options) {
options = options || {}
// Check if this is a wire protocol command or not
var wireProtocolCommand = typeof options.wireProtocolCommand == 'boolean' ? options.wireProtocolCommand : true;
// Establish type of command
if(cmd.find && wireProtocolCommand) {
// 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) {
return;
} 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++) {
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;
// Check if we wish to have a singleBatch
if(cmd.limit < 0) {
findCmd.limit = Math.abs(cmd.limit);
findCmd.singleBatch = true;
}
// Add a batchSize
if(typeof cmd.batchSize == 'number') {
if (cmd.batchSize < 0) {
if (cmd.limit != 0 && Math.abs(cmd.batchSize) < Math.abs(cmd.limit)) {
findCmd.limit = Math.abs(cmd.batchSize);
}
findCmd.singleBatch = true;
}
findCmd.batchSize = Math.abs(cmd.batchSize);
}
// 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 collation passed in
if(cmd.collation) findCmd.collation = cmd.collation;
// 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;
+357
View File
@@ -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 collection 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], {
checkKeys: this.checkKeys,
serializeFunctions: this.serializeFunctions,
ignoreUndefined: 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, {
checkKeys: this.checkKeys,
serializeFunctions: this.serializeFunctions,
ignoreUndefined: this.ignoreUndefined,
});
buffers.push(selector);
totalLength = totalLength + selector.length;
// Serialize the update
var update = this.bson.serialize(this.u, {
checkKeys: this.checkKeys,
serializeFunctions: this.serializeFunctions,
ignoreUndefined: 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, {
checkKeys: this.checkKeys,
serializeFunctions: this.serializeFunctions,
ignoreUndefined: 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
}
+27
View File
@@ -0,0 +1,27 @@
"use strict"
var ReadPreference = require('../topologies/read_preference'),
MongoError = require('../error');
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
}