This commit is contained in:
2019-01-25 01:39:48 +01:00
commit 41ac3fb32a
3269 changed files with 396937 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
'use strict';
const MongoCR = require('./mongocr');
const X509 = require('./x509');
const Plain = require('./plain');
const GSSAPI = require('./gssapi');
const SSPI = require('./sspi');
const ScramSHA1 = require('./scram').ScramSHA1;
const ScramSHA256 = require('./scram').ScramSHA256;
/**
* Returns the default authentication providers.
*
* @param {BSON} bson Bson definition
* @returns {Object} a mapping of auth names to auth types
*/
function defaultAuthProviders(bson) {
return {
mongocr: new MongoCR(bson),
x509: new X509(bson),
plain: new Plain(bson),
gssapi: new GSSAPI(bson),
sspi: new SSPI(bson),
'scram-sha-1': new ScramSHA1(bson),
'scram-sha-256': new ScramSHA256(bson)
};
}
module.exports = { defaultAuthProviders };

381
node_modules/mongodb-core/lib/auth/gssapi.js generated vendored Normal file
View File

@@ -0,0 +1,381 @@
'use strict';
const f = require('util').format;
const Query = require('../connection/commands').Query;
const MongoError = require('../error').MongoError;
const retrieveKerberos = require('../utils').retrieveKerberos;
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
);
};
/**
* 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;
let kerberos;
try {
kerberos = retrieveKerberos();
} catch (e) {
return callback(e, null);
}
// TODO: remove this once we fix URI parsing
var gssapiServiceName = options['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,
kerberos.processes.MongoAuthProcess,
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,
MongoAuthProcess,
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;

214
node_modules/mongodb-core/lib/auth/mongocr.js generated vendored Normal file
View File

@@ -0,0 +1,214 @@
'use strict';
var f = require('util').format,
crypto = require('crypto'),
Query = require('../connection/commands').Query,
MongoError = require('../error').MongoError;
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;

183
node_modules/mongodb-core/lib/auth/plain.js generated vendored Normal file
View File

@@ -0,0 +1,183 @@
'use strict';
var f = require('util').format,
retrieveBSON = require('../connection/utils').retrieveBSON,
Query = require('../connection/commands').Query,
MongoError = require('../error').MongoError;
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;

442
node_modules/mongodb-core/lib/auth/scram.js generated vendored Normal file
View File

@@ -0,0 +1,442 @@
'use strict';
var f = require('util').format,
crypto = require('crypto'),
retrieveBSON = require('../connection/utils').retrieveBSON,
Query = require('../connection/commands').Query,
MongoError = require('../error').MongoError,
Buffer = require('safe-buffer').Buffer;
let saslprep;
try {
saslprep = require('saslprep');
} catch (e) {
// don't do anything;
}
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 ScramSHA authentication mechanism
* @class
* @return {ScramSHA} A cursor instance
*/
var ScramSHA = function(bson, cryptoMethod) {
this.bson = bson;
this.authStore = [];
this.id = id++;
this.cryptoMethod = cryptoMethod || 'sha1';
};
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
function xor(a, b) {
if (!Buffer.isBuffer(a)) a = Buffer.from(a);
if (!Buffer.isBuffer(b)) b = Buffer.from(b);
const length = Math.max(a.length, b.length);
const res = [];
for (let i = 0; i < length; i += 1) {
res.push(a[i] ^ b[i]);
}
return Buffer.from(res).toString('base64');
}
function H(method, text) {
return crypto
.createHash(method)
.update(text)
.digest();
}
function HMAC(method, key, text) {
return crypto
.createHmac(method, key)
.update(text)
.digest();
}
var _hiCache = {};
var _hiCacheCount = 0;
var _hiCachePurge = function() {
_hiCache = {};
_hiCacheCount = 0;
};
const hiLengthMap = {
sha256: 32,
sha1: 20
};
function HI(data, salt, iterations, cryptoMethod) {
// omit the work if already generated
const key = [data, salt.toString('base64'), iterations].join('_');
if (_hiCache[key] !== undefined) {
return _hiCache[key];
}
// generate the salt
const saltedData = crypto.pbkdf2Sync(
data,
salt,
iterations,
hiLengthMap[cryptoMethod],
cryptoMethod
);
// 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}
*/
ScramSHA.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;
const cryptoMethod = this.cryptoMethod;
let mechanism = 'SCRAM-SHA-1';
let processedPassword;
if (cryptoMethod === 'sha256') {
mechanism = 'SCRAM-SHA-256';
let saslprepFn = (server.s && server.s.saslprep) || saslprep;
if (saslprepFn) {
processedPassword = saslprepFn(password);
} else {
console.warn('Warning: no saslprep library specified. Passwords will not be sanitized');
processedPassword = password;
}
} else {
processedPassword = passwordDigest(username, password);
}
// 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'
// NOTE: This is done b/c Javascript uses UTF-16, but the server is hashing in UTF-8.
// Since the username is not sasl-prep-d, we need to do this here.
const firstBare = Buffer.concat([
Buffer.from('n=', 'utf8'),
Buffer.from(username, 'utf8'),
Buffer.from(',r=', 'utf8'),
Buffer.from(nonce, 'utf8')
]);
// Build command structure
var cmd = {
saslStart: 1,
mechanism: mechanism,
payload: new Binary(Buffer.concat([Buffer.from('n,,', 'utf8'), 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 saltedPassword = HI(
processedPassword,
Buffer.from(salt, 'base64'),
iterations,
cryptoMethod
);
if (iterations && iterations < 4096) {
const error = new MongoError(`Server returned an invalid iteration count ${iterations}`);
return callback(error, false);
}
// Create the client key
const clientKey = HMAC(cryptoMethod, saltedPassword, 'Client Key');
// Create the stored key
const storedKey = H(cryptoMethod, clientKey);
// Create the authentication message
const authMessage = [
firstBare,
r.result.payload.value().toString('base64'),
withoutProof
].join(',');
// Create client signature
const clientSignature = HMAC(cryptoMethod, storedKey, authMessage);
// Create client proof
const clientProof = f('p=%s', xor(clientKey, clientSignature));
// Create client final
const clientFinal = [withoutProof, clientProof].join(',');
// Create continue message
const cmd = {
saslContinue: 1,
conversationId: r.result.conversationId,
payload: new Binary(Buffer.from(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: Buffer.alloc(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}
*/
ScramSHA.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}
*/
ScramSHA.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);
}
}
);
}
};
class ScramSHA1 extends ScramSHA {
constructor(bson) {
super(bson, 'sha1');
}
}
class ScramSHA256 extends ScramSHA {
constructor(bson) {
super(bson, 'sha256');
}
}
module.exports = { ScramSHA1, ScramSHA256 };

262
node_modules/mongodb-core/lib/auth/sspi.js generated vendored Normal file
View File

@@ -0,0 +1,262 @@
'use strict';
const f = require('util').format;
const Query = require('../connection/commands').Query;
const MongoError = require('../error').MongoError;
const retrieveKerberos = require('../utils').retrieveKerberos;
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
);
};
/**
* 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;
let kerberos;
try {
kerberos = retrieveKerberos();
} catch (e) {
return callback(e, null);
}
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,
kerberos.processes.MongoAuthProcess,
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());
}
};
function SSIPAuthenticate(
self,
MongoAuthProcess,
username,
password,
gssapiServiceName,
server,
connection,
options,
callback
) {
const authProcess = new MongoAuthProcess(
connection.host,
connection.port,
gssapiServiceName,
options
);
function authCommand(command, authCb) {
const query = new Query(self.bson, '$external.$cmd', command, {
numberToSkip: 0,
numberToReturn: 1
});
server(connection, query, authCb);
}
authProcess.init(username, password, err => {
if (err) return callback(err, false);
authProcess.transition('', (err, payload) => {
if (err) return callback(err, false);
const command = {
saslStart: 1,
mechanism: 'GSSAPI',
payload,
autoAuthorize: 1
};
authCommand(command, (err, result) => {
if (err) return callback(err, false);
const doc = result.result;
authProcess.transition(doc.payload, (err, payload) => {
if (err) return callback(err, false);
const command = {
saslContinue: 1,
conversationId: doc.conversationId,
payload
};
authCommand(command, (err, result) => {
if (err) return callback(err, false);
const doc = result.result;
authProcess.transition(doc.payload, (err, payload) => {
if (err) return callback(err, false);
const command = {
saslContinue: 1,
conversationId: doc.conversationId,
payload
};
authCommand(command, (err, response) => {
if (err) return callback(err, false);
authProcess.transition(null, err => {
if (err) return callback(err, null);
callback(null, response);
});
});
});
});
});
});
});
});
}
// 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;

179
node_modules/mongodb-core/lib/auth/x509.js generated vendored Normal file
View File

@@ -0,0 +1,179 @@
'use strict';
var f = require('util').format,
Query = require('../connection/commands').Query,
MongoError = require('../error').MongoError;
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;

228
node_modules/mongodb-core/lib/connection/apm.js generated vendored Normal file
View File

@@ -0,0 +1,228 @@
'use strict';
const KillCursor = require('../connection/commands').KillCursor;
const GetMore = require('../connection/commands').GetMore;
const calculateDurationInMs = require('../utils').calculateDurationInMs;
/** Commands that we want to redact because of the sensitive nature of their contents */
const SENSITIVE_COMMANDS = new Set([
'authenticate',
'saslStart',
'saslContinue',
'getnonce',
'createUser',
'updateUser',
'copydbgetnonce',
'copydbsaslstart',
'copydb'
]);
// helper methods
const extractCommandName = command => Object.keys(command)[0];
const namespace = command => command.ns;
const databaseName = command => command.ns.split('.')[0];
const collectionName = command => command.ns.split('.')[1];
const generateConnectionId = pool => `${pool.options.host}:${pool.options.port}`;
const maybeRedact = (commandName, result) => (SENSITIVE_COMMANDS.has(commandName) ? {} : result);
const LEGACY_FIND_QUERY_MAP = {
$query: 'filter',
$orderby: 'sort',
$hint: 'hint',
$comment: 'comment',
$maxScan: 'maxScan',
$max: 'max',
$min: 'min',
$returnKey: 'returnKey',
$showDiskLoc: 'showRecordId',
$maxTimeMS: 'maxTimeMS',
$snapshot: 'snapshot'
};
const LEGACY_FIND_OPTIONS_MAP = {
numberToSkip: 'skip',
numberToReturn: 'batchSize',
returnFieldsSelector: 'projection'
};
const OP_QUERY_KEYS = [
'tailable',
'oplogReplay',
'noCursorTimeout',
'awaitData',
'partial',
'exhaust'
];
/**
* Extract the actual command from the query, possibly upconverting if it's a legacy
* format
*
* @param {Object} command the command
*/
const extractCommand = command => {
if (command instanceof GetMore) {
return {
getMore: command.cursorId,
collection: collectionName(command),
batchSize: command.numberToReturn
};
}
if (command instanceof KillCursor) {
return {
killCursors: collectionName(command),
cursors: command.cursorIds
};
}
if (command.query && command.query.$query) {
let result;
if (command.ns === 'admin.$cmd') {
// upconvert legacy command
result = Object.assign({}, command.query.$query);
} else {
// upconvert legacy find command
result = { find: collectionName(command) };
Object.keys(LEGACY_FIND_QUERY_MAP).forEach(key => {
if (typeof command.query[key] !== 'undefined')
result[LEGACY_FIND_QUERY_MAP[key]] = command.query[key];
});
}
Object.keys(LEGACY_FIND_OPTIONS_MAP).forEach(key => {
if (typeof command[key] !== 'undefined') result[LEGACY_FIND_OPTIONS_MAP[key]] = command[key];
});
OP_QUERY_KEYS.forEach(key => {
if (command[key]) result[key] = command[key];
});
if (typeof command.pre32Limit !== 'undefined') {
result.limit = command.pre32Limit;
}
if (command.query.$explain) {
return { explain: result };
}
return result;
}
return command.query ? command.query : command;
};
const extractReply = (command, reply) => {
if (command instanceof GetMore) {
return {
ok: 1,
cursor: {
id: reply.message.cursorId,
ns: namespace(command),
nextBatch: reply.message.documents
}
};
}
if (command instanceof KillCursor) {
return {
ok: 1,
cursorsUnknown: command.cursorIds
};
}
// is this a legacy find command?
if (command.query && typeof command.query.$query !== 'undefined') {
return {
ok: 1,
cursor: {
id: reply.message.cursorId,
ns: namespace(command),
firstBatch: reply.message.documents
}
};
}
return reply.result;
};
/** An event indicating the start of a given command */
class CommandStartedEvent {
/**
* Create a started event
*
* @param {Pool} pool the pool that originated the command
* @param {Object} command the command
*/
constructor(pool, command) {
const cmd = extractCommand(command);
const commandName = extractCommandName(cmd);
// NOTE: remove in major revision, this is not spec behavior
if (SENSITIVE_COMMANDS.has(commandName)) {
this.commandObj = {};
this.commandObj[commandName] = true;
}
Object.assign(this, {
command: cmd,
databaseName: databaseName(command),
commandName,
requestId: command.requestId,
connectionId: generateConnectionId(pool)
});
}
}
/** An event indicating the success of a given command */
class CommandSucceededEvent {
/**
* Create a succeeded event
*
* @param {Pool} pool the pool that originated the command
* @param {Object} command the command
* @param {Object} reply the reply for this command from the server
* @param {Array} started a high resolution tuple timestamp of when the command was first sent, to calculate duration
*/
constructor(pool, command, reply, started) {
const cmd = extractCommand(command);
const commandName = extractCommandName(cmd);
Object.assign(this, {
duration: calculateDurationInMs(started),
commandName,
reply: maybeRedact(commandName, extractReply(command, reply)),
requestId: command.requestId,
connectionId: generateConnectionId(pool)
});
}
}
/** An event indicating the failure of a given command */
class CommandFailedEvent {
/**
* Create a failure event
*
* @param {Pool} pool the pool that originated the command
* @param {Object} command the command
* @param {MongoError|Object} error the generated error or a server error response
* @param {Array} started a high resolution tuple timestamp of when the command was first sent, to calculate duration
*/
constructor(pool, command, error, started) {
const cmd = extractCommand(command);
const commandName = extractCommandName(cmd);
Object.assign(this, {
duration: calculateDurationInMs(started),
commandName,
failure: maybeRedact(commandName, error),
requestId: command.requestId,
connectionId: generateConnectionId(pool)
});
}
}
module.exports = {
CommandStartedEvent,
CommandSucceededEvent,
CommandFailedEvent
};

View File

@@ -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
node_modules/mongodb-core/lib/connection/commands.js generated vendored Normal file
View File

@@ -0,0 +1,546 @@
'use strict';
var retrieveBSON = require('./utils').retrieveBSON;
var BSON = retrieveBSON();
var Long = BSON.Long;
const MongoError = require('../error').MongoError;
const Buffer = require('safe-buffer').Buffer;
// Incrementing request id
var _requestId = 0;
// Wire command operation ids
var opcodes = require('../wireprotocol/shared').opcodes;
// 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 = 1;
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') !== -1) {
throw new Error('namespace cannot contain a null character');
}
// Basic options
this.bson = bson;
this.ns = ns;
this.query = query;
// Additional options
this.numberToSkip = options.numberToSkip || 0;
this.numberToReturn = options.numberToReturn || 0;
this.returnFieldSelector = options.returnFieldSelector || null;
this.requestId = Query.getRequestId();
// special case for pre-3.2 find commands, delete ASAP
this.pre32Limit = options.pre32Limit;
// 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 = Buffer.alloc(
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] = (opcodes.OP_QUERY >> 24) & 0xff;
header[index + 2] = (opcodes.OP_QUERY >> 16) & 0xff;
header[index + 1] = (opcodes.OP_QUERY >> 8) & 0xff;
header[index] = opcodes.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 = Buffer.alloc(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] = (opcodes.OP_GETMORE >> 24) & 0xff;
_buffer[index + 2] = (opcodes.OP_GETMORE >> 16) & 0xff;
_buffer[index + 1] = (opcodes.OP_GETMORE >> 8) & 0xff;
_buffer[index] = opcodes.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, ns, cursorIds) {
this.ns = ns;
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 = Buffer.alloc(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] = (opcodes.OP_KILL_CURSORS >> 24) & 0xff;
_buffer[index + 2] = (opcodes.OP_KILL_CURSORS >> 16) & 0xff;
_buffer[index + 1] = (opcodes.OP_KILL_CURSORS >> 8) & 0xff;
_buffer[index] = opcodes.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, message, msgHeader, msgBody, opts) {
opts = opts || { promoteLongs: true, promoteValues: true, promoteBuffers: false };
this.parsed = false;
this.raw = message;
this.data = msgBody;
this.bson = bson;
this.opts = opts;
// Read the message header
this.length = msgHeader.length;
this.requestId = msgHeader.requestId;
this.responseTo = msgHeader.responseTo;
this.opCode = msgHeader.opCode;
this.fromCompressed = msgHeader.fromCompressed;
// Read the message body
this.responseFlags = msgBody.readInt32LE(0);
this.cursorId = new Long(msgBody.readInt32LE(4), msgBody.readInt32LE(8));
this.startingFrom = msgBody.readInt32LE(12);
this.numberReturned = msgBody.readInt32LE(16);
// 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
};
// Position within OP_REPLY at which documents start
// (See https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/#wire-op-reply)
this.index = 20;
//
// 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);
if (doc instanceof Error) {
throw doc;
}
if (doc.errmsg) {
throw new MongoError(doc.errmsg);
}
if (!doc.cursor) {
throw new MongoError('Cursor not found');
}
// 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
};

805
node_modules/mongodb-core/lib/connection/connection.js generated vendored Normal file
View File

@@ -0,0 +1,805 @@
'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,
parseHeader = require('../wireprotocol/shared').parseHeader,
decompress = require('../wireprotocol/compression').decompress,
Response = require('./commands').Response,
MongoNetworkError = require('../error').MongoNetworkError,
Logger = require('./logger'),
OP_COMPRESSED = require('../wireprotocol/shared').opcodes.OP_COMPRESSED,
MESSAGE_HEADER_SIZE = require('../wireprotocol/shared').MESSAGE_HEADER_SIZE,
Buffer = require('safe-buffer').Buffer;
var _id = 0;
var debugFields = [
'host',
'port',
'size',
'keepAlive',
'keepAliveInitialDelay',
'noDelay',
'connectionTimeout',
'socketTimeout',
'singleBufferSerializtion',
'ssl',
'ca',
'crl',
'cert',
'rejectUnauthorized',
'promoteLongs',
'promoteValues',
'promoteBuffers',
'checkServerIdentity'
];
var connectionAccountingSpy = undefined;
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(spy) {
if (spy) {
connectionAccountingSpy = spy;
}
connectionAccounting = true;
connections = {};
};
Connection.disableConnectionAccounting = function() {
connectionAccounting = false;
connectionAccountingSpy = undefined;
};
Connection.connections = function() {
return connections;
};
function deleteConnection(id) {
// console.log("=== deleted connection " + id + " :: " + (connections[id] ? connections[id].port : ''))
delete connections[id];
if (connectionAccountingSpy) {
connectionAccountingSpy.deleteConnection(id);
}
}
function addConnection(id, connection) {
// console.log("=== added connection " + id + " :: " + connection.port)
connections[id] = connection;
if (connectionAccountingSpy) {
connectionAccountingSpy.addConnection(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', new MongoNetworkError(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',
new MongoNetworkError(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',
new MongoNetworkError(f('connection %s to %s:%s closed', self.id, self.host, self.port)),
self
);
}
};
};
// Handle a message once it is received
var emitMessageHandler = function(self, message) {
var msgHeader = parseHeader(message);
if (msgHeader.opCode === OP_COMPRESSED) {
msgHeader.fromCompressed = true;
var index = MESSAGE_HEADER_SIZE;
msgHeader.opCode = message.readInt32LE(index);
index += 4;
msgHeader.length = message.readInt32LE(index);
index += 4;
var compressorID = message[index];
index++;
decompress(compressorID, message.slice(index), function(err, decompressedMsgBody) {
if (err) {
throw err;
}
if (decompressedMsgBody.length !== msgHeader.length) {
throw new Error(
'Decompressing a compressed message from the server failed. The message is corrupt.'
);
}
self.messageHandler(
new Response(self.bson, message, msgHeader, decompressedMsgBody, self.responseOptions),
self
);
});
} else {
self.messageHandler(
new Response(
self.bson,
message,
msgHeader,
message.slice(MESSAGE_HEADER_SIZE),
self.responseOptions
),
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 = Buffer.alloc(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;
emitMessageHandler(self, emitBuffer);
} 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 = Buffer.alloc(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 = Buffer.alloc(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 = Buffer.alloc(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 = Buffer.alloc(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 = Buffer.alloc(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 = Buffer.alloc(0);
// Emit the message
emitMessageHandler(self, emitBuffer);
} 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 = Buffer.alloc(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
emitMessageHandler(self, emitBuffer);
}
} else {
// Create a buffer that contains the space for the non-complete message
self.stubBuffer = Buffer.alloc(data.length);
// Copy the data to the stub buffer
data.copy(self.stubBuffer, 0);
// Exit parsing loop
data = Buffer.alloc(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];
}
}
}
function makeSSLConnection(self, _options) {
let sslOptions = {
socket: self.connection,
rejectUnauthorized: self.rejectUnauthorized
};
// Merge in options
merge(sslOptions, self.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
const connection = tls.connect(self.port, self.host, sslOptions, function() {
// Error on auth or skip
if (connection.authorizationError && self.rejectUnauthorized) {
return self.emit('error', connection.authorizationError, self, { ssl: true });
}
// Set socket timeout instead of connection timeout
connection.setTimeout(self.socketTimeout);
// We are done emit connect
self.emit('connect', self);
});
// Set the options for the connection
connection.setKeepAlive(self.keepAlive, self.keepAliveInitialDelay);
connection.setTimeout(self.connectionTimeout);
connection.setNoDelay(self.noDelay);
return connection;
}
function makeUnsecureConnection(self, family) {
// Create new connection instance
let connection_options;
if (self.domainSocket) {
connection_options = { path: self.host };
} else {
connection_options = { port: self.port, host: self.host };
connection_options.family = family;
}
const connection = net.createConnection(connection_options);
// Set the options for the connection
connection.setKeepAlive(self.keepAlive, self.keepAliveInitialDelay);
connection.setTimeout(self.connectionTimeout);
connection.setNoDelay(self.noDelay);
connection.once('connect', function() {
// Set socket timeout instead of connection timeout
connection.setTimeout(self.socketTimeout);
// Emit connect event
self.emit('connect', self);
});
return connection;
}
function doConnect(self, family, _options, _errorHandler) {
self.connection = self.ssl
? makeSSLConnection(self, _options)
: makeUnsecureConnection(self, family);
// Add handlers for events
self.connection.once('error', _errorHandler);
self.connection.once('timeout', timeoutHandler(self));
self.connection.once('close', closeHandler(self));
self.connection.on('data', dataHandler(self));
}
/**
* Connect
* @method
*/
Connection.prototype.connect = function(_options) {
_options = _options || {};
// Set the connections
if (connectionAccounting) addConnection(this.id, this);
// Check if we are overriding the promoteLongs
if (typeof _options.promoteLongs === 'boolean') {
this.responseOptions.promoteLongs = _options.promoteLongs;
this.responseOptions.promoteValues = _options.promoteValues;
this.responseOptions.promoteBuffers = _options.promoteBuffers;
}
const _errorHandler = errorHandler(this);
if (this.family !== void 0) {
return doConnect(this, this.family, _options, _errorHandler);
}
return doConnect(this, 6, _options, err => {
if (this.logger.isDebug()) {
this.logger.debug(
f(
'connection %s for [%s:%s] errored out with [%s]',
this.id,
this.host,
this.port,
JSON.stringify(err)
)
);
}
// clean up existing event handlers
this.connection.removeAllListeners('error');
this.connection.removeAllListeners('timeout');
this.connection.removeAllListeners('close');
this.connection.removeAllListeners('data');
this.connection = undefined;
return doConnect(this, 4, _options, _errorHandler);
});
};
/**
* 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) {} // eslint-disable-line
// 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;

246
node_modules/mongodb-core/lib/connection/logger.js generated vendored Normal file
View File

@@ -0,0 +1,246 @@
'use strict';
var f = require('util').format,
MongoError = require('../error').MongoError;
// 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;

1657
node_modules/mongodb-core/lib/connection/pool.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

113
node_modules/mongodb-core/lib/connection/utils.js generated vendored Normal file
View File

@@ -0,0 +1,113 @@
'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) {} // eslint-disable-line
return BSON;
};
// Throw an error if an attempt to use Snappy is made when Snappy is not installed
var noSnappyWarning = function() {
throw new Error(
'Attempted to use Snappy compression, but Snappy is not installed. Install or disable Snappy compression and try again.'
);
};
// Facilitate loading Snappy optionally
var retrieveSnappy = function() {
var snappy = null;
try {
snappy = require_optional('snappy');
} catch (error) {} // eslint-disable-line
if (!snappy) {
snappy = {
compress: noSnappyWarning,
uncompress: noSnappyWarning,
compressSync: noSnappyWarning,
uncompressSync: noSnappyWarning
};
}
return snappy;
};
exports.setProperty = setProperty;
exports.getProperty = getProperty;
exports.getSingleProperty = getSingleProperty;
exports.copy = copy;
exports.debugOptions = debugOptions;
exports.retrieveBSON = retrieveBSON;
exports.retrieveSnappy = retrieveSnappy;

766
node_modules/mongodb-core/lib/cursor.js generated vendored Normal file
View File

@@ -0,0 +1,766 @@
'use strict';
const Logger = require('./connection/logger');
const retrieveBSON = require('./connection/utils').retrieveBSON;
const MongoError = require('./error').MongoError;
const MongoNetworkError = require('./error').MongoNetworkError;
const mongoErrorContextSymbol = require('./error').mongoErrorContextSymbol;
const f = require('util').format;
const collationNotSupported = require('./utils').collationNotSupported;
const BSON = retrieveBSON();
const 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,
raw: options.raw || (cmd && cmd.raw)
};
if (typeof options.session === 'object') {
this.cursorState.session = options.session;
}
// 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;
}
if (topologyOptions.reconnect) {
this.cursorState.reconnect = topologyOptions.reconnect;
}
// 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;
};
Cursor.prototype._endSession = function(options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
options = options || {};
const session = this.cursorState.session;
if (session && (options.force || session.owner === this)) {
this.cursorState.session = undefined;
session.endSession(callback);
return true;
}
if (callback) {
callback();
}
return false;
};
//
// 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._getmore = function(callback) {
if (this.logger.isDebug())
this.logger.debug(f('schedule getMore call for query [%s]', JSON.stringify(this.query)));
// 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;
}
this.server.wireProtocolHandler.getMore(
this.server,
this.ns,
this.cursorState,
batchSize,
this.options,
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) {
// 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;
}
this.server.wireProtocolHandler.killCursor(this.server, this.ns, this.cursorState, 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.killed = true;
const err = new MongoNetworkError(
f('connection to host %s:%s was destroyed', self.pool.host, self.pool.port)
);
_setCursorNotifiedImpl(self, () => callback(err));
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.killed = true;
setCursorNotified(self, callback);
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, new MongoError('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) {
setCursorNotified(self, callback);
return true;
}
return false;
};
/**
* Mark cursor as being dead and notified
*/
var setCursorDeadAndNotified = function(self, callback) {
self.cursorState.dead = true;
setCursorNotified(self, callback);
};
/**
* Mark cursor as being notified
*/
var setCursorNotified = function(self, callback) {
_setCursorNotifiedImpl(self, () => handleCallback(callback, null, null));
};
var _setCursorNotifiedImpl = function(self, callback) {
self.cursorState.notified = true;
self.cursorState.documents = [];
self.cursorState.cursorIndex = 0;
if (self._endSession) {
return self._endSession(undefined, () => callback());
}
return callback();
};
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) {
return initializeCursor(self, callback);
}
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 MongoNetworkError('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) {
if (err instanceof MongoError) {
err[mongoErrorContextSymbol].isGetMore = true;
}
return handleCallback(callback, err);
}
if (self.cursorState.cursorId && self.cursorState.cursorId.isZero() && self._endSession) {
self._endSession();
}
// 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,
new MongoError({
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,
new MongoError({
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);
}
};
function initializeCursor(cursor, 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 (!cursor.topology.isConnected(cursor.options)) {
// Only need this for single server, because repl sets and mongos
// will always continue trying to reconnect
if (cursor.topology._type === 'server' && !cursor.topology.s.options.reconnect) {
// Reconnect is disabled, so we'll never reconnect
return callback(new MongoError('no connection available'));
}
if (cursor.disconnectHandler != null) {
if (cursor.topology.isDestroyed()) {
// Topology was destroyed, so don't try to wait for it to reconnect
return callback(new MongoError('Topology was destroyed'));
}
return cursor.disconnectHandler.addObjectAndMethod(
'cursor',
cursor,
'next',
[callback],
callback
);
}
}
return cursor.topology.selectServer(cursor.options, (err, server) => {
if (err) {
const disconnectHandler = cursor.disconnectHandler;
if (disconnectHandler != null) {
return disconnectHandler.addObjectAndMethod('cursor', cursor, 'next', [callback], callback);
}
return callback(err);
}
cursor.server = server;
cursor.cursorState.init = true;
if (collationNotSupported(cursor.server, cursor.cmd)) {
return callback(new MongoError(`server ${cursor.server.name} does not support collation`));
}
function done() {
if (
cursor.cursorState.cursorId &&
cursor.cursorState.cursorId.isZero() &&
cursor._endSession
) {
cursor._endSession();
}
if (
cursor.cursorState.documents.length === 0 &&
cursor.cursorState.cursorId &&
cursor.cursorState.cursorId.isZero() &&
!cursor.cmd.tailable &&
!cursor.cmd.awaitData
) {
return setCursorNotified(cursor, callback);
}
nextFunction(cursor, callback);
}
// NOTE: this is a special internal method for cloning a cursor, consider removing
if (cursor.cursorState.cursorId != null) {
return done();
}
const queryCallback = (err, r) => {
if (err) return callback(err);
const result = r.message;
if (result.queryFailure) {
return callback(new MongoError(result.documents[0]), null);
}
// Check if we have a command cursor
if (
Array.isArray(result.documents) &&
result.documents.length === 1 &&
(!cursor.cmd.find || (cursor.cmd.find && cursor.cmd.virtual === false)) &&
(typeof result.documents[0].cursor !== 'string' ||
result.documents[0]['$err'] ||
result.documents[0]['errmsg'] ||
Array.isArray(result.documents[0].result))
) {
// We have an error document, return the error
if (result.documents[0]['$err'] || result.documents[0]['errmsg']) {
return callback(new MongoError(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) {
cursor.ns = result.documents[0].cursor.ns;
}
// Promote id to long if needed
cursor.cursorState.cursorId = typeof id === 'number' ? Long.fromNumber(id) : id;
cursor.cursorState.lastCursorId = cursor.cursorState.cursorId;
cursor.cursorState.operationTime = result.documents[0].operationTime;
// If we have a firstBatch set it
if (Array.isArray(result.documents[0].cursor.firstBatch)) {
cursor.cursorState.documents = result.documents[0].cursor.firstBatch; //.reverse();
}
// Return after processing command cursor
return done(result);
}
if (Array.isArray(result.documents[0].result)) {
cursor.cursorState.documents = result.documents[0].result;
cursor.cursorState.cursorId = Long.ZERO;
return done(result);
}
}
// Otherwise fall back to regular find path
cursor.cursorState.cursorId = result.cursorId;
cursor.cursorState.documents = result.documents;
cursor.cursorState.lastCursorId = result.cursorId;
// Transform the results with passed in transformation method if provided
if (
cursor.cursorState.transforms &&
typeof cursor.cursorState.transforms.query === 'function'
) {
cursor.cursorState.documents = cursor.cursorState.transforms.query(result);
}
// Return callback
done(result);
};
if (cursor.logger.isDebug()) {
cursor.logger.debug(
`issue initial query [${JSON.stringify(cursor.cmd)}] with flags [${JSON.stringify(
cursor.query
)}]`
);
}
if (cursor.cmd.find != null) {
cursor.server.wireProtocolHandler.query(
cursor.server,
cursor.ns,
cursor.cmd,
cursor.cursorState,
cursor.options,
queryCallback
);
return;
}
cursor.query = cursor.server.wireProtocolHandler.command(
cursor.server,
cursor.ns,
cursor.cmd,
cursor.options,
queryCallback
);
});
}
/**
* 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;

146
node_modules/mongodb-core/lib/error.js generated vendored Normal file
View File

@@ -0,0 +1,146 @@
'use strict';
const mongoErrorContextSymbol = Symbol('mongoErrorContextSymbol');
/**
* Creates a new MongoError
*
* @augments Error
* @param {Error|string|object} message The error message
* @property {string} message The error message
* @property {string} stack The error call stack
*/
class MongoError extends Error {
constructor(message) {
if (message instanceof Error) {
super(message.message);
this.stack = message.stack;
} else {
if (typeof message === 'string') {
super(message);
} else {
super(message.message || message.errmsg || message.$err || 'n/a');
for (var name in message) {
this[name] = message[name];
}
}
Error.captureStackTrace(this, this.constructor);
}
this.name = 'MongoError';
this[mongoErrorContextSymbol] = this[mongoErrorContextSymbol] || {};
}
/**
* Creates a new MongoError object
*
* @param {Error|string|object} options The options used to create the error.
* @return {MongoError} A MongoError instance
* @deprecated Use `new MongoError()` instead.
*/
static create(options) {
return new MongoError(options);
}
}
/**
* Creates a new MongoNetworkError
*
* @param {Error|string|object} message The error message
* @property {string} message The error message
* @property {string} stack The error call stack
*/
class MongoNetworkError extends MongoError {
constructor(message) {
super(message);
this.name = 'MongoNetworkError';
// This is added as part of the transactions specification
this.errorLabels = ['TransientTransactionError'];
}
}
/**
* An error used when attempting to parse a value (like a connection string)
*
* @param {Error|string|object} message The error message
* @property {string} message The error message
*/
class MongoParseError extends MongoError {
constructor(message) {
super(message);
this.name = 'MongoParseError';
}
}
/**
* An error signifying a timeout event
*
* @param {Error|string|object} message The error message
* @property {string} message The error message
*/
class MongoTimeoutError extends MongoError {
constructor(message) {
super(message);
this.name = 'MongoTimeoutError';
}
}
/**
* An error thrown when the server reports a writeConcernError
*
* @param {Error|string|object} message The error message
* @param {object} result The result document (provided if ok: 1)
* @property {string} message The error message
* @property {object} [result] The result document (provided if ok: 1)
*/
class MongoWriteConcernError extends MongoError {
constructor(message, result) {
super(message);
this.name = 'MongoWriteConcernError';
if (result != null) {
this.result = result;
}
}
}
// see: https://github.com/mongodb/specifications/blob/master/source/retryable-writes/retryable-writes.rst#terms
const RETRYABLE_ERROR_CODES = new Set([
6, // HostUnreachable
7, // HostNotFound
89, // NetworkTimeout
91, // ShutdownInProgress
189, // PrimarySteppedDown
9001, // SocketException
10107, // NotMaster
11600, // InterruptedAtShutdown
11602, // InterruptedDueToReplStateChange
13435, // NotMasterNoSlaveOk
13436 // NotMasterOrSecondary
]);
/**
* Determines whether an error is something the driver should attempt to retry
*
* @param {MongoError|Error} error
*/
function isRetryableError(error) {
return (
RETRYABLE_ERROR_CODES.has(error.code) ||
error instanceof MongoNetworkError ||
error.message.match(/not master/) ||
error.message.match(/node is recovering/)
);
}
module.exports = {
MongoError,
MongoNetworkError,
MongoParseError,
MongoTimeoutError,
MongoWriteConcernError,
mongoErrorContextSymbol,
isRetryableError
};

749
node_modules/mongodb-core/lib/sdam/cursor.js generated vendored Normal file
View File

@@ -0,0 +1,749 @@
'use strict';
const Logger = require('../connection/logger');
const BSON = require('../connection/utils').retrieveBSON();
const MongoError = require('../error').MongoError;
const MongoNetworkError = require('../error').MongoNetworkError;
const mongoErrorContextSymbol = require('../error').mongoErrorContextSymbol;
const Long = BSON.Long;
const deprecate = require('util').deprecate;
const readPreferenceServerSelector = require('./server_selectors').readPreferenceServerSelector;
const ReadPreference = require('../topologies/read_preference');
/**
* Handle callback (including any exceptions thrown)
*/
function handleCallback(callback, err, result) {
try {
callback(err, result);
} catch (err) {
process.nextTick(function() {
throw err;
});
}
}
/**
* This is a cursor results callback
*
* @callback resultCallback
* @param {error} error An error object. Set to null if no error present
* @param {object} document
*/
/**
* An internal class that embodies a cursor on MongoDB, allowing for iteration over the
* results returned from a query.
*
* @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
*/
class Cursor {
/**
* Create a cursor
*
* @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.
*/
constructor(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.s = {
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
};
if (typeof options.session === 'object') {
this.s.session = options.session;
}
// Add promoteLong to cursor state
if (typeof topologyOptions.promoteLongs === 'boolean') {
this.s.promoteLongs = topologyOptions.promoteLongs;
} else if (typeof options.promoteLongs === 'boolean') {
this.s.promoteLongs = options.promoteLongs;
}
// Add promoteValues to cursor state
if (typeof topologyOptions.promoteValues === 'boolean') {
this.s.promoteValues = topologyOptions.promoteValues;
} else if (typeof options.promoteValues === 'boolean') {
this.s.promoteValues = options.promoteValues;
}
// Add promoteBuffers to cursor state
if (typeof topologyOptions.promoteBuffers === 'boolean') {
this.s.promoteBuffers = topologyOptions.promoteBuffers;
} else if (typeof options.promoteBuffers === 'boolean') {
this.s.promoteBuffers = options.promoteBuffers;
}
if (topologyOptions.reconnect) {
this.s.reconnect = topologyOptions.reconnect;
}
// Logger
this.logger = Logger('Cursor', topologyOptions);
//
// Did we pass in a cursor id
if (typeof cmd === 'number') {
this.s.cursorId = Long.fromNumber(cmd);
this.s.lastCursorId = this.s.cursorId;
} else if (cmd instanceof Long) {
this.s.cursorId = cmd;
this.s.lastCursorId = cmd;
}
}
setCursorBatchSize(value) {
this.s.batchSize = value;
}
cursorBatchSize() {
return this.s.batchSize;
}
setCursorLimit(value) {
this.s.limit = value;
}
cursorLimit() {
return this.s.limit;
}
setCursorSkip(value) {
this.s.skip = value;
}
cursorSkip() {
return this.s.skip;
}
_endSession(options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
options = options || {};
const session = this.s.session;
if (session && (options.force || session.owner === this)) {
this.s.session = undefined;
session.endSession(callback);
return true;
}
if (callback) {
callback();
}
return false;
}
/**
* Clone the cursor
* @method
* @return {Cursor}
*/
clone() {
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
*/
isDead() {
return this.s.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
*/
isKilled() {
return this.s.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
*/
isNotified() {
return this.s.notified === true;
}
/**
* Returns current buffered documents length
* @method
* @return {number} The number of items in the buffered documents
*/
bufferedCount() {
return this.s.documents.length - this.s.cursorIndex;
}
/**
* Kill the cursor
*
* @param {resultCallback} callback A callback function
*/
kill(callback) {
// Set cursor to dead
this.s.dead = true;
this.s.killed = true;
// Remove documents
this.s.documents = [];
// If no cursor id just return
if (this.s.cursorId == null || this.s.cursorId.isZero() || this.s.init === false) {
if (callback) callback(null, null);
return;
}
// Default pool
const pool = this.s.server.s.pool;
// Execute command
this.s.server.s.wireProtocolHandler.killCursor(this.bson, this.ns, this.s, pool, callback);
}
/**
* Resets the cursor
*/
rewind() {
if (this.s.init) {
if (!this.s.dead) {
this.kill();
}
this.s.currentLimit = 0;
this.s.init = false;
this.s.dead = false;
this.s.killed = false;
this.s.notified = false;
this.s.documents = [];
this.s.cursorId = null;
this.s.cursorIndex = 0;
}
}
/**
* Returns current buffered documents
* @method
* @return {Array} An array of buffered documents
*/
readBufferedDocuments(number) {
const unreadDocumentsLength = this.s.documents.length - this.s.cursorIndex;
const length = number < unreadDocumentsLength ? number : unreadDocumentsLength;
let elements = this.s.documents.slice(this.s.cursorIndex, this.s.cursorIndex + length);
// Transform the doc with passed in transformation method if provided
if (this.s.transforms && typeof this.s.transforms.doc === 'function') {
// Transform all the elements
for (let i = 0; i < elements.length; i++) {
elements[i] = this.s.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.s.limit > 0 && this.s.currentLimit + elements.length > this.s.limit) {
elements = elements.slice(0, this.s.limit - this.s.currentLimit);
this.kill();
}
// Adjust current limit
this.s.currentLimit = this.s.currentLimit + elements.length;
this.s.cursorIndex = this.s.cursorIndex + elements.length;
// Return elements
return elements;
}
/**
* Retrieve the next document from the cursor
*
* @param {resultCallback} callback A callback function
*/
next(callback) {
nextFunction(this, callback);
}
}
Cursor.prototype._find = deprecate(
callback => _find(this, callback),
'_find() is deprecated, please stop using it'
);
Cursor.prototype._getmore = deprecate(
callback => _getmore(this, callback),
'_getmore() is deprecated, please stop using it'
);
function _getmore(cursor, callback) {
if (cursor.logger.isDebug()) {
cursor.logger.debug(`schedule getMore call for query [${JSON.stringify(cursor.query)}]`);
}
// Determine if it's a raw query
const raw = cursor.options.raw || cursor.cmd.raw;
// Set the current batchSize
let batchSize = cursor.s.batchSize;
if (cursor.s.limit > 0 && cursor.s.currentLimit + batchSize > cursor.s.limit) {
batchSize = cursor.s.limit - cursor.s.currentLimit;
}
// Default pool
const pool = cursor.s.server.s.pool;
// We have a wire protocol handler
cursor.s.server.s.wireProtocolHandler.getMore(
cursor.bson,
cursor.ns,
cursor.s,
batchSize,
raw,
pool,
cursor.options,
callback
);
}
function _find(cursor, callback) {
if (cursor.logger.isDebug()) {
cursor.logger.debug(
`issue initial query [${JSON.stringify(cursor.cmd)}] with flags [${JSON.stringify(
cursor.query
)}]`
);
}
const queryCallback = (err, r) => {
if (err) return callback(err);
// Get the raw message
const result = r.message;
// Query failure bit set
if (result.queryFailure) {
return callback(new MongoError(result.documents[0]), null);
}
// Check if we have a command cursor
if (
Array.isArray(result.documents) &&
result.documents.length === 1 &&
(!cursor.cmd.find || (cursor.cmd.find && cursor.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(new MongoError(result.documents[0]), null);
}
// We have a cursor document
if (result.documents[0].cursor != null && typeof result.documents[0].cursor !== 'string') {
const id = result.documents[0].cursor.id;
// If we have a namespace change set the new namespace for getmores
if (result.documents[0].cursor.ns) {
cursor.ns = result.documents[0].cursor.ns;
}
// Promote id to long if needed
cursor.s.cursorId = typeof id === 'number' ? Long.fromNumber(id) : id;
cursor.s.lastCursorId = cursor.s.cursorId;
// If we have a firstBatch set it
if (Array.isArray(result.documents[0].cursor.firstBatch)) {
cursor.s.documents = result.documents[0].cursor.firstBatch;
}
// Return after processing command cursor
return callback(null, result);
}
if (Array.isArray(result.documents[0].result)) {
cursor.s.documents = result.documents[0].result;
cursor.s.cursorId = Long.ZERO;
return callback(null, result);
}
}
// Otherwise fall back to regular find path
cursor.s.cursorId = result.cursorId;
cursor.s.documents = result.documents;
cursor.s.lastCursorId = result.cursorId;
// Transform the results with passed in transformation method if provided
if (cursor.s.transforms && typeof cursor.s.transforms.query === 'function') {
cursor.s.documents = cursor.s.transforms.query(result);
}
// Return callback
callback(null, result);
};
// Options passed to the pool
const queryOptions = {};
// If we have a raw query decorate the function
if (cursor.options.raw || cursor.cmd.raw) {
queryOptions.raw = cursor.options.raw || cursor.cmd.raw;
}
// Do we have documentsReturnedIn set on the query
if (typeof cursor.query.documentsReturnedIn === 'string') {
queryOptions.documentsReturnedIn = cursor.query.documentsReturnedIn;
}
// Add promote Long value if defined
if (typeof cursor.s.promoteLongs === 'boolean') {
queryOptions.promoteLongs = cursor.s.promoteLongs;
}
// Add promote values if defined
if (typeof cursor.s.promoteValues === 'boolean') {
queryOptions.promoteValues = cursor.s.promoteValues;
}
// Add promote values if defined
if (typeof cursor.s.promoteBuffers === 'boolean') {
queryOptions.promoteBuffers = cursor.s.promoteBuffers;
}
if (typeof cursor.s.session === 'object') {
queryOptions.session = cursor.s.session;
}
// Write the initial command out
cursor.s.server.s.pool.write(cursor.query, queryOptions, queryCallback);
}
/**
* Validate if the pool is dead and return error
*/
function isConnectionDead(cursor, callback) {
if (cursor.pool && cursor.pool.isDestroyed()) {
cursor.s.killed = true;
const err = new MongoNetworkError(
`connection to host ${cursor.pool.host}:${cursor.pool.port} was destroyed`
);
_setCursorNotifiedImpl(cursor, () => callback(err));
return true;
}
return false;
}
/**
* Validate if the cursor is dead but was not explicitly killed by user
*/
function isCursorDeadButNotkilled(cursor, callback) {
// Cursor is dead but not marked killed, return null
if (cursor.s.dead && !cursor.s.killed) {
cursor.s.killed = true;
setCursorNotified(cursor, callback);
return true;
}
return false;
}
/**
* Validate if the cursor is dead and was killed by user
*/
function isCursorDeadAndKilled(cursor, callback) {
if (cursor.s.dead && cursor.s.killed) {
handleCallback(callback, new MongoError('cursor is dead'));
return true;
}
return false;
}
/**
* Validate if the cursor was killed by the user
*/
function isCursorKilled(cursor, callback) {
if (cursor.s.killed) {
setCursorNotified(cursor, callback);
return true;
}
return false;
}
/**
* Mark cursor as being dead and notified
*/
function setCursorDeadAndNotified(cursor, callback) {
cursor.s.dead = true;
setCursorNotified(cursor, callback);
}
/**
* Mark cursor as being notified
*/
function setCursorNotified(cursor, callback) {
_setCursorNotifiedImpl(cursor, () => handleCallback(callback, null, null));
}
function _setCursorNotifiedImpl(cursor, callback) {
cursor.s.notified = true;
cursor.s.documents = [];
cursor.s.cursorIndex = 0;
if (cursor._endSession) {
return cursor._endSession(undefined, () => callback());
}
return callback();
}
function initializeCursorAndRetryNext(cursor, callback) {
cursor.topology.selectServer(
readPreferenceServerSelector(cursor.options.readPreference || ReadPreference.primary),
(err, server) => {
if (err) {
callback(err, null);
return;
}
cursor.s.server = server;
cursor.s.init = true;
// check if server supports collation
// NOTE: this should be a part of the selection predicate!
if (cursor.cmd && cursor.cmd.collation && cursor.server.description.maxWireVersion < 5) {
callback(new MongoError(`server ${cursor.server.name} does not support collation`));
return;
}
try {
cursor.query = cursor.s.server.s.wireProtocolHandler.command(
cursor.bson,
cursor.ns,
cursor.cmd,
cursor.s,
cursor.topology,
cursor.options
);
nextFunction(cursor, callback);
} catch (err) {
callback(err);
return;
}
}
);
}
function nextFunction(cursor, callback) {
// We have notified about it
if (cursor.s.notified) {
return callback(new Error('cursor is exhausted'));
}
// Cursor is killed return null
if (isCursorKilled(cursor, callback)) return;
// Cursor is dead but not marked killed, return null
if (isCursorDeadButNotkilled(cursor, callback)) return;
// We have a dead and killed cursor, attempting to call next should error
if (isCursorDeadAndKilled(cursor, callback)) return;
// We have just started the cursor
if (!cursor.s.init) {
return initializeCursorAndRetryNext(cursor, callback);
}
// If we don't have a cursorId execute the first query
if (cursor.s.cursorId == null) {
// Check if pool is dead and return if not possible to
// execute the query against the db
if (isConnectionDead(cursor, callback)) return;
// query, cmd, options, s, callback
return _find(cursor, function(err) {
if (err) return handleCallback(callback, err, null);
if (cursor.s.cursorId && cursor.s.cursorId.isZero() && cursor._endSession) {
cursor._endSession();
}
if (
cursor.s.documents.length === 0 &&
cursor.s.cursorId &&
cursor.s.cursorId.isZero() &&
!cursor.cmd.tailable &&
!cursor.cmd.awaitData
) {
return setCursorNotified(cursor, callback);
}
nextFunction(cursor, callback);
});
}
if (cursor.s.documents.length === cursor.s.cursorIndex && Long.ZERO.equals(cursor.s.cursorId)) {
setCursorDeadAndNotified(cursor, callback);
return;
}
if (cursor.s.limit > 0 && cursor.s.currentLimit >= cursor.s.limit) {
// Ensure we kill the cursor on the server
cursor.kill();
// Set cursor in dead and notified state
setCursorDeadAndNotified(cursor, callback);
return;
}
if (
cursor.s.documents.length === cursor.s.cursorIndex &&
cursor.cmd.tailable &&
Long.ZERO.equals(cursor.s.cursorId)
) {
return handleCallback(
callback,
new MongoError({
message: 'No more documents in tailed cursor',
tailable: cursor.cmd.tailable,
awaitData: cursor.cmd.awaitData
})
);
}
if (cursor.s.cursorIndex === cursor.s.documents.length && !Long.ZERO.equals(cursor.s.cursorId)) {
// Ensure an empty cursor state
cursor.s.documents = [];
cursor.s.cursorIndex = 0;
// Check if connection is dead and return if not possible to
if (isConnectionDead(cursor, callback)) return;
// Execute the next get more
return _getmore(cursor, function(err, doc, connection) {
if (err) {
if (err instanceof MongoError) {
err[mongoErrorContextSymbol].isGetMore = true;
}
return handleCallback(callback, err);
}
if (cursor.s.cursorId && cursor.s.cursorId.isZero() && cursor._endSession) {
cursor._endSession();
}
// Save the returned connection to ensure all getMore's fire over the same connection
cursor.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 (
cursor.s.documents.length === 0 &&
cursor.cmd.tailable &&
Long.ZERO.equals(cursor.s.cursorId)
) {
// No more documents in the tailed cursor
return handleCallback(
callback,
new MongoError({
message: 'No more documents in tailed cursor',
tailable: cursor.cmd.tailable,
awaitData: cursor.cmd.awaitData
})
);
} else if (
cursor.s.documents.length === 0 &&
cursor.cmd.tailable &&
!Long.ZERO.equals(cursor.s.cursorId)
) {
return nextFunction(cursor, callback);
}
if (cursor.s.limit > 0 && cursor.s.currentLimit >= cursor.s.limit) {
return setCursorDeadAndNotified(cursor, callback);
}
nextFunction(cursor, callback);
});
}
if (cursor.s.limit > 0 && cursor.s.currentLimit >= cursor.s.limit) {
// Ensure we kill the cursor on the server
cursor.kill();
// Set cursor in dead and notified state
return setCursorDeadAndNotified(cursor, callback);
}
// Increment the current cursor limit
cursor.s.currentLimit += 1;
// Get the document
let doc = cursor.s.documents[cursor.s.cursorIndex++];
// Doc overflow
if (!doc || doc.$err) {
// Ensure we kill the cursor on the server
cursor.kill();
// Set cursor in dead and notified state
return setCursorDeadAndNotified(cursor, function() {
handleCallback(callback, new MongoError(doc ? doc.$err : undefined));
});
}
// Transform the doc with passed in transformation method if provided
if (cursor.s.transforms && typeof cursor.s.transforms.doc === 'function') {
doc = cursor.s.transforms.doc(doc);
}
// Return the document
handleCallback(callback, null, doc);
}
module.exports = Cursor;

217
node_modules/mongodb-core/lib/sdam/monitoring.js generated vendored Normal file
View File

@@ -0,0 +1,217 @@
'use strict';
const ServerDescription = require('./server_description').ServerDescription;
const calculateDurationInMs = require('../utils').calculateDurationInMs;
/**
* Published when server description changes, but does NOT include changes to the RTT.
*
* @property {Object} topologyId A unique identifier for the topology
* @property {ServerAddress} address The address (host/port pair) of the server
* @property {ServerDescription} previousDescription The previous server description
* @property {ServerDescription} newDescription The new server description
*/
class ServerDescriptionChangedEvent {
constructor(topologyId, address, previousDescription, newDescription) {
Object.assign(this, { topologyId, address, previousDescription, newDescription });
}
}
/**
* Published when server is initialized.
*
* @property {Object} topologyId A unique identifier for the topology
* @property {ServerAddress} address The address (host/port pair) of the server
*/
class ServerOpeningEvent {
constructor(topologyId, address) {
Object.assign(this, { topologyId, address });
}
}
/**
* Published when server is closed.
*
* @property {ServerAddress} address The address (host/port pair) of the server
* @property {Object} topologyId A unique identifier for the topology
*/
class ServerClosedEvent {
constructor(topologyId, address) {
Object.assign(this, { topologyId, address });
}
}
/**
* Published when topology description changes.
*
* @property {Object} topologyId
* @property {TopologyDescription} previousDescription The old topology description
* @property {TopologyDescription} newDescription The new topology description
*/
class TopologyDescriptionChangedEvent {
constructor(topologyId, previousDescription, newDescription) {
Object.assign(this, { topologyId, previousDescription, newDescription });
}
}
/**
* Published when topology is initialized.
*
* @param {Object} topologyId A unique identifier for the topology
*/
class TopologyOpeningEvent {
constructor(topologyId) {
Object.assign(this, { topologyId });
}
}
/**
* Published when topology is closed.
*
* @param {Object} topologyId A unique identifier for the topology
*/
class TopologyClosedEvent {
constructor(topologyId) {
Object.assign(this, { topologyId });
}
}
/**
* Fired when the server monitors ismaster command is started - immediately before
* the ismaster command is serialized into raw BSON and written to the socket.
*
* @property {Object} connectionId The connection id for the command
*/
class ServerHeartbeatStartedEvent {
constructor(connectionId) {
Object.assign(this, { connectionId });
}
}
/**
* Fired when the server monitors ismaster succeeds.
*
* @param {Number} duration The execution time of the event in ms
* @param {Object} reply The command reply
* @param {Object} connectionId The connection id for the command
*/
class ServerHeartbeatSucceededEvent {
constructor(duration, reply, connectionId) {
Object.assign(this, { duration, reply, connectionId });
}
}
/**
* Fired when the server monitors ismaster fails, either with an “ok: 0” or a socket exception.
*
* @param {Number} duration The execution time of the event in ms
* @param {MongoError|Object} failure The command failure
* @param {Object} connectionId The connection id for the command
*/
class ServerHeartbeatFailedEvent {
constructor(duration, failure, connectionId) {
Object.assign(this, { duration, failure, connectionId });
}
}
/**
* Performs a server check as described by the SDAM spec.
*
* NOTE: This method automatically reschedules itself, so that there is always an active
* monitoring process
*
* @param {Server} server The server to monitor
*/
function monitorServer(server) {
// executes a single check of a server
const checkServer = callback => {
let start = process.hrtime();
// emit a signal indicating we have started the heartbeat
server.emit('serverHeartbeatStarted', new ServerHeartbeatStartedEvent(server.name));
server.command(
'admin.$cmd',
{ ismaster: true },
{
monitoring: true,
socketTimeout: server.s.options.connectionTimeout || 2000
},
function(err, result) {
let duration = calculateDurationInMs(start);
if (err) {
server.emit(
'serverHeartbeatFailed',
new ServerHeartbeatFailedEvent(duration, err, server.name)
);
return callback(err, null);
}
const isMaster = result.result;
server.emit(
'serverHeartbeatSucceded',
new ServerHeartbeatSucceededEvent(duration, isMaster, server.name)
);
return callback(null, isMaster);
}
);
};
const successHandler = isMaster => {
server.s.monitoring = false;
// emit an event indicating that our description has changed
server.emit('descriptionReceived', new ServerDescription(server.description.address, isMaster));
// schedule the next monitoring process
server.s.monitorId = setTimeout(
() => monitorServer(server),
server.s.options.heartbeatFrequencyMS
);
};
// run the actual monitoring loop
server.s.monitoring = true;
checkServer((err, isMaster) => {
if (!err) {
successHandler(isMaster);
return;
}
// According to the SDAM specification's "Network error during server check" section, if
// an ismaster call fails we reset the server's pool. If a server was once connected,
// change its type to `Unknown` only after retrying once.
// TODO: we need to reset the pool here
return checkServer((err, isMaster) => {
if (err) {
server.s.monitoring = false;
// revert to `Unknown` by emitting a default description with no isMaster
server.emit('descriptionReceived', new ServerDescription(server.description.address));
// do not reschedule monitoring in this case
return;
}
successHandler(isMaster);
});
});
}
module.exports = {
ServerDescriptionChangedEvent,
ServerOpeningEvent,
ServerClosedEvent,
TopologyDescriptionChangedEvent,
TopologyOpeningEvent,
TopologyClosedEvent,
ServerHeartbeatStartedEvent,
ServerHeartbeatSucceededEvent,
ServerHeartbeatFailedEvent,
monitorServer
};

411
node_modules/mongodb-core/lib/sdam/server.js generated vendored Normal file
View File

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

View File

@@ -0,0 +1,141 @@
'use strict';
// An enumeration of server types we know about
const ServerType = {
Standalone: 'Standalone',
Mongos: 'Mongos',
PossiblePrimary: 'PossiblePrimary',
RSPrimary: 'RSPrimary',
RSSecondary: 'RSSecondary',
RSArbiter: 'RSArbiter',
RSOther: 'RSOther',
RSGhost: 'RSGhost',
Unknown: 'Unknown'
};
const WRITABLE_SERVER_TYPES = new Set([
ServerType.RSPrimary,
ServerType.Standalone,
ServerType.Mongos
]);
const ISMASTER_FIELDS = [
'minWireVersion',
'maxWireVersion',
'me',
'hosts',
'passives',
'arbiters',
'tags',
'setName',
'setVersion',
'electionId',
'primary',
'logicalSessionTimeoutMinutes'
];
/**
* The client's view of a single server, based on the most recent ismaster outcome.
*
* Internal type, not meant to be directly instantiated
*/
class ServerDescription {
/**
* Create a ServerDescription
* @param {String} address The address of the server
* @param {Object} [ismaster] An optional ismaster response for this server
* @param {Object} [options] Optional settings
* @param {Number} [options.roundTripTime] The round trip time to ping this server (in ms)
*/
constructor(address, ismaster, options) {
options = options || {};
ismaster = Object.assign(
{
minWireVersion: 0,
maxWireVersion: 0,
hosts: [],
passives: [],
arbiters: [],
tags: []
},
ismaster
);
this.address = address;
this.error = null;
this.roundTripTime = options.roundTripTime || 0;
this.lastUpdateTime = Date.now();
this.lastWriteDate = ismaster.lastWrite ? ismaster.lastWrite.lastWriteDate : null;
this.opTime = ismaster.lastWrite ? ismaster.lastWrite.opTime : null;
this.type = parseServerType(ismaster);
// direct mappings
ISMASTER_FIELDS.forEach(field => {
if (typeof ismaster[field] !== 'undefined') this[field] = ismaster[field];
});
// normalize case for hosts
this.hosts = this.hosts.map(host => host.toLowerCase());
this.passives = this.passives.map(host => host.toLowerCase());
this.arbiters = this.arbiters.map(host => host.toLowerCase());
}
get allHosts() {
return this.hosts.concat(this.arbiters).concat(this.passives);
}
/**
* @return {Boolean} Is this server available for reads
*/
get isReadable() {
return this.type === ServerType.RSSecondary || this.isWritable;
}
/**
* @return {Boolean} Is this server available for writes
*/
get isWritable() {
return WRITABLE_SERVER_TYPES.has(this.type);
}
}
/**
* Parses an `ismaster` message and determines the server type
*
* @param {Object} ismaster The `ismaster` message to parse
* @return {ServerType}
*/
function parseServerType(ismaster) {
if (!ismaster || !ismaster.ok) {
return ServerType.Unknown;
}
if (ismaster.isreplicaset) {
return ServerType.RSGhost;
}
if (ismaster.msg && ismaster.msg === 'isdbgrid') {
return ServerType.Mongos;
}
if (ismaster.setName) {
if (ismaster.hidden) {
return ServerType.RSOther;
} else if (ismaster.ismaster) {
return ServerType.RSPrimary;
} else if (ismaster.secondary) {
return ServerType.RSSecondary;
} else if (ismaster.arbiterOnly) {
return ServerType.RSArbiter;
} else {
return ServerType.RSOther;
}
}
return ServerType.Standalone;
}
module.exports = {
ServerDescription,
ServerType
};

206
node_modules/mongodb-core/lib/sdam/server_selectors.js generated vendored Normal file
View File

@@ -0,0 +1,206 @@
'use strict';
const ServerType = require('./server_description').ServerType;
const TopologyType = require('./topology_description').TopologyType;
const ReadPreference = require('../topologies/read_preference');
const MongoError = require('../error').MongoError;
// max staleness constants
const IDLE_WRITE_PERIOD = 10000;
const SMALLEST_MAX_STALENESS_SECONDS = 90;
function writableServerSelector() {
return function(topologyDescription, servers) {
return latencyWindowReducer(topologyDescription, servers.filter(s => s.isWritable));
};
}
// reducers
function maxStalenessReducer(readPreference, topologyDescription, servers) {
if (readPreference.maxStalenessSeconds == null || readPreference.maxStalenessSeconds < 0) {
return servers;
}
const maxStaleness = readPreference.maxStalenessSeconds;
const maxStalenessVariance =
(topologyDescription.heartbeatFrequencyMS + IDLE_WRITE_PERIOD) / 1000;
if (maxStaleness < maxStalenessVariance) {
throw MongoError(`maxStalenessSeconds must be at least ${maxStalenessVariance} seconds`);
}
if (maxStaleness < SMALLEST_MAX_STALENESS_SECONDS) {
throw new MongoError(
`maxStalenessSeconds must be at least ${SMALLEST_MAX_STALENESS_SECONDS} seconds`
);
}
if (topologyDescription.type === TopologyType.ReplicaSetWithPrimary) {
const primary = servers.filter(primaryFilter)[0];
return servers.reduce((result, server) => {
const stalenessMS =
server.lastUpdateTime -
server.lastWriteDate -
(primary.lastUpdateTime - primary.lastWriteDate) +
topologyDescription.heartbeatFrequencyMS;
const staleness = stalenessMS / 1000;
if (staleness <= readPreference.maxStalenessSeconds) result.push(server);
return result;
}, []);
} else if (topologyDescription.type === TopologyType.ReplicaSetNoPrimary) {
const sMax = servers.reduce((max, s) => (s.lastWriteDate > max.lastWriteDate ? s : max));
return servers.reduce((result, server) => {
const stalenessMS =
sMax.lastWriteDate - server.lastWriteDate + topologyDescription.heartbeatFrequencyMS;
const staleness = stalenessMS / 1000;
if (staleness <= readPreference.maxStalenessSeconds) result.push(server);
return result;
}, []);
}
return servers;
}
function tagSetMatch(tagSet, serverTags) {
const keys = Object.keys(tagSet);
const serverTagKeys = Object.keys(serverTags);
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
if (serverTagKeys.indexOf(key) === -1 || serverTags[key] !== tagSet[key]) {
return false;
}
}
return true;
}
function tagSetReducer(readPreference, servers) {
if (
readPreference.tags == null ||
(Array.isArray(readPreference.tags) && readPreference.tags.length === 0)
) {
return servers;
}
for (let i = 0; i < readPreference.tags.length; ++i) {
const tagSet = readPreference.tags[i];
const serversMatchingTagset = servers.reduce((matched, server) => {
if (tagSetMatch(tagSet, server.tags)) matched.push(server);
return matched;
}, []);
if (serversMatchingTagset.length) {
return serversMatchingTagset;
}
}
return [];
}
function latencyWindowReducer(topologyDescription, servers) {
const low = servers.reduce(
(min, server) => (min === -1 ? server.roundTripTime : Math.min(server.roundTripTime, min)),
-1
);
const high = low + topologyDescription.localThresholdMS;
return servers.reduce((result, server) => {
if (server.roundTripTime <= high && server.roundTripTime >= low) result.push(server);
return result;
}, []);
}
// filters
function primaryFilter(server) {
return server.type === ServerType.RSPrimary;
}
function secondaryFilter(server) {
return server.type === ServerType.RSSecondary;
}
function nearestFilter(server) {
return server.type === ServerType.RSSecondary || server.type === ServerType.RSPrimary;
}
function knownFilter(server) {
return server.type !== ServerType.Unknown;
}
function readPreferenceServerSelector(readPreference) {
if (!readPreference.isValid()) {
throw new TypeError('Invalid read preference specified');
}
return function(topologyDescription, servers) {
const commonWireVersion = topologyDescription.commonWireVersion;
if (
commonWireVersion &&
(readPreference.minWireVersion && readPreference.minWireVersion > commonWireVersion)
) {
throw new MongoError(
`Minimum wire version '${
readPreference.minWireVersion
}' required, but found '${commonWireVersion}'`
);
}
if (
topologyDescription.type === TopologyType.Single ||
topologyDescription.type === TopologyType.Sharded
) {
return latencyWindowReducer(topologyDescription, servers.filter(knownFilter));
}
if (readPreference.mode === ReadPreference.PRIMARY) {
return servers.filter(primaryFilter);
}
if (readPreference.mode === ReadPreference.SECONDARY) {
return latencyWindowReducer(
topologyDescription,
tagSetReducer(
readPreference,
maxStalenessReducer(readPreference, topologyDescription, servers)
)
).filter(secondaryFilter);
} else if (readPreference.mode === ReadPreference.NEAREST) {
return latencyWindowReducer(
topologyDescription,
tagSetReducer(
readPreference,
maxStalenessReducer(readPreference, topologyDescription, servers)
)
).filter(nearestFilter);
} else if (readPreference.mode === ReadPreference.SECONDARY_PREFERRED) {
const result = latencyWindowReducer(
topologyDescription,
tagSetReducer(
readPreference,
maxStalenessReducer(readPreference, topologyDescription, servers)
)
).filter(secondaryFilter);
return result.length === 0 ? servers.filter(primaryFilter) : result;
} else if (readPreference.mode === ReadPreference.PRIMARY_PREFERRED) {
const result = servers.filter(primaryFilter);
if (result.length) {
return result;
}
return latencyWindowReducer(
topologyDescription,
tagSetReducer(
readPreference,
maxStalenessReducer(readPreference, topologyDescription, servers)
)
).filter(secondaryFilter);
}
};
}
module.exports = {
writableServerSelector,
readPreferenceServerSelector
};

666
node_modules/mongodb-core/lib/sdam/topology.js generated vendored Normal file
View File

@@ -0,0 +1,666 @@
'use strict';
const EventEmitter = require('events');
const ServerDescription = require('./server_description').ServerDescription;
const TopologyDescription = require('./topology_description').TopologyDescription;
const TopologyType = require('./topology_description').TopologyType;
const monitoring = require('./monitoring');
const calculateDurationInMs = require('../utils').calculateDurationInMs;
const MongoTimeoutError = require('../error').MongoTimeoutError;
const MongoNetworkError = require('../error').MongoNetworkError;
const Server = require('./server');
const relayEvents = require('../utils').relayEvents;
const ReadPreference = require('../topologies/read_preference');
const readPreferenceServerSelector = require('./server_selectors').readPreferenceServerSelector;
const writableServerSelector = require('./server_selectors').writableServerSelector;
const isRetryableWritesSupported = require('../topologies/shared').isRetryableWritesSupported;
const Cursor = require('./cursor');
const deprecate = require('util').deprecate;
const BSON = require('../connection/utils').retrieveBSON();
const createCompressionInfo = require('../topologies/shared').createCompressionInfo;
// Global state
let globalTopologyCounter = 0;
// Constants
const TOPOLOGY_DEFAULTS = {
localThresholdMS: 15,
serverSelectionTimeoutMS: 10000,
heartbeatFrequencyMS: 30000,
minHeartbeatIntervalMS: 500
};
/**
* A container of server instances representing a connection to a MongoDB topology.
*
* @fires Topology#serverOpening
* @fires Topology#serverClosed
* @fires Topology#serverDescriptionChanged
* @fires Topology#topologyOpening
* @fires Topology#topologyClosed
* @fires Topology#topologyDescriptionChanged
* @fires Topology#serverHeartbeatStarted
* @fires Topology#serverHeartbeatSucceeded
* @fires Topology#serverHeartbeatFailed
*/
class Topology extends EventEmitter {
/**
* Create a topology
*
* @param {Array|String} [seedlist] a string list, or array of Server instances to connect to
* @param {Object} [options] Optional settings
* @param {Number} [options.localThresholdMS=15] The size of the latency window for selecting among multiple suitable servers
* @param {Number} [options.serverSelectionTimeoutMS=30000] How long to block for server selection before throwing an error
* @param {Number} [options.heartbeatFrequencyMS=10000] The frequency with which topology updates are scheduled
*/
constructor(seedlist, options) {
super();
if (typeof options === 'undefined') {
options = seedlist;
seedlist = [];
// this is for legacy single server constructor support
if (options.host) {
seedlist.push({ host: options.host, port: options.port });
}
}
seedlist = seedlist || [];
options = Object.assign({}, TOPOLOGY_DEFAULTS, options);
const topologyType = topologyTypeFromSeedlist(seedlist, options);
const topologyId = globalTopologyCounter++;
const serverDescriptions = seedlist.reduce((result, seed) => {
const address = seed.port ? `${seed.host}:${seed.port}` : `${seed.host}:27017`;
result.set(address, new ServerDescription(address));
return result;
}, new Map());
this.s = {
// the id of this topology
id: topologyId,
// passed in options
options: Object.assign({}, options),
// initial seedlist of servers to connect to
seedlist: seedlist,
// the topology description
description: new TopologyDescription(
topologyType,
serverDescriptions,
options.replicaSet,
null,
null,
options
),
serverSelectionTimeoutMS: options.serverSelectionTimeoutMS,
heartbeatFrequencyMS: options.heartbeatFrequencyMS,
minHeartbeatIntervalMS: options.minHeartbeatIntervalMS,
// allow users to override the cursor factory
Cursor: options.cursorFactory || Cursor,
// the bson parser
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
])
};
// amend options for server instance creation
this.s.options.compression = { compressors: createCompressionInfo(options) };
}
/**
* @return A `TopologyDescription` for this topology
*/
get description() {
return this.s.description;
}
/**
* All raw connections
* @method
* @return {Connection[]}
*/
connections() {
return Array.from(this.s.servers.values()).reduce((result, server) => {
return result.concat(server.s.pool.allConnections());
}, []);
}
/**
* Initiate server connect
*
* @param {Object} [options] Optional settings
* @param {Array} [options.auth=null] Array of auth options to apply on connect
*/
connect(/* options */) {
// emit SDAM monitoring events
this.emit('topologyOpening', new monitoring.TopologyOpeningEvent(this.s.id));
// emit an event for the topology change
this.emit(
'topologyDescriptionChanged',
new monitoring.TopologyDescriptionChangedEvent(
this.s.id,
new TopologyDescription(TopologyType.Unknown), // initial is always Unknown
this.s.description
)
);
connectServers(this, Array.from(this.s.description.servers.values()));
this.s.connected = true;
}
/**
* Close this topology
*/
close(callback) {
// destroy all child servers
this.s.servers.forEach(server => server.destroy());
// emit an event for close
this.emit('topologyClosed', new monitoring.TopologyClosedEvent(this.s.id));
this.s.connected = false;
if (typeof callback === 'function') {
callback(null, null);
}
}
/**
* Selects a server according to the selection predicate provided
*
* @param {function} [selector] An optional selector to select servers by, defaults to a random selection within a latency window
* @return {Server} An instance of a `Server` meeting the criteria of the predicate provided
*/
selectServer(selector, options, callback) {
if (typeof options === 'function') (callback = options), (options = {});
options = Object.assign(
{},
{ serverSelectionTimeoutMS: this.s.serverSelectionTimeoutMS },
options
);
selectServers(
this,
selector,
options.serverSelectionTimeoutMS,
process.hrtime(),
(err, servers) => {
if (err) return callback(err, null);
callback(null, randomSelection(servers));
}
);
}
/**
* Update the internal TopologyDescription with a ServerDescription
*
* @param {object} serverDescription The server to update in the internal list of server descriptions
*/
serverUpdateHandler(serverDescription) {
if (!this.s.description.hasServer(serverDescription.address)) {
return;
}
// these will be used for monitoring events later
const previousTopologyDescription = this.s.description;
const previousServerDescription = this.s.description.servers.get(serverDescription.address);
// first update the TopologyDescription
this.s.description = this.s.description.update(serverDescription);
// emit monitoring events for this change
this.emit(
'serverDescriptionChanged',
new monitoring.ServerDescriptionChangedEvent(
this.s.id,
serverDescription.address,
previousServerDescription,
this.s.description.servers.get(serverDescription.address)
)
);
// update server list from updated descriptions
updateServers(this, serverDescription);
this.emit(
'topologyDescriptionChanged',
new monitoring.TopologyDescriptionChangedEvent(
this.s.id,
previousTopologyDescription,
this.s.description
)
);
}
/**
* Authenticate using a specified mechanism
*
* @param {String} mechanism The auth mechanism used for authentication
* @param {String} db The db we are authenticating against
* @param {Object} options Optional settings for the authenticating mechanism
* @param {authResultCallback} callback A callback function
*/
auth(mechanism, db, options, callback) {
callback(null, null);
}
/**
* Logout from a database
*
* @param {String} db The db we are logging out from
* @param {authResultCallback} callback A callback function
*/
logout(db, callback) {
callback(null, null);
}
// Basic operation support. Eventually this should be moved into command construction
// during the command refactor.
/**
* Insert one or more documents
*
* @param {String} ns The full qualified namespace for this operation
* @param {Array} ops An array of documents to insert
* @param {Boolean} [options.ordered=true] Execute in order or out of order
* @param {Object} [options.writeConcern] Write concern for the operation
* @param {Boolean} [options.serializeFunctions=false] Specify if functions on an object should be serialized
* @param {Boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields
* @param {ClientSession} [options.session] Session to use for the operation
* @param {boolean} [options.retryWrites] Enable retryable writes for this operation
* @param {opResultCallback} callback A callback function
*/
insert(ns, ops, options, callback) {
executeWriteOperation({ topology: this, op: 'insert', ns, ops }, options, callback);
}
/**
* Perform one or more update operations
*
* @param {string} ns The fully qualified namespace for this operation
* @param {array} ops An array of updates
* @param {boolean} [options.ordered=true] Execute in order or out of order
* @param {object} [options.writeConcern] Write concern for the operation
* @param {Boolean} [options.serializeFunctions=false] Specify if functions on an object should be serialized
* @param {Boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields
* @param {ClientSession} [options.session] Session to use for the operation
* @param {boolean} [options.retryWrites] Enable retryable writes for this operation
* @param {opResultCallback} callback A callback function
*/
update(ns, ops, options, callback) {
executeWriteOperation({ topology: this, op: 'update', ns, ops }, options, callback);
}
/**
* Perform one or more remove operations
*
* @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1)
* @param {array} ops An array of removes
* @param {boolean} [options.ordered=true] Execute in order or out of order
* @param {object} [options.writeConcern={}] Write concern for the operation
* @param {Boolean} [options.serializeFunctions=false] Specify if functions on an object should be serialized.
* @param {Boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
* @param {ClientSession} [options.session=null] Session to use for the operation
* @param {boolean} [options.retryWrites] Enable retryable writes for this operation
* @param {opResultCallback} callback A callback function
*/
remove(ns, ops, options, callback) {
executeWriteOperation({ topology: this, op: 'remove', ns, ops }, options, callback);
}
/**
* Execute a command
*
* @method
* @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1)
* @param {object} cmd The command hash
* @param {ReadPreference} [options.readPreference] Specify read preference if command supports it
* @param {Connection} [options.connection] Specify connection object to execute command against
* @param {Boolean} [options.serializeFunctions=false] Specify if functions on an object should be serialized.
* @param {Boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
* @param {ClientSession} [options.session=null] Session to use for the operation
* @param {opResultCallback} callback A callback function
*/
command(ns, cmd, options, callback) {
if (typeof options === 'function') {
(callback = options), (options = {}), (options = options || {});
}
const readPreference = options.readPreference ? options.readPreference : ReadPreference.primary;
this.selectServer(readPreferenceServerSelector(readPreference), (err, server) => {
if (err) {
callback(err, null);
return;
}
server.command(ns, cmd, options, callback);
});
}
/**
* Create 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] Options for the cursor
* @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 {ClientSession} [options.session=null] Session to use for the operation
* @param {object} [options.topology] The internal topology of the created cursor
* @returns {Cursor}
*/
cursor(ns, cmd, options) {
options = options || {};
const topology = options.topology || this;
const CursorClass = options.cursorFactory || this.s.Cursor;
return new CursorClass(this.s.bson, ns, cmd, options, topology, this.s.options);
}
}
// legacy aliases
Topology.prototype.destroy = deprecate(
Topology.prototype.close,
'destroy() is deprecated, please use close() instead'
);
function topologyTypeFromSeedlist(seedlist, options) {
if (seedlist.length === 1 && !options.replicaSet) return TopologyType.Single;
if (options.replicaSet) return TopologyType.ReplicaSetNoPrimary;
return TopologyType.Unknown;
}
function randomSelection(array) {
return array[Math.floor(Math.random() * array.length)];
}
/**
* Selects servers using the provided selector
*
* @private
* @param {Topology} topology The topology to select servers from
* @param {function} selector The actual predicate used for selecting servers
* @param {Number} timeout The max time we are willing wait for selection
* @param {Number} start A high precision timestamp for the start of the selection process
* @param {function} callback The callback used to convey errors or the resultant servers
*/
function selectServers(topology, selector, timeout, start, callback) {
const serverDescriptions = Array.from(topology.description.servers.values());
let descriptions;
try {
descriptions = selector
? selector(topology.description, serverDescriptions)
: serverDescriptions;
} catch (e) {
return callback(e, null);
}
if (descriptions.length) {
const servers = descriptions.map(description => topology.s.servers.get(description.address));
return callback(null, servers);
}
const duration = calculateDurationInMs(start);
if (duration >= timeout) {
return callback(new MongoTimeoutError(`Server selection timed out after ${timeout} ms`));
}
const retrySelection = () => {
// ensure all server monitors attempt monitoring immediately
topology.s.servers.forEach(server => server.monitor());
const iterationTimer = setTimeout(() => {
callback(new MongoTimeoutError('Server selection timed out due to monitoring'));
}, topology.s.minHeartbeatIntervalMS);
topology.once('topologyDescriptionChanged', () => {
// successful iteration, clear the check timer
clearTimeout(iterationTimer);
// topology description has changed due to monitoring, reattempt server selection
selectServers(topology, selector, timeout, start, callback);
});
};
// ensure we are connected
if (!topology.s.connected) {
topology.connect();
// we want to make sure we're still within the requested timeout window
const failToConnectTimer = setTimeout(() => {
callback(new MongoTimeoutError('Server selection timed out waiting to connect'));
}, timeout - duration);
topology.once('connect', () => {
clearTimeout(failToConnectTimer);
retrySelection();
});
return;
}
retrySelection();
}
/**
* Create `Server` instances for all initially known servers, connect them, and assign
* them to the passed in `Topology`.
*
* @param {Topology} topology The topology responsible for the servers
* @param {ServerDescription[]} serverDescriptions A list of server descriptions to connect
*/
function connectServers(topology, serverDescriptions) {
topology.s.servers = serverDescriptions.reduce((servers, serverDescription) => {
// publish an open event for each ServerDescription created
topology.emit(
'serverOpening',
new monitoring.ServerOpeningEvent(topology.s.id, serverDescription.address)
);
const server = new Server(serverDescription, topology.s.options);
relayEvents(server, topology, [
'serverHeartbeatStarted',
'serverHeartbeatSucceeded',
'serverHeartbeatFailed'
]);
server.on('descriptionReceived', topology.serverUpdateHandler.bind(topology));
server.on('connect', serverConnectEventHandler(server, topology));
servers.set(serverDescription.address, server);
server.connect();
return servers;
}, new Map());
}
function updateServers(topology, currentServerDescription) {
// update the internal server's description
if (topology.s.servers.has(currentServerDescription.address)) {
const server = topology.s.servers.get(currentServerDescription.address);
server.s.description = currentServerDescription;
}
// add new servers for all descriptions we currently don't know about locally
for (const serverDescription of topology.description.servers.values()) {
if (!topology.s.servers.has(serverDescription.address)) {
topology.emit(
'serverOpening',
new monitoring.ServerOpeningEvent(topology.s.id, serverDescription.address)
);
const server = new Server(serverDescription, topology.s.options);
relayEvents(server, topology, [
'serverHeartbeatStarted',
'serverHeartbeatSucceeded',
'serverHeartbeatFailed'
]);
server.on('descriptionReceived', topology.serverUpdateHandler.bind(topology));
server.on('connect', serverConnectEventHandler(server, topology));
topology.s.servers.set(serverDescription.address, server);
server.connect();
}
}
// for all servers no longer known, remove their descriptions and destroy their instances
for (const entry of topology.s.servers) {
const serverAddress = entry[0];
if (topology.description.hasServer(serverAddress)) {
continue;
}
const server = topology.s.servers.get(serverAddress);
topology.s.servers.delete(serverAddress);
server.destroy(() =>
topology.emit('serverClosed', new monitoring.ServerClosedEvent(topology.s.id, serverAddress))
);
}
}
function serverConnectEventHandler(server, topology) {
return function(/* ismaster */) {
topology.emit('connect', topology);
};
}
function executeWriteOperation(args, options, callback) {
if (typeof options === 'function') (callback = options), (options = {});
options = options || {};
// TODO: once we drop Node 4, use destructuring either here or in arguments.
const topology = args.topology;
const op = args.op;
const ns = args.ns;
const ops = args.ops;
const willRetryWrite =
!args.retrying &&
options.retryWrites &&
options.session &&
isRetryableWritesSupported(topology) &&
!options.session.inTransaction();
topology.selectServer(writableServerSelector(), (err, server) => {
if (err) {
callback(err, null);
return;
}
const handler = (err, result) => {
if (!err) return callback(null, result);
if (!(err instanceof MongoNetworkError) && !err.message.match(/not master/)) {
return callback(err);
}
if (willRetryWrite) {
const newArgs = Object.assign({}, args, { retrying: true });
return executeWriteOperation(newArgs, options, callback);
}
return callback(err);
};
if (callback.operationId) {
handler.operationId = callback.operationId;
}
// increment and assign txnNumber
if (willRetryWrite) {
options.session.incrementTransactionNumber();
options.willRetryWrite = willRetryWrite;
}
// execute the write operation
server[op](ns, ops, options, handler);
// we need to increment the statement id if we're in a transaction
if (options.session && options.session.inTransaction()) {
options.session.incrementStatementId(ops.length);
}
});
}
/**
* A server opening SDAM monitoring event
*
* @event Topology#serverOpening
* @type {ServerOpeningEvent}
*/
/**
* A server closed SDAM monitoring event
*
* @event Topology#serverClosed
* @type {ServerClosedEvent}
*/
/**
* A server description SDAM change monitoring event
*
* @event Topology#serverDescriptionChanged
* @type {ServerDescriptionChangedEvent}
*/
/**
* A topology open SDAM event
*
* @event Topology#topologyOpening
* @type {TopologyOpeningEvent}
*/
/**
* A topology closed SDAM event
*
* @event Topology#topologyClosed
* @type {TopologyClosedEvent}
*/
/**
* A topology structure SDAM change event
*
* @event Topology#topologyDescriptionChanged
* @type {TopologyDescriptionChangedEvent}
*/
/**
* A topology serverHeartbeatStarted SDAM event
*
* @event Topology#serverHeartbeatStarted
* @type {ServerHeartbeatStartedEvent}
*/
/**
* A topology serverHeartbeatFailed SDAM event
*
* @event Topology#serverHeartbeatFailed
* @type {ServerHearbeatFailedEvent}
*/
/**
* A topology serverHeartbeatSucceeded SDAM change event
*
* @event Topology#serverHeartbeatSucceeded
* @type {ServerHeartbeatSucceededEvent}
*/
module.exports = Topology;

View File

@@ -0,0 +1,364 @@
'use strict';
const ServerType = require('./server_description').ServerType;
const ServerDescription = require('./server_description').ServerDescription;
const ReadPreference = require('../topologies/read_preference');
// contstants related to compatability checks
const MIN_SUPPORTED_SERVER_VERSION = '2.6';
const MIN_SUPPORTED_WIRE_VERSION = 2;
const MAX_SUPPORTED_WIRE_VERSION = 5;
// An enumeration of topology types we know about
const TopologyType = {
Single: 'Single',
ReplicaSetNoPrimary: 'ReplicaSetNoPrimary',
ReplicaSetWithPrimary: 'ReplicaSetWithPrimary',
Sharded: 'Sharded',
Unknown: 'Unknown'
};
// Representation of a deployment of servers
class TopologyDescription {
/**
* Create a TopologyDescription
*
* @param {string} topologyType
* @param {Map<string, ServerDescription>} serverDescriptions the a map of address to ServerDescription
* @param {string} setName
* @param {number} maxSetVersion
* @param {ObjectId} maxElectionId
*/
constructor(topologyType, serverDescriptions, setName, maxSetVersion, maxElectionId, options) {
options = options || {};
// TODO: consider assigning all these values to a temporary value `s` which
// we use `Object.freeze` on, ensuring the internal state of this type
// is immutable.
this.type = topologyType || TopologyType.Unknown;
this.setName = setName || null;
this.maxSetVersion = maxSetVersion || null;
this.maxElectionId = maxElectionId || null;
this.servers = serverDescriptions || new Map();
this.stale = false;
this.compatible = true;
this.compatibilityError = null;
this.logicalSessionTimeoutMinutes = null;
this.heartbeatFrequencyMS = options.heartbeatFrequencyMS || 0;
this.localThresholdMS = options.localThresholdMS || 0;
this.options = options;
// determine server compatibility
for (const serverDescription of this.servers.values()) {
if (serverDescription.minWireVersion > MAX_SUPPORTED_WIRE_VERSION) {
this.compatible = false;
this.compatibilityError = `Server at ${serverDescription.address} requires wire version ${
serverDescription.minWireVersion
}, but this version of the driver only supports up to ${MAX_SUPPORTED_WIRE_VERSION}.`;
}
if (serverDescription.maxWireVersion < MIN_SUPPORTED_WIRE_VERSION) {
this.compatible = false;
this.compatibilityError = `Server at ${serverDescription.address} reports wire version ${
serverDescription.maxWireVersion
}, but this version of the driver requires at least ${MIN_SUPPORTED_WIRE_VERSION} (MongoDB ${MIN_SUPPORTED_SERVER_VERSION}).`;
break;
}
}
// Whenever a client updates the TopologyDescription from an ismaster response, it MUST set
// TopologyDescription.logicalSessionTimeoutMinutes to the smallest logicalSessionTimeoutMinutes
// value among ServerDescriptions of all data-bearing server types. If any have a null
// logicalSessionTimeoutMinutes, then TopologyDescription.logicalSessionTimeoutMinutes MUST be
// set to null.
const readableServers = Array.from(this.servers.values()).filter(s => s.isReadable);
this.logicalSessionTimeoutMinutes = readableServers.reduce((result, server) => {
if (server.logicalSessionTimeoutMinutes == null) return null;
if (result == null) return server.logicalSessionTimeoutMinutes;
return Math.min(result, server.logicalSessionTimeoutMinutes);
}, null);
}
/**
* @returns The minimum reported wire version of all known servers
*/
get commonWireVersion() {
return Array.from(this.servers.values())
.filter(server => server.type !== ServerType.Unknown)
.reduce(
(min, server) =>
min == null ? server.maxWireVersion : Math.min(min, server.maxWireVersion),
null
);
}
/**
* Returns a copy of this description updated with a given ServerDescription
*
* @param {ServerDescription} serverDescription
*/
update(serverDescription) {
const address = serverDescription.address;
// NOTE: there are a number of prime targets for refactoring here
// once we support destructuring assignments
// potentially mutated values
let topologyType = this.type;
let setName = this.setName;
let maxSetVersion = this.maxSetVersion;
let maxElectionId = this.maxElectionId;
const serverType = serverDescription.type;
let serverDescriptions = new Map(this.servers);
// update the actual server description
serverDescriptions.set(address, serverDescription);
if (topologyType === TopologyType.Single) {
// once we are defined as single, that never changes
return new TopologyDescription(
TopologyType.Single,
serverDescriptions,
setName,
maxSetVersion,
maxElectionId,
this.options
);
}
if (topologyType === TopologyType.Unknown) {
if (serverType === ServerType.Standalone) {
serverDescriptions.delete(address);
} else {
topologyType = topologyTypeForServerType(serverType);
}
}
if (topologyType === TopologyType.Sharded) {
if ([ServerType.Mongos, ServerType.Unknown].indexOf(serverType) === -1) {
serverDescriptions.delete(address);
}
}
if (topologyType === TopologyType.ReplicaSetNoPrimary) {
if ([ServerType.Mongos, ServerType.Unknown].indexOf(serverType) >= 0) {
serverDescriptions.delete(address);
}
if (serverType === ServerType.RSPrimary) {
const result = updateRsFromPrimary(
serverDescriptions,
setName,
serverDescription,
maxSetVersion,
maxElectionId
);
(topologyType = result[0]),
(setName = result[1]),
(maxSetVersion = result[2]),
(maxElectionId = result[3]);
} else if (
[ServerType.RSSecondary, ServerType.RSArbiter, ServerType.RSOther].indexOf(serverType) >= 0
) {
const result = updateRsNoPrimaryFromMember(serverDescriptions, setName, serverDescription);
(topologyType = result[0]), (setName = result[1]);
}
}
if (topologyType === TopologyType.ReplicaSetWithPrimary) {
if ([ServerType.Standalone, ServerType.Mongos].indexOf(serverType) >= 0) {
serverDescriptions.delete(address);
topologyType = checkHasPrimary(serverDescriptions);
} else if (serverType === ServerType.RSPrimary) {
const result = updateRsFromPrimary(
serverDescriptions,
setName,
serverDescription,
maxSetVersion,
maxElectionId
);
(topologyType = result[0]),
(setName = result[1]),
(maxSetVersion = result[2]),
(maxElectionId = result[3]);
} else if (
[ServerType.RSSecondary, ServerType.RSArbiter, ServerType.RSOther].indexOf(serverType) >= 0
) {
topologyType = updateRsWithPrimaryFromMember(
serverDescriptions,
setName,
serverDescription
);
} else {
topologyType = checkHasPrimary(serverDescriptions);
}
}
return new TopologyDescription(
topologyType,
serverDescriptions,
setName,
maxSetVersion,
maxElectionId,
this.options
);
}
/**
* Determines if the topology has a readable server available. See the table in the
* following section for behaviour rules.
*
* @param {ReadPreference} [readPreference] An optional read preference for determining if a readable server is present
* @return {Boolean} Whether there is a readable server in this topology
*/
hasReadableServer(/* readPreference */) {
// To be implemented when server selection is implemented
}
/**
* Determines if the topology has a writable server available. See the table in the
* following section for behaviour rules.
*
* @return {Boolean} Whether there is a writable server in this topology
*/
hasWritableServer() {
return this.hasReadableServer(ReadPreference.primary);
}
/**
* Determines if the topology has a definition for the provided address
*
* @param {String} address
* @return {Boolean} Whether the topology knows about this server
*/
hasServer(address) {
return this.servers.has(address);
}
}
function topologyTypeForServerType(serverType) {
if (serverType === ServerType.Mongos) return TopologyType.Sharded;
if (serverType === ServerType.RSPrimary) return TopologyType.ReplicaSetWithPrimary;
return TopologyType.ReplicaSetNoPrimary;
}
function updateRsFromPrimary(
serverDescriptions,
setName,
serverDescription,
maxSetVersion,
maxElectionId
) {
setName = setName || serverDescription.setName;
if (setName !== serverDescription.setName) {
serverDescriptions.delete(serverDescription.address);
return [checkHasPrimary(serverDescriptions), setName, maxSetVersion, maxElectionId];
}
const electionIdOID = serverDescription.electionId ? serverDescription.electionId.$oid : null;
const maxElectionIdOID = maxElectionId ? maxElectionId.$oid : null;
if (serverDescription.setVersion != null && electionIdOID != null) {
if (maxSetVersion != null && maxElectionIdOID != null) {
if (maxSetVersion > serverDescription.setVersion || maxElectionIdOID > electionIdOID) {
// this primary is stale, we must remove it
serverDescriptions.set(
serverDescription.address,
new ServerDescription(serverDescription.address)
);
return [checkHasPrimary(serverDescriptions), setName, maxSetVersion, maxElectionId];
}
}
maxElectionId = serverDescription.electionId;
}
if (
serverDescription.setVersion != null &&
(maxSetVersion == null || serverDescription.setVersion > maxSetVersion)
) {
maxSetVersion = serverDescription.setVersion;
}
// We've heard from the primary. Is it the same primary as before?
for (const address of serverDescriptions.keys()) {
const server = serverDescriptions.get(address);
if (server.type === ServerType.RSPrimary && server.address !== serverDescription.address) {
// Reset old primary's type to Unknown.
serverDescriptions.set(address, new ServerDescription(server.address));
// There can only be one primary
break;
}
}
// Discover new hosts from this primary's response.
serverDescription.allHosts.forEach(address => {
if (!serverDescriptions.has(address)) {
serverDescriptions.set(address, new ServerDescription(address));
}
});
// Remove hosts not in the response.
const currentAddresses = Array.from(serverDescriptions.keys());
const responseAddresses = serverDescription.allHosts;
currentAddresses.filter(addr => responseAddresses.indexOf(addr) === -1).forEach(address => {
serverDescriptions.delete(address);
});
return [checkHasPrimary(serverDescriptions), setName, maxSetVersion, maxElectionId];
}
function updateRsWithPrimaryFromMember(serverDescriptions, setName, serverDescription) {
if (setName == null) {
throw new TypeError('setName is required');
}
if (
setName !== serverDescription.setName ||
(serverDescription.me && serverDescription.address !== serverDescription.me)
) {
serverDescriptions.delete(serverDescription.address);
}
return checkHasPrimary(serverDescriptions);
}
function updateRsNoPrimaryFromMember(serverDescriptions, setName, serverDescription) {
let topologyType = TopologyType.ReplicaSetNoPrimary;
setName = setName || serverDescription.setName;
if (setName !== serverDescription.setName) {
serverDescriptions.delete(serverDescription.address);
return [topologyType, setName];
}
serverDescription.allHosts.forEach(address => {
if (!serverDescriptions.has(address)) {
serverDescriptions.set(address, new ServerDescription(address));
}
});
if (serverDescription.me && serverDescription.address !== serverDescription.me) {
serverDescriptions.delete(serverDescription.address);
}
return [topologyType, setName];
}
function checkHasPrimary(serverDescriptions) {
for (const addr of serverDescriptions.keys()) {
if (serverDescriptions.get(addr).type === ServerType.RSPrimary) {
return TopologyType.ReplicaSetWithPrimary;
}
}
return TopologyType.ReplicaSetNoPrimary;
}
module.exports = {
TopologyType,
TopologyDescription
};

459
node_modules/mongodb-core/lib/sessions.js generated vendored Normal file
View File

@@ -0,0 +1,459 @@
'use strict';
const retrieveBSON = require('./connection/utils').retrieveBSON;
const EventEmitter = require('events');
const BSON = retrieveBSON();
const Binary = BSON.Binary;
const uuidV4 = require('./utils').uuidV4;
const MongoError = require('./error').MongoError;
const isRetryableError = require('././error').isRetryableError;
const MongoNetworkError = require('./error').MongoNetworkError;
const MongoWriteConcernError = require('./error').MongoWriteConcernError;
const Transaction = require('./transactions').Transaction;
const TxnState = require('./transactions').TxnState;
function assertAlive(session, callback) {
if (session.serverSession == null) {
const error = new MongoError('Cannot use a session that has ended');
if (typeof callback === 'function') {
callback(error, null);
return false;
}
throw error;
}
return true;
}
/**
* Options to pass when creating a Client Session
* @typedef {Object} SessionOptions
* @property {boolean} [causalConsistency=true] Whether causal consistency should be enabled on this session
* @property {TransactionOptions} [defaultTransactionOptions] The default TransactionOptions to use for transactions started on this session.
*/
/**
* A BSON document reflecting the lsid of a {@link ClientSession}
* @typedef {Object} SessionId
*/
/**
* A class representing a client session on the server
* WARNING: not meant to be instantiated directly.
* @class
* @hideconstructor
*/
class ClientSession extends EventEmitter {
/**
* Create a client session.
* WARNING: not meant to be instantiated directly
*
* @param {Topology} topology The current client's topology (Internal Class)
* @param {ServerSessionPool} sessionPool The server session pool (Internal Class)
* @param {SessionOptions} [options] Optional settings
* @param {Object} [clientOptions] Optional settings provided when creating a client in the porcelain driver
*/
constructor(topology, sessionPool, options, clientOptions) {
super();
if (topology == null) {
throw new Error('ClientSession requires a topology');
}
if (sessionPool == null || !(sessionPool instanceof ServerSessionPool)) {
throw new Error('ClientSession requires a ServerSessionPool');
}
options = options || {};
this.topology = topology;
this.sessionPool = sessionPool;
this.hasEnded = false;
this.serverSession = sessionPool.acquire();
this.clientOptions = clientOptions;
this.supports = {
causalConsistency:
typeof options.causalConsistency !== 'undefined' ? options.causalConsistency : true
};
options = options || {};
if (typeof options.initialClusterTime !== 'undefined') {
this.clusterTime = options.initialClusterTime;
} else {
this.clusterTime = null;
}
this.operationTime = null;
this.explicit = !!options.explicit;
this.owner = options.owner;
this.defaultTransactionOptions = Object.assign({}, options.defaultTransactionOptions);
this.transaction = new Transaction();
}
/**
* The server id associated with this session
* @type {SessionId}
*/
get id() {
return this.serverSession.id;
}
/**
* Ends this session on the server
*
* @param {Object} [options] Optional settings. Currently reserved for future use
* @param {Function} [callback] Optional callback for completion of this operation
*/
endSession(options, callback) {
if (typeof options === 'function') (callback = options), (options = {});
options = options || {};
if (this.hasEnded) {
if (typeof callback === 'function') callback(null, null);
return;
}
if (this.serverSession && this.inTransaction()) {
this.abortTransaction(); // pass in callback?
}
// mark the session as ended, and emit a signal
this.hasEnded = true;
this.emit('ended', this);
// release the server session back to the pool
this.sessionPool.release(this.serverSession);
// spec indicates that we should ignore all errors for `endSessions`
if (typeof callback === 'function') callback(null, null);
}
/**
* Advances the operationTime for a ClientSession.
*
* @param {Timestamp} operationTime the `BSON.Timestamp` of the operation type it is desired to advance to
*/
advanceOperationTime(operationTime) {
if (this.operationTime == null) {
this.operationTime = operationTime;
return;
}
if (operationTime.greaterThan(this.operationTime)) {
this.operationTime = operationTime;
}
}
/**
* Used to determine if this session equals another
* @param {ClientSession} session
* @return {boolean} true if the sessions are equal
*/
equals(session) {
if (!(session instanceof ClientSession)) {
return false;
}
return this.id.id.buffer.equals(session.id.id.buffer);
}
/**
* Increment the transaction number on the internal ServerSession
*/
incrementTransactionNumber() {
this.serverSession.txnNumber++;
}
/**
* @returns {boolean} whether this session is currently in a transaction or not
*/
inTransaction() {
return this.transaction.isActive;
}
/**
* Starts a new transaction with the given options.
*
* @param {TransactionOptions} options Options for the transaction
*/
startTransaction(options) {
assertAlive(this);
if (this.inTransaction()) {
throw new MongoError('Transaction already in progress');
}
// increment txnNumber
this.incrementTransactionNumber();
// create transaction state
this.transaction = new Transaction(
Object.assign({}, this.clientOptions, options || this.defaultTransactionOptions)
);
this.transaction.transition(TxnState.STARTING_TRANSACTION);
}
/**
* Commits the currently active transaction in this session.
*
* @param {Function} [callback] optional callback for completion of this operation
* @return {Promise} A promise is returned if no callback is provided
*/
commitTransaction(callback) {
if (typeof callback === 'function') {
endTransaction(this, 'commitTransaction', callback);
return;
}
return new Promise((resolve, reject) => {
endTransaction(
this,
'commitTransaction',
(err, reply) => (err ? reject(err) : resolve(reply))
);
});
}
/**
* Aborts the currently active transaction in this session.
*
* @param {Function} [callback] optional callback for completion of this operation
* @return {Promise} A promise is returned if no callback is provided
*/
abortTransaction(callback) {
if (typeof callback === 'function') {
endTransaction(this, 'abortTransaction', callback);
return;
}
return new Promise((resolve, reject) => {
endTransaction(
this,
'abortTransaction',
(err, reply) => (err ? reject(err) : resolve(reply))
);
});
}
/**
* This is here to ensure that ClientSession is never serialized to BSON.
* @ignore
*/
toBSON() {
throw new Error('ClientSession cannot be serialized to BSON.');
}
}
function endTransaction(session, commandName, callback) {
if (!assertAlive(session, callback)) {
// checking result in case callback was called
return;
}
// handle any initial problematic cases
let txnState = session.transaction.state;
if (txnState === TxnState.NO_TRANSACTION) {
callback(new MongoError('No transaction started'));
return;
}
if (commandName === 'commitTransaction') {
if (
txnState === TxnState.STARTING_TRANSACTION ||
txnState === TxnState.TRANSACTION_COMMITTED_EMPTY
) {
// the transaction was never started, we can safely exit here
session.transaction.transition(TxnState.TRANSACTION_COMMITTED_EMPTY);
callback(null, null);
return;
}
if (txnState === TxnState.TRANSACTION_ABORTED) {
callback(new MongoError('Cannot call commitTransaction after calling abortTransaction'));
return;
}
} else {
if (txnState === TxnState.STARTING_TRANSACTION) {
// the transaction was never started, we can safely exit here
session.transaction.transition(TxnState.TRANSACTION_ABORTED);
callback(null, null);
return;
}
if (txnState === TxnState.TRANSACTION_ABORTED) {
callback(new MongoError('Cannot call abortTransaction twice'));
return;
}
if (
txnState === TxnState.TRANSACTION_COMMITTED ||
txnState === TxnState.TRANSACTION_COMMITTED_EMPTY
) {
callback(new MongoError('Cannot call abortTransaction after calling commitTransaction'));
return;
}
}
// construct and send the command
const command = { [commandName]: 1 };
// apply a writeConcern if specified
if (session.transaction.options.writeConcern) {
Object.assign(command, { writeConcern: session.transaction.options.writeConcern });
} else if (session.clientOptions && session.clientOptions.w) {
Object.assign(command, { writeConcern: { w: session.clientOptions.w } });
}
function commandHandler(e, r) {
if (commandName === 'commitTransaction') {
session.transaction.transition(TxnState.TRANSACTION_COMMITTED);
if (
e &&
(e instanceof MongoNetworkError ||
e instanceof MongoWriteConcernError ||
isRetryableError(e))
) {
if (e.errorLabels) {
const idx = e.errorLabels.indexOf('TransientTransactionError');
if (idx !== -1) {
e.errorLabels.splice(idx, 1);
}
} else {
e.errorLabels = [];
}
e.errorLabels.push('UnknownTransactionCommitResult');
}
} else {
session.transaction.transition(TxnState.TRANSACTION_ABORTED);
}
callback(e, r);
}
// The spec indicates that we should ignore all errors on `abortTransaction`
function transactionError(err) {
return commandName === 'commitTransaction' ? err : null;
}
// send the command
session.topology.command('admin.$cmd', command, { session }, (err, reply) => {
if (err && isRetryableError(err)) {
return session.topology.command('admin.$cmd', command, { session }, (_err, _reply) =>
commandHandler(transactionError(_err), _reply)
);
}
commandHandler(transactionError(err), reply);
});
}
/**
* Reflects the existence of a session on the server. Can be reused by the session pool.
* WARNING: not meant to be instantiated directly. For internal use only.
* @ignore
*/
class ServerSession {
constructor() {
this.id = { id: new Binary(uuidV4(), Binary.SUBTYPE_UUID) };
this.lastUse = Date.now();
this.txnNumber = 0;
}
/**
* Determines if the server session has timed out.
* @ignore
* @param {Date} sessionTimeoutMinutes The server's "logicalSessionTimeoutMinutes"
* @return {boolean} true if the session has timed out.
*/
hasTimedOut(sessionTimeoutMinutes) {
// Take the difference of the lastUse timestamp and now, which will result in a value in
// milliseconds, and then convert milliseconds to minutes to compare to `sessionTimeoutMinutes`
const idleTimeMinutes = Math.round(
(((Date.now() - this.lastUse) % 86400000) % 3600000) / 60000
);
return idleTimeMinutes > sessionTimeoutMinutes - 1;
}
}
/**
* Maintains a pool of Server Sessions.
* For internal use only
* @ignore
*/
class ServerSessionPool {
constructor(topology) {
if (topology == null) {
throw new Error('ServerSessionPool requires a topology');
}
this.topology = topology;
this.sessions = [];
}
/**
* Ends all sessions in the session pool.
* @ignore
*/
endAllPooledSessions() {
if (this.sessions.length) {
this.topology.endSessions(this.sessions.map(session => session.id));
this.sessions = [];
}
}
/**
* Acquire a Server Session from the pool.
* Iterates through each session in the pool, removing any stale sessions
* along the way. The first non-stale session found is removed from the
* pool and returned. If no non-stale session is found, a new ServerSession
* is created.
* @ignore
* @returns {ServerSession}
*/
acquire() {
const sessionTimeoutMinutes = this.topology.logicalSessionTimeoutMinutes;
while (this.sessions.length) {
const session = this.sessions.shift();
if (!session.hasTimedOut(sessionTimeoutMinutes)) {
return session;
}
}
return new ServerSession();
}
/**
* Release a session to the session pool
* Adds the session back to the session pool if the session has not timed out yet.
* This method also removes any stale sessions from the pool.
* @ignore
* @param {ServerSession} session The session to release to the pool
*/
release(session) {
const sessionTimeoutMinutes = this.topology.logicalSessionTimeoutMinutes;
while (this.sessions.length) {
const session = this.sessions[this.sessions.length - 1];
if (session.hasTimedOut(sessionTimeoutMinutes)) {
this.sessions.pop();
} else {
break;
}
}
if (!session.hasTimedOut(sessionTimeoutMinutes)) {
this.sessions.unshift(session);
}
}
}
module.exports = {
ClientSession,
ServerSession,
ServerSessionPool,
TxnState
};

61
node_modules/mongodb-core/lib/tools/smoke_plugin.js generated vendored Normal file
View File

@@ -0,0 +1,61 @@
'use strict';
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;
};

1504
node_modules/mongodb-core/lib/topologies/mongos.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,193 @@
'use strict';
/**
* The **ReadPreference** class is a class that represents a MongoDB ReadPreference and is
* used to construct connections.
* @class
* @param {string} mode A string describing the read preference mode (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 staleness in seconds, Minimum value is 90 seconds.
* @return {ReadPreference}
* @example
* const ReplSet = require('mongodb-core').ReplSet,
* ReadPreference = require('mongodb-core').ReadPreference,
* assert = require('assert');
*
* const server = new ReplSet([{host: 'localhost', port: 30000}], {setName: 'rs'});
* // Wait for the connection event
* server.on('connect', function(server) {
* const cursor = server.cursor(
* 'db.test',
* { find: 'db.test', query: {} },
* { readPreference: new ReadPreference('secondary') }
* );
*
* cursor.next(function(err, doc) {
* server.destroy();
* });
* });
*
* // Start connecting
* server.connect();
* @see https://docs.mongodb.com/manual/core/read-preference/
*/
const ReadPreference = function(mode, tags, options) {
// TODO(major): tags MUST be an array of tagsets
if (tags && !Array.isArray(tags)) {
console.warn(
'ReadPreference tags must be an array, this will change in the next major version'
);
if (typeof tags.maxStalenessSeconds !== 'undefined') {
// this is likely an options object
options = tags;
tags = undefined;
} else {
tags = [tags];
}
}
this.mode = mode;
this.tags = tags;
options = options || {};
if (options.maxStalenessSeconds != null) {
if (options.maxStalenessSeconds <= 0) {
throw new TypeError('maxStalenessSeconds must be a positive integer');
}
this.maxStalenessSeconds = options.maxStalenessSeconds;
// NOTE: The minimum required wire version is 5 for this read preference. If the existing
// topology has a lower value then a MongoError will be thrown during server selection.
this.minWireVersion = 5;
}
if (this.mode === ReadPreference.PRIMARY || this.mode === true) {
if (this.tags && Array.isArray(this.tags) && this.tags.length > 0) {
throw new TypeError('Primary read preference cannot be combined with tags');
}
if (this.maxStalenessSeconds) {
throw new TypeError('Primary read preference cannot be combined with maxStalenessSeconds');
}
}
};
// Support the deprecated `preference` property introduced in the porcelain layer
Object.defineProperty(ReadPreference.prototype, 'preference', {
enumerable: true,
get: function() {
return this.mode;
}
});
/*
* Read preference mode constants
*/
ReadPreference.PRIMARY = 'primary';
ReadPreference.PRIMARY_PREFERRED = 'primaryPreferred';
ReadPreference.SECONDARY = 'secondary';
ReadPreference.SECONDARY_PREFERRED = 'secondaryPreferred';
ReadPreference.NEAREST = 'nearest';
const VALID_MODES = [
ReadPreference.PRIMARY,
ReadPreference.PRIMARY_PREFERRED,
ReadPreference.SECONDARY,
ReadPreference.SECONDARY_PREFERRED,
ReadPreference.NEAREST,
true,
false,
null
];
/**
* Validate if a mode is legal
*
* @method
* @param {string} mode The string representing the read preference mode.
* @return {boolean} True if a mode is valid
*/
ReadPreference.isValid = function(mode) {
return VALID_MODES.indexOf(mode) !== -1;
};
/**
* Validate if a mode is legal
*
* @method
* @param {string} mode The string representing the read preference mode.
* @return {boolean} True if a mode is valid
*/
ReadPreference.prototype.isValid = function(mode) {
return ReadPreference.isValid(typeof mode === 'string' ? mode : this.mode);
};
const needSlaveOk = ['primaryPreferred', 'secondary', 'secondaryPreferred', 'nearest'];
/**
* Indicates that this readPreference needs the "slaveOk" bit when sent over the wire
* @method
* @return {boolean}
* @see https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/#op-query
*/
ReadPreference.prototype.slaveOk = function() {
return needSlaveOk.indexOf(this.mode) !== -1;
};
/**
* Are the two read preference equal
* @method
* @param {ReadPreference} readPreference The read preference with which to check equality
* @return {boolean} True if the two ReadPreferences are equivalent
*/
ReadPreference.prototype.equals = function(readPreference) {
return readPreference.mode === this.mode;
};
/**
* Return JSON representation
* @method
* @return {Object} A JSON representation of the ReadPreference
*/
ReadPreference.prototype.toJSON = function() {
const readPreference = { mode: this.mode };
if (Array.isArray(this.tags)) readPreference.tags = this.tags;
if (this.maxStalenessSeconds) readPreference.maxStalenessSeconds = this.maxStalenessSeconds;
return readPreference;
};
/**
* Primary read preference
* @member
* @type {ReadPreference}
*/
ReadPreference.primary = new ReadPreference('primary');
/**
* Primary Preferred read preference
* @member
* @type {ReadPreference}
*/
ReadPreference.primaryPreferred = new ReadPreference('primaryPreferred');
/**
* Secondary read preference
* @member
* @type {ReadPreference}
*/
ReadPreference.secondary = new ReadPreference('secondary');
/**
* Secondary Preferred read preference
* @member
* @type {ReadPreference}
*/
ReadPreference.secondaryPreferred = new ReadPreference('secondaryPreferred');
/**
* Nearest read preference
* @member
* @type {ReadPreference}
*/
ReadPreference.nearest = new ReadPreference('nearest');
module.exports = ReadPreference;

1724
node_modules/mongodb-core/lib/topologies/replset.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

1099
node_modules/mongodb-core/lib/topologies/replset_state.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

1117
node_modules/mongodb-core/lib/topologies/server.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

434
node_modules/mongodb-core/lib/topologies/shared.js generated vendored Normal file
View File

@@ -0,0 +1,434 @@
'use strict';
const os = require('os');
const f = require('util').format;
const ReadPreference = require('./read_preference');
const Buffer = require('safe-buffer').Buffer;
/**
* 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 = Buffer.from(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 createCompressionInfo(options) {
if (!options.compression || !options.compression.compressors) {
return [];
}
// Check that all supplied compressors are valid
options.compression.compressors.forEach(function(compressor) {
if (compressor !== 'snappy' && compressor !== 'zlib') {
throw new Error('compressors must be at least one of snappy or zlib');
}
});
return options.compression.compressors;
}
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(fn, 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 (j = 0; j < current.servers.length; j++) {
found = false;
// Go over all the previous servers
for (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 (i = 0; i < previous.servers.length; i++) {
var prevServer = previous.servers[i];
// Go through all current servers
for (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;
}
/**
* Shared function to determine clusterTime for a given topology
*
* @param {*} topology
* @param {*} clusterTime
*/
function resolveClusterTime(topology, $clusterTime) {
if (topology.clusterTime == null) {
topology.clusterTime = $clusterTime;
} else {
if ($clusterTime.clusterTime.greaterThan(topology.clusterTime.clusterTime)) {
topology.clusterTime = $clusterTime;
}
}
}
// NOTE: this is a temporary move until the topologies can be more formally refactored
// to share code.
const SessionMixins = {
endSessions: function(sessions, callback) {
if (!Array.isArray(sessions)) {
sessions = [sessions];
}
// TODO:
// When connected to a sharded cluster the endSessions command
// can be sent to any mongos. When connected to a replica set the
// endSessions command MUST be sent to the primary if the primary
// is available, otherwise it MUST be sent to any available secondary.
// Is it enough to use: ReadPreference.primaryPreferred ?
this.command(
'admin.$cmd',
{ endSessions: sessions },
{ readPreference: ReadPreference.primaryPreferred },
() => {
// intentionally ignored, per spec
if (typeof callback === 'function') callback();
}
);
}
};
const RETRYABLE_WIRE_VERSION = 6;
/**
* Determines whether the provided topology supports retryable writes
*
* @param {Mongos|Replset} topology
*/
const isRetryableWritesSupported = function(topology) {
const maxWireVersion = topology.lastIsMaster().maxWireVersion;
if (maxWireVersion < RETRYABLE_WIRE_VERSION) {
return false;
}
if (!topology.logicalSessionTimeoutMinutes) {
return false;
}
return true;
};
module.exports.SessionMixins = SessionMixins;
module.exports.resolveClusterTime = resolveClusterTime;
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.createCompressionInfo = createCompressionInfo;
module.exports.clone = clone;
module.exports.diff = diff;
module.exports.Interval = Interval;
module.exports.Timeout = Timeout;
module.exports.isRetryableWritesSupported = isRetryableWritesSupported;

134
node_modules/mongodb-core/lib/transactions.js generated vendored Normal file
View File

@@ -0,0 +1,134 @@
'use strict';
const MongoError = require('./error').MongoError;
let TxnState;
let stateMachine;
(() => {
const NO_TRANSACTION = 'NO_TRANSACTION';
const STARTING_TRANSACTION = 'STARTING_TRANSACTION';
const TRANSACTION_IN_PROGRESS = 'TRANSACTION_IN_PROGRESS';
const TRANSACTION_COMMITTED = 'TRANSACTION_COMMITTED';
const TRANSACTION_COMMITTED_EMPTY = 'TRANSACTION_COMMITTED_EMPTY';
const TRANSACTION_ABORTED = 'TRANSACTION_ABORTED';
TxnState = {
NO_TRANSACTION,
STARTING_TRANSACTION,
TRANSACTION_IN_PROGRESS,
TRANSACTION_COMMITTED,
TRANSACTION_COMMITTED_EMPTY,
TRANSACTION_ABORTED
};
stateMachine = {
[NO_TRANSACTION]: [NO_TRANSACTION, STARTING_TRANSACTION],
[STARTING_TRANSACTION]: [
TRANSACTION_IN_PROGRESS,
TRANSACTION_COMMITTED,
TRANSACTION_COMMITTED_EMPTY,
TRANSACTION_ABORTED
],
[TRANSACTION_IN_PROGRESS]: [
TRANSACTION_IN_PROGRESS,
TRANSACTION_COMMITTED,
TRANSACTION_ABORTED
],
[TRANSACTION_COMMITTED]: [
TRANSACTION_COMMITTED,
TRANSACTION_COMMITTED_EMPTY,
STARTING_TRANSACTION,
NO_TRANSACTION
],
[TRANSACTION_ABORTED]: [STARTING_TRANSACTION, NO_TRANSACTION],
[TRANSACTION_COMMITTED_EMPTY]: [TRANSACTION_COMMITTED_EMPTY, NO_TRANSACTION]
};
})();
/**
* The MongoDB ReadConcern, which allows for control of the consistency and isolation properties
* of the data read from replica sets and replica set shards.
* @typedef {Object} ReadConcern
* @property {'local'|'available'|'majority'|'linearizable'|'snapshot'} level The readConcern Level
* @see https://docs.mongodb.com/manual/reference/read-concern/
*/
/**
* A MongoDB WriteConcern, which describes the level of acknowledgement
* requested from MongoDB for write operations.
* @typedef {Object} WriteConcern
* @property {number|'majority'|string} [w=1] requests acknowledgement that the write operation has
* propagated to a specified number of mongod hosts
* @property {boolean} [j=false] requests acknowledgement from MongoDB that the write operation has
* been written to the journal
* @property {number} [wtimeout] a time limit, in milliseconds, for the write concern
* @see https://docs.mongodb.com/manual/reference/write-concern/
*/
/**
* Configuration options for a transaction.
* @typedef {Object} TransactionOptions
* @property {ReadConcern} [readConcern] A default read concern for commands in this transaction
* @property {WriteConcern} [writeConcern] A default writeConcern for commands in this transaction
* @property {ReadPreference} [readPreference] A default read preference for commands in this transaction
*/
/**
* A class maintaining state related to a server transaction. Internal Only
* @ignore
*/
class Transaction {
/**
* Create a transaction
*
* @ignore
* @param {TransactionOptions} [options] Optional settings
*/
constructor(options) {
options = options || {};
this.state = TxnState.NO_TRANSACTION;
this.options = {};
if (options.writeConcern || typeof options.w !== 'undefined') {
const w = options.writeConcern ? options.writeConcern.w : options.w;
if (w <= 0) {
throw new MongoError('Transactions do not support unacknowledged write concern');
}
this.options.writeConcern = options.writeConcern ? options.writeConcern : { w: options.w };
}
if (options.readConcern) this.options.readConcern = options.readConcern;
if (options.readPreference) this.options.readPreference = options.readPreference;
}
/**
* @ignore
* @return Whether this session is presently in a transaction
*/
get isActive() {
return (
[TxnState.STARTING_TRANSACTION, TxnState.TRANSACTION_IN_PROGRESS].indexOf(this.state) !== -1
);
}
/**
* Transition the transaction in the state machine
* @ignore
* @param {TxnState} state The new state to transition to
*/
transition(nextState) {
const nextStates = stateMachine[this.state];
if (nextStates && nextStates.indexOf(nextState) !== -1) {
this.state = nextState;
return;
}
throw new MongoError(
`Attempted illegal state transition from [${this.state}] to [${nextState}]`
);
}
}
module.exports = { TxnState, Transaction };

536
node_modules/mongodb-core/lib/uri_parser.js generated vendored Normal file
View File

@@ -0,0 +1,536 @@
'use strict';
const URL = require('url');
const qs = require('querystring');
const dns = require('dns');
const MongoParseError = require('./error').MongoParseError;
const ReadPreference = require('./topologies/read_preference');
/**
* The following regular expression validates a connection string and breaks the
* provide string into the following capture groups: [protocol, username, password, hosts]
*/
const HOSTS_RX = /(mongodb(?:\+srv|)):\/\/(?: (?:[^:]*) (?: : ([^@]*) )? @ )?([^/?]*)(?:\/|)(.*)/;
/**
* Determines whether a provided address matches the provided parent domain in order
* to avoid certain attack vectors.
*
* @param {String} srvAddress The address to check against a domain
* @param {String} parentDomain The domain to check the provided address against
* @return {Boolean} Whether the provided address matches the parent domain
*/
function matchesParentDomain(srvAddress, parentDomain) {
const regex = /^.*?\./;
const srv = `.${srvAddress.replace(regex, '')}`;
const parent = `.${parentDomain.replace(regex, '')}`;
return srv.endsWith(parent);
}
/**
* Lookup a `mongodb+srv` connection string, combine the parts and reparse it as a normal
* connection string.
*
* @param {string} uri The connection string to parse
* @param {object} options Optional user provided connection string options
* @param {function} callback
*/
function parseSrvConnectionString(uri, options, callback) {
const result = URL.parse(uri, true);
if (result.hostname.split('.').length < 3) {
return callback(new MongoParseError('URI does not have hostname, domain name and tld'));
}
result.domainLength = result.hostname.split('.').length;
if (result.pathname && result.pathname.match(',')) {
return callback(new MongoParseError('Invalid URI, cannot contain multiple hostnames'));
}
if (result.port) {
return callback(new MongoParseError(`Ports not accepted with '${PROTOCOL_MONGODB_SRV}' URIs`));
}
// Resolve the SRV record and use the result as the list of hosts to connect to.
const lookupAddress = result.host;
dns.resolveSrv(`_mongodb._tcp.${lookupAddress}`, (err, addresses) => {
if (err) return callback(err);
if (addresses.length === 0) {
return callback(new MongoParseError('No addresses found at host'));
}
for (let i = 0; i < addresses.length; i++) {
if (!matchesParentDomain(addresses[i].name, result.hostname, result.domainLength)) {
return callback(
new MongoParseError('Server record does not share hostname with parent URI')
);
}
}
// Convert the original URL to a non-SRV URL.
result.protocol = 'mongodb';
result.host = addresses.map(address => `${address.name}:${address.port}`).join(',');
// Default to SSL true if it's not specified.
if (
!('ssl' in options) &&
(!result.search || !('ssl' in result.query) || result.query.ssl === null)
) {
result.query.ssl = true;
}
// Resolve TXT record and add options from there if they exist.
dns.resolveTxt(lookupAddress, (err, record) => {
if (err) {
if (err.code !== 'ENODATA') {
return callback(err);
}
record = null;
}
if (record) {
if (record.length > 1) {
return callback(new MongoParseError('Multiple text records not allowed'));
}
record = qs.parse(record[0].join(''));
if (Object.keys(record).some(key => key !== 'authSource' && key !== 'replicaSet')) {
return callback(
new MongoParseError('Text record must only set `authSource` or `replicaSet`')
);
}
Object.assign(result.query, record);
}
// Set completed options back into the URL object.
result.search = qs.stringify(result.query);
const finalString = URL.format(result);
parseConnectionString(finalString, options, callback);
});
});
}
/**
* Parses a query string item according to the connection string spec
*
* @param {string} key The key for the parsed value
* @param {Array|String} value The value to parse
* @return {Array|Object|String} The parsed value
*/
function parseQueryStringItemValue(key, value) {
if (Array.isArray(value)) {
// deduplicate and simplify arrays
value = value.filter((v, idx) => value.indexOf(v) === idx);
if (value.length === 1) value = value[0];
} else if (value.indexOf(':') > 0) {
value = value.split(',').reduce((result, pair) => {
const parts = pair.split(':');
result[parts[0]] = parseQueryStringItemValue(key, parts[1]);
return result;
}, {});
} else if (value.indexOf(',') > 0) {
value = value.split(',').map(v => {
return parseQueryStringItemValue(key, v);
});
} else if (value.toLowerCase() === 'true' || value.toLowerCase() === 'false') {
value = value.toLowerCase() === 'true';
} else if (!Number.isNaN(value) && !STRING_OPTIONS.has(key)) {
const numericValue = parseFloat(value);
if (!Number.isNaN(numericValue)) {
value = parseFloat(value);
}
}
return value;
}
// Options that are known boolean types
const BOOLEAN_OPTIONS = new Set([
'slaveok',
'slave_ok',
'sslvalidate',
'fsync',
'safe',
'retrywrites',
'j'
]);
// Known string options, only used to bypass Number coercion in `parseQueryStringItemValue`
const STRING_OPTIONS = new Set(['authsource', 'replicaset']);
// Supported text representations of auth mechanisms
// NOTE: this list exists in native already, if it is merged here we should deduplicate
const AUTH_MECHANISMS = new Set([
'GSSAPI',
'MONGODB-X509',
'MONGODB-CR',
'DEFAULT',
'SCRAM-SHA-1',
'SCRAM-SHA-256',
'PLAIN'
]);
// Lookup table used to translate normalized (lower-cased) forms of connection string
// options to their expected camelCase version
const CASE_TRANSLATION = {
replicaset: 'replicaSet',
connecttimeoutms: 'connectTimeoutMS',
sockettimeoutms: 'socketTimeoutMS',
maxpoolsize: 'maxPoolSize',
minpoolsize: 'minPoolSize',
maxidletimems: 'maxIdleTimeMS',
waitqueuemultiple: 'waitQueueMultiple',
waitqueuetimeoutms: 'waitQueueTimeoutMS',
wtimeoutms: 'wtimeoutMS',
readconcern: 'readConcern',
readconcernlevel: 'readConcernLevel',
readpreference: 'readPreference',
maxstalenessseconds: 'maxStalenessSeconds',
readpreferencetags: 'readPreferenceTags',
authsource: 'authSource',
authmechanism: 'authMechanism',
authmechanismproperties: 'authMechanismProperties',
gssapiservicename: 'gssapiServiceName',
localthresholdms: 'localThresholdMS',
serverselectiontimeoutms: 'serverSelectionTimeoutMS',
serverselectiontryonce: 'serverSelectionTryOnce',
heartbeatfrequencyms: 'heartbeatFrequencyMS',
appname: 'appName',
retrywrites: 'retryWrites',
uuidrepresentation: 'uuidRepresentation',
zlibcompressionlevel: 'zlibCompressionLevel'
};
/**
* Sets the value for `key`, allowing for any required translation
*
* @param {object} obj The object to set the key on
* @param {string} key The key to set the value for
* @param {*} value The value to set
* @param {object} options The options used for option parsing
*/
function applyConnectionStringOption(obj, key, value, options) {
// simple key translation
if (key === 'journal') {
key = 'j';
} else if (key === 'wtimeoutms') {
key = 'wtimeout';
}
// more complicated translation
if (BOOLEAN_OPTIONS.has(key)) {
value = value === 'true' || value === true;
} else if (key === 'appname') {
value = decodeURIComponent(value);
} else if (key === 'readconcernlevel') {
key = 'readconcern';
value = { level: value };
}
// simple validation
if (key === 'compressors') {
value = Array.isArray(value) ? value : [value];
if (!value.every(c => c === 'snappy' || c === 'zlib')) {
throw new MongoParseError(
'Value for `compressors` must be at least one of: `snappy`, `zlib`'
);
}
}
if (key === 'authmechanism' && !AUTH_MECHANISMS.has(value)) {
throw new MongoParseError(
'Value for `authMechanism` must be one of: `DEFAULT`, `GSSAPI`, `PLAIN`, `MONGODB-X509`, `SCRAM-SHA-1`, `SCRAM-SHA-256`'
);
}
if (key === 'readpreference' && !ReadPreference.isValid(value)) {
throw new MongoParseError(
'Value for `readPreference` must be one of: `primary`, `primaryPreferred`, `secondary`, `secondaryPreferred`, `nearest`'
);
}
if (key === 'zlibcompressionlevel' && (value < -1 || value > 9)) {
throw new MongoParseError('zlibCompressionLevel must be an integer between -1 and 9');
}
// special cases
if (key === 'compressors' || key === 'zlibcompressionlevel') {
obj.compression = obj.compression || {};
obj = obj.compression;
}
if (key === 'authmechanismproperties') {
if (typeof value.SERVICE_NAME === 'string') obj.gssapiServiceName = value.SERVICE_NAME;
if (typeof value.SERVICE_REALM === 'string') obj.gssapiServiceRealm = value.SERVICE_REALM;
if (typeof value.CANONICALIZE_HOST_NAME !== 'undefined') {
obj.gssapiCanonicalizeHostName = value.CANONICALIZE_HOST_NAME;
}
}
// set the actual value
if (options.caseTranslate && CASE_TRANSLATION[key]) {
obj[CASE_TRANSLATION[key]] = value;
return;
}
obj[key] = value;
}
const USERNAME_REQUIRED_MECHANISMS = new Set([
'GSSAPI',
'MONGODB-CR',
'PLAIN',
'SCRAM-SHA-1',
'SCRAM-SHA-256'
]);
/**
* Modifies the parsed connection string object taking into account expectations we
* have for authentication-related options.
*
* @param {object} parsed The parsed connection string result
* @return The parsed connection string result possibly modified for auth expectations
*/
function applyAuthExpectations(parsed) {
if (parsed.options == null) {
return;
}
const options = parsed.options;
const authSource = options.authsource || options.authSource;
if (authSource != null) {
parsed.auth = Object.assign({}, parsed.auth, { db: authSource });
}
const authMechanism = options.authmechanism || options.authMechanism;
if (authMechanism != null) {
if (
USERNAME_REQUIRED_MECHANISMS.has(authMechanism) &&
(!parsed.auth || parsed.auth.username == null)
) {
throw new MongoParseError(`Username required for mechanism \`${authMechanism}\``);
}
if (authMechanism === 'GSSAPI') {
if (authSource != null && authSource !== '$external') {
throw new MongoParseError(
`Invalid source \`${authSource}\` for mechanism \`${authMechanism}\` specified.`
);
}
parsed.auth = Object.assign({}, parsed.auth, { db: '$external' });
}
if (authMechanism === 'MONGODB-X509') {
if (parsed.auth && parsed.auth.password != null) {
throw new MongoParseError(`Password not allowed for mechanism \`${authMechanism}\``);
}
if (authSource != null && authSource !== '$external') {
throw new MongoParseError(
`Invalid source \`${authSource}\` for mechanism \`${authMechanism}\` specified.`
);
}
parsed.auth = Object.assign({}, parsed.auth, { db: '$external' });
}
if (authMechanism === 'PLAIN') {
if (parsed.auth && parsed.auth.db == null) {
parsed.auth = Object.assign({}, parsed.auth, { db: '$external' });
}
}
}
// default to `admin` if nothing else was resolved
if (parsed.auth && parsed.auth.db == null) {
parsed.auth = Object.assign({}, parsed.auth, { db: 'admin' });
}
return parsed;
}
/**
* Parses a query string according the connection string spec.
*
* @param {String} query The query string to parse
* @param {object} [options] The options used for options parsing
* @return {Object|Error} The parsed query string as an object, or an error if one was encountered
*/
function parseQueryString(query, options) {
const result = {};
let parsedQueryString = qs.parse(query);
for (const key in parsedQueryString) {
const value = parsedQueryString[key];
if (value === '' || value == null) {
throw new MongoParseError('Incomplete key value pair for option');
}
const normalizedKey = key.toLowerCase();
const parsedValue = parseQueryStringItemValue(normalizedKey, value);
applyConnectionStringOption(result, normalizedKey, parsedValue, options);
}
// special cases for known deprecated options
if (result.wtimeout && result.wtimeoutms) {
delete result.wtimeout;
console.warn('Unsupported option `wtimeout` specified');
}
return Object.keys(result).length ? result : null;
}
const PROTOCOL_MONGODB = 'mongodb';
const PROTOCOL_MONGODB_SRV = 'mongodb+srv';
const SUPPORTED_PROTOCOLS = [PROTOCOL_MONGODB, PROTOCOL_MONGODB_SRV];
/**
* Parses a MongoDB connection string
*
* @param {*} uri the MongoDB connection string to parse
* @param {object} [options] Optional settings.
* @param {boolean} [options.caseTranslate] Whether the parser should translate options back into camelCase after normalization
* @param {parseCallback} callback
*/
function parseConnectionString(uri, options, callback) {
if (typeof options === 'function') (callback = options), (options = {});
options = Object.assign({}, { caseTranslate: true }, options);
// Check for bad uris before we parse
try {
URL.parse(uri);
} catch (e) {
return callback(new MongoParseError('URI malformed, cannot be parsed'));
}
const cap = uri.match(HOSTS_RX);
if (!cap) {
return callback(new MongoParseError('Invalid connection string'));
}
const protocol = cap[1];
if (SUPPORTED_PROTOCOLS.indexOf(protocol) === -1) {
return callback(new MongoParseError('Invalid protocol provided'));
}
if (protocol === PROTOCOL_MONGODB_SRV) {
return parseSrvConnectionString(uri, options, callback);
}
const dbAndQuery = cap[4].split('?');
const db = dbAndQuery.length > 0 ? dbAndQuery[0] : null;
const query = dbAndQuery.length > 1 ? dbAndQuery[1] : null;
let parsedOptions;
try {
parsedOptions = parseQueryString(query, options);
} catch (parseError) {
return callback(parseError);
}
parsedOptions = Object.assign({}, parsedOptions, options);
const auth = { username: null, password: null, db: db && db !== '' ? qs.unescape(db) : null };
if (parsedOptions.auth) {
// maintain support for legacy options passed into `MongoClient`
if (parsedOptions.auth.username) auth.username = parsedOptions.auth.username;
if (parsedOptions.auth.user) auth.username = parsedOptions.auth.user;
if (parsedOptions.auth.password) auth.password = parsedOptions.auth.password;
}
if (cap[4].split('?')[0].indexOf('@') !== -1) {
return callback(new MongoParseError('Unescaped slash in userinfo section'));
}
const authorityParts = cap[3].split('@');
if (authorityParts.length > 2) {
return callback(new MongoParseError('Unescaped at-sign in authority section'));
}
if (authorityParts.length > 1) {
const authParts = authorityParts.shift().split(':');
if (authParts.length > 2) {
return callback(new MongoParseError('Unescaped colon in authority section'));
}
auth.username = qs.unescape(authParts[0]);
auth.password = authParts[1] ? qs.unescape(authParts[1]) : null;
}
let hostParsingError = null;
const hosts = authorityParts
.shift()
.split(',')
.map(host => {
let parsedHost = URL.parse(`mongodb://${host}`);
if (parsedHost.path === '/:') {
hostParsingError = new MongoParseError('Double colon in host identifier');
return null;
}
// heuristically determine if we're working with a domain socket
if (host.match(/\.sock/)) {
parsedHost.hostname = qs.unescape(host);
parsedHost.port = null;
}
if (Number.isNaN(parsedHost.port)) {
hostParsingError = new MongoParseError('Invalid port (non-numeric string)');
return;
}
const result = {
host: parsedHost.hostname,
port: parsedHost.port ? parseInt(parsedHost.port) : 27017
};
if (result.port === 0) {
hostParsingError = new MongoParseError('Invalid port (zero) with hostname');
return;
}
if (result.port > 65535) {
hostParsingError = new MongoParseError('Invalid port (larger than 65535) with hostname');
return;
}
if (result.port < 0) {
hostParsingError = new MongoParseError('Invalid port (negative number)');
return;
}
return result;
})
.filter(host => !!host);
if (hostParsingError) {
return callback(hostParsingError);
}
if (hosts.length === 0 || hosts[0].host === '' || hosts[0].host === null) {
return callback(new MongoParseError('No hostname or hostnames provided in connection string'));
}
const result = {
hosts: hosts,
auth: auth.db || auth.username ? auth : null,
options: Object.keys(parsedOptions).length ? parsedOptions : null
};
if (result.auth && result.auth.db) {
result.defaultDatabase = result.auth.db;
}
try {
applyAuthExpectations(result);
} catch (authError) {
return callback(authError);
}
callback(null, result);
}
module.exports = parseConnectionString;

97
node_modules/mongodb-core/lib/utils.js generated vendored Normal file
View File

@@ -0,0 +1,97 @@
'use strict';
const crypto = require('crypto');
const requireOptional = require('require_optional');
/**
* Generate a UUIDv4
*/
const uuidV4 = () => {
const result = crypto.randomBytes(16);
result[6] = (result[6] & 0x0f) | 0x40;
result[8] = (result[8] & 0x3f) | 0x80;
return result;
};
/**
* Returns the duration calculated from two high resolution timers in milliseconds
*
* @param {Object} started A high resolution timestamp created from `process.hrtime()`
* @returns {Number} The duration in milliseconds
*/
const calculateDurationInMs = started => {
const hrtime = process.hrtime(started);
return (hrtime[0] * 1e9 + hrtime[1]) / 1e6;
};
/**
* Relays events for a given listener and emitter
*
* @param {EventEmitter} listener the EventEmitter to listen to the events from
* @param {EventEmitter} emitter the EventEmitter to relay the events to
*/
function relayEvents(listener, emitter, events) {
events.forEach(eventName => listener.on(eventName, event => emitter.emit(eventName, event)));
}
function retrieveKerberos() {
let kerberos;
try {
kerberos = requireOptional('kerberos');
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') {
throw new Error('The `kerberos` module was not found. Please install it and try again.');
}
throw err;
}
return kerberos;
}
// Throw an error if an attempt to use EJSON is made when it is not installed
const noEJSONError = function() {
throw new Error('The `mongodb-extjson` module was not found. Please install it and try again.');
};
// Facilitate loading EJSON optionally
function retrieveEJSON() {
let EJSON = null;
try {
EJSON = requireOptional('mongodb-extjson');
} catch (error) {} // eslint-disable-line
if (!EJSON) {
EJSON = {
parse: noEJSONError,
deserialize: noEJSONError,
serialize: noEJSONError,
stringify: noEJSONError,
setBSONModule: noEJSONError,
BSON: noEJSONError
};
}
return EJSON;
}
/*
* Checks that collation is supported by server.
*
* @param {Server} [server] to check against
* @param {object} [cmd] object where collation may be specified
* @param {function} [callback] callback function
* @return true if server does not support collation
*/
function collationNotSupported(server, cmd) {
return cmd && cmd.collation && server.ismaster && server.ismaster.maxWireVersion < 5;
}
module.exports = {
uuidV4,
calculateDurationInMs,
relayEvents,
collationNotSupported,
retrieveEJSON,
retrieveKerberos
};

View File

@@ -0,0 +1,267 @@
'use strict';
const retrieveBSON = require('../connection/utils').retrieveBSON;
const KillCursor = require('../connection/commands').KillCursor;
const GetMore = require('../connection/commands').GetMore;
const Query = require('../connection/commands').Query;
const MongoError = require('../error').MongoError;
const getReadPreference = require('./shared').getReadPreference;
const applyCommonQueryOptions = require('./shared').applyCommonQueryOptions;
const isMongos = require('./shared').isMongos;
const databaseNamespace = require('./shared').databaseNamespace;
const collectionNamespace = require('./shared').collectionNamespace;
const BSON = retrieveBSON();
const Long = BSON.Long;
class WireProtocol {
insert(server, ns, ops, options, callback) {
executeWrite(this, server, 'insert', 'documents', ns, ops, options, callback);
}
update(server, ns, ops, options, callback) {
executeWrite(this, server, 'update', 'updates', ns, ops, options, callback);
}
remove(server, ns, ops, options, callback) {
executeWrite(this, server, 'delete', 'deletes', ns, ops, options, callback);
}
killCursor(server, ns, cursorState, callback) {
const bson = server.s.bson;
const pool = server.s.pool;
const cursorId = cursorState.cursorId;
const killCursor = new KillCursor(bson, ns, [cursorId]);
const options = {
immediateRelease: true,
noResponse: true
};
if (typeof cursorState.session === 'object') {
options.session = cursorState.session;
}
if (pool && pool.isConnected()) {
try {
pool.write(killCursor, options, callback);
} catch (err) {
if (typeof callback === 'function') {
callback(err, null);
} else {
console.warn(err);
}
}
}
}
getMore(server, ns, cursorState, batchSize, options, callback) {
const bson = server.s.bson;
const getMore = new GetMore(bson, ns, cursorState.cursorId, { numberToReturn: batchSize });
function queryCallback(err, result) {
if (err) return callback(err);
const response = result.message;
// If we have a timed out query or a cursor that was killed
if (response.cursorNotFound) {
return callback(new MongoError('Cursor does not exist, was killed, or timed out'), null);
}
const cursorId =
typeof response.cursorId === 'number'
? Long.fromNumber(response.cursorId)
: response.cursorId;
cursorState.documents = response.documents;
cursorState.cursorId = cursorId;
callback(null, null, response.connection);
}
const queryOptions = applyCommonQueryOptions({}, cursorState);
server.s.pool.write(getMore, queryOptions, queryCallback);
}
query(server, ns, cmd, cursorState, options, callback) {
if (cursorState.cursorId != null) {
return;
}
const query = setupClassicFind(server, ns, cmd, cursorState, options);
const queryOptions = applyCommonQueryOptions({}, cursorState);
if (typeof query.documentsReturnedIn === 'string') {
queryOptions.documentsReturnedIn = query.documentsReturnedIn;
}
server.s.pool.write(query, queryOptions, callback);
}
command(server, ns, cmd, options, callback) {
if (cmd == null) {
return callback(new MongoError(`command ${JSON.stringify(cmd)} does not return a cursor`));
}
options = options || {};
const bson = server.s.bson;
const pool = server.s.pool;
const readPreference = getReadPreference(cmd, options);
let finalCmd = Object.assign({}, cmd);
if (finalCmd.readConcern) {
if (finalCmd.readConcern.level !== 'local') {
return callback(
new MongoError(
`server ${JSON.stringify(finalCmd)} command does not support a readConcern level of ${
finalCmd.readConcern.level
}`
)
);
}
delete finalCmd['readConcern'];
}
if (isMongos(server) && readPreference && readPreference.preference !== 'primary') {
finalCmd = {
$query: finalCmd,
$readPreference: readPreference.toJSON()
};
}
const commandOptions = Object.assign(
{
command: true,
numberToSkip: 0,
numberToReturn: -1,
checkKeys: false
},
options
);
// This value is not overridable
commandOptions.slaveOk = readPreference.slaveOk();
try {
const query = new Query(bson, `${databaseNamespace(ns)}.$cmd`, finalCmd, commandOptions);
pool.write(query, commandOptions, callback);
} catch (err) {
callback(err);
}
}
}
function executeWrite(handler, server, 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 || {};
}
const ordered = typeof options.ordered === 'boolean' ? options.ordered : true;
const writeConcern = options.writeConcern;
const writeCommand = {};
writeCommand[type] = collectionNamespace(ns);
writeCommand[opsField] = ops;
writeCommand.ordered = ordered;
if (writeConcern && Object.keys(writeConcern).length > 0) {
writeCommand.writeConcern = writeConcern;
}
if (options.bypassDocumentValidation === true) {
writeCommand.bypassDocumentValidation = options.bypassDocumentValidation;
}
const commandOptions = Object.assign(
{
checkKeys: type === 'insert',
numberToReturn: 1
},
options
);
handler.command(server, ns, writeCommand, commandOptions, callback);
}
function setupClassicFind(server, ns, cmd, cursorState, options) {
options = options || {};
const bson = server.s.bson;
const readPreference = getReadPreference(cmd, options);
cursorState.batchSize = cmd.batchSize || cursorState.batchSize;
let numberToReturn = 0;
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;
}
const numberToSkip = cursorState.skip || 0;
const findCmd = {};
if (isMongos(server) && readPreference) {
findCmd['$readPreference'] = readPreference.toJSON();
}
if (cmd.sort) findCmd['$orderby'] = cmd.sort;
if (cmd.hint) findCmd['$hint'] = cmd.hint;
if (cmd.snapshot) findCmd['$snapshot'] = cmd.snapshot;
if (typeof cmd.returnKey !== 'undefined') findCmd['$returnKey'] = cmd.returnKey;
if (cmd.maxScan) findCmd['$maxScan'] = cmd.maxScan;
if (cmd.min) findCmd['$min'] = cmd.min;
if (cmd.max) findCmd['$max'] = cmd.max;
if (typeof cmd.showDiskLoc !== 'undefined') findCmd['$showDiskLoc'] = cmd.showDiskLoc;
if (cmd.comment) findCmd['$comment'] = cmd.comment;
if (cmd.maxTimeMS) findCmd['$maxTimeMS'] = cmd.maxTimeMS;
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);
findCmd['$explain'] = true;
}
findCmd['$query'] = cmd.query;
if (cmd.readConcern && cmd.readConcern.level !== 'local') {
throw new MongoError(
`server find command does not support a readConcern level of ${cmd.readConcern.level}`
);
}
if (cmd.readConcern) {
cmd = Object.assign({}, cmd);
delete cmd['readConcern'];
}
const serializeFunctions =
typeof options.serializeFunctions === 'boolean' ? options.serializeFunctions : false;
const ignoreUndefined =
typeof options.ignoreUndefined === 'boolean' ? options.ignoreUndefined : false;
const query = new Query(bson, ns, findCmd, {
numberToSkip: numberToSkip,
numberToReturn: numberToReturn,
pre32Limit: typeof cmd.limit !== 'undefined' ? cmd.limit : undefined,
checkKeys: false,
returnFieldSelector: cmd.fields,
serializeFunctions: serializeFunctions,
ignoreUndefined: ignoreUndefined
});
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;
query.slaveOk = readPreference.slaveOk();
return query;
}
module.exports = WireProtocol;

View File

@@ -0,0 +1,397 @@
'use strict';
const Query = require('../connection/commands').Query;
const retrieveBSON = require('../connection/utils').retrieveBSON;
const MongoError = require('../error').MongoError;
const MongoNetworkError = require('../error').MongoNetworkError;
const getReadPreference = require('./shared').getReadPreference;
const BSON = retrieveBSON();
const Long = BSON.Long;
const ReadPreference = require('../topologies/read_preference');
const TxnState = require('../transactions').TxnState;
const isMongos = require('./shared').isMongos;
const databaseNamespace = require('./shared').databaseNamespace;
const collectionNamespace = require('./shared').collectionNamespace;
class WireProtocol {
insert(server, ns, ops, options, callback) {
executeWrite(this, server, 'insert', 'documents', ns, ops, options, callback);
}
update(server, ns, ops, options, callback) {
executeWrite(this, server, 'update', 'updates', ns, ops, options, callback);
}
remove(server, ns, ops, options, callback) {
executeWrite(this, server, 'delete', 'deletes', ns, ops, options, callback);
}
killCursor(server, ns, cursorState, callback) {
callback = typeof callback === 'function' ? callback : () => {};
const cursorId = cursorState.cursorId;
const killCursorCmd = {
killCursors: collectionNamespace(ns),
cursors: [cursorId]
};
const options = {};
if (typeof cursorState.session === 'object') options.session = cursorState.session;
this.command(server, ns, killCursorCmd, options, (err, result) => {
if (err) {
return callback(err);
}
const response = result.message;
if (response.cursorNotFound) {
return callback(new MongoNetworkError('cursor killed or timed out'), null);
}
if (!Array.isArray(response.documents) || response.documents.length === 0) {
return callback(
new MongoError(`invalid killCursors result returned for cursor id ${cursorId}`)
);
}
callback(null, response.documents[0]);
});
}
getMore(server, ns, cursorState, batchSize, options, callback) {
options = options || {};
const getMoreCmd = {
getMore: cursorState.cursorId,
collection: collectionNamespace(ns),
batchSize: Math.abs(batchSize)
};
if (cursorState.cmd.tailable && typeof cursorState.cmd.maxAwaitTimeMS === 'number') {
getMoreCmd.maxTimeMS = cursorState.cmd.maxAwaitTimeMS;
}
function queryCallback(err, result) {
if (err) return callback(err);
const response = result.message;
// If we have a timed out query or a cursor that was killed
if (response.cursorNotFound) {
return callback(new MongoNetworkError('cursor killed or timed out'), null);
}
// Raw, return all the extracted documents
if (cursorState.raw) {
cursorState.documents = response.documents;
cursorState.cursorId = response.cursorId;
return callback(null, response.documents);
}
// We have an error detected
if (response.documents[0].ok === 0) {
return callback(new MongoError(response.documents[0]));
}
// Ensure we have a Long valid cursor id
const cursorId =
typeof response.documents[0].cursor.id === 'number'
? Long.fromNumber(response.documents[0].cursor.id)
: response.documents[0].cursor.id;
cursorState.documents = response.documents[0].cursor.nextBatch;
cursorState.cursorId = cursorId;
callback(null, response.documents[0], response.connection);
}
const commandOptions = Object.assign(
{
returnFieldSelector: null,
documentsReturnedIn: 'nextBatch'
},
options
);
this.command(server, ns, getMoreCmd, commandOptions, queryCallback);
}
query(server, ns, cmd, cursorState, options, callback) {
options = options || {};
if (cursorState.cursorId != null) {
return callback();
}
if (cmd == null) {
return callback(new MongoError(`command ${JSON.stringify(cmd)} does not return a cursor`));
}
const readPreference = getReadPreference(cmd, options);
const findCmd = prepareFindCommand(server, ns, cmd, cursorState, options);
// NOTE: This actually modifies the passed in cmd, and our code _depends_ on this
// side-effect. Change this ASAP
cmd.virtual = false;
const commandOptions = Object.assign(
{
documentsReturnedIn: 'firstBatch',
numberToReturn: 1,
slaveOk: readPreference.slaveOk()
},
options
);
if (cmd.readPreference) commandOptions.readPreference = readPreference;
this.command(server, ns, findCmd, commandOptions, callback);
}
command(server, ns, cmd, options, callback) {
if (typeof options === 'function') (callback = options), (options = {});
options = options || {};
if (cmd == null) {
return callback(new MongoError(`command ${JSON.stringify(cmd)} does not return a cursor`));
}
const bson = server.s.bson;
const pool = server.s.pool;
const readPreference = getReadPreference(cmd, options);
let finalCmd = Object.assign({}, cmd);
if (isMongos(server) && readPreference && readPreference.preference !== 'primary') {
finalCmd = {
$query: finalCmd,
$readPreference: readPreference.toJSON()
};
}
const err = decorateWithSessionsData(finalCmd, options.session, options);
if (err) {
return callback(err);
}
const commandOptions = Object.assign(
{
command: true,
numberToSkip: 0,
numberToReturn: -1,
checkKeys: false
},
options
);
// This value is not overridable
commandOptions.slaveOk = readPreference.slaveOk();
try {
const query = new Query(bson, `${databaseNamespace(ns)}.$cmd`, finalCmd, commandOptions);
pool.write(query, commandOptions, callback);
} catch (err) {
callback(err);
}
}
}
function isTransactionCommand(command) {
return !!(command.commitTransaction || command.abortTransaction);
}
/**
* Optionally decorate a command with sessions specific keys
*
* @param {Object} command the command to decorate
* @param {ClientSession} session the session tracking transaction state
* @param {Object} [options] Optional settings passed to calling operation
* @param {Function} [callback] Optional callback passed from calling operation
* @return {MongoError|null} An error, if some error condition was met
*/
function decorateWithSessionsData(command, session, options) {
if (!session) {
return;
}
// first apply non-transaction-specific sessions data
const serverSession = session.serverSession;
const inTransaction = session.inTransaction() || isTransactionCommand(command);
const isRetryableWrite = options.willRetryWrite;
if (serverSession.txnNumber && (isRetryableWrite || inTransaction)) {
command.txnNumber = BSON.Long.fromNumber(serverSession.txnNumber);
}
// now attempt to apply transaction-specific sessions data
if (!inTransaction) {
if (session.transaction.state !== TxnState.NO_TRANSACTION) {
session.transaction.transition(TxnState.NO_TRANSACTION);
}
// for causal consistency
if (session.supports.causalConsistency && session.operationTime) {
command.readConcern = command.readConcern || {};
Object.assign(command.readConcern, { afterClusterTime: session.operationTime });
}
return;
}
if (options.readPreference && !options.readPreference.equals(ReadPreference.primary)) {
return new MongoError(
`Read preference in a transaction must be primary, not: ${options.readPreference.mode}`
);
}
// `autocommit` must always be false to differentiate from retryable writes
command.autocommit = false;
if (session.transaction.state === TxnState.STARTING_TRANSACTION) {
session.transaction.transition(TxnState.TRANSACTION_IN_PROGRESS);
command.startTransaction = true;
const readConcern =
session.transaction.options.readConcern || session.clientOptions.readConcern;
if (readConcern) {
command.readConcern = readConcern;
}
if (session.supports.causalConsistency && session.operationTime) {
command.readConcern = command.readConcern || {};
Object.assign(command.readConcern, { afterClusterTime: session.operationTime });
}
}
}
function executeWrite(handler, server, 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 || {};
}
const ordered = typeof options.ordered === 'boolean' ? options.ordered : true;
const writeConcern = options.writeConcern;
const writeCommand = {};
writeCommand[type] = collectionNamespace(ns);
writeCommand[opsField] = ops;
writeCommand.ordered = ordered;
if (writeConcern && Object.keys(writeConcern).length > 0) {
writeCommand.writeConcern = writeConcern;
}
if (options.collation) {
for (let i = 0; i < writeCommand[opsField].length; i++) {
if (!writeCommand[opsField][i].collation) {
writeCommand[opsField][i].collation = options.collation;
}
}
}
if (options.bypassDocumentValidation === true) {
writeCommand.bypassDocumentValidation = options.bypassDocumentValidation;
}
const commandOptions = Object.assign(
{
checkKeys: type === 'insert',
numberToReturn: 1
},
options
);
handler.command(server, ns, writeCommand, commandOptions, callback);
}
function prepareFindCommand(server, ns, cmd, cursorState) {
cursorState.batchSize = cmd.batchSize || cursorState.batchSize;
let findCmd = {
find: collectionNamespace(ns)
};
if (cmd.query) {
if (cmd.query['$query']) {
findCmd.filter = cmd.query['$query'];
} else {
findCmd.filter = cmd.query;
}
}
let sortValue = cmd.sort;
if (Array.isArray(sortValue)) {
const sortObject = {};
if (sortValue.length > 0 && !Array.isArray(sortValue[0])) {
let sortDirection = sortValue[1];
if (sortDirection === 'asc') {
sortDirection = 1;
} else if (sortDirection === 'desc') {
sortDirection = -1;
}
sortObject[sortValue[0]] = sortDirection;
} else {
for (let i = 0; i < sortValue.length; i++) {
let sortDirection = sortValue[i][1];
if (sortDirection === 'asc') {
sortDirection = 1;
} else if (sortDirection === 'desc') {
sortDirection = -1;
}
sortObject[sortValue[i][0]] = sortDirection;
}
}
sortValue = sortObject;
}
if (cmd.sort) findCmd.sort = sortValue;
if (cmd.fields) findCmd.projection = cmd.fields;
if (cmd.hint) findCmd.hint = cmd.hint;
if (cmd.skip) findCmd.skip = cmd.skip;
if (cmd.limit) findCmd.limit = cmd.limit;
if (cmd.limit < 0) {
findCmd.limit = Math.abs(cmd.limit);
findCmd.singleBatch = true;
}
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 (cmd.comment) findCmd.comment = cmd.comment;
if (cmd.maxScan) findCmd.maxScan = cmd.maxScan;
if (cmd.maxTimeMS) findCmd.maxTimeMS = cmd.maxTimeMS;
if (cmd.min) findCmd.min = cmd.min;
if (cmd.max) findCmd.max = cmd.max;
findCmd.returnKey = cmd.returnKey ? cmd.returnKey : false;
findCmd.showRecordId = cmd.showDiskLoc ? cmd.showDiskLoc : false;
if (cmd.snapshot) findCmd.snapshot = cmd.snapshot;
if (cmd.tailable) findCmd.tailable = cmd.tailable;
if (cmd.oplogReplay) findCmd.oplogReplay = cmd.oplogReplay;
if (cmd.noCursorTimeout) findCmd.noCursorTimeout = cmd.noCursorTimeout;
if (cmd.awaitData) findCmd.awaitData = cmd.awaitData;
if (cmd.awaitdata) findCmd.awaitData = cmd.awaitdata;
if (cmd.partial) findCmd.partial = cmd.partial;
if (cmd.collation) findCmd.collation = cmd.collation;
if (cmd.readConcern) findCmd.readConcern = cmd.readConcern;
// If we have explain, we need to rewrite the find command
// to wrap it in the explain command
if (cmd.explain) {
findCmd = {
explain: findCmd
};
}
return findCmd;
}
module.exports = WireProtocol;

View File

@@ -0,0 +1,73 @@
'use strict';
var Snappy = require('../connection/utils').retrieveSnappy(),
zlib = require('zlib');
var compressorIDs = {
snappy: 1,
zlib: 2
};
var uncompressibleCommands = [
'ismaster',
'saslStart',
'saslContinue',
'getnonce',
'authenticate',
'createUser',
'updateUser',
'copydbSaslStart',
'copydbgetnonce',
'copydb'
];
// Facilitate compressing a message using an agreed compressor
var compress = function(self, dataToBeCompressed, callback) {
switch (self.options.agreedCompressor) {
case 'snappy':
Snappy.compress(dataToBeCompressed, callback);
break;
case 'zlib':
// Determine zlibCompressionLevel
var zlibOptions = {};
if (self.options.zlibCompressionLevel) {
zlibOptions.level = self.options.zlibCompressionLevel;
}
zlib.deflate(dataToBeCompressed, zlibOptions, callback);
break;
default:
throw new Error(
'Attempt to compress message using unknown compressor "' +
self.options.agreedCompressor +
'".'
);
}
};
// Decompress a message using the given compressor
var decompress = function(compressorID, compressedData, callback) {
if (compressorID < 0 || compressorID > compressorIDs.length) {
throw new Error(
'Server sent message compressed using an unsupported compressor. (Received compressor ID ' +
compressorID +
')'
);
}
switch (compressorID) {
case compressorIDs.snappy:
Snappy.uncompress(compressedData, callback);
break;
case compressorIDs.zlib:
zlib.inflate(compressedData, callback);
break;
default:
callback(null, compressedData);
}
};
module.exports = {
compressorIDs: compressorIDs,
uncompressibleCommands: uncompressibleCommands,
compress: compress,
decompress: decompress
};

101
node_modules/mongodb-core/lib/wireprotocol/shared.js generated vendored Normal file
View File

@@ -0,0 +1,101 @@
'use strict';
var ReadPreference = require('../topologies/read_preference'),
MongoError = require('../error').MongoError;
var MESSAGE_HEADER_SIZE = 16;
// OPCODE Numbers
// Defined at https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/#request-opcodes
var opcodes = {
OP_REPLY: 1,
OP_UPDATE: 2001,
OP_INSERT: 2002,
OP_QUERY: 2004,
OP_GETMORE: 2005,
OP_DELETE: 2006,
OP_KILL_CURSORS: 2007,
OP_COMPRESSED: 2012
};
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('read preference must be a ReadPreference instance');
}
return readPreference;
};
// Parses the header of a wire protocol message
var parseHeader = function(message) {
return {
length: message.readInt32LE(0),
requestId: message.readInt32LE(4),
responseTo: message.readInt32LE(8),
opCode: message.readInt32LE(12)
};
};
function applyCommonQueryOptions(queryOptions, options) {
Object.assign(queryOptions, {
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,
monitoring: typeof options.monitoring === 'boolean' ? options.monitoring : false,
fullResult: typeof options.fullResult === 'boolean' ? options.fullResult : false
});
if (typeof options.socketTimeout === 'number') {
queryOptions.socketTimeout = options.socketTimeout;
}
if (options.session) {
queryOptions.session = options.session;
}
if (typeof options.documentsReturnedIn === 'string') {
queryOptions.documentsReturnedIn = options.documentsReturnedIn;
}
return queryOptions;
}
function isMongos(server) {
if (server.type === 'mongos') return true;
if (server.parent && server.parent.type === 'mongos') return true;
// NOTE: handle unified topology
return false;
}
function databaseNamespace(ns) {
return ns.split('.')[0];
}
function collectionNamespace(ns) {
return ns
.split('.')
.slice(1)
.join('.');
}
module.exports = {
getReadPreference,
MESSAGE_HEADER_SIZE,
opcodes,
parseHeader,
applyCommonQueryOptions,
isMongos,
databaseNamespace,
collectionNamespace
};