Update Node_modules
This commit is contained in:
+136
@@ -2,6 +2,142 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
<a name="3.2.7"></a>
|
||||
## [3.2.7](https://github.com/mongodb-js/mongodb-core/compare/v3.2.6...v3.2.7) (2019-06-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **pool:** clean up connections if pool is destroyed mid-handshake ([4bd7f1c](https://github.com/mongodb-js/mongodb-core/commit/4bd7f1c))
|
||||
* **server:** emit "first connect" error if initial connect fails due to ECONNREFUSED ([853bcfe](https://github.com/mongodb-js/mongodb-core/commit/853bcfe))
|
||||
|
||||
|
||||
|
||||
<a name="3.2.6"></a>
|
||||
## [3.2.6](https://github.com/mongodb-js/mongodb-core/compare/v3.2.4...v3.2.6) (2019-05-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **connect:** fixed syntax issue in connect error handler ([83e224b](https://github.com/mongodb-js/mongodb-core/commit/83e224b))
|
||||
* **uri-parser:** always treat appname as a string ([238aca8](https://github.com/mongodb-js/mongodb-core/commit/238aca8))
|
||||
|
||||
|
||||
|
||||
<a name="3.2.5"></a>
|
||||
## [3.2.5](https://github.com/mongodb-js/mongodb-core/compare/v3.2.3...v3.2.5) (2019-05-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **connect:** fixed syntax issue in connect error handler ([83e224b](https://github.com/mongodb-js/mongodb-core/commit/83e224b))
|
||||
|
||||
|
||||
|
||||
<a name="3.2.4"></a>
|
||||
## [3.2.4](https://github.com/mongodb-js/mongodb-core/compare/v3.2.3...v3.2.4) (2019-05-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **authentication:** fix auth issue with BSON promoteBuffers: true ([bcb87ca](https://github.com/mongodb-js/mongodb-core/commit/bcb87ca))
|
||||
* **command_result:** return entire result in toJSON ([b958513](https://github.com/mongodb-js/mongodb-core/commit/b958513))
|
||||
* **connect:** ensure connection errors are MongoNetworkErrors ([380a386](https://github.com/mongodb-js/mongodb-core/commit/380a386))
|
||||
* **retryable-writes:** don't add `txnNumber` for standalone servers ([8489265](https://github.com/mongodb-js/mongodb-core/commit/8489265))
|
||||
* **transactions:** send recovery token on abortTransaction ([1fa5763](https://github.com/mongodb-js/mongodb-core/commit/1fa5763))
|
||||
|
||||
|
||||
|
||||
<a name="3.2.3"></a>
|
||||
## [3.2.3](https://github.com/mongodb-js/mongodb-core/compare/v3.2.2...v3.2.3) (2019-04-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **uri_parser:** restore original compression parsing ([70a7d94](https://github.com/mongodb-js/mongodb-core/commit/70a7d94))
|
||||
* **uri_parser:** support URI Options spec tests ([c067dbc](https://github.com/mongodb-js/mongodb-core/commit/c067dbc))
|
||||
|
||||
|
||||
|
||||
<a name="3.2.2"></a>
|
||||
## [3.2.2](https://github.com/mongodb-js/mongodb-core/compare/v3.2.1...v3.2.2) (2019-03-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* do not attempt to auth against an arbiter ([72bb011](https://github.com/mongodb-js/mongodb-core/commit/72bb011))
|
||||
* **connection:** do not leak a connection if initial handshak fails ([6cba222](https://github.com/mongodb-js/mongodb-core/commit/6cba222))
|
||||
|
||||
|
||||
|
||||
<a name="3.2.1"></a>
|
||||
## [3.2.1](https://github.com/mongodb-js/mongodb-core/compare/v3.2.0...v3.2.1) (2019-03-21)
|
||||
|
||||
|
||||
|
||||
<a name="3.2.0"></a>
|
||||
# [3.2.0](https://github.com/mongodb-js/mongodb-core/compare/v3.1.11...v3.2.0) (2019-03-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **command:** invert boolean expression for applying `$query` ([8513ad5](https://github.com/mongodb-js/mongodb-core/commit/8513ad5))
|
||||
* **command:** only add `$query` to command in OP_QUERY ([3e57690](https://github.com/mongodb-js/mongodb-core/commit/3e57690))
|
||||
* **cursor:** ensure that cursor id defaults to 0 ([e7e1775](https://github.com/mongodb-js/mongodb-core/commit/e7e1775))
|
||||
* **get-more:** documents are already returned as raw in this case ([c81f609](https://github.com/mongodb-js/mongodb-core/commit/c81f609))
|
||||
* **msg:** support raw cursor results using OP_MSG ([f91304b](https://github.com/mongodb-js/mongodb-core/commit/f91304b))
|
||||
* **op-msg:** only include `$readPreference` if not primary ([0d10317](https://github.com/mongodb-js/mongodb-core/commit/0d10317))
|
||||
* **scram:** allow errors to be passed through callbacks ([dccc2ba](https://github.com/mongodb-js/mongodb-core/commit/dccc2ba))
|
||||
* **sessions:** enable sessions in OP_MSG ([d8bf209](https://github.com/mongodb-js/mongodb-core/commit/d8bf209))
|
||||
* **topology:** correctly pick up command options for cursors ([259231e](https://github.com/mongodb-js/mongodb-core/commit/259231e))
|
||||
* **topology:** ensure read preferences are translated on selection ([ebefb7b](https://github.com/mongodb-js/mongodb-core/commit/ebefb7b))
|
||||
* **transactions:** only send recovery token on commitTransaction ([923a089](https://github.com/mongodb-js/mongodb-core/commit/923a089))
|
||||
* **transactions:** special case non-deterministic wc errors in txns ([5a2ae77](https://github.com/mongodb-js/mongodb-core/commit/5a2ae77))
|
||||
* **transactions:** write concern is always majority on txn retry ([7b240ea](https://github.com/mongodb-js/mongodb-core/commit/7b240ea))
|
||||
* **with-transaction:** throw a useful error on invalid return type ([ae64bb4](https://github.com/mongodb-js/mongodb-core/commit/ae64bb4))
|
||||
* make mongos write commands work the same as replset write commands ([31b984f](https://github.com/mongodb-js/mongodb-core/commit/31b984f))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add authentication to handshake process ([aacac68](https://github.com/mongodb-js/mongodb-core/commit/aacac68))
|
||||
* **error:** all `hasErrorLabel` method to MongoError ([32a5e74](https://github.com/mongodb-js/mongodb-core/commit/32a5e74))
|
||||
* **OP_MSG:** add in parsing of OP_MSG ([c310a83](https://github.com/mongodb-js/mongodb-core/commit/c310a83))
|
||||
* **OP_MSG:** adding class for translating OP_MSG from binary ([11e4132](https://github.com/mongodb-js/mongodb-core/commit/11e4132))
|
||||
* **OP_MSG:** adding OP_MSG implementation ([c5adfa3](https://github.com/mongodb-js/mongodb-core/commit/c5adfa3))
|
||||
* **op-msg:** add support for `OP_MSG` to command monitoring ([9124b67](https://github.com/mongodb-js/mongodb-core/commit/9124b67))
|
||||
* **sdam:** backport unified SDAM changes from `next` to `master` ([83d744c](https://github.com/mongodb-js/mongodb-core/commit/83d744c))
|
||||
* **topology-description:** always calculate commonWireVersion ([5c630ab](https://github.com/mongodb-js/mongodb-core/commit/5c630ab))
|
||||
* **transactions:** tack recovery token for sharded transactions ([e12ae70](https://github.com/mongodb-js/mongodb-core/commit/e12ae70))
|
||||
* add ability to pin server to transaction state machine ([da13e55](https://github.com/mongodb-js/mongodb-core/commit/da13e55))
|
||||
* update proxy selection to consider pinned server on session ([189e428](https://github.com/mongodb-js/mongodb-core/commit/189e428))
|
||||
* **txns:** support mongos pinning in unified topology ([78dab5d](https://github.com/mongodb-js/mongodb-core/commit/78dab5d))
|
||||
* **with-transaction:** provide helper for convenient txn api ([478d1e7](https://github.com/mongodb-js/mongodb-core/commit/478d1e7))
|
||||
* **withTransaction:** retry transaction commit on wtimeout ([2bc705c](https://github.com/mongodb-js/mongodb-core/commit/2bc705c))
|
||||
|
||||
|
||||
|
||||
<a name="3.1.11"></a>
|
||||
## [3.1.11](https://github.com/mongodb-js/mongodb-core/compare/v3.1.10...v3.1.11) (2019-01-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **wire-protocol:** don't allow override of `slaveOk` ([8fcef69](https://github.com/mongodb-js/mongodb-core/commit/8fcef69))
|
||||
|
||||
|
||||
|
||||
<a name="3.1.10"></a>
|
||||
## [3.1.10](https://github.com/mongodb-js/mongodb-core/compare/v3.1.9...v3.1.10) (2019-01-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **mongos-replset:** pass connect options to child server instances ([7ffb4bb](https://github.com/mongodb-js/mongodb-core/commit/7ffb4bb))
|
||||
* **prettier:** fix prettier file paths for Windows ([00c631e](https://github.com/mongodb-js/mongodb-core/commit/00c631e))
|
||||
|
||||
|
||||
|
||||
<a name="3.1.9"></a>
|
||||
## [3.1.9](https://github.com/mongodb-js/mongodb-core/compare/v3.1.8...v3.1.9) (2018-11-16)
|
||||
|
||||
|
||||
+2
@@ -33,9 +33,11 @@ module.exports = {
|
||||
Sessions: require('./lib/sessions'),
|
||||
BSON: BSON,
|
||||
EJSON: EJSON,
|
||||
Topology: require('./lib/sdam/topology'),
|
||||
// Raw operations
|
||||
Query: require('./lib/connection/commands').Query,
|
||||
// Auth mechanisms
|
||||
MongoCredentials: require('./lib/auth/mongo_credentials').MongoCredentials,
|
||||
defaultAuthProviders: require('./lib/auth/defaultAuthProviders').defaultAuthProviders,
|
||||
MongoCR: require('./lib/auth/mongocr'),
|
||||
X509: require('./lib/auth/x509'),
|
||||
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
'use strict';
|
||||
|
||||
const MongoError = require('../error').MongoError;
|
||||
|
||||
/**
|
||||
* Creates a new AuthProvider, which dictates how to authenticate for a given
|
||||
* mechanism.
|
||||
* @class
|
||||
*/
|
||||
class AuthProvider {
|
||||
constructor(bson) {
|
||||
this.bson = bson;
|
||||
this.authStore = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @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 };
|
||||
+96
-236
@@ -1,120 +1,61 @@
|
||||
'use strict';
|
||||
|
||||
const f = require('util').format;
|
||||
const Query = require('../connection/commands').Query;
|
||||
const MongoError = require('../error').MongoError;
|
||||
const AuthProvider = require('./auth_provider').AuthProvider;
|
||||
const retrieveKerberos = require('../utils').retrieveKerberos;
|
||||
|
||||
var AuthSession = function(db, username, password, options) {
|
||||
this.db = db;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.options = options;
|
||||
};
|
||||
|
||||
AuthSession.prototype.equal = function(session) {
|
||||
return (
|
||||
session.db === this.db &&
|
||||
session.username === this.username &&
|
||||
session.password === this.password
|
||||
);
|
||||
};
|
||||
let kerberos;
|
||||
|
||||
/**
|
||||
* Creates a new GSSAPI authentication mechanism
|
||||
* @class
|
||||
* @return {GSSAPI} A cursor instance
|
||||
* @extends AuthProvider
|
||||
*/
|
||||
var GSSAPI = function(bson) {
|
||||
this.bson = bson;
|
||||
this.authStore = [];
|
||||
};
|
||||
class GSSAPI extends AuthProvider {
|
||||
/**
|
||||
* Implementation of authentication for a single connection
|
||||
* @override
|
||||
*/
|
||||
_authenticateSingleConnection(sendAuthCommand, connection, credentials, callback) {
|
||||
const source = credentials.source;
|
||||
const username = credentials.username;
|
||||
const password = credentials.password;
|
||||
const mechanismProperties = credentials.mechanismProperties;
|
||||
const gssapiServiceName =
|
||||
mechanismProperties['gssapiservicename'] ||
|
||||
mechanismProperties['gssapiServiceName'] ||
|
||||
'mongodb';
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
* @method
|
||||
* @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
|
||||
* @param {[]Connections} connections Connections to authenticate using this authenticator
|
||||
* @param {string} db Name of the database
|
||||
* @param {string} username Username
|
||||
* @param {string} password Password
|
||||
* @param {authResultCallback} callback The callback to return the result from the authentication
|
||||
* @return {object}
|
||||
*/
|
||||
GSSAPI.prototype.auth = function(server, connections, db, username, password, options, callback) {
|
||||
var self = this;
|
||||
let kerberos;
|
||||
try {
|
||||
kerberos = retrieveKerberos();
|
||||
} catch (e) {
|
||||
return callback(e, null);
|
||||
GSSAPIInitialize(
|
||||
this,
|
||||
kerberos.processes.MongoAuthProcess,
|
||||
source,
|
||||
username,
|
||||
password,
|
||||
source,
|
||||
gssapiServiceName,
|
||||
sendAuthCommand,
|
||||
connection,
|
||||
mechanismProperties,
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: remove this once we fix URI parsing
|
||||
var gssapiServiceName = options['gssapiservicename'] || options['gssapiServiceName'] || 'mongodb';
|
||||
// Total connections
|
||||
var count = connections.length;
|
||||
if (count === 0) return callback(null, null);
|
||||
/**
|
||||
* Authenticate
|
||||
* @override
|
||||
* @method
|
||||
*/
|
||||
auth(sendAuthCommand, connections, credentials, callback) {
|
||||
if (kerberos == null) {
|
||||
try {
|
||||
kerberos = retrieveKerberos();
|
||||
} catch (e) {
|
||||
return callback(e, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Valid connections
|
||||
var numberOfValidConnections = 0;
|
||||
var errorObject = null;
|
||||
|
||||
// For each connection we need to authenticate
|
||||
while (connections.length > 0) {
|
||||
// Execute MongoCR
|
||||
var execute = function(connection) {
|
||||
// Start Auth process for a connection
|
||||
GSSAPIInitialize(
|
||||
self,
|
||||
kerberos.processes.MongoAuthProcess,
|
||||
db,
|
||||
username,
|
||||
password,
|
||||
db,
|
||||
gssapiServiceName,
|
||||
server,
|
||||
connection,
|
||||
options,
|
||||
function(err, r) {
|
||||
// Adjust count
|
||||
count = count - 1;
|
||||
|
||||
// If we have an error
|
||||
if (err) {
|
||||
errorObject = err;
|
||||
} else if (r.result['$err']) {
|
||||
errorObject = r.result;
|
||||
} else if (r.result['errmsg']) {
|
||||
errorObject = r.result;
|
||||
} else {
|
||||
numberOfValidConnections = numberOfValidConnections + 1;
|
||||
}
|
||||
|
||||
// We have authenticated all connections
|
||||
if (count === 0 && numberOfValidConnections > 0) {
|
||||
// Store the auth details
|
||||
addAuthSession(self.authStore, new AuthSession(db, username, password, options));
|
||||
// Return correct authentication
|
||||
callback(null, true);
|
||||
} else if (count === 0) {
|
||||
if (errorObject == null)
|
||||
errorObject = new MongoError(f('failed to authenticate using mongocr'));
|
||||
callback(errorObject, false);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var _execute = function(_connection) {
|
||||
process.nextTick(function() {
|
||||
execute(_connection);
|
||||
});
|
||||
};
|
||||
|
||||
_execute(connections.shift());
|
||||
super.auth(sendAuthCommand, connections, credentials, callback);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Initialize step
|
||||
@@ -126,7 +67,7 @@ var GSSAPIInitialize = function(
|
||||
password,
|
||||
authdb,
|
||||
gssapiServiceName,
|
||||
server,
|
||||
sendAuthCommand,
|
||||
connection,
|
||||
options,
|
||||
callback
|
||||
@@ -156,7 +97,7 @@ var GSSAPIInitialize = function(
|
||||
username,
|
||||
password,
|
||||
authdb,
|
||||
server,
|
||||
sendAuthCommand,
|
||||
connection,
|
||||
callback
|
||||
);
|
||||
@@ -174,7 +115,7 @@ var MongoDBGSSAPIFirstStep = function(
|
||||
username,
|
||||
password,
|
||||
authdb,
|
||||
server,
|
||||
sendAuthCommand,
|
||||
connection,
|
||||
callback
|
||||
) {
|
||||
@@ -187,36 +128,28 @@ var MongoDBGSSAPIFirstStep = function(
|
||||
};
|
||||
|
||||
// Write the commmand on the connection
|
||||
server(
|
||||
connection,
|
||||
new Query(self.bson, '$external.$cmd', command, {
|
||||
numberToSkip: 0,
|
||||
numberToReturn: 1
|
||||
}),
|
||||
function(err, r) {
|
||||
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);
|
||||
var doc = r.result;
|
||||
// Execute mongodb transition
|
||||
mongo_auth_process.transition(r.result.payload, function(err, payload) {
|
||||
if (err) return callback(err, false);
|
||||
|
||||
// MongoDB API Second Step
|
||||
MongoDBGSSAPISecondStep(
|
||||
self,
|
||||
mongo_auth_process,
|
||||
payload,
|
||||
doc,
|
||||
db,
|
||||
username,
|
||||
password,
|
||||
authdb,
|
||||
server,
|
||||
connection,
|
||||
callback
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
// MongoDB API Second Step
|
||||
MongoDBGSSAPISecondStep(
|
||||
self,
|
||||
mongo_auth_process,
|
||||
payload,
|
||||
doc,
|
||||
db,
|
||||
username,
|
||||
password,
|
||||
authdb,
|
||||
sendAuthCommand,
|
||||
connection,
|
||||
callback
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
@@ -230,7 +163,7 @@ var MongoDBGSSAPISecondStep = function(
|
||||
username,
|
||||
password,
|
||||
authdb,
|
||||
server,
|
||||
sendAuthCommand,
|
||||
connection,
|
||||
callback
|
||||
) {
|
||||
@@ -243,36 +176,28 @@ var MongoDBGSSAPISecondStep = function(
|
||||
|
||||
// Execute the command
|
||||
// Write the commmand on the connection
|
||||
server(
|
||||
connection,
|
||||
new Query(self.bson, '$external.$cmd', command, {
|
||||
numberToSkip: 0,
|
||||
numberToReturn: 1
|
||||
}),
|
||||
function(err, r) {
|
||||
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);
|
||||
var doc = r.result;
|
||||
// Call next transition for kerberos
|
||||
mongo_auth_process.transition(doc.payload, function(err, payload) {
|
||||
if (err) return callback(err, false);
|
||||
|
||||
// Call the last and third step
|
||||
MongoDBGSSAPIThirdStep(
|
||||
self,
|
||||
mongo_auth_process,
|
||||
payload,
|
||||
doc,
|
||||
db,
|
||||
username,
|
||||
password,
|
||||
authdb,
|
||||
server,
|
||||
connection,
|
||||
callback
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
// Call the last and third step
|
||||
MongoDBGSSAPIThirdStep(
|
||||
self,
|
||||
mongo_auth_process,
|
||||
payload,
|
||||
doc,
|
||||
db,
|
||||
username,
|
||||
password,
|
||||
authdb,
|
||||
sendAuthCommand,
|
||||
connection,
|
||||
callback
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var MongoDBGSSAPIThirdStep = function(
|
||||
@@ -284,7 +209,7 @@ var MongoDBGSSAPIThirdStep = function(
|
||||
username,
|
||||
password,
|
||||
authdb,
|
||||
server,
|
||||
sendAuthCommand,
|
||||
connection,
|
||||
callback
|
||||
) {
|
||||
@@ -296,80 +221,15 @@ var MongoDBGSSAPIThirdStep = function(
|
||||
};
|
||||
|
||||
// Execute the command
|
||||
server(
|
||||
connection,
|
||||
new Query(self.bson, '$external.$cmd', command, {
|
||||
numberToSkip: 0,
|
||||
numberToReturn: 1
|
||||
}),
|
||||
function(err, r) {
|
||||
if (err) return callback(err, false);
|
||||
mongo_auth_process.transition(null, function(err) {
|
||||
if (err) return callback(err, null);
|
||||
callback(null, r);
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Add to store only if it does not exist
|
||||
var addAuthSession = function(authStore, session) {
|
||||
var found = false;
|
||||
|
||||
for (var i = 0; i < authStore.length; i++) {
|
||||
if (authStore[i].equal(session)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) authStore.push(session);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove authStore credentials
|
||||
* @method
|
||||
* @param {string} db Name of database we are removing authStore details about
|
||||
* @return {object}
|
||||
*/
|
||||
GSSAPI.prototype.logout = function(dbName) {
|
||||
this.authStore = this.authStore.filter(function(x) {
|
||||
return x.db !== dbName;
|
||||
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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Re authenticate pool
|
||||
* @method
|
||||
* @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
|
||||
* @param {[]Connections} connections Connections to authenticate using this authenticator
|
||||
* @param {authResultCallback} callback The callback to return the result from the authentication
|
||||
* @return {object}
|
||||
*/
|
||||
GSSAPI.prototype.reauthenticate = function(server, connections, callback) {
|
||||
var authStore = this.authStore.slice(0);
|
||||
var count = authStore.length;
|
||||
if (count === 0) return callback(null, null);
|
||||
// Iterate over all the auth details stored
|
||||
for (var i = 0; i < authStore.length; i++) {
|
||||
this.auth(
|
||||
server,
|
||||
connections,
|
||||
authStore[i].db,
|
||||
authStore[i].username,
|
||||
authStore[i].password,
|
||||
authStore[i].options,
|
||||
function(err) {
|
||||
count = count - 1;
|
||||
// Done re-authenticating
|
||||
if (count === 0) {
|
||||
callback(err, null);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This is a result from a authentication strategy
|
||||
*
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
'use strict';
|
||||
|
||||
// Resolves the default auth mechanism according to
|
||||
// https://github.com/mongodb/specifications/blob/master/source/auth/auth.rst
|
||||
function getDefaultAuthMechanism(ismaster) {
|
||||
if (ismaster) {
|
||||
// If ismaster contains saslSupportedMechs, use scram-sha-256
|
||||
// if it is available, else scram-sha-1
|
||||
if (Array.isArray(ismaster.saslSupportedMechs)) {
|
||||
return ismaster.saslSupportedMechs.indexOf('SCRAM-SHA-256') >= 0
|
||||
? 'scram-sha-256'
|
||||
: 'scram-sha-1';
|
||||
}
|
||||
|
||||
// Fallback to legacy selection method. If wire version >= 3, use scram-sha-1
|
||||
if (ismaster.maxWireVersion >= 3) {
|
||||
return 'scram-sha-1';
|
||||
}
|
||||
}
|
||||
|
||||
// Default for wireprotocol < 3
|
||||
return 'mongocr';
|
||||
}
|
||||
|
||||
/**
|
||||
* A representation of the credentials used by MongoDB
|
||||
* @class
|
||||
* @property {string} mechanism The method used to authenticate
|
||||
* @property {string} [username] The username used for authentication
|
||||
* @property {string} [password] The password used for authentication
|
||||
* @property {string} [source] The database that the user should authenticate against
|
||||
* @property {object} [mechanismProperties] Special properties used by some types of auth mechanisms
|
||||
*/
|
||||
class MongoCredentials {
|
||||
/**
|
||||
* Creates a new MongoCredentials object
|
||||
* @param {object} [options]
|
||||
* @param {string} [options.username] The username used for authentication
|
||||
* @param {string} [options.password] The password used for authentication
|
||||
* @param {string} [options.source] The database that the user should authenticate against
|
||||
* @param {string} [options.mechanism] The method used to authenticate
|
||||
* @param {object} [options.mechanismProperties] Special properties used by some types of auth mechanisms
|
||||
*/
|
||||
constructor(options) {
|
||||
options = options || {};
|
||||
this.username = options.username;
|
||||
this.password = options.password;
|
||||
this.source = options.source || options.db;
|
||||
this.mechanism = options.mechanism || 'default';
|
||||
this.mechanismProperties = options.mechanismProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if two MongoCredentials objects are equivalent
|
||||
* @param {MongoCredentials} other another MongoCredentials object
|
||||
* @returns {boolean} true if the two objects are equal.
|
||||
*/
|
||||
equals(other) {
|
||||
return (
|
||||
this.mechanism === other.mechanism &&
|
||||
this.username === other.username &&
|
||||
this.password === other.password &&
|
||||
this.source === other.source
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the authentication mechanism is set to "default", resolves the authMechanism
|
||||
* based on the server version and server supported sasl mechanisms.
|
||||
*
|
||||
* @param {Object} [ismaster] An ismaster response from the server
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { MongoCredentials };
|
||||
+42
-205
@@ -1,214 +1,51 @@
|
||||
'use strict';
|
||||
|
||||
var f = require('util').format,
|
||||
crypto = require('crypto'),
|
||||
Query = require('../connection/commands').Query,
|
||||
MongoError = require('../error').MongoError;
|
||||
|
||||
var AuthSession = function(db, username, password) {
|
||||
this.db = db;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
};
|
||||
|
||||
AuthSession.prototype.equal = function(session) {
|
||||
return (
|
||||
session.db === this.db &&
|
||||
session.username === this.username &&
|
||||
session.password === this.password
|
||||
);
|
||||
};
|
||||
const crypto = require('crypto');
|
||||
const AuthProvider = require('./auth_provider').AuthProvider;
|
||||
|
||||
/**
|
||||
* Creates a new MongoCR authentication mechanism
|
||||
* @class
|
||||
* @return {MongoCR} A cursor instance
|
||||
*/
|
||||
var MongoCR = function(bson) {
|
||||
this.bson = bson;
|
||||
this.authStore = [];
|
||||
};
|
||||
|
||||
// Add to store only if it does not exist
|
||||
var addAuthSession = function(authStore, session) {
|
||||
var found = false;
|
||||
|
||||
for (var i = 0; i < authStore.length; i++) {
|
||||
if (authStore[i].equal(session)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) authStore.push(session);
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
* @method
|
||||
* @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
|
||||
* @param {[]Connections} connections Connections to authenticate using this authenticator
|
||||
* @param {string} db Name of the database
|
||||
* @param {string} username Username
|
||||
* @param {string} password Password
|
||||
* @param {authResultCallback} callback The callback to return the result from the authentication
|
||||
* @return {object}
|
||||
*/
|
||||
MongoCR.prototype.auth = function(server, connections, db, username, password, callback) {
|
||||
var self = this;
|
||||
// Total connections
|
||||
var count = connections.length;
|
||||
if (count === 0) return callback(null, null);
|
||||
|
||||
// Valid connections
|
||||
var numberOfValidConnections = 0;
|
||||
var errorObject = null;
|
||||
|
||||
// For each connection we need to authenticate
|
||||
while (connections.length > 0) {
|
||||
// Execute MongoCR
|
||||
var executeMongoCR = function(connection) {
|
||||
// Write the commmand on the connection
|
||||
server(
|
||||
connection,
|
||||
new Query(
|
||||
self.bson,
|
||||
f('%s.$cmd', db),
|
||||
{
|
||||
getnonce: 1
|
||||
},
|
||||
{
|
||||
numberToSkip: 0,
|
||||
numberToReturn: 1
|
||||
}
|
||||
),
|
||||
function(err, r) {
|
||||
var nonce = null;
|
||||
var key = null;
|
||||
|
||||
// Adjust the number of connections left
|
||||
// Get nonce
|
||||
if (err == null) {
|
||||
nonce = r.result.nonce;
|
||||
// Use node md5 generator
|
||||
var md5 = crypto.createHash('md5');
|
||||
// Generate keys used for authentication
|
||||
md5.update(username + ':mongo:' + password, 'utf8');
|
||||
var hash_password = md5.digest('hex');
|
||||
// Final key
|
||||
md5 = crypto.createHash('md5');
|
||||
md5.update(nonce + username + hash_password, 'utf8');
|
||||
key = md5.digest('hex');
|
||||
}
|
||||
|
||||
// Execute command
|
||||
// Write the commmand on the connection
|
||||
server(
|
||||
connection,
|
||||
new Query(
|
||||
self.bson,
|
||||
f('%s.$cmd', db),
|
||||
{
|
||||
authenticate: 1,
|
||||
user: username,
|
||||
nonce: nonce,
|
||||
key: key
|
||||
},
|
||||
{
|
||||
numberToSkip: 0,
|
||||
numberToReturn: 1
|
||||
}
|
||||
),
|
||||
function(err, r) {
|
||||
count = count - 1;
|
||||
|
||||
// If we have an error
|
||||
if (err) {
|
||||
errorObject = err;
|
||||
} else if (r.result['$err']) {
|
||||
errorObject = r.result;
|
||||
} else if (r.result['errmsg']) {
|
||||
errorObject = r.result;
|
||||
} else {
|
||||
numberOfValidConnections = numberOfValidConnections + 1;
|
||||
}
|
||||
|
||||
// We have authenticated all connections
|
||||
if (count === 0 && numberOfValidConnections > 0) {
|
||||
// Store the auth details
|
||||
addAuthSession(self.authStore, new AuthSession(db, username, password));
|
||||
// Return correct authentication
|
||||
callback(null, true);
|
||||
} else if (count === 0) {
|
||||
if (errorObject == null)
|
||||
errorObject = new MongoError(f('failed to authenticate using mongocr'));
|
||||
callback(errorObject, false);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var _execute = function(_connection) {
|
||||
process.nextTick(function() {
|
||||
executeMongoCR(_connection);
|
||||
});
|
||||
};
|
||||
|
||||
_execute(connections.shift());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove authStore credentials
|
||||
* @method
|
||||
* @param {string} db Name of database we are removing authStore details about
|
||||
* @return {object}
|
||||
*/
|
||||
MongoCR.prototype.logout = function(dbName) {
|
||||
this.authStore = this.authStore.filter(function(x) {
|
||||
return x.db !== dbName;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Re authenticate pool
|
||||
* @method
|
||||
* @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
|
||||
* @param {[]Connections} connections Connections to authenticate using this authenticator
|
||||
* @param {authResultCallback} callback The callback to return the result from the authentication
|
||||
* @return {object}
|
||||
*/
|
||||
MongoCR.prototype.reauthenticate = function(server, connections, callback) {
|
||||
var authStore = this.authStore.slice(0);
|
||||
var count = authStore.length;
|
||||
if (count === 0) return callback(null, null);
|
||||
// Iterate over all the auth details stored
|
||||
for (var i = 0; i < authStore.length; i++) {
|
||||
this.auth(
|
||||
server,
|
||||
connections,
|
||||
authStore[i].db,
|
||||
authStore[i].username,
|
||||
authStore[i].password,
|
||||
function(err) {
|
||||
count = count - 1;
|
||||
// Done re-authenticating
|
||||
if (count === 0) {
|
||||
callback(err, null);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This is a result from a authentication strategy
|
||||
*
|
||||
* @callback authResultCallback
|
||||
* @param {error} error An error object. Set to null if no error present
|
||||
* @param {boolean} result The result of the authentication process
|
||||
* @extends AuthProvider
|
||||
*/
|
||||
class MongoCR extends AuthProvider {
|
||||
/**
|
||||
* Implementation of authentication for a single connection
|
||||
* @override
|
||||
*/
|
||||
_authenticateSingleConnection(sendAuthCommand, connection, credentials, callback) {
|
||||
const username = credentials.username;
|
||||
const password = credentials.password;
|
||||
const source = credentials.source;
|
||||
|
||||
sendAuthCommand(connection, `${source}.$cmd`, { getnonce: 1 }, (err, r) => {
|
||||
let nonce = null;
|
||||
let key = null;
|
||||
|
||||
// Get nonce
|
||||
if (err == null) {
|
||||
nonce = r.nonce;
|
||||
// Use node md5 generator
|
||||
let md5 = crypto.createHash('md5');
|
||||
// Generate keys used for authentication
|
||||
md5.update(username + ':mongo:' + password, 'utf8');
|
||||
const hash_password = md5.digest('hex');
|
||||
// Final key
|
||||
md5 = crypto.createHash('md5');
|
||||
md5.update(nonce + username + hash_password, 'utf8');
|
||||
key = md5.digest('hex');
|
||||
}
|
||||
|
||||
const authenticateCommand = {
|
||||
authenticate: 1,
|
||||
user: username,
|
||||
nonce,
|
||||
key
|
||||
};
|
||||
|
||||
sendAuthCommand(connection, `${source}.$cmd`, authenticateCommand, callback);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MongoCR;
|
||||
|
||||
+25
-173
@@ -1,183 +1,35 @@
|
||||
'use strict';
|
||||
|
||||
var f = require('util').format,
|
||||
retrieveBSON = require('../connection/utils').retrieveBSON,
|
||||
Query = require('../connection/commands').Query,
|
||||
MongoError = require('../error').MongoError;
|
||||
const retrieveBSON = require('../connection/utils').retrieveBSON;
|
||||
const AuthProvider = require('./auth_provider').AuthProvider;
|
||||
|
||||
var BSON = retrieveBSON(),
|
||||
Binary = BSON.Binary;
|
||||
|
||||
var AuthSession = function(db, username, password) {
|
||||
this.db = db;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
};
|
||||
|
||||
AuthSession.prototype.equal = function(session) {
|
||||
return (
|
||||
session.db === this.db &&
|
||||
session.username === this.username &&
|
||||
session.password === this.password
|
||||
);
|
||||
};
|
||||
// TODO: can we get the Binary type from this.bson instead?
|
||||
const BSON = retrieveBSON();
|
||||
const Binary = BSON.Binary;
|
||||
|
||||
/**
|
||||
* Creates a new Plain authentication mechanism
|
||||
* @class
|
||||
* @return {Plain} A cursor instance
|
||||
*/
|
||||
var Plain = function(bson) {
|
||||
this.bson = bson;
|
||||
this.authStore = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
* @method
|
||||
* @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
|
||||
* @param {[]Connections} connections Connections to authenticate using this authenticator
|
||||
* @param {string} db Name of the database
|
||||
* @param {string} username Username
|
||||
* @param {string} password Password
|
||||
* @param {authResultCallback} callback The callback to return the result from the authentication
|
||||
* @return {object}
|
||||
*/
|
||||
Plain.prototype.auth = function(server, connections, db, username, password, callback) {
|
||||
var self = this;
|
||||
// Total connections
|
||||
var count = connections.length;
|
||||
if (count === 0) return callback(null, null);
|
||||
|
||||
// Valid connections
|
||||
var numberOfValidConnections = 0;
|
||||
var errorObject = null;
|
||||
|
||||
// For each connection we need to authenticate
|
||||
while (connections.length > 0) {
|
||||
// Execute MongoCR
|
||||
var execute = function(connection) {
|
||||
// Create payload
|
||||
var payload = new Binary(f('\x00%s\x00%s', username, password));
|
||||
|
||||
// Let's start the sasl process
|
||||
var command = {
|
||||
saslStart: 1,
|
||||
mechanism: 'PLAIN',
|
||||
payload: payload,
|
||||
autoAuthorize: 1
|
||||
};
|
||||
|
||||
// Let's start the process
|
||||
server(
|
||||
connection,
|
||||
new Query(self.bson, '$external.$cmd', command, {
|
||||
numberToSkip: 0,
|
||||
numberToReturn: 1
|
||||
}),
|
||||
function(err, r) {
|
||||
// Adjust count
|
||||
count = count - 1;
|
||||
|
||||
// If we have an error
|
||||
if (err) {
|
||||
errorObject = err;
|
||||
} else if (r.result['$err']) {
|
||||
errorObject = r.result;
|
||||
} else if (r.result['errmsg']) {
|
||||
errorObject = r.result;
|
||||
} else {
|
||||
numberOfValidConnections = numberOfValidConnections + 1;
|
||||
}
|
||||
|
||||
// We have authenticated all connections
|
||||
if (count === 0 && numberOfValidConnections > 0) {
|
||||
// Store the auth details
|
||||
addAuthSession(self.authStore, new AuthSession(db, username, password));
|
||||
// Return correct authentication
|
||||
callback(null, true);
|
||||
} else if (count === 0) {
|
||||
if (errorObject == null)
|
||||
errorObject = new MongoError(f('failed to authenticate using mongocr'));
|
||||
callback(errorObject, false);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var _execute = function(_connection) {
|
||||
process.nextTick(function() {
|
||||
execute(_connection);
|
||||
});
|
||||
};
|
||||
|
||||
_execute(connections.shift());
|
||||
}
|
||||
};
|
||||
|
||||
// Add to store only if it does not exist
|
||||
var addAuthSession = function(authStore, session) {
|
||||
var found = false;
|
||||
|
||||
for (var i = 0; i < authStore.length; i++) {
|
||||
if (authStore[i].equal(session)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) authStore.push(session);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove authStore credentials
|
||||
* @method
|
||||
* @param {string} db Name of database we are removing authStore details about
|
||||
* @return {object}
|
||||
*/
|
||||
Plain.prototype.logout = function(dbName) {
|
||||
this.authStore = this.authStore.filter(function(x) {
|
||||
return x.db !== dbName;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Re authenticate pool
|
||||
* @method
|
||||
* @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
|
||||
* @param {[]Connections} connections Connections to authenticate using this authenticator
|
||||
* @param {authResultCallback} callback The callback to return the result from the authentication
|
||||
* @return {object}
|
||||
*/
|
||||
Plain.prototype.reauthenticate = function(server, connections, callback) {
|
||||
var authStore = this.authStore.slice(0);
|
||||
var count = authStore.length;
|
||||
if (count === 0) return callback(null, null);
|
||||
// Iterate over all the auth details stored
|
||||
for (var i = 0; i < authStore.length; i++) {
|
||||
this.auth(
|
||||
server,
|
||||
connections,
|
||||
authStore[i].db,
|
||||
authStore[i].username,
|
||||
authStore[i].password,
|
||||
function(err) {
|
||||
count = count - 1;
|
||||
// Done re-authenticating
|
||||
if (count === 0) {
|
||||
callback(err, null);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This is a result from a authentication strategy
|
||||
*
|
||||
* @callback authResultCallback
|
||||
* @param {error} error An error object. Set to null if no error present
|
||||
* @param {boolean} result The result of the authentication process
|
||||
* @extends AuthProvider
|
||||
*/
|
||||
class Plain extends AuthProvider {
|
||||
/**
|
||||
* Implementation of authentication for a single connection
|
||||
* @override
|
||||
*/
|
||||
_authenticateSingleConnection(sendAuthCommand, connection, credentials, callback) {
|
||||
const username = credentials.username;
|
||||
const password = credentials.password;
|
||||
const payload = new Binary(`\x00${username}\x00${password}`);
|
||||
const command = {
|
||||
saslStart: 1,
|
||||
mechanism: 'PLAIN',
|
||||
payload: payload,
|
||||
autoAuthorize: 1
|
||||
};
|
||||
|
||||
sendAuthCommand(connection, '$external.$cmd', command, callback);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Plain;
|
||||
|
||||
+142
-291
@@ -1,51 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
var f = require('util').format,
|
||||
crypto = require('crypto'),
|
||||
retrieveBSON = require('../connection/utils').retrieveBSON,
|
||||
Query = require('../connection/commands').Query,
|
||||
MongoError = require('../error').MongoError,
|
||||
Buffer = require('safe-buffer').Buffer;
|
||||
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 BSON = retrieveBSON();
|
||||
const Binary = BSON.Binary;
|
||||
|
||||
let saslprep;
|
||||
|
||||
try {
|
||||
saslprep = require('saslprep');
|
||||
} catch (e) {
|
||||
// don't do anything;
|
||||
}
|
||||
|
||||
var BSON = retrieveBSON(),
|
||||
Binary = BSON.Binary;
|
||||
|
||||
var AuthSession = function(db, username, password) {
|
||||
this.db = db;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
};
|
||||
|
||||
AuthSession.prototype.equal = function(session) {
|
||||
return (
|
||||
session.db === this.db &&
|
||||
session.username === this.username &&
|
||||
session.password === this.password
|
||||
);
|
||||
};
|
||||
|
||||
var id = 0;
|
||||
|
||||
/**
|
||||
* Creates a new ScramSHA authentication mechanism
|
||||
* @class
|
||||
* @return {ScramSHA} A cursor instance
|
||||
*/
|
||||
var ScramSHA = function(bson, cryptoMethod) {
|
||||
this.bson = bson;
|
||||
this.authStore = [];
|
||||
this.id = id++;
|
||||
this.cryptoMethod = cryptoMethod || 'sha1';
|
||||
};
|
||||
|
||||
var parsePayload = function(payload) {
|
||||
var dict = {};
|
||||
var parts = payload.split(',');
|
||||
@@ -136,54 +106,53 @@ function HI(data, salt, iterations, cryptoMethod) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
* @method
|
||||
* @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
|
||||
* @param {[]Connections} connections Connections to authenticate using this authenticator
|
||||
* @param {string} db Name of the database
|
||||
* @param {string} username Username
|
||||
* @param {string} password Password
|
||||
* @param {authResultCallback} callback The callback to return the result from the authentication
|
||||
* @return {object}
|
||||
* Creates a new ScramSHA authentication mechanism
|
||||
* @class
|
||||
* @extends AuthProvider
|
||||
*/
|
||||
ScramSHA.prototype.auth = function(server, connections, db, username, password, callback) {
|
||||
var self = this;
|
||||
// Total connections
|
||||
var count = connections.length;
|
||||
if (count === 0) return callback(null, null);
|
||||
|
||||
// Valid connections
|
||||
var numberOfValidConnections = 0;
|
||||
var errorObject = null;
|
||||
|
||||
const cryptoMethod = this.cryptoMethod;
|
||||
let mechanism = 'SCRAM-SHA-1';
|
||||
let processedPassword;
|
||||
|
||||
if (cryptoMethod === 'sha256') {
|
||||
mechanism = 'SCRAM-SHA-256';
|
||||
|
||||
let saslprepFn = (server.s && server.s.saslprep) || saslprep;
|
||||
|
||||
if (saslprepFn) {
|
||||
processedPassword = saslprepFn(password);
|
||||
} else {
|
||||
console.warn('Warning: no saslprep library specified. Passwords will not be sanitized');
|
||||
processedPassword = password;
|
||||
}
|
||||
} else {
|
||||
processedPassword = passwordDigest(username, password);
|
||||
class ScramSHA extends AuthProvider {
|
||||
constructor(bson, cryptoMethod) {
|
||||
super(bson);
|
||||
this.cryptoMethod = cryptoMethod || 'sha1';
|
||||
}
|
||||
|
||||
// Execute MongoCR
|
||||
var executeScram = function(connection) {
|
||||
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');
|
||||
|
||||
// Create a random nonce
|
||||
var nonce = crypto.randomBytes(24).toString('base64');
|
||||
// var nonce = 'MsQUY9iw0T9fx2MUEz6LZPwGuhVvWAhc'
|
||||
|
||||
// NOTE: This is done b/c Javascript uses UTF-16, but the server is hashing in UTF-8.
|
||||
// Since the username is not sasl-prep-d, we need to do this here.
|
||||
const firstBare = Buffer.concat([
|
||||
@@ -194,245 +163,127 @@ ScramSHA.prototype.auth = function(server, connections, db, username, password,
|
||||
]);
|
||||
|
||||
// Build command structure
|
||||
var cmd = {
|
||||
const saslStartCmd = {
|
||||
saslStart: 1,
|
||||
mechanism: mechanism,
|
||||
mechanism,
|
||||
payload: new Binary(Buffer.concat([Buffer.from('n,,', 'utf8'), firstBare])),
|
||||
autoAuthorize: 1
|
||||
};
|
||||
|
||||
// Handle the error
|
||||
var handleError = function(err, r) {
|
||||
if (err) {
|
||||
numberOfValidConnections = numberOfValidConnections - 1;
|
||||
errorObject = err;
|
||||
return false;
|
||||
} else if (r.result['$err']) {
|
||||
errorObject = r.result;
|
||||
return false;
|
||||
} else if (r.result['errmsg']) {
|
||||
errorObject = r.result;
|
||||
return false;
|
||||
} else {
|
||||
numberOfValidConnections = numberOfValidConnections + 1;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Finish up
|
||||
var finish = function(_count, _numberOfValidConnections) {
|
||||
if (_count === 0 && _numberOfValidConnections > 0) {
|
||||
// Store the auth details
|
||||
addAuthSession(self.authStore, new AuthSession(db, username, password));
|
||||
// Return correct authentication
|
||||
return callback(null, true);
|
||||
} else if (_count === 0) {
|
||||
if (errorObject == null)
|
||||
errorObject = new MongoError(f('failed to authenticate using scram'));
|
||||
return callback(errorObject, false);
|
||||
}
|
||||
};
|
||||
|
||||
var handleEnd = function(_err, _r) {
|
||||
// Handle any error
|
||||
handleError(_err, _r);
|
||||
// Adjust the number of connections
|
||||
count = count - 1;
|
||||
// Execute the finish
|
||||
finish(count, numberOfValidConnections);
|
||||
};
|
||||
|
||||
// Write the commmand on the connection
|
||||
server(
|
||||
connection,
|
||||
new Query(self.bson, f('%s.$cmd', db), cmd, {
|
||||
numberToSkip: 0,
|
||||
numberToReturn: 1
|
||||
}),
|
||||
function(err, r) {
|
||||
// Do we have an error, handle it
|
||||
if (handleError(err, r) === false) {
|
||||
count = count - 1;
|
||||
sendAuthCommand(connection, `${db}.$cmd`, saslStartCmd, (err, r) => {
|
||||
let tmpError = ScramSHA._getError(err, r);
|
||||
if (tmpError) {
|
||||
return callback(tmpError, null);
|
||||
}
|
||||
|
||||
if (count === 0 && numberOfValidConnections > 0) {
|
||||
// Store the auth details
|
||||
addAuthSession(self.authStore, new AuthSession(db, username, password));
|
||||
// Return correct authentication
|
||||
return callback(null, true);
|
||||
} else if (count === 0) {
|
||||
if (errorObject == null)
|
||||
errorObject = new MongoError(f('failed to authenticate using scram'));
|
||||
return callback(errorObject, false);
|
||||
}
|
||||
const payload = Buffer.isBuffer(r.payload) ? new Binary(r.payload) : r.payload;
|
||||
const dict = parsePayload(payload.value());
|
||||
const iterations = parseInt(dict.i, 10);
|
||||
const salt = dict.s;
|
||||
const rnonce = dict.r;
|
||||
|
||||
return;
|
||||
// Set up start of proof
|
||||
const withoutProof = `c=biws,r=${rnonce}`;
|
||||
const saltedPassword = HI(
|
||||
processedPassword,
|
||||
Buffer.from(salt, 'base64'),
|
||||
iterations,
|
||||
cryptoMethod
|
||||
);
|
||||
|
||||
if (iterations && iterations < 4096) {
|
||||
const error = new MongoError(`Server returned an invalid iteration count ${iterations}`);
|
||||
return callback(error, false);
|
||||
}
|
||||
|
||||
const clientKey = HMAC(cryptoMethod, saltedPassword, 'Client 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 saslContinueCmd = {
|
||||
saslContinue: 1,
|
||||
conversationId: r.conversationId,
|
||||
payload: new Binary(Buffer.from(clientFinal))
|
||||
};
|
||||
|
||||
sendAuthCommand(connection, `${db}.$cmd`, saslContinueCmd, (err, r) => {
|
||||
if (!r || r.done !== false) {
|
||||
return callback(err, r);
|
||||
}
|
||||
|
||||
// Get the dictionary
|
||||
var dict = parsePayload(r.result.payload.value());
|
||||
|
||||
// Unpack dictionary
|
||||
var iterations = parseInt(dict.i, 10);
|
||||
var salt = dict.s;
|
||||
var rnonce = dict.r;
|
||||
|
||||
// Set up start of proof
|
||||
var withoutProof = f('c=biws,r=%s', rnonce);
|
||||
var saltedPassword = HI(
|
||||
processedPassword,
|
||||
Buffer.from(salt, 'base64'),
|
||||
iterations,
|
||||
cryptoMethod
|
||||
);
|
||||
|
||||
if (iterations && iterations < 4096) {
|
||||
const error = new MongoError(`Server returned an invalid iteration count ${iterations}`);
|
||||
return callback(error, false);
|
||||
}
|
||||
|
||||
// Create the client key
|
||||
const clientKey = HMAC(cryptoMethod, saltedPassword, 'Client Key');
|
||||
|
||||
// Create the stored key
|
||||
const storedKey = H(cryptoMethod, clientKey);
|
||||
|
||||
// Create the authentication message
|
||||
const authMessage = [
|
||||
firstBare,
|
||||
r.result.payload.value().toString('base64'),
|
||||
withoutProof
|
||||
].join(',');
|
||||
|
||||
// Create client signature
|
||||
const clientSignature = HMAC(cryptoMethod, storedKey, authMessage);
|
||||
|
||||
// Create client proof
|
||||
const clientProof = f('p=%s', xor(clientKey, clientSignature));
|
||||
|
||||
// Create client final
|
||||
const clientFinal = [withoutProof, clientProof].join(',');
|
||||
|
||||
// Create continue message
|
||||
const cmd = {
|
||||
const retrySaslContinueCmd = {
|
||||
saslContinue: 1,
|
||||
conversationId: r.result.conversationId,
|
||||
payload: new Binary(Buffer.from(clientFinal))
|
||||
conversationId: r.conversationId,
|
||||
payload: Buffer.alloc(0)
|
||||
};
|
||||
|
||||
//
|
||||
// Execute sasl continue
|
||||
// Write the commmand on the connection
|
||||
server(
|
||||
connection,
|
||||
new Query(self.bson, f('%s.$cmd', db), cmd, {
|
||||
numberToSkip: 0,
|
||||
numberToReturn: 1
|
||||
}),
|
||||
function(err, r) {
|
||||
if (r && r.result.done === false) {
|
||||
var cmd = {
|
||||
saslContinue: 1,
|
||||
conversationId: r.result.conversationId,
|
||||
payload: Buffer.alloc(0)
|
||||
};
|
||||
|
||||
// Write the commmand on the connection
|
||||
server(
|
||||
connection,
|
||||
new Query(self.bson, f('%s.$cmd', db), cmd, {
|
||||
numberToSkip: 0,
|
||||
numberToReturn: 1
|
||||
}),
|
||||
function(err, r) {
|
||||
handleEnd(err, r);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
handleEnd(err, r);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var _execute = function(_connection) {
|
||||
process.nextTick(function() {
|
||||
executeScram(_connection);
|
||||
sendAuthCommand(connection, `${db}.$cmd`, retrySaslContinueCmd, callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// For each connection we need to authenticate
|
||||
while (connections.length > 0) {
|
||||
_execute(connections.shift());
|
||||
}
|
||||
};
|
||||
|
||||
// Add to store only if it does not exist
|
||||
var addAuthSession = function(authStore, session) {
|
||||
var found = false;
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
for (var i = 0; i < authStore.length; i++) {
|
||||
if (authStore[i].equal(session)) {
|
||||
found = true;
|
||||
break;
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) authStore.push(session);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove authStore credentials
|
||||
* @method
|
||||
* @param {string} db Name of database we are removing authStore details about
|
||||
* @return {object}
|
||||
* Creates a new ScramSHA1 authentication mechanism
|
||||
* @class
|
||||
* @extends ScramSHA
|
||||
*/
|
||||
ScramSHA.prototype.logout = function(dbName) {
|
||||
this.authStore = this.authStore.filter(function(x) {
|
||||
return x.db !== dbName;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Re authenticate pool
|
||||
* @method
|
||||
* @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
|
||||
* @param {[]Connections} connections Connections to authenticate using this authenticator
|
||||
* @param {authResultCallback} callback The callback to return the result from the authentication
|
||||
* @return {object}
|
||||
*/
|
||||
ScramSHA.prototype.reauthenticate = function(server, connections, callback) {
|
||||
var authStore = this.authStore.slice(0);
|
||||
var count = authStore.length;
|
||||
// No connections
|
||||
if (count === 0) return callback(null, null);
|
||||
// Iterate over all the auth details stored
|
||||
for (var i = 0; i < authStore.length; i++) {
|
||||
this.auth(
|
||||
server,
|
||||
connections,
|
||||
authStore[i].db,
|
||||
authStore[i].username,
|
||||
authStore[i].password,
|
||||
function(err) {
|
||||
count = count - 1;
|
||||
// Done re-authenticating
|
||||
if (count === 0) {
|
||||
callback(err, null);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
class ScramSHA1 extends ScramSHA {
|
||||
constructor(bson) {
|
||||
super(bson, 'sha1');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ScramSHA256 authentication mechanism
|
||||
* @class
|
||||
* @extends ScramSHA
|
||||
*/
|
||||
class ScramSHA256 extends ScramSHA {
|
||||
constructor(bson) {
|
||||
super(bson, 'sha256');
|
||||
|
||||
+47
-178
@@ -1,117 +1,59 @@
|
||||
'use strict';
|
||||
|
||||
const f = require('util').format;
|
||||
const Query = require('../connection/commands').Query;
|
||||
const MongoError = require('../error').MongoError;
|
||||
const AuthProvider = require('./auth_provider').AuthProvider;
|
||||
const retrieveKerberos = require('../utils').retrieveKerberos;
|
||||
|
||||
var AuthSession = function(db, username, password, options) {
|
||||
this.db = db;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.options = options;
|
||||
};
|
||||
|
||||
AuthSession.prototype.equal = function(session) {
|
||||
return (
|
||||
session.db === this.db &&
|
||||
session.username === this.username &&
|
||||
session.password === this.password
|
||||
);
|
||||
};
|
||||
let kerberos;
|
||||
|
||||
/**
|
||||
* Creates a new SSPI authentication mechanism
|
||||
* @class
|
||||
* @return {SSPI} A cursor instance
|
||||
* @extends AuthProvider
|
||||
*/
|
||||
var SSPI = function(bson) {
|
||||
this.bson = bson;
|
||||
this.authStore = [];
|
||||
};
|
||||
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';
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
* @method
|
||||
* @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
|
||||
* @param {[]Connections} connections Connections to authenticate using this authenticator
|
||||
* @param {string} db Name of the database
|
||||
* @param {string} username Username
|
||||
* @param {string} password Password
|
||||
* @param {authResultCallback} callback The callback to return the result from the authentication
|
||||
* @return {object}
|
||||
*/
|
||||
SSPI.prototype.auth = function(server, connections, db, username, password, options, callback) {
|
||||
var self = this;
|
||||
let kerberos;
|
||||
try {
|
||||
kerberos = retrieveKerberos();
|
||||
} catch (e) {
|
||||
return callback(e, null);
|
||||
SSIPAuthenticate(
|
||||
this,
|
||||
kerberos.processes.MongoAuthProcess,
|
||||
username,
|
||||
password,
|
||||
gssapiServiceName,
|
||||
sendAuthCommand,
|
||||
connection,
|
||||
mechanismProperties,
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
var gssapiServiceName = options['gssapiServiceName'] || 'mongodb';
|
||||
// Total connections
|
||||
var count = connections.length;
|
||||
if (count === 0) return callback(null, null);
|
||||
/**
|
||||
* Authenticate
|
||||
* @override
|
||||
* @method
|
||||
*/
|
||||
auth(sendAuthCommand, connections, credentials, callback) {
|
||||
if (kerberos == null) {
|
||||
try {
|
||||
kerberos = retrieveKerberos();
|
||||
} catch (e) {
|
||||
return callback(e, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Valid connections
|
||||
var numberOfValidConnections = 0;
|
||||
var errorObject = null;
|
||||
|
||||
// For each connection we need to authenticate
|
||||
while (connections.length > 0) {
|
||||
// Execute MongoCR
|
||||
var execute = function(connection) {
|
||||
// Start Auth process for a connection
|
||||
SSIPAuthenticate(
|
||||
self,
|
||||
kerberos.processes.MongoAuthProcess,
|
||||
username,
|
||||
password,
|
||||
gssapiServiceName,
|
||||
server,
|
||||
connection,
|
||||
options,
|
||||
function(err, r) {
|
||||
// Adjust count
|
||||
count = count - 1;
|
||||
|
||||
// If we have an error
|
||||
if (err) {
|
||||
errorObject = err;
|
||||
} else if (r && typeof r === 'object' && r.result['$err']) {
|
||||
errorObject = r.result;
|
||||
} else if (r && typeof r === 'object' && r.result['errmsg']) {
|
||||
errorObject = r.result;
|
||||
} else {
|
||||
numberOfValidConnections = numberOfValidConnections + 1;
|
||||
}
|
||||
|
||||
// We have authenticated all connections
|
||||
if (count === 0 && numberOfValidConnections > 0) {
|
||||
// Store the auth details
|
||||
addAuthSession(self.authStore, new AuthSession(db, username, password, options));
|
||||
// Return correct authentication
|
||||
callback(null, true);
|
||||
} else if (count === 0) {
|
||||
if (errorObject == null)
|
||||
errorObject = new MongoError(f('failed to authenticate using mongocr'));
|
||||
callback(errorObject, false);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var _execute = function(_connection) {
|
||||
process.nextTick(function() {
|
||||
execute(_connection);
|
||||
});
|
||||
};
|
||||
|
||||
_execute(connections.shift());
|
||||
super.auth(sendAuthCommand, connections, credentials, callback);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function SSIPAuthenticate(
|
||||
self,
|
||||
@@ -119,7 +61,7 @@ function SSIPAuthenticate(
|
||||
username,
|
||||
password,
|
||||
gssapiServiceName,
|
||||
server,
|
||||
sendAuthCommand,
|
||||
connection,
|
||||
options,
|
||||
callback
|
||||
@@ -132,12 +74,7 @@ function SSIPAuthenticate(
|
||||
);
|
||||
|
||||
function authCommand(command, authCb) {
|
||||
const query = new Query(self.bson, '$external.$cmd', command, {
|
||||
numberToSkip: 0,
|
||||
numberToReturn: 1
|
||||
});
|
||||
|
||||
server(connection, query, authCb);
|
||||
sendAuthCommand(connection, '$external.$cmd', command, authCb);
|
||||
}
|
||||
|
||||
authProcess.init(username, password, err => {
|
||||
@@ -153,9 +90,8 @@ function SSIPAuthenticate(
|
||||
autoAuthorize: 1
|
||||
};
|
||||
|
||||
authCommand(command, (err, result) => {
|
||||
authCommand(command, (err, doc) => {
|
||||
if (err) return callback(err, false);
|
||||
const doc = result.result;
|
||||
|
||||
authProcess.transition(doc.payload, (err, payload) => {
|
||||
if (err) return callback(err, false);
|
||||
@@ -165,9 +101,8 @@ function SSIPAuthenticate(
|
||||
payload
|
||||
};
|
||||
|
||||
authCommand(command, (err, result) => {
|
||||
authCommand(command, (err, doc) => {
|
||||
if (err) return callback(err, false);
|
||||
const doc = result.result;
|
||||
|
||||
authProcess.transition(doc.payload, (err, payload) => {
|
||||
if (err) return callback(err, false);
|
||||
@@ -193,70 +128,4 @@ function SSIPAuthenticate(
|
||||
});
|
||||
}
|
||||
|
||||
// Add to store only if it does not exist
|
||||
var addAuthSession = function(authStore, session) {
|
||||
var found = false;
|
||||
|
||||
for (var i = 0; i < authStore.length; i++) {
|
||||
if (authStore[i].equal(session)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) authStore.push(session);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove authStore credentials
|
||||
* @method
|
||||
* @param {string} db Name of database we are removing authStore details about
|
||||
* @return {object}
|
||||
*/
|
||||
SSPI.prototype.logout = function(dbName) {
|
||||
this.authStore = this.authStore.filter(function(x) {
|
||||
return x.db !== dbName;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Re authenticate pool
|
||||
* @method
|
||||
* @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
|
||||
* @param {[]Connections} connections Connections to authenticate using this authenticator
|
||||
* @param {authResultCallback} callback The callback to return the result from the authentication
|
||||
* @return {object}
|
||||
*/
|
||||
SSPI.prototype.reauthenticate = function(server, connections, callback) {
|
||||
var authStore = this.authStore.slice(0);
|
||||
var count = authStore.length;
|
||||
if (count === 0) return callback(null, null);
|
||||
// Iterate over all the auth details stored
|
||||
for (var i = 0; i < authStore.length; i++) {
|
||||
this.auth(
|
||||
server,
|
||||
connections,
|
||||
authStore[i].db,
|
||||
authStore[i].username,
|
||||
authStore[i].password,
|
||||
authStore[i].options,
|
||||
function(err) {
|
||||
count = count - 1;
|
||||
// Done re-authenticating
|
||||
if (count === 0) {
|
||||
callback(err, null);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This is a result from a authentication strategy
|
||||
*
|
||||
* @callback authResultCallback
|
||||
* @param {error} error An error object. Set to null if no error present
|
||||
* @param {boolean} result The result of the authentication process
|
||||
*/
|
||||
|
||||
module.exports = SSPI;
|
||||
|
||||
+15
-168
@@ -1,179 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
var f = require('util').format,
|
||||
Query = require('../connection/commands').Query,
|
||||
MongoError = require('../error').MongoError;
|
||||
|
||||
var AuthSession = function(db, username, password) {
|
||||
this.db = db;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
};
|
||||
|
||||
AuthSession.prototype.equal = function(session) {
|
||||
return (
|
||||
session.db === this.db &&
|
||||
session.username === this.username &&
|
||||
session.password === this.password
|
||||
);
|
||||
};
|
||||
const AuthProvider = require('./auth_provider').AuthProvider;
|
||||
|
||||
/**
|
||||
* Creates a new X509 authentication mechanism
|
||||
* @class
|
||||
* @return {X509} A cursor instance
|
||||
* @extends AuthProvider
|
||||
*/
|
||||
var X509 = function(bson) {
|
||||
this.bson = bson;
|
||||
this.authStore = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
* @method
|
||||
* @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
|
||||
* @param {[]Connections} connections Connections to authenticate using this authenticator
|
||||
* @param {string} db Name of the database
|
||||
* @param {string} username Username
|
||||
* @param {string} password Password
|
||||
* @param {authResultCallback} callback The callback to return the result from the authentication
|
||||
* @return {object}
|
||||
*/
|
||||
X509.prototype.auth = function(server, connections, db, username, password, callback) {
|
||||
var self = this;
|
||||
// Total connections
|
||||
var count = connections.length;
|
||||
if (count === 0) return callback(null, null);
|
||||
|
||||
// Valid connections
|
||||
var numberOfValidConnections = 0;
|
||||
var errorObject = null;
|
||||
|
||||
// For each connection we need to authenticate
|
||||
while (connections.length > 0) {
|
||||
// Execute MongoCR
|
||||
var execute = function(connection) {
|
||||
// Let's start the sasl process
|
||||
var command = {
|
||||
authenticate: 1,
|
||||
mechanism: 'MONGODB-X509'
|
||||
};
|
||||
|
||||
// Add username if specified
|
||||
if (username) {
|
||||
command.user = username;
|
||||
}
|
||||
|
||||
// Let's start the process
|
||||
server(
|
||||
connection,
|
||||
new Query(self.bson, '$external.$cmd', command, {
|
||||
numberToSkip: 0,
|
||||
numberToReturn: 1
|
||||
}),
|
||||
function(err, r) {
|
||||
// Adjust count
|
||||
count = count - 1;
|
||||
|
||||
// If we have an error
|
||||
if (err) {
|
||||
errorObject = err;
|
||||
} else if (r.result['$err']) {
|
||||
errorObject = r.result;
|
||||
} else if (r.result['errmsg']) {
|
||||
errorObject = r.result;
|
||||
} else {
|
||||
numberOfValidConnections = numberOfValidConnections + 1;
|
||||
}
|
||||
|
||||
// We have authenticated all connections
|
||||
if (count === 0 && numberOfValidConnections > 0) {
|
||||
// Store the auth details
|
||||
addAuthSession(self.authStore, new AuthSession(db, username, password));
|
||||
// Return correct authentication
|
||||
callback(null, true);
|
||||
} else if (count === 0) {
|
||||
if (errorObject == null)
|
||||
errorObject = new MongoError(f('failed to authenticate using mongocr'));
|
||||
callback(errorObject, false);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var _execute = function(_connection) {
|
||||
process.nextTick(function() {
|
||||
execute(_connection);
|
||||
});
|
||||
};
|
||||
|
||||
_execute(connections.shift());
|
||||
}
|
||||
};
|
||||
|
||||
// Add to store only if it does not exist
|
||||
var addAuthSession = function(authStore, session) {
|
||||
var found = false;
|
||||
|
||||
for (var i = 0; i < authStore.length; i++) {
|
||||
if (authStore[i].equal(session)) {
|
||||
found = true;
|
||||
break;
|
||||
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;
|
||||
}
|
||||
|
||||
sendAuthCommand(connection, '$external.$cmd', command, callback);
|
||||
}
|
||||
|
||||
if (!found) authStore.push(session);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove authStore credentials
|
||||
* @method
|
||||
* @param {string} db Name of database we are removing authStore details about
|
||||
* @return {object}
|
||||
*/
|
||||
X509.prototype.logout = function(dbName) {
|
||||
this.authStore = this.authStore.filter(function(x) {
|
||||
return x.db !== dbName;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Re authenticate pool
|
||||
* @method
|
||||
* @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
|
||||
* @param {[]Connections} connections Connections to authenticate using this authenticator
|
||||
* @param {authResultCallback} callback The callback to return the result from the authentication
|
||||
* @return {object}
|
||||
*/
|
||||
X509.prototype.reauthenticate = function(server, connections, callback) {
|
||||
var authStore = this.authStore.slice(0);
|
||||
var count = authStore.length;
|
||||
if (count === 0) return callback(null, null);
|
||||
// Iterate over all the auth details stored
|
||||
for (var i = 0; i < authStore.length; i++) {
|
||||
this.auth(
|
||||
server,
|
||||
connections,
|
||||
authStore[i].db,
|
||||
authStore[i].username,
|
||||
authStore[i].password,
|
||||
function(err) {
|
||||
count = count - 1;
|
||||
// Done re-authenticating
|
||||
if (count === 0) {
|
||||
callback(err, null);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This is a result from a authentication strategy
|
||||
*
|
||||
* @callback authResultCallback
|
||||
* @param {error} error An error object. Set to null if no error present
|
||||
* @param {boolean} result The result of the authentication process
|
||||
*/
|
||||
}
|
||||
|
||||
module.exports = X509;
|
||||
|
||||
+10
-3
@@ -1,4 +1,5 @@
|
||||
'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;
|
||||
@@ -17,7 +18,7 @@ const SENSITIVE_COMMANDS = new Set([
|
||||
]);
|
||||
|
||||
// helper methods
|
||||
const extractCommandName = command => Object.keys(command)[0];
|
||||
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];
|
||||
@@ -75,6 +76,10 @@ const extractCommand = command => {
|
||||
};
|
||||
}
|
||||
|
||||
if (command instanceof Msg) {
|
||||
return command.command;
|
||||
}
|
||||
|
||||
if (command.query && command.query.$query) {
|
||||
let result;
|
||||
if (command.ns === 'admin.$cmd') {
|
||||
@@ -90,8 +95,7 @@ const extractCommand = command => {
|
||||
}
|
||||
|
||||
Object.keys(LEGACY_FIND_OPTIONS_MAP).forEach(key => {
|
||||
if (typeof command.options[key] !== 'undefined')
|
||||
result[LEGACY_FIND_OPTIONS_MAP[key]] = command.options[key];
|
||||
if (typeof command[key] !== 'undefined') result[LEGACY_FIND_OPTIONS_MAP[key]] = command[key];
|
||||
});
|
||||
|
||||
OP_QUERY_KEYS.forEach(key => {
|
||||
@@ -143,6 +147,9 @@ const extractReply = (command, reply) => {
|
||||
};
|
||||
}
|
||||
|
||||
// in the event of a `noResponse` command, just return
|
||||
if (reply === null) return reply;
|
||||
|
||||
return reply.result;
|
||||
};
|
||||
|
||||
|
||||
+3
-1
@@ -19,7 +19,9 @@ var CommandResult = function(result, connection, message) {
|
||||
* @return {object}
|
||||
*/
|
||||
CommandResult.prototype.toJSON = function() {
|
||||
return this.result;
|
||||
let result = Object.assign({}, this, this.result);
|
||||
delete result.message;
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+10
-52
@@ -3,7 +3,6 @@
|
||||
var retrieveBSON = require('./utils').retrieveBSON;
|
||||
var BSON = retrieveBSON();
|
||||
var Long = BSON.Long;
|
||||
const MongoError = require('../error').MongoError;
|
||||
const Buffer = require('safe-buffer').Buffer;
|
||||
|
||||
// Incrementing request id
|
||||
@@ -22,7 +21,7 @@ var OPTS_EXHAUST = 64;
|
||||
var OPTS_PARTIAL = 128;
|
||||
|
||||
// Response flags
|
||||
var CURSOR_NOT_FOUND = 0;
|
||||
var CURSOR_NOT_FOUND = 1;
|
||||
var QUERY_FAILURE = 2;
|
||||
var SHARD_CONFIG_STALE = 4;
|
||||
var AWAIT_CAPABLE = 8;
|
||||
@@ -46,9 +45,6 @@ var Query = function(bson, ns, query, options) {
|
||||
this.ns = ns;
|
||||
this.query = query;
|
||||
|
||||
// Ensure empty options
|
||||
this.options = options || {};
|
||||
|
||||
// Additional options
|
||||
this.numberToSkip = options.numberToSkip || 0;
|
||||
this.numberToReturn = options.numberToReturn || 0;
|
||||
@@ -466,53 +462,6 @@ Response.prototype.parse = function(options) {
|
||||
// (See https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/#wire-op-reply)
|
||||
this.index = 20;
|
||||
|
||||
//
|
||||
// Single document and documentsReturnedIn set
|
||||
//
|
||||
if (this.numberReturned === 1 && documentsReturnedIn != null && raw) {
|
||||
// Calculate the bson size
|
||||
bsonSize =
|
||||
this.data[this.index] |
|
||||
(this.data[this.index + 1] << 8) |
|
||||
(this.data[this.index + 2] << 16) |
|
||||
(this.data[this.index + 3] << 24);
|
||||
// Slice out the buffer containing the command result document
|
||||
var document = this.data.slice(this.index, this.index + bsonSize);
|
||||
// Set up field we wish to keep as raw
|
||||
var fieldsAsRaw = {};
|
||||
fieldsAsRaw[documentsReturnedIn] = true;
|
||||
_options.fieldsAsRaw = fieldsAsRaw;
|
||||
|
||||
// Deserialize but keep the array of documents in non-parsed form
|
||||
var doc = this.bson.deserialize(document, _options);
|
||||
|
||||
if (doc instanceof Error) {
|
||||
throw doc;
|
||||
}
|
||||
|
||||
if (doc.errmsg) {
|
||||
throw new MongoError(doc.errmsg);
|
||||
}
|
||||
|
||||
if (!doc.cursor) {
|
||||
throw new MongoError('Cursor not found');
|
||||
}
|
||||
|
||||
// Get the documents
|
||||
this.documents = doc.cursor[documentsReturnedIn];
|
||||
this.numberReturned = this.documents.length;
|
||||
// Ensure we have a Long valie cursor id
|
||||
this.cursorId =
|
||||
typeof doc.cursor.id === 'number' ? Long.fromNumber(doc.cursor.id) : doc.cursor.id;
|
||||
|
||||
// Adjust the index
|
||||
this.index = this.index + bsonSize;
|
||||
|
||||
// Set as parsed
|
||||
this.parsed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Parse Body
|
||||
//
|
||||
@@ -537,6 +486,15 @@ Response.prototype.parse = function(options) {
|
||||
this.index = this.index + bsonSize;
|
||||
}
|
||||
|
||||
if (this.documents.length === 1 && documentsReturnedIn != null && raw) {
|
||||
const fieldsAsRaw = {};
|
||||
fieldsAsRaw[documentsReturnedIn] = true;
|
||||
_options.fieldsAsRaw = fieldsAsRaw;
|
||||
|
||||
const doc = this.bson.deserialize(this.documents[0], _options);
|
||||
this.documents = [doc];
|
||||
}
|
||||
|
||||
// Set parsed
|
||||
this.parsed = true;
|
||||
};
|
||||
|
||||
+370
@@ -0,0 +1,370 @@
|
||||
'use strict';
|
||||
const net = require('net');
|
||||
const tls = require('tls');
|
||||
const Connection = require('./connection');
|
||||
const Query = require('./commands').Query;
|
||||
const createClientInfo = require('../topologies/shared').createClientInfo;
|
||||
const MongoError = require('../error').MongoError;
|
||||
const MongoNetworkError = require('../error').MongoNetworkError;
|
||||
const defaultAuthProviders = require('../auth/defaultAuthProviders').defaultAuthProviders;
|
||||
const WIRE_CONSTANTS = require('../wireprotocol/constants');
|
||||
const MAX_SUPPORTED_WIRE_VERSION = WIRE_CONSTANTS.MAX_SUPPORTED_WIRE_VERSION;
|
||||
const MAX_SUPPORTED_SERVER_VERSION = WIRE_CONSTANTS.MAX_SUPPORTED_SERVER_VERSION;
|
||||
const MIN_SUPPORTED_WIRE_VERSION = WIRE_CONSTANTS.MIN_SUPPORTED_WIRE_VERSION;
|
||||
const MIN_SUPPORTED_SERVER_VERSION = WIRE_CONSTANTS.MIN_SUPPORTED_SERVER_VERSION;
|
||||
let AUTH_PROVIDERS;
|
||||
|
||||
function connect(options, callback) {
|
||||
if (AUTH_PROVIDERS == null) {
|
||||
AUTH_PROVIDERS = defaultAuthProviders(options.bson);
|
||||
}
|
||||
|
||||
if (options.family !== void 0) {
|
||||
makeConnection(options.family, options, (err, socket) => {
|
||||
if (err) {
|
||||
callback(err, socket); // in the error case, `socket` is the originating error event name
|
||||
return;
|
||||
}
|
||||
|
||||
performInitialHandshake(new Connection(socket, options), options, callback);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return makeConnection(6, options, (err, ipv6Socket) => {
|
||||
if (err) {
|
||||
makeConnection(4, options, (err, ipv4Socket) => {
|
||||
if (err) {
|
||||
callback(err, ipv4Socket); // in the error case, `ipv4Socket` is the originating error event name
|
||||
return;
|
||||
}
|
||||
|
||||
performInitialHandshake(new Connection(ipv4Socket, options), options, callback);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
performInitialHandshake(new Connection(ipv6Socket, options), options, callback);
|
||||
});
|
||||
}
|
||||
|
||||
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}` };
|
||||
}
|
||||
|
||||
function checkSupportedServer(ismaster, options) {
|
||||
const serverVersionHighEnough =
|
||||
ismaster &&
|
||||
typeof ismaster.maxWireVersion === 'number' &&
|
||||
ismaster.maxWireVersion >= MIN_SUPPORTED_WIRE_VERSION;
|
||||
const serverVersionLowEnough =
|
||||
ismaster &&
|
||||
typeof ismaster.minWireVersion === 'number' &&
|
||||
ismaster.minWireVersion <= MAX_SUPPORTED_WIRE_VERSION;
|
||||
|
||||
if (serverVersionHighEnough) {
|
||||
if (serverVersionLowEnough) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const message = `Server at ${options.host}:${options.port} reports minimum wire version ${
|
||||
ismaster.minWireVersion
|
||||
}, but this version of the Node.js Driver requires at most ${MAX_SUPPORTED_WIRE_VERSION} (MongoDB ${MAX_SUPPORTED_SERVER_VERSION})`;
|
||||
return new MongoError(message);
|
||||
}
|
||||
|
||||
const message = `Server at ${options.host}:${
|
||||
options.port
|
||||
} reports maximum wire version ${ismaster.maxWireVersion ||
|
||||
0}, but this version of the Node.js Driver requires at least ${MIN_SUPPORTED_WIRE_VERSION} (MongoDB ${MIN_SUPPORTED_SERVER_VERSION})`;
|
||||
return new MongoError(message);
|
||||
}
|
||||
|
||||
function performInitialHandshake(conn, options, _callback) {
|
||||
const callback = function(err, ret) {
|
||||
if (err && conn) {
|
||||
conn.destroy();
|
||||
}
|
||||
_callback(err, ret);
|
||||
};
|
||||
|
||||
let compressors = [];
|
||||
if (options.compression && options.compression.compressors) {
|
||||
compressors = options.compression.compressors;
|
||||
}
|
||||
|
||||
const handshakeDoc = Object.assign(
|
||||
{
|
||||
ismaster: true,
|
||||
client: createClientInfo(options),
|
||||
compression: compressors
|
||||
},
|
||||
getSaslSupportedMechs(options)
|
||||
);
|
||||
|
||||
const start = new Date().getTime();
|
||||
runCommand(conn, 'admin.$cmd', handshakeDoc, options, (err, ismaster) => {
|
||||
if (err) {
|
||||
callback(err, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ismaster.ok === 0) {
|
||||
callback(new MongoError(ismaster), null);
|
||||
return;
|
||||
}
|
||||
|
||||
const supportedServerErr = checkSupportedServer(ismaster, options);
|
||||
if (supportedServerErr) {
|
||||
callback(supportedServerErr, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// resolve compression
|
||||
if (ismaster.compression) {
|
||||
const agreedCompressors = compressors.filter(
|
||||
compressor => ismaster.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;
|
||||
|
||||
const credentials = options.credentials;
|
||||
if (!ismaster.arbiterOnly && credentials) {
|
||||
credentials.resolveAuthMechanism(ismaster);
|
||||
authenticate(conn, credentials, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, conn);
|
||||
});
|
||||
}
|
||||
|
||||
const LEGAL_SSL_SOCKET_OPTIONS = [
|
||||
'pfx',
|
||||
'key',
|
||||
'passphrase',
|
||||
'cert',
|
||||
'ca',
|
||||
'ciphers',
|
||||
'NPNProtocols',
|
||||
'ALPNProtocols',
|
||||
'servername',
|
||||
'ecdhCurve',
|
||||
'secureProtocol',
|
||||
'secureContext',
|
||||
'session',
|
||||
'minDHSize',
|
||||
'crl',
|
||||
'rejectUnauthorized'
|
||||
];
|
||||
|
||||
function parseConnectOptions(family, options) {
|
||||
const host = typeof options.host === 'string' ? options.host : 'localhost';
|
||||
if (host.indexOf('/') !== -1) {
|
||||
return { path: host };
|
||||
}
|
||||
|
||||
const result = {
|
||||
family,
|
||||
host,
|
||||
port: typeof options.port === 'number' ? options.port : 27017,
|
||||
rejectUnauthorized: false
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseSslOptions(family, options) {
|
||||
const result = parseConnectOptions(family, options);
|
||||
|
||||
// Merge in valid SSL options
|
||||
for (const name in options) {
|
||||
if (options[name] != null && LEGAL_SSL_SOCKET_OPTIONS.indexOf(name) !== -1) {
|
||||
result[name] = options[name];
|
||||
}
|
||||
}
|
||||
|
||||
// Override checkServerIdentity behavior
|
||||
if (options.checkServerIdentity === false) {
|
||||
// Skip the identiy check by retuning undefined as per node documents
|
||||
// https://nodejs.org/api/tls.html#tls_tls_connect_options_callback
|
||||
result.checkServerIdentity = function() {
|
||||
return undefined;
|
||||
};
|
||||
} else if (typeof options.checkServerIdentity === 'function') {
|
||||
result.checkServerIdentity = options.checkServerIdentity;
|
||||
}
|
||||
|
||||
// Set default sni servername to be the same as host
|
||||
if (result.servername == null) {
|
||||
result.servername = result.host;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function makeConnection(family, options, _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;
|
||||
const noDelay = typeof options.noDelay === 'boolean' ? options.noDelay : true;
|
||||
const connectionTimeout =
|
||||
typeof options.connectionTimeout === 'number' ? options.connectionTimeout : 30000;
|
||||
const socketTimeout = typeof options.socketTimeout === 'number' ? options.socketTimeout : 360000;
|
||||
const rejectUnauthorized =
|
||||
typeof options.rejectUnauthorized === 'boolean' ? options.rejectUnauthorized : true;
|
||||
|
||||
if (keepAliveInitialDelay > socketTimeout) {
|
||||
keepAliveInitialDelay = Math.round(socketTimeout / 2);
|
||||
}
|
||||
|
||||
let socket;
|
||||
const callback = function(err, ret) {
|
||||
if (err && socket) {
|
||||
socket.destroy();
|
||||
}
|
||||
_callback(err, ret);
|
||||
};
|
||||
|
||||
try {
|
||||
if (useSsl) {
|
||||
socket = tls.connect(parseSslOptions(family, options));
|
||||
if (typeof socket.disableRenegotiation === 'function') {
|
||||
socket.disableRenegotiation();
|
||||
}
|
||||
} else {
|
||||
socket = net.createConnection(parseConnectOptions(family, options));
|
||||
}
|
||||
} catch (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
socket.setKeepAlive(keepAlive, keepAliveInitialDelay);
|
||||
socket.setTimeout(connectionTimeout);
|
||||
socket.setNoDelay(noDelay);
|
||||
|
||||
const errorEvents = ['error', 'close', 'timeout', 'parseError', 'connect'];
|
||||
function errorHandler(eventName) {
|
||||
return err => {
|
||||
errorEvents.forEach(event => socket.removeAllListeners(event));
|
||||
socket.removeListener('connect', connectHandler);
|
||||
callback(connectionFailureError(eventName, err), eventName);
|
||||
};
|
||||
}
|
||||
|
||||
function connectHandler() {
|
||||
errorEvents.forEach(event => socket.removeAllListeners(event));
|
||||
if (socket.authorizationError && rejectUnauthorized) {
|
||||
return callback(socket.authorizationError);
|
||||
}
|
||||
|
||||
socket.setTimeout(socketTimeout);
|
||||
callback(null, socket);
|
||||
}
|
||||
|
||||
socket.once('error', errorHandler('error'));
|
||||
socket.once('close', errorHandler('close'));
|
||||
socket.once('timeout', errorHandler('timeout'));
|
||||
socket.once('parseError', errorHandler('parseError'));
|
||||
socket.once('connect', connectHandler);
|
||||
}
|
||||
|
||||
const CONNECTION_ERROR_EVENTS = ['error', 'close', 'timeout', 'parseError'];
|
||||
function runCommand(conn, ns, command, options, callback) {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
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
|
||||
});
|
||||
|
||||
function errorHandler(err) {
|
||||
conn.resetSocketTimeout();
|
||||
CONNECTION_ERROR_EVENTS.forEach(eventName => conn.removeListener(eventName, errorHandler));
|
||||
conn.removeListener('message', messageHandler);
|
||||
callback(err, null);
|
||||
}
|
||||
|
||||
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(null, 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(null, conn);
|
||||
});
|
||||
}
|
||||
|
||||
function connectionFailureError(type, err) {
|
||||
switch (type) {
|
||||
case 'error':
|
||||
return new MongoNetworkError(err);
|
||||
case 'timeout':
|
||||
return new MongoNetworkError(`connection timed out`);
|
||||
case 'close':
|
||||
return new MongoNetworkError(`connection closed`);
|
||||
default:
|
||||
return new MongoNetworkError(`unknown network error`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = connect;
|
||||
+406
-587
File diff suppressed because it is too large
Load Diff
+219
@@ -0,0 +1,219 @@
|
||||
'use strict';
|
||||
|
||||
// Implementation of OP_MSG spec:
|
||||
// https://github.com/mongodb/specifications/blob/master/source/message/OP_MSG.rst
|
||||
//
|
||||
// struct Section {
|
||||
// uint8 payloadType;
|
||||
// union payload {
|
||||
// document document; // payloadType == 0
|
||||
// struct sequence { // payloadType == 1
|
||||
// int32 size;
|
||||
// cstring identifier;
|
||||
// document* documents;
|
||||
// };
|
||||
// };
|
||||
// };
|
||||
|
||||
// struct OP_MSG {
|
||||
// struct MsgHeader {
|
||||
// int32 messageLength;
|
||||
// int32 requestID;
|
||||
// int32 responseTo;
|
||||
// int32 opCode = 2013;
|
||||
// };
|
||||
// uint32 flagBits;
|
||||
// Section+ sections;
|
||||
// [uint32 checksum;]
|
||||
// };
|
||||
|
||||
const opcodes = require('../wireprotocol/shared').opcodes;
|
||||
const databaseNamespace = require('../wireprotocol/shared').databaseNamespace;
|
||||
const ReadPreference = require('../topologies/read_preference');
|
||||
|
||||
// Incrementing request id
|
||||
let _requestId = 0;
|
||||
|
||||
// Msg Flags
|
||||
const OPTS_CHECKSUM_PRESENT = 1;
|
||||
const OPTS_MORE_TO_COME = 2;
|
||||
const OPTS_EXHAUST_ALLOWED = 1 << 16;
|
||||
|
||||
class Msg {
|
||||
constructor(bson, ns, command, options) {
|
||||
// Basic options needed to be passed in
|
||||
if (command == null) throw new Error('query must be specified for query');
|
||||
|
||||
// Basic options
|
||||
this.bson = bson;
|
||||
this.ns = ns;
|
||||
this.command = command;
|
||||
this.command.$db = databaseNamespace(ns);
|
||||
|
||||
if (options.readPreference && options.readPreference.mode !== ReadPreference.PRIMARY) {
|
||||
this.command.$readPreference = options.readPreference.toJSON();
|
||||
}
|
||||
|
||||
// Ensure empty options
|
||||
this.options = options || {};
|
||||
|
||||
// Additional options
|
||||
this.requestId = Msg.getRequestId();
|
||||
|
||||
// Serialization option
|
||||
this.serializeFunctions =
|
||||
typeof options.serializeFunctions === 'boolean' ? options.serializeFunctions : false;
|
||||
this.ignoreUndefined =
|
||||
typeof options.ignoreUndefined === 'boolean' ? options.ignoreUndefined : false;
|
||||
this.checkKeys = typeof options.checkKeys === 'boolean' ? options.checkKeys : false;
|
||||
this.maxBsonSize = options.maxBsonSize || 1024 * 1024 * 16;
|
||||
|
||||
// flags
|
||||
this.checksumPresent = false;
|
||||
this.moreToCome = options.moreToCome || false;
|
||||
this.exhaustAllowed = false;
|
||||
}
|
||||
|
||||
toBin() {
|
||||
const buffers = [];
|
||||
let flags = 0;
|
||||
|
||||
if (this.checksumPresent) {
|
||||
flags |= OPTS_CHECKSUM_PRESENT;
|
||||
}
|
||||
|
||||
if (this.moreToCome) {
|
||||
flags |= OPTS_MORE_TO_COME;
|
||||
}
|
||||
|
||||
if (this.exhaustAllowed) {
|
||||
flags |= OPTS_EXHAUST_ALLOWED;
|
||||
}
|
||||
|
||||
const header = new Buffer(
|
||||
4 * 4 + // Header
|
||||
4 // Flags
|
||||
);
|
||||
|
||||
buffers.push(header);
|
||||
|
||||
let totalLength = header.length;
|
||||
const command = this.command;
|
||||
totalLength += this.makeDocumentSegment(buffers, command);
|
||||
|
||||
header.writeInt32LE(totalLength, 0); // messageLength
|
||||
header.writeInt32LE(this.requestId, 4); // requestID
|
||||
header.writeInt32LE(0, 8); // responseTo
|
||||
header.writeInt32LE(opcodes.OP_MSG, 12); // opCode
|
||||
header.writeUInt32LE(flags, 16); // flags
|
||||
return buffers;
|
||||
}
|
||||
|
||||
makeDocumentSegment(buffers, document) {
|
||||
const payloadTypeBuffer = new Buffer(1);
|
||||
payloadTypeBuffer[0] = 0;
|
||||
|
||||
const documentBuffer = this.serializeBson(document);
|
||||
buffers.push(payloadTypeBuffer);
|
||||
buffers.push(documentBuffer);
|
||||
|
||||
return payloadTypeBuffer.length + documentBuffer.length;
|
||||
}
|
||||
|
||||
serializeBson(document) {
|
||||
return this.bson.serialize(document, {
|
||||
checkKeys: this.checkKeys,
|
||||
serializeFunctions: this.serializeFunctions,
|
||||
ignoreUndefined: this.ignoreUndefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Msg.getRequestId = function() {
|
||||
return ++_requestId;
|
||||
};
|
||||
|
||||
class BinMsg {
|
||||
constructor(bson, message, msgHeader, msgBody, opts) {
|
||||
opts = opts || { promoteLongs: true, promoteValues: true, promoteBuffers: false };
|
||||
this.parsed = false;
|
||||
this.raw = message;
|
||||
this.data = msgBody;
|
||||
this.bson = bson;
|
||||
this.opts = opts;
|
||||
|
||||
// Read the message header
|
||||
this.length = msgHeader.length;
|
||||
this.requestId = msgHeader.requestId;
|
||||
this.responseTo = msgHeader.responseTo;
|
||||
this.opCode = msgHeader.opCode;
|
||||
this.fromCompressed = msgHeader.fromCompressed;
|
||||
|
||||
// Read response flags
|
||||
this.responseFlags = msgBody.readInt32LE(0);
|
||||
this.checksumPresent = (this.responseFlags & OPTS_CHECKSUM_PRESENT) !== 0;
|
||||
this.moreToCome = (this.responseFlags & OPTS_MORE_TO_COME) !== 0;
|
||||
this.exhaustAllowed = (this.responseFlags & OPTS_EXHAUST_ALLOWED) !== 0;
|
||||
this.promoteLongs = typeof opts.promoteLongs === 'boolean' ? opts.promoteLongs : true;
|
||||
this.promoteValues = typeof opts.promoteValues === 'boolean' ? opts.promoteValues : true;
|
||||
this.promoteBuffers = typeof opts.promoteBuffers === 'boolean' ? opts.promoteBuffers : false;
|
||||
|
||||
this.documents = [];
|
||||
}
|
||||
|
||||
isParsed() {
|
||||
return this.parsed;
|
||||
}
|
||||
|
||||
parse(options) {
|
||||
// Don't parse again if not needed
|
||||
if (this.parsed) return;
|
||||
options = options || {};
|
||||
|
||||
this.index = 4;
|
||||
// Allow the return of raw documents instead of parsing
|
||||
const raw = options.raw || false;
|
||||
const documentsReturnedIn = options.documentsReturnedIn || null;
|
||||
const promoteLongs =
|
||||
typeof options.promoteLongs === 'boolean' ? options.promoteLongs : this.opts.promoteLongs;
|
||||
const promoteValues =
|
||||
typeof options.promoteValues === 'boolean' ? options.promoteValues : this.opts.promoteValues;
|
||||
const promoteBuffers =
|
||||
typeof options.promoteBuffers === 'boolean'
|
||||
? options.promoteBuffers
|
||||
: this.opts.promoteBuffers;
|
||||
|
||||
// Set up the options
|
||||
const _options = {
|
||||
promoteLongs: promoteLongs,
|
||||
promoteValues: promoteValues,
|
||||
promoteBuffers: promoteBuffers
|
||||
};
|
||||
|
||||
while (this.index < this.data.length) {
|
||||
const payloadType = this.data.readUInt8(this.index++);
|
||||
if (payloadType === 1) {
|
||||
console.error('TYPE 1');
|
||||
} else if (payloadType === 0) {
|
||||
const bsonSize = this.data.readUInt32LE(this.index);
|
||||
const bin = this.data.slice(this.index, this.index + bsonSize);
|
||||
this.documents.push(raw ? bin : this.bson.deserialize(bin, _options));
|
||||
|
||||
this.index += bsonSize;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.documents.length === 1 && documentsReturnedIn != null && raw) {
|
||||
const fieldsAsRaw = {};
|
||||
fieldsAsRaw[documentsReturnedIn] = true;
|
||||
_options.fieldsAsRaw = fieldsAsRaw;
|
||||
|
||||
const doc = this.bson.deserialize(this.documents[0], _options);
|
||||
this.documents = [doc];
|
||||
}
|
||||
|
||||
this.parsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Msg, BinMsg };
|
||||
+490
-886
File diff suppressed because it is too large
Load Diff
+14
-70
@@ -1,71 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
var f = require('util').format,
|
||||
require_optional = require('require_optional');
|
||||
const require_optional = require('require_optional');
|
||||
|
||||
// Set property function
|
||||
var setProperty = function(obj, prop, flag, values) {
|
||||
Object.defineProperty(obj, prop.name, {
|
||||
enumerable: true,
|
||||
set: function(value) {
|
||||
if (typeof value !== 'boolean') throw new Error(f('%s required a boolean', prop.name));
|
||||
// Flip the bit to 1
|
||||
if (value === true) values.flags |= flag;
|
||||
// Flip the bit to 0 if it's set, otherwise ignore
|
||||
if (value === false && (values.flags & flag) === flag) values.flags ^= flag;
|
||||
prop.value = value;
|
||||
},
|
||||
get: function() {
|
||||
return prop.value;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Set property function
|
||||
var getProperty = function(obj, propName, fieldName, values, func) {
|
||||
Object.defineProperty(obj, propName, {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
// Not parsed yet, parse it
|
||||
if (values[fieldName] == null && obj.isParsed && !obj.isParsed()) {
|
||||
obj.parse();
|
||||
}
|
||||
|
||||
// Do we have a post processing function
|
||||
if (typeof func === 'function') return func(values[fieldName]);
|
||||
// Return raw value
|
||||
return values[fieldName];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Set simple property
|
||||
var getSingleProperty = function(obj, name, value) {
|
||||
Object.defineProperty(obj, name, {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
return value;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Shallow copy
|
||||
var copy = function(fObj, tObj) {
|
||||
tObj = tObj || {};
|
||||
for (var name in fObj) tObj[name] = fObj[name];
|
||||
return tObj;
|
||||
};
|
||||
|
||||
var debugOptions = function(debugFields, options) {
|
||||
function debugOptions(debugFields, options) {
|
||||
var finaloptions = {};
|
||||
debugFields.forEach(function(n) {
|
||||
finaloptions[n] = options[n];
|
||||
});
|
||||
|
||||
return finaloptions;
|
||||
};
|
||||
}
|
||||
|
||||
var retrieveBSON = function() {
|
||||
function retrieveBSON() {
|
||||
var BSON = require('bson');
|
||||
BSON.native = false;
|
||||
|
||||
@@ -78,17 +24,17 @@ var retrieveBSON = function() {
|
||||
} catch (err) {} // eslint-disable-line
|
||||
|
||||
return BSON;
|
||||
};
|
||||
}
|
||||
|
||||
// Throw an error if an attempt to use Snappy is made when Snappy is not installed
|
||||
var noSnappyWarning = function() {
|
||||
function noSnappyWarning() {
|
||||
throw new Error(
|
||||
'Attempted to use Snappy compression, but Snappy is not installed. Install or disable Snappy compression and try again.'
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// Facilitate loading Snappy optionally
|
||||
var retrieveSnappy = function() {
|
||||
function retrieveSnappy() {
|
||||
var snappy = null;
|
||||
try {
|
||||
snappy = require_optional('snappy');
|
||||
@@ -102,12 +48,10 @@ var retrieveSnappy = function() {
|
||||
};
|
||||
}
|
||||
return snappy;
|
||||
};
|
||||
}
|
||||
|
||||
exports.setProperty = setProperty;
|
||||
exports.getProperty = getProperty;
|
||||
exports.getSingleProperty = getSingleProperty;
|
||||
exports.copy = copy;
|
||||
exports.debugOptions = debugOptions;
|
||||
exports.retrieveBSON = retrieveBSON;
|
||||
exports.retrieveSnappy = retrieveSnappy;
|
||||
module.exports = {
|
||||
debugOptions,
|
||||
retrieveBSON,
|
||||
retrieveSnappy
|
||||
};
|
||||
|
||||
+138
-205
@@ -7,9 +7,9 @@ const MongoNetworkError = require('./error').MongoNetworkError;
|
||||
const mongoErrorContextSymbol = require('./error').mongoErrorContextSymbol;
|
||||
const f = require('util').format;
|
||||
const collationNotSupported = require('./utils').collationNotSupported;
|
||||
|
||||
var BSON = retrieveBSON(),
|
||||
Long = BSON.Long;
|
||||
const wireProtocol = require('./wireprotocol');
|
||||
const BSON = retrieveBSON();
|
||||
const Long = BSON.Long;
|
||||
|
||||
/**
|
||||
* This is a cursor results callback
|
||||
@@ -105,7 +105,8 @@ var Cursor = function(bson, ns, cmd, options, topology, topologyOptions) {
|
||||
batchSize: options.batchSize || cmd.batchSize || 1000,
|
||||
currentLimit: 0,
|
||||
// Result field name if not a cursor (contains the array of results)
|
||||
transforms: options.transforms
|
||||
transforms: options.transforms,
|
||||
raw: options.raw || (cmd && cmd.raw)
|
||||
};
|
||||
|
||||
if (typeof options.session === 'object') {
|
||||
@@ -209,129 +210,9 @@ var handleCallback = function(callback, err, result) {
|
||||
};
|
||||
|
||||
// Internal methods
|
||||
Cursor.prototype._find = function(callback) {
|
||||
var self = this;
|
||||
|
||||
if (self.logger.isDebug()) {
|
||||
self.logger.debug(
|
||||
f(
|
||||
'issue initial query [%s] with flags [%s]',
|
||||
JSON.stringify(self.cmd),
|
||||
JSON.stringify(self.query)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
var queryCallback = function(err, r) {
|
||||
if (err) return callback(err);
|
||||
|
||||
// Get the raw message
|
||||
var result = r.message;
|
||||
|
||||
// Query failure bit set
|
||||
if (result.queryFailure) {
|
||||
return callback(new MongoError(result.documents[0]), null);
|
||||
}
|
||||
|
||||
// Check if we have a command cursor
|
||||
if (
|
||||
Array.isArray(result.documents) &&
|
||||
result.documents.length === 1 &&
|
||||
(!self.cmd.find || (self.cmd.find && self.cmd.virtual === false)) &&
|
||||
(typeof result.documents[0].cursor !== 'string' ||
|
||||
result.documents[0]['$err'] ||
|
||||
result.documents[0]['errmsg'] ||
|
||||
Array.isArray(result.documents[0].result))
|
||||
) {
|
||||
// We have a an error document return the error
|
||||
if (result.documents[0]['$err'] || result.documents[0]['errmsg']) {
|
||||
return callback(new MongoError(result.documents[0]), null);
|
||||
}
|
||||
|
||||
// We have a cursor document
|
||||
if (result.documents[0].cursor != null && typeof result.documents[0].cursor !== 'string') {
|
||||
var id = result.documents[0].cursor.id;
|
||||
// If we have a namespace change set the new namespace for getmores
|
||||
if (result.documents[0].cursor.ns) {
|
||||
self.ns = result.documents[0].cursor.ns;
|
||||
}
|
||||
// Promote id to long if needed
|
||||
self.cursorState.cursorId = typeof id === 'number' ? Long.fromNumber(id) : id;
|
||||
self.cursorState.lastCursorId = self.cursorState.cursorId;
|
||||
self.cursorState.operationTime = result.documents[0].operationTime;
|
||||
// If we have a firstBatch set it
|
||||
if (Array.isArray(result.documents[0].cursor.firstBatch)) {
|
||||
self.cursorState.documents = result.documents[0].cursor.firstBatch; //.reverse();
|
||||
}
|
||||
|
||||
// Return after processing command cursor
|
||||
return callback(null, result);
|
||||
}
|
||||
|
||||
if (Array.isArray(result.documents[0].result)) {
|
||||
self.cursorState.documents = result.documents[0].result;
|
||||
self.cursorState.cursorId = Long.ZERO;
|
||||
return callback(null, result);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise fall back to regular find path
|
||||
self.cursorState.cursorId = result.cursorId;
|
||||
self.cursorState.documents = result.documents;
|
||||
self.cursorState.lastCursorId = result.cursorId;
|
||||
|
||||
// Transform the results with passed in transformation method if provided
|
||||
if (self.cursorState.transforms && typeof self.cursorState.transforms.query === 'function') {
|
||||
self.cursorState.documents = self.cursorState.transforms.query(result);
|
||||
}
|
||||
|
||||
// Return callback
|
||||
callback(null, result);
|
||||
};
|
||||
|
||||
// Options passed to the pool
|
||||
var queryOptions = {};
|
||||
|
||||
// If we have a raw query decorate the function
|
||||
if (self.options.raw || self.cmd.raw) {
|
||||
// queryCallback.raw = self.options.raw || self.cmd.raw;
|
||||
queryOptions.raw = self.options.raw || self.cmd.raw;
|
||||
}
|
||||
|
||||
// Do we have documentsReturnedIn set on the query
|
||||
if (typeof self.query.documentsReturnedIn === 'string') {
|
||||
// queryCallback.documentsReturnedIn = self.query.documentsReturnedIn;
|
||||
queryOptions.documentsReturnedIn = self.query.documentsReturnedIn;
|
||||
}
|
||||
|
||||
// Add promote Long value if defined
|
||||
if (typeof self.cursorState.promoteLongs === 'boolean') {
|
||||
queryOptions.promoteLongs = self.cursorState.promoteLongs;
|
||||
}
|
||||
|
||||
// Add promote values if defined
|
||||
if (typeof self.cursorState.promoteValues === 'boolean') {
|
||||
queryOptions.promoteValues = self.cursorState.promoteValues;
|
||||
}
|
||||
|
||||
// Add promote values if defined
|
||||
if (typeof self.cursorState.promoteBuffers === 'boolean') {
|
||||
queryOptions.promoteBuffers = self.cursorState.promoteBuffers;
|
||||
}
|
||||
|
||||
if (typeof self.cursorState.session === 'object') {
|
||||
queryOptions.session = self.cursorState.session;
|
||||
}
|
||||
|
||||
// Write the initial command out
|
||||
self.server.s.pool.write(self.query, queryOptions, queryCallback);
|
||||
};
|
||||
|
||||
Cursor.prototype._getmore = function(callback) {
|
||||
if (this.logger.isDebug())
|
||||
this.logger.debug(f('schedule getMore call for query [%s]', JSON.stringify(this.query)));
|
||||
// Determine if it's a raw query
|
||||
var raw = this.options.raw || this.cmd.raw;
|
||||
|
||||
// Set the current batchSize
|
||||
var batchSize = this.cursorState.batchSize;
|
||||
@@ -342,20 +223,7 @@ Cursor.prototype._getmore = function(callback) {
|
||||
batchSize = this.cursorState.limit - this.cursorState.currentLimit;
|
||||
}
|
||||
|
||||
// Default pool
|
||||
var pool = this.server.s.pool;
|
||||
|
||||
// We have a wire protocol handler
|
||||
this.server.wireProtocolHandler.getMore(
|
||||
this.bson,
|
||||
this.ns,
|
||||
this.cursorState,
|
||||
batchSize,
|
||||
raw,
|
||||
pool,
|
||||
this.options,
|
||||
callback
|
||||
);
|
||||
wireProtocol.getMore(this.server, this.ns, this.cursorState, batchSize, this.options, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -464,10 +332,7 @@ Cursor.prototype.kill = function(callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Default pool
|
||||
var pool = this.server.s.pool;
|
||||
// Execute command
|
||||
this.server.wireProtocolHandler.killCursor(this.bson, this.ns, this.cursorState, pool, callback);
|
||||
wireProtocol.killCursors(this.server, this.ns, this.cursorState, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -591,42 +456,7 @@ var nextFunction = function(self, callback) {
|
||||
return initializeCursor(self, callback);
|
||||
}
|
||||
|
||||
// If we don't have a cursorId execute the first query
|
||||
if (self.cursorState.cursorId == null) {
|
||||
// Check if pool is dead and return if not possible to
|
||||
// execute the query against the db
|
||||
if (isConnectionDead(self, callback)) return;
|
||||
|
||||
// Check if topology is destroyed
|
||||
if (self.topology.isDestroyed())
|
||||
return callback(
|
||||
new MongoNetworkError('connection destroyed, not possible to instantiate cursor')
|
||||
);
|
||||
|
||||
// query, cmd, options, cursorState, callback
|
||||
self._find(function(err) {
|
||||
if (err) return handleCallback(callback, err, null);
|
||||
|
||||
if (self.cursorState.cursorId && self.cursorState.cursorId.isZero() && self._endSession) {
|
||||
self._endSession();
|
||||
}
|
||||
|
||||
if (
|
||||
self.cursorState.documents.length === 0 &&
|
||||
self.cursorState.cursorId &&
|
||||
self.cursorState.cursorId.isZero() &&
|
||||
!self.cmd.tailable &&
|
||||
!self.cmd.awaitData
|
||||
) {
|
||||
return setCursorNotified(self, callback);
|
||||
}
|
||||
|
||||
nextFunction(self, callback);
|
||||
});
|
||||
} else if (
|
||||
self.cursorState.limit > 0 &&
|
||||
self.cursorState.currentLimit >= self.cursorState.limit
|
||||
) {
|
||||
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
|
||||
@@ -776,51 +606,154 @@ function initializeCursor(cursor, callback) {
|
||||
}
|
||||
}
|
||||
|
||||
return cursor.topology.selectServer(cursor.options, (err, server) => {
|
||||
// Very explicitly choose what is passed to selectServer
|
||||
const serverSelectOptions = {};
|
||||
if (cursor.cursorState.session) {
|
||||
serverSelectOptions.session = cursor.cursorState.session;
|
||||
}
|
||||
if (cursor.options.readPreference) {
|
||||
serverSelectOptions.readPreference = cursor.options.readPreference;
|
||||
}
|
||||
|
||||
return cursor.topology.selectServer(serverSelectOptions, (err, server) => {
|
||||
if (err) {
|
||||
// Handle the error and add object to next method call
|
||||
if (cursor.disconnectHandler != null) {
|
||||
return cursor.disconnectHandler.addObjectAndMethod(
|
||||
'cursor',
|
||||
cursor,
|
||||
'next',
|
||||
[callback],
|
||||
callback
|
||||
);
|
||||
const disconnectHandler = cursor.disconnectHandler;
|
||||
if (disconnectHandler != null) {
|
||||
return disconnectHandler.addObjectAndMethod('cursor', cursor, 'next', [callback], callback);
|
||||
}
|
||||
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
cursor.server = server;
|
||||
|
||||
// Set as init
|
||||
cursor.cursorState.init = true;
|
||||
|
||||
// error if collation not supported
|
||||
if (collationNotSupported(cursor.server, cursor.cmd)) {
|
||||
return callback(new MongoError(`server ${cursor.server.name} does not support collation`));
|
||||
}
|
||||
|
||||
try {
|
||||
cursor.query = cursor.server.wireProtocolHandler.command(
|
||||
cursor.bson,
|
||||
function done() {
|
||||
if (
|
||||
cursor.cursorState.cursorId &&
|
||||
cursor.cursorState.cursorId.isZero() &&
|
||||
cursor._endSession
|
||||
) {
|
||||
cursor._endSession();
|
||||
}
|
||||
|
||||
if (
|
||||
cursor.cursorState.documents.length === 0 &&
|
||||
cursor.cursorState.cursorId &&
|
||||
cursor.cursorState.cursorId.isZero() &&
|
||||
!cursor.cmd.tailable &&
|
||||
!cursor.cmd.awaitData
|
||||
) {
|
||||
return setCursorNotified(cursor, callback);
|
||||
}
|
||||
|
||||
nextFunction(cursor, callback);
|
||||
}
|
||||
|
||||
// NOTE: this is a special internal method for cloning a cursor, consider removing
|
||||
if (cursor.cursorState.cursorId != null) {
|
||||
return done();
|
||||
}
|
||||
|
||||
const queryCallback = (err, r) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
const result = r.message;
|
||||
if (result.queryFailure) {
|
||||
return callback(new MongoError(result.documents[0]), null);
|
||||
}
|
||||
|
||||
// Check if we have a command cursor
|
||||
if (
|
||||
Array.isArray(result.documents) &&
|
||||
result.documents.length === 1 &&
|
||||
(!cursor.cmd.find || (cursor.cmd.find && cursor.cmd.virtual === false)) &&
|
||||
(typeof result.documents[0].cursor !== 'string' ||
|
||||
result.documents[0]['$err'] ||
|
||||
result.documents[0]['errmsg'] ||
|
||||
Array.isArray(result.documents[0].result))
|
||||
) {
|
||||
// We have an error document, return the error
|
||||
if (result.documents[0]['$err'] || result.documents[0]['errmsg']) {
|
||||
return callback(new MongoError(result.documents[0]), null);
|
||||
}
|
||||
|
||||
// We have a cursor document
|
||||
if (result.documents[0].cursor != null && typeof result.documents[0].cursor !== 'string') {
|
||||
var id = result.documents[0].cursor.id;
|
||||
// If we have a namespace change set the new namespace for getmores
|
||||
if (result.documents[0].cursor.ns) {
|
||||
cursor.ns = result.documents[0].cursor.ns;
|
||||
}
|
||||
// Promote id to long if needed
|
||||
cursor.cursorState.cursorId = typeof id === 'number' ? Long.fromNumber(id) : id;
|
||||
cursor.cursorState.lastCursorId = cursor.cursorState.cursorId;
|
||||
cursor.cursorState.operationTime = result.documents[0].operationTime;
|
||||
// If we have a firstBatch set it
|
||||
if (Array.isArray(result.documents[0].cursor.firstBatch)) {
|
||||
cursor.cursorState.documents = result.documents[0].cursor.firstBatch; //.reverse();
|
||||
}
|
||||
|
||||
// Return after processing command cursor
|
||||
return done(result);
|
||||
}
|
||||
|
||||
if (Array.isArray(result.documents[0].result)) {
|
||||
cursor.cursorState.documents = result.documents[0].result;
|
||||
cursor.cursorState.cursorId = Long.ZERO;
|
||||
return done(result);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise fall back to regular find path
|
||||
const cursorId = result.cursorId || 0;
|
||||
cursor.cursorState.cursorId = Long.fromNumber(cursorId);
|
||||
cursor.cursorState.documents = result.documents;
|
||||
cursor.cursorState.lastCursorId = result.cursorId;
|
||||
|
||||
// Transform the results with passed in transformation method if provided
|
||||
if (
|
||||
cursor.cursorState.transforms &&
|
||||
typeof cursor.cursorState.transforms.query === 'function'
|
||||
) {
|
||||
cursor.cursorState.documents = cursor.cursorState.transforms.query(result);
|
||||
}
|
||||
|
||||
// Return callback
|
||||
done(result);
|
||||
};
|
||||
|
||||
if (cursor.logger.isDebug()) {
|
||||
cursor.logger.debug(
|
||||
`issue initial query [${JSON.stringify(cursor.cmd)}] with flags [${JSON.stringify(
|
||||
cursor.query
|
||||
)}]`
|
||||
);
|
||||
}
|
||||
|
||||
if (cursor.cmd.find != null) {
|
||||
wireProtocol.query(
|
||||
cursor.server,
|
||||
cursor.ns,
|
||||
cursor.cmd,
|
||||
cursor.cursorState,
|
||||
cursor.topology,
|
||||
cursor.options
|
||||
cursor.options,
|
||||
queryCallback
|
||||
);
|
||||
|
||||
if (cursor.query instanceof MongoError) {
|
||||
return callback(cursor.query);
|
||||
}
|
||||
|
||||
// call `nextFunction` again now that we are initialized
|
||||
nextFunction(cursor, callback);
|
||||
} catch (err) {
|
||||
return callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
cursor.query = wireProtocol.command(
|
||||
cursor.server,
|
||||
cursor.ns,
|
||||
cursor.cmd,
|
||||
cursor.options,
|
||||
queryCallback
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+18
-1
@@ -42,6 +42,10 @@ class MongoError extends Error {
|
||||
static create(options) {
|
||||
return new MongoError(options);
|
||||
}
|
||||
|
||||
hasErrorLabel(label) {
|
||||
return this.errorLabels && this.errorLabels.indexOf(label) !== -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,6 +91,19 @@ class MongoTimeoutError extends MongoError {
|
||||
}
|
||||
}
|
||||
|
||||
function makeWriteConcernResultObject(input) {
|
||||
const output = Object.assign({}, input);
|
||||
|
||||
if (output.ok === 0) {
|
||||
output.ok = 1;
|
||||
delete output.errmsg;
|
||||
delete output.code;
|
||||
delete output.codeName;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* An error thrown when the server reports a writeConcernError
|
||||
*
|
||||
@@ -101,7 +118,7 @@ class MongoWriteConcernError extends MongoError {
|
||||
this.name = 'MongoWriteConcernError';
|
||||
|
||||
if (result != null) {
|
||||
this.result = result;
|
||||
this.result = makeWriteConcernResultObject(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-749
@@ -1,749 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const Logger = require('../connection/logger');
|
||||
const BSON = require('../connection/utils').retrieveBSON();
|
||||
const MongoError = require('../error').MongoError;
|
||||
const MongoNetworkError = require('../error').MongoNetworkError;
|
||||
const mongoErrorContextSymbol = require('../error').mongoErrorContextSymbol;
|
||||
const Long = BSON.Long;
|
||||
const deprecate = require('util').deprecate;
|
||||
const readPreferenceServerSelector = require('./server_selectors').readPreferenceServerSelector;
|
||||
const ReadPreference = require('../topologies/read_preference');
|
||||
|
||||
/**
|
||||
* Handle callback (including any exceptions thrown)
|
||||
*/
|
||||
function handleCallback(callback, err, result) {
|
||||
try {
|
||||
callback(err, result);
|
||||
} catch (err) {
|
||||
process.nextTick(function() {
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a cursor results callback
|
||||
*
|
||||
* @callback resultCallback
|
||||
* @param {error} error An error object. Set to null if no error present
|
||||
* @param {object} document
|
||||
*/
|
||||
|
||||
/**
|
||||
* An internal class that embodies a cursor on MongoDB, allowing for iteration over the
|
||||
* results returned from a query.
|
||||
*
|
||||
* @property {number} cursorBatchSize The current cursorBatchSize for the cursor
|
||||
* @property {number} cursorLimit The current cursorLimit for the cursor
|
||||
* @property {number} cursorSkip The current cursorSkip for the cursor
|
||||
*/
|
||||
class Cursor {
|
||||
/**
|
||||
* Create a cursor
|
||||
*
|
||||
* @param {object} bson An instance of the BSON parser
|
||||
* @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1)
|
||||
* @param {{object}|Long} cmd The selector (can be a command or a cursorId)
|
||||
* @param {object} [options=null] Optional settings.
|
||||
* @param {object} [options.batchSize=1000] Batchsize for the operation
|
||||
* @param {array} [options.documents=[]] Initial documents list for cursor
|
||||
* @param {object} [options.transforms=null] Transform methods for the cursor results
|
||||
* @param {function} [options.transforms.query] Transform the value returned from the initial query
|
||||
* @param {function} [options.transforms.doc] Transform each document returned from Cursor.prototype.next
|
||||
* @param {object} topology The server topology instance.
|
||||
* @param {object} topologyOptions The server topology options.
|
||||
*/
|
||||
constructor(bson, ns, cmd, options, topology, topologyOptions) {
|
||||
options = options || {};
|
||||
|
||||
// Cursor pool
|
||||
this.pool = null;
|
||||
// Cursor server
|
||||
this.server = null;
|
||||
|
||||
// Do we have a not connected handler
|
||||
this.disconnectHandler = options.disconnectHandler;
|
||||
|
||||
// Set local values
|
||||
this.bson = bson;
|
||||
this.ns = ns;
|
||||
this.cmd = cmd;
|
||||
this.options = options;
|
||||
this.topology = topology;
|
||||
|
||||
// All internal state
|
||||
this.s = {
|
||||
cursorId: null,
|
||||
cmd: cmd,
|
||||
documents: options.documents || [],
|
||||
cursorIndex: 0,
|
||||
dead: false,
|
||||
killed: false,
|
||||
init: false,
|
||||
notified: false,
|
||||
limit: options.limit || cmd.limit || 0,
|
||||
skip: options.skip || cmd.skip || 0,
|
||||
batchSize: options.batchSize || cmd.batchSize || 1000,
|
||||
currentLimit: 0,
|
||||
// Result field name if not a cursor (contains the array of results)
|
||||
transforms: options.transforms
|
||||
};
|
||||
|
||||
if (typeof options.session === 'object') {
|
||||
this.s.session = options.session;
|
||||
}
|
||||
|
||||
// Add promoteLong to cursor state
|
||||
if (typeof topologyOptions.promoteLongs === 'boolean') {
|
||||
this.s.promoteLongs = topologyOptions.promoteLongs;
|
||||
} else if (typeof options.promoteLongs === 'boolean') {
|
||||
this.s.promoteLongs = options.promoteLongs;
|
||||
}
|
||||
|
||||
// Add promoteValues to cursor state
|
||||
if (typeof topologyOptions.promoteValues === 'boolean') {
|
||||
this.s.promoteValues = topologyOptions.promoteValues;
|
||||
} else if (typeof options.promoteValues === 'boolean') {
|
||||
this.s.promoteValues = options.promoteValues;
|
||||
}
|
||||
|
||||
// Add promoteBuffers to cursor state
|
||||
if (typeof topologyOptions.promoteBuffers === 'boolean') {
|
||||
this.s.promoteBuffers = topologyOptions.promoteBuffers;
|
||||
} else if (typeof options.promoteBuffers === 'boolean') {
|
||||
this.s.promoteBuffers = options.promoteBuffers;
|
||||
}
|
||||
|
||||
if (topologyOptions.reconnect) {
|
||||
this.s.reconnect = topologyOptions.reconnect;
|
||||
}
|
||||
|
||||
// Logger
|
||||
this.logger = Logger('Cursor', topologyOptions);
|
||||
|
||||
//
|
||||
// Did we pass in a cursor id
|
||||
if (typeof cmd === 'number') {
|
||||
this.s.cursorId = Long.fromNumber(cmd);
|
||||
this.s.lastCursorId = this.s.cursorId;
|
||||
} else if (cmd instanceof Long) {
|
||||
this.s.cursorId = cmd;
|
||||
this.s.lastCursorId = cmd;
|
||||
}
|
||||
}
|
||||
|
||||
setCursorBatchSize(value) {
|
||||
this.s.batchSize = value;
|
||||
}
|
||||
|
||||
cursorBatchSize() {
|
||||
return this.s.batchSize;
|
||||
}
|
||||
|
||||
setCursorLimit(value) {
|
||||
this.s.limit = value;
|
||||
}
|
||||
|
||||
cursorLimit() {
|
||||
return this.s.limit;
|
||||
}
|
||||
|
||||
setCursorSkip(value) {
|
||||
this.s.skip = value;
|
||||
}
|
||||
|
||||
cursorSkip() {
|
||||
return this.s.skip;
|
||||
}
|
||||
|
||||
_endSession(options, callback) {
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
options = options || {};
|
||||
|
||||
const session = this.s.session;
|
||||
if (session && (options.force || session.owner === this)) {
|
||||
this.s.session = undefined;
|
||||
session.endSession(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone the cursor
|
||||
* @method
|
||||
* @return {Cursor}
|
||||
*/
|
||||
clone() {
|
||||
return this.topology.cursor(this.ns, this.cmd, this.options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the cursor is dead
|
||||
* @method
|
||||
* @return {boolean} A boolean signifying if the cursor is dead or not
|
||||
*/
|
||||
isDead() {
|
||||
return this.s.dead === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the cursor was killed by the application
|
||||
* @method
|
||||
* @return {boolean} A boolean signifying if the cursor was killed by the application
|
||||
*/
|
||||
isKilled() {
|
||||
return this.s.killed === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the cursor notified it's caller about it's death
|
||||
* @method
|
||||
* @return {boolean} A boolean signifying if the cursor notified the callback
|
||||
*/
|
||||
isNotified() {
|
||||
return this.s.notified === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current buffered documents length
|
||||
* @method
|
||||
* @return {number} The number of items in the buffered documents
|
||||
*/
|
||||
bufferedCount() {
|
||||
return this.s.documents.length - this.s.cursorIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill the cursor
|
||||
*
|
||||
* @param {resultCallback} callback A callback function
|
||||
*/
|
||||
kill(callback) {
|
||||
// Set cursor to dead
|
||||
this.s.dead = true;
|
||||
this.s.killed = true;
|
||||
// Remove documents
|
||||
this.s.documents = [];
|
||||
|
||||
// If no cursor id just return
|
||||
if (this.s.cursorId == null || this.s.cursorId.isZero() || this.s.init === false) {
|
||||
if (callback) callback(null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Default pool
|
||||
const pool = this.s.server.s.pool;
|
||||
|
||||
// Execute command
|
||||
this.s.server.s.wireProtocolHandler.killCursor(this.bson, this.ns, this.s, pool, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the cursor
|
||||
*/
|
||||
rewind() {
|
||||
if (this.s.init) {
|
||||
if (!this.s.dead) {
|
||||
this.kill();
|
||||
}
|
||||
|
||||
this.s.currentLimit = 0;
|
||||
this.s.init = false;
|
||||
this.s.dead = false;
|
||||
this.s.killed = false;
|
||||
this.s.notified = false;
|
||||
this.s.documents = [];
|
||||
this.s.cursorId = null;
|
||||
this.s.cursorIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current buffered documents
|
||||
* @method
|
||||
* @return {Array} An array of buffered documents
|
||||
*/
|
||||
readBufferedDocuments(number) {
|
||||
const unreadDocumentsLength = this.s.documents.length - this.s.cursorIndex;
|
||||
const length = number < unreadDocumentsLength ? number : unreadDocumentsLength;
|
||||
let elements = this.s.documents.slice(this.s.cursorIndex, this.s.cursorIndex + length);
|
||||
|
||||
// Transform the doc with passed in transformation method if provided
|
||||
if (this.s.transforms && typeof this.s.transforms.doc === 'function') {
|
||||
// Transform all the elements
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
elements[i] = this.s.transforms.doc(elements[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure we do not return any more documents than the limit imposed
|
||||
// Just return the number of elements up to the limit
|
||||
if (this.s.limit > 0 && this.s.currentLimit + elements.length > this.s.limit) {
|
||||
elements = elements.slice(0, this.s.limit - this.s.currentLimit);
|
||||
this.kill();
|
||||
}
|
||||
|
||||
// Adjust current limit
|
||||
this.s.currentLimit = this.s.currentLimit + elements.length;
|
||||
this.s.cursorIndex = this.s.cursorIndex + elements.length;
|
||||
|
||||
// Return elements
|
||||
return elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the next document from the cursor
|
||||
*
|
||||
* @param {resultCallback} callback A callback function
|
||||
*/
|
||||
next(callback) {
|
||||
nextFunction(this, callback);
|
||||
}
|
||||
}
|
||||
|
||||
Cursor.prototype._find = deprecate(
|
||||
callback => _find(this, callback),
|
||||
'_find() is deprecated, please stop using it'
|
||||
);
|
||||
|
||||
Cursor.prototype._getmore = deprecate(
|
||||
callback => _getmore(this, callback),
|
||||
'_getmore() is deprecated, please stop using it'
|
||||
);
|
||||
|
||||
function _getmore(cursor, callback) {
|
||||
if (cursor.logger.isDebug()) {
|
||||
cursor.logger.debug(`schedule getMore call for query [${JSON.stringify(cursor.query)}]`);
|
||||
}
|
||||
|
||||
// Determine if it's a raw query
|
||||
const raw = cursor.options.raw || cursor.cmd.raw;
|
||||
|
||||
// Set the current batchSize
|
||||
let batchSize = cursor.s.batchSize;
|
||||
if (cursor.s.limit > 0 && cursor.s.currentLimit + batchSize > cursor.s.limit) {
|
||||
batchSize = cursor.s.limit - cursor.s.currentLimit;
|
||||
}
|
||||
|
||||
// Default pool
|
||||
const pool = cursor.s.server.s.pool;
|
||||
|
||||
// We have a wire protocol handler
|
||||
cursor.s.server.s.wireProtocolHandler.getMore(
|
||||
cursor.bson,
|
||||
cursor.ns,
|
||||
cursor.s,
|
||||
batchSize,
|
||||
raw,
|
||||
pool,
|
||||
cursor.options,
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
function _find(cursor, callback) {
|
||||
if (cursor.logger.isDebug()) {
|
||||
cursor.logger.debug(
|
||||
`issue initial query [${JSON.stringify(cursor.cmd)}] with flags [${JSON.stringify(
|
||||
cursor.query
|
||||
)}]`
|
||||
);
|
||||
}
|
||||
|
||||
const queryCallback = (err, r) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
// Get the raw message
|
||||
const result = r.message;
|
||||
|
||||
// Query failure bit set
|
||||
if (result.queryFailure) {
|
||||
return callback(new MongoError(result.documents[0]), null);
|
||||
}
|
||||
|
||||
// Check if we have a command cursor
|
||||
if (
|
||||
Array.isArray(result.documents) &&
|
||||
result.documents.length === 1 &&
|
||||
(!cursor.cmd.find || (cursor.cmd.find && cursor.cmd.virtual === false)) &&
|
||||
(result.documents[0].cursor !== 'string' ||
|
||||
result.documents[0]['$err'] ||
|
||||
result.documents[0]['errmsg'] ||
|
||||
Array.isArray(result.documents[0].result))
|
||||
) {
|
||||
// We have a an error document return the error
|
||||
if (result.documents[0]['$err'] || result.documents[0]['errmsg']) {
|
||||
return callback(new MongoError(result.documents[0]), null);
|
||||
}
|
||||
|
||||
// We have a cursor document
|
||||
if (result.documents[0].cursor != null && typeof result.documents[0].cursor !== 'string') {
|
||||
const id = result.documents[0].cursor.id;
|
||||
// If we have a namespace change set the new namespace for getmores
|
||||
if (result.documents[0].cursor.ns) {
|
||||
cursor.ns = result.documents[0].cursor.ns;
|
||||
}
|
||||
// Promote id to long if needed
|
||||
cursor.s.cursorId = typeof id === 'number' ? Long.fromNumber(id) : id;
|
||||
cursor.s.lastCursorId = cursor.s.cursorId;
|
||||
// If we have a firstBatch set it
|
||||
if (Array.isArray(result.documents[0].cursor.firstBatch)) {
|
||||
cursor.s.documents = result.documents[0].cursor.firstBatch;
|
||||
}
|
||||
|
||||
// Return after processing command cursor
|
||||
return callback(null, result);
|
||||
}
|
||||
|
||||
if (Array.isArray(result.documents[0].result)) {
|
||||
cursor.s.documents = result.documents[0].result;
|
||||
cursor.s.cursorId = Long.ZERO;
|
||||
return callback(null, result);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise fall back to regular find path
|
||||
cursor.s.cursorId = result.cursorId;
|
||||
cursor.s.documents = result.documents;
|
||||
cursor.s.lastCursorId = result.cursorId;
|
||||
|
||||
// Transform the results with passed in transformation method if provided
|
||||
if (cursor.s.transforms && typeof cursor.s.transforms.query === 'function') {
|
||||
cursor.s.documents = cursor.s.transforms.query(result);
|
||||
}
|
||||
|
||||
// Return callback
|
||||
callback(null, result);
|
||||
};
|
||||
|
||||
// Options passed to the pool
|
||||
const queryOptions = {};
|
||||
|
||||
// If we have a raw query decorate the function
|
||||
if (cursor.options.raw || cursor.cmd.raw) {
|
||||
queryOptions.raw = cursor.options.raw || cursor.cmd.raw;
|
||||
}
|
||||
|
||||
// Do we have documentsReturnedIn set on the query
|
||||
if (typeof cursor.query.documentsReturnedIn === 'string') {
|
||||
queryOptions.documentsReturnedIn = cursor.query.documentsReturnedIn;
|
||||
}
|
||||
|
||||
// Add promote Long value if defined
|
||||
if (typeof cursor.s.promoteLongs === 'boolean') {
|
||||
queryOptions.promoteLongs = cursor.s.promoteLongs;
|
||||
}
|
||||
|
||||
// Add promote values if defined
|
||||
if (typeof cursor.s.promoteValues === 'boolean') {
|
||||
queryOptions.promoteValues = cursor.s.promoteValues;
|
||||
}
|
||||
|
||||
// Add promote values if defined
|
||||
if (typeof cursor.s.promoteBuffers === 'boolean') {
|
||||
queryOptions.promoteBuffers = cursor.s.promoteBuffers;
|
||||
}
|
||||
|
||||
if (typeof cursor.s.session === 'object') {
|
||||
queryOptions.session = cursor.s.session;
|
||||
}
|
||||
|
||||
// Write the initial command out
|
||||
cursor.s.server.s.pool.write(cursor.query, queryOptions, queryCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if the pool is dead and return error
|
||||
*/
|
||||
function isConnectionDead(cursor, callback) {
|
||||
if (cursor.pool && cursor.pool.isDestroyed()) {
|
||||
cursor.s.killed = true;
|
||||
const err = new MongoNetworkError(
|
||||
`connection to host ${cursor.pool.host}:${cursor.pool.port} was destroyed`
|
||||
);
|
||||
_setCursorNotifiedImpl(cursor, () => callback(err));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if the cursor is dead but was not explicitly killed by user
|
||||
*/
|
||||
function isCursorDeadButNotkilled(cursor, callback) {
|
||||
// Cursor is dead but not marked killed, return null
|
||||
if (cursor.s.dead && !cursor.s.killed) {
|
||||
cursor.s.killed = true;
|
||||
setCursorNotified(cursor, callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if the cursor is dead and was killed by user
|
||||
*/
|
||||
function isCursorDeadAndKilled(cursor, callback) {
|
||||
if (cursor.s.dead && cursor.s.killed) {
|
||||
handleCallback(callback, new MongoError('cursor is dead'));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if the cursor was killed by the user
|
||||
*/
|
||||
function isCursorKilled(cursor, callback) {
|
||||
if (cursor.s.killed) {
|
||||
setCursorNotified(cursor, callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark cursor as being dead and notified
|
||||
*/
|
||||
function setCursorDeadAndNotified(cursor, callback) {
|
||||
cursor.s.dead = true;
|
||||
setCursorNotified(cursor, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark cursor as being notified
|
||||
*/
|
||||
function setCursorNotified(cursor, callback) {
|
||||
_setCursorNotifiedImpl(cursor, () => handleCallback(callback, null, null));
|
||||
}
|
||||
|
||||
function _setCursorNotifiedImpl(cursor, callback) {
|
||||
cursor.s.notified = true;
|
||||
cursor.s.documents = [];
|
||||
cursor.s.cursorIndex = 0;
|
||||
if (cursor._endSession) {
|
||||
return cursor._endSession(undefined, () => callback());
|
||||
}
|
||||
return callback();
|
||||
}
|
||||
|
||||
function initializeCursorAndRetryNext(cursor, callback) {
|
||||
cursor.topology.selectServer(
|
||||
readPreferenceServerSelector(cursor.options.readPreference || ReadPreference.primary),
|
||||
(err, server) => {
|
||||
if (err) {
|
||||
callback(err, null);
|
||||
return;
|
||||
}
|
||||
|
||||
cursor.s.server = server;
|
||||
cursor.s.init = true;
|
||||
|
||||
// check if server supports collation
|
||||
// NOTE: this should be a part of the selection predicate!
|
||||
if (cursor.cmd && cursor.cmd.collation && cursor.server.description.maxWireVersion < 5) {
|
||||
callback(new MongoError(`server ${cursor.server.name} does not support collation`));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
cursor.query = cursor.s.server.s.wireProtocolHandler.command(
|
||||
cursor.bson,
|
||||
cursor.ns,
|
||||
cursor.cmd,
|
||||
cursor.s,
|
||||
cursor.topology,
|
||||
cursor.options
|
||||
);
|
||||
|
||||
nextFunction(cursor, callback);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function nextFunction(cursor, callback) {
|
||||
// We have notified about it
|
||||
if (cursor.s.notified) {
|
||||
return callback(new Error('cursor is exhausted'));
|
||||
}
|
||||
|
||||
// Cursor is killed return null
|
||||
if (isCursorKilled(cursor, callback)) return;
|
||||
|
||||
// Cursor is dead but not marked killed, return null
|
||||
if (isCursorDeadButNotkilled(cursor, callback)) return;
|
||||
|
||||
// We have a dead and killed cursor, attempting to call next should error
|
||||
if (isCursorDeadAndKilled(cursor, callback)) return;
|
||||
|
||||
// We have just started the cursor
|
||||
if (!cursor.s.init) {
|
||||
return initializeCursorAndRetryNext(cursor, callback);
|
||||
}
|
||||
|
||||
// If we don't have a cursorId execute the first query
|
||||
if (cursor.s.cursorId == null) {
|
||||
// Check if pool is dead and return if not possible to
|
||||
// execute the query against the db
|
||||
if (isConnectionDead(cursor, callback)) return;
|
||||
|
||||
// query, cmd, options, s, callback
|
||||
return _find(cursor, function(err) {
|
||||
if (err) return handleCallback(callback, err, null);
|
||||
|
||||
if (cursor.s.cursorId && cursor.s.cursorId.isZero() && cursor._endSession) {
|
||||
cursor._endSession();
|
||||
}
|
||||
|
||||
if (
|
||||
cursor.s.documents.length === 0 &&
|
||||
cursor.s.cursorId &&
|
||||
cursor.s.cursorId.isZero() &&
|
||||
!cursor.cmd.tailable &&
|
||||
!cursor.cmd.awaitData
|
||||
) {
|
||||
return setCursorNotified(cursor, callback);
|
||||
}
|
||||
|
||||
nextFunction(cursor, callback);
|
||||
});
|
||||
}
|
||||
|
||||
if (cursor.s.documents.length === cursor.s.cursorIndex && Long.ZERO.equals(cursor.s.cursorId)) {
|
||||
setCursorDeadAndNotified(cursor, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cursor.s.limit > 0 && cursor.s.currentLimit >= cursor.s.limit) {
|
||||
// Ensure we kill the cursor on the server
|
||||
cursor.kill();
|
||||
// Set cursor in dead and notified state
|
||||
setCursorDeadAndNotified(cursor, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
cursor.s.documents.length === cursor.s.cursorIndex &&
|
||||
cursor.cmd.tailable &&
|
||||
Long.ZERO.equals(cursor.s.cursorId)
|
||||
) {
|
||||
return handleCallback(
|
||||
callback,
|
||||
new MongoError({
|
||||
message: 'No more documents in tailed cursor',
|
||||
tailable: cursor.cmd.tailable,
|
||||
awaitData: cursor.cmd.awaitData
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (cursor.s.cursorIndex === cursor.s.documents.length && !Long.ZERO.equals(cursor.s.cursorId)) {
|
||||
// Ensure an empty cursor state
|
||||
cursor.s.documents = [];
|
||||
cursor.s.cursorIndex = 0;
|
||||
|
||||
// Check if connection is dead and return if not possible to
|
||||
if (isConnectionDead(cursor, callback)) return;
|
||||
|
||||
// Execute the next get more
|
||||
return _getmore(cursor, function(err, doc, connection) {
|
||||
if (err) {
|
||||
if (err instanceof MongoError) {
|
||||
err[mongoErrorContextSymbol].isGetMore = true;
|
||||
}
|
||||
|
||||
return handleCallback(callback, err);
|
||||
}
|
||||
|
||||
if (cursor.s.cursorId && cursor.s.cursorId.isZero() && cursor._endSession) {
|
||||
cursor._endSession();
|
||||
}
|
||||
|
||||
// Save the returned connection to ensure all getMore's fire over the same connection
|
||||
cursor.connection = connection;
|
||||
|
||||
// Tailable cursor getMore result, notify owner about it
|
||||
// No attempt is made here to retry, this is left to the user of the
|
||||
// core module to handle to keep core simple
|
||||
if (
|
||||
cursor.s.documents.length === 0 &&
|
||||
cursor.cmd.tailable &&
|
||||
Long.ZERO.equals(cursor.s.cursorId)
|
||||
) {
|
||||
// No more documents in the tailed cursor
|
||||
return handleCallback(
|
||||
callback,
|
||||
new MongoError({
|
||||
message: 'No more documents in tailed cursor',
|
||||
tailable: cursor.cmd.tailable,
|
||||
awaitData: cursor.cmd.awaitData
|
||||
})
|
||||
);
|
||||
} else if (
|
||||
cursor.s.documents.length === 0 &&
|
||||
cursor.cmd.tailable &&
|
||||
!Long.ZERO.equals(cursor.s.cursorId)
|
||||
) {
|
||||
return nextFunction(cursor, callback);
|
||||
}
|
||||
|
||||
if (cursor.s.limit > 0 && cursor.s.currentLimit >= cursor.s.limit) {
|
||||
return setCursorDeadAndNotified(cursor, callback);
|
||||
}
|
||||
|
||||
nextFunction(cursor, callback);
|
||||
});
|
||||
}
|
||||
|
||||
if (cursor.s.limit > 0 && cursor.s.currentLimit >= cursor.s.limit) {
|
||||
// Ensure we kill the cursor on the server
|
||||
cursor.kill();
|
||||
// Set cursor in dead and notified state
|
||||
return setCursorDeadAndNotified(cursor, callback);
|
||||
}
|
||||
|
||||
// Increment the current cursor limit
|
||||
cursor.s.currentLimit += 1;
|
||||
|
||||
// Get the document
|
||||
let doc = cursor.s.documents[cursor.s.cursorIndex++];
|
||||
|
||||
// Doc overflow
|
||||
if (!doc || doc.$err) {
|
||||
// Ensure we kill the cursor on the server
|
||||
cursor.kill();
|
||||
// Set cursor in dead and notified state
|
||||
return setCursorDeadAndNotified(cursor, function() {
|
||||
handleCallback(callback, new MongoError(doc ? doc.$err : undefined));
|
||||
});
|
||||
}
|
||||
|
||||
// Transform the doc with passed in transformation method if provided
|
||||
if (cursor.s.transforms && typeof cursor.s.transforms.doc === 'function') {
|
||||
doc = cursor.s.transforms.doc(doc);
|
||||
}
|
||||
|
||||
// Return the document
|
||||
handleCallback(callback, null, doc);
|
||||
}
|
||||
|
||||
module.exports = Cursor;
|
||||
+29
-18
@@ -122,7 +122,15 @@ class ServerHeartbeatFailedEvent {
|
||||
*
|
||||
* @param {Server} server The server to monitor
|
||||
*/
|
||||
function monitorServer(server) {
|
||||
function monitorServer(server, options) {
|
||||
options = options || {};
|
||||
const heartbeatFrequencyMS = options.heartbeatFrequencyMS || 10000;
|
||||
|
||||
if (options.initial === true) {
|
||||
server.s.monitorId = setTimeout(() => monitorServer(server), heartbeatFrequencyMS);
|
||||
return;
|
||||
}
|
||||
|
||||
// executes a single check of a server
|
||||
const checkServer = callback => {
|
||||
let start = process.hrtime();
|
||||
@@ -130,6 +138,9 @@ function monitorServer(server) {
|
||||
// emit a signal indicating we have started the heartbeat
|
||||
server.emit('serverHeartbeatStarted', new ServerHeartbeatStartedEvent(server.name));
|
||||
|
||||
// NOTE: legacy monitoring event
|
||||
process.nextTick(() => server.emit('monitoring', server));
|
||||
|
||||
server.command(
|
||||
'admin.$cmd',
|
||||
{ ismaster: true },
|
||||
@@ -137,7 +148,7 @@ function monitorServer(server) {
|
||||
monitoring: true,
|
||||
socketTimeout: server.s.options.connectionTimeout || 2000
|
||||
},
|
||||
function(err, result) {
|
||||
(err, result) => {
|
||||
let duration = calculateDurationInMs(start);
|
||||
|
||||
if (err) {
|
||||
@@ -167,10 +178,7 @@ function monitorServer(server) {
|
||||
server.emit('descriptionReceived', new ServerDescription(server.description.address, isMaster));
|
||||
|
||||
// schedule the next monitoring process
|
||||
server.s.monitorId = setTimeout(
|
||||
() => monitorServer(server),
|
||||
server.s.options.heartbeatFrequencyMS
|
||||
);
|
||||
server.s.monitorId = setTimeout(() => monitorServer(server), heartbeatFrequencyMS);
|
||||
};
|
||||
|
||||
// run the actual monitoring loop
|
||||
@@ -184,21 +192,24 @@ function monitorServer(server) {
|
||||
// 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.
|
||||
server.s.pool.reset(() => {
|
||||
// otherwise re-attempt monitoring once
|
||||
checkServer((error, isMaster) => {
|
||||
if (error) {
|
||||
server.s.monitoring = false;
|
||||
|
||||
// TODO: we need to reset the pool here
|
||||
// we revert to an `Unknown` by emitting a default description with no isMaster
|
||||
server.emit(
|
||||
'descriptionReceived',
|
||||
new ServerDescription(server.description.address, null, { error })
|
||||
);
|
||||
|
||||
return checkServer((err, isMaster) => {
|
||||
if (err) {
|
||||
server.s.monitoring = false;
|
||||
// we do not reschedule monitoring in this case
|
||||
return;
|
||||
}
|
||||
|
||||
// revert to `Unknown` by emitting a default description with no isMaster
|
||||
server.emit('descriptionReceived', new ServerDescription(server.description.address));
|
||||
|
||||
// do not reschedule monitoring in this case
|
||||
return;
|
||||
}
|
||||
|
||||
successHandler(isMaster);
|
||||
successHandler(isMaster);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+148
-169
@@ -3,16 +3,49 @@ const EventEmitter = require('events');
|
||||
const MongoError = require('../error').MongoError;
|
||||
const Pool = require('../connection/pool');
|
||||
const relayEvents = require('../utils').relayEvents;
|
||||
const calculateDurationInMs = require('../utils').calculateDurationInMs;
|
||||
const Query = require('../connection/commands').Query;
|
||||
const TwoSixWireProtocolSupport = require('../wireprotocol/2_6_support');
|
||||
const ThreeTwoWireProtocolSupport = require('../wireprotocol/3_2_support');
|
||||
const wireProtocol = require('../wireprotocol');
|
||||
const BSON = require('../connection/utils').retrieveBSON();
|
||||
const createClientInfo = require('../topologies/shared').createClientInfo;
|
||||
const Logger = require('../connection/logger');
|
||||
const ServerDescription = require('./server_description').ServerDescription;
|
||||
const ReadPreference = require('../topologies/read_preference');
|
||||
const monitorServer = require('./monitoring').monitorServer;
|
||||
const MongoParseError = require('../error').MongoParseError;
|
||||
const MongoNetworkError = require('../error').MongoNetworkError;
|
||||
const collationNotSupported = require('../utils').collationNotSupported;
|
||||
const debugOptions = require('../connection/utils').debugOptions;
|
||||
|
||||
// Used for filtering out fields for logging
|
||||
const DEBUG_FIELDS = [
|
||||
'reconnect',
|
||||
'reconnectTries',
|
||||
'reconnectInterval',
|
||||
'emitError',
|
||||
'cursorFactory',
|
||||
'host',
|
||||
'port',
|
||||
'size',
|
||||
'keepAlive',
|
||||
'keepAliveInitialDelay',
|
||||
'noDelay',
|
||||
'connectionTimeout',
|
||||
'checkServerIdentity',
|
||||
'socketTimeout',
|
||||
'ssl',
|
||||
'ca',
|
||||
'crl',
|
||||
'cert',
|
||||
'key',
|
||||
'rejectUnauthorized',
|
||||
'promoteLongs',
|
||||
'promoteValues',
|
||||
'promoteBuffers',
|
||||
'servername'
|
||||
];
|
||||
|
||||
const STATE_DISCONNECTED = 0;
|
||||
const STATE_CONNECTING = 1;
|
||||
const STATE_CONNECTED = 2;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -27,7 +60,7 @@ class Server extends EventEmitter {
|
||||
* @param {ServerDescription} description
|
||||
* @param {Object} options
|
||||
*/
|
||||
constructor(description, options) {
|
||||
constructor(description, options, topology) {
|
||||
super();
|
||||
|
||||
this.s = {
|
||||
@@ -43,8 +76,14 @@ class Server extends EventEmitter {
|
||||
clientInfo: createClientInfo(options),
|
||||
// state variable to determine if there is an active server check in progress
|
||||
monitoring: false,
|
||||
// the implementation of the monitoring method
|
||||
monitorFunction: options.monitorFunction || monitorServer,
|
||||
// the connection pool
|
||||
pool: null
|
||||
pool: null,
|
||||
// the server state
|
||||
state: STATE_DISCONNECTED,
|
||||
credentials: options.credentials,
|
||||
topology
|
||||
};
|
||||
}
|
||||
|
||||
@@ -58,8 +97,6 @@ class Server extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Initiate server connect
|
||||
*
|
||||
* @param {Array} [options.auth] Array of auth options to apply on connect
|
||||
*/
|
||||
connect(options) {
|
||||
options = options || {};
|
||||
@@ -70,21 +107,35 @@ class Server extends EventEmitter {
|
||||
}
|
||||
|
||||
// create a pool
|
||||
this.s.pool = new Pool(this, Object.assign(this.s.options, options, { bson: this.s.bson }));
|
||||
const addressParts = this.description.address.split(':');
|
||||
const poolOptions = Object.assign(
|
||||
{ host: addressParts[0], port: parseInt(addressParts[1], 10) },
|
||||
this.s.options,
|
||||
options,
|
||||
{ bson: this.s.bson }
|
||||
);
|
||||
|
||||
// Set up listeners
|
||||
// NOTE: this should only be the case if we are connecting to a single server
|
||||
poolOptions.reconnect = true;
|
||||
|
||||
this.s.pool = new Pool(this, poolOptions);
|
||||
|
||||
// setup listeners
|
||||
this.s.pool.on('connect', connectEventHandler(this));
|
||||
this.s.pool.on('close', closeEventHandler(this));
|
||||
this.s.pool.on('close', errorEventHandler(this));
|
||||
this.s.pool.on('error', errorEventHandler(this));
|
||||
this.s.pool.on('parseError', parseErrorEventHandler(this));
|
||||
|
||||
// this.s.pool.on('error', errorEventHandler(this));
|
||||
// it is unclear whether consumers should even know about these events
|
||||
// this.s.pool.on('timeout', timeoutEventHandler(this));
|
||||
// this.s.pool.on('parseError', errorEventHandler(this));
|
||||
// this.s.pool.on('reconnect', reconnectEventHandler(this));
|
||||
// this.s.pool.on('reconnectFailed', errorEventHandler(this));
|
||||
|
||||
// relay all command monitoring events
|
||||
relayEvents(this.s.pool, this, ['commandStarted', 'commandSucceeded', 'commandFailed']);
|
||||
|
||||
this.s.state = STATE_CONNECTING;
|
||||
|
||||
// If auth settings have been provided, use them
|
||||
if (options.auth) {
|
||||
this.s.pool.connect.apply(this.s.pool, options.auth);
|
||||
@@ -97,24 +148,44 @@ class Server extends EventEmitter {
|
||||
/**
|
||||
* Destroy the server connection
|
||||
*
|
||||
* @param {Boolean} [options.emitClose=false] Emit close event on destroy
|
||||
* @param {Boolean} [options.emitDestroy=false] Emit destroy event on destroy
|
||||
* @param {Boolean} [options.force=false] Force destroy the pool
|
||||
*/
|
||||
destroy(callback) {
|
||||
if (typeof callback === 'function') {
|
||||
callback(null, null);
|
||||
destroy(options, callback) {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = Object.assign({}, { force: false }, options);
|
||||
|
||||
if (!this.s.pool) {
|
||||
this.s.state = STATE_DISCONNECTED;
|
||||
if (typeof callback === 'function') {
|
||||
callback(null, null);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
['close', 'error', 'timeout', 'parseError', 'connect'].forEach(event => {
|
||||
this.s.pool.removeAllListeners(event);
|
||||
});
|
||||
|
||||
if (this.s.monitorId) {
|
||||
clearTimeout(this.s.monitorId);
|
||||
}
|
||||
|
||||
this.s.pool.destroy(options.force, err => {
|
||||
this.s.state = STATE_DISCONNECTED;
|
||||
callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately schedule monitoring of this server. If there already an attempt being made
|
||||
* this will be a no-op.
|
||||
*/
|
||||
monitor() {
|
||||
if (this.s.monitoring) return;
|
||||
monitor(options) {
|
||||
options = options || {};
|
||||
if (this.s.state !== STATE_CONNECTED || this.s.monitoring) return;
|
||||
if (this.s.monitorId) clearTimeout(this.s.monitorId);
|
||||
monitorServer(this);
|
||||
this.s.monitorFunction(this, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,39 +217,21 @@ class Server extends EventEmitter {
|
||||
// Debug log
|
||||
if (this.s.logger.isDebug()) {
|
||||
this.s.logger.debug(
|
||||
`executing command [${JSON.stringify({ ns, cmd, options })}] against ${this.name}`
|
||||
`executing command [${JSON.stringify({
|
||||
ns,
|
||||
cmd,
|
||||
options: debugOptions(DEBUG_FIELDS, options)
|
||||
})}] against ${this.name}`
|
||||
);
|
||||
}
|
||||
|
||||
// Check if we have collation support
|
||||
if (this.description.maxWireVersion < 5 && cmd.collation) {
|
||||
// error if collation not supported
|
||||
if (collationNotSupported(this, cmd)) {
|
||||
callback(new MongoError(`server ${this.name} does not support collation`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Are we executing against a specific topology
|
||||
const topology = options.topology || {};
|
||||
// Create the query object
|
||||
const query = this.s.wireProtocolHandler.command(this.s.bson, ns, cmd, {}, topology, options);
|
||||
// Set slave OK of the query
|
||||
query.slaveOk = options.readPreference ? options.readPreference.slaveOk() : false;
|
||||
|
||||
// write options
|
||||
const writeOptions = {
|
||||
raw: typeof options.raw === 'boolean' ? options.raw : false,
|
||||
promoteLongs: typeof options.promoteLongs === 'boolean' ? options.promoteLongs : true,
|
||||
promoteValues: typeof options.promoteValues === 'boolean' ? options.promoteValues : true,
|
||||
promoteBuffers: typeof options.promoteBuffers === 'boolean' ? options.promoteBuffers : false,
|
||||
command: true,
|
||||
monitoring: typeof options.monitoring === 'boolean' ? options.monitoring : false,
|
||||
fullResult: typeof options.fullResult === 'boolean' ? options.fullResult : false,
|
||||
requestId: query.requestId,
|
||||
socketTimeout: typeof options.socketTimeout === 'number' ? options.socketTimeout : null,
|
||||
session: options.session || null
|
||||
};
|
||||
|
||||
// write the operation to the pool
|
||||
this.s.pool.write(query, writeOptions, callback);
|
||||
wireProtocol.command(this, ns, cmd, options, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,6 +283,15 @@ class Server extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(Server.prototype, 'clusterTime', {
|
||||
get: function() {
|
||||
return this.s.topology.clusterTime;
|
||||
},
|
||||
set: function(clusterTime) {
|
||||
this.s.topology.clusterTime = clusterTime;
|
||||
}
|
||||
});
|
||||
|
||||
function basicWriteValidations(server) {
|
||||
if (!server.s.pool) {
|
||||
return new MongoError('server instance is not connected');
|
||||
@@ -269,145 +331,62 @@ function executeWriteOperation(args, options, callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have collation support
|
||||
if (server.description.maxWireVersion < 5 && options.collation) {
|
||||
if (collationNotSupported(server, options)) {
|
||||
callback(new MongoError(`server ${this.name} does not support collation`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute write
|
||||
return server.s.wireProtocolHandler[op](server.s.pool, ns, server.s.bson, ops, options, callback);
|
||||
}
|
||||
|
||||
function saslSupportedMechs(options) {
|
||||
if (!options) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const authArray = options.auth || [];
|
||||
const authMechanism = authArray[0] || options.authMechanism;
|
||||
const authSource = authArray[1] || options.authSource || options.dbName || 'admin';
|
||||
const user = authArray[2] || options.user;
|
||||
|
||||
if (typeof authMechanism === 'string' && authMechanism.toUpperCase() !== 'DEFAULT') {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return { saslSupportedMechs: `${authSource}.${user}` };
|
||||
}
|
||||
|
||||
function extractIsMasterError(err, result) {
|
||||
if (err) return err;
|
||||
if (result && result.result && result.result.ok === 0) {
|
||||
return new MongoError(result.result);
|
||||
}
|
||||
}
|
||||
|
||||
function executeServerHandshake(server, callback) {
|
||||
// construct an `ismaster` query
|
||||
const compressors =
|
||||
server.s.options.compression && server.s.options.compression.compressors
|
||||
? server.s.options.compression.compressors
|
||||
: [];
|
||||
|
||||
const queryOptions = { numberToSkip: 0, numberToReturn: -1, checkKeys: false, slaveOk: true };
|
||||
const query = new Query(
|
||||
server.s.bson,
|
||||
'admin.$cmd',
|
||||
Object.assign(
|
||||
{ ismaster: true, client: server.s.clientInfo, compression: compressors },
|
||||
saslSupportedMechs(server.s.options)
|
||||
),
|
||||
queryOptions
|
||||
);
|
||||
|
||||
// execute the query
|
||||
server.s.pool.write(
|
||||
query,
|
||||
{ socketTimeout: server.s.options.connectionTimeout || 2000 },
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
function configureWireProtocolHandler(ismaster) {
|
||||
// 3.2 wire protocol handler
|
||||
if (ismaster.maxWireVersion >= 4) {
|
||||
return new ThreeTwoWireProtocolSupport();
|
||||
}
|
||||
|
||||
// default to 2.6 wire protocol handler
|
||||
return new TwoSixWireProtocolSupport();
|
||||
return wireProtocol[op](server, ns, ops, options, callback);
|
||||
}
|
||||
|
||||
function connectEventHandler(server) {
|
||||
return function() {
|
||||
// log information of received information if in info mode
|
||||
// if (server.s.logger.isInfo()) {
|
||||
// var object = err instanceof MongoError ? JSON.stringify(err) : {};
|
||||
// server.s.logger.info(`server ${server.name} fired event ${event} out with message ${object}`);
|
||||
// }
|
||||
return function(pool, conn) {
|
||||
const ismaster = conn.ismaster;
|
||||
server.s.lastIsMasterMS = conn.lastIsMasterMS;
|
||||
if (conn.agreedCompressor) {
|
||||
server.s.pool.options.agreedCompressor = conn.agreedCompressor;
|
||||
}
|
||||
|
||||
// begin initial server handshake
|
||||
const start = process.hrtime();
|
||||
executeServerHandshake(server, (err, response) => {
|
||||
// Set initial lastIsMasterMS - is this needed?
|
||||
server.s.lastIsMasterMS = calculateDurationInMs(start);
|
||||
if (conn.zlibCompressionLevel) {
|
||||
server.s.pool.options.zlibCompressionLevel = conn.zlibCompressionLevel;
|
||||
}
|
||||
|
||||
const serverError = extractIsMasterError(err, response);
|
||||
if (serverError) {
|
||||
server.emit('error', serverError);
|
||||
return;
|
||||
}
|
||||
if (conn.ismaster.$clusterTime) {
|
||||
const $clusterTime = conn.ismaster.$clusterTime;
|
||||
server.s.sclusterTime = $clusterTime;
|
||||
}
|
||||
|
||||
// extract the ismaster from the server response
|
||||
const isMaster = response.result;
|
||||
|
||||
// compression negotation
|
||||
if (isMaster && isMaster.compression) {
|
||||
const localCompressionInfo = server.s.options.compression;
|
||||
const localCompressors = localCompressionInfo.compressors;
|
||||
for (var i = 0; i < localCompressors.length; i++) {
|
||||
if (isMaster.compression.indexOf(localCompressors[i]) > -1) {
|
||||
server.s.pool.options.agreedCompressor = localCompressors[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (localCompressionInfo.zlibCompressionLevel) {
|
||||
server.s.pool.options.zlibCompressionLevel = localCompressionInfo.zlibCompressionLevel;
|
||||
}
|
||||
}
|
||||
|
||||
// configure the wire protocol handler
|
||||
server.s.wireProtocolHandler = configureWireProtocolHandler(isMaster);
|
||||
|
||||
// log the connection event if requested
|
||||
if (server.s.logger.isInfo()) {
|
||||
server.s.logger.info(
|
||||
`server ${server.name} connected with ismaster [${JSON.stringify(isMaster)}]`
|
||||
);
|
||||
}
|
||||
|
||||
// emit an event indicating that our description has changed
|
||||
server.emit(
|
||||
'descriptionReceived',
|
||||
new ServerDescription(server.description.address, isMaster)
|
||||
// log the connection event if requested
|
||||
if (server.s.logger.isInfo()) {
|
||||
server.s.logger.info(
|
||||
`server ${server.name} connected with ismaster [${JSON.stringify(ismaster)}]`
|
||||
);
|
||||
}
|
||||
|
||||
// emit a connect event
|
||||
server.emit('connect', isMaster);
|
||||
});
|
||||
// emit an event indicating that our description has changed
|
||||
server.emit('descriptionReceived', new ServerDescription(server.description.address, ismaster));
|
||||
|
||||
// we are connected and handshaked (guaranteed by the pool)
|
||||
server.s.state = STATE_CONNECTED;
|
||||
server.emit('connect', server);
|
||||
};
|
||||
}
|
||||
|
||||
function closeEventHandler(server) {
|
||||
return function() {
|
||||
function errorEventHandler(server) {
|
||||
return function(err) {
|
||||
if (err) {
|
||||
server.emit('error', new MongoNetworkError(err));
|
||||
}
|
||||
|
||||
server.emit('close');
|
||||
};
|
||||
}
|
||||
|
||||
function parseErrorEventHandler(server) {
|
||||
return function(err) {
|
||||
server.s.state = STATE_DISCONNECTED;
|
||||
server.emit('error', new MongoParseError(err));
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Server;
|
||||
|
||||
+10
-2
@@ -22,6 +22,10 @@ const WRITABLE_SERVER_TYPES = new Set([
|
||||
const ISMASTER_FIELDS = [
|
||||
'minWireVersion',
|
||||
'maxWireVersion',
|
||||
'maxBsonObjectSize',
|
||||
'maxMessageSizeBytes',
|
||||
'maxWriteBatchSize',
|
||||
'compression',
|
||||
'me',
|
||||
'hosts',
|
||||
'passives',
|
||||
@@ -31,7 +35,10 @@ const ISMASTER_FIELDS = [
|
||||
'setVersion',
|
||||
'electionId',
|
||||
'primary',
|
||||
'logicalSessionTimeoutMinutes'
|
||||
'logicalSessionTimeoutMinutes',
|
||||
'saslSupportedMechs',
|
||||
'__nodejs_mock_server__',
|
||||
'$clusterTime'
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -62,7 +69,7 @@ class ServerDescription {
|
||||
);
|
||||
|
||||
this.address = address;
|
||||
this.error = null;
|
||||
this.error = options.error || null;
|
||||
this.roundTripTime = options.roundTripTime || 0;
|
||||
this.lastUpdateTime = Date.now();
|
||||
this.lastWriteDate = ismaster.lastWrite ? ismaster.lastWrite.lastWriteDate : null;
|
||||
@@ -75,6 +82,7 @@ class ServerDescription {
|
||||
});
|
||||
|
||||
// normalize case for hosts
|
||||
if (this.me) this.me = this.me.toLowerCase();
|
||||
this.hosts = this.hosts.map(host => host.toLowerCase());
|
||||
this.passives = this.passives.map(host => host.toLowerCase());
|
||||
this.arbiters = this.arbiters.map(host => host.toLowerCase());
|
||||
|
||||
+40
-2
@@ -8,13 +8,24 @@ const MongoError = require('../error').MongoError;
|
||||
const IDLE_WRITE_PERIOD = 10000;
|
||||
const SMALLEST_MAX_STALENESS_SECONDS = 90;
|
||||
|
||||
/**
|
||||
* Returns a server selector that selects for writable servers
|
||||
*/
|
||||
function writableServerSelector() {
|
||||
return function(topologyDescription, servers) {
|
||||
return latencyWindowReducer(topologyDescription, servers.filter(s => s.isWritable));
|
||||
};
|
||||
}
|
||||
|
||||
// reducers
|
||||
/**
|
||||
* Reduces the passed in array of servers by the rules of the "Max Staleness" specification
|
||||
* found here: https://github.com/mongodb/specifications/blob/master/source/max-staleness/max-staleness.rst
|
||||
*
|
||||
* @param {ReadPreference} readPreference The read preference providing max staleness guidance
|
||||
* @param {topologyDescription} topologyDescription The topology description
|
||||
* @param {ServerDescription[]} servers The list of server descriptions to be reduced
|
||||
* @return {ServerDescription[]} The list of servers that satisfy the requirements of max staleness
|
||||
*/
|
||||
function maxStalenessReducer(readPreference, topologyDescription, servers) {
|
||||
if (readPreference.maxStalenessSeconds == null || readPreference.maxStalenessSeconds < 0) {
|
||||
return servers;
|
||||
@@ -24,7 +35,7 @@ function maxStalenessReducer(readPreference, topologyDescription, servers) {
|
||||
const maxStalenessVariance =
|
||||
(topologyDescription.heartbeatFrequencyMS + IDLE_WRITE_PERIOD) / 1000;
|
||||
if (maxStaleness < maxStalenessVariance) {
|
||||
throw MongoError(`maxStalenessSeconds must be at least ${maxStalenessVariance} seconds`);
|
||||
throw new MongoError(`maxStalenessSeconds must be at least ${maxStalenessVariance} seconds`);
|
||||
}
|
||||
|
||||
if (maxStaleness < SMALLEST_MAX_STALENESS_SECONDS) {
|
||||
@@ -61,6 +72,12 @@ function maxStalenessReducer(readPreference, topologyDescription, servers) {
|
||||
return servers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a server's tags match a given set of tags
|
||||
*
|
||||
* @param {String[]} tagSet The requested tag set to match
|
||||
* @param {String[]} serverTags The server's tags
|
||||
*/
|
||||
function tagSetMatch(tagSet, serverTags) {
|
||||
const keys = Object.keys(tagSet);
|
||||
const serverTagKeys = Object.keys(serverTags);
|
||||
@@ -74,6 +91,13 @@ function tagSetMatch(tagSet, serverTags) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces a set of server descriptions based on tags requested by the read preference
|
||||
*
|
||||
* @param {ReadPreference} readPreference The read preference providing the requested tags
|
||||
* @param {ServerDescription[]} servers The list of server descriptions to reduce
|
||||
* @return {ServerDescription[]} The list of servers matching the requested tags
|
||||
*/
|
||||
function tagSetReducer(readPreference, servers) {
|
||||
if (
|
||||
readPreference.tags == null ||
|
||||
@@ -97,6 +121,15 @@ function tagSetReducer(readPreference, servers) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces a list of servers to ensure they fall within an acceptable latency window. This is
|
||||
* further specified in the "Server Selection" specification, found here:
|
||||
* https://github.com/mongodb/specifications/blob/master/source/server-selection/server-selection.rst
|
||||
*
|
||||
* @param {topologyDescription} topologyDescription The topology description
|
||||
* @param {ServerDescription[]} servers The list of servers to reduce
|
||||
* @returns {ServerDescription[]} The servers which fall within an acceptable latency window
|
||||
*/
|
||||
function latencyWindowReducer(topologyDescription, servers) {
|
||||
const low = servers.reduce(
|
||||
(min, server) => (min === -1 ? server.roundTripTime : Math.min(server.roundTripTime, min)),
|
||||
@@ -128,6 +161,11 @@ function knownFilter(server) {
|
||||
return server.type !== ServerType.Unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function which selects servers based on a provided read preference
|
||||
*
|
||||
* @param {ReadPreference} readPreference The read preference to select with
|
||||
*/
|
||||
function readPreferenceServerSelector(readPreference) {
|
||||
if (!readPreference.isValid()) {
|
||||
throw new TypeError('Invalid read preference specified');
|
||||
|
||||
+523
-135
@@ -1,22 +1,28 @@
|
||||
'use strict';
|
||||
const EventEmitter = require('events');
|
||||
const ServerDescription = require('./server_description').ServerDescription;
|
||||
const ServerType = require('./server_description').ServerType;
|
||||
const TopologyDescription = require('./topology_description').TopologyDescription;
|
||||
const TopologyType = require('./topology_description').TopologyType;
|
||||
const monitoring = require('./monitoring');
|
||||
const calculateDurationInMs = require('../utils').calculateDurationInMs;
|
||||
const MongoTimeoutError = require('../error').MongoTimeoutError;
|
||||
const MongoNetworkError = require('../error').MongoNetworkError;
|
||||
const Server = require('./server');
|
||||
const relayEvents = require('../utils').relayEvents;
|
||||
const ReadPreference = require('../topologies/read_preference');
|
||||
const readPreferenceServerSelector = require('./server_selectors').readPreferenceServerSelector;
|
||||
const writableServerSelector = require('./server_selectors').writableServerSelector;
|
||||
const isRetryableWritesSupported = require('../topologies/shared').isRetryableWritesSupported;
|
||||
const Cursor = require('./cursor');
|
||||
const Cursor = require('../cursor');
|
||||
const deprecate = require('util').deprecate;
|
||||
const BSON = require('../connection/utils').retrieveBSON();
|
||||
const createCompressionInfo = require('../topologies/shared').createCompressionInfo;
|
||||
const isRetryableError = require('../error').isRetryableError;
|
||||
const MongoParseError = require('../error').MongoParseError;
|
||||
const ClientSession = require('../sessions').ClientSession;
|
||||
const createClientInfo = require('../topologies/shared').createClientInfo;
|
||||
const MongoError = require('../error').MongoError;
|
||||
const resolveClusterTime = require('../topologies/shared').resolveClusterTime;
|
||||
|
||||
// Global state
|
||||
let globalTopologyCounter = 0;
|
||||
@@ -26,9 +32,31 @@ const TOPOLOGY_DEFAULTS = {
|
||||
localThresholdMS: 15,
|
||||
serverSelectionTimeoutMS: 10000,
|
||||
heartbeatFrequencyMS: 30000,
|
||||
minHeartbeatIntervalMS: 500
|
||||
minHeartbeatFrequencyMS: 500
|
||||
};
|
||||
|
||||
// events that we relay to the `Topology`
|
||||
const SERVER_RELAY_EVENTS = [
|
||||
'serverHeartbeatStarted',
|
||||
'serverHeartbeatSucceeded',
|
||||
'serverHeartbeatFailed',
|
||||
'commandStarted',
|
||||
'commandSucceeded',
|
||||
'commandFailed',
|
||||
|
||||
// NOTE: Legacy events
|
||||
'monitoring'
|
||||
];
|
||||
|
||||
// all events we listen to from `Server` instances
|
||||
const LOCAL_SERVER_EVENTS = SERVER_RELAY_EVENTS.concat([
|
||||
'error',
|
||||
'connect',
|
||||
'descriptionReceived',
|
||||
'close',
|
||||
'ended'
|
||||
]);
|
||||
|
||||
/**
|
||||
* A container of server instances representing a connection to a MongoDB topology.
|
||||
*
|
||||
@@ -54,7 +82,7 @@ class Topology extends EventEmitter {
|
||||
*/
|
||||
constructor(seedlist, options) {
|
||||
super();
|
||||
if (typeof options === 'undefined') {
|
||||
if (typeof options === 'undefined' && typeof seedlist !== 'string') {
|
||||
options = seedlist;
|
||||
seedlist = [];
|
||||
|
||||
@@ -65,11 +93,16 @@ class Topology extends EventEmitter {
|
||||
}
|
||||
|
||||
seedlist = seedlist || [];
|
||||
if (typeof seedlist === 'string') {
|
||||
seedlist = parseStringSeedlist(seedlist);
|
||||
}
|
||||
|
||||
options = Object.assign({}, TOPOLOGY_DEFAULTS, options);
|
||||
|
||||
const topologyType = topologyTypeFromSeedlist(seedlist, options);
|
||||
const topologyId = globalTopologyCounter++;
|
||||
const serverDescriptions = seedlist.reduce((result, seed) => {
|
||||
if (seed.domain_socket) seed.host = seed.domain_socket;
|
||||
const address = seed.port ? `${seed.host}:${seed.port}` : `${seed.host}:27017`;
|
||||
result.set(address, new ServerDescription(address));
|
||||
return result;
|
||||
@@ -79,7 +112,7 @@ class Topology extends EventEmitter {
|
||||
// the id of this topology
|
||||
id: topologyId,
|
||||
// passed in options
|
||||
options: Object.assign({}, options),
|
||||
options,
|
||||
// initial seedlist of servers to connect to
|
||||
seedlist: seedlist,
|
||||
// the topology description
|
||||
@@ -89,6 +122,7 @@ class Topology extends EventEmitter {
|
||||
options.replicaSet,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
options
|
||||
),
|
||||
serverSelectionTimeoutMS: options.serverSelectionTimeoutMS,
|
||||
@@ -97,28 +131,24 @@ class Topology extends EventEmitter {
|
||||
// allow users to override the cursor factory
|
||||
Cursor: options.cursorFactory || Cursor,
|
||||
// the bson parser
|
||||
bson:
|
||||
options.bson ||
|
||||
new BSON([
|
||||
BSON.Binary,
|
||||
BSON.Code,
|
||||
BSON.DBRef,
|
||||
BSON.Decimal128,
|
||||
BSON.Double,
|
||||
BSON.Int32,
|
||||
BSON.Long,
|
||||
BSON.Map,
|
||||
BSON.MaxKey,
|
||||
BSON.MinKey,
|
||||
BSON.ObjectId,
|
||||
BSON.BSONRegExp,
|
||||
BSON.Symbol,
|
||||
BSON.Timestamp
|
||||
])
|
||||
bson: options.bson || new BSON(),
|
||||
// a map of server instances to normalized addresses
|
||||
servers: new Map(),
|
||||
// Server Session Pool
|
||||
sessionPool: null,
|
||||
// Active client sessions
|
||||
sessions: [],
|
||||
// Promise library
|
||||
promiseLibrary: options.promiseLibrary || Promise,
|
||||
credentials: options.credentials,
|
||||
clusterTime: null
|
||||
};
|
||||
|
||||
// amend options for server instance creation
|
||||
this.s.options.compression = { compressors: createCompressionInfo(options) };
|
||||
|
||||
// add client info
|
||||
this.s.clientInfo = createClientInfo(options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,6 +158,10 @@ class Topology extends EventEmitter {
|
||||
return this.s.description;
|
||||
}
|
||||
|
||||
get parserType() {
|
||||
return BSON.native ? 'c++' : 'js';
|
||||
}
|
||||
|
||||
/**
|
||||
* All raw connections
|
||||
* @method
|
||||
@@ -144,8 +178,12 @@ class Topology extends EventEmitter {
|
||||
*
|
||||
* @param {Object} [options] Optional settings
|
||||
* @param {Array} [options.auth=null] Array of auth options to apply on connect
|
||||
* @param {function} [callback] An optional callback called once on the first connected server
|
||||
*/
|
||||
connect(/* options */) {
|
||||
connect(options, callback) {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = options || {};
|
||||
|
||||
// emit SDAM monitoring events
|
||||
this.emit('topologyOpening', new monitoring.TopologyOpeningEvent(this.s.id));
|
||||
|
||||
@@ -161,39 +199,126 @@ class Topology extends EventEmitter {
|
||||
|
||||
connectServers(this, Array.from(this.s.description.servers.values()));
|
||||
this.s.connected = true;
|
||||
|
||||
// otherwise, wait for a server to properly connect based on user provided read preference,
|
||||
// or primary.
|
||||
|
||||
translateReadPreference(options);
|
||||
const readPreference = options.readPreference || ReadPreference.primary;
|
||||
|
||||
this.selectServer(readPreferenceServerSelector(readPreference), options, (err, server) => {
|
||||
if (err) {
|
||||
if (typeof callback === 'function') {
|
||||
callback(err, null);
|
||||
} else {
|
||||
this.emit('error', err);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const errorHandler = err => {
|
||||
server.removeListener('connect', connectHandler);
|
||||
if (typeof callback === 'function') callback(err, null);
|
||||
};
|
||||
|
||||
const connectHandler = (_, err) => {
|
||||
server.removeListener('error', errorHandler);
|
||||
this.emit('open', err, this);
|
||||
this.emit('connect', this);
|
||||
|
||||
if (typeof callback === 'function') callback(err, this);
|
||||
};
|
||||
|
||||
const STATE_CONNECTING = 1;
|
||||
if (server.s.state === STATE_CONNECTING) {
|
||||
server.once('error', errorHandler);
|
||||
server.once('connect', connectHandler);
|
||||
return;
|
||||
}
|
||||
|
||||
connectHandler();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Close this topology
|
||||
*/
|
||||
close(callback) {
|
||||
// destroy all child servers
|
||||
this.s.servers.forEach(server => server.destroy());
|
||||
close(options, callback) {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = options || {};
|
||||
|
||||
// emit an event for close
|
||||
this.emit('topologyClosed', new monitoring.TopologyClosedEvent(this.s.id));
|
||||
|
||||
this.s.connected = false;
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback(null, null);
|
||||
if (this.s.sessionPool) {
|
||||
this.s.sessions.forEach(session => session.endSession());
|
||||
this.s.sessionPool.endAllPooledSessions();
|
||||
}
|
||||
|
||||
const servers = this.s.servers;
|
||||
if (servers.size === 0) {
|
||||
this.s.connected = false;
|
||||
if (typeof callback === 'function') {
|
||||
callback(null, null);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// destroy all child servers
|
||||
let destroyed = 0;
|
||||
servers.forEach(server =>
|
||||
destroyServer(server, this, () => {
|
||||
destroyed++;
|
||||
if (destroyed === servers.size) {
|
||||
// emit an event for close
|
||||
this.emit('topologyClosed', new monitoring.TopologyClosedEvent(this.s.id));
|
||||
|
||||
this.s.connected = false;
|
||||
if (typeof callback === 'function') {
|
||||
callback(null, null);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a server according to the selection predicate provided
|
||||
*
|
||||
* @param {function} [selector] An optional selector to select servers by, defaults to a random selection within a latency window
|
||||
* @param {object} [options] Optional settings related to server selection
|
||||
* @param {number} [options.serverSelectionTimeoutMS] How long to block for server selection before throwing an error
|
||||
* @param {function} callback The callback used to indicate success or failure
|
||||
* @return {Server} An instance of a `Server` meeting the criteria of the predicate provided
|
||||
*/
|
||||
selectServer(selector, options, callback) {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
if (typeof selector !== 'function') {
|
||||
options = selector;
|
||||
|
||||
translateReadPreference(options);
|
||||
const readPreference = options.readPreference || ReadPreference.primary;
|
||||
selector = readPreferenceServerSelector(readPreference);
|
||||
} else {
|
||||
options = {};
|
||||
}
|
||||
}
|
||||
|
||||
options = Object.assign(
|
||||
{},
|
||||
{ serverSelectionTimeoutMS: this.s.serverSelectionTimeoutMS },
|
||||
options
|
||||
);
|
||||
|
||||
const isSharded = this.description.type === TopologyType.Sharded;
|
||||
const session = options.session;
|
||||
const transaction = session && session.transaction;
|
||||
|
||||
if (isSharded && transaction && transaction.server) {
|
||||
callback(null, transaction.server);
|
||||
return;
|
||||
}
|
||||
|
||||
selectServers(
|
||||
this,
|
||||
selector,
|
||||
@@ -201,7 +326,56 @@ class Topology extends EventEmitter {
|
||||
process.hrtime(),
|
||||
(err, servers) => {
|
||||
if (err) return callback(err, null);
|
||||
callback(null, randomSelection(servers));
|
||||
|
||||
const selectedServer = randomSelection(servers);
|
||||
if (isSharded && transaction && transaction.isActive) {
|
||||
transaction.pinServer(selectedServer);
|
||||
}
|
||||
|
||||
callback(null, selectedServer);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Sessions related methods
|
||||
/**
|
||||
* @return Whether sessions are supported on the current topology
|
||||
*/
|
||||
hasSessionSupport() {
|
||||
return this.description.logicalSessionTimeoutMinutes != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a logical session
|
||||
*/
|
||||
startSession(options, clientOptions) {
|
||||
const session = new ClientSession(this, this.s.sessionPool, options, clientOptions);
|
||||
session.once('ended', () => {
|
||||
this.s.sessions = this.s.sessions.filter(s => !s.equals(session));
|
||||
});
|
||||
|
||||
this.s.sessions.push(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send endSessions command(s) with the given session ids
|
||||
*
|
||||
* @param {Array} sessions The sessions to end
|
||||
* @param {function} [callback]
|
||||
*/
|
||||
endSessions(sessions, callback) {
|
||||
if (!Array.isArray(sessions)) {
|
||||
sessions = [sessions];
|
||||
}
|
||||
|
||||
this.command(
|
||||
'admin.$cmd',
|
||||
{ endSessions: sessions },
|
||||
{ readPreference: ReadPreference.primaryPreferred, noResponse: true },
|
||||
() => {
|
||||
// intentionally ignored, per spec
|
||||
if (typeof callback === 'function') callback();
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -222,6 +396,10 @@ class Topology extends EventEmitter {
|
||||
|
||||
// first update the TopologyDescription
|
||||
this.s.description = this.s.description.update(serverDescription);
|
||||
if (this.s.description.compatibilityError) {
|
||||
this.emit('error', new MongoError(this.s.description.compatibilityError));
|
||||
return;
|
||||
}
|
||||
|
||||
// emit monitoring events for this change
|
||||
this.emit(
|
||||
@@ -237,6 +415,17 @@ class Topology extends EventEmitter {
|
||||
// update server list from updated descriptions
|
||||
updateServers(this, serverDescription);
|
||||
|
||||
// Driver Sessions Spec: "Whenever a driver receives a cluster time from
|
||||
// a server it MUST compare it to the current highest seen cluster time
|
||||
// for the deployment. If the new cluster time is higher than the
|
||||
// highest seen cluster time it MUST become the new highest seen cluster
|
||||
// time. Two cluster times are compared using only the BsonTimestamp
|
||||
// value of the clusterTime embedded field."
|
||||
const clusterTime = serverDescription.$clusterTime;
|
||||
if (clusterTime) {
|
||||
resolveClusterTime(this, clusterTime);
|
||||
}
|
||||
|
||||
this.emit(
|
||||
'topologyDescriptionChanged',
|
||||
new monitoring.TopologyDescriptionChangedEvent(
|
||||
@@ -247,26 +436,13 @@ class Topology extends EventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate using a specified mechanism
|
||||
*
|
||||
* @param {String} mechanism The auth mechanism used for authentication
|
||||
* @param {String} db The db we are authenticating against
|
||||
* @param {Object} options Optional settings for the authenticating mechanism
|
||||
* @param {authResultCallback} callback A callback function
|
||||
*/
|
||||
auth(mechanism, db, options, callback) {
|
||||
callback(null, null);
|
||||
auth(credentials, callback) {
|
||||
if (typeof credentials === 'function') (callback = credentials), (credentials = null);
|
||||
if (typeof callback === 'function') callback(null, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout from a database
|
||||
*
|
||||
* @param {String} db The db we are logging out from
|
||||
* @param {authResultCallback} callback A callback function
|
||||
*/
|
||||
logout(db, callback) {
|
||||
callback(null, null);
|
||||
logout(callback) {
|
||||
if (typeof callback === 'function') callback(null, true);
|
||||
}
|
||||
|
||||
// Basic operation support. Eventually this should be moved into command construction
|
||||
@@ -341,14 +517,44 @@ class Topology extends EventEmitter {
|
||||
(callback = options), (options = {}), (options = options || {});
|
||||
}
|
||||
|
||||
const readPreference = options.readPreference ? options.readPreference : ReadPreference.primary;
|
||||
this.selectServer(readPreferenceServerSelector(readPreference), (err, server) => {
|
||||
translateReadPreference(options);
|
||||
const readPreference = options.readPreference || ReadPreference.primary;
|
||||
|
||||
this.selectServer(readPreferenceServerSelector(readPreference), options, (err, server) => {
|
||||
if (err) {
|
||||
callback(err, null);
|
||||
return;
|
||||
}
|
||||
|
||||
server.command(ns, cmd, options, callback);
|
||||
const willRetryWrite =
|
||||
!options.retrying &&
|
||||
!!options.retryWrites &&
|
||||
options.session &&
|
||||
isRetryableWritesSupported(this) &&
|
||||
!options.session.inTransaction() &&
|
||||
isWriteCommand(cmd);
|
||||
|
||||
const cb = (err, result) => {
|
||||
if (!err) return callback(null, result);
|
||||
if (!isRetryableError(err)) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (willRetryWrite) {
|
||||
const newOptions = Object.assign({}, options, { retrying: true });
|
||||
return this.command(ns, cmd, newOptions, callback);
|
||||
}
|
||||
|
||||
return callback(err);
|
||||
};
|
||||
|
||||
// increment and assign txnNumber
|
||||
if (willRetryWrite) {
|
||||
options.session.incrementTransactionNumber();
|
||||
options.willRetryWrite = willRetryWrite;
|
||||
}
|
||||
|
||||
server.command(ns, cmd, options, cb);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -372,20 +578,106 @@ class Topology extends EventEmitter {
|
||||
options = options || {};
|
||||
const topology = options.topology || this;
|
||||
const CursorClass = options.cursorFactory || this.s.Cursor;
|
||||
translateReadPreference(options);
|
||||
|
||||
return new CursorClass(this.s.bson, ns, cmd, options, topology, this.s.options);
|
||||
}
|
||||
|
||||
get clientInfo() {
|
||||
return this.s.clientInfo;
|
||||
}
|
||||
|
||||
// Legacy methods for compat with old topology types
|
||||
isConnected() {
|
||||
// console.log('not implemented: `isConnected`');
|
||||
return true;
|
||||
}
|
||||
|
||||
isDestroyed() {
|
||||
// console.log('not implemented: `isDestroyed`');
|
||||
return false;
|
||||
}
|
||||
|
||||
unref() {
|
||||
console.log('not implemented: `unref`');
|
||||
}
|
||||
|
||||
// NOTE: There are many places in code where we explicitly check the last isMaster
|
||||
// to do feature support detection. This should be done any other way, but for
|
||||
// now we will just return the first isMaster seen, which should suffice.
|
||||
lastIsMaster() {
|
||||
const serverDescriptions = Array.from(this.description.servers.values());
|
||||
if (serverDescriptions.length === 0) return {};
|
||||
|
||||
const sd = serverDescriptions.filter(sd => sd.type !== ServerType.Unknown)[0];
|
||||
const result = sd || { maxWireVersion: this.description.commonWireVersion };
|
||||
return result;
|
||||
}
|
||||
|
||||
get logicalSessionTimeoutMinutes() {
|
||||
return this.description.logicalSessionTimeoutMinutes;
|
||||
}
|
||||
|
||||
get bson() {
|
||||
return this.s.bson;
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(Topology.prototype, 'clusterTime', {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
return this.s.clusterTime;
|
||||
},
|
||||
set: function(clusterTime) {
|
||||
this.s.clusterTime = clusterTime;
|
||||
}
|
||||
});
|
||||
|
||||
// legacy aliases
|
||||
Topology.prototype.destroy = deprecate(
|
||||
Topology.prototype.close,
|
||||
'destroy() is deprecated, please use close() instead'
|
||||
);
|
||||
|
||||
const RETRYABLE_WRITE_OPERATIONS = ['findAndModify', 'insert', 'update', 'delete'];
|
||||
function isWriteCommand(command) {
|
||||
return RETRYABLE_WRITE_OPERATIONS.some(op => command[op]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys a server, and removes all event listeners from the instance
|
||||
*
|
||||
* @param {Server} server
|
||||
*/
|
||||
function destroyServer(server, topology, callback) {
|
||||
LOCAL_SERVER_EVENTS.forEach(event => server.removeAllListeners(event));
|
||||
|
||||
server.destroy(() => {
|
||||
topology.emit(
|
||||
'serverClosed',
|
||||
new monitoring.ServerClosedEvent(topology.s.id, server.description.address)
|
||||
);
|
||||
|
||||
if (typeof callback === 'function') callback(null, null);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a basic seedlist in string form
|
||||
*
|
||||
* @param {string} seedlist The seedlist to parse
|
||||
*/
|
||||
function parseStringSeedlist(seedlist) {
|
||||
return seedlist.split(',').map(seed => ({
|
||||
host: seed.split(':')[0],
|
||||
port: seed.split(':')[1] || 27017
|
||||
}));
|
||||
}
|
||||
|
||||
function topologyTypeFromSeedlist(seedlist, options) {
|
||||
if (seedlist.length === 1 && !options.replicaSet) return TopologyType.Single;
|
||||
if (options.replicaSet) return TopologyType.ReplicaSetNoPrimary;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -404,9 +696,43 @@ function randomSelection(array) {
|
||||
* @param {function} callback The callback used to convey errors or the resultant servers
|
||||
*/
|
||||
function selectServers(topology, selector, timeout, start, callback) {
|
||||
const duration = calculateDurationInMs(start);
|
||||
if (duration >= timeout) {
|
||||
return callback(new MongoTimeoutError(`Server selection timed out after ${timeout} ms`));
|
||||
}
|
||||
|
||||
// ensure we are connected
|
||||
if (!topology.s.connected) {
|
||||
topology.connect();
|
||||
|
||||
// we want to make sure we're still within the requested timeout window
|
||||
const failToConnectTimer = setTimeout(() => {
|
||||
topology.removeListener('connect', connectHandler);
|
||||
callback(new MongoTimeoutError('Server selection timed out waiting to connect'));
|
||||
}, timeout - duration);
|
||||
|
||||
const connectHandler = () => {
|
||||
clearTimeout(failToConnectTimer);
|
||||
selectServers(topology, selector, timeout, process.hrtime(), callback);
|
||||
};
|
||||
|
||||
topology.once('connect', connectHandler);
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise, attempt server selection
|
||||
const serverDescriptions = Array.from(topology.description.servers.values());
|
||||
let descriptions;
|
||||
|
||||
// support server selection by options with readPreference
|
||||
if (typeof selector === 'object') {
|
||||
const readPreference = selector.readPreference
|
||||
? selector.readPreference
|
||||
: ReadPreference.primary;
|
||||
|
||||
selector = readPreferenceServerSelector(readPreference);
|
||||
}
|
||||
|
||||
try {
|
||||
descriptions = selector
|
||||
? selector(topology.description, serverDescriptions)
|
||||
@@ -420,48 +746,56 @@ function selectServers(topology, selector, timeout, start, callback) {
|
||||
return callback(null, servers);
|
||||
}
|
||||
|
||||
const duration = calculateDurationInMs(start);
|
||||
if (duration >= timeout) {
|
||||
return callback(new MongoTimeoutError(`Server selection timed out after ${timeout} ms`));
|
||||
}
|
||||
|
||||
const retrySelection = () => {
|
||||
// ensure all server monitors attempt monitoring immediately
|
||||
topology.s.servers.forEach(server => server.monitor());
|
||||
// ensure all server monitors attempt monitoring soon
|
||||
topology.s.servers.forEach(server => {
|
||||
setTimeout(
|
||||
() => server.monitor({ heartbeatFrequencyMS: topology.description.heartbeatFrequencyMS }),
|
||||
TOPOLOGY_DEFAULTS.minHeartbeatFrequencyMS
|
||||
);
|
||||
});
|
||||
|
||||
const iterationTimer = setTimeout(() => {
|
||||
callback(new MongoTimeoutError('Server selection timed out due to monitoring'));
|
||||
}, topology.s.minHeartbeatIntervalMS);
|
||||
|
||||
topology.once('topologyDescriptionChanged', () => {
|
||||
const descriptionChangedHandler = () => {
|
||||
// successful iteration, clear the check timer
|
||||
clearTimeout(iterationTimer);
|
||||
|
||||
if (topology.description.error) {
|
||||
callback(topology.description.error, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// topology description has changed due to monitoring, reattempt server selection
|
||||
selectServers(topology, selector, timeout, start, callback);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// ensure we are connected
|
||||
if (!topology.s.connected) {
|
||||
topology.connect();
|
||||
|
||||
// we want to make sure we're still within the requested timeout window
|
||||
const failToConnectTimer = setTimeout(() => {
|
||||
callback(new MongoTimeoutError('Server selection timed out waiting to connect'));
|
||||
const iterationTimer = setTimeout(() => {
|
||||
topology.removeListener('topologyDescriptionChanged', descriptionChangedHandler);
|
||||
callback(new MongoTimeoutError(`Server selection timed out after ${timeout} ms`));
|
||||
}, timeout - duration);
|
||||
|
||||
topology.once('connect', () => {
|
||||
clearTimeout(failToConnectTimer);
|
||||
retrySelection();
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
topology.once('topologyDescriptionChanged', descriptionChangedHandler);
|
||||
};
|
||||
|
||||
retrySelection();
|
||||
}
|
||||
|
||||
function createAndConnectServer(topology, serverDescription) {
|
||||
topology.emit(
|
||||
'serverOpening',
|
||||
new monitoring.ServerOpeningEvent(topology.s.id, serverDescription.address)
|
||||
);
|
||||
|
||||
const server = new Server(serverDescription, topology.s.options, topology);
|
||||
relayEvents(server, topology, SERVER_RELAY_EVENTS);
|
||||
|
||||
server.once('connect', serverConnectEventHandler(server, topology));
|
||||
server.on('descriptionReceived', topology.serverUpdateHandler.bind(topology));
|
||||
server.on('error', serverErrorEventHandler(server, topology));
|
||||
server.on('close', () => topology.emit('close', server));
|
||||
server.connect();
|
||||
return server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create `Server` instances for all initially known servers, connect them, and assign
|
||||
* them to the passed in `Topology`.
|
||||
@@ -471,53 +805,24 @@ function selectServers(topology, selector, timeout, start, callback) {
|
||||
*/
|
||||
function connectServers(topology, serverDescriptions) {
|
||||
topology.s.servers = serverDescriptions.reduce((servers, serverDescription) => {
|
||||
// publish an open event for each ServerDescription created
|
||||
topology.emit(
|
||||
'serverOpening',
|
||||
new monitoring.ServerOpeningEvent(topology.s.id, serverDescription.address)
|
||||
);
|
||||
|
||||
const server = new Server(serverDescription, topology.s.options);
|
||||
relayEvents(server, topology, [
|
||||
'serverHeartbeatStarted',
|
||||
'serverHeartbeatSucceeded',
|
||||
'serverHeartbeatFailed'
|
||||
]);
|
||||
|
||||
server.on('descriptionReceived', topology.serverUpdateHandler.bind(topology));
|
||||
server.on('connect', serverConnectEventHandler(server, topology));
|
||||
const server = createAndConnectServer(topology, serverDescription);
|
||||
servers.set(serverDescription.address, server);
|
||||
server.connect();
|
||||
return servers;
|
||||
}, new Map());
|
||||
}
|
||||
|
||||
function updateServers(topology, currentServerDescription) {
|
||||
function updateServers(topology, incomingServerDescription) {
|
||||
// update the internal server's description
|
||||
if (topology.s.servers.has(currentServerDescription.address)) {
|
||||
const server = topology.s.servers.get(currentServerDescription.address);
|
||||
server.s.description = currentServerDescription;
|
||||
if (topology.s.servers.has(incomingServerDescription.address)) {
|
||||
const server = topology.s.servers.get(incomingServerDescription.address);
|
||||
server.s.description = incomingServerDescription;
|
||||
}
|
||||
|
||||
// add new servers for all descriptions we currently don't know about locally
|
||||
for (const serverDescription of topology.description.servers.values()) {
|
||||
if (!topology.s.servers.has(serverDescription.address)) {
|
||||
topology.emit(
|
||||
'serverOpening',
|
||||
new monitoring.ServerOpeningEvent(topology.s.id, serverDescription.address)
|
||||
);
|
||||
|
||||
const server = new Server(serverDescription, topology.s.options);
|
||||
relayEvents(server, topology, [
|
||||
'serverHeartbeatStarted',
|
||||
'serverHeartbeatSucceeded',
|
||||
'serverHeartbeatFailed'
|
||||
]);
|
||||
|
||||
server.on('descriptionReceived', topology.serverUpdateHandler.bind(topology));
|
||||
server.on('connect', serverConnectEventHandler(server, topology));
|
||||
const server = createAndConnectServer(topology, serverDescription);
|
||||
topology.s.servers.set(serverDescription.address, server);
|
||||
server.connect();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,15 +836,33 @@ function updateServers(topology, currentServerDescription) {
|
||||
const server = topology.s.servers.get(serverAddress);
|
||||
topology.s.servers.delete(serverAddress);
|
||||
|
||||
server.destroy(() =>
|
||||
topology.emit('serverClosed', new monitoring.ServerClosedEvent(topology.s.id, serverAddress))
|
||||
);
|
||||
// prepare server for garbage collection
|
||||
destroyServer(server, topology);
|
||||
}
|
||||
}
|
||||
|
||||
function serverConnectEventHandler(server, topology) {
|
||||
return function(/* ismaster */) {
|
||||
topology.emit('connect', topology);
|
||||
return function(/* isMaster, err */) {
|
||||
server.monitor({
|
||||
initial: true,
|
||||
heartbeatFrequencyMS: topology.description.heartbeatFrequencyMS
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function serverErrorEventHandler(server, topology) {
|
||||
return function(err) {
|
||||
topology.emit(
|
||||
'serverClosed',
|
||||
new monitoring.ServerClosedEvent(topology.s.id, server.description.address)
|
||||
);
|
||||
|
||||
if (err instanceof MongoParseError) {
|
||||
resetServerState(server, err, { clearPool: true });
|
||||
return;
|
||||
}
|
||||
|
||||
resetServerState(server, err);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -555,12 +878,12 @@ function executeWriteOperation(args, options, callback) {
|
||||
|
||||
const willRetryWrite =
|
||||
!args.retrying &&
|
||||
options.retryWrites &&
|
||||
!!options.retryWrites &&
|
||||
options.session &&
|
||||
isRetryableWritesSupported(topology) &&
|
||||
!options.session.inTransaction();
|
||||
|
||||
topology.selectServer(writableServerSelector(), (err, server) => {
|
||||
topology.selectServer(writableServerSelector(), options, (err, server) => {
|
||||
if (err) {
|
||||
callback(err, null);
|
||||
return;
|
||||
@@ -568,7 +891,7 @@ function executeWriteOperation(args, options, callback) {
|
||||
|
||||
const handler = (err, result) => {
|
||||
if (!err) return callback(null, result);
|
||||
if (!(err instanceof MongoNetworkError) && !err.message.match(/not master/)) {
|
||||
if (!isRetryableError(err)) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
@@ -592,14 +915,58 @@ function executeWriteOperation(args, options, callback) {
|
||||
|
||||
// execute the write operation
|
||||
server[op](ns, ops, options, handler);
|
||||
|
||||
// we need to increment the statement id if we're in a transaction
|
||||
if (options.session && options.session.inTransaction()) {
|
||||
options.session.incrementStatementId(ops.length);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the internal state of this server to `Unknown` by simulating an empty ismaster
|
||||
*
|
||||
* @private
|
||||
* @param {Server} server
|
||||
* @param {MongoError} error The error that caused the state reset
|
||||
* @param {object} [options] Optional settings
|
||||
* @param {boolean} [options.clearPool=false] Pool should be cleared out on state reset
|
||||
*/
|
||||
function resetServerState(server, error, options) {
|
||||
options = Object.assign({}, { clearPool: false }, options);
|
||||
|
||||
function resetState() {
|
||||
server.emit(
|
||||
'descriptionReceived',
|
||||
new ServerDescription(server.description.address, null, { error })
|
||||
);
|
||||
}
|
||||
|
||||
if (options.clearPool && server.pool) {
|
||||
server.pool.reset(() => resetState());
|
||||
return;
|
||||
}
|
||||
|
||||
resetState();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* A server opening SDAM monitoring event
|
||||
*
|
||||
@@ -663,4 +1030,25 @@ function executeWriteOperation(args, options, callback) {
|
||||
* @type {ServerHeartbeatSucceededEvent}
|
||||
*/
|
||||
|
||||
/**
|
||||
* An event emitted indicating a command was started, if command monitoring is enabled
|
||||
*
|
||||
* @event Topology#commandStarted
|
||||
* @type {object}
|
||||
*/
|
||||
|
||||
/**
|
||||
* An event emitted indicating a command succeeded, if command monitoring is enabled
|
||||
*
|
||||
* @event Topology#commandSucceeded
|
||||
* @type {object}
|
||||
*/
|
||||
|
||||
/**
|
||||
* An event emitted indicating a command failed, if command monitoring is enabled
|
||||
*
|
||||
* @event Topology#commandFailed
|
||||
* @type {object}
|
||||
*/
|
||||
|
||||
module.exports = Topology;
|
||||
|
||||
+37
-20
@@ -2,11 +2,13 @@
|
||||
const ServerType = require('./server_description').ServerType;
|
||||
const ServerDescription = require('./server_description').ServerDescription;
|
||||
const ReadPreference = require('../topologies/read_preference');
|
||||
const WIRE_CONSTANTS = require('../wireprotocol/constants');
|
||||
|
||||
// contstants related to compatability checks
|
||||
const MIN_SUPPORTED_SERVER_VERSION = '2.6';
|
||||
const MIN_SUPPORTED_WIRE_VERSION = 2;
|
||||
const MAX_SUPPORTED_WIRE_VERSION = 5;
|
||||
const MIN_SUPPORTED_SERVER_VERSION = WIRE_CONSTANTS.MIN_SUPPORTED_SERVER_VERSION;
|
||||
const MAX_SUPPORTED_SERVER_VERSION = WIRE_CONSTANTS.MAX_SUPPORTED_SERVER_VERSION;
|
||||
const MIN_SUPPORTED_WIRE_VERSION = WIRE_CONSTANTS.MIN_SUPPORTED_WIRE_VERSION;
|
||||
const MAX_SUPPORTED_WIRE_VERSION = WIRE_CONSTANTS.MAX_SUPPORTED_WIRE_VERSION;
|
||||
|
||||
// An enumeration of topology types we know about
|
||||
const TopologyType = {
|
||||
@@ -28,7 +30,16 @@ class TopologyDescription {
|
||||
* @param {number} maxSetVersion
|
||||
* @param {ObjectId} maxElectionId
|
||||
*/
|
||||
constructor(topologyType, serverDescriptions, setName, maxSetVersion, maxElectionId, options) {
|
||||
constructor(
|
||||
topologyType,
|
||||
serverDescriptions,
|
||||
setName,
|
||||
maxSetVersion,
|
||||
maxElectionId,
|
||||
commonWireVersion,
|
||||
options,
|
||||
error
|
||||
) {
|
||||
options = options || {};
|
||||
|
||||
// TODO: consider assigning all these values to a temporary value `s` which
|
||||
@@ -46,14 +57,18 @@ class TopologyDescription {
|
||||
this.heartbeatFrequencyMS = options.heartbeatFrequencyMS || 0;
|
||||
this.localThresholdMS = options.localThresholdMS || 0;
|
||||
this.options = options;
|
||||
this.error = error;
|
||||
this.commonWireVersion = commonWireVersion || null;
|
||||
|
||||
// determine server compatibility
|
||||
for (const serverDescription of this.servers.values()) {
|
||||
if (serverDescription.type === ServerType.Unknown) continue;
|
||||
|
||||
if (serverDescription.minWireVersion > MAX_SUPPORTED_WIRE_VERSION) {
|
||||
this.compatible = false;
|
||||
this.compatibilityError = `Server at ${serverDescription.address} requires wire version ${
|
||||
serverDescription.minWireVersion
|
||||
}, but this version of the driver only supports up to ${MAX_SUPPORTED_WIRE_VERSION}.`;
|
||||
}, but this version of the driver only supports up to ${MAX_SUPPORTED_WIRE_VERSION} (MongoDB ${MAX_SUPPORTED_SERVER_VERSION})`;
|
||||
}
|
||||
|
||||
if (serverDescription.maxWireVersion < MIN_SUPPORTED_WIRE_VERSION) {
|
||||
@@ -78,19 +93,6 @@ class TopologyDescription {
|
||||
}, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The minimum reported wire version of all known servers
|
||||
*/
|
||||
get commonWireVersion() {
|
||||
return Array.from(this.servers.values())
|
||||
.filter(server => server.type !== ServerType.Unknown)
|
||||
.reduce(
|
||||
(min, server) =>
|
||||
min == null ? server.maxWireVersion : Math.min(min, server.maxWireVersion),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this description updated with a given ServerDescription
|
||||
*
|
||||
@@ -106,10 +108,21 @@ class TopologyDescription {
|
||||
let setName = this.setName;
|
||||
let maxSetVersion = this.maxSetVersion;
|
||||
let maxElectionId = this.maxElectionId;
|
||||
let commonWireVersion = this.commonWireVersion;
|
||||
let error = serverDescription.error || null;
|
||||
|
||||
const serverType = serverDescription.type;
|
||||
let serverDescriptions = new Map(this.servers);
|
||||
|
||||
// update common wire version
|
||||
if (serverDescription.maxWireVersion !== 0) {
|
||||
if (commonWireVersion == null) {
|
||||
commonWireVersion = serverDescription.maxWireVersion;
|
||||
} else {
|
||||
commonWireVersion = Math.min(commonWireVersion, serverDescription.maxWireVersion);
|
||||
}
|
||||
}
|
||||
|
||||
// update the actual server description
|
||||
serverDescriptions.set(address, serverDescription);
|
||||
|
||||
@@ -121,7 +134,9 @@ class TopologyDescription {
|
||||
setName,
|
||||
maxSetVersion,
|
||||
maxElectionId,
|
||||
this.options
|
||||
commonWireVersion,
|
||||
this.options,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
@@ -201,7 +216,9 @@ class TopologyDescription {
|
||||
setName,
|
||||
maxSetVersion,
|
||||
maxElectionId,
|
||||
this.options
|
||||
commonWireVersion,
|
||||
this.options,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+247
-4
@@ -11,6 +11,10 @@ const MongoNetworkError = require('./error').MongoNetworkError;
|
||||
const MongoWriteConcernError = require('./error').MongoWriteConcernError;
|
||||
const Transaction = require('./transactions').Transaction;
|
||||
const TxnState = require('./transactions').TxnState;
|
||||
const isPromiseLike = require('./utils').isPromiseLike;
|
||||
const ReadPreference = require('./topologies/read_preference');
|
||||
const isTransactionCommand = require('./transactions').isTransactionCommand;
|
||||
const resolveClusterTime = require('./topologies/shared').resolveClusterTime;
|
||||
|
||||
function assertAlive(session, callback) {
|
||||
if (session.serverSession == null) {
|
||||
@@ -124,6 +128,7 @@ class ClientSession extends EventEmitter {
|
||||
|
||||
// release the server session back to the pool
|
||||
this.sessionPool.release(this.serverSession);
|
||||
this.serverSession = null;
|
||||
|
||||
// spec indicates that we should ignore all errors for `endSessions`
|
||||
if (typeof callback === 'function') callback(null, null);
|
||||
@@ -243,6 +248,121 @@ class ClientSession extends EventEmitter {
|
||||
toBSON() {
|
||||
throw new Error('ClientSession cannot be serialized to BSON.');
|
||||
}
|
||||
|
||||
/**
|
||||
* A user provided function to be run within a transaction
|
||||
*
|
||||
* @callback WithTransactionCallback
|
||||
* @param {ClientSession} session The parent session of the transaction running the operation. This should be passed into each operation within the lambda.
|
||||
* @returns {Promise} The resulting Promise of operations run within this transaction
|
||||
*/
|
||||
|
||||
/**
|
||||
* Runs a provided lambda within a transaction, retrying either the commit operation
|
||||
* or entire transaction as needed (and when the error permits) to better ensure that
|
||||
* the transaction can complete successfully.
|
||||
*
|
||||
* IMPORTANT: This method requires the user to return a Promise, all lambdas that do not
|
||||
* return a Promise will result in undefined behavior.
|
||||
*
|
||||
* @param {WithTransactionCallback} fn
|
||||
* @param {TransactionOptions} [options] Optional settings for the transaction
|
||||
*/
|
||||
withTransaction(fn, options) {
|
||||
const startTime = Date.now();
|
||||
return attemptTransaction(this, startTime, fn, options);
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_WITH_TRANSACTION_TIMEOUT = 120000;
|
||||
const UNSATISFIABLE_WRITE_CONCERN_CODE = 100;
|
||||
const UNKNOWN_REPL_WRITE_CONCERN_CODE = 79;
|
||||
const NON_DETERMINISTIC_WRITE_CONCERN_ERRORS = new Set([
|
||||
'CannotSatisfyWriteConcern',
|
||||
'UnknownReplWriteConcern',
|
||||
'UnsatisfiableWriteConcern'
|
||||
]);
|
||||
|
||||
function hasNotTimedOut(startTime, max) {
|
||||
return Date.now() - startTime < max;
|
||||
}
|
||||
|
||||
function isUnknownTransactionCommitResult(err) {
|
||||
return (
|
||||
!NON_DETERMINISTIC_WRITE_CONCERN_ERRORS.has(err.codeName) &&
|
||||
err.code !== UNSATISFIABLE_WRITE_CONCERN_CODE &&
|
||||
err.code !== UNKNOWN_REPL_WRITE_CONCERN_CODE
|
||||
);
|
||||
}
|
||||
|
||||
function attemptTransactionCommit(session, startTime, fn, options) {
|
||||
return session.commitTransaction().catch(err => {
|
||||
if (err instanceof MongoError && hasNotTimedOut(startTime, MAX_WITH_TRANSACTION_TIMEOUT)) {
|
||||
if (err.hasErrorLabel('UnknownTransactionCommitResult')) {
|
||||
return attemptTransactionCommit(session, startTime, fn, options);
|
||||
}
|
||||
|
||||
if (err.hasErrorLabel('TransientTransactionError')) {
|
||||
return attemptTransaction(session, startTime, fn, options);
|
||||
}
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
const USER_EXPLICIT_TXN_END_STATES = new Set([
|
||||
TxnState.NO_TRANSACTION,
|
||||
TxnState.TRANSACTION_COMMITTED,
|
||||
TxnState.TRANSACTION_ABORTED
|
||||
]);
|
||||
|
||||
function userExplicitlyEndedTransaction(session) {
|
||||
return USER_EXPLICIT_TXN_END_STATES.has(session.transaction.state);
|
||||
}
|
||||
|
||||
function attemptTransaction(session, startTime, fn, options) {
|
||||
session.startTransaction(options);
|
||||
|
||||
let promise;
|
||||
try {
|
||||
promise = fn(session);
|
||||
} catch (err) {
|
||||
promise = Promise.reject(err);
|
||||
}
|
||||
|
||||
if (!isPromiseLike(promise)) {
|
||||
session.abortTransaction();
|
||||
throw new TypeError('Function provided to `withTransaction` must return a Promise');
|
||||
}
|
||||
|
||||
return promise
|
||||
.then(() => {
|
||||
if (userExplicitlyEndedTransaction(session)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return attemptTransactionCommit(session, startTime, fn, options);
|
||||
})
|
||||
.catch(err => {
|
||||
function maybeRetryOrThrow(err) {
|
||||
if (
|
||||
err instanceof MongoError &&
|
||||
err.hasErrorLabel('TransientTransactionError') &&
|
||||
hasNotTimedOut(startTime, MAX_WITH_TRANSACTION_TIMEOUT)
|
||||
) {
|
||||
return attemptTransaction(session, startTime, fn, options);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (session.transaction.isActive) {
|
||||
return session.abortTransaction().then(() => maybeRetryOrThrow(err));
|
||||
}
|
||||
|
||||
return maybeRetryOrThrow(err);
|
||||
});
|
||||
}
|
||||
|
||||
function endTransaction(session, commandName, callback) {
|
||||
@@ -300,10 +420,19 @@ function endTransaction(session, commandName, callback) {
|
||||
const command = { [commandName]: 1 };
|
||||
|
||||
// apply a writeConcern if specified
|
||||
let writeConcern;
|
||||
if (session.transaction.options.writeConcern) {
|
||||
Object.assign(command, { writeConcern: session.transaction.options.writeConcern });
|
||||
writeConcern = Object.assign({}, session.transaction.options.writeConcern);
|
||||
} else if (session.clientOptions && session.clientOptions.w) {
|
||||
Object.assign(command, { writeConcern: { w: session.clientOptions.w } });
|
||||
writeConcern = { w: session.clientOptions.w };
|
||||
}
|
||||
|
||||
if (txnState === TxnState.TRANSACTION_COMMITTED) {
|
||||
writeConcern = Object.assign({ wtimeout: 10000 }, writeConcern, { w: 'majority' });
|
||||
}
|
||||
|
||||
if (writeConcern) {
|
||||
Object.assign(command, { writeConcern });
|
||||
}
|
||||
|
||||
function commandHandler(e, r) {
|
||||
@@ -325,7 +454,12 @@ function endTransaction(session, commandName, callback) {
|
||||
e.errorLabels = [];
|
||||
}
|
||||
|
||||
e.errorLabels.push('UnknownTransactionCommitResult');
|
||||
if (isUnknownTransactionCommitResult(e)) {
|
||||
e.errorLabels.push('UnknownTransactionCommitResult');
|
||||
|
||||
// per txns spec, must unpin session in this case
|
||||
session.transaction.unpinServer();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
session.transaction.transition(TxnState.TRANSACTION_ABORTED);
|
||||
@@ -339,9 +473,27 @@ function endTransaction(session, commandName, callback) {
|
||||
return commandName === 'commitTransaction' ? err : null;
|
||||
}
|
||||
|
||||
if (
|
||||
// Assumption here that commandName is "commitTransaction" or "abortTransaction"
|
||||
session.transaction.recoveryToken &&
|
||||
supportsRecoveryToken(session)
|
||||
) {
|
||||
command.recoveryToken = session.transaction.recoveryToken;
|
||||
}
|
||||
|
||||
// send the command
|
||||
session.topology.command('admin.$cmd', command, { session }, (err, reply) => {
|
||||
if (err && isRetryableError(err)) {
|
||||
// SPEC-1185: apply majority write concern when retrying commitTransaction
|
||||
if (command.commitTransaction) {
|
||||
// per txns spec, must unpin session in this case
|
||||
session.transaction.unpinServer();
|
||||
|
||||
command.writeConcern = Object.assign({ wtimeout: 10000 }, command.writeConcern, {
|
||||
w: 'majority'
|
||||
});
|
||||
}
|
||||
|
||||
return session.topology.command('admin.$cmd', command, { session }, (_err, _reply) =>
|
||||
commandHandler(transactionError(_err), _reply)
|
||||
);
|
||||
@@ -351,6 +503,11 @@ function endTransaction(session, commandName, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function supportsRecoveryToken(session) {
|
||||
const topology = session.topology;
|
||||
return !!topology.s.options.useRecoveryToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reflects the existence of a session on the server. Can be reused by the session pool.
|
||||
* WARNING: not meant to be instantiated directly. For internal use only.
|
||||
@@ -451,9 +608,95 @@ class ServerSessionPool {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionally decorate a command with sessions specific keys
|
||||
*
|
||||
* @param {ClientSession} session the session tracking transaction state
|
||||
* @param {Object} command the command to decorate
|
||||
* @param {Object} topology the topology for tracking the cluster time
|
||||
* @param {Object} [options] Optional settings passed to calling operation
|
||||
* @return {MongoError|null} An error, if some error condition was met
|
||||
*/
|
||||
function applySession(session, command, options) {
|
||||
const serverSession = session.serverSession;
|
||||
if (serverSession == null) {
|
||||
// TODO: merge this with `assertAlive`, did not want to throw a try/catch here
|
||||
return new MongoError('Cannot use a session that has ended');
|
||||
}
|
||||
|
||||
// mark the last use of this session, and apply the `lsid`
|
||||
serverSession.lastUse = Date.now();
|
||||
command.lsid = serverSession.id;
|
||||
|
||||
// first apply non-transaction-specific sessions data
|
||||
const inTransaction = session.inTransaction() || isTransactionCommand(command);
|
||||
const isRetryableWrite = options.willRetryWrite;
|
||||
|
||||
if (serverSession.txnNumber && (isRetryableWrite || inTransaction)) {
|
||||
command.txnNumber = BSON.Long.fromNumber(serverSession.txnNumber);
|
||||
}
|
||||
|
||||
// now attempt to apply transaction-specific sessions data
|
||||
if (!inTransaction) {
|
||||
if (session.transaction.state !== TxnState.NO_TRANSACTION) {
|
||||
session.transaction.transition(TxnState.NO_TRANSACTION);
|
||||
}
|
||||
|
||||
// TODO: the following should only be applied to read operation per spec.
|
||||
// for causal consistency
|
||||
if (session.supports.causalConsistency && session.operationTime) {
|
||||
command.readConcern = command.readConcern || {};
|
||||
Object.assign(command.readConcern, { afterClusterTime: session.operationTime });
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.readPreference && !options.readPreference.equals(ReadPreference.primary)) {
|
||||
return new MongoError(
|
||||
`Read preference in a transaction must be primary, not: ${options.readPreference.mode}`
|
||||
);
|
||||
}
|
||||
|
||||
// `autocommit` must always be false to differentiate from retryable writes
|
||||
command.autocommit = false;
|
||||
|
||||
if (session.transaction.state === TxnState.STARTING_TRANSACTION) {
|
||||
session.transaction.transition(TxnState.TRANSACTION_IN_PROGRESS);
|
||||
command.startTransaction = true;
|
||||
|
||||
const readConcern =
|
||||
session.transaction.options.readConcern || session.clientOptions.readConcern;
|
||||
if (readConcern) {
|
||||
command.readConcern = readConcern;
|
||||
}
|
||||
|
||||
if (session.supports.causalConsistency && session.operationTime) {
|
||||
command.readConcern = command.readConcern || {};
|
||||
Object.assign(command.readConcern, { afterClusterTime: session.operationTime });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateSessionFromResponse(session, document) {
|
||||
if (document.$clusterTime) {
|
||||
resolveClusterTime(session, document.$clusterTime);
|
||||
}
|
||||
|
||||
if (document.operationTime && session && session.supports.causalConsistency) {
|
||||
session.advanceOperationTime(document.operationTime);
|
||||
}
|
||||
|
||||
if (document.recoveryToken && session && session.inTransaction()) {
|
||||
session.transaction._recoveryToken = document.recoveryToken;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ClientSession,
|
||||
ServerSession,
|
||||
ServerSessionPool,
|
||||
TxnState
|
||||
TxnState,
|
||||
applySession,
|
||||
updateSessionFromResponse
|
||||
};
|
||||
|
||||
+179
-332
@@ -37,8 +37,6 @@ const BSON = retrieveBSON();
|
||||
* server.connect();
|
||||
*/
|
||||
|
||||
const defaultAuthProviders = require('../auth/defaultAuthProviders').defaultAuthProviders;
|
||||
|
||||
//
|
||||
// States
|
||||
var DISCONNECTED = 'disconnected';
|
||||
@@ -91,7 +89,6 @@ var handlers = ['connect', 'close', 'error', 'timeout', 'parseError'];
|
||||
* @param {boolean} [options.noDelay=true] TCP Connection no delay
|
||||
* @param {number} [options.connectionTimeout=1000] TCP Connection timeout setting
|
||||
* @param {number} [options.socketTimeout=0] TCP Socket timeout setting
|
||||
* @param {boolean} [options.singleBufferSerializtion=true] Serialize into single buffer, trade of peak memory for serialization speed
|
||||
* @param {boolean} [options.ssl=false] Use SSL for connection
|
||||
* @param {boolean|function} [options.checkServerIdentity=true] Ensure we check server identify during SSL, set to false to disable checking. Only works for Node 0.12.x or higher. You can pass in a boolean or your own checkServerIdentity override function.
|
||||
* @param {Buffer} [options.ca] SSL Certificate store binary buffer
|
||||
@@ -170,9 +167,7 @@ var Mongos = function(seedlist, options) {
|
||||
// localThresholdMS
|
||||
localThresholdMS: options.localThresholdMS || 15,
|
||||
// Client info
|
||||
clientInfo: createClientInfo(options),
|
||||
// Authentication context
|
||||
authenticationContexts: []
|
||||
clientInfo: createClientInfo(options)
|
||||
};
|
||||
|
||||
// Set the client info
|
||||
@@ -194,9 +189,6 @@ var Mongos = function(seedlist, options) {
|
||||
);
|
||||
}
|
||||
|
||||
// All the authProviders
|
||||
this.authProviders = options.authProviders || defaultAuthProviders(this.s.bson);
|
||||
|
||||
// Disconnected state
|
||||
this.state = DISCONNECTED;
|
||||
|
||||
@@ -206,8 +198,6 @@ var Mongos = function(seedlist, options) {
|
||||
this.connectedProxies = [];
|
||||
// Disconnected proxies
|
||||
this.disconnectedProxies = [];
|
||||
// Are we authenticating
|
||||
this.authenticating = false;
|
||||
// Index of proxy to run operations against
|
||||
this.index = 0;
|
||||
// High availability timeout id
|
||||
@@ -264,16 +254,14 @@ function emitSDAMEvent(self, event, description) {
|
||||
}
|
||||
|
||||
const SERVER_EVENTS = ['serverDescriptionChanged', 'error', 'close', 'timeout', 'parseError'];
|
||||
function destroyServer(server, options) {
|
||||
function destroyServer(server, options, callback) {
|
||||
options = options || {};
|
||||
SERVER_EVENTS.forEach(event => server.removeAllListeners(event));
|
||||
server.destroy(options);
|
||||
server.destroy(options, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate server connect
|
||||
* @method
|
||||
* @param {array} [options.auth=null] Array of auth options to apply on connect
|
||||
*/
|
||||
Mongos.prototype.connect = function(options) {
|
||||
var self = this;
|
||||
@@ -286,8 +274,7 @@ Mongos.prototype.connect = function(options) {
|
||||
// Create server instances
|
||||
var servers = this.s.seedlist.map(function(x) {
|
||||
const server = new Server(
|
||||
Object.assign({}, self.s.options, x, {
|
||||
authProviders: self.authProviders,
|
||||
Object.assign({}, self.s.options, x, options, {
|
||||
reconnect: false,
|
||||
monitoring: false,
|
||||
parent: self,
|
||||
@@ -306,6 +293,16 @@ Mongos.prototype.connect = function(options) {
|
||||
connectProxies(self, servers);
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticate the topology.
|
||||
* @method
|
||||
* @param {MongoCredentials} credentials The credentials for authentication we are using
|
||||
* @param {authResultCallback} callback A callback function
|
||||
*/
|
||||
Mongos.prototype.auth = function(credentials, callback) {
|
||||
if (typeof callback === 'function') callback(null, null);
|
||||
};
|
||||
|
||||
function handleEvent(self) {
|
||||
return function() {
|
||||
if (self.state === DESTROYED) return;
|
||||
@@ -338,60 +335,57 @@ function handleInitialConnectEvent(self, event) {
|
||||
|
||||
// Check the type of server
|
||||
if (event === 'connect') {
|
||||
// Do we have authentication contexts that need to be applied
|
||||
applyAuthenticationContexts(self, _this, function() {
|
||||
// Get last known ismaster
|
||||
self.ismaster = _this.lastIsMaster();
|
||||
// Get last known ismaster
|
||||
self.ismaster = _this.lastIsMaster();
|
||||
|
||||
// Is this not a proxy, remove t
|
||||
if (self.ismaster.msg === 'isdbgrid') {
|
||||
// Add to the connectd list
|
||||
for (var i = 0; i < self.connectedProxies.length; i++) {
|
||||
if (self.connectedProxies[i].name === _this.name) {
|
||||
// Move from connectingProxies
|
||||
moveServerFrom(self.connectingProxies, self.disconnectedProxies, _this);
|
||||
// Emit the initial topology
|
||||
emitTopologyDescriptionChanged(self);
|
||||
_this.destroy();
|
||||
return self.emit('failed', _this);
|
||||
}
|
||||
// Is this not a proxy, remove t
|
||||
if (self.ismaster.msg === 'isdbgrid') {
|
||||
// Add to the connectd list
|
||||
for (let i = 0; i < self.connectedProxies.length; i++) {
|
||||
if (self.connectedProxies[i].name === _this.name) {
|
||||
// Move from connectingProxies
|
||||
moveServerFrom(self.connectingProxies, self.disconnectedProxies, _this);
|
||||
// Emit the initial topology
|
||||
emitTopologyDescriptionChanged(self);
|
||||
_this.destroy();
|
||||
return self.emit('failed', _this);
|
||||
}
|
||||
|
||||
// Remove the handlers
|
||||
for (i = 0; i < handlers.length; i++) {
|
||||
_this.removeAllListeners(handlers[i]);
|
||||
}
|
||||
|
||||
// Add stable state handlers
|
||||
_this.on('error', handleEvent(self, 'error'));
|
||||
_this.on('close', handleEvent(self, 'close'));
|
||||
_this.on('timeout', handleEvent(self, 'timeout'));
|
||||
_this.on('parseError', handleEvent(self, 'parseError'));
|
||||
|
||||
// Move from connecting proxies connected
|
||||
moveServerFrom(self.connectingProxies, self.connectedProxies, _this);
|
||||
// Emit the joined event
|
||||
self.emit('joined', 'mongos', _this);
|
||||
} else {
|
||||
// Print warning if we did not find a mongos proxy
|
||||
if (self.s.logger.isWarn()) {
|
||||
var message = 'expected mongos proxy, but found replicaset member mongod for server %s';
|
||||
// We have a standalone server
|
||||
if (!self.ismaster.hosts) {
|
||||
message = 'expected mongos proxy, but found standalone mongod for server %s';
|
||||
}
|
||||
|
||||
self.s.logger.warn(f(message, _this.name));
|
||||
}
|
||||
|
||||
// This is not a mongos proxy, remove it completely
|
||||
removeProxyFrom(self.connectingProxies, _this);
|
||||
// Emit the left event
|
||||
self.emit('left', 'server', _this);
|
||||
// Emit failed event
|
||||
self.emit('failed', _this);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove the handlers
|
||||
for (let i = 0; i < handlers.length; i++) {
|
||||
_this.removeAllListeners(handlers[i]);
|
||||
}
|
||||
|
||||
// Add stable state handlers
|
||||
_this.on('error', handleEvent(self, 'error'));
|
||||
_this.on('close', handleEvent(self, 'close'));
|
||||
_this.on('timeout', handleEvent(self, 'timeout'));
|
||||
_this.on('parseError', handleEvent(self, 'parseError'));
|
||||
|
||||
// Move from connecting proxies connected
|
||||
moveServerFrom(self.connectingProxies, self.connectedProxies, _this);
|
||||
// Emit the joined event
|
||||
self.emit('joined', 'mongos', _this);
|
||||
} else {
|
||||
// Print warning if we did not find a mongos proxy
|
||||
if (self.s.logger.isWarn()) {
|
||||
var message = 'expected mongos proxy, but found replicaset member mongod for server %s';
|
||||
// We have a standalone server
|
||||
if (!self.ismaster.hosts) {
|
||||
message = 'expected mongos proxy, but found standalone mongod for server %s';
|
||||
}
|
||||
|
||||
self.s.logger.warn(f(message, _this.name));
|
||||
}
|
||||
|
||||
// This is not a mongos proxy, remove it completely
|
||||
removeProxyFrom(self.connectingProxies, _this);
|
||||
// Emit the left event
|
||||
self.emit('left', 'server', _this);
|
||||
// Emit failed event
|
||||
self.emit('failed', _this);
|
||||
}
|
||||
} else {
|
||||
moveServerFrom(self.connectingProxies, self.disconnectedProxies, this);
|
||||
// Emit the left event
|
||||
@@ -470,7 +464,18 @@ function connectProxies(self, servers) {
|
||||
}
|
||||
}
|
||||
|
||||
function pickProxy(self) {
|
||||
function pickProxy(self, session) {
|
||||
// TODO: Destructure :)
|
||||
const transaction = session && session.transaction;
|
||||
|
||||
if (transaction && transaction.server) {
|
||||
if (transaction.server.isConnected()) {
|
||||
return transaction.server;
|
||||
} else {
|
||||
transaction.unpinServer();
|
||||
}
|
||||
}
|
||||
|
||||
// Get the currently connected Proxies
|
||||
var connectedProxies = self.connectedProxies.slice(0);
|
||||
|
||||
@@ -494,15 +499,22 @@ function pickProxy(self) {
|
||||
}
|
||||
});
|
||||
|
||||
let proxy;
|
||||
|
||||
// We have no connectedProxies pick first of the connected ones
|
||||
if (connectedProxies.length === 0) {
|
||||
return self.connectedProxies[0];
|
||||
proxy = self.connectedProxies[0];
|
||||
} else {
|
||||
// Get proxy
|
||||
proxy = connectedProxies[self.index % connectedProxies.length];
|
||||
// Update the index
|
||||
self.index = (self.index + 1) % connectedProxies.length;
|
||||
}
|
||||
|
||||
if (transaction && transaction.isActive && proxy && proxy.isConnected()) {
|
||||
transaction.pinServer(proxy);
|
||||
}
|
||||
|
||||
// Get proxy
|
||||
var proxy = connectedProxies[self.index % connectedProxies.length];
|
||||
// Update the index
|
||||
self.index = (self.index + 1) % connectedProxies.length;
|
||||
// Return the proxy
|
||||
return proxy;
|
||||
}
|
||||
@@ -547,33 +559,30 @@ function reconnectProxies(self, proxies, callback) {
|
||||
return this.destroy();
|
||||
}
|
||||
|
||||
if (event === 'connect' && !self.authenticating) {
|
||||
// Do we have authentication contexts that need to be applied
|
||||
applyAuthenticationContexts(self, _self, function() {
|
||||
// Destroyed
|
||||
if (self.state === DESTROYED || self.state === UNREFERENCED) {
|
||||
moveServerFrom(self.connectingProxies, self.disconnectedProxies, _self);
|
||||
return _self.destroy();
|
||||
}
|
||||
if (event === 'connect') {
|
||||
// Destroyed
|
||||
if (self.state === DESTROYED || self.state === UNREFERENCED) {
|
||||
moveServerFrom(self.connectingProxies, self.disconnectedProxies, _self);
|
||||
return _self.destroy();
|
||||
}
|
||||
|
||||
// Remove the handlers
|
||||
for (var i = 0; i < handlers.length; i++) {
|
||||
_self.removeAllListeners(handlers[i]);
|
||||
}
|
||||
// Remove the handlers
|
||||
for (var i = 0; i < handlers.length; i++) {
|
||||
_self.removeAllListeners(handlers[i]);
|
||||
}
|
||||
|
||||
// Add stable state handlers
|
||||
_self.on('error', handleEvent(self, 'error'));
|
||||
_self.on('close', handleEvent(self, 'close'));
|
||||
_self.on('timeout', handleEvent(self, 'timeout'));
|
||||
_self.on('parseError', handleEvent(self, 'parseError'));
|
||||
// Add stable state handlers
|
||||
_self.on('error', handleEvent(self, 'error'));
|
||||
_self.on('close', handleEvent(self, 'close'));
|
||||
_self.on('timeout', handleEvent(self, 'timeout'));
|
||||
_self.on('parseError', handleEvent(self, 'parseError'));
|
||||
|
||||
// Move to the connected servers
|
||||
moveServerFrom(self.connectingProxies, self.connectedProxies, _self);
|
||||
// Emit topology Change
|
||||
emitTopologyDescriptionChanged(self);
|
||||
// Emit joined event
|
||||
self.emit('joined', 'mongos', _self);
|
||||
});
|
||||
// Move to the connected servers
|
||||
moveServerFrom(self.connectingProxies, self.connectedProxies, _self);
|
||||
// Emit topology Change
|
||||
emitTopologyDescriptionChanged(self);
|
||||
// Emit joined event
|
||||
self.emit('joined', 'mongos', _self);
|
||||
} else {
|
||||
// Move from connectingProxies
|
||||
moveServerFrom(self.connectingProxies, self.disconnectedProxies, _self);
|
||||
@@ -605,7 +614,6 @@ function reconnectProxies(self, proxies, callback) {
|
||||
Object.assign({}, self.s.options, {
|
||||
host: _server.name.split(':')[0],
|
||||
port: parseInt(_server.name.split(':')[1], 10),
|
||||
authProviders: self.authProviders,
|
||||
reconnect: false,
|
||||
monitoring: false,
|
||||
parent: self,
|
||||
@@ -647,35 +655,6 @@ function reconnectProxies(self, proxies, callback) {
|
||||
}
|
||||
}
|
||||
|
||||
function applyAuthenticationContexts(self, server, callback) {
|
||||
if (self.s.authenticationContexts.length === 0) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
// Copy contexts to ensure no modificiation in the middle of
|
||||
// auth process.
|
||||
var authContexts = self.s.authenticationContexts.slice(0);
|
||||
|
||||
// Apply one of the contexts
|
||||
function applyAuth(authContexts, server, callback) {
|
||||
if (authContexts.length === 0) return callback();
|
||||
// Get the first auth context
|
||||
var authContext = authContexts.shift();
|
||||
// Copy the params
|
||||
var customAuthContext = authContext.slice(0);
|
||||
// Push our callback handler
|
||||
customAuthContext.push(function(/* err */) {
|
||||
applyAuth(authContexts, server, callback);
|
||||
});
|
||||
|
||||
// Attempt authentication
|
||||
server.auth.apply(server, customAuthContext);
|
||||
}
|
||||
|
||||
// Apply all auth contexts
|
||||
applyAuth(authContexts, server, callback);
|
||||
}
|
||||
|
||||
function topologyMonitor(self, options) {
|
||||
options = options || {};
|
||||
|
||||
@@ -829,34 +808,43 @@ Mongos.prototype.unref = function() {
|
||||
* @param {boolean} [options.force=false] Force destroy the pool
|
||||
* @method
|
||||
*/
|
||||
Mongos.prototype.destroy = function(options) {
|
||||
var self = this;
|
||||
// Transition state
|
||||
stateTransition(this, DESTROYED);
|
||||
// Get all proxies
|
||||
var proxies = this.connectedProxies.concat(this.connectingProxies);
|
||||
// Clear out any monitoring process
|
||||
if (this.haTimeoutId) clearTimeout(this.haTimeoutId);
|
||||
// Clear out authentication contexts
|
||||
this.s.authenticationContexts = [];
|
||||
Mongos.prototype.destroy = function(options, callback) {
|
||||
if (this.haTimeoutId) {
|
||||
clearTimeout(this.haTimeoutId);
|
||||
}
|
||||
|
||||
const proxies = this.connectedProxies.concat(this.connectingProxies);
|
||||
let serverCount = proxies.length;
|
||||
const serverDestroyed = () => {
|
||||
serverCount--;
|
||||
if (serverCount > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
emitTopologyDescriptionChanged(this);
|
||||
emitSDAMEvent(this, 'topologyClosed', { topologyId: this.id });
|
||||
stateTransition(this, DESTROYED);
|
||||
if (typeof callback === 'function') {
|
||||
callback(null, null);
|
||||
}
|
||||
};
|
||||
|
||||
if (serverCount === 0) {
|
||||
serverDestroyed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Destroy all connecting servers
|
||||
proxies.forEach(function(server) {
|
||||
proxies.forEach(server => {
|
||||
// Emit the sdam event
|
||||
self.emit('serverClosed', {
|
||||
topologyId: self.id,
|
||||
this.emit('serverClosed', {
|
||||
topologyId: this.id,
|
||||
address: server.name
|
||||
});
|
||||
|
||||
destroyServer(server, options);
|
||||
|
||||
// Move to list of disconnectedProxies
|
||||
moveServerFrom(self.connectedProxies, self.disconnectedProxies, server);
|
||||
destroyServer(server, options, serverDestroyed);
|
||||
moveServerFrom(this.connectedProxies, this.disconnectedProxies, server);
|
||||
});
|
||||
// Emit the final topology change
|
||||
emitTopologyDescriptionChanged(self);
|
||||
// Emit toplogy closing event
|
||||
emitSDAMEvent(this, 'topologyClosed', { topologyId: this.id });
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -881,43 +869,59 @@ Mongos.prototype.isDestroyed = function() {
|
||||
// Operations
|
||||
//
|
||||
|
||||
// Execute write operation
|
||||
var executeWriteOperation = function(self, op, ns, ops, options, callback) {
|
||||
function executeWriteOperation(args, options, callback) {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = options || {};
|
||||
|
||||
// TODO: once we drop Node 4, use destructuring either here or in arguments.
|
||||
const self = args.self;
|
||||
const op = args.op;
|
||||
const ns = args.ns;
|
||||
const ops = args.ops;
|
||||
|
||||
// Pick a server
|
||||
let server = pickProxy(self);
|
||||
let server = pickProxy(self, options.session);
|
||||
// No server found error out
|
||||
if (!server) return callback(new MongoError('no mongos proxy available'));
|
||||
|
||||
if (!options.retryWrites || !options.session || !isRetryableWritesSupported(self)) {
|
||||
// Execute the command
|
||||
return server[op](ns, ops, options, callback);
|
||||
}
|
||||
const willRetryWrite =
|
||||
!args.retrying &&
|
||||
!!options.retryWrites &&
|
||||
options.session &&
|
||||
isRetryableWritesSupported(self) &&
|
||||
!options.session.inTransaction();
|
||||
|
||||
// increment and assign txnNumber
|
||||
options.willRetryWrite = true;
|
||||
options.session.incrementTransactionNumber();
|
||||
|
||||
server[op](ns, ops, options, (err, result) => {
|
||||
const handler = (err, result) => {
|
||||
if (!err) return callback(null, result);
|
||||
if (!isRetryableError(err)) {
|
||||
if (!isRetryableError(err) || !willRetryWrite) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Pick another server
|
||||
server = pickProxy(self);
|
||||
server = pickProxy(self, options.session);
|
||||
|
||||
// No server found error out with original error
|
||||
if (!server || !isRetryableWritesSupported(server)) {
|
||||
if (!server) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// rerun the operation
|
||||
server[op](ns, ops, options, callback);
|
||||
});
|
||||
};
|
||||
const newArgs = Object.assign({}, args, { retrying: true });
|
||||
return executeWriteOperation(newArgs, options, callback);
|
||||
};
|
||||
|
||||
if (callback.operationId) {
|
||||
handler.operationId = callback.operationId;
|
||||
}
|
||||
|
||||
// increment and assign txnNumber
|
||||
if (willRetryWrite) {
|
||||
options.session.incrementTransactionNumber();
|
||||
options.willRetryWrite = willRetryWrite;
|
||||
}
|
||||
|
||||
// rerun the operation
|
||||
server[op](ns, ops, options, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert one or more documents
|
||||
@@ -950,7 +954,7 @@ Mongos.prototype.insert = function(ns, ops, options, callback) {
|
||||
}
|
||||
|
||||
// Execute write operation
|
||||
executeWriteOperation(this, 'insert', ns, ops, options, callback);
|
||||
executeWriteOperation({ self: this, op: 'insert', ns, ops }, options, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -984,7 +988,7 @@ Mongos.prototype.update = function(ns, ops, options, callback) {
|
||||
}
|
||||
|
||||
// Execute write operation
|
||||
executeWriteOperation(this, 'update', ns, ops, options, callback);
|
||||
executeWriteOperation({ self: this, op: 'update', ns, ops }, options, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1018,7 +1022,7 @@ Mongos.prototype.remove = function(ns, ops, options, callback) {
|
||||
}
|
||||
|
||||
// Execute write operation
|
||||
executeWriteOperation(this, 'remove', ns, ops, options, callback);
|
||||
executeWriteOperation({ self: this, op: 'remove', ns, ops }, options, callback);
|
||||
};
|
||||
|
||||
const RETRYABLE_WRITE_OPERATIONS = ['findAndModify', 'insert', 'update', 'delete'];
|
||||
@@ -1048,7 +1052,7 @@ Mongos.prototype.command = function(ns, cmd, options, callback) {
|
||||
var self = this;
|
||||
|
||||
// Pick a proxy
|
||||
var server = pickProxy(self);
|
||||
var server = pickProxy(self, options.session);
|
||||
|
||||
// Topology is not connected, save the call in the provided store to be
|
||||
// Executed at some point when the handler deems it's reconnected
|
||||
@@ -1123,170 +1127,13 @@ Mongos.prototype.cursor = function(ns, cmd, options) {
|
||||
return new FinalCursor(this.s.bson, ns, cmd, options, topology, this.s.options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticate using a specified mechanism
|
||||
* @method
|
||||
* @param {string} mechanism The Auth mechanism we are invoking
|
||||
* @param {string} db The db we are invoking the mechanism against
|
||||
* @param {...object} param Parameters for the specific mechanism
|
||||
* @param {authResultCallback} callback A callback function
|
||||
*/
|
||||
Mongos.prototype.auth = function(mechanism, db) {
|
||||
var allArgs = Array.prototype.slice.call(arguments, 0).slice(0);
|
||||
var self = this;
|
||||
var args = Array.prototype.slice.call(arguments, 2);
|
||||
var callback = args.pop();
|
||||
var currentContextIndex = 0;
|
||||
|
||||
// If we don't have the mechanism fail
|
||||
if (this.authProviders[mechanism] == null && mechanism !== 'default') {
|
||||
return callback(new MongoError(f('auth provider %s does not exist', mechanism)));
|
||||
}
|
||||
|
||||
// Are we already authenticating, throw
|
||||
if (this.authenticating) {
|
||||
return callback(new MongoError('authentication or logout allready in process'));
|
||||
}
|
||||
|
||||
// Topology is not connected, save the call in the provided store to be
|
||||
// Executed at some point when the handler deems it's reconnected
|
||||
if (!self.isConnected() && self.s.disconnectHandler != null) {
|
||||
return self.s.disconnectHandler.add('auth', db, allArgs, {}, callback);
|
||||
}
|
||||
|
||||
// Set to authenticating
|
||||
this.authenticating = true;
|
||||
// All errors
|
||||
var errors = [];
|
||||
|
||||
// Get all the servers
|
||||
var servers = this.connectedProxies.slice(0);
|
||||
// No servers return
|
||||
if (servers.length === 0) {
|
||||
this.authenticating = false;
|
||||
callback(null, true);
|
||||
}
|
||||
|
||||
// Authenticate
|
||||
function auth(server) {
|
||||
// Arguments without a callback
|
||||
var argsWithoutCallback = [mechanism, db].concat(args.slice(0));
|
||||
// Create arguments
|
||||
var finalArguments = argsWithoutCallback.concat([
|
||||
function(err) {
|
||||
count = count - 1;
|
||||
// Save all the errors
|
||||
if (err) errors.push({ name: server.name, err: err });
|
||||
// We are done
|
||||
if (count === 0) {
|
||||
// Auth is done
|
||||
self.authenticating = false;
|
||||
|
||||
// Return the auth error
|
||||
if (errors.length) {
|
||||
// Remove the entry from the stored authentication contexts
|
||||
self.s.authenticationContexts.splice(currentContextIndex, 0);
|
||||
// Return error
|
||||
return callback(
|
||||
new MongoError({
|
||||
message: 'authentication fail',
|
||||
errors: errors
|
||||
}),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// Successfully authenticated session
|
||||
callback(null, self);
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
// Execute the auth only against non arbiter servers
|
||||
if (!server.lastIsMaster().arbiterOnly) {
|
||||
server.auth.apply(server, finalArguments);
|
||||
}
|
||||
}
|
||||
|
||||
// Save current context index
|
||||
currentContextIndex = this.s.authenticationContexts.length;
|
||||
// Store the auth context and return the last index
|
||||
this.s.authenticationContexts.push([mechanism, db].concat(args.slice(0)));
|
||||
|
||||
// Get total count
|
||||
var count = servers.length;
|
||||
// Authenticate against all servers
|
||||
while (servers.length > 0) {
|
||||
auth(servers.shift());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Logout from a database
|
||||
* @method
|
||||
* @param {string} db The db we are logging out from
|
||||
* @param {authResultCallback} callback A callback function
|
||||
*/
|
||||
Mongos.prototype.logout = function(dbName, callback) {
|
||||
var self = this;
|
||||
// Are we authenticating or logging out, throw
|
||||
if (this.authenticating) {
|
||||
throw new MongoError('authentication or logout allready in process');
|
||||
}
|
||||
|
||||
// Ensure no new members are processed while logging out
|
||||
this.authenticating = true;
|
||||
|
||||
// Remove from all auth providers (avoid any reaplication of the auth details)
|
||||
var providers = Object.keys(this.authProviders);
|
||||
for (var i = 0; i < providers.length; i++) {
|
||||
this.authProviders[providers[i]].logout(dbName);
|
||||
}
|
||||
|
||||
// Now logout all the servers
|
||||
var servers = this.connectedProxies.slice(0);
|
||||
var count = servers.length;
|
||||
if (count === 0) return callback();
|
||||
var errors = [];
|
||||
|
||||
function logoutServer(_server, cb) {
|
||||
_server.logout(dbName, function(err) {
|
||||
if (err) errors.push({ name: _server.name, err: err });
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
// Execute logout on all server instances
|
||||
for (i = 0; i < servers.length; i++) {
|
||||
logoutServer(servers[i], function() {
|
||||
count = count - 1;
|
||||
|
||||
if (count === 0) {
|
||||
// Do not block new operations
|
||||
self.authenticating = false;
|
||||
// If we have one or more errors
|
||||
if (errors.length)
|
||||
return callback(
|
||||
new MongoError({
|
||||
message: f('logout failed against db %s', dbName),
|
||||
errors: errors
|
||||
}),
|
||||
false
|
||||
);
|
||||
|
||||
// No errors
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Selects a server
|
||||
*
|
||||
* @method
|
||||
* @param {function} selector Unused
|
||||
* @param {ReadPreference} [options.readPreference] Specify read preference if command supports it
|
||||
* @param {ReadPreference} [options.readPreference] Unused
|
||||
* @param {ClientSession} [options.session] Specify a session if it is being used
|
||||
* @param {function} callback
|
||||
*/
|
||||
Mongos.prototype.selectServer = function(selector, options, callback) {
|
||||
@@ -1296,7 +1143,7 @@ Mongos.prototype.selectServer = function(selector, options, callback) {
|
||||
(callback = options), (options = selector), (selector = undefined);
|
||||
options = options || {};
|
||||
|
||||
const server = pickProxy(this);
|
||||
const server = pickProxy(this, options.session);
|
||||
if (this.s.debug) this.emit('pickedServer', null, server);
|
||||
callback(null, server);
|
||||
};
|
||||
|
||||
+124
-339
@@ -18,10 +18,7 @@ const SessionMixins = require('./shared').SessionMixins;
|
||||
const isRetryableWritesSupported = require('./shared').isRetryableWritesSupported;
|
||||
const relayEvents = require('../utils').relayEvents;
|
||||
const isRetryableError = require('../error').isRetryableError;
|
||||
|
||||
const defaultAuthProviders = require('../auth/defaultAuthProviders').defaultAuthProviders;
|
||||
|
||||
var BSON = retrieveBSON();
|
||||
const BSON = retrieveBSON();
|
||||
|
||||
//
|
||||
// States
|
||||
@@ -77,7 +74,6 @@ var handlers = ['connect', 'close', 'error', 'timeout', 'parseError'];
|
||||
* @param {boolean} [options.noDelay=true] TCP Connection no delay
|
||||
* @param {number} [options.connectionTimeout=10000] TCP Connection timeout setting
|
||||
* @param {number} [options.socketTimeout=0] TCP Socket timeout setting
|
||||
* @param {boolean} [options.singleBufferSerializtion=true] Serialize into single buffer, trade of peak memory for serialization speed
|
||||
* @param {boolean} [options.ssl=false] Use SSL for connection
|
||||
* @param {boolean|function} [options.checkServerIdentity=true] Ensure we check server identify during SSL, set to false to disable checking. Only works for Node 0.12.x or higher. You can pass in a boolean or your own checkServerIdentity override function.
|
||||
* @param {Buffer} [options.ca] SSL Certificate store binary buffer
|
||||
@@ -191,9 +187,7 @@ var ReplSet = function(seedlist, options) {
|
||||
// Are we running in debug mode
|
||||
debug: typeof options.debug === 'boolean' ? options.debug : false,
|
||||
// Client info
|
||||
clientInfo: createClientInfo(options),
|
||||
// Authentication context
|
||||
authenticationContexts: []
|
||||
clientInfo: createClientInfo(options)
|
||||
};
|
||||
|
||||
// Add handler for topology change
|
||||
@@ -217,9 +211,6 @@ var ReplSet = function(seedlist, options) {
|
||||
);
|
||||
}
|
||||
|
||||
// All the authProviders
|
||||
this.authProviders = options.authProviders || defaultAuthProviders(this.s.bson);
|
||||
|
||||
// Add forwarding of events from state handler
|
||||
var types = ['joined', 'left'];
|
||||
types.forEach(function(x) {
|
||||
@@ -238,8 +229,6 @@ var ReplSet = function(seedlist, options) {
|
||||
// Disconnected state
|
||||
this.state = DISCONNECTED;
|
||||
this.haTimeoutId = null;
|
||||
// Are we authenticating
|
||||
this.authenticating = false;
|
||||
// Last ismaster
|
||||
this.ismaster = null;
|
||||
// Contains the intervalId
|
||||
@@ -301,50 +290,40 @@ function connectNewServers(self, servers, callback) {
|
||||
return this.destroy({ force: true });
|
||||
}
|
||||
|
||||
if (event === 'connect' && !self.authenticating) {
|
||||
if (event === 'connect') {
|
||||
// Destroyed
|
||||
if (self.state === DESTROYED || self.state === UNREFERENCED) {
|
||||
return _self.destroy({ force: true });
|
||||
}
|
||||
|
||||
// Do we have authentication contexts that need to be applied
|
||||
applyAuthenticationContexts(self, _self, function() {
|
||||
// Destroy the instance
|
||||
if (self.state === DESTROYED || self.state === UNREFERENCED) {
|
||||
return _self.destroy({ force: true });
|
||||
// Update the state
|
||||
var result = self.s.replicaSetState.update(_self);
|
||||
// Update the state with the new server
|
||||
if (result) {
|
||||
// Primary lastIsMaster store it
|
||||
if (_self.lastIsMaster() && _self.lastIsMaster().ismaster) {
|
||||
self.ismaster = _self.lastIsMaster();
|
||||
}
|
||||
|
||||
// Update the state
|
||||
var result = self.s.replicaSetState.update(_self);
|
||||
// Update the state with the new server
|
||||
if (result) {
|
||||
// Primary lastIsMaster store it
|
||||
if (_self.lastIsMaster() && _self.lastIsMaster().ismaster) {
|
||||
self.ismaster = _self.lastIsMaster();
|
||||
}
|
||||
|
||||
// Remove the handlers
|
||||
for (var i = 0; i < handlers.length; i++) {
|
||||
_self.removeAllListeners(handlers[i]);
|
||||
}
|
||||
|
||||
// Add stable state handlers
|
||||
_self.on('error', handleEvent(self, 'error'));
|
||||
_self.on('close', handleEvent(self, 'close'));
|
||||
_self.on('timeout', handleEvent(self, 'timeout'));
|
||||
_self.on('parseError', handleEvent(self, 'parseError'));
|
||||
|
||||
// Enalbe the monitoring of the new server
|
||||
monitorServer(_self.lastIsMaster().me, self, {});
|
||||
|
||||
// Rexecute any stalled operation
|
||||
rexecuteOperations(self);
|
||||
} else {
|
||||
_self.destroy({ force: true });
|
||||
// Remove the handlers
|
||||
for (let i = 0; i < handlers.length; i++) {
|
||||
_self.removeAllListeners(handlers[i]);
|
||||
}
|
||||
});
|
||||
} else if (event === 'connect' && self.authenticating) {
|
||||
this.destroy({ force: true });
|
||||
|
||||
// Add stable state handlers
|
||||
_self.on('error', handleEvent(self, 'error'));
|
||||
_self.on('close', handleEvent(self, 'close'));
|
||||
_self.on('timeout', handleEvent(self, 'timeout'));
|
||||
_self.on('parseError', handleEvent(self, 'parseError'));
|
||||
|
||||
// Enalbe the monitoring of the new server
|
||||
monitorServer(_self.lastIsMaster().me, self, {});
|
||||
|
||||
// Rexecute any stalled operation
|
||||
rexecuteOperations(self);
|
||||
} else {
|
||||
_self.destroy({ force: true });
|
||||
}
|
||||
} else if (event === 'error') {
|
||||
error = err;
|
||||
}
|
||||
@@ -375,7 +354,6 @@ function connectNewServers(self, servers, callback) {
|
||||
Object.assign({}, self.s.options, {
|
||||
host: _server.split(':')[0],
|
||||
port: parseInt(_server.split(':')[1], 10),
|
||||
authProviders: self.authProviders,
|
||||
reconnect: false,
|
||||
monitoring: false,
|
||||
parent: self,
|
||||
@@ -726,40 +704,6 @@ function handleEvent(self, event) {
|
||||
};
|
||||
}
|
||||
|
||||
function applyAuthenticationContexts(self, server, callback) {
|
||||
if (self.s.authenticationContexts.length === 0) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
// Do not apply any auth contexts if it's an arbiter
|
||||
if (server.lastIsMaster() && server.lastIsMaster().arbiterOnly) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
// Copy contexts to ensure no modificiation in the middle of
|
||||
// auth process.
|
||||
var authContexts = self.s.authenticationContexts.slice(0);
|
||||
|
||||
// Apply one of the contexts
|
||||
function applyAuth(authContexts, server, callback) {
|
||||
if (authContexts.length === 0) return callback();
|
||||
// Get the first auth context
|
||||
var authContext = authContexts.shift();
|
||||
// Copy the params
|
||||
var customAuthContext = authContext.slice(0);
|
||||
// Push our callback handler
|
||||
customAuthContext.push(function(/* err */) {
|
||||
applyAuth(authContexts, server, callback);
|
||||
});
|
||||
|
||||
// Attempt authentication
|
||||
server.auth.apply(server, customAuthContext);
|
||||
}
|
||||
|
||||
// Apply all auth contexts
|
||||
applyAuth(authContexts, server, callback);
|
||||
}
|
||||
|
||||
function shouldTriggerConnect(self) {
|
||||
const isConnecting = self.state === CONNECTING;
|
||||
const hasPrimary = self.s.replicaSetState.hasPrimary();
|
||||
@@ -798,67 +742,59 @@ function handleInitialConnectEvent(self, event) {
|
||||
|
||||
// Check the type of server
|
||||
if (event === 'connect') {
|
||||
// Do we have authentication contexts that need to be applied
|
||||
applyAuthenticationContexts(self, _this, function() {
|
||||
// Destroy the instance
|
||||
if (self.state === DESTROYED || self.state === UNREFERENCED) {
|
||||
return _this.destroy({ force: true });
|
||||
// Update the state
|
||||
var result = self.s.replicaSetState.update(_this);
|
||||
if (result === true) {
|
||||
// Primary lastIsMaster store it
|
||||
if (_this.lastIsMaster() && _this.lastIsMaster().ismaster) {
|
||||
self.ismaster = _this.lastIsMaster();
|
||||
}
|
||||
|
||||
// Update the state
|
||||
var result = self.s.replicaSetState.update(_this);
|
||||
if (result === true) {
|
||||
// Primary lastIsMaster store it
|
||||
if (_this.lastIsMaster() && _this.lastIsMaster().ismaster) {
|
||||
self.ismaster = _this.lastIsMaster();
|
||||
}
|
||||
|
||||
// Debug log
|
||||
if (self.s.logger.isDebug()) {
|
||||
self.s.logger.debug(
|
||||
f(
|
||||
'handleInitialConnectEvent %s from server %s in replset with id %s has state [%s]',
|
||||
event,
|
||||
_this.name,
|
||||
self.id,
|
||||
JSON.stringify(self.s.replicaSetState.set)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Remove the handlers
|
||||
for (var i = 0; i < handlers.length; i++) {
|
||||
_this.removeAllListeners(handlers[i]);
|
||||
}
|
||||
|
||||
// Add stable state handlers
|
||||
_this.on('error', handleEvent(self, 'error'));
|
||||
_this.on('close', handleEvent(self, 'close'));
|
||||
_this.on('timeout', handleEvent(self, 'timeout'));
|
||||
_this.on('parseError', handleEvent(self, 'parseError'));
|
||||
|
||||
// Do we have a primary or primaryAndSecondary
|
||||
if (shouldTriggerConnect(self)) {
|
||||
// We are connected
|
||||
self.state = CONNECTED;
|
||||
|
||||
// Set initial connect state
|
||||
self.initialConnectState.connect = true;
|
||||
// Emit connect event
|
||||
process.nextTick(function() {
|
||||
self.emit('connect', self);
|
||||
});
|
||||
|
||||
topologyMonitor(self, {});
|
||||
}
|
||||
} else if (result instanceof MongoError) {
|
||||
_this.destroy({ force: true });
|
||||
self.destroy({ force: true });
|
||||
return self.emit('error', result);
|
||||
} else {
|
||||
_this.destroy({ force: true });
|
||||
// Debug log
|
||||
if (self.s.logger.isDebug()) {
|
||||
self.s.logger.debug(
|
||||
f(
|
||||
'handleInitialConnectEvent %s from server %s in replset with id %s has state [%s]',
|
||||
event,
|
||||
_this.name,
|
||||
self.id,
|
||||
JSON.stringify(self.s.replicaSetState.set)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove the handlers
|
||||
for (let i = 0; i < handlers.length; i++) {
|
||||
_this.removeAllListeners(handlers[i]);
|
||||
}
|
||||
|
||||
// Add stable state handlers
|
||||
_this.on('error', handleEvent(self, 'error'));
|
||||
_this.on('close', handleEvent(self, 'close'));
|
||||
_this.on('timeout', handleEvent(self, 'timeout'));
|
||||
_this.on('parseError', handleEvent(self, 'parseError'));
|
||||
|
||||
// Do we have a primary or primaryAndSecondary
|
||||
if (shouldTriggerConnect(self)) {
|
||||
// We are connected
|
||||
self.state = CONNECTED;
|
||||
|
||||
// Set initial connect state
|
||||
self.initialConnectState.connect = true;
|
||||
// Emit connect event
|
||||
process.nextTick(function() {
|
||||
self.emit('connect', self);
|
||||
});
|
||||
|
||||
topologyMonitor(self, {});
|
||||
}
|
||||
} else if (result instanceof MongoError) {
|
||||
_this.destroy({ force: true });
|
||||
self.destroy({ force: true });
|
||||
return self.emit('error', result);
|
||||
} else {
|
||||
_this.destroy({ force: true });
|
||||
}
|
||||
} else {
|
||||
// Emit failure to connect
|
||||
self.emit('failed', this);
|
||||
@@ -953,20 +889,19 @@ function emitSDAMEvent(self, event, description) {
|
||||
|
||||
/**
|
||||
* Initiate server connect
|
||||
* @method
|
||||
* @param {array} [options.auth=null] Array of auth options to apply on connect
|
||||
*/
|
||||
ReplSet.prototype.connect = function(options) {
|
||||
var self = this;
|
||||
// Add any connect level options to the internal state
|
||||
this.s.connectOptions = options || {};
|
||||
|
||||
// Set connecting state
|
||||
stateTransition(this, CONNECTING);
|
||||
|
||||
// Create server instances
|
||||
var servers = this.s.seedlist.map(function(x) {
|
||||
return new Server(
|
||||
Object.assign({}, self.s.options, x, {
|
||||
authProviders: self.authProviders,
|
||||
Object.assign({}, self.s.options, x, options, {
|
||||
reconnect: false,
|
||||
monitoring: false,
|
||||
parent: self,
|
||||
@@ -998,26 +933,44 @@ ReplSet.prototype.connect = function(options) {
|
||||
connectServers(self, servers);
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticate the topology.
|
||||
* @method
|
||||
* @param {MongoCredentials} credentials The credentials for authentication we are using
|
||||
* @param {authResultCallback} callback A callback function
|
||||
*/
|
||||
ReplSet.prototype.auth = function(credentials, callback) {
|
||||
if (typeof callback === 'function') callback(null, null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroy the server connection
|
||||
* @param {boolean} [options.force=false] Force destroy the pool
|
||||
* @method
|
||||
*/
|
||||
ReplSet.prototype.destroy = function(options) {
|
||||
ReplSet.prototype.destroy = function(options, callback) {
|
||||
options = options || {};
|
||||
// Transition state
|
||||
stateTransition(this, DESTROYED);
|
||||
|
||||
let destroyCount = this.s.connectingServers.length + 1; // +1 for the callback from `replicaSetState.destroy`
|
||||
const serverDestroyed = () => {
|
||||
destroyCount--;
|
||||
if (destroyCount > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Emit toplogy closing event
|
||||
emitSDAMEvent(this, 'topologyClosed', { topologyId: this.id });
|
||||
|
||||
// Transition state
|
||||
stateTransition(this, DESTROYED);
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback(null, null);
|
||||
}
|
||||
};
|
||||
|
||||
// Clear out any monitoring process
|
||||
if (this.haTimeoutId) clearTimeout(this.haTimeoutId);
|
||||
// Destroy the replicaset
|
||||
this.s.replicaSetState.destroy(options);
|
||||
// Clear out authentication contexts
|
||||
this.s.authenticationContexts = [];
|
||||
|
||||
// Destroy all connecting servers
|
||||
this.s.connectingServers.forEach(function(x) {
|
||||
x.destroy(options);
|
||||
});
|
||||
|
||||
// Clear out all monitoring
|
||||
for (var i = 0; i < this.intervalIds.length; i++) {
|
||||
@@ -1027,8 +980,18 @@ ReplSet.prototype.destroy = function(options) {
|
||||
// Reset list of intervalIds
|
||||
this.intervalIds = [];
|
||||
|
||||
// Emit toplogy closing event
|
||||
emitSDAMEvent(this, 'topologyClosed', { topologyId: this.id });
|
||||
if (destroyCount === 0) {
|
||||
serverDestroyed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Destroy the replicaset
|
||||
this.s.replicaSetState.destroy(options, serverDestroyed);
|
||||
|
||||
// Destroy all connecting servers
|
||||
this.s.connectingServers.forEach(function(x) {
|
||||
x.destroy(options, serverDestroyed);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1091,10 +1054,6 @@ ReplSet.prototype.connections = function() {
|
||||
ReplSet.prototype.isConnected = function(options) {
|
||||
options = options || {};
|
||||
|
||||
// If we are authenticating signal not connected
|
||||
// To avoid interleaving of operations
|
||||
if (this.authenticating) return false;
|
||||
|
||||
// If we specified a read preference check if we are connected to something
|
||||
// than can satisfy this
|
||||
if (options.readPreference && options.readPreference.equals(ReadPreference.secondary)) {
|
||||
@@ -1135,6 +1094,7 @@ ReplSet.prototype.isDestroyed = function() {
|
||||
* @method
|
||||
* @param {function} selector Unused
|
||||
* @param {ReadPreference} [options.readPreference] Specify read preference if command supports it
|
||||
* @param {ClientSession} [options.session] Unused
|
||||
* @param {function} callback
|
||||
*/
|
||||
ReplSet.prototype.selectServer = function(selector, options, callback) {
|
||||
@@ -1381,181 +1341,6 @@ ReplSet.prototype.command = function(ns, cmd, options, callback) {
|
||||
server.command(ns, cmd, options, cb);
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticate using a specified mechanism
|
||||
* @method
|
||||
* @param {string} mechanism The Auth mechanism we are invoking
|
||||
* @param {string} db The db we are invoking the mechanism against
|
||||
* @param {...object} param Parameters for the specific mechanism
|
||||
* @param {authResultCallback} callback A callback function
|
||||
*/
|
||||
ReplSet.prototype.auth = function(mechanism, db) {
|
||||
var allArgs = Array.prototype.slice.call(arguments, 0).slice(0);
|
||||
var self = this;
|
||||
var args = Array.prototype.slice.call(arguments, 2);
|
||||
var callback = args.pop();
|
||||
var currentContextIndex = 0;
|
||||
|
||||
// If we don't have the mechanism fail
|
||||
if (this.authProviders[mechanism] == null && mechanism !== 'default') {
|
||||
return callback(new MongoError(f('auth provider %s does not exist', mechanism)));
|
||||
}
|
||||
|
||||
// Are we already authenticating, throw
|
||||
if (this.authenticating) {
|
||||
return callback(new MongoError('authentication or logout allready in process'));
|
||||
}
|
||||
|
||||
// Topology is not connected, save the call in the provided store to be
|
||||
// Executed at some point when the handler deems it's reconnected
|
||||
if (!this.isConnected() && self.s.disconnectHandler != null) {
|
||||
if (!self.s.replicaSetState.hasPrimary() && !self.s.options.secondaryOnlyConnectionAllowed) {
|
||||
return self.s.disconnectHandler.add('auth', db, allArgs, {}, callback);
|
||||
} else if (
|
||||
!self.s.replicaSetState.hasSecondary() &&
|
||||
self.s.options.secondaryOnlyConnectionAllowed
|
||||
) {
|
||||
return self.s.disconnectHandler.add('auth', db, allArgs, {}, callback);
|
||||
}
|
||||
}
|
||||
|
||||
// Set to authenticating
|
||||
this.authenticating = true;
|
||||
// All errors
|
||||
var errors = [];
|
||||
|
||||
// Get all the servers
|
||||
var servers = this.s.replicaSetState.allServers();
|
||||
// No servers return
|
||||
if (servers.length === 0) {
|
||||
this.authenticating = false;
|
||||
callback(null, true);
|
||||
}
|
||||
|
||||
// Authenticate
|
||||
function auth(server) {
|
||||
// Arguments without a callback
|
||||
var argsWithoutCallback = [mechanism, db].concat(args.slice(0));
|
||||
// Create arguments
|
||||
var finalArguments = argsWithoutCallback.concat([
|
||||
function(err) {
|
||||
count = count - 1;
|
||||
// Save all the errors
|
||||
if (err) errors.push({ name: server.name, err: err });
|
||||
// We are done
|
||||
if (count === 0) {
|
||||
// Auth is done
|
||||
self.authenticating = false;
|
||||
|
||||
// Return the auth error
|
||||
if (errors.length) {
|
||||
// Remove the entry from the stored authentication contexts
|
||||
self.s.authenticationContexts.splice(currentContextIndex, 0);
|
||||
// Return error
|
||||
return callback(
|
||||
new MongoError({
|
||||
message: 'authentication fail',
|
||||
errors: errors
|
||||
}),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// Successfully authenticated session
|
||||
callback(null, self);
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
if (!server.lastIsMaster().arbiterOnly) {
|
||||
// Execute the auth only against non arbiter servers
|
||||
server.auth.apply(server, finalArguments);
|
||||
} else {
|
||||
// If we are authenticating against an arbiter just ignore it
|
||||
finalArguments.pop()(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Get total count
|
||||
var count = servers.length;
|
||||
|
||||
// Save current context index
|
||||
currentContextIndex = this.s.authenticationContexts.length;
|
||||
|
||||
// Store the auth context and return the last index
|
||||
this.s.authenticationContexts.push([mechanism, db].concat(args.slice(0)));
|
||||
|
||||
// Authenticate against all servers
|
||||
while (servers.length > 0) {
|
||||
auth(servers.shift());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Logout from a database
|
||||
* @method
|
||||
* @param {string} db The db we are logging out from
|
||||
* @param {authResultCallback} callback A callback function
|
||||
*/
|
||||
ReplSet.prototype.logout = function(dbName, callback) {
|
||||
var self = this;
|
||||
// Are we authenticating or logging out, throw
|
||||
if (this.authenticating) {
|
||||
throw new MongoError('authentication or logout allready in process');
|
||||
}
|
||||
|
||||
// Ensure no new members are processed while logging out
|
||||
this.authenticating = true;
|
||||
|
||||
// Remove from all auth providers (avoid any reaplication of the auth details)
|
||||
var providers = Object.keys(this.authProviders);
|
||||
for (var i = 0; i < providers.length; i++) {
|
||||
this.authProviders[providers[i]].logout(dbName);
|
||||
}
|
||||
|
||||
// Clear out any contexts associated with the db
|
||||
self.s.authenticationContexts = self.s.authenticationContexts.filter(function(context) {
|
||||
return context[1] !== dbName;
|
||||
});
|
||||
|
||||
// Now logout all the servers
|
||||
var servers = this.s.replicaSetState.allServers();
|
||||
var count = servers.length;
|
||||
if (count === 0) return callback();
|
||||
var errors = [];
|
||||
|
||||
function logoutServer(_server, cb) {
|
||||
_server.logout(dbName, function(err) {
|
||||
if (err) errors.push({ name: _server.name, err: err });
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
// Execute logout on all server instances
|
||||
for (i = 0; i < servers.length; i++) {
|
||||
logoutServer(servers[i], function() {
|
||||
count = count - 1;
|
||||
|
||||
if (count === 0) {
|
||||
// Do not block new operations
|
||||
self.authenticating = false;
|
||||
// If we have one or more errors
|
||||
if (errors.length)
|
||||
return callback(
|
||||
new MongoError({
|
||||
message: f('logout failed against db %s', dbName),
|
||||
errors: errors
|
||||
}),
|
||||
false
|
||||
);
|
||||
|
||||
// No errors
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a new cursor
|
||||
* @method
|
||||
|
||||
+65
-43
@@ -116,31 +116,43 @@ ReplSetState.prototype.allServers = function(options) {
|
||||
return servers;
|
||||
};
|
||||
|
||||
ReplSetState.prototype.destroy = function(options) {
|
||||
// Destroy all sockets
|
||||
if (this.primary) this.primary.destroy(options);
|
||||
this.secondaries.forEach(function(x) {
|
||||
x.destroy(options);
|
||||
});
|
||||
this.arbiters.forEach(function(x) {
|
||||
x.destroy(options);
|
||||
});
|
||||
this.passives.forEach(function(x) {
|
||||
x.destroy(options);
|
||||
});
|
||||
this.ghosts.forEach(function(x) {
|
||||
x.destroy(options);
|
||||
});
|
||||
// Clear out the complete state
|
||||
this.secondaries = [];
|
||||
this.arbiters = [];
|
||||
this.passives = [];
|
||||
this.ghosts = [];
|
||||
this.unknownServers = [];
|
||||
this.set = {};
|
||||
this.primary = null;
|
||||
// Emit the topology changed
|
||||
emitTopologyDescriptionChanged(this);
|
||||
ReplSetState.prototype.destroy = function(options, callback) {
|
||||
const serversToDestroy = this.secondaries
|
||||
.concat(this.arbiters)
|
||||
.concat(this.passives)
|
||||
.concat(this.ghosts);
|
||||
if (this.primary) serversToDestroy.push(this.primary);
|
||||
|
||||
let serverCount = serversToDestroy.length;
|
||||
const serverDestroyed = () => {
|
||||
serverCount--;
|
||||
if (serverCount > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear out the complete state
|
||||
this.secondaries = [];
|
||||
this.arbiters = [];
|
||||
this.passives = [];
|
||||
this.ghosts = [];
|
||||
this.unknownServers = [];
|
||||
this.set = {};
|
||||
this.primary = null;
|
||||
|
||||
// Emit the topology changed
|
||||
emitTopologyDescriptionChanged(this);
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback(null, null);
|
||||
}
|
||||
};
|
||||
|
||||
if (serverCount === 0) {
|
||||
serverDestroyed();
|
||||
return;
|
||||
}
|
||||
|
||||
serversToDestroy.forEach(server => server.destroy(options, serverDestroyed));
|
||||
};
|
||||
|
||||
ReplSetState.prototype.remove = function(server, options) {
|
||||
@@ -287,6 +299,34 @@ ReplSetState.prototype.update = function(server) {
|
||||
// Is this a mongos
|
||||
//
|
||||
if (ismaster && ismaster.msg === 'isdbgrid') {
|
||||
if (this.primary && this.primary.name === serverName) {
|
||||
this.primary = null;
|
||||
this.topologyType = TopologyType.ReplicaSetNoPrimary;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// A RSGhost instance
|
||||
if (ismaster.isreplicaset) {
|
||||
self.set[serverName] = {
|
||||
type: ServerType.RSGhost,
|
||||
setVersion: null,
|
||||
electionId: null,
|
||||
setName: ismaster.setName
|
||||
};
|
||||
|
||||
if (this.primary && this.primary.name === serverName) {
|
||||
this.primary = null;
|
||||
}
|
||||
|
||||
// Set the topology
|
||||
this.topologyType = this.primary
|
||||
? TopologyType.ReplicaSetWithPrimary
|
||||
: TopologyType.ReplicaSetNoPrimary;
|
||||
if (ismaster.setName) this.setName = ismaster.setName;
|
||||
|
||||
// Set the topology
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -305,30 +345,12 @@ ReplSetState.prototype.update = function(server) {
|
||||
electionId: null,
|
||||
setName: ismaster.setName
|
||||
};
|
||||
// Set the topology
|
||||
this.topologyType = this.primary
|
||||
? TopologyType.ReplicaSetWithPrimary
|
||||
: TopologyType.ReplicaSetNoPrimary;
|
||||
if (ismaster.setName) this.setName = ismaster.setName;
|
||||
return false;
|
||||
}
|
||||
|
||||
// A RSGhost instance
|
||||
if (ismaster.isreplicaset) {
|
||||
self.set[serverName] = {
|
||||
type: ServerType.RSGhost,
|
||||
setVersion: null,
|
||||
electionId: null,
|
||||
setName: null
|
||||
};
|
||||
|
||||
// Set the topology
|
||||
this.topologyType = this.primary
|
||||
? TopologyType.ReplicaSetWithPrimary
|
||||
: TopologyType.ReplicaSetNoPrimary;
|
||||
if (ismaster.setName) this.setName = ismaster.setName;
|
||||
|
||||
// Set the topology
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
+87
-281
@@ -8,11 +8,9 @@ var inherits = require('util').inherits,
|
||||
debugOptions = require('../connection/utils').debugOptions,
|
||||
retrieveBSON = require('../connection/utils').retrieveBSON,
|
||||
Pool = require('../connection/pool'),
|
||||
Query = require('../connection/commands').Query,
|
||||
MongoError = require('../error').MongoError,
|
||||
MongoNetworkError = require('../error').MongoNetworkError,
|
||||
TwoSixWireProtocolSupport = require('../wireprotocol/2_6_support'),
|
||||
ThreeTwoWireProtocolSupport = require('../wireprotocol/3_2_support'),
|
||||
wireProtocol = require('../wireprotocol'),
|
||||
BasicCursor = require('../cursor'),
|
||||
sdam = require('./shared'),
|
||||
createClientInfo = require('./shared').createClientInfo,
|
||||
@@ -23,57 +21,6 @@ var inherits = require('util').inherits,
|
||||
|
||||
const collationNotSupported = require('../utils').collationNotSupported;
|
||||
|
||||
function getSaslSupportedMechs(options) {
|
||||
if (!options) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const authArray = options.auth || [];
|
||||
const authMechanism = authArray[0] || options.authMechanism;
|
||||
const authSource = authArray[1] || options.authSource || options.dbName || 'admin';
|
||||
const user = authArray[2] || options.user;
|
||||
|
||||
if (typeof authMechanism === 'string' && authMechanism.toUpperCase() !== 'DEFAULT') {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return { saslSupportedMechs: `${authSource}.${user}` };
|
||||
}
|
||||
|
||||
function getDefaultAuthMechanism(ismaster) {
|
||||
if (ismaster) {
|
||||
// If ismaster contains saslSupportedMechs, use scram-sha-256
|
||||
// if it is available, else scram-sha-1
|
||||
if (Array.isArray(ismaster.saslSupportedMechs)) {
|
||||
return ismaster.saslSupportedMechs.indexOf('SCRAM-SHA-256') >= 0
|
||||
? 'scram-sha-256'
|
||||
: 'scram-sha-1';
|
||||
}
|
||||
|
||||
// Fallback to legacy selection method. If wire version >= 3, use scram-sha-1
|
||||
if (ismaster.maxWireVersion >= 3) {
|
||||
return 'scram-sha-1';
|
||||
}
|
||||
}
|
||||
|
||||
// Default for wireprotocol < 3
|
||||
return 'mongocr';
|
||||
}
|
||||
|
||||
function extractIsMasterError(err, result) {
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
|
||||
if (result && result.result && result.result.ok === 0) {
|
||||
return new MongoError(result.result);
|
||||
}
|
||||
}
|
||||
|
||||
// Used for filtering out fields for loggin
|
||||
var debugFields = [
|
||||
'reconnect',
|
||||
@@ -90,7 +37,6 @@ var debugFields = [
|
||||
'connectionTimeout',
|
||||
'checkServerIdentity',
|
||||
'socketTimeout',
|
||||
'singleBufferSerializtion',
|
||||
'ssl',
|
||||
'ca',
|
||||
'crl',
|
||||
@@ -225,9 +171,6 @@ var Server = function(options) {
|
||||
this.monitoringProcessId = null;
|
||||
// Initial connection
|
||||
this.initialConnect = true;
|
||||
// Wire protocol handler, default to oldest known protocol handler
|
||||
// this gets changed when the first ismaster is called.
|
||||
this.wireProtocolHandler = new TwoSixWireProtocolSupport();
|
||||
// Default type
|
||||
this._type = 'server';
|
||||
// Set the client info
|
||||
@@ -303,20 +246,6 @@ Object.defineProperty(Server.prototype, 'name', {
|
||||
}
|
||||
});
|
||||
|
||||
function isSupportedServer(response) {
|
||||
return response && typeof response.maxWireVersion === 'number' && response.maxWireVersion >= 2;
|
||||
}
|
||||
|
||||
function configureWireProtocolHandler(self, ismaster) {
|
||||
// 3.2 wire protocol handler
|
||||
if (ismaster.maxWireVersion >= 4) {
|
||||
return new ThreeTwoWireProtocolSupport();
|
||||
}
|
||||
|
||||
// default to 2.6 wire protocol handler
|
||||
return new TwoSixWireProtocolSupport();
|
||||
}
|
||||
|
||||
function disconnectHandler(self, type, ns, cmd, options, callback) {
|
||||
// Topology is not connected, save the call in the provided store to be
|
||||
// Executed at some point when the handler deems it's reconnected
|
||||
@@ -344,16 +273,13 @@ function monitoringProcess(self) {
|
||||
// Emit monitoring Process event
|
||||
self.emit('monitoring', self);
|
||||
// Perform ismaster call
|
||||
// Query options
|
||||
var queryOptions = { numberToSkip: 0, numberToReturn: -1, checkKeys: false, slaveOk: true };
|
||||
// Create a query instance
|
||||
var query = new Query(self.s.bson, 'admin.$cmd', { ismaster: true }, queryOptions);
|
||||
// Get start time
|
||||
var start = new Date().getTime();
|
||||
|
||||
// Execute the ismaster query
|
||||
self.s.pool.write(
|
||||
query,
|
||||
self.command(
|
||||
'admin.$cmd',
|
||||
{ ismaster: true },
|
||||
{
|
||||
socketTimeout:
|
||||
typeof self.s.options.connectionTimeout !== 'number'
|
||||
@@ -361,7 +287,7 @@ function monitoringProcess(self) {
|
||||
: self.s.options.connectionTimeout,
|
||||
monitoring: true
|
||||
},
|
||||
function(err, result) {
|
||||
(err, result) => {
|
||||
// Set initial lastIsMasterMS
|
||||
self.lastIsMasterMS = new Date().getTime() - start;
|
||||
if (self.s.pool.isDestroyed()) return;
|
||||
@@ -377,7 +303,7 @@ function monitoringProcess(self) {
|
||||
}
|
||||
|
||||
var eventHandler = function(self, event) {
|
||||
return function(err) {
|
||||
return function(err, conn) {
|
||||
// Log information of received information if in info mode
|
||||
if (self.s.logger.isInfo()) {
|
||||
var object = err instanceof MongoError ? JSON.stringify(err) : {};
|
||||
@@ -388,126 +314,67 @@ var eventHandler = function(self, event) {
|
||||
|
||||
// Handle connect event
|
||||
if (event === 'connect') {
|
||||
// Issue an ismaster command at connect
|
||||
// Query options
|
||||
var queryOptions = { numberToSkip: 0, numberToReturn: -1, checkKeys: false, slaveOk: true };
|
||||
// Create a query instance
|
||||
var compressors =
|
||||
self.s.compression && self.s.compression.compressors ? self.s.compression.compressors : [];
|
||||
var query = new Query(
|
||||
self.s.bson,
|
||||
'admin.$cmd',
|
||||
Object.assign(
|
||||
{ ismaster: true, client: self.clientInfo, compression: compressors },
|
||||
getSaslSupportedMechs(self.s.options)
|
||||
),
|
||||
queryOptions
|
||||
);
|
||||
// Get start time
|
||||
var start = new Date().getTime();
|
||||
// Execute the ismaster query
|
||||
self.s.pool.write(
|
||||
query,
|
||||
{
|
||||
socketTimeout: self.s.options.connectionTimeout || 2000
|
||||
},
|
||||
function(err, result) {
|
||||
// Set initial lastIsMasterMS
|
||||
self.lastIsMasterMS = new Date().getTime() - start;
|
||||
self.initialConnect = false;
|
||||
self.ismaster = conn.ismaster;
|
||||
self.lastIsMasterMS = conn.lastIsMasterMS;
|
||||
if (conn.agreedCompressor) {
|
||||
self.s.pool.options.agreedCompressor = conn.agreedCompressor;
|
||||
}
|
||||
|
||||
const serverError = extractIsMasterError(err, result);
|
||||
if (conn.zlibCompressionLevel) {
|
||||
self.s.pool.options.zlibCompressionLevel = conn.zlibCompressionLevel;
|
||||
}
|
||||
|
||||
if (serverError) {
|
||||
self.destroy();
|
||||
return self.emit('error', serverError);
|
||||
}
|
||||
if (conn.ismaster.$clusterTime) {
|
||||
const $clusterTime = conn.ismaster.$clusterTime;
|
||||
self.clusterTime = $clusterTime;
|
||||
}
|
||||
|
||||
if (!isSupportedServer(result.result)) {
|
||||
self.destroy();
|
||||
const latestSupportedVersion = '2.6';
|
||||
const message =
|
||||
'Server at ' +
|
||||
self.s.options.host +
|
||||
':' +
|
||||
self.s.options.port +
|
||||
' reports wire version ' +
|
||||
(result.result.maxWireVersion || 0) +
|
||||
', but this version of Node.js Driver requires at least 2 (MongoDB' +
|
||||
latestSupportedVersion +
|
||||
').';
|
||||
return self.emit('error', new MongoError(message), self);
|
||||
}
|
||||
// It's a proxy change the type so
|
||||
// the wireprotocol will send $readPreference
|
||||
if (self.ismaster.msg === 'isdbgrid') {
|
||||
self._type = 'mongos';
|
||||
}
|
||||
|
||||
// Determine whether the server is instructing us to use a compressor
|
||||
if (result.result && result.result.compression) {
|
||||
for (var i = 0; i < self.s.compression.compressors.length; i++) {
|
||||
if (result.result.compression.indexOf(self.s.compression.compressors[i]) > -1) {
|
||||
self.s.pool.options.agreedCompressor = self.s.compression.compressors[i];
|
||||
break;
|
||||
}
|
||||
// Have we defined self monitoring
|
||||
if (self.s.monitoring) {
|
||||
self.monitoringProcessId = setTimeout(monitoringProcess(self), self.s.monitoringInterval);
|
||||
}
|
||||
|
||||
// Emit server description changed if something listening
|
||||
sdam.emitServerDescriptionChanged(self, {
|
||||
address: self.name,
|
||||
arbiters: [],
|
||||
hosts: [],
|
||||
passives: [],
|
||||
type: sdam.getTopologyType(self)
|
||||
});
|
||||
|
||||
if (!self.s.inTopology) {
|
||||
// Emit topology description changed if something listening
|
||||
sdam.emitTopologyDescriptionChanged(self, {
|
||||
topologyType: 'Single',
|
||||
servers: [
|
||||
{
|
||||
address: self.name,
|
||||
arbiters: [],
|
||||
hosts: [],
|
||||
passives: [],
|
||||
type: sdam.getTopologyType(self)
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
if (self.s.compression.zlibCompressionLevel) {
|
||||
self.s.pool.options.zlibCompressionLevel = self.s.compression.zlibCompressionLevel;
|
||||
}
|
||||
}
|
||||
// Log the ismaster if available
|
||||
if (self.s.logger.isInfo()) {
|
||||
self.s.logger.info(
|
||||
f('server %s connected with ismaster [%s]', self.name, JSON.stringify(self.ismaster))
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure no error emitted after initial connect when reconnecting
|
||||
self.initialConnect = false;
|
||||
// Save the ismaster
|
||||
self.ismaster = result.result;
|
||||
|
||||
// It's a proxy change the type so
|
||||
// the wireprotocol will send $readPreference
|
||||
if (self.ismaster.msg === 'isdbgrid') {
|
||||
self._type = 'mongos';
|
||||
}
|
||||
// Add the correct wire protocol handler
|
||||
self.wireProtocolHandler = configureWireProtocolHandler(self, self.ismaster);
|
||||
// Have we defined self monitoring
|
||||
if (self.s.monitoring) {
|
||||
self.monitoringProcessId = setTimeout(
|
||||
monitoringProcess(self),
|
||||
self.s.monitoringInterval
|
||||
);
|
||||
}
|
||||
|
||||
// Emit server description changed if something listening
|
||||
sdam.emitServerDescriptionChanged(self, {
|
||||
address: self.name,
|
||||
arbiters: [],
|
||||
hosts: [],
|
||||
passives: [],
|
||||
type: sdam.getTopologyType(self)
|
||||
});
|
||||
|
||||
if (!self.s.inTopology) {
|
||||
// Emit topology description changed if something listening
|
||||
sdam.emitTopologyDescriptionChanged(self, {
|
||||
topologyType: 'Single',
|
||||
servers: [
|
||||
{
|
||||
address: self.name,
|
||||
arbiters: [],
|
||||
hosts: [],
|
||||
passives: [],
|
||||
type: sdam.getTopologyType(self)
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// Log the ismaster if available
|
||||
if (self.s.logger.isInfo()) {
|
||||
self.s.logger.info(
|
||||
f('server %s connected with ismaster [%s]', self.name, JSON.stringify(self.ismaster))
|
||||
);
|
||||
}
|
||||
|
||||
// Emit connect
|
||||
self.emit('connect', self);
|
||||
}
|
||||
);
|
||||
// Emit connect
|
||||
self.emit('connect', self);
|
||||
} else if (
|
||||
event === 'error' ||
|
||||
event === 'parseError' ||
|
||||
@@ -554,7 +421,7 @@ var eventHandler = function(self, event) {
|
||||
|
||||
// On first connect fail
|
||||
if (
|
||||
self.s.pool.state === 'disconnected' &&
|
||||
['disconnected', 'connecting'].indexOf(self.s.pool.state) !== -1 &&
|
||||
self.initialConnect &&
|
||||
['close', 'timeout', 'error', 'parseError'].indexOf(event) !== -1
|
||||
) {
|
||||
@@ -589,8 +456,6 @@ var eventHandler = function(self, event) {
|
||||
|
||||
/**
|
||||
* Initiate server connect
|
||||
* @method
|
||||
* @param {array} [options.auth=null] Array of auth options to apply on connect
|
||||
*/
|
||||
Server.prototype.connect = function(options) {
|
||||
var self = this;
|
||||
@@ -630,12 +495,17 @@ Server.prototype.connect = function(options) {
|
||||
address: self.name
|
||||
});
|
||||
|
||||
// Connect with optional auth settings
|
||||
if (options.auth) {
|
||||
self.s.pool.connect.apply(self.s.pool, options.auth);
|
||||
} else {
|
||||
self.s.pool.connect();
|
||||
}
|
||||
self.s.pool.connect();
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticate the topology.
|
||||
* @method
|
||||
* @param {MongoCredentials} credentials The credentials for authentication we are using
|
||||
* @param {authResultCallback} callback A callback function
|
||||
*/
|
||||
Server.prototype.auth = function(credentials, callback) {
|
||||
if (typeof callback === 'function') callback(null, null);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -755,33 +625,7 @@ Server.prototype.command = function(ns, cmd, options, callback) {
|
||||
return callback(new MongoError(`server ${this.name} does not support collation`));
|
||||
}
|
||||
|
||||
// Are we executing against a specific topology
|
||||
var topology = options.topology || {};
|
||||
// Create the query object
|
||||
var query = self.wireProtocolHandler.command(self.s.bson, ns, cmd, {}, topology, options);
|
||||
if (query instanceof MongoError) {
|
||||
return callback(query, null);
|
||||
}
|
||||
|
||||
// Set slave OK of the query
|
||||
query.slaveOk = options.readPreference ? options.readPreference.slaveOk() : false;
|
||||
|
||||
// Write options
|
||||
var writeOptions = {
|
||||
raw: typeof options.raw === 'boolean' ? options.raw : false,
|
||||
promoteLongs: typeof options.promoteLongs === 'boolean' ? options.promoteLongs : true,
|
||||
promoteValues: typeof options.promoteValues === 'boolean' ? options.promoteValues : true,
|
||||
promoteBuffers: typeof options.promoteBuffers === 'boolean' ? options.promoteBuffers : false,
|
||||
command: true,
|
||||
monitoring: typeof options.monitoring === 'boolean' ? options.monitoring : false,
|
||||
fullResult: typeof options.fullResult === 'boolean' ? options.fullResult : false,
|
||||
requestId: query.requestId,
|
||||
socketTimeout: typeof options.socketTimeout === 'number' ? options.socketTimeout : null,
|
||||
session: options.session || null
|
||||
};
|
||||
|
||||
// Write the operation to the pool
|
||||
self.s.pool.write(query, writeOptions, callback);
|
||||
wireProtocol.command(self, ns, cmd, options, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -812,7 +656,7 @@ Server.prototype.insert = function(ns, ops, options, callback) {
|
||||
ops = Array.isArray(ops) ? ops : [ops];
|
||||
|
||||
// Execute write
|
||||
return self.wireProtocolHandler.insert(self.s.pool, ns, self.s.bson, ops, options, callback);
|
||||
return wireProtocol.insert(self, ns, ops, options, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -847,7 +691,7 @@ Server.prototype.update = function(ns, ops, options, callback) {
|
||||
// Setup the docs as an array
|
||||
ops = Array.isArray(ops) ? ops : [ops];
|
||||
// Execute write
|
||||
return self.wireProtocolHandler.update(self.s.pool, ns, self.s.bson, ops, options, callback);
|
||||
return wireProtocol.update(self, ns, ops, options, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -882,7 +726,7 @@ Server.prototype.remove = function(ns, ops, options, callback) {
|
||||
// Setup the docs as an array
|
||||
ops = Array.isArray(ops) ? ops : [ops];
|
||||
// Execute write
|
||||
return self.wireProtocolHandler.remove(self.s.pool, ns, self.s.bson, ops, options, callback);
|
||||
return wireProtocol.remove(self, ns, ops, options, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -911,52 +755,6 @@ Server.prototype.cursor = function(ns, cmd, options) {
|
||||
return new FinalCursor(this.s.bson, ns, cmd, options, topology, this.s.options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Logout from a database
|
||||
* @method
|
||||
* @param {string} db The db we are logging out from
|
||||
* @param {authResultCallback} callback A callback function
|
||||
*/
|
||||
Server.prototype.logout = function(dbName, callback) {
|
||||
this.s.pool.logout(dbName, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticate using a specified mechanism
|
||||
* @method
|
||||
* @param {string} mechanism The Auth mechanism we are invoking
|
||||
* @param {string} db The db we are invoking the mechanism against
|
||||
* @param {...object} param Parameters for the specific mechanism
|
||||
* @param {authResultCallback} callback A callback function
|
||||
*/
|
||||
Server.prototype.auth = function(mechanism, db) {
|
||||
var self = this;
|
||||
|
||||
if (mechanism === 'default') {
|
||||
mechanism = getDefaultAuthMechanism(self.ismaster);
|
||||
}
|
||||
|
||||
// Slice all the arguments off
|
||||
var args = Array.prototype.slice.call(arguments, 0);
|
||||
// Set the mechanism
|
||||
args[0] = mechanism;
|
||||
// Get the callback
|
||||
var callback = args[args.length - 1];
|
||||
|
||||
// If we are not connected or have a disconnectHandler specified
|
||||
if (disconnectHandler(self, 'auth', db, args, {}, callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not authenticate if we are an arbiter
|
||||
if (this.lastIsMaster() && this.lastIsMaster().arbiterOnly) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
// Apply the arguments to the pool
|
||||
self.s.pool.auth.apply(self.s.pool, args);
|
||||
};
|
||||
|
||||
/**
|
||||
* Compare two server instances
|
||||
* @method
|
||||
@@ -980,6 +778,10 @@ Server.prototype.connections = function() {
|
||||
|
||||
/**
|
||||
* Selects a server
|
||||
* @method
|
||||
* @param {function} selector Unused
|
||||
* @param {ReadPreference} [options.readPreference] Unused
|
||||
* @param {ClientSession} [options.session] Unused
|
||||
* @return {Server}
|
||||
*/
|
||||
Server.prototype.selectServer = function(selector, options, callback) {
|
||||
@@ -1000,8 +802,11 @@ var listeners = ['close', 'error', 'timeout', 'parseError', 'connect'];
|
||||
* @param {boolean} [options.emitDestroy=false] Emit destroy event on destroy
|
||||
* @param {boolean} [options.force=false] Force destroy the pool
|
||||
*/
|
||||
Server.prototype.destroy = function(options) {
|
||||
if (this._destroyed) return;
|
||||
Server.prototype.destroy = function(options, callback) {
|
||||
if (this._destroyed) {
|
||||
if (typeof callback === 'function') callback(null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
var self = this;
|
||||
@@ -1017,6 +822,7 @@ Server.prototype.destroy = function(options) {
|
||||
// No pool, return
|
||||
if (!self.s.pool) {
|
||||
this._destroyed = true;
|
||||
if (typeof callback === 'function') callback(null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1052,7 +858,7 @@ Server.prototype.destroy = function(options) {
|
||||
}
|
||||
|
||||
// Destroy the pool
|
||||
this.s.pool.destroy(options.force);
|
||||
this.s.pool.destroy(options.force, callback);
|
||||
this._destroyed = true;
|
||||
};
|
||||
|
||||
|
||||
+19
@@ -4,6 +4,7 @@ const os = require('os');
|
||||
const f = require('util').format;
|
||||
const ReadPreference = require('./read_preference');
|
||||
const Buffer = require('safe-buffer').Buffer;
|
||||
const TopologyType = require('../sdam/topology_description').TopologyType;
|
||||
|
||||
/**
|
||||
* Emit event if it exists
|
||||
@@ -398,6 +399,20 @@ const SessionMixins = {
|
||||
}
|
||||
};
|
||||
|
||||
function topologyType(topology) {
|
||||
if (topology.description) {
|
||||
return topology.description.type;
|
||||
}
|
||||
|
||||
if (topology.type === 'mongos') {
|
||||
return TopologyType.Sharded;
|
||||
} else if (topology.type === 'replset') {
|
||||
return TopologyType.ReplicaSetWithPrimary;
|
||||
}
|
||||
|
||||
return TopologyType.Single;
|
||||
}
|
||||
|
||||
const RETRYABLE_WIRE_VERSION = 6;
|
||||
|
||||
/**
|
||||
@@ -415,6 +430,10 @@ const isRetryableWritesSupported = function(topology) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (topologyType(topology) === TopologyType.Single) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
+34
-1
@@ -101,6 +101,22 @@ class Transaction {
|
||||
|
||||
if (options.readConcern) this.options.readConcern = options.readConcern;
|
||||
if (options.readPreference) this.options.readPreference = options.readPreference;
|
||||
|
||||
// TODO: This isn't technically necessary
|
||||
this._pinnedServer = undefined;
|
||||
this._recoveryToken = undefined;
|
||||
}
|
||||
|
||||
get server() {
|
||||
return this._pinnedServer;
|
||||
}
|
||||
|
||||
get recoveryToken() {
|
||||
return this._recoveryToken;
|
||||
}
|
||||
|
||||
get isPinned() {
|
||||
return !!this.server;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,6 +138,9 @@ 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) {
|
||||
this.unpinServer();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -129,6 +148,20 @@ class Transaction {
|
||||
`Attempted illegal state transition from [${this.state}] to [${nextState}]`
|
||||
);
|
||||
}
|
||||
|
||||
pinServer(server) {
|
||||
if (this.isActive) {
|
||||
this._pinnedServer = server;
|
||||
}
|
||||
}
|
||||
|
||||
unpinServer() {
|
||||
this._pinnedServer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { TxnState, Transaction };
|
||||
function isTransactionCommand(command) {
|
||||
return !!(command.commitTransaction || command.abortTransaction);
|
||||
}
|
||||
|
||||
module.exports = { TxnState, Transaction, isTransactionCommand };
|
||||
|
||||
+98
-5
@@ -124,6 +124,10 @@ function parseQueryStringItemValue(key, value) {
|
||||
// deduplicate and simplify arrays
|
||||
value = value.filter((v, idx) => value.indexOf(v) === idx);
|
||||
if (value.length === 1) value = value[0];
|
||||
} else if (STRING_OPTIONS.has(key)) {
|
||||
// TODO: refactor function to make this early return not
|
||||
// stand out
|
||||
return value;
|
||||
} else if (value.indexOf(':') > 0) {
|
||||
value = value.split(',').reduce((result, pair) => {
|
||||
const parts = pair.split(':');
|
||||
@@ -136,7 +140,7 @@ function parseQueryStringItemValue(key, value) {
|
||||
});
|
||||
} else if (value.toLowerCase() === 'true' || value.toLowerCase() === 'false') {
|
||||
value = value.toLowerCase() === 'true';
|
||||
} else if (!Number.isNaN(value) && !STRING_OPTIONS.has(key)) {
|
||||
} else if (!Number.isNaN(value)) {
|
||||
const numericValue = parseFloat(value);
|
||||
if (!Number.isNaN(numericValue)) {
|
||||
value = parseFloat(value);
|
||||
@@ -157,8 +161,9 @@ const BOOLEAN_OPTIONS = new Set([
|
||||
'j'
|
||||
]);
|
||||
|
||||
// Known string options, only used to bypass Number coercion in `parseQueryStringItemValue`
|
||||
const STRING_OPTIONS = new Set(['authsource', 'replicaset']);
|
||||
// Known string options
|
||||
// TODO: Do this for more types
|
||||
const STRING_OPTIONS = new Set(['authsource', 'replicaset', 'appname']);
|
||||
|
||||
// Supported text representations of auth mechanisms
|
||||
// NOTE: this list exists in native already, if it is merged here we should deduplicate
|
||||
@@ -197,10 +202,17 @@ const CASE_TRANSLATION = {
|
||||
serverselectiontimeoutms: 'serverSelectionTimeoutMS',
|
||||
serverselectiontryonce: 'serverSelectionTryOnce',
|
||||
heartbeatfrequencyms: 'heartbeatFrequencyMS',
|
||||
appname: 'appName',
|
||||
retrywrites: 'retryWrites',
|
||||
uuidrepresentation: 'uuidRepresentation',
|
||||
zlibcompressionlevel: 'zlibCompressionLevel'
|
||||
zlibcompressionlevel: 'zlibCompressionLevel',
|
||||
tlsallowinvalidcertificates: 'tlsAllowInvalidCertificates',
|
||||
tlsallowinvalidhostnames: 'tlsAllowInvalidHostnames',
|
||||
tlsinsecure: 'tlsInsecure',
|
||||
tlscafile: 'tlsCAFile',
|
||||
tlscertificatekeyfile: 'tlsCertificateKeyFile',
|
||||
tlscertificatekeyfilepassword: 'tlsCertificateKeyFilePassword',
|
||||
wtimeout: 'wTimeoutMS',
|
||||
j: 'journal'
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -225,6 +237,7 @@ function applyConnectionStringOption(obj, key, value, options) {
|
||||
} else if (key === 'appname') {
|
||||
value = decodeURIComponent(value);
|
||||
} else if (key === 'readconcernlevel') {
|
||||
obj['readConcernLevel'] = value;
|
||||
key = 'readconcern';
|
||||
value = { level: value };
|
||||
}
|
||||
@@ -270,6 +283,10 @@ function applyConnectionStringOption(obj, key, value, options) {
|
||||
}
|
||||
}
|
||||
|
||||
if (key === 'readpreferencetags' && Array.isArray(value)) {
|
||||
value = splitArrayOfMultipleReadPreferenceTags(value);
|
||||
}
|
||||
|
||||
// set the actual value
|
||||
if (options.caseTranslate && CASE_TRANSLATION[key]) {
|
||||
obj[CASE_TRANSLATION[key]] = value;
|
||||
@@ -287,6 +304,20 @@ const USERNAME_REQUIRED_MECHANISMS = new Set([
|
||||
'SCRAM-SHA-256'
|
||||
]);
|
||||
|
||||
function splitArrayOfMultipleReadPreferenceTags(value) {
|
||||
const parsedTags = [];
|
||||
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
parsedTags[i] = {};
|
||||
value[i].split(',').forEach(individualTag => {
|
||||
const splitTag = individualTag.split(':');
|
||||
parsedTags[i][splitTag[0]] = splitTag[1];
|
||||
});
|
||||
}
|
||||
|
||||
return parsedTags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the parsed connection string object taking into account expectations we
|
||||
* have for authentication-related options.
|
||||
@@ -364,6 +395,8 @@ function parseQueryString(query, options) {
|
||||
const result = {};
|
||||
let parsedQueryString = qs.parse(query);
|
||||
|
||||
checkTLSOptions(parsedQueryString);
|
||||
|
||||
for (const key in parsedQueryString) {
|
||||
const value = parsedQueryString[key];
|
||||
if (value === '' || value == null) {
|
||||
@@ -384,6 +417,66 @@ function parseQueryString(query, options) {
|
||||
return Object.keys(result).length ? result : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a query string for invalid tls options according to the URI options spec.
|
||||
*
|
||||
* @param {string} queryString The query string to check
|
||||
* @throws {MongoParseError}
|
||||
*/
|
||||
function checkTLSOptions(queryString) {
|
||||
const queryStringKeys = Object.keys(queryString);
|
||||
if (
|
||||
queryStringKeys.indexOf('tlsInsecure') !== -1 &&
|
||||
(queryStringKeys.indexOf('tlsAllowInvalidCertificates') !== -1 ||
|
||||
queryStringKeys.indexOf('tlsAllowInvalidHostnames') !== -1)
|
||||
) {
|
||||
throw new MongoParseError(
|
||||
'The `tlsInsecure` option cannot be used with `tlsAllowInvalidCertificates` or `tlsAllowInvalidHostnames`.'
|
||||
);
|
||||
}
|
||||
|
||||
const tlsValue = assertTlsOptionsAreEqual('tls', queryString, queryStringKeys);
|
||||
const sslValue = assertTlsOptionsAreEqual('ssl', queryString, queryStringKeys);
|
||||
|
||||
if (tlsValue != null && sslValue != null) {
|
||||
if (tlsValue !== sslValue) {
|
||||
throw new MongoParseError('All values of `tls` and `ssl` must be the same.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a query string to ensure all tls/ssl options are the same.
|
||||
*
|
||||
* @param {string} key The key (tls or ssl) to check
|
||||
* @param {string} queryString The query string to check
|
||||
* @throws {MongoParseError}
|
||||
* @return The value of the tls/ssl option
|
||||
*/
|
||||
function assertTlsOptionsAreEqual(optionName, queryString, queryStringKeys) {
|
||||
const queryStringHasTLSOption = queryStringKeys.indexOf(optionName) !== -1;
|
||||
|
||||
let optionValue;
|
||||
if (Array.isArray(queryString[optionName])) {
|
||||
optionValue = queryString[optionName][0];
|
||||
} else {
|
||||
optionValue = queryString[optionName];
|
||||
}
|
||||
|
||||
if (queryStringHasTLSOption) {
|
||||
if (Array.isArray(queryString[optionName])) {
|
||||
const firstValue = queryString[optionName][0];
|
||||
queryString[optionName].forEach(tlsValue => {
|
||||
if (tlsValue !== firstValue) {
|
||||
throw new MongoParseError('All values of ${optionName} must be the same.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return optionValue;
|
||||
}
|
||||
|
||||
const PROTOCOL_MONGODB = 'mongodb';
|
||||
const PROTOCOL_MONGODB_SRV = 'mongodb+srv';
|
||||
const SUPPORTED_PROTOCOLS = [PROTOCOL_MONGODB, PROTOCOL_MONGODB_SRV];
|
||||
|
||||
+33
-2
@@ -75,6 +75,25 @@ function retrieveEJSON() {
|
||||
return EJSON;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function for determining `maxWireVersion` between legacy and new topology
|
||||
* instances
|
||||
*
|
||||
* @private
|
||||
* @param {(Topology|Server)} topologyOrServer
|
||||
*/
|
||||
function maxWireVersion(topologyOrServer) {
|
||||
if (topologyOrServer.ismaster) {
|
||||
return topologyOrServer.ismaster.maxWireVersion;
|
||||
}
|
||||
|
||||
if (topologyOrServer.description) {
|
||||
return topologyOrServer.description.maxWireVersion;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Checks that collation is supported by server.
|
||||
*
|
||||
@@ -84,7 +103,17 @@ function retrieveEJSON() {
|
||||
* @return true if server does not support collation
|
||||
*/
|
||||
function collationNotSupported(server, cmd) {
|
||||
return cmd && cmd.collation && server.ismaster && server.ismaster.maxWireVersion < 5;
|
||||
return cmd && cmd.collation && maxWireVersion(server) < 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given value is a Promise
|
||||
*
|
||||
* @param {*} maybePromise
|
||||
* @return true if the provided value is a Promise
|
||||
*/
|
||||
function isPromiseLike(maybePromise) {
|
||||
return maybePromise && typeof maybePromise.then === 'function';
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@@ -93,5 +122,7 @@ module.exports = {
|
||||
relayEvents,
|
||||
collationNotSupported,
|
||||
retrieveEJSON,
|
||||
retrieveKerberos
|
||||
retrieveKerberos,
|
||||
maxWireVersion,
|
||||
isPromiseLike
|
||||
};
|
||||
|
||||
-368
@@ -1,368 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var copy = require('../connection/utils').copy,
|
||||
retrieveBSON = require('../connection/utils').retrieveBSON,
|
||||
KillCursor = require('../connection/commands').KillCursor,
|
||||
GetMore = require('../connection/commands').GetMore,
|
||||
Query = require('../connection/commands').Query,
|
||||
f = require('util').format,
|
||||
MongoError = require('../error').MongoError,
|
||||
getReadPreference = require('./shared').getReadPreference;
|
||||
|
||||
var BSON = retrieveBSON(),
|
||||
Long = BSON.Long;
|
||||
|
||||
var WireProtocol = function() {};
|
||||
|
||||
//
|
||||
// Execute a write operation
|
||||
var executeWrite = function(pool, bson, type, opsField, ns, ops, options, callback) {
|
||||
if (ops.length === 0) throw new MongoError('insert must contain at least one document');
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
options = options || {};
|
||||
}
|
||||
|
||||
// Split the ns up to get db and collection
|
||||
var p = ns.split('.');
|
||||
var d = p.shift();
|
||||
// Options
|
||||
var ordered = typeof options.ordered === 'boolean' ? options.ordered : true;
|
||||
var writeConcern = options.writeConcern;
|
||||
|
||||
// return skeleton
|
||||
var writeCommand = {};
|
||||
writeCommand[type] = p.join('.');
|
||||
writeCommand[opsField] = ops;
|
||||
writeCommand.ordered = ordered;
|
||||
|
||||
// Did we specify a write concern
|
||||
if (writeConcern && Object.keys(writeConcern).length > 0) {
|
||||
writeCommand.writeConcern = writeConcern;
|
||||
}
|
||||
|
||||
// Do we have bypassDocumentValidation set, then enable it on the write command
|
||||
if (options.bypassDocumentValidation === true) {
|
||||
writeCommand.bypassDocumentValidation = options.bypassDocumentValidation;
|
||||
}
|
||||
|
||||
// Options object
|
||||
var opts = { command: true };
|
||||
if (typeof options.session !== 'undefined') opts.session = options.session;
|
||||
var queryOptions = { checkKeys: false, numberToSkip: 0, numberToReturn: 1 };
|
||||
if (type === 'insert') queryOptions.checkKeys = true;
|
||||
if (typeof options.checkKeys === 'boolean') queryOptions.checkKeys = options.checkKeys;
|
||||
// Ensure we support serialization of functions
|
||||
if (options.serializeFunctions) queryOptions.serializeFunctions = options.serializeFunctions;
|
||||
// Do not serialize the undefined fields
|
||||
if (options.ignoreUndefined) queryOptions.ignoreUndefined = options.ignoreUndefined;
|
||||
|
||||
try {
|
||||
// Create write command
|
||||
var cmd = new Query(bson, f('%s.$cmd', d), writeCommand, queryOptions);
|
||||
// Execute command
|
||||
pool.write(cmd, opts, callback);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Needs to support legacy mass insert as well as ordered/unordered legacy
|
||||
// emulation
|
||||
//
|
||||
WireProtocol.prototype.insert = function(pool, ns, bson, ops, options, callback) {
|
||||
executeWrite(pool, bson, 'insert', 'documents', ns, ops, options, callback);
|
||||
};
|
||||
|
||||
WireProtocol.prototype.update = function(pool, ns, bson, ops, options, callback) {
|
||||
executeWrite(pool, bson, 'update', 'updates', ns, ops, options, callback);
|
||||
};
|
||||
|
||||
WireProtocol.prototype.remove = function(pool, ns, bson, ops, options, callback) {
|
||||
executeWrite(pool, bson, 'delete', 'deletes', ns, ops, options, callback);
|
||||
};
|
||||
|
||||
WireProtocol.prototype.killCursor = function(bson, ns, cursorState, pool, callback) {
|
||||
var cursorId = cursorState.cursorId;
|
||||
// Create a kill cursor command
|
||||
var killCursor = new KillCursor(bson, ns, [cursorId]);
|
||||
|
||||
// Build killCursor options
|
||||
const options = {
|
||||
immediateRelease: true,
|
||||
noResponse: true
|
||||
};
|
||||
|
||||
if (typeof cursorState.session === 'object') {
|
||||
options.session = cursorState.session;
|
||||
}
|
||||
|
||||
// Execute the kill cursor command
|
||||
if (pool && pool.isConnected()) {
|
||||
try {
|
||||
pool.write(killCursor, options, callback);
|
||||
} catch (err) {
|
||||
if (typeof callback === 'function') {
|
||||
callback(err, null);
|
||||
} else {
|
||||
console.warn(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
WireProtocol.prototype.getMore = function(
|
||||
bson,
|
||||
ns,
|
||||
cursorState,
|
||||
batchSize,
|
||||
raw,
|
||||
connection,
|
||||
options,
|
||||
callback
|
||||
) {
|
||||
// Create getMore command
|
||||
var getMore = new GetMore(bson, ns, cursorState.cursorId, { numberToReturn: batchSize });
|
||||
|
||||
// Query callback
|
||||
var queryCallback = function(err, result) {
|
||||
if (err) return callback(err);
|
||||
// Get the raw message
|
||||
var r = result.message;
|
||||
|
||||
// If we have a timed out query or a cursor that was killed
|
||||
if ((r.responseFlags & (1 << 0)) !== 0) {
|
||||
return callback(new MongoError('cursor does not exist, was killed or timed out'), null);
|
||||
}
|
||||
|
||||
// Ensure we have a Long valie cursor id
|
||||
var cursorId = typeof r.cursorId === 'number' ? Long.fromNumber(r.cursorId) : r.cursorId;
|
||||
|
||||
// Set all the values
|
||||
cursorState.documents = r.documents;
|
||||
cursorState.cursorId = cursorId;
|
||||
|
||||
// Return
|
||||
callback(null, null, r.connection);
|
||||
};
|
||||
|
||||
// Contains any query options
|
||||
var queryOptions = {};
|
||||
|
||||
// If we have a raw query decorate the function
|
||||
if (raw) {
|
||||
queryOptions.raw = raw;
|
||||
}
|
||||
|
||||
// Check if we need to promote longs
|
||||
if (typeof cursorState.promoteLongs === 'boolean') {
|
||||
queryOptions.promoteLongs = cursorState.promoteLongs;
|
||||
}
|
||||
|
||||
if (typeof cursorState.promoteValues === 'boolean') {
|
||||
queryOptions.promoteValues = cursorState.promoteValues;
|
||||
}
|
||||
|
||||
if (typeof cursorState.promoteBuffers === 'boolean') {
|
||||
queryOptions.promoteBuffers = cursorState.promoteBuffers;
|
||||
}
|
||||
|
||||
if (typeof cursorState.session === 'object') {
|
||||
queryOptions.session = cursorState.session;
|
||||
}
|
||||
|
||||
// Write out the getMore command
|
||||
connection.write(getMore, queryOptions, queryCallback);
|
||||
};
|
||||
|
||||
WireProtocol.prototype.command = function(bson, ns, cmd, cursorState, topology, options) {
|
||||
// Establish type of command
|
||||
if (cmd.find) {
|
||||
return setupClassicFind(bson, ns, cmd, cursorState, topology, options);
|
||||
} else if (cursorState.cursorId != null) {
|
||||
return;
|
||||
} else if (cmd) {
|
||||
return setupCommand(bson, ns, cmd, cursorState, topology, options);
|
||||
} else {
|
||||
throw new MongoError(f('command %s does not return a cursor', JSON.stringify(cmd)));
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Execute a find command
|
||||
var setupClassicFind = function(bson, ns, cmd, cursorState, topology, options) {
|
||||
// Ensure we have at least some options
|
||||
options = options || {};
|
||||
// Get the readPreference
|
||||
var readPreference = getReadPreference(cmd, options);
|
||||
// Set the optional batchSize
|
||||
cursorState.batchSize = cmd.batchSize || cursorState.batchSize;
|
||||
var numberToReturn = 0;
|
||||
|
||||
// Unpack the limit and batchSize values
|
||||
if (cursorState.limit === 0) {
|
||||
numberToReturn = cursorState.batchSize;
|
||||
} else if (
|
||||
cursorState.limit < 0 ||
|
||||
cursorState.limit < cursorState.batchSize ||
|
||||
(cursorState.limit > 0 && cursorState.batchSize === 0)
|
||||
) {
|
||||
numberToReturn = cursorState.limit;
|
||||
} else {
|
||||
numberToReturn = cursorState.batchSize;
|
||||
}
|
||||
|
||||
var numberToSkip = cursorState.skip || 0;
|
||||
// Build actual find command
|
||||
var findCmd = {};
|
||||
|
||||
// We have a Mongos topology, check if we need to add a readPreference
|
||||
if (topology.type === 'mongos' && readPreference) {
|
||||
findCmd['$readPreference'] = readPreference.toJSON();
|
||||
}
|
||||
|
||||
// Add special modifiers to the query
|
||||
if (cmd.sort) findCmd['$orderby'] = cmd.sort;
|
||||
if (cmd.hint) findCmd['$hint'] = cmd.hint;
|
||||
if (cmd.snapshot) findCmd['$snapshot'] = cmd.snapshot;
|
||||
if (typeof cmd.returnKey !== 'undefined') findCmd['$returnKey'] = cmd.returnKey;
|
||||
if (cmd.maxScan) findCmd['$maxScan'] = cmd.maxScan;
|
||||
if (cmd.min) findCmd['$min'] = cmd.min;
|
||||
if (cmd.max) findCmd['$max'] = cmd.max;
|
||||
if (typeof cmd.showDiskLoc !== 'undefined') findCmd['$showDiskLoc'] = cmd.showDiskLoc;
|
||||
if (cmd.comment) findCmd['$comment'] = cmd.comment;
|
||||
if (cmd.maxTimeMS) findCmd['$maxTimeMS'] = cmd.maxTimeMS;
|
||||
|
||||
if (cmd.explain) {
|
||||
// nToReturn must be 0 (match all) or negative (match N and close cursor)
|
||||
// nToReturn > 0 will give explain results equivalent to limit(0)
|
||||
numberToReturn = -Math.abs(cmd.limit || 0);
|
||||
findCmd['$explain'] = true;
|
||||
}
|
||||
|
||||
// Add the query
|
||||
findCmd['$query'] = cmd.query;
|
||||
|
||||
// Throw on majority readConcern passed in
|
||||
if (cmd.readConcern && cmd.readConcern.level !== 'local') {
|
||||
throw new MongoError(
|
||||
f('server find command does not support a readConcern level of %s', cmd.readConcern.level)
|
||||
);
|
||||
}
|
||||
|
||||
// Remove readConcern, ensure no failing commands
|
||||
if (cmd.readConcern) {
|
||||
cmd = copy(cmd);
|
||||
delete cmd['readConcern'];
|
||||
}
|
||||
|
||||
// Serialize functions
|
||||
var serializeFunctions =
|
||||
typeof options.serializeFunctions === 'boolean' ? options.serializeFunctions : false;
|
||||
var ignoreUndefined =
|
||||
typeof options.ignoreUndefined === 'boolean' ? options.ignoreUndefined : false;
|
||||
|
||||
// Build Query object
|
||||
var query = new Query(bson, ns, findCmd, {
|
||||
numberToSkip: numberToSkip,
|
||||
numberToReturn: numberToReturn,
|
||||
pre32Limit: typeof cmd.limit !== 'undefined' ? cmd.limit : undefined,
|
||||
checkKeys: false,
|
||||
returnFieldSelector: cmd.fields,
|
||||
serializeFunctions: serializeFunctions,
|
||||
ignoreUndefined: ignoreUndefined
|
||||
});
|
||||
|
||||
// Set query flags
|
||||
query.slaveOk = readPreference.slaveOk();
|
||||
|
||||
// Set up the option bits for wire protocol
|
||||
if (typeof cmd.tailable === 'boolean') {
|
||||
query.tailable = cmd.tailable;
|
||||
}
|
||||
|
||||
if (typeof cmd.oplogReplay === 'boolean') {
|
||||
query.oplogReplay = cmd.oplogReplay;
|
||||
}
|
||||
|
||||
if (typeof cmd.noCursorTimeout === 'boolean') {
|
||||
query.noCursorTimeout = cmd.noCursorTimeout;
|
||||
}
|
||||
|
||||
if (typeof cmd.awaitData === 'boolean') {
|
||||
query.awaitData = cmd.awaitData;
|
||||
}
|
||||
|
||||
if (typeof cmd.partial === 'boolean') {
|
||||
query.partial = cmd.partial;
|
||||
}
|
||||
|
||||
// Return the query
|
||||
return query;
|
||||
};
|
||||
|
||||
//
|
||||
// Set up a command cursor
|
||||
var setupCommand = function(bson, ns, cmd, cursorState, topology, options) {
|
||||
// Set empty options object
|
||||
options = options || {};
|
||||
// Get the readPreference
|
||||
var readPreference = getReadPreference(cmd, options);
|
||||
|
||||
// Final query
|
||||
var finalCmd = {};
|
||||
for (var name in cmd) {
|
||||
finalCmd[name] = cmd[name];
|
||||
}
|
||||
|
||||
// Build command namespace
|
||||
var parts = ns.split(/\./);
|
||||
|
||||
// Serialize functions
|
||||
var serializeFunctions =
|
||||
typeof options.serializeFunctions === 'boolean' ? options.serializeFunctions : false;
|
||||
|
||||
var ignoreUndefined =
|
||||
typeof options.ignoreUndefined === 'boolean' ? options.ignoreUndefined : false;
|
||||
|
||||
// Throw on majority readConcern passed in
|
||||
if (cmd.readConcern && cmd.readConcern.level !== 'local') {
|
||||
throw new MongoError(
|
||||
f(
|
||||
'server %s command does not support a readConcern level of %s',
|
||||
JSON.stringify(cmd),
|
||||
cmd.readConcern.level
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Remove readConcern, ensure no failing commands
|
||||
if (cmd.readConcern) delete cmd['readConcern'];
|
||||
|
||||
// We have a Mongos topology, check if we need to add a readPreference
|
||||
if (topology.type === 'mongos' && readPreference && readPreference.preference !== 'primary') {
|
||||
finalCmd = {
|
||||
$query: finalCmd,
|
||||
$readPreference: readPreference.toJSON()
|
||||
};
|
||||
}
|
||||
|
||||
// Build Query object
|
||||
var query = new Query(bson, f('%s.$cmd', parts.shift()), finalCmd, {
|
||||
numberToSkip: 0,
|
||||
numberToReturn: -1,
|
||||
checkKeys: false,
|
||||
serializeFunctions: serializeFunctions,
|
||||
ignoreUndefined: ignoreUndefined
|
||||
});
|
||||
|
||||
// Set query flags
|
||||
query.slaveOk = readPreference.slaveOk();
|
||||
|
||||
// Return the query
|
||||
return query;
|
||||
};
|
||||
|
||||
module.exports = WireProtocol;
|
||||
-682
@@ -1,682 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const Query = require('../connection/commands').Query;
|
||||
const retrieveBSON = require('../connection/utils').retrieveBSON;
|
||||
const MongoError = require('../error').MongoError;
|
||||
const MongoNetworkError = require('../error').MongoNetworkError;
|
||||
const getReadPreference = require('./shared').getReadPreference;
|
||||
const BSON = retrieveBSON();
|
||||
const Long = BSON.Long;
|
||||
const ReadPreference = require('../topologies/read_preference');
|
||||
const TxnState = require('../transactions').TxnState;
|
||||
|
||||
const WireProtocol = function() {};
|
||||
|
||||
function isTransactionCommand(command) {
|
||||
return !!(command.commitTransaction || command.abortTransaction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionally decorate a command with sessions specific keys
|
||||
*
|
||||
* @param {Object} command the command to decorate
|
||||
* @param {ClientSession} session the session tracking transaction state
|
||||
* @param {Object} [options] Optional settings passed to calling operation
|
||||
* @param {Function} [callback] Optional callback passed from calling operation
|
||||
* @return {MongoError|null} An error, if some error condition was met
|
||||
*/
|
||||
function decorateWithSessionsData(command, session, options) {
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
// first apply non-transaction-specific sessions data
|
||||
const serverSession = session.serverSession;
|
||||
const inTransaction = session.inTransaction() || isTransactionCommand(command);
|
||||
const isRetryableWrite = options.willRetryWrite;
|
||||
|
||||
if (serverSession.txnNumber && (isRetryableWrite || inTransaction)) {
|
||||
command.txnNumber = BSON.Long.fromNumber(serverSession.txnNumber);
|
||||
}
|
||||
|
||||
// now attempt to apply transaction-specific sessions data
|
||||
if (!inTransaction) {
|
||||
if (session.transaction.state !== TxnState.NO_TRANSACTION) {
|
||||
session.transaction.transition(TxnState.NO_TRANSACTION);
|
||||
}
|
||||
|
||||
// for causal consistency
|
||||
if (session.supports.causalConsistency && session.operationTime) {
|
||||
command.readConcern = command.readConcern || {};
|
||||
Object.assign(command.readConcern, { afterClusterTime: session.operationTime });
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.readPreference && !options.readPreference.equals(ReadPreference.primary)) {
|
||||
return new MongoError(
|
||||
`Read preference in a transaction must be primary, not: ${options.readPreference.mode}`
|
||||
);
|
||||
}
|
||||
|
||||
// `autocommit` must always be false to differentiate from retryable writes
|
||||
command.autocommit = false;
|
||||
|
||||
if (session.transaction.state === TxnState.STARTING_TRANSACTION) {
|
||||
session.transaction.transition(TxnState.TRANSACTION_IN_PROGRESS);
|
||||
command.startTransaction = true;
|
||||
|
||||
const readConcern =
|
||||
session.transaction.options.readConcern || session.clientOptions.readConcern;
|
||||
if (readConcern) {
|
||||
command.readConcern = readConcern;
|
||||
}
|
||||
|
||||
if (session.supports.causalConsistency && session.operationTime) {
|
||||
command.readConcern = command.readConcern || {};
|
||||
Object.assign(command.readConcern, { afterClusterTime: session.operationTime });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Execute a write operation
|
||||
function executeWrite(pool, bson, type, opsField, ns, ops, options, callback) {
|
||||
if (ops.length === 0) throw new MongoError('insert must contain at least one document');
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
options = options || {};
|
||||
}
|
||||
|
||||
// Split the ns up to get db and collection
|
||||
const p = ns.split('.');
|
||||
const d = p.shift();
|
||||
// Options
|
||||
const ordered = typeof options.ordered === 'boolean' ? options.ordered : true;
|
||||
const writeConcern = options.writeConcern;
|
||||
|
||||
// return skeleton
|
||||
const writeCommand = {};
|
||||
writeCommand[type] = p.join('.');
|
||||
writeCommand[opsField] = ops;
|
||||
writeCommand.ordered = ordered;
|
||||
|
||||
// Did we specify a write concern
|
||||
if (writeConcern && Object.keys(writeConcern).length > 0) {
|
||||
writeCommand.writeConcern = writeConcern;
|
||||
}
|
||||
|
||||
// If we have collation passed in
|
||||
if (options.collation) {
|
||||
for (let i = 0; i < writeCommand[opsField].length; i++) {
|
||||
if (!writeCommand[opsField][i].collation) {
|
||||
writeCommand[opsField][i].collation = options.collation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do we have bypassDocumentValidation set, then enable it on the write command
|
||||
if (options.bypassDocumentValidation === true) {
|
||||
writeCommand.bypassDocumentValidation = options.bypassDocumentValidation;
|
||||
}
|
||||
|
||||
// optionally decorate command with transactions data
|
||||
const err = decorateWithSessionsData(writeCommand, options.session, options, callback);
|
||||
if (err) {
|
||||
return callback(err, null);
|
||||
}
|
||||
|
||||
// Options object
|
||||
const opts = { command: true };
|
||||
if (typeof options.session !== 'undefined') opts.session = options.session;
|
||||
const queryOptions = { checkKeys: false, numberToSkip: 0, numberToReturn: 1 };
|
||||
if (type === 'insert') queryOptions.checkKeys = true;
|
||||
if (typeof options.checkKeys === 'boolean') queryOptions.checkKeys = options.checkKeys;
|
||||
|
||||
// Ensure we support serialization of functions
|
||||
if (options.serializeFunctions) queryOptions.serializeFunctions = options.serializeFunctions;
|
||||
// Do not serialize the undefined fields
|
||||
if (options.ignoreUndefined) queryOptions.ignoreUndefined = options.ignoreUndefined;
|
||||
|
||||
try {
|
||||
// Create write command
|
||||
const cmd = new Query(bson, `${d}.$cmd`, writeCommand, queryOptions);
|
||||
// Execute command
|
||||
pool.write(cmd, opts, callback);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Needs to support legacy mass insert as well as ordered/unordered legacy
|
||||
// emulation
|
||||
//
|
||||
WireProtocol.prototype.insert = function(pool, ns, bson, ops, options, callback) {
|
||||
executeWrite(pool, bson, 'insert', 'documents', ns, ops, options, callback);
|
||||
};
|
||||
|
||||
WireProtocol.prototype.update = function(pool, ns, bson, ops, options, callback) {
|
||||
executeWrite(pool, bson, 'update', 'updates', ns, ops, options, callback);
|
||||
};
|
||||
|
||||
WireProtocol.prototype.remove = function(pool, ns, bson, ops, options, callback) {
|
||||
executeWrite(pool, bson, 'delete', 'deletes', ns, ops, options, callback);
|
||||
};
|
||||
|
||||
WireProtocol.prototype.killCursor = function(bson, ns, cursorState, pool, callback) {
|
||||
// Build command namespace
|
||||
const parts = ns.split(/\./);
|
||||
// Command namespace
|
||||
const commandns = `${parts.shift()}.$cmd`;
|
||||
const cursorId = cursorState.cursorId;
|
||||
// Create killCursor command
|
||||
const killcursorCmd = {
|
||||
killCursors: parts.join('.'),
|
||||
cursors: [cursorId]
|
||||
};
|
||||
|
||||
// Build Query object
|
||||
const query = new Query(bson, commandns, killcursorCmd, {
|
||||
numberToSkip: 0,
|
||||
numberToReturn: -1,
|
||||
checkKeys: false,
|
||||
returnFieldSelector: null
|
||||
});
|
||||
|
||||
// Kill cursor callback
|
||||
function killCursorCallback(err, result) {
|
||||
if (err) {
|
||||
if (typeof callback !== 'function') return;
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Result
|
||||
const r = result.message;
|
||||
// If we have a timed out query or a cursor that was killed
|
||||
if ((r.responseFlags & (1 << 0)) !== 0) {
|
||||
if (typeof callback !== 'function') return;
|
||||
return callback(new MongoNetworkError('cursor killed or timed out'), null);
|
||||
}
|
||||
|
||||
if (!Array.isArray(r.documents) || r.documents.length === 0) {
|
||||
if (typeof callback !== 'function') return;
|
||||
return callback(
|
||||
new MongoError(`invalid killCursors result returned for cursor id ${cursorId}`)
|
||||
);
|
||||
}
|
||||
|
||||
// Return the result
|
||||
if (typeof callback === 'function') {
|
||||
callback(null, r.documents[0]);
|
||||
}
|
||||
}
|
||||
|
||||
const options = { command: true };
|
||||
if (typeof cursorState.session === 'object') {
|
||||
options.session = cursorState.session;
|
||||
}
|
||||
|
||||
// Execute the kill cursor command
|
||||
if (pool && pool.isConnected()) {
|
||||
try {
|
||||
pool.write(query, options, killCursorCallback);
|
||||
} catch (err) {
|
||||
killCursorCallback(err, null);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Callback
|
||||
if (typeof callback === 'function') callback(null, null);
|
||||
};
|
||||
|
||||
WireProtocol.prototype.getMore = function(
|
||||
bson,
|
||||
ns,
|
||||
cursorState,
|
||||
batchSize,
|
||||
raw,
|
||||
connection,
|
||||
options,
|
||||
callback
|
||||
) {
|
||||
options = options || {};
|
||||
// Build command namespace
|
||||
const parts = ns.split(/\./);
|
||||
// Command namespace
|
||||
const commandns = `${parts.shift()}.$cmd`;
|
||||
|
||||
// Create getMore command
|
||||
const getMoreCmd = {
|
||||
getMore: cursorState.cursorId,
|
||||
collection: parts.join('.'),
|
||||
batchSize: Math.abs(batchSize)
|
||||
};
|
||||
|
||||
// optionally decorate command with transactions data
|
||||
const err = decorateWithSessionsData(getMoreCmd, options.session, options, callback);
|
||||
if (err) {
|
||||
return callback(err, null);
|
||||
}
|
||||
|
||||
if (cursorState.cmd.tailable && typeof cursorState.cmd.maxAwaitTimeMS === 'number') {
|
||||
getMoreCmd.maxTimeMS = cursorState.cmd.maxAwaitTimeMS;
|
||||
}
|
||||
|
||||
// Build Query object
|
||||
const query = new Query(bson, commandns, getMoreCmd, {
|
||||
numberToSkip: 0,
|
||||
numberToReturn: -1,
|
||||
checkKeys: false,
|
||||
returnFieldSelector: null
|
||||
});
|
||||
|
||||
// Query callback
|
||||
function queryCallback(err, result) {
|
||||
if (err) return callback(err);
|
||||
// Get the raw message
|
||||
const r = result.message;
|
||||
|
||||
// If we have a timed out query or a cursor that was killed
|
||||
if ((r.responseFlags & (1 << 0)) !== 0) {
|
||||
return callback(new MongoNetworkError('cursor killed or timed out'), null);
|
||||
}
|
||||
|
||||
// Raw, return all the extracted documents
|
||||
if (raw) {
|
||||
cursorState.documents = r.documents;
|
||||
cursorState.cursorId = r.cursorId;
|
||||
return callback(null, r.documents);
|
||||
}
|
||||
|
||||
// We have an error detected
|
||||
if (r.documents[0].ok === 0) {
|
||||
return callback(new MongoError(r.documents[0]));
|
||||
}
|
||||
|
||||
// Ensure we have a Long valid cursor id
|
||||
const cursorId =
|
||||
typeof r.documents[0].cursor.id === 'number'
|
||||
? Long.fromNumber(r.documents[0].cursor.id)
|
||||
: r.documents[0].cursor.id;
|
||||
|
||||
// Set all the values
|
||||
cursorState.documents = r.documents[0].cursor.nextBatch;
|
||||
cursorState.cursorId = cursorId;
|
||||
|
||||
// Return the result
|
||||
callback(null, r.documents[0], r.connection);
|
||||
}
|
||||
|
||||
// Query options
|
||||
const queryOptions = { command: true };
|
||||
|
||||
// If we have a raw query decorate the function
|
||||
if (raw) {
|
||||
queryOptions.raw = raw;
|
||||
}
|
||||
|
||||
// Add the result field needed
|
||||
queryOptions.documentsReturnedIn = 'nextBatch';
|
||||
|
||||
// Check if we need to promote longs
|
||||
if (typeof cursorState.promoteLongs === 'boolean') {
|
||||
queryOptions.promoteLongs = cursorState.promoteLongs;
|
||||
}
|
||||
|
||||
if (typeof cursorState.promoteValues === 'boolean') {
|
||||
queryOptions.promoteValues = cursorState.promoteValues;
|
||||
}
|
||||
|
||||
if (typeof cursorState.promoteBuffers === 'boolean') {
|
||||
queryOptions.promoteBuffers = cursorState.promoteBuffers;
|
||||
}
|
||||
|
||||
if (typeof cursorState.session === 'object') {
|
||||
queryOptions.session = cursorState.session;
|
||||
}
|
||||
|
||||
// Write out the getMore command
|
||||
connection.write(query, queryOptions, queryCallback);
|
||||
};
|
||||
|
||||
WireProtocol.prototype.command = function(bson, ns, cmd, cursorState, topology, options) {
|
||||
options = options || {};
|
||||
// Check if this is a wire protocol command or not
|
||||
const wireProtocolCommand =
|
||||
typeof options.wireProtocolCommand === 'boolean' ? options.wireProtocolCommand : true;
|
||||
|
||||
// Establish type of command
|
||||
let query;
|
||||
if (cmd.find && wireProtocolCommand) {
|
||||
// Create the find command
|
||||
query = executeFindCommand(bson, ns, cmd, cursorState, topology, options);
|
||||
|
||||
// Mark the cmd as virtual
|
||||
cmd.virtual = false;
|
||||
// Signal the documents are in the firstBatch value
|
||||
query.documentsReturnedIn = 'firstBatch';
|
||||
} else if (cursorState.cursorId != null) {
|
||||
return;
|
||||
} else if (cmd) {
|
||||
query = setupCommand(bson, ns, cmd, cursorState, topology, options);
|
||||
} else {
|
||||
return new MongoError(`command ${JSON.stringify(cmd)} does not return a cursor`);
|
||||
}
|
||||
|
||||
if (query instanceof MongoError) {
|
||||
return query;
|
||||
}
|
||||
|
||||
// optionally decorate query with transaction data
|
||||
const err = decorateWithSessionsData(query.query, options.session, options);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
// // Command
|
||||
// {
|
||||
// find: ns
|
||||
// , query: <object>
|
||||
// , limit: <n>
|
||||
// , fields: <object>
|
||||
// , skip: <n>
|
||||
// , hint: <string>
|
||||
// , explain: <boolean>
|
||||
// , snapshot: <boolean>
|
||||
// , batchSize: <n>
|
||||
// , returnKey: <boolean>
|
||||
// , maxScan: <n>
|
||||
// , min: <n>
|
||||
// , max: <n>
|
||||
// , showDiskLoc: <boolean>
|
||||
// , comment: <string>
|
||||
// , maxTimeMS: <n>
|
||||
// , raw: <boolean>
|
||||
// , readPreference: <ReadPreference>
|
||||
// , tailable: <boolean>
|
||||
// , oplogReplay: <boolean>
|
||||
// , noCursorTimeout: <boolean>
|
||||
// , awaitdata: <boolean>
|
||||
// , exhaust: <boolean>
|
||||
// , partial: <boolean>
|
||||
// }
|
||||
|
||||
// FIND/GETMORE SPEC
|
||||
// {
|
||||
// “find”: <string>,
|
||||
// “filter”: { ... },
|
||||
// “sort”: { ... },
|
||||
// “projection”: { ... },
|
||||
// “hint”: { ... },
|
||||
// “skip”: <int>,
|
||||
// “limit”: <int>,
|
||||
// “batchSize”: <int>,
|
||||
// “singleBatch”: <bool>,
|
||||
// “comment”: <string>,
|
||||
// “maxScan”: <int>,
|
||||
// “maxTimeMS”: <int>,
|
||||
// “max”: { ... },
|
||||
// “min”: { ... },
|
||||
// “returnKey”: <bool>,
|
||||
// “showRecordId”: <bool>,
|
||||
// “snapshot”: <bool>,
|
||||
// “tailable”: <bool>,
|
||||
// “oplogReplay”: <bool>,
|
||||
// “noCursorTimeout”: <bool>,
|
||||
// “awaitData”: <bool>,
|
||||
// “partial”: <bool>,
|
||||
// “$readPreference”: { ... }
|
||||
// }
|
||||
|
||||
//
|
||||
// Execute a find command
|
||||
function executeFindCommand(bson, ns, cmd, cursorState, topology, options) {
|
||||
// Ensure we have at least some options
|
||||
options = options || {};
|
||||
// Get the readPreference
|
||||
const readPreference = getReadPreference(cmd, options);
|
||||
|
||||
// Set the optional batchSize
|
||||
cursorState.batchSize = cmd.batchSize || cursorState.batchSize;
|
||||
|
||||
// Build command namespace
|
||||
const parts = ns.split(/\./);
|
||||
// Command namespace
|
||||
const commandns = `${parts.shift()}.$cmd`;
|
||||
|
||||
// Build actual find command
|
||||
let findCmd = {
|
||||
find: parts.join('.')
|
||||
};
|
||||
|
||||
// I we provided a filter
|
||||
if (cmd.query) {
|
||||
// Check if the user is passing in the $query parameter
|
||||
if (cmd.query['$query']) {
|
||||
findCmd.filter = cmd.query['$query'];
|
||||
} else {
|
||||
findCmd.filter = cmd.query;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort value
|
||||
let sortValue = cmd.sort;
|
||||
|
||||
// Handle issue of sort being an Array
|
||||
if (Array.isArray(sortValue)) {
|
||||
const sortObject = {};
|
||||
|
||||
if (sortValue.length > 0 && !Array.isArray(sortValue[0])) {
|
||||
let sortDirection = sortValue[1];
|
||||
// Translate the sort order text
|
||||
if (sortDirection === 'asc') {
|
||||
sortDirection = 1;
|
||||
} else if (sortDirection === 'desc') {
|
||||
sortDirection = -1;
|
||||
}
|
||||
|
||||
// Set the sort order
|
||||
sortObject[sortValue[0]] = sortDirection;
|
||||
} else {
|
||||
for (var i = 0; i < sortValue.length; i++) {
|
||||
let sortDirection = sortValue[i][1];
|
||||
// Translate the sort order text
|
||||
if (sortDirection === 'asc') {
|
||||
sortDirection = 1;
|
||||
} else if (sortDirection === 'desc') {
|
||||
sortDirection = -1;
|
||||
}
|
||||
|
||||
// Set the sort order
|
||||
sortObject[sortValue[i][0]] = sortDirection;
|
||||
}
|
||||
}
|
||||
|
||||
sortValue = sortObject;
|
||||
}
|
||||
|
||||
// Add sort to command
|
||||
if (cmd.sort) findCmd.sort = sortValue;
|
||||
// Add a projection to the command
|
||||
if (cmd.fields) findCmd.projection = cmd.fields;
|
||||
// Add a hint to the command
|
||||
if (cmd.hint) findCmd.hint = cmd.hint;
|
||||
// Add a skip
|
||||
if (cmd.skip) findCmd.skip = cmd.skip;
|
||||
// Add a limit
|
||||
if (cmd.limit) findCmd.limit = cmd.limit;
|
||||
|
||||
// Check if we wish to have a singleBatch
|
||||
if (cmd.limit < 0) {
|
||||
findCmd.limit = Math.abs(cmd.limit);
|
||||
findCmd.singleBatch = true;
|
||||
}
|
||||
|
||||
// Add a batchSize
|
||||
if (typeof cmd.batchSize === 'number') {
|
||||
if (cmd.batchSize < 0) {
|
||||
if (cmd.limit !== 0 && Math.abs(cmd.batchSize) < Math.abs(cmd.limit)) {
|
||||
findCmd.limit = Math.abs(cmd.batchSize);
|
||||
}
|
||||
|
||||
findCmd.singleBatch = true;
|
||||
}
|
||||
|
||||
findCmd.batchSize = Math.abs(cmd.batchSize);
|
||||
}
|
||||
|
||||
// If we have comment set
|
||||
if (cmd.comment) findCmd.comment = cmd.comment;
|
||||
|
||||
// If we have maxScan
|
||||
if (cmd.maxScan) findCmd.maxScan = cmd.maxScan;
|
||||
|
||||
// If we have maxTimeMS set
|
||||
if (cmd.maxTimeMS) findCmd.maxTimeMS = cmd.maxTimeMS;
|
||||
|
||||
// If we have min
|
||||
if (cmd.min) findCmd.min = cmd.min;
|
||||
|
||||
// If we have max
|
||||
if (cmd.max) findCmd.max = cmd.max;
|
||||
|
||||
// If we have returnKey set
|
||||
findCmd.returnKey = cmd.returnKey ? cmd.returnKey : false;
|
||||
|
||||
// If we have showDiskLoc set
|
||||
findCmd.showRecordId = cmd.showDiskLoc ? cmd.showDiskLoc : false;
|
||||
|
||||
// If we have snapshot set
|
||||
if (cmd.snapshot) findCmd.snapshot = cmd.snapshot;
|
||||
|
||||
// If we have tailable set
|
||||
if (cmd.tailable) findCmd.tailable = cmd.tailable;
|
||||
|
||||
// If we have oplogReplay set
|
||||
if (cmd.oplogReplay) findCmd.oplogReplay = cmd.oplogReplay;
|
||||
|
||||
// If we have noCursorTimeout set
|
||||
if (cmd.noCursorTimeout) findCmd.noCursorTimeout = cmd.noCursorTimeout;
|
||||
|
||||
// If we have awaitData set
|
||||
if (cmd.awaitData) findCmd.awaitData = cmd.awaitData;
|
||||
if (cmd.awaitdata) findCmd.awaitData = cmd.awaitdata;
|
||||
|
||||
// If we have partial set
|
||||
if (cmd.partial) findCmd.partial = cmd.partial;
|
||||
|
||||
// If we have collation passed in
|
||||
if (cmd.collation) findCmd.collation = cmd.collation;
|
||||
|
||||
// If we have explain, we need to rewrite the find command
|
||||
// to wrap it in the explain command
|
||||
if (cmd.explain) {
|
||||
findCmd = {
|
||||
explain: findCmd
|
||||
};
|
||||
}
|
||||
|
||||
// Did we provide a readConcern
|
||||
if (cmd.readConcern) findCmd.readConcern = cmd.readConcern;
|
||||
|
||||
// Set up the serialize and ignoreUndefined fields
|
||||
const serializeFunctions =
|
||||
typeof options.serializeFunctions === 'boolean' ? options.serializeFunctions : false;
|
||||
const ignoreUndefined =
|
||||
typeof options.ignoreUndefined === 'boolean' ? options.ignoreUndefined : false;
|
||||
|
||||
// We have a Mongos topology, check if we need to add a readPreference
|
||||
if (topology.type === 'mongos' && readPreference && readPreference.preference !== 'primary') {
|
||||
findCmd = {
|
||||
$query: findCmd,
|
||||
$readPreference: readPreference.toJSON()
|
||||
};
|
||||
}
|
||||
|
||||
// optionally decorate query with transaction data
|
||||
const err = decorateWithSessionsData(findCmd, options.session, options);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
|
||||
// Build Query object
|
||||
const query = new Query(bson, commandns, findCmd, {
|
||||
numberToSkip: 0,
|
||||
numberToReturn: 1,
|
||||
checkKeys: false,
|
||||
returnFieldSelector: null,
|
||||
serializeFunctions: serializeFunctions,
|
||||
ignoreUndefined: ignoreUndefined
|
||||
});
|
||||
|
||||
// Set query flags
|
||||
query.slaveOk = readPreference.slaveOk();
|
||||
|
||||
// Return the query
|
||||
return query;
|
||||
}
|
||||
|
||||
//
|
||||
// Set up a command cursor
|
||||
function setupCommand(bson, ns, cmd, cursorState, topology, options) {
|
||||
// Set empty options object
|
||||
options = options || {};
|
||||
// Get the readPreference
|
||||
const readPreference = getReadPreference(cmd, options);
|
||||
|
||||
// Final query
|
||||
let finalCmd = {};
|
||||
for (let name in cmd) {
|
||||
finalCmd[name] = cmd[name];
|
||||
}
|
||||
|
||||
// Build command namespace
|
||||
const parts = ns.split(/\./);
|
||||
|
||||
// Serialize functions
|
||||
const serializeFunctions =
|
||||
typeof options.serializeFunctions === 'boolean' ? options.serializeFunctions : false;
|
||||
|
||||
// Set up the serialize and ignoreUndefined fields
|
||||
const ignoreUndefined =
|
||||
typeof options.ignoreUndefined === 'boolean' ? options.ignoreUndefined : false;
|
||||
|
||||
// We have a Mongos topology, check if we need to add a readPreference
|
||||
if (topology.type === 'mongos' && readPreference && readPreference.preference !== 'primary') {
|
||||
finalCmd = {
|
||||
$query: finalCmd,
|
||||
$readPreference: readPreference.toJSON()
|
||||
};
|
||||
}
|
||||
|
||||
// optionally decorate query with transaction data
|
||||
const err = decorateWithSessionsData(finalCmd, options.session, options);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
|
||||
// Build Query object
|
||||
const query = new Query(bson, `${parts.shift()}.$cmd`, finalCmd, {
|
||||
numberToSkip: 0,
|
||||
numberToReturn: -1,
|
||||
checkKeys: false,
|
||||
serializeFunctions: serializeFunctions,
|
||||
ignoreUndefined: ignoreUndefined
|
||||
});
|
||||
|
||||
// Set query flags
|
||||
query.slaveOk = readPreference.slaveOk();
|
||||
|
||||
// Return the query
|
||||
return query;
|
||||
}
|
||||
|
||||
module.exports = WireProtocol;
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
'use strict';
|
||||
|
||||
const Query = require('../connection/commands').Query;
|
||||
const Msg = require('../connection/msg').Msg;
|
||||
const MongoError = require('../error').MongoError;
|
||||
const getReadPreference = require('./shared').getReadPreference;
|
||||
const isSharded = require('./shared').isSharded;
|
||||
const databaseNamespace = require('./shared').databaseNamespace;
|
||||
const isTransactionCommand = require('../transactions').isTransactionCommand;
|
||||
const applySession = require('../sessions').applySession;
|
||||
|
||||
function command(server, ns, cmd, options, callback) {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = options || {};
|
||||
|
||||
if (cmd == null) {
|
||||
return callback(new MongoError(`command ${JSON.stringify(cmd)} does not return a cursor`));
|
||||
}
|
||||
|
||||
const bson = server.s.bson;
|
||||
const pool = server.s.pool;
|
||||
const readPreference = getReadPreference(cmd, options);
|
||||
const shouldUseOpMsg = supportsOpMsg(server);
|
||||
const session = options.session;
|
||||
|
||||
let clusterTime = server.clusterTime;
|
||||
let finalCmd = Object.assign({}, cmd);
|
||||
if (hasSessionSupport(server) && session) {
|
||||
if (
|
||||
session.clusterTime &&
|
||||
session.clusterTime.clusterTime.greaterThan(clusterTime.clusterTime)
|
||||
) {
|
||||
clusterTime = session.clusterTime;
|
||||
}
|
||||
|
||||
const err = applySession(session, finalCmd, options);
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
}
|
||||
|
||||
// if we have a known cluster time, gossip it
|
||||
if (clusterTime) {
|
||||
finalCmd.$clusterTime = clusterTime;
|
||||
}
|
||||
|
||||
if (
|
||||
isSharded(server) &&
|
||||
!shouldUseOpMsg &&
|
||||
readPreference &&
|
||||
readPreference.preference !== 'primary'
|
||||
) {
|
||||
finalCmd = {
|
||||
$query: finalCmd,
|
||||
$readPreference: readPreference.toJSON()
|
||||
};
|
||||
}
|
||||
|
||||
const commandOptions = Object.assign(
|
||||
{
|
||||
command: true,
|
||||
numberToSkip: 0,
|
||||
numberToReturn: -1,
|
||||
checkKeys: false
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
// This value is not overridable
|
||||
commandOptions.slaveOk = readPreference.slaveOk();
|
||||
|
||||
const cmdNs = `${databaseNamespace(ns)}.$cmd`;
|
||||
const message = shouldUseOpMsg
|
||||
? new Msg(bson, cmdNs, finalCmd, commandOptions)
|
||||
: new Query(bson, cmdNs, finalCmd, commandOptions);
|
||||
|
||||
const inTransaction = session && (session.inTransaction() || isTransactionCommand(finalCmd));
|
||||
const commandResponseHandler = inTransaction
|
||||
? function(err) {
|
||||
if (
|
||||
!cmd.commitTransaction &&
|
||||
err &&
|
||||
err instanceof MongoError &&
|
||||
err.hasErrorLabel('TransientTransactionError')
|
||||
) {
|
||||
session.transaction.unpinServer();
|
||||
}
|
||||
|
||||
return callback.apply(null, arguments);
|
||||
}
|
||||
: callback;
|
||||
|
||||
try {
|
||||
pool.write(message, commandOptions, commandResponseHandler);
|
||||
} catch (err) {
|
||||
commandResponseHandler(err);
|
||||
}
|
||||
}
|
||||
|
||||
function hasSessionSupport(topology) {
|
||||
if (topology == null) return false;
|
||||
if (topology.description) {
|
||||
return topology.description.maxWireVersion >= 6;
|
||||
}
|
||||
|
||||
return topology.ismaster == null ? false : topology.ismaster.maxWireVersion >= 6;
|
||||
}
|
||||
|
||||
function supportsOpMsg(topologyOrServer) {
|
||||
const description = topologyOrServer.ismaster
|
||||
? topologyOrServer.ismaster
|
||||
: topologyOrServer.description;
|
||||
|
||||
if (description == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return description.maxWireVersion >= 6 && description.__nodejs_mock_server__ == null;
|
||||
}
|
||||
|
||||
module.exports = command;
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
const MIN_SUPPORTED_SERVER_VERSION = '2.6';
|
||||
const MAX_SUPPORTED_SERVER_VERSION = '4.2';
|
||||
const MIN_SUPPORTED_WIRE_VERSION = 2;
|
||||
const MAX_SUPPORTED_WIRE_VERSION = 8;
|
||||
|
||||
module.exports = {
|
||||
MIN_SUPPORTED_SERVER_VERSION,
|
||||
MAX_SUPPORTED_SERVER_VERSION,
|
||||
MIN_SUPPORTED_WIRE_VERSION,
|
||||
MAX_SUPPORTED_WIRE_VERSION
|
||||
};
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
'use strict';
|
||||
|
||||
const GetMore = require('../connection/commands').GetMore;
|
||||
const retrieveBSON = require('../connection/utils').retrieveBSON;
|
||||
const MongoError = require('../error').MongoError;
|
||||
const MongoNetworkError = require('../error').MongoNetworkError;
|
||||
const BSON = retrieveBSON();
|
||||
const Long = BSON.Long;
|
||||
const collectionNamespace = require('./shared').collectionNamespace;
|
||||
const maxWireVersion = require('../utils').maxWireVersion;
|
||||
const applyCommonQueryOptions = require('./shared').applyCommonQueryOptions;
|
||||
const command = require('./command');
|
||||
|
||||
function getMore(server, ns, cursorState, batchSize, options, callback) {
|
||||
options = options || {};
|
||||
|
||||
const wireVersion = maxWireVersion(server);
|
||||
function queryCallback(err, result) {
|
||||
if (err) return callback(err);
|
||||
const response = result.message;
|
||||
|
||||
// If we have a timed out query or a cursor that was killed
|
||||
if (response.cursorNotFound) {
|
||||
return callback(new MongoNetworkError('cursor killed or timed out'), null);
|
||||
}
|
||||
|
||||
if (wireVersion < 4) {
|
||||
const cursorId =
|
||||
typeof response.cursorId === 'number'
|
||||
? Long.fromNumber(response.cursorId)
|
||||
: response.cursorId;
|
||||
|
||||
cursorState.documents = response.documents;
|
||||
cursorState.cursorId = cursorId;
|
||||
|
||||
callback(null, null, response.connection);
|
||||
return;
|
||||
}
|
||||
|
||||
// We have an error detected
|
||||
if (response.documents[0].ok === 0) {
|
||||
return callback(new MongoError(response.documents[0]));
|
||||
}
|
||||
|
||||
// Ensure we have a Long valid cursor id
|
||||
const cursorId =
|
||||
typeof response.documents[0].cursor.id === 'number'
|
||||
? Long.fromNumber(response.documents[0].cursor.id)
|
||||
: response.documents[0].cursor.id;
|
||||
|
||||
cursorState.documents = response.documents[0].cursor.nextBatch;
|
||||
cursorState.cursorId = cursorId;
|
||||
|
||||
callback(null, response.documents[0], response.connection);
|
||||
}
|
||||
|
||||
if (wireVersion < 4) {
|
||||
const bson = server.s.bson;
|
||||
const getMoreOp = new GetMore(bson, ns, cursorState.cursorId, { numberToReturn: batchSize });
|
||||
const queryOptions = applyCommonQueryOptions({}, cursorState);
|
||||
server.s.pool.write(getMoreOp, queryOptions, queryCallback);
|
||||
return;
|
||||
}
|
||||
|
||||
const getMoreCmd = {
|
||||
getMore: cursorState.cursorId,
|
||||
collection: collectionNamespace(ns),
|
||||
batchSize: Math.abs(batchSize)
|
||||
};
|
||||
|
||||
if (cursorState.cmd.tailable && typeof cursorState.cmd.maxAwaitTimeMS === 'number') {
|
||||
getMoreCmd.maxTimeMS = cursorState.cmd.maxAwaitTimeMS;
|
||||
}
|
||||
|
||||
const commandOptions = Object.assign(
|
||||
{
|
||||
returnFieldSelector: null,
|
||||
documentsReturnedIn: 'nextBatch'
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
command(server, ns, getMoreCmd, commandOptions, queryCallback);
|
||||
}
|
||||
|
||||
module.exports = getMore;
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
const writeCommand = require('./write_command');
|
||||
|
||||
module.exports = {
|
||||
insert: function insert(server, ns, ops, options, callback) {
|
||||
writeCommand(server, 'insert', 'documents', ns, ops, options, callback);
|
||||
},
|
||||
update: function update(server, ns, ops, options, callback) {
|
||||
writeCommand(server, 'update', 'updates', ns, ops, options, callback);
|
||||
},
|
||||
remove: function remove(server, ns, ops, options, callback) {
|
||||
writeCommand(server, 'delete', 'deletes', ns, ops, options, callback);
|
||||
},
|
||||
killCursors: require('./kill_cursors'),
|
||||
getMore: require('./get_more'),
|
||||
query: require('./query'),
|
||||
command: require('./command')
|
||||
};
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
'use strict';
|
||||
|
||||
const KillCursor = require('../connection/commands').KillCursor;
|
||||
const MongoError = require('../error').MongoError;
|
||||
const MongoNetworkError = require('../error').MongoNetworkError;
|
||||
const collectionNamespace = require('./shared').collectionNamespace;
|
||||
const maxWireVersion = require('../utils').maxWireVersion;
|
||||
const command = require('./command');
|
||||
|
||||
function killCursors(server, ns, cursorState, callback) {
|
||||
callback = typeof callback === 'function' ? callback : () => {};
|
||||
const cursorId = cursorState.cursorId;
|
||||
|
||||
if (maxWireVersion(server) < 4) {
|
||||
const bson = server.s.bson;
|
||||
const pool = server.s.pool;
|
||||
const killCursor = new KillCursor(bson, ns, [cursorId]);
|
||||
const options = {
|
||||
immediateRelease: true,
|
||||
noResponse: true
|
||||
};
|
||||
|
||||
if (typeof cursorState.session === 'object') {
|
||||
options.session = cursorState.session;
|
||||
}
|
||||
|
||||
if (pool && pool.isConnected()) {
|
||||
try {
|
||||
pool.write(killCursor, options, callback);
|
||||
} catch (err) {
|
||||
if (typeof callback === 'function') {
|
||||
callback(err, null);
|
||||
} else {
|
||||
console.warn(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const killCursorCmd = {
|
||||
killCursors: collectionNamespace(ns),
|
||||
cursors: [cursorId]
|
||||
};
|
||||
|
||||
const options = {};
|
||||
if (typeof cursorState.session === 'object') options.session = cursorState.session;
|
||||
|
||||
command(server, ns, killCursorCmd, options, (err, result) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const response = result.message;
|
||||
if (response.cursorNotFound) {
|
||||
return callback(new MongoNetworkError('cursor killed or timed out'), null);
|
||||
}
|
||||
|
||||
if (!Array.isArray(response.documents) || response.documents.length === 0) {
|
||||
return callback(
|
||||
new MongoError(`invalid killCursors result returned for cursor id ${cursorId}`)
|
||||
);
|
||||
}
|
||||
|
||||
callback(null, response.documents[0]);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = killCursors;
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
'use strict';
|
||||
|
||||
const Query = require('../connection/commands').Query;
|
||||
const MongoError = require('../error').MongoError;
|
||||
const getReadPreference = require('./shared').getReadPreference;
|
||||
const collectionNamespace = require('./shared').collectionNamespace;
|
||||
const isSharded = require('./shared').isSharded;
|
||||
const maxWireVersion = require('../utils').maxWireVersion;
|
||||
const applyCommonQueryOptions = require('./shared').applyCommonQueryOptions;
|
||||
const command = require('./command');
|
||||
|
||||
function query(server, ns, cmd, cursorState, options, callback) {
|
||||
options = options || {};
|
||||
if (cursorState.cursorId != null) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (cmd == null) {
|
||||
return callback(new MongoError(`command ${JSON.stringify(cmd)} does not return a cursor`));
|
||||
}
|
||||
|
||||
if (maxWireVersion(server) < 4) {
|
||||
const query = prepareLegacyFindQuery(server, ns, cmd, cursorState, options);
|
||||
const queryOptions = applyCommonQueryOptions({}, cursorState);
|
||||
if (typeof query.documentsReturnedIn === 'string') {
|
||||
queryOptions.documentsReturnedIn = query.documentsReturnedIn;
|
||||
}
|
||||
|
||||
server.s.pool.write(query, queryOptions, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
const readPreference = getReadPreference(cmd, options);
|
||||
const findCmd = prepareFindCommand(server, ns, cmd, cursorState, options);
|
||||
|
||||
// NOTE: This actually modifies the passed in cmd, and our code _depends_ on this
|
||||
// side-effect. Change this ASAP
|
||||
cmd.virtual = false;
|
||||
|
||||
const commandOptions = Object.assign(
|
||||
{
|
||||
documentsReturnedIn: 'firstBatch',
|
||||
numberToReturn: 1,
|
||||
slaveOk: readPreference.slaveOk()
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
if (cmd.readPreference) commandOptions.readPreference = readPreference;
|
||||
command(server, ns, findCmd, commandOptions, callback);
|
||||
}
|
||||
|
||||
function prepareFindCommand(server, ns, cmd, cursorState) {
|
||||
cursorState.batchSize = cmd.batchSize || cursorState.batchSize;
|
||||
let findCmd = {
|
||||
find: collectionNamespace(ns)
|
||||
};
|
||||
|
||||
if (cmd.query) {
|
||||
if (cmd.query['$query']) {
|
||||
findCmd.filter = cmd.query['$query'];
|
||||
} else {
|
||||
findCmd.filter = cmd.query;
|
||||
}
|
||||
}
|
||||
|
||||
let sortValue = cmd.sort;
|
||||
if (Array.isArray(sortValue)) {
|
||||
const sortObject = {};
|
||||
|
||||
if (sortValue.length > 0 && !Array.isArray(sortValue[0])) {
|
||||
let sortDirection = sortValue[1];
|
||||
if (sortDirection === 'asc') {
|
||||
sortDirection = 1;
|
||||
} else if (sortDirection === 'desc') {
|
||||
sortDirection = -1;
|
||||
}
|
||||
|
||||
sortObject[sortValue[0]] = sortDirection;
|
||||
} else {
|
||||
for (let i = 0; i < sortValue.length; i++) {
|
||||
let sortDirection = sortValue[i][1];
|
||||
if (sortDirection === 'asc') {
|
||||
sortDirection = 1;
|
||||
} else if (sortDirection === 'desc') {
|
||||
sortDirection = -1;
|
||||
}
|
||||
|
||||
sortObject[sortValue[i][0]] = sortDirection;
|
||||
}
|
||||
}
|
||||
|
||||
sortValue = sortObject;
|
||||
}
|
||||
|
||||
if (cmd.sort) findCmd.sort = sortValue;
|
||||
if (cmd.fields) findCmd.projection = cmd.fields;
|
||||
if (cmd.hint) findCmd.hint = cmd.hint;
|
||||
if (cmd.skip) findCmd.skip = cmd.skip;
|
||||
if (cmd.limit) findCmd.limit = cmd.limit;
|
||||
if (cmd.limit < 0) {
|
||||
findCmd.limit = Math.abs(cmd.limit);
|
||||
findCmd.singleBatch = true;
|
||||
}
|
||||
|
||||
if (typeof cmd.batchSize === 'number') {
|
||||
if (cmd.batchSize < 0) {
|
||||
if (cmd.limit !== 0 && Math.abs(cmd.batchSize) < Math.abs(cmd.limit)) {
|
||||
findCmd.limit = Math.abs(cmd.batchSize);
|
||||
}
|
||||
|
||||
findCmd.singleBatch = true;
|
||||
}
|
||||
|
||||
findCmd.batchSize = Math.abs(cmd.batchSize);
|
||||
}
|
||||
|
||||
if (cmd.comment) findCmd.comment = cmd.comment;
|
||||
if (cmd.maxScan) findCmd.maxScan = cmd.maxScan;
|
||||
if (cmd.maxTimeMS) findCmd.maxTimeMS = cmd.maxTimeMS;
|
||||
if (cmd.min) findCmd.min = cmd.min;
|
||||
if (cmd.max) findCmd.max = cmd.max;
|
||||
findCmd.returnKey = cmd.returnKey ? cmd.returnKey : false;
|
||||
findCmd.showRecordId = cmd.showDiskLoc ? cmd.showDiskLoc : false;
|
||||
if (cmd.snapshot) findCmd.snapshot = cmd.snapshot;
|
||||
if (cmd.tailable) findCmd.tailable = cmd.tailable;
|
||||
if (cmd.oplogReplay) findCmd.oplogReplay = cmd.oplogReplay;
|
||||
if (cmd.noCursorTimeout) findCmd.noCursorTimeout = cmd.noCursorTimeout;
|
||||
if (cmd.awaitData) findCmd.awaitData = cmd.awaitData;
|
||||
if (cmd.awaitdata) findCmd.awaitData = cmd.awaitdata;
|
||||
if (cmd.partial) findCmd.partial = cmd.partial;
|
||||
if (cmd.collation) findCmd.collation = cmd.collation;
|
||||
if (cmd.readConcern) findCmd.readConcern = cmd.readConcern;
|
||||
|
||||
// If we have explain, we need to rewrite the find command
|
||||
// to wrap it in the explain command
|
||||
if (cmd.explain) {
|
||||
findCmd = {
|
||||
explain: findCmd
|
||||
};
|
||||
}
|
||||
|
||||
return findCmd;
|
||||
}
|
||||
|
||||
function prepareLegacyFindQuery(server, ns, cmd, cursorState, options) {
|
||||
options = options || {};
|
||||
const bson = server.s.bson;
|
||||
const readPreference = getReadPreference(cmd, options);
|
||||
cursorState.batchSize = cmd.batchSize || cursorState.batchSize;
|
||||
|
||||
let numberToReturn = 0;
|
||||
if (
|
||||
cursorState.limit < 0 ||
|
||||
(cursorState.limit !== 0 && cursorState.limit < cursorState.batchSize) ||
|
||||
(cursorState.limit > 0 && cursorState.batchSize === 0)
|
||||
) {
|
||||
numberToReturn = cursorState.limit;
|
||||
} else {
|
||||
numberToReturn = cursorState.batchSize;
|
||||
}
|
||||
|
||||
const numberToSkip = cursorState.skip || 0;
|
||||
|
||||
const findCmd = {};
|
||||
if (isSharded(server) && readPreference) {
|
||||
findCmd['$readPreference'] = readPreference.toJSON();
|
||||
}
|
||||
|
||||
if (cmd.sort) findCmd['$orderby'] = cmd.sort;
|
||||
if (cmd.hint) findCmd['$hint'] = cmd.hint;
|
||||
if (cmd.snapshot) findCmd['$snapshot'] = cmd.snapshot;
|
||||
if (typeof cmd.returnKey !== 'undefined') findCmd['$returnKey'] = cmd.returnKey;
|
||||
if (cmd.maxScan) findCmd['$maxScan'] = cmd.maxScan;
|
||||
if (cmd.min) findCmd['$min'] = cmd.min;
|
||||
if (cmd.max) findCmd['$max'] = cmd.max;
|
||||
if (typeof cmd.showDiskLoc !== 'undefined') findCmd['$showDiskLoc'] = cmd.showDiskLoc;
|
||||
if (cmd.comment) findCmd['$comment'] = cmd.comment;
|
||||
if (cmd.maxTimeMS) findCmd['$maxTimeMS'] = cmd.maxTimeMS;
|
||||
if (cmd.explain) {
|
||||
// nToReturn must be 0 (match all) or negative (match N and close cursor)
|
||||
// nToReturn > 0 will give explain results equivalent to limit(0)
|
||||
numberToReturn = -Math.abs(cmd.limit || 0);
|
||||
findCmd['$explain'] = true;
|
||||
}
|
||||
|
||||
findCmd['$query'] = cmd.query;
|
||||
if (cmd.readConcern && cmd.readConcern.level !== 'local') {
|
||||
throw new MongoError(
|
||||
`server find command does not support a readConcern level of ${cmd.readConcern.level}`
|
||||
);
|
||||
}
|
||||
|
||||
if (cmd.readConcern) {
|
||||
cmd = Object.assign({}, cmd);
|
||||
delete cmd['readConcern'];
|
||||
}
|
||||
|
||||
const serializeFunctions =
|
||||
typeof options.serializeFunctions === 'boolean' ? options.serializeFunctions : false;
|
||||
const ignoreUndefined =
|
||||
typeof options.ignoreUndefined === 'boolean' ? options.ignoreUndefined : false;
|
||||
|
||||
const query = new Query(bson, ns, findCmd, {
|
||||
numberToSkip: numberToSkip,
|
||||
numberToReturn: numberToReturn,
|
||||
pre32Limit: typeof cmd.limit !== 'undefined' ? cmd.limit : undefined,
|
||||
checkKeys: false,
|
||||
returnFieldSelector: cmd.fields,
|
||||
serializeFunctions: serializeFunctions,
|
||||
ignoreUndefined: ignoreUndefined
|
||||
});
|
||||
|
||||
if (typeof cmd.tailable === 'boolean') query.tailable = cmd.tailable;
|
||||
if (typeof cmd.oplogReplay === 'boolean') query.oplogReplay = cmd.oplogReplay;
|
||||
if (typeof cmd.noCursorTimeout === 'boolean') query.noCursorTimeout = cmd.noCursorTimeout;
|
||||
if (typeof cmd.awaitData === 'boolean') query.awaitData = cmd.awaitData;
|
||||
if (typeof cmd.partial === 'boolean') query.partial = cmd.partial;
|
||||
|
||||
query.slaveOk = readPreference.slaveOk();
|
||||
return query;
|
||||
}
|
||||
|
||||
module.exports = query;
|
||||
+68
-8
@@ -1,9 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
var ReadPreference = require('../topologies/read_preference'),
|
||||
MongoError = require('../error').MongoError;
|
||||
const ReadPreference = require('../topologies/read_preference');
|
||||
const MongoError = require('../error').MongoError;
|
||||
const ServerType = require('../sdam/server_description').ServerType;
|
||||
const TopologyDescription = require('../sdam/topology_description').TopologyDescription;
|
||||
|
||||
var MESSAGE_HEADER_SIZE = 16;
|
||||
const MESSAGE_HEADER_SIZE = 16;
|
||||
const COMPRESSION_DETAILS_SIZE = 9; // originalOpcode + uncompressedSize, compressorID
|
||||
|
||||
// OPCODE Numbers
|
||||
// Defined at https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/#request-opcodes
|
||||
@@ -15,7 +18,8 @@ var opcodes = {
|
||||
OP_GETMORE: 2005,
|
||||
OP_DELETE: 2006,
|
||||
OP_KILL_CURSORS: 2007,
|
||||
OP_COMPRESSED: 2012
|
||||
OP_COMPRESSED: 2012,
|
||||
OP_MSG: 2013
|
||||
};
|
||||
|
||||
var getReadPreference = function(cmd, options) {
|
||||
@@ -47,9 +51,65 @@ var parseHeader = function(message) {
|
||||
};
|
||||
};
|
||||
|
||||
function applyCommonQueryOptions(queryOptions, options) {
|
||||
Object.assign(queryOptions, {
|
||||
raw: typeof options.raw === 'boolean' ? options.raw : false,
|
||||
promoteLongs: typeof options.promoteLongs === 'boolean' ? options.promoteLongs : true,
|
||||
promoteValues: typeof options.promoteValues === 'boolean' ? options.promoteValues : true,
|
||||
promoteBuffers: typeof options.promoteBuffers === 'boolean' ? options.promoteBuffers : false,
|
||||
monitoring: typeof options.monitoring === 'boolean' ? options.monitoring : false,
|
||||
fullResult: typeof options.fullResult === 'boolean' ? options.fullResult : false
|
||||
});
|
||||
|
||||
if (typeof options.socketTimeout === 'number') {
|
||||
queryOptions.socketTimeout = options.socketTimeout;
|
||||
}
|
||||
|
||||
if (options.session) {
|
||||
queryOptions.session = options.session;
|
||||
}
|
||||
|
||||
if (typeof options.documentsReturnedIn === 'string') {
|
||||
queryOptions.documentsReturnedIn = options.documentsReturnedIn;
|
||||
}
|
||||
|
||||
return queryOptions;
|
||||
}
|
||||
|
||||
function isSharded(topologyOrServer) {
|
||||
if (topologyOrServer.type === 'mongos') return true;
|
||||
if (topologyOrServer.description && topologyOrServer.description.type === ServerType.Mongos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// NOTE: This is incredibly inefficient, and should be removed once command construction
|
||||
// happens based on `Server` not `Topology`.
|
||||
if (topologyOrServer.description && topologyOrServer.description instanceof TopologyDescription) {
|
||||
const servers = Array.from(topologyOrServer.description.servers.values());
|
||||
return servers.some(server => server.type === ServerType.Mongos);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function databaseNamespace(ns) {
|
||||
return ns.split('.')[0];
|
||||
}
|
||||
function collectionNamespace(ns) {
|
||||
return ns
|
||||
.split('.')
|
||||
.slice(1)
|
||||
.join('.');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getReadPreference: getReadPreference,
|
||||
MESSAGE_HEADER_SIZE: MESSAGE_HEADER_SIZE,
|
||||
opcodes: opcodes,
|
||||
parseHeader: parseHeader
|
||||
getReadPreference,
|
||||
MESSAGE_HEADER_SIZE,
|
||||
COMPRESSION_DETAILS_SIZE,
|
||||
opcodes,
|
||||
parseHeader,
|
||||
applyCommonQueryOptions,
|
||||
isSharded,
|
||||
databaseNamespace,
|
||||
collectionNamespace
|
||||
};
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
'use strict';
|
||||
|
||||
const MongoError = require('../error').MongoError;
|
||||
const collectionNamespace = require('./shared').collectionNamespace;
|
||||
const command = require('./command');
|
||||
|
||||
function writeCommand(server, type, opsField, ns, ops, options, callback) {
|
||||
if (ops.length === 0) throw new MongoError(`${type} must contain at least one document`);
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
const ordered = typeof options.ordered === 'boolean' ? options.ordered : true;
|
||||
const writeConcern = options.writeConcern;
|
||||
|
||||
const writeCommand = {};
|
||||
writeCommand[type] = collectionNamespace(ns);
|
||||
writeCommand[opsField] = ops;
|
||||
writeCommand.ordered = ordered;
|
||||
|
||||
if (writeConcern && Object.keys(writeConcern).length > 0) {
|
||||
writeCommand.writeConcern = writeConcern;
|
||||
}
|
||||
|
||||
if (options.collation) {
|
||||
for (let i = 0; i < writeCommand[opsField].length; i++) {
|
||||
if (!writeCommand[opsField][i].collation) {
|
||||
writeCommand[opsField][i].collation = options.collation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (options.bypassDocumentValidation === true) {
|
||||
writeCommand.bypassDocumentValidation = options.bypassDocumentValidation;
|
||||
}
|
||||
|
||||
const commandOptions = Object.assign(
|
||||
{
|
||||
checkKeys: type === 'insert',
|
||||
numberToReturn: 1
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
command(server, ns, writeCommand, commandOptions, callback);
|
||||
}
|
||||
|
||||
module.exports = writeCommand;
|
||||
+16
-16
@@ -1,28 +1,28 @@
|
||||
{
|
||||
"_from": "mongodb-core@3.1.9",
|
||||
"_id": "mongodb-core@3.1.9",
|
||||
"_from": "mongodb-core@3.2.7",
|
||||
"_id": "mongodb-core@3.2.7",
|
||||
"_inBundle": false,
|
||||
"_integrity": "sha512-MJpciDABXMchrZphh3vMcqu8hkNf/Mi+Gk6btOimVg1XMxLXh87j6FAvRm+KmwD1A9fpu3qRQYcbQe4egj23og==",
|
||||
"_integrity": "sha512-WypKdLxFNPOH/Jy6i9z47IjG2wIldA54iDZBmHMINcgKOUcWJh8og+Wix76oGd7EyYkHJKssQ2FAOw5Su/n4XQ==",
|
||||
"_location": "/mongodb-core",
|
||||
"_phantomChildren": {},
|
||||
"_requested": {
|
||||
"type": "version",
|
||||
"registry": true,
|
||||
"raw": "mongodb-core@3.1.9",
|
||||
"raw": "mongodb-core@3.2.7",
|
||||
"name": "mongodb-core",
|
||||
"escapedName": "mongodb-core",
|
||||
"rawSpec": "3.1.9",
|
||||
"rawSpec": "3.2.7",
|
||||
"saveSpec": null,
|
||||
"fetchSpec": "3.1.9"
|
||||
"fetchSpec": "3.2.7"
|
||||
},
|
||||
"_requiredBy": [
|
||||
"/mongodb",
|
||||
"/mongoose"
|
||||
],
|
||||
"_resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.1.9.tgz",
|
||||
"_shasum": "c31ee407bf932b0149eaed775c17ee09974e4ca3",
|
||||
"_spec": "mongodb-core@3.1.9",
|
||||
"_where": "D:\\Gitterra\\om\\node_modules\\mongoose",
|
||||
"_resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.2.7.tgz",
|
||||
"_shasum": "a8ef1fe764a192c979252dacbc600dc88d77e28f",
|
||||
"_spec": "mongodb-core@3.2.7",
|
||||
"_where": "/home/erik/Documents/workspace_brackets/a1_BME_Project_Ohm/om/node_modules/mongodb",
|
||||
"author": {
|
||||
"name": "Christian Kvalheim"
|
||||
},
|
||||
@@ -31,7 +31,7 @@
|
||||
},
|
||||
"bundleDependencies": false,
|
||||
"dependencies": {
|
||||
"bson": "^1.1.0",
|
||||
"bson": "^1.1.1",
|
||||
"require_optional": "^1.0.1",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"saslprep": "^1.0.0"
|
||||
@@ -39,15 +39,15 @@
|
||||
"deprecated": false,
|
||||
"description": "Core MongoDB driver functionality, no bells and whistles and meant for integration not end applications",
|
||||
"devDependencies": {
|
||||
"chai": "^4.1.2",
|
||||
"chai": "^4.2.0",
|
||||
"chai-subset": "^1.6.0",
|
||||
"co": "^4.6.0",
|
||||
"eslint": "^4.6.1",
|
||||
"eslint-plugin-prettier": "^2.2.0",
|
||||
"jsdoc": "3.5.4",
|
||||
"mongodb-extjson": "^2.1.2",
|
||||
"mongodb-mock-server": "^1.0.0",
|
||||
"mongodb-test-runner": "^1.1.18",
|
||||
"mongodb-mock-server": "^1.0.1",
|
||||
"mongodb-test-runner": "^1.3.4",
|
||||
"prettier": "~1.12.0",
|
||||
"sinon": "^6.0.0",
|
||||
"snappy": "^6.1.1",
|
||||
@@ -82,10 +82,10 @@
|
||||
"atlas": "node ./test/atlas.js",
|
||||
"changelog": "conventional-changelog -p angular -i HISTORY.md -s",
|
||||
"coverage": "nyc node test/runner.js -t functional -l && node_modules/.bin/nyc report --reporter=text-lcov | node_modules/.bin/coveralls",
|
||||
"format": "prettier --print-width 100 --tab-width 2 --single-quote --write index.js 'test/**/*.js' 'lib/**/*.js'",
|
||||
"format": "prettier --print-width 100 --tab-width 2 --single-quote --write index.js test/**/*.js lib/**/*.js",
|
||||
"lint": "eslint index.js lib test",
|
||||
"release": "standard-version -i HISTORY.md",
|
||||
"test": "npm run lint && mongodb-test-runner -t 60000 test/tests"
|
||||
},
|
||||
"version": "3.1.9"
|
||||
"version": "3.2.7"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user