This commit is contained in:
Jonasz Bigda
2023-03-25 21:51:42 +01:00
parent 0db1d5117e
commit b332e9ceb0
1044 changed files with 37502 additions and 63938 deletions

View File

@@ -1,158 +1,55 @@
'use strict';
const MongoError = require('../error').MongoError;
/**
* Creates a new AuthProvider, which dictates how to authenticate for a given
* mechanism.
* @class
* Context used during authentication
*
* @property {Connection} connection The connection to authenticate
* @property {MongoCredentials} credentials The credentials to use for authentication
* @property {object} options The options passed to the `connect` method
* @property {object?} response The response of the initial handshake
* @property {Buffer?} nonce A random nonce generated for use in an authentication conversation
*/
class AuthContext {
constructor(connection, credentials, options) {
this.connection = connection;
this.credentials = credentials;
this.options = options;
}
}
class AuthProvider {
constructor(bson) {
this.bson = bson;
this.authStore = [];
}
/**
* Prepare the handshake document before the initial handshake.
*
* @param {object} handshakeDoc The document used for the initial handshake on a connection
* @param {AuthContext} authContext Context for authentication flow
* @param {function} callback
*/
prepare(handshakeDoc, context, callback) {
callback(undefined, handshakeDoc);
}
/**
* Authenticate
* @method
* @param {SendAuthCommand} sendAuthCommand Writes an auth command directly to a specific connection
* @param {Connection[]} connections Connections to authenticate using this authenticator
* @param {MongoCredentials} credentials Authentication credentials
*
* @param {AuthContext} context A shared context for authentication flow
* @param {authResultCallback} callback The callback to return the result from the authentication
*/
auth(sendAuthCommand, connections, credentials, callback) {
// Total connections
let count = connections.length;
if (count === 0) {
callback(null, null);
return;
}
// Valid connections
let numberOfValidConnections = 0;
let errorObject = null;
const execute = connection => {
this._authenticateSingleConnection(sendAuthCommand, connection, credentials, (err, r) => {
// Adjust count
count = count - 1;
// If we have an error
if (err) {
errorObject = new MongoError(err);
} else if (r && (r.$err || r.errmsg)) {
errorObject = new MongoError(r);
} else {
numberOfValidConnections = numberOfValidConnections + 1;
}
// Still authenticating against other connections.
if (count !== 0) {
return;
}
// We have authenticated all connections
if (numberOfValidConnections > 0) {
// Store the auth details
this.addCredentials(credentials);
// Return correct authentication
callback(null, true);
} else {
if (errorObject == null) {
errorObject = new MongoError(`failed to authenticate using ${credentials.mechanism}`);
}
callback(errorObject, false);
}
});
};
const executeInNextTick = _connection => process.nextTick(() => execute(_connection));
// For each connection we need to authenticate
while (connections.length > 0) {
executeInNextTick(connections.shift());
}
}
/**
* Implementation of a single connection authenticating. Is meant to be overridden.
* Will error if called directly
* @ignore
*/
_authenticateSingleConnection(/*sendAuthCommand, connection, credentials, callback*/) {
throw new Error('_authenticateSingleConnection must be overridden');
}
/**
* Adds credentials to store only if it does not exist
* @param {MongoCredentials} credentials credentials to add to store
*/
addCredentials(credentials) {
const found = this.authStore.some(cred => cred.equals(credentials));
if (!found) {
this.authStore.push(credentials);
}
}
/**
* Re authenticate pool
* @method
* @param {SendAuthCommand} sendAuthCommand Writes an auth command directly to a specific connection
* @param {Connection[]} connections Connections to authenticate using this authenticator
* @param {authResultCallback} callback The callback to return the result from the authentication
*/
reauthenticate(sendAuthCommand, connections, callback) {
const authStore = this.authStore.slice(0);
let count = authStore.length;
if (count === 0) {
return callback(null, null);
}
for (let i = 0; i < authStore.length; i++) {
this.auth(sendAuthCommand, connections, authStore[i], function(err) {
count = count - 1;
if (count === 0) {
callback(err, null);
}
});
}
}
/**
* Remove credentials that have been previously stored in the auth provider
* @method
* @param {string} source Name of database we are removing authStore details about
* @return {object}
*/
logout(source) {
this.authStore = this.authStore.filter(credentials => credentials.source !== source);
auth(context, callback) {
callback(new TypeError('`auth` method must be overridden by subclass'));
}
}
/**
* A function that writes authentication commands to a specific connection
* @callback SendAuthCommand
* @param {Connection} connection The connection to write to
* @param {Command} command A command with a toBin method that can be written to a connection
* @param {AuthWriteCallback} callback Callback called when command response is received
*/
/**
* A callback for a specific auth command
* @callback AuthWriteCallback
* @param {Error} err If command failed, an error from the server
* @param {object} r The response from the server
*/
/**
* This is a result from an authentication strategy
* This is a result from an authentication provider
*
* @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 = { AuthProvider };
module.exports = { AuthContext, AuthProvider };

View File

@@ -4,9 +4,9 @@ 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;
const MongoDBAWS = require('./mongodb_aws');
/**
* Returns the default authentication providers.
@@ -16,11 +16,11 @@ const ScramSHA256 = require('./scram').ScramSHA256;
*/
function defaultAuthProviders(bson) {
return {
'mongodb-aws': new MongoDBAWS(bson),
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)
};

View File

@@ -1,241 +1,151 @@
'use strict';
const dns = require('dns');
const AuthProvider = require('./auth_provider').AuthProvider;
const retrieveKerberos = require('../utils').retrieveKerberos;
const MongoError = require('../error').MongoError;
let kerberos;
/**
* Creates a new GSSAPI authentication mechanism
* @class
* @extends AuthProvider
*/
class GSSAPI extends AuthProvider {
/**
* Implementation of authentication for a single connection
* @override
*/
_authenticateSingleConnection(sendAuthCommand, connection, credentials, callback) {
const source = credentials.source;
auth(authContext, callback) {
const connection = authContext.connection;
const credentials = authContext.credentials;
if (credentials == null) return callback(new MongoError('credentials required'));
const username = credentials.username;
const password = credentials.password;
const mechanismProperties = credentials.mechanismProperties;
const gssapiServiceName =
mechanismProperties['gssapiservicename'] ||
mechanismProperties['gssapiServiceName'] ||
'mongodb';
GSSAPIInitialize(
this,
kerberos.processes.MongoAuthProcess,
source,
username,
password,
source,
gssapiServiceName,
sendAuthCommand,
connection,
mechanismProperties,
callback
);
}
/**
* Authenticate
* @override
* @method
*/
auth(sendAuthCommand, connections, credentials, callback) {
if (kerberos == null) {
try {
kerberos = retrieveKerberos();
} catch (e) {
return callback(e, null);
}
function externalCommand(command, cb) {
return connection.command('$external.$cmd', command, cb);
}
super.auth(sendAuthCommand, connections, credentials, callback);
makeKerberosClient(authContext, (err, client) => {
if (err) return callback(err);
if (client == null) return callback(new MongoError('gssapi client missing'));
client.step('', (err, payload) => {
if (err) return callback(err);
externalCommand(saslStart(payload), (err, response) => {
if (err) return callback(err);
const result = response.result;
negotiate(client, 10, result.payload, (err, payload) => {
if (err) return callback(err);
externalCommand(saslContinue(payload, result.conversationId), (err, response) => {
if (err) return callback(err);
const result = response.result;
finalize(client, username, result.payload, (err, payload) => {
if (err) return callback(err);
externalCommand(
{
saslContinue: 1,
conversationId: result.conversationId,
payload
},
(err, result) => {
if (err) return callback(err);
callback(undefined, result);
}
);
});
});
});
});
});
});
}
}
module.exports = GSSAPI;
//
// Initialize step
var GSSAPIInitialize = function(
self,
MongoAuthProcess,
db,
username,
password,
authdb,
gssapiServiceName,
sendAuthCommand,
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,
sendAuthCommand,
connection,
callback
);
});
function makeKerberosClient(authContext, callback) {
const host = authContext.options.host;
const port = authContext.options.port;
const credentials = authContext.credentials;
if (!host || !port || !credentials) {
return callback(
new MongoError(
`Connection must specify: ${host ? 'host' : ''}, ${port ? 'port' : ''}, ${
credentials ? 'host' : 'credentials'
}.`
)
);
}
if (kerberos == null) {
try {
kerberos = retrieveKerberos();
} catch (e) {
return callback(e);
}
}
const username = credentials.username;
const password = credentials.password;
const mechanismProperties = credentials.mechanismProperties;
const serviceName =
mechanismProperties['gssapiservicename'] ||
mechanismProperties['gssapiServiceName'] ||
'mongodb';
performGssapiCanonicalizeHostName(host, mechanismProperties, (err, host) => {
if (err) return callback(err);
const initOptions = {};
if (password != null) {
Object.assign(initOptions, { user: username, password: password });
}
kerberos.initializeClient(
`${serviceName}${process.platform === 'win32' ? '/' : '@'}${host}`,
initOptions,
(err, client) => {
if (err) return callback(new MongoError(err));
callback(null, client);
}
);
});
};
}
//
// Perform first step against mongodb
var MongoDBGSSAPIFirstStep = function(
self,
mongo_auth_process,
payload,
db,
username,
password,
authdb,
sendAuthCommand,
connection,
callback
) {
// Build the sasl start command
var command = {
function saslStart(payload) {
return {
saslStart: 1,
mechanism: 'GSSAPI',
payload: payload,
payload,
autoAuthorize: 1
};
// Write the commmand on the connection
sendAuthCommand(connection, '$external.$cmd', command, (err, doc) => {
if (err) return callback(err, false);
// Execute mongodb transition
mongo_auth_process.transition(doc.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,
sendAuthCommand,
connection,
callback
);
});
});
};
//
// Perform first step against mongodb
var MongoDBGSSAPISecondStep = function(
self,
mongo_auth_process,
payload,
doc,
db,
username,
password,
authdb,
sendAuthCommand,
connection,
callback
) {
// Build Authentication command to send to MongoDB
var command = {
}
function saslContinue(payload, conversationId) {
return {
saslContinue: 1,
conversationId: doc.conversationId,
payload: payload
conversationId,
payload
};
// Execute the command
// Write the commmand on the connection
sendAuthCommand(connection, '$external.$cmd', command, (err, doc) => {
if (err) return callback(err, false);
// 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,
sendAuthCommand,
connection,
callback
);
}
function negotiate(client, retries, payload, callback) {
client.step(payload, (err, response) => {
// Retries exhausted, raise error
if (err && retries === 0) return callback(err);
// Adjust number of retries and call step again
if (err) return negotiate(client, retries - 1, payload, callback);
// Return the payload
callback(undefined, response || '');
});
}
function finalize(client, user, payload, callback) {
// GSS Client Unwrap
client.unwrap(payload, (err, response) => {
if (err) return callback(err);
// Wrap the response
client.wrap(response || '', { user }, (err, wrapped) => {
if (err) return callback(err);
// Return the payload
callback(undefined, wrapped);
});
});
};
var MongoDBGSSAPIThirdStep = function(
self,
mongo_auth_process,
payload,
doc,
db,
username,
password,
authdb,
sendAuthCommand,
connection,
callback
) {
// Build final command
var command = {
saslContinue: 1,
conversationId: doc.conversationId,
payload: payload
};
// Execute the command
sendAuthCommand(connection, '$external.$cmd', command, (err, r) => {
if (err) return callback(err, false);
mongo_auth_process.transition(null, function(err) {
if (err) return callback(err, null);
callback(null, r);
});
}
function performGssapiCanonicalizeHostName(host, mechanismProperties, callback) {
const canonicalizeHostName =
typeof mechanismProperties.gssapiCanonicalizeHostName === 'boolean'
? mechanismProperties.gssapiCanonicalizeHostName
: false;
if (!canonicalizeHostName) return callback(undefined, host);
// Attempt to resolve the host name
dns.resolveCname(host, (err, r) => {
if (err) return callback(err);
// Get the first resolve host id
if (Array.isArray(r) && r.length > 0) {
return callback(undefined, r[0]);
}
callback(undefined, host);
});
};
/**
* 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;
}

View File

@@ -47,7 +47,27 @@ class MongoCredentials {
this.password = options.password;
this.source = options.source || options.db;
this.mechanism = options.mechanism || 'default';
this.mechanismProperties = options.mechanismProperties;
this.mechanismProperties = options.mechanismProperties || {};
if (/MONGODB-AWS/i.test(this.mechanism)) {
if (!this.username && process.env.AWS_ACCESS_KEY_ID) {
this.username = process.env.AWS_ACCESS_KEY_ID;
}
if (!this.password && process.env.AWS_SECRET_ACCESS_KEY) {
this.password = process.env.AWS_SECRET_ACCESS_KEY;
}
if (
this.mechanismProperties.AWS_SESSION_TOKEN == null &&
process.env.AWS_SESSION_TOKEN != null
) {
this.mechanismProperties.AWS_SESSION_TOKEN = process.env.AWS_SESSION_TOKEN;
}
}
Object.freeze(this.mechanismProperties);
Object.freeze(this);
}
/**
@@ -69,12 +89,21 @@ class MongoCredentials {
* based on the server version and server supported sasl mechanisms.
*
* @param {Object} [ismaster] An ismaster response from the server
* @returns {MongoCredentials}
*/
resolveAuthMechanism(ismaster) {
// If the mechanism is not "default", then it does not need to be resolved
if (this.mechanism.toLowerCase() === 'default') {
this.mechanism = getDefaultAuthMechanism(ismaster);
if (/DEFAULT/i.test(this.mechanism)) {
return new MongoCredentials({
username: this.username,
password: this.password,
source: this.source,
mechanism: getDefaultAuthMechanism(ismaster),
mechanismProperties: this.mechanismProperties
});
}
return this;
}
}

View File

@@ -3,27 +3,21 @@
const crypto = require('crypto');
const AuthProvider = require('./auth_provider').AuthProvider;
/**
* Creates a new MongoCR authentication mechanism
*
* @extends AuthProvider
*/
class MongoCR extends AuthProvider {
/**
* Implementation of authentication for a single connection
* @override
*/
_authenticateSingleConnection(sendAuthCommand, connection, credentials, callback) {
auth(authContext, callback) {
const connection = authContext.connection;
const credentials = authContext.credentials;
const username = credentials.username;
const password = credentials.password;
const source = credentials.source;
sendAuthCommand(connection, `${source}.$cmd`, { getnonce: 1 }, (err, r) => {
connection.command(`${source}.$cmd`, { getnonce: 1 }, (err, result) => {
let nonce = null;
let key = null;
// Get nonce
if (err == null) {
const r = result.result;
nonce = r.nonce;
// Use node md5 generator
let md5 = crypto.createHash('md5');
@@ -43,7 +37,7 @@ class MongoCR extends AuthProvider {
key
};
sendAuthCommand(connection, `${source}.$cmd`, authenticateCommand, callback);
connection.command(`${source}.$cmd`, authenticateCommand, callback);
});
}
}

View File

@@ -1,5 +1,4 @@
'use strict';
const retrieveBSON = require('../connection/utils').retrieveBSON;
const AuthProvider = require('./auth_provider').AuthProvider;
@@ -7,19 +6,13 @@ const AuthProvider = require('./auth_provider').AuthProvider;
const BSON = retrieveBSON();
const Binary = BSON.Binary;
/**
* Creates a new Plain authentication mechanism
*
* @extends AuthProvider
*/
class Plain extends AuthProvider {
/**
* Implementation of authentication for a single connection
* @override
*/
_authenticateSingleConnection(sendAuthCommand, connection, credentials, callback) {
auth(authContext, callback) {
const connection = authContext.connection;
const credentials = authContext.credentials;
const username = credentials.username;
const password = credentials.password;
const payload = new Binary(`\x00${username}\x00${password}`);
const command = {
saslStart: 1,
@@ -28,7 +21,7 @@ class Plain extends AuthProvider {
autoAuthorize: 1
};
sendAuthCommand(connection, '$external.$cmd', command, callback);
connection.command('$external.$cmd', command, callback);
}
}

View File

@@ -1,47 +1,252 @@
'use strict';
const crypto = require('crypto');
const Buffer = require('safe-buffer').Buffer;
const retrieveBSON = require('../connection/utils').retrieveBSON;
const MongoError = require('../error').MongoError;
const AuthProvider = require('./auth_provider').AuthProvider;
const emitWarningOnce = require('../../utils').emitWarning;
const BSON = retrieveBSON();
const Binary = BSON.Binary;
let saslprep;
try {
// Ensure you always wrap an optional require in the try block NODE-3199
saslprep = require('saslprep');
} catch (e) {
// don't do anything;
}
var parsePayload = function(payload) {
var dict = {};
var parts = payload.split(',');
for (var i = 0; i < parts.length; i++) {
var valueParts = parts[i].split('=');
class ScramSHA extends AuthProvider {
constructor(bson, cryptoMethod) {
super(bson);
this.cryptoMethod = cryptoMethod || 'sha1';
}
prepare(handshakeDoc, authContext, callback) {
const cryptoMethod = this.cryptoMethod;
if (cryptoMethod === 'sha256' && saslprep == null) {
emitWarningOnce('Warning: no saslprep library specified. Passwords will not be sanitized');
}
crypto.randomBytes(24, (err, nonce) => {
if (err) {
return callback(err);
}
// store the nonce for later use
Object.assign(authContext, { nonce });
const credentials = authContext.credentials;
const request = Object.assign({}, handshakeDoc, {
speculativeAuthenticate: Object.assign(makeFirstMessage(cryptoMethod, credentials, nonce), {
db: credentials.source
})
});
callback(undefined, request);
});
}
auth(authContext, callback) {
const response = authContext.response;
if (response && response.speculativeAuthenticate) {
continueScramConversation(
this.cryptoMethod,
response.speculativeAuthenticate,
authContext,
callback
);
return;
}
executeScram(this.cryptoMethod, authContext, callback);
}
}
function cleanUsername(username) {
return username.replace('=', '=3D').replace(',', '=2C');
}
function clientFirstMessageBare(username, nonce) {
// 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.
return Buffer.concat([
Buffer.from('n=', 'utf8'),
Buffer.from(username, 'utf8'),
Buffer.from(',r=', 'utf8'),
Buffer.from(nonce.toString('base64'), 'utf8')
]);
}
function makeFirstMessage(cryptoMethod, credentials, nonce) {
const username = cleanUsername(credentials.username);
const mechanism = cryptoMethod === 'sha1' ? 'SCRAM-SHA-1' : 'SCRAM-SHA-256';
// 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.
return {
saslStart: 1,
mechanism,
payload: new Binary(
Buffer.concat([Buffer.from('n,,', 'utf8'), clientFirstMessageBare(username, nonce)])
),
autoAuthorize: 1,
options: { skipEmptyExchange: true }
};
}
function executeScram(cryptoMethod, authContext, callback) {
const connection = authContext.connection;
const credentials = authContext.credentials;
const nonce = authContext.nonce;
const db = credentials.source;
const saslStartCmd = makeFirstMessage(cryptoMethod, credentials, nonce);
connection.command(`${db}.$cmd`, saslStartCmd, (_err, result) => {
const err = resolveError(_err, result);
if (err) {
return callback(err);
}
continueScramConversation(cryptoMethod, result.result, authContext, callback);
});
}
function continueScramConversation(cryptoMethod, response, authContext, callback) {
const connection = authContext.connection;
const credentials = authContext.credentials;
const nonce = authContext.nonce;
const db = credentials.source;
const username = cleanUsername(credentials.username);
const password = credentials.password;
let processedPassword;
if (cryptoMethod === 'sha256') {
processedPassword = saslprep ? saslprep(password) : password;
} else {
try {
processedPassword = passwordDigest(username, password);
} catch (e) {
return callback(e);
}
}
const payload = Buffer.isBuffer(response.payload)
? new Binary(response.payload)
: response.payload;
const dict = parsePayload(payload.value());
const iterations = parseInt(dict.i, 10);
if (iterations && iterations < 4096) {
callback(new MongoError(`Server returned an invalid iteration count ${iterations}`), false);
return;
}
const salt = dict.s;
const rnonce = dict.r;
if (rnonce.startsWith('nonce')) {
callback(new MongoError(`Server returned an invalid nonce: ${rnonce}`), false);
return;
}
// Set up start of proof
const withoutProof = `c=biws,r=${rnonce}`;
const saltedPassword = HI(
processedPassword,
Buffer.from(salt, 'base64'),
iterations,
cryptoMethod
);
const clientKey = HMAC(cryptoMethod, saltedPassword, 'Client Key');
const serverKey = HMAC(cryptoMethod, saltedPassword, 'Server Key');
const storedKey = H(cryptoMethod, clientKey);
const authMessage = [
clientFirstMessageBare(username, nonce),
payload.value().toString('base64'),
withoutProof
].join(',');
const clientSignature = HMAC(cryptoMethod, storedKey, authMessage);
const clientProof = `p=${xor(clientKey, clientSignature)}`;
const clientFinal = [withoutProof, clientProof].join(',');
const serverSignature = HMAC(cryptoMethod, serverKey, authMessage);
const saslContinueCmd = {
saslContinue: 1,
conversationId: response.conversationId,
payload: new Binary(Buffer.from(clientFinal))
};
connection.command(`${db}.$cmd`, saslContinueCmd, (_err, result) => {
const err = resolveError(_err, result);
if (err) {
return callback(err);
}
const r = result.result;
const parsedResponse = parsePayload(r.payload.value());
if (!compareDigest(Buffer.from(parsedResponse.v, 'base64'), serverSignature)) {
callback(new MongoError('Server returned an invalid signature'));
return;
}
if (!r || r.done !== false) {
return callback(err, r);
}
const retrySaslContinueCmd = {
saslContinue: 1,
conversationId: r.conversationId,
payload: Buffer.alloc(0)
};
connection.command(`${db}.$cmd`, retrySaslContinueCmd, callback);
});
}
function parsePayload(payload) {
const dict = {};
const parts = payload.split(',');
for (let i = 0; i < parts.length; i++) {
const 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');
function passwordDigest(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');
}
const md5 = crypto.createHash('md5');
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);
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 = [];
@@ -66,12 +271,12 @@ function HMAC(method, key, text) {
.digest();
}
var _hiCache = {};
var _hiCacheCount = 0;
var _hiCachePurge = function() {
let _hiCache = {};
let _hiCacheCount = 0;
function _hiCachePurge() {
_hiCache = {};
_hiCacheCount = 0;
};
}
const hiLengthMap = {
sha256: 32,
@@ -121,205 +326,19 @@ function compareDigest(lhs, rhs) {
return result === 0;
}
/**
* Creates a new ScramSHA authentication mechanism
* @class
* @extends AuthProvider
*/
class ScramSHA extends AuthProvider {
constructor(bson, cryptoMethod) {
super(bson);
this.cryptoMethod = cryptoMethod || 'sha1';
}
function resolveError(err, result) {
if (err) return err;
static _getError(err, r) {
if (err) {
return err;
}
if (r.$err || r.errmsg) {
return new MongoError(r);
}
}
/**
* @ignore
*/
_executeScram(sendAuthCommand, connection, credentials, nonce, callback) {
let username = credentials.username;
const password = credentials.password;
const db = credentials.source;
const cryptoMethod = this.cryptoMethod;
let mechanism = 'SCRAM-SHA-1';
let processedPassword;
if (cryptoMethod === 'sha256') {
mechanism = 'SCRAM-SHA-256';
processedPassword = saslprep ? saslprep(password) : password;
} else {
try {
processedPassword = passwordDigest(username, password);
} catch (e) {
return callback(e);
}
}
// Clean up the user
username = username.replace('=', '=3D').replace(',', '=2C');
// 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
const saslStartCmd = {
saslStart: 1,
mechanism,
payload: new Binary(Buffer.concat([Buffer.from('n,,', 'utf8'), firstBare])),
autoAuthorize: 1
};
// Write the commmand on the connection
sendAuthCommand(connection, `${db}.$cmd`, saslStartCmd, (err, r) => {
let tmpError = ScramSHA._getError(err, r);
if (tmpError) {
return callback(tmpError, null);
}
const payload = Buffer.isBuffer(r.payload) ? new Binary(r.payload) : r.payload;
const dict = parsePayload(payload.value());
const iterations = parseInt(dict.i, 10);
if (iterations && iterations < 4096) {
callback(new MongoError(`Server returned an invalid iteration count ${iterations}`), false);
return;
}
const salt = dict.s;
const rnonce = dict.r;
if (rnonce.startsWith('nonce')) {
callback(new MongoError(`Server returned an invalid nonce: ${rnonce}`), false);
return;
}
// Set up start of proof
const withoutProof = `c=biws,r=${rnonce}`;
const saltedPassword = HI(
processedPassword,
Buffer.from(salt, 'base64'),
iterations,
cryptoMethod
);
const clientKey = HMAC(cryptoMethod, saltedPassword, 'Client Key');
const serverKey = HMAC(cryptoMethod, saltedPassword, 'Server Key');
const storedKey = H(cryptoMethod, clientKey);
const authMessage = [firstBare, payload.value().toString('base64'), withoutProof].join(',');
const clientSignature = HMAC(cryptoMethod, storedKey, authMessage);
const clientProof = `p=${xor(clientKey, clientSignature)}`;
const clientFinal = [withoutProof, clientProof].join(',');
const serverSignature = HMAC(cryptoMethod, serverKey, authMessage);
const saslContinueCmd = {
saslContinue: 1,
conversationId: r.conversationId,
payload: new Binary(Buffer.from(clientFinal))
};
sendAuthCommand(connection, `${db}.$cmd`, saslContinueCmd, (err, r) => {
if (err || (r && typeof r.ok === 'number' && r.ok === 0)) {
callback(err, r);
return;
}
const parsedResponse = parsePayload(r.payload.value());
if (!compareDigest(Buffer.from(parsedResponse.v, 'base64'), serverSignature)) {
callback(new MongoError('Server returned an invalid signature'));
return;
}
if (!r || r.done !== false) {
return callback(err, r);
}
const retrySaslContinueCmd = {
saslContinue: 1,
conversationId: r.conversationId,
payload: Buffer.alloc(0)
};
sendAuthCommand(connection, `${db}.$cmd`, retrySaslContinueCmd, callback);
});
});
}
/**
* Implementation of authentication for a single connection
* @override
*/
_authenticateSingleConnection(sendAuthCommand, connection, credentials, callback) {
// Create a random nonce
crypto.randomBytes(24, (err, buff) => {
if (err) {
return callback(err, null);
}
return this._executeScram(
sendAuthCommand,
connection,
credentials,
buff.toString('base64'),
callback
);
});
}
/**
* Authenticate
* @override
* @method
*/
auth(sendAuthCommand, connections, credentials, callback) {
this._checkSaslprep();
super.auth(sendAuthCommand, connections, credentials, callback);
}
_checkSaslprep() {
const cryptoMethod = this.cryptoMethod;
if (cryptoMethod === 'sha256') {
if (!saslprep) {
console.warn('Warning: no saslprep library specified. Passwords will not be sanitized');
}
}
}
const r = result.result;
if (r.$err || r.errmsg) return new MongoError(r);
}
/**
* Creates a new ScramSHA1 authentication mechanism
* @class
* @extends ScramSHA
*/
class ScramSHA1 extends ScramSHA {
constructor(bson) {
super(bson, 'sha1');
}
}
/**
* Creates a new ScramSHA256 authentication mechanism
* @class
* @extends ScramSHA
*/
class ScramSHA256 extends ScramSHA {
constructor(bson) {
super(bson, 'sha256');

View File

@@ -1,131 +0,0 @@
'use strict';
const AuthProvider = require('./auth_provider').AuthProvider;
const retrieveKerberos = require('../utils').retrieveKerberos;
let kerberos;
/**
* Creates a new SSPI authentication mechanism
* @class
* @extends AuthProvider
*/
class SSPI extends AuthProvider {
/**
* Implementation of authentication for a single connection
* @override
*/
_authenticateSingleConnection(sendAuthCommand, connection, credentials, callback) {
// TODO: Destructure this
const username = credentials.username;
const password = credentials.password;
const mechanismProperties = credentials.mechanismProperties;
const gssapiServiceName =
mechanismProperties['gssapiservicename'] ||
mechanismProperties['gssapiServiceName'] ||
'mongodb';
SSIPAuthenticate(
this,
kerberos.processes.MongoAuthProcess,
username,
password,
gssapiServiceName,
sendAuthCommand,
connection,
mechanismProperties,
callback
);
}
/**
* Authenticate
* @override
* @method
*/
auth(sendAuthCommand, connections, credentials, callback) {
if (kerberos == null) {
try {
kerberos = retrieveKerberos();
} catch (e) {
return callback(e, null);
}
}
super.auth(sendAuthCommand, connections, credentials, callback);
}
}
function SSIPAuthenticate(
self,
MongoAuthProcess,
username,
password,
gssapiServiceName,
sendAuthCommand,
connection,
options,
callback
) {
const authProcess = new MongoAuthProcess(
connection.host,
connection.port,
gssapiServiceName,
options
);
function authCommand(command, authCb) {
sendAuthCommand(connection, '$external.$cmd', command, 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, doc) => {
if (err) return callback(err, false);
authProcess.transition(doc.payload, (err, payload) => {
if (err) return callback(err, false);
const command = {
saslContinue: 1,
conversationId: doc.conversationId,
payload
};
authCommand(command, (err, doc) => {
if (err) return callback(err, false);
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);
});
});
});
});
});
});
});
});
}
module.exports = SSPI;

View File

@@ -1,26 +1,35 @@
'use strict';
const AuthProvider = require('./auth_provider').AuthProvider;
/**
* Creates a new X509 authentication mechanism
* @class
* @extends AuthProvider
*/
class X509 extends AuthProvider {
/**
* Implementation of authentication for a single connection
* @override
*/
_authenticateSingleConnection(sendAuthCommand, connection, credentials, callback) {
const username = credentials.username;
const command = { authenticate: 1, mechanism: 'MONGODB-X509' };
if (username) {
command.user = username;
prepare(handshakeDoc, authContext, callback) {
const credentials = authContext.credentials;
Object.assign(handshakeDoc, {
speculativeAuthenticate: x509AuthenticateCommand(credentials)
});
callback(undefined, handshakeDoc);
}
auth(authContext, callback) {
const connection = authContext.connection;
const credentials = authContext.credentials;
const response = authContext.response;
if (response.speculativeAuthenticate) {
return callback();
}
sendAuthCommand(connection, '$external.$cmd', command, callback);
connection.command('$external.$cmd', x509AuthenticateCommand(credentials), callback);
}
}
function x509AuthenticateCommand(credentials) {
const command = { authenticate: 1, mechanism: 'MONGODB-X509' };
if (credentials.username) {
Object.assign(command, { user: credentials.username });
}
return command;
}
module.exports = X509;

View File

@@ -1,123 +1,16 @@
'use strict';
const Msg = require('../connection/msg').Msg;
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'
]);
const extractCommand = require('../../command_utils').extractCommand;
// helper methods
const extractCommandName = commandDoc => Object.keys(commandDoc)[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 ? `${pool.options.host}:${pool.options.port}` : pool.address;
const maybeRedact = (commandName, result) => (SENSITIVE_COMMANDS.has(commandName) ? {} : result);
const isLegacyPool = pool => pool.s && pool.queue;
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 instanceof Msg) {
return command.command;
}
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 {
@@ -177,21 +70,15 @@ class CommandStartedEvent {
* @param {Object} command the command
*/
constructor(pool, command) {
const cmd = extractCommand(command);
const commandName = extractCommandName(cmd);
const extractedCommand = extractCommand(command);
const commandName = extractedCommand.name;
const connectionDetails = extractConnectionDetails(pool);
// NOTE: remove in major revision, this is not spec behavior
if (SENSITIVE_COMMANDS.has(commandName)) {
this.commandObj = {};
this.commandObj[commandName] = true;
}
Object.assign(this, connectionDetails, {
requestId: command.requestId,
databaseName: databaseName(command),
commandName,
command: cmd
command: extractedCommand.shouldRedact ? {} : extractedCommand.cmd
});
}
}
@@ -207,15 +94,15 @@ class CommandSucceededEvent {
* @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);
const extractedCommand = extractCommand(command);
const commandName = extractedCommand.name;
const connectionDetails = extractConnectionDetails(pool);
Object.assign(this, connectionDetails, {
requestId: command.requestId,
commandName,
duration: calculateDurationInMs(started),
reply: maybeRedact(commandName, extractReply(command, reply))
reply: extractedCommand.shouldRedact ? {} : extractReply(command, reply)
});
}
}
@@ -231,15 +118,15 @@ class CommandFailedEvent {
* @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);
const extractedCommand = extractCommand(command);
const commandName = extractedCommand.name;
const connectionDetails = extractConnectionDetails(pool);
Object.assign(this, connectionDetails, {
requestId: command.requestId,
commandName,
duration: calculateDurationInMs(started),
failure: maybeRedact(commandName, error)
failure: extractedCommand.shouldRedact ? {} : error
});
}
}

View File

@@ -398,7 +398,12 @@ KillCursor.prototype.toBin = function() {
};
var Response = function(bson, message, msgHeader, msgBody, opts) {
opts = opts || { promoteLongs: true, promoteValues: true, promoteBuffers: false };
opts = opts || {
promoteLongs: true,
promoteValues: true,
promoteBuffers: false,
bsonRegExp: false
};
this.parsed = false;
this.raw = message;
this.data = msgBody;
@@ -429,6 +434,7 @@ var Response = function(bson, message, msgHeader, msgBody, opts) {
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;
this.bsonRegExp = typeof opts.bsonRegExp === 'boolean' ? opts.bsonRegExp : false;
};
Response.prototype.isParsed = function() {
@@ -449,13 +455,16 @@ Response.prototype.parse = function(options) {
typeof options.promoteValues === 'boolean' ? options.promoteValues : this.opts.promoteValues;
var promoteBuffers =
typeof options.promoteBuffers === 'boolean' ? options.promoteBuffers : this.opts.promoteBuffers;
var bsonRegExp =
typeof options.bsonRegExp === 'boolean' ? options.bsonRegExp : this.opts.bsonRegExp;
var bsonSize, _options;
// Set up the options
_options = {
promoteLongs: promoteLongs,
promoteValues: promoteValues,
promoteBuffers: promoteBuffers
promoteBuffers: promoteBuffers,
bsonRegExp: bsonRegExp
};
// Position within OP_REPLY at which documents start

View File

@@ -2,10 +2,11 @@
const net = require('net');
const tls = require('tls');
const Connection = require('./connection');
const Query = require('./commands').Query;
const MongoError = require('../error').MongoError;
const MongoNetworkError = require('../error').MongoNetworkError;
const MongoNetworkTimeoutError = require('../error').MongoNetworkTimeoutError;
const defaultAuthProviders = require('../auth/defaultAuthProviders').defaultAuthProviders;
const AuthContext = require('../auth/auth_provider').AuthContext;
const WIRE_CONSTANTS = require('../wireprotocol/constants');
const makeClientMetadata = require('../utils').makeClientMetadata;
const MAX_SUPPORTED_WIRE_VERSION = WIRE_CONSTANTS.MAX_SUPPORTED_WIRE_VERSION;
@@ -37,30 +38,7 @@ function connect(options, cancellationToken, callback) {
}
function isModernConnectionType(conn) {
return typeof conn.command === 'function';
}
function getSaslSupportedMechs(options) {
if (!(options && options.credentials)) {
return {};
}
const credentials = options.credentials;
// TODO: revisit whether or not items like `options.user` and `options.dbName` should be checked here
const authMechanism = credentials.mechanism;
const authSource = credentials.source || options.dbName || 'admin';
const user = credentials.username || options.user;
if (typeof authMechanism === 'string' && authMechanism.toUpperCase() !== 'DEFAULT') {
return {};
}
if (!user) {
return {};
}
return { saslSupportedMechs: `${authSource}.${user}` };
return !(conn instanceof Connection);
}
function checkSupportedServer(ismaster, options) {
@@ -97,77 +75,130 @@ function performInitialHandshake(conn, options, _callback) {
_callback(err, ret);
};
let compressors = [];
if (options.compression && options.compression.compressors) {
compressors = options.compression.compressors;
const credentials = options.credentials;
if (credentials) {
if (!credentials.mechanism.match(/DEFAULT/i) && !AUTH_PROVIDERS[credentials.mechanism]) {
callback(new MongoError(`authMechanism '${credentials.mechanism}' not supported`));
return;
}
}
const handshakeDoc = Object.assign(
{
ismaster: true,
client: options.metadata || makeClientMetadata(options),
compression: compressors
},
getSaslSupportedMechs(options)
);
const handshakeOptions = Object.assign({}, options);
// The handshake technically is a monitoring check, so its socket timeout should be connectTimeoutMS
if (options.connectTimeoutMS || options.connectionTimeout) {
handshakeOptions.socketTimeout = options.connectTimeoutMS || options.connectionTimeout;
}
const start = new Date().getTime();
runCommand(conn, 'admin.$cmd', handshakeDoc, handshakeOptions, (err, ismaster) => {
const authContext = new AuthContext(conn, credentials, options);
prepareHandshakeDocument(authContext, (err, handshakeDoc) => {
if (err) {
callback(err);
return;
return callback(err);
}
if (ismaster.ok === 0) {
callback(new MongoError(ismaster));
return;
const handshakeOptions = Object.assign({}, options);
if (options.connectTimeoutMS || options.connectionTimeout) {
// The handshake technically is a monitoring check, so its socket timeout should be connectTimeoutMS
handshakeOptions.socketTimeout = options.connectTimeoutMS || options.connectionTimeout;
}
const supportedServerErr = checkSupportedServer(ismaster, options);
if (supportedServerErr) {
callback(supportedServerErr);
return;
}
handshakeDoc.helloOk = !!options.useUnifiedTopology;
if (!isModernConnectionType(conn)) {
// resolve compression
if (ismaster.compression) {
const agreedCompressors = compressors.filter(
compressor => ismaster.compression.indexOf(compressor) !== -1
);
const start = new Date().getTime();
conn.command('admin.$cmd', handshakeDoc, handshakeOptions, (err, result) => {
if (err) {
callback(err);
return;
}
if (agreedCompressors.length) {
conn.agreedCompressor = agreedCompressors[0];
}
const response = result.result;
if (response.ok === 0) {
callback(new MongoError(response));
return;
}
if (options.compression && options.compression.zlibCompressionLevel) {
conn.zlibCompressionLevel = options.compression.zlibCompressionLevel;
if ('isWritablePrimary' in response) {
// Provide pre-hello-style response document.
response.ismaster = response.isWritablePrimary;
}
if (options.useUnifiedTopology && response.helloOk) {
conn.helloOk = true;
}
const supportedServerErr = checkSupportedServer(response, options);
if (supportedServerErr) {
callback(supportedServerErr);
return;
}
if (!isModernConnectionType(conn)) {
// resolve compression
if (response.compression) {
const agreedCompressors = handshakeDoc.compression.filter(
compressor => response.compression.indexOf(compressor) !== -1
);
if (agreedCompressors.length) {
conn.agreedCompressor = agreedCompressors[0];
}
if (options.compression && options.compression.zlibCompressionLevel) {
conn.zlibCompressionLevel = options.compression.zlibCompressionLevel;
}
}
}
}
// NOTE: This is metadata attached to the connection while porting away from
// handshake being done in the `Server` class. Likely, it should be
// relocated, or at very least restructured.
conn.ismaster = ismaster;
conn.lastIsMasterMS = new Date().getTime() - start;
// NOTE: This is metadata attached to the connection while porting away from
// handshake being done in the `Server` class. Likely, it should be
// relocated, or at very least restructured.
conn.ismaster = response;
conn.lastIsMasterMS = new Date().getTime() - start;
const credentials = options.credentials;
if (!ismaster.arbiterOnly && credentials) {
credentials.resolveAuthMechanism(ismaster);
authenticate(conn, credentials, callback);
if (!response.arbiterOnly && credentials) {
// store the response on auth context
Object.assign(authContext, { response });
const resolvedCredentials = credentials.resolveAuthMechanism(response);
const authProvider = AUTH_PROVIDERS[resolvedCredentials.mechanism];
authProvider.auth(authContext, err => {
if (err) return callback(err);
callback(undefined, conn);
});
return;
}
callback(undefined, conn);
});
});
}
function prepareHandshakeDocument(authContext, callback) {
const options = authContext.options;
const serverApi = authContext.connection.serverApi;
const compressors =
options.compression && options.compression.compressors ? options.compression.compressors : [];
const handshakeDoc = {
[serverApi ? 'hello' : 'ismaster']: true,
client: options.metadata || makeClientMetadata(options),
compression: compressors
};
const credentials = authContext.credentials;
if (credentials) {
if (credentials.mechanism.match(/DEFAULT/i) && credentials.username) {
Object.assign(handshakeDoc, {
saslSupportedMechs: `${credentials.source}.${credentials.username}`
});
AUTH_PROVIDERS['scram-sha-256'].prepare(handshakeDoc, authContext, callback);
return;
}
callback(undefined, conn);
});
const authProvider = AUTH_PROVIDERS[credentials.mechanism];
if (authProvider == null) {
return callback(new MongoError(`No AuthProvider for ${credentials.mechanism} defined.`));
}
authProvider.prepare(handshakeDoc, authContext, callback);
return;
}
callback(undefined, handshakeDoc);
}
const LEGAL_SSL_SOCKET_OPTIONS = [
@@ -227,7 +258,7 @@ function parseSslOptions(family, options) {
}
// Set default sni servername to be the same as host
if (result.servername == null) {
if (result.servername == null && !net.isIP(result.host)) {
result.servername = result.host;
}
@@ -239,7 +270,7 @@ function makeConnection(family, options, cancellationToken, _callback) {
const useSsl = typeof options.ssl === 'boolean' ? options.ssl : false;
const keepAlive = typeof options.keepAlive === 'boolean' ? options.keepAlive : true;
let keepAliveInitialDelay =
typeof options.keepAliveInitialDelay === 'number' ? options.keepAliveInitialDelay : 300000;
typeof options.keepAliveInitialDelay === 'number' ? options.keepAliveInitialDelay : 120000;
const noDelay = typeof options.noDelay === 'boolean' ? options.noDelay : true;
const connectionTimeout =
typeof options.connectionTimeout === 'number'
@@ -247,12 +278,17 @@ function makeConnection(family, options, cancellationToken, _callback) {
: typeof options.connectTimeoutMS === 'number'
? options.connectTimeoutMS
: 30000;
const socketTimeout = typeof options.socketTimeout === 'number' ? options.socketTimeout : 360000;
const socketTimeoutMS =
typeof options.socketTimeoutMS === 'number'
? options.socketTimeoutMS
: typeof options.socketTimeout === 'number'
? options.socketTimeout
: 0;
const rejectUnauthorized =
typeof options.rejectUnauthorized === 'boolean' ? options.rejectUnauthorized : true;
if (keepAliveInitialDelay > socketTimeout) {
keepAliveInitialDelay = Math.round(socketTimeout / 2);
if (keepAliveInitialDelay > socketTimeoutMS) {
keepAliveInitialDelay = Math.round(socketTimeoutMS / 2);
}
let socket;
@@ -305,7 +341,7 @@ function makeConnection(family, options, cancellationToken, _callback) {
return callback(socket.authorizationError);
}
socket.setTimeout(socketTimeout);
socket.setTimeout(socketTimeoutMS);
callback(null, socket);
}
@@ -318,92 +354,12 @@ function makeConnection(family, options, cancellationToken, _callback) {
socket.once(connectEvent, connectHandler);
}
const CONNECTION_ERROR_EVENTS = ['error', 'close', 'timeout', 'parseError'];
function runCommand(conn, ns, command, options, callback) {
if (typeof options === 'function') (callback = options), (options = {});
// are we using the new connection type? if so, no need to simulate a rpc `command` method
if (isModernConnectionType(conn)) {
conn.command(ns, command, options, (err, result) => {
if (err) {
callback(err);
return;
}
// NODE-2382: raw wire protocol messages, or command results should not be used anymore
callback(undefined, result.result);
});
return;
}
const socketTimeout = typeof options.socketTimeout === 'number' ? options.socketTimeout : 360000;
const bson = conn.options.bson;
const query = new Query(bson, ns, command, {
numberToSkip: 0,
numberToReturn: 1
});
const noop = () => {};
function _callback(err, result) {
callback(err, result);
callback = noop;
}
function errorHandler(err) {
conn.resetSocketTimeout();
CONNECTION_ERROR_EVENTS.forEach(eventName => conn.removeListener(eventName, errorHandler));
conn.removeListener('message', messageHandler);
if (err == null) {
err = new MongoError(`runCommand failed for connection to '${conn.address}'`);
}
// ignore all future errors
conn.on('error', noop);
_callback(err);
}
function messageHandler(msg) {
if (msg.responseTo !== query.requestId) {
return;
}
conn.resetSocketTimeout();
CONNECTION_ERROR_EVENTS.forEach(eventName => conn.removeListener(eventName, errorHandler));
conn.removeListener('message', messageHandler);
msg.parse({ promoteValues: true });
_callback(undefined, msg.documents[0]);
}
conn.setSocketTimeout(socketTimeout);
CONNECTION_ERROR_EVENTS.forEach(eventName => conn.once(eventName, errorHandler));
conn.on('message', messageHandler);
conn.write(query.toBin());
}
function authenticate(conn, credentials, callback) {
const mechanism = credentials.mechanism;
if (!AUTH_PROVIDERS[mechanism]) {
callback(new MongoError(`authMechanism '${mechanism}' not supported`));
return;
}
const provider = AUTH_PROVIDERS[mechanism];
provider.auth(runCommand, [conn], credentials, err => {
if (err) return callback(err);
callback(undefined, conn);
});
}
function connectionFailureError(type, err) {
switch (type) {
case 'error':
return new MongoNetworkError(err);
case 'timeout':
return new MongoNetworkError(`connection timed out`);
return new MongoNetworkTimeoutError(`connection timed out`);
case 'close':
return new MongoNetworkError(`connection closed`);
case 'cancel':

View File

@@ -8,12 +8,15 @@ const decompress = require('../wireprotocol/compression').decompress;
const Response = require('./commands').Response;
const BinMsg = require('./msg').BinMsg;
const MongoNetworkError = require('../error').MongoNetworkError;
const MongoNetworkTimeoutError = require('../error').MongoNetworkTimeoutError;
const MongoError = require('../error').MongoError;
const Logger = require('./logger');
const OP_COMPRESSED = require('../wireprotocol/shared').opcodes.OP_COMPRESSED;
const OP_MSG = require('../wireprotocol/shared').opcodes.OP_MSG;
const MESSAGE_HEADER_SIZE = require('../wireprotocol/shared').MESSAGE_HEADER_SIZE;
const Buffer = require('safe-buffer').Buffer;
const Query = require('./commands').Query;
const CommandResult = require('./command_result');
let _id = 0;
@@ -35,6 +38,7 @@ const DEBUG_FIELDS = [
'promoteLongs',
'promoteValues',
'promoteBuffers',
'bsonRegExp',
'checkServerIdentity'
];
@@ -64,12 +68,13 @@ class Connection extends EventEmitter {
* @param {string} [options.host='localhost'] The host the socket is connected to
* @param {number} [options.port=27017] The port used for the socket connection
* @param {boolean} [options.keepAlive=true] TCP Connection keep alive enabled
* @param {number} [options.keepAliveInitialDelay=300000] Initial delay before TCP keep alive enabled
* @param {number} [options.keepAliveInitialDelay=120000] Initial delay before TCP keep alive enabled
* @param {number} [options.connectionTimeout=30000] TCP Connection timeout setting
* @param {number} [options.socketTimeout=360000] TCP Socket timeout setting
* @param {number} [options.socketTimeout=0] TCP Socket timeout setting
* @param {boolean} [options.promoteLongs] Convert Long values from the db into Numbers if they fit into 53 bits
* @param {boolean} [options.promoteValues] Promotes BSON values to native types where possible, set to false to only receive wrapper types.
* @param {boolean} [options.promoteBuffers] Promotes Binary BSON values to native Node Buffers.
* @param {boolean} [options.bsonRegExp] By default, regex returned from MDB will be native to the language. Setting to true will ensure that a BSON.BSONRegExp object is returned.
* @param {number} [options.maxBsonMessageSize=0x4000000] Largest possible size of a BSON message (for legacy purposes)
*/
constructor(socket, options) {
@@ -86,15 +91,16 @@ class Connection extends EventEmitter {
this.bson = options.bson;
this.tag = options.tag;
this.maxBsonMessageSize = options.maxBsonMessageSize || DEFAULT_MAX_BSON_MESSAGE_SIZE;
this.helloOk = undefined;
this.port = options.port || 27017;
this.host = options.host || 'localhost';
this.socketTimeout = typeof options.socketTimeout === 'number' ? options.socketTimeout : 360000;
this.socketTimeout = typeof options.socketTimeout === 'number' ? options.socketTimeout : 0;
// These values are inspected directly in tests, but maybe not necessary to keep around
this.keepAlive = typeof options.keepAlive === 'boolean' ? options.keepAlive : true;
this.keepAliveInitialDelay =
typeof options.keepAliveInitialDelay === 'number' ? options.keepAliveInitialDelay : 300000;
typeof options.keepAliveInitialDelay === 'number' ? options.keepAliveInitialDelay : 120000;
this.connectionTimeout =
typeof options.connectionTimeout === 'number' ? options.connectionTimeout : 30000;
if (this.keepAliveInitialDelay > this.socketTimeout) {
@@ -114,7 +120,8 @@ class Connection extends EventEmitter {
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
promoteBuffers: typeof options.promoteBuffers === 'boolean' ? options.promoteBuffers : false,
bsonRegExp: typeof options.bsonRegExp === 'boolean' ? options.bsonRegExp : false
};
// Flushing
@@ -184,6 +191,7 @@ class Connection extends EventEmitter {
* Unref this connection
* @method
* @return {boolean}
* @deprecated This function is deprecated and will be removed in the next major version.
*/
unref() {
if (this.socket == null) {
@@ -251,10 +259,10 @@ class Connection extends EventEmitter {
// Debug Log
if (this.logger.isDebug()) {
if (!Array.isArray(buffer)) {
this.logger.debug(`writing buffer [${buffer.toString('hex')}] to ${this.address}`);
this.logger.debug(`writing buffer [ ${buffer.length} ] to ${this.address}`);
} else {
for (let i = 0; i < buffer.length; i++)
this.logger.debug(`writing buffer [${buffer[i].toString('hex')}] to ${this.address}`);
this.logger.debug(`writing buffer [ ${buffer[i].length} ] to ${this.address}`);
}
}
@@ -305,8 +313,70 @@ class Connection extends EventEmitter {
if (this.destroyed) return false;
return !this.socket.destroyed && this.socket.writable;
}
/**
* @ignore
*/
command(ns, command, options, callback) {
if (typeof options === 'function') (callback = options), (options = {});
const conn = this;
const socketTimeout = typeof options.socketTimeout === 'number' ? options.socketTimeout : 0;
const bson = conn.options.bson;
const query = new Query(bson, ns, command, {
numberToSkip: 0,
numberToReturn: 1
});
const noop = () => {};
function _callback(err, result) {
callback(err, result);
callback = noop;
}
function errorHandler(err) {
conn.resetSocketTimeout();
CONNECTION_ERROR_EVENTS.forEach(eventName => conn.removeListener(eventName, errorHandler));
conn.removeListener('message', messageHandler);
if (err == null) {
err = new MongoError(`runCommand failed for connection to '${conn.address}'`);
}
// ignore all future errors
conn.on('error', noop);
_callback(err);
}
function messageHandler(msg) {
if (msg.responseTo !== query.requestId) {
return;
}
conn.resetSocketTimeout();
CONNECTION_ERROR_EVENTS.forEach(eventName => conn.removeListener(eventName, errorHandler));
conn.removeListener('message', messageHandler);
msg.parse({ promoteValues: true });
const response = msg.documents[0];
if (response.ok === 0 || response.$err || response.errmsg || response.code) {
_callback(new MongoError(response));
return;
}
_callback(undefined, new CommandResult(response, this, msg));
}
conn.setSocketTimeout(socketTimeout);
CONNECTION_ERROR_EVENTS.forEach(eventName => conn.once(eventName, errorHandler));
conn.on('message', messageHandler);
conn.write(query.toBin());
}
}
const CONNECTION_ERROR_EVENTS = ['error', 'close', 'timeout', 'parseError'];
function deleteConnection(id) {
// console.log("=== deleted connection " + id + " :: " + (connections[id] ? connections[id].port : ''))
delete connections[id];
@@ -352,7 +422,9 @@ function timeoutHandler(conn) {
conn.timedOut = true;
conn.emit(
'timeout',
new MongoNetworkError(`connection ${conn.id} to ${conn.address} timed out`),
new MongoNetworkTimeoutError(`connection ${conn.id} to ${conn.address} timed out`, {
beforeHandshake: conn.ismaster == null
}),
conn
);
};

View File

@@ -37,6 +37,7 @@ var Logger = function(className, options) {
if (options.logger) {
currentLogger = options.logger;
} else if (currentLogger == null) {
// eslint-disable-next-line no-console
currentLogger = console.log;
}

View File

@@ -31,6 +31,7 @@ const Buffer = require('safe-buffer').Buffer;
const opcodes = require('../wireprotocol/shared').opcodes;
const databaseNamespace = require('../wireprotocol/shared').databaseNamespace;
const ReadPreference = require('../topologies/read_preference');
const MongoError = require('../../core/error').MongoError;
// Incrementing request id
let _requestId = 0;
@@ -138,7 +139,12 @@ Msg.getRequestId = function() {
class BinMsg {
constructor(bson, message, msgHeader, msgBody, opts) {
opts = opts || { promoteLongs: true, promoteValues: true, promoteBuffers: false };
opts = opts || {
promoteLongs: true,
promoteValues: true,
promoteBuffers: false,
bsonRegExp: false
};
this.parsed = false;
this.raw = message;
this.data = msgBody;
@@ -160,6 +166,7 @@ class BinMsg {
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;
this.bsonRegExp = typeof opts.bsonRegExp === 'boolean' ? opts.bsonRegExp : false;
this.documents = [];
}
@@ -185,18 +192,22 @@ class BinMsg {
typeof options.promoteBuffers === 'boolean'
? options.promoteBuffers
: this.opts.promoteBuffers;
const bsonRegExp =
typeof options.bsonRegExp === 'boolean' ? options.bsonRegExp : this.opts.bsonRegExp;
// Set up the options
const _options = {
promoteLongs: promoteLongs,
promoteValues: promoteValues,
promoteBuffers: promoteBuffers
promoteBuffers: promoteBuffers,
bsonRegExp: bsonRegExp
};
while (this.index < this.data.length) {
const payloadType = this.data.readUInt8(this.index++);
if (payloadType === 1) {
console.error('TYPE 1');
// It was decided that no driver makes use of payload type 1
throw new MongoError('OP_MSG Payload Type 1 detected unsupported protocol');
} else if (payloadType === 0) {
const bsonSize = this.data.readUInt32LE(this.index);
const bin = this.data.slice(this.index, this.index + bsonSize);

View File

@@ -60,11 +60,11 @@ var _id = 0;
* @param {number} [options.reconnectTries=30] Server attempt to reconnect #times
* @param {number} [options.reconnectInterval=1000] Server will wait # milliseconds between retries
* @param {boolean} [options.keepAlive=true] TCP Connection keep alive enabled
* @param {number} [options.keepAliveInitialDelay=300000] Initial delay before TCP keep alive enabled
* @param {number} [options.keepAliveInitialDelay=120000] 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 {number} [options.monitoringSocketTimeout=30000] TCP Socket timeout setting for replicaset monitoring socket
* @param {number} [options.socketTimeout=0] TCP Socket timeout setting
* @param {number} [options.monitoringSocketTimeout=0] TCP Socket timeout setting for replicaset monitoring socket
* @param {boolean} [options.ssl=false] Use SSL for connection
* @param {boolean|function} [options.checkServerIdentity=true] Ensure we check server identify during SSL, set to false to disable checking. Only works for Node 0.12.x or higher. You can pass in a boolean or your own checkServerIdentity override function.
* @param {Buffer} [options.ca] SSL Certificate store binary buffer
@@ -76,6 +76,7 @@ var _id = 0;
* @param {boolean} [options.promoteLongs=true] Convert Long values from the db into Numbers if they fit into 53 bits
* @param {boolean} [options.promoteValues=true] Promotes BSON values to native types where possible, set to false to only receive wrapper types.
* @param {boolean} [options.promoteBuffers=false] Promotes Binary BSON values to native Node Buffers.
* @param {boolean} [options.bsonRegExp=false] By default, regex returned from MDB will be native to the language. Setting to true will ensure that a BSON.BSONRegExp object is returned.
* @param {boolean} [options.domainsEnabled=false] Enable the wrapping of the callback in the current domain, disabled by default to avoid perf hit.
* @fires Pool#connect
* @fires Pool#close
@@ -111,9 +112,9 @@ var Pool = function(topology, options) {
minSize: 0,
// socket settings
connectionTimeout: 30000,
socketTimeout: 360000,
socketTimeout: 0,
keepAlive: true,
keepAliveInitialDelay: 300000,
keepAliveInitialDelay: 120000,
noDelay: true,
// SSL Settings
ssl: false,
@@ -127,6 +128,7 @@ var Pool = function(topology, options) {
promoteLongs: true,
promoteValues: true,
promoteBuffers: false,
bsonRegExp: false,
// Reconnection options
reconnect: true,
reconnectInterval: 1000,
@@ -390,8 +392,8 @@ function messageHandler(self) {
if (self.logger.isDebug()) {
self.logger.debug(
f(
'message [%s] received from %s:%s',
message.raw.toString('hex'),
'message [ %s ] received from %s:%s',
message.raw.length,
self.options.host,
self.options.port
)
@@ -602,6 +604,7 @@ Pool.prototype.logout = function(dbName, callback) {
/**
* Unref the pool
* @method
* @deprecated This function is deprecated and will be removed in the next major version.
*/
Pool.prototype.unref = function() {
// Get all the known connections
@@ -870,6 +873,7 @@ Pool.prototype.write = function(command, options, cb) {
promoteLongs: true,
promoteValues: true,
promoteBuffers: false,
bsonRegExp: false,
fullResult: false
};
@@ -879,6 +883,7 @@ Pool.prototype.write = function(command, options, cb) {
typeof options.promoteValues === 'boolean' ? options.promoteValues : true;
operation.promoteBuffers =
typeof options.promoteBuffers === 'boolean' ? options.promoteBuffers : false;
operation.bsonRegExp = typeof options.bsonRegExp === 'boolean' ? options.bsonRegExp : false;
operation.raw = typeof options.raw === 'boolean' ? options.raw : false;
operation.immediateRelease =
typeof options.immediateRelease === 'boolean' ? options.immediateRelease : false;

View File

@@ -1,9 +1,12 @@
'use strict';
const require_optional = require('require_optional');
const parsePackageVersion = require('../../utils').parsePackageVersion;
const MongoError = require('../error').MongoError;
const require_optional = require('optional-require')(require);
function debugOptions(debugFields, options) {
var finaloptions = {};
const finaloptions = {};
debugFields.forEach(function(n) {
finaloptions[n] = options[n];
});
@@ -12,16 +15,22 @@ function debugOptions(debugFields, options) {
}
function retrieveBSON() {
var BSON = require('bson');
const BSON = require('bson');
BSON.native = false;
try {
var optionalBSON = require_optional('bson-ext');
if (optionalBSON) {
optionalBSON.native = true;
return optionalBSON;
const optionalBSON = require_optional('bson-ext');
const bsonExtVersion = parsePackageVersion(
require_optional('bson-ext/package.json') || { version: '0.0.0' }
);
if (optionalBSON) {
if (bsonExtVersion.major >= 4) {
throw new MongoError(
'bson-ext version 4 and above does not work with the 3.x version of the mongodb driver'
);
}
} catch (err) {} // eslint-disable-line
optionalBSON.native = true;
return optionalBSON;
}
return BSON;
}
@@ -33,24 +42,43 @@ function noSnappyWarning() {
);
}
const PKG_VERSION = Symbol('kPkgVersion');
// Facilitate loading Snappy optionally
function retrieveSnappy() {
var snappy = null;
try {
snappy = require_optional('snappy');
} catch (error) {} // eslint-disable-line
const snappy = require_optional('snappy');
if (!snappy) {
snappy = {
return {
compress: noSnappyWarning,
uncompress: noSnappyWarning,
compressSync: noSnappyWarning,
uncompressSync: noSnappyWarning
};
}
const snappyPkg = require_optional('snappy/package.json') || { version: '0.0.0' };
const version = parsePackageVersion(snappyPkg);
snappy[PKG_VERSION] = version;
if (version.major >= 7) {
const compressOriginal = snappy.compress;
const uncompressOriginal = snappy.uncompress;
snappy.compress = (data, callback) => {
compressOriginal(data)
.then(res => callback(undefined, res))
.catch(error => callback(error));
};
snappy.uncompress = (data, callback) => {
uncompressOriginal(data)
.then(res => callback(undefined, res))
.catch(error => callback(error));
};
}
return snappy;
}
module.exports = {
PKG_VERSION,
debugOptions,
retrieveBSON,
retrieveSnappy

View File

@@ -11,6 +11,7 @@ const executeOperation = require('../operations/execute_operation');
const Readable = require('stream').Readable;
const SUPPORTS = require('../utils').SUPPORTS;
const MongoDBNamespace = require('../utils').MongoDBNamespace;
const mergeOptions = require('../utils').mergeOptions;
const OperationBase = require('../operations/operation').OperationBase;
const BSON = retrieveBSON();
@@ -145,6 +146,13 @@ class CoreCursor extends Readable {
this.cursorState.promoteBuffers = options.promoteBuffers;
}
// Add bsonRegExp to cursor state
if (typeof topologyOptions.bsonRegExp === 'boolean') {
this.cursorState.bsonRegExp = topologyOptions.bsonRegExp;
} else if (typeof options.bsonRegExp === 'boolean') {
this.cursorState.bsonRegExp = options.bsonRegExp;
}
if (topologyOptions.reconnect) {
this.cursorState.reconnect = topologyOptions.reconnect;
}
@@ -207,7 +215,9 @@ class CoreCursor extends Readable {
* @return {Cursor}
*/
clone() {
return this.topology.cursor(this.ns, this.cmd, this.options);
const clonedOptions = mergeOptions({}, this.options);
delete clonedOptions.session;
return this.topology.cursor(this.ns, this.cmd, clonedOptions);
}
/**
@@ -464,50 +474,41 @@ class CoreCursor extends Readable {
}
const result = r.message;
if (result.queryFailure) {
return done(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 done(new MongoError(result.documents[0]), null);
if (Array.isArray(result.documents) && result.documents.length === 1) {
const document = result.documents[0];
if (result.queryFailure) {
return done(new MongoError(document), 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.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();
// Check if we have a command cursor
if (!cursor.cmd.find || (cursor.cmd.find && cursor.cmd.virtual === false)) {
// We have an error document, return the error
if (document.$err || document.errmsg) {
return done(new MongoError(document), null);
}
// Return after processing command cursor
return done(null, result);
}
// We have a cursor document
if (document.cursor != null && typeof document.cursor !== 'string') {
const id = document.cursor.id;
// If we have a namespace change set the new namespace for getmores
if (document.cursor.ns) {
cursor.ns = document.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 = document.operationTime;
if (Array.isArray(result.documents[0].result)) {
cursor.cursorState.documents = result.documents[0].result;
cursor.cursorState.cursorId = Long.ZERO;
return done(null, result);
// If we have a firstBatch set it
if (Array.isArray(document.cursor.firstBatch)) {
cursor.cursorState.documents = document.cursor.firstBatch; //.reverse();
}
// Return after processing command cursor
return done(null, result);
}
}
}
@@ -754,9 +755,10 @@ function nextFunction(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);
self.kill(() =>
// Set cursor in dead and notified state
setCursorDeadAndNotified(self, callback)
);
} else if (
self.cursorState.cursorIndex === self.cursorState.documents.length &&
!Long.ZERO.equals(self.cursorState.cursorId)
@@ -836,9 +838,12 @@ function nextFunction(self, callback) {
} else {
if (self.cursorState.limit > 0 && self.cursorState.currentLimit >= self.cursorState.limit) {
// Ensure we kill the cursor on the server
self.kill();
// Set cursor in dead and notified state
return setCursorDeadAndNotified(self, callback);
self.kill(() =>
// Set cursor in dead and notified state
setCursorDeadAndNotified(self, callback)
);
return;
}
// Increment the current cursor limit
@@ -850,11 +855,14 @@ function nextFunction(self, callback) {
// 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));
});
self.kill(() =>
// Set cursor in dead and notified state
setCursorDeadAndNotified(self, function() {
handleCallback(callback, new MongoError(doc ? doc.$err : undefined));
})
);
return;
}
// Transform the doc with passed in transformation method if provided

View File

@@ -1,5 +1,9 @@
'use strict';
const MONGODB_ERROR_CODES = require('../error_codes').MONGODB_ERROR_CODES;
const kErrorLabels = Symbol('errorLabels');
/**
* Creates a new MongoError
*
@@ -18,8 +22,12 @@ class MongoError extends Error {
super(message);
} else {
super(message.message || message.errmsg || message.$err || 'n/a');
if (message.errorLabels) {
this[kErrorLabels] = new Set(message.errorLabels);
}
for (var name in message) {
if (name === 'errmsg') {
if (name === 'errorLabels' || name === 'errmsg') {
continue;
}
@@ -57,8 +65,29 @@ class MongoError extends Error {
* @returns {boolean} returns true if the error has the provided error label
*/
hasErrorLabel(label) {
return this.errorLabels && this.errorLabels.indexOf(label) !== -1;
if (this[kErrorLabels] == null) {
return false;
}
return this[kErrorLabels].has(label);
}
addErrorLabel(label) {
if (this[kErrorLabels] == null) {
this[kErrorLabels] = new Set();
}
this[kErrorLabels].add(label);
}
get errorLabels() {
return this[kErrorLabels] ? Array.from(this[kErrorLabels]) : [];
}
}
const kBeforeHandshake = Symbol('beforeHandshake');
function isNetworkErrorBeforeHandshake(err) {
return err[kBeforeHandshake] === true;
}
/**
@@ -71,9 +100,28 @@ class MongoError extends Error {
* @extends MongoError
*/
class MongoNetworkError extends MongoError {
constructor(message) {
constructor(message, options) {
super(message);
this.name = 'MongoNetworkError';
if (options && typeof options.beforeHandshake === 'boolean') {
this[kBeforeHandshake] = options.beforeHandshake;
}
}
}
/**
* An error indicating a network timeout occurred
*
* @param {Error|string|object} message The error message
* @property {string} message The error message
* @property {object} [options.beforeHandshake] Indicates the timeout happened before a connection handshake completed
* @extends MongoError
*/
class MongoNetworkTimeoutError extends MongoNetworkError {
constructor(message, options) {
super(message, options);
this.name = 'MongoNetworkTimeoutError';
}
}
@@ -158,6 +206,10 @@ class MongoWriteConcernError extends MongoError {
super(message);
this.name = 'MongoWriteConcernError';
if (result && Array.isArray(result.errorLabels)) {
this[kErrorLabels] = new Set(result.errorLabels);
}
if (result != null) {
this.result = makeWriteConcernResultObject(result);
}
@@ -166,19 +218,45 @@ class MongoWriteConcernError extends MongoError {
// 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
MONGODB_ERROR_CODES.HostUnreachable,
MONGODB_ERROR_CODES.HostNotFound,
MONGODB_ERROR_CODES.NetworkTimeout,
MONGODB_ERROR_CODES.ShutdownInProgress,
MONGODB_ERROR_CODES.PrimarySteppedDown,
MONGODB_ERROR_CODES.SocketException,
MONGODB_ERROR_CODES.NotMaster,
MONGODB_ERROR_CODES.InterruptedAtShutdown,
MONGODB_ERROR_CODES.InterruptedDueToReplStateChange,
MONGODB_ERROR_CODES.NotMasterNoSlaveOk,
MONGODB_ERROR_CODES.NotMasterOrSecondary
]);
const RETRYABLE_WRITE_ERROR_CODES = new Set([
MONGODB_ERROR_CODES.InterruptedAtShutdown,
MONGODB_ERROR_CODES.InterruptedDueToReplStateChange,
MONGODB_ERROR_CODES.NotMaster,
MONGODB_ERROR_CODES.NotMasterNoSlaveOk,
MONGODB_ERROR_CODES.NotMasterOrSecondary,
MONGODB_ERROR_CODES.PrimarySteppedDown,
MONGODB_ERROR_CODES.ShutdownInProgress,
MONGODB_ERROR_CODES.HostNotFound,
MONGODB_ERROR_CODES.HostUnreachable,
MONGODB_ERROR_CODES.NetworkTimeout,
MONGODB_ERROR_CODES.SocketException,
MONGODB_ERROR_CODES.ExceededTimeLimit
]);
function isRetryableWriteError(error) {
if (error instanceof MongoWriteConcernError) {
return (
RETRYABLE_WRITE_ERROR_CODES.has(error.code) ||
RETRYABLE_WRITE_ERROR_CODES.has(error.result.code)
);
}
return RETRYABLE_WRITE_ERROR_CODES.has(error.code);
}
/**
* Determines whether an error is something the driver should attempt to retry
*
@@ -195,41 +273,44 @@ function isRetryableError(error) {
}
const SDAM_RECOVERING_CODES = new Set([
91, // ShutdownInProgress
189, // PrimarySteppedDown
11600, // InterruptedAtShutdown
11602, // InterruptedDueToReplStateChange
13436 // NotMasterOrSecondary
MONGODB_ERROR_CODES.ShutdownInProgress,
MONGODB_ERROR_CODES.PrimarySteppedDown,
MONGODB_ERROR_CODES.InterruptedAtShutdown,
MONGODB_ERROR_CODES.InterruptedDueToReplStateChange,
MONGODB_ERROR_CODES.NotMasterOrSecondary
]);
const SDAM_NOTMASTER_CODES = new Set([
10107, // NotMaster
13435 // NotMasterNoSlaveOk
MONGODB_ERROR_CODES.NotMaster,
MONGODB_ERROR_CODES.NotMasterNoSlaveOk,
MONGODB_ERROR_CODES.LegacyNotPrimary
]);
const SDAM_NODE_SHUTTING_DOWN_ERROR_CODES = new Set([
11600, // InterruptedAtShutdown
91 // ShutdownInProgress
MONGODB_ERROR_CODES.InterruptedAtShutdown,
MONGODB_ERROR_CODES.ShutdownInProgress
]);
function isRecoveringError(err) {
if (err.code && SDAM_RECOVERING_CODES.has(err.code)) {
return true;
if (typeof err.code === 'number') {
// If any error code exists, we ignore the error.message
return SDAM_RECOVERING_CODES.has(err.code);
}
return err.message.match(/not master or secondary/) || err.message.match(/node is recovering/);
return /not master or secondary/.test(err.message) || /node is recovering/.test(err.message);
}
function isNotMasterError(err) {
if (err.code && SDAM_NOTMASTER_CODES.has(err.code)) {
return true;
if (typeof err.code === 'number') {
// If any error code exists, we ignore the error.message
return SDAM_NOTMASTER_CODES.has(err.code);
}
if (isRecoveringError(err)) {
return false;
}
return err.message.match(/not master/);
return /not master/.test(err.message);
}
function isNodeShuttingDownError(err) {
@@ -240,10 +321,9 @@ function isNodeShuttingDownError(err) {
* Determines whether SDAM can recover from a given error. If it cannot
* then the pool will be cleared, and server state will completely reset
* locally.
*
* @ignore
* @see https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/server-discovery-and-monitoring.rst#not-master-and-node-is-recovering
* @param {MongoError|Error} error
* @param {MongoError} error
* @returns {boolean}
*/
function isSDAMUnrecoverableError(error) {
// NOTE: null check is here for a strictly pre-CMAP world, a timeout or
@@ -252,20 +332,13 @@ function isSDAMUnrecoverableError(error) {
return true;
}
if (isRecoveringError(error) || isNotMasterError(error)) {
return true;
}
return false;
}
function isNetworkTimeoutError(err) {
return err instanceof MongoNetworkError && err.message.match(/timed out/);
return isRecoveringError(error) || isNotMasterError(error);
}
module.exports = {
MongoError,
MongoNetworkError,
MongoNetworkTimeoutError,
MongoParseError,
MongoTimeoutError,
MongoServerSelectionError,
@@ -273,5 +346,6 @@ module.exports = {
isRetryableError,
isSDAMUnrecoverableError,
isNodeShuttingDownError,
isNetworkTimeoutError
isRetryableWriteError,
isNetworkErrorBeforeHandshake
};

View File

@@ -1,10 +1,11 @@
'use strict';
let BSON = require('bson');
const require_optional = require('require_optional');
const require_optional = require('optional-require')(require);
const EJSON = require('./utils').retrieveEJSON();
try {
// Ensure you always wrap an optional require in the try block NODE-3199
// Attempt to grab the native BSON parser
const BSONNative = require_optional('bson-ext');
// If we got the native parser, use it instead of the
@@ -14,7 +15,16 @@ try {
}
} catch (err) {} // eslint-disable-line
/** An enumeration of valid server API versions */
const ServerApiVersion = Object.freeze({
v1: '1'
});
const ValidServerApiVersions = Object.keys(ServerApiVersion).map(key => ServerApiVersion[key]);
module.exports = {
// Versioned API
ServerApiVersion,
ValidServerApiVersions,
// Errors
MongoError: require('./error').MongoError,
MongoNetworkError: require('./error').MongoNetworkError,

View File

@@ -28,6 +28,13 @@ const ServerType = {
Unknown: 'Unknown'
};
// helper to get a server's type that works for both legacy and unified topologies
function serverType(server) {
let description = server.s.description || server.s.serverDescription;
if (description.topologyType === TopologyType.Single) return description.servers[0].type;
return description.type;
}
const TOPOLOGY_DEFAULTS = {
useUnifiedTopology: true,
localThresholdMS: 15,
@@ -54,6 +61,7 @@ module.exports = {
TOPOLOGY_DEFAULTS,
TopologyType,
ServerType,
serverType,
drainTimerQueue,
clearAndRemoveTimerFrom
};

View File

@@ -6,7 +6,8 @@ const connect = require('../connection/connect');
const Connection = require('../../cmap/connection').Connection;
const common = require('./common');
const makeStateMachine = require('../utils').makeStateMachine;
const MongoError = require('../error').MongoError;
const MongoNetworkError = require('../error').MongoNetworkError;
const BSON = require('../connection/utils').retrieveBSON();
const makeInterruptableAsyncInterval = require('../../utils').makeInterruptableAsyncInterval;
const calculateDurationInMs = require('../../utils').calculateDurationInMs;
const now = require('../../utils').now;
@@ -20,13 +21,15 @@ const kServer = Symbol('server');
const kMonitorId = Symbol('monitorId');
const kConnection = Symbol('connection');
const kCancellationToken = Symbol('cancellationToken');
const kRTTPinger = Symbol('rttPinger');
const kRoundTripTime = Symbol('roundTripTime');
const STATE_CLOSED = common.STATE_CLOSED;
const STATE_CLOSING = common.STATE_CLOSING;
const STATE_IDLE = 'idle';
const STATE_MONITORING = 'monitoring';
const stateTransition = makeStateMachine({
[STATE_CLOSING]: [STATE_CLOSING, STATE_CLOSED],
[STATE_CLOSING]: [STATE_CLOSING, STATE_IDLE, STATE_CLOSED],
[STATE_CLOSED]: [STATE_CLOSED, STATE_MONITORING],
[STATE_IDLE]: [STATE_IDLE, STATE_MONITORING, STATE_CLOSING],
[STATE_MONITORING]: [STATE_MONITORING, STATE_IDLE, STATE_CLOSING]
@@ -62,32 +65,39 @@ class Monitor extends EventEmitter {
heartbeatFrequencyMS:
typeof options.heartbeatFrequencyMS === 'number' ? options.heartbeatFrequencyMS : 10000,
minHeartbeatFrequencyMS:
typeof options.minHeartbeatFrequencyMS === 'number' ? options.minHeartbeatFrequencyMS : 500
typeof options.minHeartbeatFrequencyMS === 'number' ? options.minHeartbeatFrequencyMS : 500,
useUnifiedTopology: options.useUnifiedTopology
});
// TODO: refactor this to pull it directly from the pool, requires new ConnectionPool integration
const addressParts = server.description.address.split(':');
this.connectOptions = Object.freeze(
Object.assign(
{
id: '<monitor>',
host: addressParts[0],
port: parseInt(addressParts[1], 10),
bson: server.s.bson,
connectionType: Connection
},
server.s.options,
this.options,
const connectOptions = Object.assign(
{
id: '<monitor>',
host: server.description.host,
port: server.description.port,
bson: server.s.bson,
connectionType: Connection
},
server.s.options,
this.options,
// force BSON serialization options
{
raw: false,
promoteLongs: true,
promoteValues: true,
promoteBuffers: true
}
)
// force BSON serialization options
{
raw: false,
promoteLongs: true,
promoteValues: true,
promoteBuffers: true,
bsonRegExp: true
}
);
// ensure no authentication is used for monitoring
delete connectOptions.credentials;
// ensure encryption is not requested for monitoring
delete connectOptions.autoEncrypter;
this.connectOptions = Object.freeze(connectOptions);
}
connect() {
@@ -113,88 +123,182 @@ class Monitor extends EventEmitter {
this[kMonitorId].wake();
}
reset() {
const topologyVersion = this[kServer].description.topologyVersion;
if (isInCloseState(this) || topologyVersion == null) {
return;
}
stateTransition(this, STATE_CLOSING);
resetMonitorState(this);
// restart monitor
stateTransition(this, STATE_IDLE);
// restart monitoring
const heartbeatFrequencyMS = this.options.heartbeatFrequencyMS;
const minHeartbeatFrequencyMS = this.options.minHeartbeatFrequencyMS;
this[kMonitorId] = makeInterruptableAsyncInterval(monitorServer(this), {
interval: heartbeatFrequencyMS,
minInterval: minHeartbeatFrequencyMS
});
}
close() {
if (isInCloseState(this)) {
return;
}
stateTransition(this, STATE_CLOSING);
this[kCancellationToken].emit('cancel');
if (this[kMonitorId]) {
this[kMonitorId].stop();
this[kMonitorId] = null;
}
if (this[kConnection]) {
this[kConnection].destroy({ force: true });
}
resetMonitorState(this);
// close monitor
this.emit('close');
stateTransition(this, STATE_CLOSED);
}
}
function checkServer(monitor, callback) {
if (monitor[kConnection] && monitor[kConnection].closed) {
monitor[kConnection] = undefined;
function resetMonitorState(monitor) {
if (monitor[kMonitorId]) {
monitor[kMonitorId].stop();
monitor[kMonitorId] = null;
}
const start = now();
if (monitor[kRTTPinger]) {
monitor[kRTTPinger].close();
monitor[kRTTPinger] = undefined;
}
monitor[kCancellationToken].emit('cancel');
if (monitor[kMonitorId]) {
clearTimeout(monitor[kMonitorId]);
monitor[kMonitorId] = undefined;
}
if (monitor[kConnection]) {
monitor[kConnection].destroy({ force: true });
}
}
function checkServer(monitor, callback) {
let start = now();
monitor.emit('serverHeartbeatStarted', new ServerHeartbeatStartedEvent(monitor.address));
function failureHandler(err) {
if (monitor[kConnection]) {
monitor[kConnection].destroy({ force: true });
monitor[kConnection] = undefined;
}
monitor.emit(
'serverHeartbeatFailed',
new ServerHeartbeatFailedEvent(calculateDurationInMs(start), err, monitor.address)
);
monitor.emit('resetServer', err);
monitor.emit('resetConnectionPool');
callback(err);
}
function successHandler(isMaster) {
monitor.emit(
'serverHeartbeatSucceeded',
new ServerHeartbeatSucceededEvent(calculateDurationInMs(start), isMaster, monitor.address)
);
return callback(undefined, isMaster);
}
if (monitor[kConnection] != null) {
if (monitor[kConnection] != null && !monitor[kConnection].closed) {
const connectTimeoutMS = monitor.options.connectTimeoutMS;
monitor[kConnection].command(
'admin.$cmd',
{ ismaster: true },
{ socketTimeout: connectTimeoutMS },
(err, result) => {
if (err) {
failureHandler(err);
return;
const maxAwaitTimeMS = monitor.options.heartbeatFrequencyMS;
const topologyVersion = monitor[kServer].description.topologyVersion;
const isAwaitable = topologyVersion != null;
const serverApi = monitor[kConnection].serverApi;
const helloOk = monitor[kConnection].helloOk;
const cmd = {
[serverApi || helloOk ? 'hello' : 'ismaster']: true
};
// written this way omit helloOk from the command if its false-y (do not want -> helloOk: null)
if (helloOk) cmd.helloOk = helloOk;
const options = { socketTimeout: connectTimeoutMS };
if (isAwaitable) {
cmd.maxAwaitTimeMS = maxAwaitTimeMS;
cmd.topologyVersion = makeTopologyVersion(topologyVersion);
if (connectTimeoutMS) {
options.socketTimeout = connectTimeoutMS + maxAwaitTimeMS;
}
options.exhaustAllowed = true;
if (monitor[kRTTPinger] == null) {
monitor[kRTTPinger] = new RTTPinger(monitor[kCancellationToken], monitor.connectOptions);
}
}
monitor[kConnection].command('admin.$cmd', cmd, options, (err, result) => {
if (err) {
failureHandler(err);
return;
}
const isMaster = result.result;
const rttPinger = monitor[kRTTPinger];
if ('isWritablePrimary' in isMaster) {
// Provide pre-hello-style response document.
isMaster.ismaster = isMaster.isWritablePrimary;
}
const duration =
isAwaitable && rttPinger ? rttPinger.roundTripTime : calculateDurationInMs(start);
monitor.emit(
'serverHeartbeatSucceeded',
new ServerHeartbeatSucceededEvent(duration, isMaster, monitor.address)
);
// if we are using the streaming protocol then we immediately issue another `started`
// event, otherwise the "check" is complete and return to the main monitor loop
if (isAwaitable && isMaster.topologyVersion) {
monitor.emit('serverHeartbeatStarted', new ServerHeartbeatStartedEvent(monitor.address));
start = now();
} else {
if (monitor[kRTTPinger]) {
monitor[kRTTPinger].close();
monitor[kRTTPinger] = undefined;
}
successHandler(result.result);
callback(undefined, isMaster);
}
);
});
return;
}
// connecting does an implicit `ismaster`
connect(monitor.connectOptions, monitor[kCancellationToken], (err, conn) => {
if (conn && isInCloseState(monitor)) {
conn.destroy({ force: true });
return;
}
if (err) {
monitor[kConnection] = undefined;
// we already reset the connection pool on network errors in all cases
if (!(err instanceof MongoNetworkError)) {
monitor.emit('resetConnectionPool');
}
failureHandler(err);
return;
}
if (isInCloseState(monitor)) {
conn.destroy({ force: true });
failureHandler(new MongoError('monitor was destroyed'));
return;
}
monitor[kConnection] = conn;
successHandler(conn.ismaster);
monitor.emit(
'serverHeartbeatSucceeded',
new ServerHeartbeatSucceededEvent(
calculateDurationInMs(start),
conn.ismaster,
monitor.address
)
);
callback(undefined, conn.ismaster);
});
}
@@ -212,33 +316,113 @@ function monitorServer(monitor) {
// TODO: the next line is a legacy event, remove in v4
process.nextTick(() => monitor.emit('monitoring', monitor[kServer]));
checkServer(monitor, e0 => {
if (e0 == null) {
return done();
}
// otherwise an error occured on initial discovery, also bail
if (monitor[kServer].description.type === ServerType.Unknown) {
monitor.emit('resetServer', e0);
return done();
}
// 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.
monitor.emit('resetConnectionPool');
checkServer(monitor, e1 => {
if (e1) {
monitor.emit('resetServer', e1);
checkServer(monitor, (err, isMaster) => {
if (err) {
// otherwise an error occured on initial discovery, also bail
if (monitor[kServer].description.type === ServerType.Unknown) {
monitor.emit('resetServer', err);
return done();
}
}
done();
});
// if the check indicates streaming is supported, immediately reschedule monitoring
if (isMaster && isMaster.topologyVersion) {
setTimeout(() => {
if (!isInCloseState(monitor)) {
monitor[kMonitorId].wake();
}
});
}
done();
});
};
}
function makeTopologyVersion(tv) {
return {
processId: tv.processId,
counter: BSON.Long.fromNumber(tv.counter)
};
}
class RTTPinger {
constructor(cancellationToken, options) {
this[kConnection] = null;
this[kCancellationToken] = cancellationToken;
this[kRoundTripTime] = 0;
this.closed = false;
const heartbeatFrequencyMS = options.heartbeatFrequencyMS;
this[kMonitorId] = setTimeout(() => measureRoundTripTime(this, options), heartbeatFrequencyMS);
}
get roundTripTime() {
return this[kRoundTripTime];
}
close() {
this.closed = true;
clearTimeout(this[kMonitorId]);
this[kMonitorId] = undefined;
if (this[kConnection]) {
this[kConnection].destroy({ force: true });
}
}
}
function measureRoundTripTime(rttPinger, options) {
const start = now();
const cancellationToken = rttPinger[kCancellationToken];
const heartbeatFrequencyMS = options.heartbeatFrequencyMS;
if (rttPinger.closed) {
return;
}
function measureAndReschedule(conn) {
if (rttPinger.closed) {
conn.destroy({ force: true });
return;
}
if (rttPinger[kConnection] == null) {
rttPinger[kConnection] = conn;
}
rttPinger[kRoundTripTime] = calculateDurationInMs(start);
rttPinger[kMonitorId] = setTimeout(
() => measureRoundTripTime(rttPinger, options),
heartbeatFrequencyMS
);
}
if (rttPinger[kConnection] == null) {
connect(options, cancellationToken, (err, conn) => {
if (err) {
rttPinger[kConnection] = undefined;
rttPinger[kRoundTripTime] = 0;
return;
}
measureAndReschedule(conn);
});
return;
}
rttPinger[kConnection].command('admin.$cmd', { ismaster: 1 }, err => {
if (err) {
rttPinger[kConnection] = undefined;
rttPinger[kRoundTripTime] = 0;
return;
}
measureAndReschedule();
});
}
module.exports = {
Monitor
};

View File

@@ -7,17 +7,23 @@ const relayEvents = require('../utils').relayEvents;
const BSON = require('../connection/utils').retrieveBSON();
const Logger = require('../connection/logger');
const ServerDescription = require('./server_description').ServerDescription;
const compareTopologyVersion = require('./server_description').compareTopologyVersion;
const ReadPreference = require('../topologies/read_preference');
const Monitor = require('./monitor').Monitor;
const MongoNetworkError = require('../error').MongoNetworkError;
const MongoNetworkTimeoutError = require('../error').MongoNetworkTimeoutError;
const collationNotSupported = require('../utils').collationNotSupported;
const debugOptions = require('../connection/utils').debugOptions;
const isSDAMUnrecoverableError = require('../error').isSDAMUnrecoverableError;
const isNetworkTimeoutError = require('../error').isNetworkTimeoutError;
const isRetryableWriteError = require('../error').isRetryableWriteError;
const isNodeShuttingDownError = require('../error').isNodeShuttingDownError;
const isNetworkErrorBeforeHandshake = require('../error').isNetworkErrorBeforeHandshake;
const maxWireVersion = require('../utils').maxWireVersion;
const makeStateMachine = require('../utils').makeStateMachine;
const extractCommand = require('../../command_utils').extractCommand;
const common = require('./common');
const ServerType = common.ServerType;
const isTransactionCommand = require('../transactions').isTransactionCommand;
// Used for filtering out fields for logging
const DEBUG_FIELDS = [
@@ -44,6 +50,7 @@ const DEBUG_FIELDS = [
'promoteLongs',
'promoteValues',
'promoteBuffers',
'bsonRegExp',
'servername'
];
@@ -107,12 +114,12 @@ class Server extends EventEmitter {
credentials: options.credentials,
topology
};
this.serverApi = options.serverApi;
// create the connection pool
// NOTE: this used to happen in `connect`, we supported overriding pool options there
const addressParts = this.description.address.split(':');
const poolOptions = Object.assign(
{ host: addressParts[0], port: parseInt(addressParts[1], 10), bson: this.s.bson },
{ host: this.description.host, port: this.description.port, bson: this.s.bson },
options
);
@@ -162,6 +169,10 @@ class Server extends EventEmitter {
return this.s.description;
}
get supportsRetryableWrites() {
return supportsRetryableWrites(this);
}
get name() {
return this.s.description.address;
}
@@ -241,6 +252,7 @@ class Server extends EventEmitter {
if (typeof options === 'function') {
(callback = options), (options = {}), (options = options || {});
}
options.serverApi = this.serverApi;
if (this.s.state === STATE_CLOSING || this.s.state === STATE_CLOSED) {
callback(new MongoError('server is closed'));
@@ -257,10 +269,11 @@ class Server extends EventEmitter {
// Debug log
if (this.s.logger.isDebug()) {
const extractedCommand = extractCommand(cmd);
this.s.logger.debug(
`executing command [${JSON.stringify({
ns,
cmd,
cmd: extractedCommand.shouldRedact ? `${extractedCommand.name} details REDACTED` : cmd,
options: debugOptions(DEBUG_FIELDS, options)
})}] against ${this.name}`
);
@@ -278,7 +291,7 @@ class Server extends EventEmitter {
return cb(err);
}
conn.command(ns, cmd, options, makeOperationHandler(this, options, cb));
conn.command(ns, cmd, options, makeOperationHandler(this, conn, cmd, options, cb));
}, callback);
}
@@ -302,7 +315,7 @@ class Server extends EventEmitter {
return cb(err);
}
conn.query(ns, cmd, cursorState, options, makeOperationHandler(this, options, cb));
conn.query(ns, cmd, cursorState, options, makeOperationHandler(this, conn, cmd, options, cb));
}, callback);
}
@@ -326,7 +339,13 @@ class Server extends EventEmitter {
return cb(err);
}
conn.getMore(ns, cursorState, batchSize, options, makeOperationHandler(this, options, cb));
conn.getMore(
ns,
cursorState,
batchSize,
options,
makeOperationHandler(this, conn, null, options, cb)
);
}, callback);
}
@@ -352,7 +371,7 @@ class Server extends EventEmitter {
return cb(err);
}
conn.killCursors(ns, cursorState, makeOperationHandler(this, null, cb));
conn.killCursors(ns, cursorState, makeOperationHandler(this, conn, null, undefined, cb));
}, callback);
}
@@ -414,6 +433,14 @@ Object.defineProperty(Server.prototype, 'clusterTime', {
}
});
function supportsRetryableWrites(server) {
return (
server.description.maxWireVersion >= 6 &&
server.description.logicalSessionTimeoutMinutes &&
server.description.type !== ServerType.Standalone
);
}
function calculateRoundTripTime(oldRtt, duration) {
if (oldRtt === -1) {
return duration;
@@ -448,6 +475,13 @@ function executeWriteOperation(args, options, callback) {
callback(new MongoError(`server ${server.name} does not support collation`));
return;
}
const unacknowledgedWrite = options.writeConcern && options.writeConcern.w === 0;
if (unacknowledgedWrite || maxWireVersion(server) < 5) {
if ((op === 'update' || op === 'remove') && ops.find(o => o.hint)) {
callback(new MongoError(`servers < 3.4 do not support hint on ${op}`));
return;
}
}
server.s.pool.withConnection((err, conn, cb) => {
if (err) {
@@ -455,38 +489,78 @@ function executeWriteOperation(args, options, callback) {
return cb(err);
}
conn[op](ns, ops, options, makeOperationHandler(server, options, cb));
conn[op](ns, ops, options, makeOperationHandler(server, conn, ops, options, cb));
}, callback);
}
function markServerUnknown(server, error) {
if (error instanceof MongoNetworkError && !(error instanceof MongoNetworkTimeoutError)) {
server[kMonitor].reset();
}
server.emit(
'descriptionReceived',
new ServerDescription(server.description.address, null, { error })
new ServerDescription(server.description.address, null, {
error,
topologyVersion:
error && error.topologyVersion ? error.topologyVersion : server.description.topologyVersion
})
);
}
function makeOperationHandler(server, options, callback) {
function connectionIsStale(pool, connection) {
return connection.generation !== pool.generation;
}
function shouldHandleStateChangeError(server, err) {
const etv = err.topologyVersion;
const stv = server.description.topologyVersion;
return compareTopologyVersion(stv, etv) < 0;
}
function inActiveTransaction(session, cmd) {
return session && session.inTransaction() && !isTransactionCommand(cmd);
}
function makeOperationHandler(server, connection, cmd, options, callback) {
const session = options && options.session;
return function handleOperationResult(err, result) {
if (err) {
if (err && !connectionIsStale(server.s.pool, connection)) {
if (err instanceof MongoNetworkError) {
if (session && !session.hasEnded) {
session.serverSession.isDirty = true;
}
if (!isNetworkTimeoutError(err)) {
if (supportsRetryableWrites(server) && !inActiveTransaction(session, cmd)) {
err.addErrorLabel('RetryableWriteError');
}
if (!(err instanceof MongoNetworkTimeoutError) || isNetworkErrorBeforeHandshake(err)) {
markServerUnknown(server, err);
server.s.pool.clear();
}
} else if (isSDAMUnrecoverableError(err)) {
if (maxWireVersion(server) <= 7 || isNodeShuttingDownError(err)) {
server.s.pool.clear();
} else {
// if pre-4.4 server, then add error label if its a retryable write error
if (
maxWireVersion(server) < 9 &&
isRetryableWriteError(err) &&
!inActiveTransaction(session, cmd)
) {
err.addErrorLabel('RetryableWriteError');
}
markServerUnknown(server, err);
process.nextTick(() => server.requestCheck());
if (isSDAMUnrecoverableError(err)) {
if (shouldHandleStateChangeError(server, err)) {
if (maxWireVersion(server) <= 7 || isNodeShuttingDownError(err)) {
server.s.pool.clear();
}
markServerUnknown(server, err);
process.nextTick(() => server.requestCheck());
}
}
}
}

View File

@@ -53,6 +53,8 @@ class ServerDescription {
* @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)
* @param {Error} [options.error] An Error used for better reporting debugging
* @param {any} [options.topologyVersion] The topologyVersion
*/
constructor(address, ismaster, options) {
options = options || {};
@@ -68,6 +70,10 @@ class ServerDescription {
ismaster
);
if (ismaster.isWritablePrimary != null) {
ismaster.ismaster = ismaster.isWritablePrimary;
}
this.address = address;
this.error = options.error;
this.roundTripTime = options.roundTripTime || -1;
@@ -75,6 +81,7 @@ class ServerDescription {
this.lastWriteDate = ismaster.lastWrite ? ismaster.lastWrite.lastWriteDate : null;
this.opTime = ismaster.lastWrite ? ismaster.lastWrite.opTime : null;
this.type = parseServerType(ismaster);
this.topologyVersion = options.topologyVersion || ismaster.topologyVersion;
// direct mappings
ISMASTER_FIELDS.forEach(field => {
@@ -113,6 +120,16 @@ class ServerDescription {
return WRITABLE_SERVER_TYPES.has(this.type);
}
get host() {
const chopLength = `:${this.port}`.length;
return this.address.slice(0, -chopLength);
}
get port() {
const port = this.address.split(':').pop();
return port ? Number.parseInt(port, 10) : port;
}
/**
* Determines if another `ServerDescription` is equal to this one per the rules defined
* in the {@link https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/server-discovery-and-monitoring.rst#serverdescription|SDAM spec}
@@ -121,6 +138,10 @@ class ServerDescription {
* @return {Boolean}
*/
equals(other) {
const topologyVersionsEqual =
this.topologyVersion === other.topologyVersion ||
compareTopologyVersion(this.topologyVersion, other.topologyVersion) === 0;
return (
other != null &&
errorStrictEqual(this.error, other.error) &&
@@ -135,7 +156,8 @@ class ServerDescription {
? other.electionId && this.electionId.equals(other.electionId)
: this.electionId === other.electionId) &&
this.primary === other.primary &&
this.logicalSessionTimeoutMinutes === other.logicalSessionTimeoutMinutes
this.logicalSessionTimeoutMinutes === other.logicalSessionTimeoutMinutes &&
topologyVersionsEqual
);
}
}
@@ -176,7 +198,34 @@ function parseServerType(ismaster) {
return ServerType.Standalone;
}
/**
* Compares two topology versions.
*
* @param {object} lhs
* @param {object} rhs
* @returns A negative number if `lhs` is older than `rhs`; positive if `lhs` is newer than `rhs`; 0 if they are equivalent.
*/
function compareTopologyVersion(lhs, rhs) {
if (lhs == null || rhs == null) {
return -1;
}
if (lhs.processId.equals(rhs.processId)) {
// TODO: handle counters as Longs
if (lhs.counter === rhs.counter) {
return 0;
} else if (lhs.counter < rhs.counter) {
return -1;
}
return 1;
}
return -1;
}
module.exports = {
ServerDescription,
parseServerType
parseServerType,
compareTopologyVersion
};

View File

@@ -9,12 +9,10 @@ const events = require('./events');
const Server = require('./server').Server;
const relayEvents = require('../utils').relayEvents;
const ReadPreference = require('../topologies/read_preference');
const isRetryableWritesSupported = require('../topologies/shared').isRetryableWritesSupported;
const CoreCursor = require('../cursor').CoreCursor;
const deprecate = require('util').deprecate;
const BSON = require('../connection/utils').retrieveBSON();
const createCompressionInfo = require('../topologies/shared').createCompressionInfo;
const isRetryableError = require('../error').isRetryableError;
const ClientSession = require('../sessions').ClientSession;
const MongoError = require('../error').MongoError;
const MongoServerSelectionError = require('../error').MongoServerSelectionError;
@@ -27,6 +25,8 @@ const emitDeprecationWarning = require('../../utils').emitDeprecationWarning;
const ServerSessionPool = require('../sessions').ServerSessionPool;
const makeClientMetadata = require('../utils').makeClientMetadata;
const CMAP_EVENT_NAMES = require('../../cmap/events').CMAP_EVENT_NAMES;
const compareTopologyVersion = require('./server_description').compareTopologyVersion;
const emitWarning = require('../../utils').emitWarning;
const common = require('./common');
const drainTimerQueue = common.drainTimerQueue;
@@ -200,6 +200,7 @@ class Topology extends EventEmitter {
// timer management
connectionTimers: new Set()
};
this.serverApi = options.serverApi;
if (options.srvHost) {
this.s.srvPoller =
@@ -275,9 +276,9 @@ class Topology extends EventEmitter {
// connect all known servers, then attempt server selection to connect
connectServers(this, Array.from(this.s.description.servers.values()));
translateReadPreference(options);
ReadPreference.translate(options);
const readPreference = options.readPreference || ReadPreference.primary;
this.selectServer(readPreferenceServerSelector(readPreference), options, err => {
const connectHandler = err => {
if (err) {
this.close();
@@ -295,7 +296,15 @@ class Topology extends EventEmitter {
this.emit('connect', this);
if (typeof callback === 'function') callback(err, this);
});
};
// TODO: NODE-2471
if (this.s.credentials) {
this.command('admin.$cmd', { ping: 1 }, { readPreference }, connectHandler);
return;
}
this.selectServer(readPreferenceServerSelector(readPreference), options, connectHandler);
}
/**
@@ -350,7 +359,6 @@ class Topology extends EventEmitter {
this.emit('topologyClosed', new events.TopologyClosedEvent(this.s.id));
stateTransition(this, STATE_CLOSED);
this.emit('close');
if (typeof callback === 'function') {
callback(err);
@@ -381,7 +389,7 @@ class Topology extends EventEmitter {
} else if (typeof selector === 'string') {
readPreference = new ReadPreference(selector);
} else {
translateReadPreference(options);
ReadPreference.translate(options);
readPreference = options.readPreference || ReadPreference.primary;
}
@@ -487,7 +495,11 @@ class Topology extends EventEmitter {
this.command(
'admin.$cmd',
{ endSessions: sessions },
{ readPreference: ReadPreference.primaryPreferred, noResponse: true },
{
readPreference: ReadPreference.primaryPreferred,
noResponse: true,
serverApi: this.serverApi
},
() => {
// intentionally ignored, per spec
if (typeof callback === 'function') callback();
@@ -505,6 +517,11 @@ class Topology extends EventEmitter {
return;
}
// ignore this server update if its from an outdated topologyVersion
if (isStaleServerDescription(this.s.description, serverDescription)) {
return;
}
// these will be used for monitoring events later
const previousTopologyDescription = this.s.description;
const previousServerDescription = this.s.description.servers.get(serverDescription.address);
@@ -647,7 +664,7 @@ class Topology extends EventEmitter {
(callback = options), (options = {}), (options = options || {});
}
translateReadPreference(options);
ReadPreference.translate(options);
const readPreference = options.readPreference || ReadPreference.primary;
this.selectServer(readPreferenceServerSelector(readPreference), options, (err, server) => {
@@ -656,17 +673,22 @@ class Topology extends EventEmitter {
return;
}
const notAlreadyRetrying = !options.retrying;
const retryWrites = !!options.retryWrites;
const hasSession = !!options.session;
const supportsRetryableWrites = server.supportsRetryableWrites;
const notInTransaction = !hasSession || !options.session.inTransaction();
const willRetryWrite =
!options.retrying &&
!!options.retryWrites &&
options.session &&
isRetryableWritesSupported(this) &&
!options.session.inTransaction() &&
notAlreadyRetrying &&
retryWrites &&
hasSession &&
supportsRetryableWrites &&
notInTransaction &&
isWriteCommand(cmd);
const cb = (err, result) => {
if (!err) return callback(null, result);
if (!isRetryableError(err)) {
if (!shouldRetryOperation(err)) {
return callback(err);
}
@@ -708,7 +730,7 @@ class Topology extends EventEmitter {
options = options || {};
const topology = options.topology || this;
const CursorClass = options.cursorFactory || this.s.Cursor;
translateReadPreference(options);
ReadPreference.translate(options);
return new CursorClass(topology, ns, cmd, options);
}
@@ -725,8 +747,11 @@ class Topology extends EventEmitter {
return this.s.state === STATE_CLOSED;
}
/**
* @deprecated This function is deprecated and will be removed in the next major version.
*/
unref() {
console.log('not implemented: `unref`');
emitWarning('`unref` is a noop and will be removed in the next major version');
}
// NOTE: There are many places in code where we explicitly check the last isMaster
@@ -771,6 +796,16 @@ function isWriteCommand(command) {
return RETRYABLE_WRITE_OPERATIONS.some(op => command[op]);
}
function isStaleServerDescription(topologyDescription, incomingServerDescription) {
const currentServerDescription = topologyDescription.servers.get(
incomingServerDescription.address
);
const currentTopologyVersion = currentServerDescription.topologyVersion;
return (
compareTopologyVersion(currentTopologyVersion, incomingServerDescription.topologyVersion) > 0
);
}
/**
* Destroys a server, and removes all event listeners from the instance
*
@@ -806,10 +841,16 @@ function parseStringSeedlist(seedlist) {
}
function topologyTypeFromSeedlist(seedlist, options) {
if (options.directConnection) {
return TopologyType.Single;
}
const replicaSet = options.replicaSet || options.setName || options.rs_name;
if (seedlist.length === 1 && !replicaSet) return TopologyType.Single;
if (replicaSet) return TopologyType.ReplicaSetNoPrimary;
return TopologyType.Unknown;
if (replicaSet == null) {
return TopologyType.Unknown;
}
return TopologyType.ReplicaSetNoPrimary;
}
function randomSelection(array) {
@@ -896,22 +937,29 @@ function executeWriteOperation(args, options, callback) {
const ns = args.ns;
const ops = args.ops;
const willRetryWrite =
!args.retrying &&
!!options.retryWrites &&
options.session &&
isRetryableWritesSupported(topology) &&
!options.session.inTransaction();
topology.selectServer(writableServerSelector(), options, (err, server) => {
if (err) {
callback(err, null);
return;
}
const notAlreadyRetrying = !args.retrying;
const retryWrites = !!options.retryWrites;
const hasSession = !!options.session;
const supportsRetryableWrites = server.supportsRetryableWrites;
const notInTransaction = !hasSession || !options.session.inTransaction();
const notExplaining = options.explain === undefined;
const willRetryWrite =
notAlreadyRetrying &&
retryWrites &&
hasSession &&
supportsRetryableWrites &&
notInTransaction &&
notExplaining;
const handler = (err, result) => {
if (!err) return callback(null, result);
if (!isRetryableError(err)) {
if (!shouldRetryOperation(err)) {
err = getMMAPError(err);
return callback(err);
}
@@ -939,26 +987,8 @@ function executeWriteOperation(args, options, callback) {
});
}
function translateReadPreference(options) {
if (options.readPreference == null) {
return;
}
let r = options.readPreference;
if (typeof r === 'string') {
options.readPreference = new ReadPreference(r);
} else if (r && !(r instanceof ReadPreference) && typeof r === 'object') {
const mode = r.mode || r.preference;
if (mode && typeof mode === 'string') {
options.readPreference = new ReadPreference(mode, r.tags, {
maxStalenessSeconds: r.maxStalenessSeconds
});
}
} else if (!(r instanceof ReadPreference)) {
throw new TypeError('Invalid read preference: ' + r);
}
return options;
function shouldRetryOperation(err) {
return err instanceof MongoError && err.hasErrorLabel('RetryableWriteError');
}
function srvPollingHandler(topology) {

View File

@@ -72,12 +72,30 @@ class TopologyDescription {
// 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);
this.logicalSessionTimeoutMinutes = null;
for (const addressServerTuple of this.servers) {
const server = addressServerTuple[1];
if (server.isReadable) {
if (server.logicalSessionTimeoutMinutes == null) {
// If any of the servers have a null logicalSessionsTimeout, then the whole topology does
this.logicalSessionTimeoutMinutes = null;
break;
}
if (this.logicalSessionTimeoutMinutes == null) {
// First server with a non null logicalSessionsTimeout
this.logicalSessionTimeoutMinutes = server.logicalSessionTimeoutMinutes;
continue;
}
// Always select the smaller of the:
// current server logicalSessionsTimeout and the topologies logicalSessionsTimeout
this.logicalSessionTimeoutMinutes = Math.min(
this.logicalSessionTimeoutMinutes,
server.logicalSessionTimeoutMinutes
);
}
}
}
/**
@@ -132,6 +150,10 @@ class TopologyDescription {
let maxElectionId = this.maxElectionId;
let commonWireVersion = this.commonWireVersion;
if (serverDescription.setName && setName && serverDescription.setName !== setName) {
serverDescription = new ServerDescription(address, null);
}
const serverType = serverDescription.type;
let serverDescriptions = new Map(this.servers);
@@ -161,7 +183,7 @@ class TopologyDescription {
}
if (topologyType === TopologyType.Unknown) {
if (serverType === ServerType.Standalone) {
if (serverType === ServerType.Standalone && this.servers.size !== 1) {
serverDescriptions.delete(address);
} else {
topologyType = topologyTypeForServerType(serverType);
@@ -246,6 +268,7 @@ class TopologyDescription {
if (descriptionsWithError.length > 0) {
return descriptionsWithError[0].error;
}
return undefined;
}
/**
@@ -274,8 +297,22 @@ class TopologyDescription {
}
function topologyTypeForServerType(serverType) {
if (serverType === ServerType.Mongos) return TopologyType.Sharded;
if (serverType === ServerType.RSPrimary) return TopologyType.ReplicaSetWithPrimary;
if (serverType === ServerType.Standalone) {
return TopologyType.Single;
}
if (serverType === ServerType.Mongos) {
return TopologyType.Sharded;
}
if (serverType === ServerType.RSPrimary) {
return TopologyType.ReplicaSetWithPrimary;
}
if (serverType === ServerType.RSGhost || serverType === ServerType.Unknown) {
return TopologyType.Unknown;
}
return TopologyType.ReplicaSetNoPrimary;
}

View File

@@ -13,6 +13,7 @@ const Transaction = require('./transactions').Transaction;
const TxnState = require('./transactions').TxnState;
const isPromiseLike = require('./utils').isPromiseLike;
const ReadPreference = require('./topologies/read_preference');
const maybePromise = require('../utils').maybePromise;
const isTransactionCommand = require('./transactions').isTransactionCommand;
const resolveClusterTime = require('./topologies/shared').resolveClusterTime;
const isSharded = require('./wireprotocol/shared').isSharded;
@@ -125,25 +126,36 @@ class ClientSession extends EventEmitter {
if (typeof options === 'function') (callback = options), (options = {});
options = options || {};
if (this.hasEnded) {
if (typeof callback === 'function') callback(null, null);
return;
}
const session = this;
return maybePromise(this, callback, done => {
if (session.hasEnded) {
return done();
}
if (this.serverSession && this.inTransaction()) {
this.abortTransaction(); // pass in callback?
}
function completeEndSession() {
// release the server session back to the pool
session.sessionPool.release(session.serverSession);
session[kServerSession] = undefined;
// release the server session back to the pool
this.sessionPool.release(this.serverSession);
this[kServerSession] = undefined;
// mark the session as ended, and emit a signal
session.hasEnded = true;
session.emit('ended', session);
// mark the session as ended, and emit a signal
this.hasEnded = true;
this.emit('ended', this);
// spec indicates that we should ignore all errors for `endSessions`
done();
}
// spec indicates that we should ignore all errors for `endSessions`
if (typeof callback === 'function') callback(null, null);
if (session.serverSession && session.inTransaction()) {
session.abortTransaction(err => {
if (err) return done(err);
completeEndSession();
});
return;
}
completeEndSession();
});
}
/**
@@ -227,16 +239,7 @@ class ClientSession extends EventEmitter {
* @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)
);
});
return maybePromise(this, callback, done => endTransaction(this, 'commitTransaction', done));
}
/**
@@ -246,16 +249,7 @@ class ClientSession extends EventEmitter {
* @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)
);
});
return maybePromise(this, callback, done => endTransaction(this, 'abortTransaction', done));
}
/**
@@ -386,10 +380,7 @@ function attemptTransaction(session, startTime, fn, options) {
}
if (isMaxTimeMSExpiredError(err)) {
if (err.errorLabels == null) {
err.errorLabels = [];
}
err.errorLabels.push('UnknownTransactionCommitResult');
err.addErrorLabel('UnknownTransactionCommitResult');
}
throw err;
@@ -481,26 +472,20 @@ function endTransaction(session, commandName, callback) {
if (commandName === 'commitTransaction') {
session.transaction.transition(TxnState.TRANSACTION_COMMITTED);
if (
e &&
(e instanceof MongoNetworkError ||
if (e) {
if (
e instanceof MongoNetworkError ||
e instanceof MongoWriteConcernError ||
isRetryableError(e) ||
isMaxTimeMSExpiredError(e))
) {
if (e.errorLabels) {
const idx = e.errorLabels.indexOf('TransientTransactionError');
if (idx !== -1) {
e.errorLabels.splice(idx, 1);
isMaxTimeMSExpiredError(e)
) {
if (isUnknownTransactionCommitResult(e)) {
e.addErrorLabel('UnknownTransactionCommitResult');
// per txns spec, must unpin session in this case
session.transaction.unpinServer();
}
} else {
e.errorLabels = [];
}
if (isUnknownTransactionCommitResult(e)) {
e.errorLabels.push('UnknownTransactionCommitResult');
// per txns spec, must unpin session in this case
} else if (e.hasErrorLabel('TransientTransactionError')) {
session.transaction.unpinServer();
}
}
@@ -685,7 +670,12 @@ function commandSupportsReadConcern(command, options) {
return true;
}
if (command.mapReduce && options.out && (options.out.inline === 1 || options.out === 'inline')) {
if (
command.mapReduce &&
options &&
options.out &&
(options.out.inline === 1 || options.out === 'inline')
) {
return true;
}
@@ -708,6 +698,11 @@ function applySession(session, command, options) {
return new MongoError('Cannot use a session that has ended');
}
// SPEC-1019: silently ignore explicit session with unacknowledged write for backwards compatibility
if (options && options.writeConcern && options.writeConcern.w === 0) {
return;
}
const serverSession = session.serverSession;
serverSession.lastUse = now();
command.lsid = serverSession.id;
@@ -715,7 +710,7 @@ function applySession(session, command, options) {
// first apply non-transaction-specific sessions data
const inTransaction = session.inTransaction() || isTransactionCommand(command);
const isRetryableWrite = options.willRetryWrite;
const shouldApplyReadConcern = commandSupportsReadConcern(command);
const shouldApplyReadConcern = commandSupportsReadConcern(command, options);
if (serverSession.txnNumber && (isRetryableWrite || inTransaction)) {
command.txnNumber = BSON.Long.fromNumber(serverSession.txnNumber);

View File

@@ -52,6 +52,7 @@ exports.attachToRunner = function(runner, outputFile) {
fs.writeFileSync(outputFile, JSON.stringify(smokeOutput));
// Standard NodeJS uncaught exception handler
// eslint-disable-next-line no-console
console.error(err.stack);
process.exit(1);
});

View File

@@ -13,10 +13,10 @@ const cloneOptions = require('./shared').cloneOptions;
const SessionMixins = require('./shared').SessionMixins;
const isRetryableWritesSupported = require('./shared').isRetryableWritesSupported;
const relayEvents = require('../utils').relayEvents;
const isRetryableError = require('../error').isRetryableError;
const BSON = retrieveBSON();
const getMMAPError = require('./shared').getMMAPError;
const makeClientMetadata = require('../utils').makeClientMetadata;
const legacyIsRetryableWriteError = require('./shared').legacyIsRetryableWriteError;
/**
* @fileOverview The **Mongos** class is a class that represents a Mongos Proxy topology and is
@@ -71,7 +71,7 @@ var handlers = ['connect', 'close', 'error', 'timeout', 'parseError'];
* @param {Cursor} [options.cursorFactory=Cursor] The cursor factory class used for all query cursors
* @param {number} [options.size=5] Server connection pool size
* @param {boolean} [options.keepAlive=true] TCP Connection keep alive enabled
* @param {number} [options.keepAliveInitialDelay=0] Initial delay before TCP keep alive enabled
* @param {number} [options.keepAliveInitialDelay=120000] Initial delay before TCP keep alive enabled
* @param {number} [options.localThresholdMS=15] Cutoff latency point in MS for MongoS proxy selection
* @param {boolean} [options.noDelay=true] TCP Connection no delay
* @param {number} [options.connectionTimeout=1000] TCP Connection timeout setting
@@ -88,6 +88,7 @@ var handlers = ['connect', 'close', 'error', 'timeout', 'parseError'];
* @param {boolean} [options.promoteLongs=true] Convert Long values from the db into Numbers if they fit into 53 bits
* @param {boolean} [options.promoteValues=true] Promotes BSON values to native types where possible, set to false to only receive wrapper types.
* @param {boolean} [options.promoteBuffers=false] Promotes Binary BSON values to native Node Buffers.
* @param {boolean} [options.bsonRegExp=false] By default, regex returned from MDB will be native to the language. Setting to true will ensure that a BSON.BSONRegExp object is returned.
* @param {boolean} [options.domainsEnabled=false] Enable the wrapping of the callback in the current domain, disabled by default to avoid perf hit.
* @param {boolean} [options.monitorCommands=false] Enable command monitoring for this topology
* @return {Mongos} A cursor instance
@@ -113,6 +114,18 @@ var Mongos = function(seedlist, options) {
// Get replSet Id
this.id = id++;
// deduplicate seedlist
if (Array.isArray(seedlist)) {
seedlist = seedlist.reduce((seeds, seed) => {
if (seeds.find(s => s.host === seed.host && s.port === seed.port)) {
return seeds;
}
seeds.push(seed);
return seeds;
}, []);
}
// Internal state
this.s = {
options: Object.assign({ metadata: makeClientMetadata(options) }, options),
@@ -907,11 +920,12 @@ function executeWriteOperation(args, options, callback) {
!!options.retryWrites &&
options.session &&
isRetryableWritesSupported(self) &&
!options.session.inTransaction();
!options.session.inTransaction() &&
options.explain === undefined;
const handler = (err, result) => {
if (!err) return callback(null, result);
if (!isRetryableError(err) || !willRetryWrite) {
if (!legacyIsRetryableWriteError(err, self) || !willRetryWrite) {
err = getMMAPError(err);
return callback(err);
}
@@ -1107,7 +1121,7 @@ Mongos.prototype.command = function(ns, cmd, options, callback) {
const cb = (err, result) => {
if (!err) return callback(null, result);
if (!isRetryableError(err)) {
if (!legacyIsRetryableWriteError(err, self)) {
return callback(err);
}
@@ -1121,8 +1135,8 @@ Mongos.prototype.command = function(ns, cmd, options, callback) {
// increment and assign txnNumber
if (willRetryWrite) {
options.session.incrementTransactionNumber();
options.willRetryWrite = willRetryWrite;
clonedOptions.session.incrementTransactionNumber();
clonedOptions.willRetryWrite = willRetryWrite;
}
// Execute the command

View File

@@ -1,4 +1,5 @@
'use strict';
const emitWarningOnce = require('../../utils').emitWarningOnce;
/**
* The **ReadPreference** class is a class that represents a MongoDB ReadPreference and is
@@ -8,6 +9,8 @@
* @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.
* @param {object} [options.hedge] Server mode in which the same query is dispatched in parallel to multiple replica set members.
* @param {boolean} [options.hedge.enabled] Explicitly enable or disable hedged reads.
* @see https://docs.mongodb.com/manual/core/read-preference/
* @return {ReadPreference}
*/
@@ -18,11 +21,14 @@ const ReadPreference = function(mode, tags, options) {
// TODO(major): tags MUST be an array of tagsets
if (tags && !Array.isArray(tags)) {
console.warn(
emitWarningOnce(
'ReadPreference tags must be an array, this will change in the next major version'
);
if (typeof tags.maxStalenessSeconds !== 'undefined') {
const tagsHasMaxStalenessSeconds = typeof tags.maxStalenessSeconds !== 'undefined';
const tagsHasHedge = typeof tags.hedge !== 'undefined';
const tagsHasOptions = tagsHasMaxStalenessSeconds || tagsHasHedge;
if (tagsHasOptions) {
// this is likely an options object
options = tags;
tags = undefined;
@@ -33,6 +39,7 @@ const ReadPreference = function(mode, tags, options) {
this.mode = mode;
this.tags = tags;
this.hedge = options && options.hedge;
options = options || {};
if (options.maxStalenessSeconds != null) {
@@ -55,6 +62,10 @@ const ReadPreference = function(mode, tags, options) {
if (this.maxStalenessSeconds) {
throw new TypeError('Primary read preference cannot be combined with maxStalenessSeconds');
}
if (this.hedge) {
throw new TypeError('Primary read preference cannot be combined with hedge');
}
}
};
@@ -91,20 +102,19 @@ const VALID_MODES = [
* @return {ReadPreference}
*/
ReadPreference.fromOptions = function(options) {
if (!options) return null;
const readPreference = options.readPreference;
if (!readPreference) return null;
const readPreferenceTags = options.readPreferenceTags;
if (readPreference == null) {
return null;
}
const maxStalenessSeconds = options.maxStalenessSeconds;
if (typeof readPreference === 'string') {
return new ReadPreference(readPreference, readPreferenceTags);
} else if (!(readPreference instanceof ReadPreference) && typeof readPreference === 'object') {
const mode = readPreference.mode || readPreference.preference;
if (mode && typeof mode === 'string') {
return new ReadPreference(mode, readPreference.tags, {
maxStalenessSeconds: readPreference.maxStalenessSeconds
maxStalenessSeconds: readPreference.maxStalenessSeconds || maxStalenessSeconds,
hedge: readPreference.hedge
});
}
}
@@ -112,6 +122,60 @@ ReadPreference.fromOptions = function(options) {
return readPreference;
};
/**
* Resolves a read preference based on well-defined inheritance rules. This method will not only
* determine the read preference (if there is one), but will also ensure the returned value is a
* properly constructed instance of `ReadPreference`.
*
* @param {Collection|Db|MongoClient} parent The parent of the operation on which to determine the read
* preference, used for determining the inherited read preference.
* @param {object} options The options passed into the method, potentially containing a read preference
* @returns {(ReadPreference|null)} The resolved read preference
*/
ReadPreference.resolve = function(parent, options) {
options = options || {};
const session = options.session;
const inheritedReadPreference = parent && parent.readPreference;
let readPreference;
if (options.readPreference) {
readPreference = ReadPreference.fromOptions(options);
} else if (session && session.inTransaction() && session.transaction.options.readPreference) {
// The transactions read preference MUST override all other user configurable read preferences.
readPreference = session.transaction.options.readPreference;
} else if (inheritedReadPreference != null) {
readPreference = inheritedReadPreference;
} else {
readPreference = ReadPreference.primary;
}
return typeof readPreference === 'string' ? new ReadPreference(readPreference) : readPreference;
};
/**
* Replaces options.readPreference with a ReadPreference instance
*/
ReadPreference.translate = function(options) {
if (options.readPreference == null) return options;
const r = options.readPreference;
if (typeof r === 'string') {
options.readPreference = new ReadPreference(r);
} else if (r && !(r instanceof ReadPreference) && typeof r === 'object') {
const mode = r.mode || r.preference;
if (mode && typeof mode === 'string') {
options.readPreference = new ReadPreference(mode, r.tags, {
maxStalenessSeconds: r.maxStalenessSeconds
});
}
} else if (!(r instanceof ReadPreference)) {
throw new TypeError('Invalid read preference: ' + r);
}
return options;
};
/**
* Validate if a mode is legal
*
@@ -165,6 +229,7 @@ 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;
if (this.hedge) readPreference.hedge = this.hedge;
return readPreference;
};

View File

@@ -15,10 +15,10 @@ const Interval = require('./shared').Interval;
const SessionMixins = require('./shared').SessionMixins;
const isRetryableWritesSupported = require('./shared').isRetryableWritesSupported;
const relayEvents = require('../utils').relayEvents;
const isRetryableError = require('../error').isRetryableError;
const BSON = retrieveBSON();
const getMMAPError = require('./shared').getMMAPError;
const makeClientMetadata = require('../utils').makeClientMetadata;
const legacyIsRetryableWriteError = require('./shared').legacyIsRetryableWriteError;
const now = require('../../utils').now;
const calculateDurationInMs = require('../../utils').calculateDurationInMs;
@@ -72,7 +72,7 @@ var handlers = ['connect', 'close', 'error', 'timeout', 'parseError'];
* @param {Cursor} [options.cursorFactory=Cursor] The cursor factory class used for all query cursors
* @param {number} [options.size=5] Server connection pool size
* @param {boolean} [options.keepAlive=true] TCP Connection keep alive enabled
* @param {number} [options.keepAliveInitialDelay=0] Initial delay before TCP keep alive enabled
* @param {number} [options.keepAliveInitialDelay=120000] Initial delay before TCP keep alive enabled
* @param {boolean} [options.noDelay=true] TCP Connection no delay
* @param {number} [options.connectionTimeout=10000] TCP Connection timeout setting
* @param {number} [options.socketTimeout=0] TCP Socket timeout setting
@@ -88,6 +88,7 @@ var handlers = ['connect', 'close', 'error', 'timeout', 'parseError'];
* @param {boolean} [options.promoteLongs=true] Convert Long values from the db into Numbers if they fit into 53 bits
* @param {boolean} [options.promoteValues=true] Promotes BSON values to native types where possible, set to false to only receive wrapper types.
* @param {boolean} [options.promoteBuffers=false] Promotes Binary BSON values to native Node Buffers.
* @param {boolean} [options.bsonRegExp=false] By default, regex returned from MDB will be native to the language. Setting to true will ensure that a BSON.BSONRegExp object is returned.
* @param {number} [options.pingInterval=5000] Ping interval to check the response time to the different servers
* @param {number} [options.localThresholdMS=15] Cutoff latency point in MS for Replicaset member selection
* @param {boolean} [options.domainsEnabled=false] Enable the wrapping of the callback in the current domain, disabled by default to avoid perf hit.
@@ -541,7 +542,7 @@ var monitorServer = function(host, self, options) {
self.s.options.secondaryOnlyConnectionAllowed) ||
self.s.replicaSetState.hasPrimary())
) {
self.state = CONNECTED;
stateTransition(self, CONNECTED);
// Emit connected sign
process.nextTick(function() {
@@ -558,7 +559,7 @@ var monitorServer = function(host, self, options) {
self.s.options.secondaryOnlyConnectionAllowed) ||
self.s.replicaSetState.hasPrimary())
) {
self.state = CONNECTED;
stateTransition(self, CONNECTING);
// Rexecute any stalled operation
rexecuteOperations(self);
@@ -787,7 +788,7 @@ function handleInitialConnectEvent(self, event) {
// Do we have a primary or primaryAndSecondary
if (shouldTriggerConnect(self)) {
// We are connected
self.state = CONNECTED;
stateTransition(self, CONNECTED);
// Set initial connect state
self.initialConnectState.connect = true;
@@ -919,7 +920,7 @@ ReplSet.prototype.connect = function(options) {
);
});
// Error out as high availbility interval must be < than socketTimeout
// Error out as high availability interval must be < than socketTimeout
if (
this.s.options.socketTimeout > 0 &&
this.s.options.socketTimeout <= this.s.options.haInterval
@@ -975,14 +976,19 @@ ReplSet.prototype.destroy = function(options, callback) {
// Emit toplogy closing event
emitSDAMEvent(this, 'topologyClosed', { topologyId: this.id });
// Transition state
stateTransition(this, DESTROYED);
if (typeof callback === 'function') {
callback(null, null);
}
};
if (this.state === DESTROYED) {
if (typeof callback === 'function') callback(null, null);
return;
}
// Transition state
stateTransition(this, DESTROYED);
// Clear out any monitoring process
if (this.haTimeoutId) clearTimeout(this.haTimeoutId);
@@ -1188,7 +1194,8 @@ function executeWriteOperation(args, options, callback) {
!!options.retryWrites &&
options.session &&
isRetryableWritesSupported(self) &&
!options.session.inTransaction();
!options.session.inTransaction() &&
options.explain === undefined;
if (!self.s.replicaSetState.hasPrimary()) {
if (self.s.disconnectHandler) {
@@ -1202,7 +1209,7 @@ function executeWriteOperation(args, options, callback) {
const handler = (err, result) => {
if (!err) return callback(null, result);
if (!isRetryableError(err)) {
if (!legacyIsRetryableWriteError(err, self)) {
err = getMMAPError(err);
return callback(err);
}
@@ -1365,7 +1372,7 @@ ReplSet.prototype.command = function(ns, cmd, options, callback) {
const cb = (err, result) => {
if (!err) return callback(null, result);
if (!isRetryableError(err)) {
if (!legacyIsRetryableWriteError(err, self)) {
return callback(err);
}

View File

@@ -34,7 +34,7 @@ var ReplSetState = function(options) {
// Add event listener
EventEmitter.call(this);
// Topology state
this.topologyType = TopologyType.ReplicaSetNoPrimary;
this.topologyType = options.setName ? TopologyType.ReplicaSetNoPrimary : TopologyType.Unknown;
this.setName = options.setName;
// Server set
@@ -218,7 +218,8 @@ const isArbiter = ismaster => ismaster.arbiterOnly && ismaster.setName;
ReplSetState.prototype.update = function(server) {
var self = this;
// Get the current ismaster
var ismaster = server.lastIsMaster();
const ismaster = server.lastIsMaster();
if (ismaster && ismaster.isWritablePrimary) ismaster.ismaster = ismaster.isWritablePrimary;
// Get the server name and lowerCase it
var serverName = server.name.toLowerCase();
@@ -358,7 +359,8 @@ ReplSetState.prototype.update = function(server) {
// Standalone server, destroy and return
//
if (ismaster && ismaster.ismaster && !ismaster.setName) {
this.topologyType = this.primary ? TopologyType.ReplicaSetWithPrimary : TopologyType.Unknown;
// We should not mark the topology as Unknown because of one standalone
// we should just remove this server from the set
this.remove(server, { force: true });
return false;
}

View File

@@ -16,6 +16,7 @@ var inherits = require('util').inherits,
createCompressionInfo = require('./shared').createCompressionInfo,
resolveClusterTime = require('./shared').resolveClusterTime,
SessionMixins = require('./shared').SessionMixins,
extractCommand = require('../../command_utils').extractCommand,
relayEvents = require('../utils').relayEvents;
const collationNotSupported = require('../utils').collationNotSupported;
@@ -46,6 +47,7 @@ var debugFields = [
'promoteLongs',
'promoteValues',
'promoteBuffers',
'bsonRegExp',
'servername'
];
@@ -72,10 +74,10 @@ function topologyId(server) {
* @param {number} options.port The server port
* @param {number} [options.size=5] Server connection pool size
* @param {boolean} [options.keepAlive=true] TCP Connection keep alive enabled
* @param {number} [options.keepAliveInitialDelay=300000] Initial delay before TCP keep alive enabled
* @param {number} [options.keepAliveInitialDelay=120000] 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 {number} [options.socketTimeout=0] TCP Socket timeout setting
* @param {boolean} [options.ssl=false] Use SSL for connection
* @param {boolean|function} [options.checkServerIdentity=true] Ensure we check server identify during SSL, set to false to disable checking. Only works for Node 0.12.x or higher. You can pass in a boolean or your own checkServerIdentity override function.
* @param {Buffer} [options.ca] SSL Certificate store binary buffer
@@ -88,6 +90,7 @@ function topologyId(server) {
* @param {boolean} [options.promoteLongs=true] Convert Long values from the db into Numbers if they fit into 53 bits
* @param {boolean} [options.promoteValues=true] Promotes BSON values to native types where possible, set to false to only receive wrapper types.
* @param {boolean} [options.promoteBuffers=false] Promotes Binary BSON values to native Node Buffers.
* @param {boolean} [options.bsonRegExp=false] By default, regex returned from MDB will be native to the language. Setting to true will ensure that a BSON.BSONRegExp object is returned.
* @param {string} [options.appname=null] Application name, passed in on ismaster call and logged in mongod server logs. Maximum size 128 bytes.
* @param {boolean} [options.domainsEnabled=false] Enable the wrapping of the callback in the current domain, disabled by default to avoid perf hit.
* @param {boolean} [options.monitorCommands=false] Enable command monitoring for this topology
@@ -389,7 +392,7 @@ var eventHandler = function(self, event) {
event === 'timeout' ||
event === 'reconnect' ||
event === 'attemptReconnect' ||
'reconnectFailed'
event === 'reconnectFailed'
) {
// Remove server instance from accounting
if (
@@ -608,18 +611,20 @@ Server.prototype.command = function(ns, cmd, options, callback) {
options = Object.assign({}, options, { wireProtocolCommand: false });
// Debug log
if (self.s.logger.isDebug())
if (self.s.logger.isDebug()) {
const extractedCommand = extractCommand(cmd);
self.s.logger.debug(
f(
'executing command [%s] against %s',
JSON.stringify({
ns: ns,
cmd: cmd,
cmd: extractedCommand.shouldRedact ? `${extractedCommand.name} details REDACTED` : cmd,
options: debugOptions(debugFields, options)
}),
self.name
)
);
}
// If we are not connected or have a disconnectHandler specified
if (disconnectHandler(self, 'command', ns, cmd, options, callback)) return;
@@ -864,12 +869,14 @@ Server.prototype.destroy = function(options, callback) {
}
// No pool, return
if (!self.s.pool) {
if (!self.s.pool || this._destroyed) {
this._destroyed = true;
if (typeof callback === 'function') callback(null, null);
return;
}
this._destroyed = true;
// Emit close event
if (options.emitClose) {
self.emit('close', self);
@@ -900,7 +907,6 @@ Server.prototype.destroy = function(options, callback) {
// Destroy the pool
this.s.pool.destroy(options.force, callback);
this._destroyed = true;
};
/**

View File

@@ -1,9 +1,14 @@
'use strict';
const MONGODB_ERROR_CODES = require('../../error_codes').MONGODB_ERROR_CODES;
const ReadPreference = require('./read_preference');
const TopologyType = require('../sdam/common').TopologyType;
const MongoError = require('../error').MongoError;
const isRetryableWriteError = require('../error').isRetryableWriteError;
const maxWireVersion = require('../utils').maxWireVersion;
const MongoNetworkError = require('../error').MongoNetworkError;
const MMAPv1_RETRY_WRITES_ERROR_CODE = 20;
const MMAPv1_RETRY_WRITES_ERROR_CODE = MONGODB_ERROR_CODES.IllegalOperation;
/**
* Emit event if it exists
@@ -416,18 +421,39 @@ function getMMAPError(err) {
return newErr;
}
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.createCompressionInfo = createCompressionInfo;
module.exports.clone = clone;
module.exports.diff = diff;
module.exports.Interval = Interval;
module.exports.Timeout = Timeout;
module.exports.isRetryableWritesSupported = isRetryableWritesSupported;
module.exports.getMMAPError = getMMAPError;
module.exports.topologyType = topologyType;
// NOTE: only used for legacy topology types
function legacyIsRetryableWriteError(err, topology) {
if (!(err instanceof MongoError)) {
return false;
}
// if pre-4.4 server, then add error label if its a retryable write error
if (
isRetryableWritesSupported(topology) &&
(err instanceof MongoNetworkError ||
(maxWireVersion(topology) < 9 && isRetryableWriteError(err)))
) {
err.addErrorLabel('RetryableWriteError');
}
return err.hasErrorLabel('RetryableWriteError');
}
module.exports = {
SessionMixins,
resolveClusterTime,
inquireServerState,
getTopologyType,
emitServerDescriptionChanged,
emitTopologyDescriptionChanged,
cloneOptions,
createCompressionInfo,
clone,
diff,
Interval,
Timeout,
isRetryableWritesSupported,
getMMAPError,
topologyType,
legacyIsRetryableWriteError
};

View File

@@ -150,7 +150,11 @@ class Transaction {
const nextStates = stateMachine[this.state];
if (nextStates && nextStates.indexOf(nextState) !== -1) {
this.state = nextState;
if (this.state === TxnState.NO_TRANSACTION || this.state === TxnState.STARTING_TRANSACTION) {
if (
this.state === TxnState.NO_TRANSACTION ||
this.state === TxnState.STARTING_TRANSACTION ||
this.state === TxnState.TRANSACTION_ABORTED
) {
this.unpinServer();
}
return;

View File

@@ -4,6 +4,7 @@ const qs = require('querystring');
const dns = require('dns');
const MongoParseError = require('./error').MongoParseError;
const ReadPreference = require('./topologies/read_preference');
const emitWarningOnce = require('../utils').emitWarningOnce;
/**
* The following regular expression validates a connection string and breaks the
@@ -11,6 +12,11 @@ const ReadPreference = require('./topologies/read_preference');
*/
const HOSTS_RX = /(mongodb(?:\+srv|)):\/\/(?: (?:[^:]*) (?: : ([^@]*) )? @ )?([^/?]*)(?:\/|)(.*)/;
// Options that reference file paths should not be parsed
const FILE_PATH_OPTIONS = new Set(
['sslCA', 'sslCert', 'sslKey', 'tlsCAFile', 'tlsCertificateKeyFile'].map(key => key.toLowerCase())
);
/**
* Determines whether a provided address matches the provided parent domain in order
* to avoid certain attack vectors.
@@ -37,12 +43,18 @@ function matchesParentDomain(srvAddress, parentDomain) {
function parseSrvConnectionString(uri, options, callback) {
const result = URL.parse(uri, true);
if (options.directConnection || options.directconnection) {
return callback(new MongoParseError('directConnection not supported with SRV URI'));
}
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(',')) {
const hostname = uri.substring('mongodb+srv://'.length).split('/')[0];
if (hostname.match(',')) {
return callback(new MongoParseError('Invalid URI, cannot contain multiple hostnames'));
}
@@ -82,7 +94,7 @@ function parseSrvConnectionString(uri, options, callback) {
// Resolve TXT record and add options from there if they exist.
dns.resolveTxt(lookupAddress, (err, record) => {
if (err) {
if (err.code !== 'ENODATA') {
if (err.code !== 'ENODATA' && err.code !== 'ENOTFOUND') {
return callback(err);
}
record = null;
@@ -94,6 +106,11 @@ function parseSrvConnectionString(uri, options, callback) {
}
record = qs.parse(record[0].join(''));
if (Object.keys(record).some(k => k.toLowerCase() === 'loadbalanced')) {
return callback(new MongoParseError('Load balancer mode requires driver version 4+'));
}
if (Object.keys(record).some(key => key !== 'authSource' && key !== 'replicaSet')) {
return callback(
new MongoParseError('Text record must only set `authSource` or `replicaSet`')
@@ -171,6 +188,7 @@ const STRING_OPTIONS = new Set(['authsource', 'replicaset']);
// NOTE: this list exists in native already, if it is merged here we should deduplicate
const AUTH_MECHANISMS = new Set([
'GSSAPI',
'MONGODB-AWS',
'MONGODB-X509',
'MONGODB-CR',
'DEFAULT',
@@ -214,7 +232,8 @@ const CASE_TRANSLATION = {
tlscertificatekeyfile: 'tlsCertificateKeyFile',
tlscertificatekeyfilepassword: 'tlsCertificateKeyFilePassword',
wtimeout: 'wTimeoutMS',
j: 'journal'
j: 'journal',
directconnection: 'directConnection'
};
/**
@@ -257,7 +276,9 @@ function applyConnectionStringOption(obj, key, value, options) {
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`'
`Value for authMechanism must be one of: ${Array.from(AUTH_MECHANISMS).join(
', '
)}, found: ${value}`
);
}
@@ -357,6 +378,16 @@ function applyAuthExpectations(parsed) {
parsed.auth = Object.assign({}, parsed.auth, { db: '$external' });
}
if (authMechanism === 'MONGODB-AWS') {
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}\``);
@@ -406,14 +437,21 @@ function parseQueryString(query, options) {
}
const normalizedKey = key.toLowerCase();
const parsedValue = parseQueryStringItemValue(normalizedKey, value);
if (normalizedKey === 'serverapi') {
throw new MongoParseError(
'URI cannot contain `serverApi`, it can only be passed to the client'
);
}
const parsedValue = FILE_PATH_OPTIONS.has(normalizedKey)
? value
: 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');
emitWarningOnce('Unsupported option `wtimeout` specified');
}
return Object.keys(result).length ? result : null;
@@ -552,10 +590,6 @@ function parseConnectionString(uri, options, callback) {
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;
@@ -568,6 +602,15 @@ function parseConnectionString(uri, options, callback) {
}
parsedOptions = Object.assign({}, parsedOptions, options);
if (Object.keys(parsedOptions).some(k => k.toLowerCase() === 'loadbalanced')) {
return callback(new MongoParseError('Load balancer mode requires driver version 4+'));
}
if (protocol === PROTOCOL_MONGODB_SRV) {
return parseSrvConnectionString(uri, parsedOptions, callback);
}
const auth = { username: null, password: null, db: db && db !== '' ? qs.unescape(db) : null };
if (parsedOptions.auth) {
// maintain support for legacy options passed into `MongoClient`
@@ -661,6 +704,22 @@ function parseConnectionString(uri, options, callback) {
return callback(new MongoParseError('No hostname or hostnames provided in connection string'));
}
const directConnection = !!parsedOptions.directConnection;
if (directConnection && hosts.length !== 1) {
// If the option is set to true, the driver MUST validate that there is exactly one host given
// in the host list in the URI, and fail client creation otherwise.
return callback(new MongoParseError('directConnection option requires exactly one host'));
}
// NOTE: this behavior will go away in v4.0, we will always auto discover there
if (
parsedOptions.directConnection == null &&
hosts.length === 1 &&
parsedOptions.replicaSet == null
) {
parsedOptions.directConnection = true;
}
const result = {
hosts: hosts,
auth: auth.db || auth.username ? auth : null,

View File

@@ -1,7 +1,7 @@
'use strict';
const os = require('os');
const crypto = require('crypto');
const requireOptional = require('require_optional');
const requireOptional = require('optional-require')(require);
/**
* Generate a UUIDv4
@@ -27,6 +27,7 @@ function retrieveKerberos() {
let kerberos;
try {
// Ensure you always wrap an optional require in the try block NODE-3199
kerberos = requireOptional('kerberos');
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') {
@@ -46,10 +47,7 @@ const noEJSONError = function() {
// Facilitate loading EJSON optionally
function retrieveEJSON() {
let EJSON = null;
try {
EJSON = requireOptional('mongodb-extjson');
} catch (error) {} // eslint-disable-line
let EJSON = requireOptional('mongodb-extjson');
if (!EJSON) {
EJSON = {
parse: noEJSONError,
@@ -149,6 +147,35 @@ function eachAsync(arr, eachFn, callback) {
}
}
function eachAsyncSeries(arr, eachFn, callback) {
arr = arr || [];
let idx = 0;
let awaiting = arr.length;
if (awaiting === 0) {
callback();
return;
}
function eachCallback(err) {
idx++;
awaiting--;
if (err) {
callback(err);
return;
}
if (idx === arr.length && awaiting <= 0) {
callback();
return;
}
eachFn(arr[idx], eachCallback);
}
eachFn(arr[idx], eachCallback);
}
function isUnifiedTopology(topology) {
return topology.description != null;
}
@@ -257,6 +284,7 @@ module.exports = {
maxWireVersion,
isPromiseLike,
eachAsync,
eachAsyncSeries,
isUnifiedTopology,
arrayStrictEqual,
tagsStrictEqual,

View File

@@ -45,14 +45,43 @@ function _command(server, ns, cmd, options, callback) {
const shouldUseOpMsg = supportsOpMsg(server);
const session = options.session;
let clusterTime = server.clusterTime;
const serverClusterTime = server.clusterTime;
let clusterTime = serverClusterTime;
let finalCmd = Object.assign({}, cmd);
const serverApi = options.serverApi;
if (serverApi) {
finalCmd.apiVersion = serverApi.version || serverApi;
if (serverApi.strict != null) {
finalCmd.apiStrict = serverApi.strict;
}
if (serverApi.deprecationErrors != null) {
finalCmd.apiDeprecationErrors = serverApi.deprecationErrors;
}
}
if (hasSessionSupport(server) && session) {
const sessionClusterTime = session.clusterTime;
if (
session.clusterTime &&
session.clusterTime.clusterTime.greaterThan(clusterTime.clusterTime)
serverClusterTime &&
serverClusterTime.clusterTime &&
sessionClusterTime &&
sessionClusterTime.clusterTime &&
sessionClusterTime.clusterTime.greaterThan(serverClusterTime.clusterTime)
) {
clusterTime = session.clusterTime;
clusterTime = sessionClusterTime;
}
// We need to unpin any read or write commands that happen outside of a pinned
// transaction, so we check if we have a pinned transaction that is no longer
// active, and unpin for all except start or commit.
if (
!session.transaction.isActive &&
session.transaction.isPinned &&
!finalCmd.startTransaction &&
!finalCmd.commitTransaction
) {
session.transaction.unpinServer();
}
const err = applySession(session, finalCmd, options);
@@ -61,17 +90,12 @@ function _command(server, ns, cmd, options, callback) {
}
}
// if we have a known cluster time, gossip it
if (clusterTime) {
// if we have a known cluster time, gossip it
finalCmd.$clusterTime = clusterTime;
}
if (
isSharded(server) &&
!shouldUseOpMsg &&
readPreference &&
readPreference.preference !== 'primary'
) {
if (isSharded(server) && !shouldUseOpMsg && readPreference && readPreference.mode !== 'primary') {
finalCmd = {
$query: finalCmd,
$readPreference: readPreference.toJSON()
@@ -105,10 +129,7 @@ function _command(server, ns, cmd, options, callback) {
err instanceof MongoNetworkError &&
!err.hasErrorLabel('TransientTransactionError')
) {
if (err.errorLabels == null) {
err.errorLabels = [];
}
err.errorLabels.push('TransientTransactionError');
err.addErrorLabel('TransientTransactionError');
}
if (

View File

@@ -1,9 +1,9 @@
'use strict';
const MIN_SUPPORTED_SERVER_VERSION = '2.6';
const MAX_SUPPORTED_SERVER_VERSION = '4.2';
const MAX_SUPPORTED_SERVER_VERSION = '5.0';
const MIN_SUPPORTED_WIRE_VERSION = 2;
const MAX_SUPPORTED_WIRE_VERSION = 8;
const MAX_SUPPORTED_WIRE_VERSION = 13;
module.exports = {
MIN_SUPPORTED_SERVER_VERSION,

View File

@@ -5,10 +5,16 @@ const MongoError = require('../error').MongoError;
const MongoNetworkError = require('../error').MongoNetworkError;
const collectionNamespace = require('./shared').collectionNamespace;
const maxWireVersion = require('../utils').maxWireVersion;
const emitWarning = require('../utils').emitWarning;
const command = require('./command');
function killCursors(server, ns, cursorState, callback) {
function killCursors(server, ns, cursorState, defaultOptions, callback) {
if (typeof defaultOptions === 'function') {
callback = defaultOptions;
defaultOptions = {};
}
callback = typeof callback === 'function' ? callback : () => {};
const cursorId = cursorState.cursorId;
if (maxWireVersion(server) < 4) {
@@ -31,7 +37,7 @@ function killCursors(server, ns, cursorState, callback) {
if (typeof callback === 'function') {
callback(err, null);
} else {
console.warn(err);
emitWarning(err);
}
}
}
@@ -44,7 +50,7 @@ function killCursors(server, ns, cursorState, callback) {
cursors: [cursorId]
};
const options = {};
const options = defaultOptions || {};
if (typeof cursorState.session === 'object') options.session = cursorState.session;
command(server, ns, killCursorCmd, options, (err, result) => {

View File

@@ -8,6 +8,8 @@ const isSharded = require('./shared').isSharded;
const maxWireVersion = require('../utils').maxWireVersion;
const applyCommonQueryOptions = require('./shared').applyCommonQueryOptions;
const command = require('./command');
const decorateWithExplain = require('../../utils').decorateWithExplain;
const Explain = require('../../explain').Explain;
function query(server, ns, cmd, cursorState, options, callback) {
options = options || {};
@@ -31,7 +33,18 @@ function query(server, ns, cmd, cursorState, options, callback) {
}
const readPreference = getReadPreference(cmd, options);
const findCmd = prepareFindCommand(server, ns, cmd, cursorState, options);
let findCmd = prepareFindCommand(server, ns, cmd, cursorState, options);
// If we have explain, we need to rewrite the find command
// to wrap it in the explain command
try {
const explain = Explain.fromOptions(options);
if (explain) {
findCmd = decorateWithExplain(findCmd, explain);
}
} catch (err) {
return callback(err);
}
// NOTE: This actually modifies the passed in cmd, and our code _depends_ on this
// side-effect. Change this ASAP
@@ -59,7 +72,7 @@ function query(server, ns, cmd, cursorState, options, callback) {
function prepareFindCommand(server, ns, cmd, cursorState) {
cursorState.batchSize = cmd.batchSize || cursorState.batchSize;
let findCmd = {
const findCmd = {
find: collectionNamespace(ns)
};
@@ -100,6 +113,10 @@ function prepareFindCommand(server, ns, cmd, cursorState) {
sortValue = sortObject;
}
if (typeof cmd.allowDiskUse === 'boolean') {
findCmd.allowDiskUse = cmd.allowDiskUse;
}
if (cmd.sort) findCmd.sort = sortValue;
if (cmd.fields) findCmd.projection = cmd.fields;
if (cmd.hint) findCmd.hint = cmd.hint;
@@ -127,8 +144,8 @@ function prepareFindCommand(server, ns, cmd, cursorState) {
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 (typeof cmd.returnKey === 'boolean') findCmd.returnKey = cmd.returnKey;
if (typeof cmd.showDiskLoc === 'boolean') findCmd.showRecordId = cmd.showDiskLoc;
if (cmd.snapshot) findCmd.snapshot = cmd.snapshot;
if (cmd.tailable) findCmd.tailable = cmd.tailable;
if (cmd.oplogReplay) findCmd.oplogReplay = cmd.oplogReplay;
@@ -139,14 +156,6 @@ function prepareFindCommand(server, ns, cmd, cursorState) {
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;
}
@@ -184,7 +193,7 @@ function prepareLegacyFindQuery(server, ns, cmd, cursorState, options) {
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) {
if (options.explain !== undefined) {
// 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);

View File

@@ -57,6 +57,7 @@ function applyCommonQueryOptions(queryOptions, options) {
promoteLongs: typeof options.promoteLongs === 'boolean' ? options.promoteLongs : true,
promoteValues: typeof options.promoteValues === 'boolean' ? options.promoteValues : true,
promoteBuffers: typeof options.promoteBuffers === 'boolean' ? options.promoteBuffers : false,
bsonRegExp: typeof options.bsonRegExp === 'boolean' ? options.bsonRegExp : false,
monitoring: typeof options.monitoring === 'boolean' ? options.monitoring : false,
fullResult: typeof options.fullResult === 'boolean' ? options.fullResult : false
});

View File

@@ -3,6 +3,8 @@
const MongoError = require('../error').MongoError;
const collectionNamespace = require('./shared').collectionNamespace;
const command = require('./command');
const decorateWithExplain = require('../../utils').decorateWithExplain;
const Explain = require('../../explain').Explain;
function writeCommand(server, type, opsField, ns, ops, options, callback) {
if (ops.length === 0) throw new MongoError(`${type} must contain at least one document`);
@@ -15,7 +17,7 @@ function writeCommand(server, type, opsField, ns, ops, options, callback) {
const ordered = typeof options.ordered === 'boolean' ? options.ordered : true;
const writeConcern = options.writeConcern;
const writeCommand = {};
let writeCommand = {};
writeCommand[type] = collectionNamespace(ns);
writeCommand[opsField] = ops;
writeCommand.ordered = ordered;
@@ -36,6 +38,13 @@ function writeCommand(server, type, opsField, ns, ops, options, callback) {
writeCommand.bypassDocumentValidation = options.bypassDocumentValidation;
}
// If a command is to be explained, we need to reformat the command after
// the other command properties are specified.
const explain = Explain.fromOptions(options);
if (explain) {
writeCommand = decorateWithExplain(writeCommand, explain);
}
const commandOptions = Object.assign(
{
checkKeys: type === 'insert',