1 /**
  2  * Wrapper around a mongobridge process. Construction of a MongoBridge instance will start a new
  3  * mongobridge process that listens on 'options.port' and forwards messages to 'options.dest'.
  4  *
  5  * @param {Object} options
  6  * @param {string} options.dest - The host:port to forward messages to.
  7  * @param {string} [options.hostName=localhost] - The hostname to specify when connecting to the
  8  * mongobridge process.
  9  * @param {number} [options.port=allocatePort()] - The port number the mongobridge should listen on.
 10  *
 11  * @returns {Proxy} Acts as a typical connection object to options.hostName:options.port that has
 12  * additional functions exposed to shape network traffic from other processes.
 13  */
 14 function MongoBridge(options) {
 15     'use strict';
 16 
 17     if (!(this instanceof MongoBridge)) {
 18         return new MongoBridge(options);
 19     }
 20 
 21     options = options || {};
 22     if (!options.hasOwnProperty('dest')) {
 23         throw new Error('Missing required field "dest"');
 24     }
 25 
 26     var hostName = options.hostName || 'localhost';
 27 
 28     this.dest = options.dest;
 29     this.port = options.port || allocatePort();
 30 
 31     // The connection used by a test for running commands against the mongod or mongos process.
 32     var userConn;
 33 
 34     // A separate (hidden) connection for configuring the mongobridge process.
 35     var controlConn;
 36 
 37     // Start the mongobridge on port 'this.port' routing network traffic to 'this.dest'.
 38     var args = ['mongobridge', '--port', this.port, '--dest', this.dest];
 39     var keysToSkip = [
 40         'dest',
 41         'hostName',
 42         'port',
 43     ];
 44 
 45     // Append any command line arguments that are optional for mongobridge.
 46     Object.keys(options).forEach(function(key) {
 47         if (Array.contains(keysToSkip, key)) {
 48             return;
 49         }
 50 
 51         var value = options[key];
 52         if (value === null || value === undefined) {
 53             throw new Error("Value '" + value + "' for '" + key + "' option is ambiguous; specify" +
 54                             " {flag: ''} to add --flag command line options'");
 55         }
 56 
 57         args.push('--' + key);
 58         if (value !== '') {
 59             args.push(value.toString());
 60         }
 61     });
 62 
 63     var pid = _startMongoProgram.apply(null, args);
 64 
 65     /**
 66      * Initializes the mongo shell's connections to the mongobridge process. Throws an error if the
 67      * mongobridge process stopped running or if a connection cannot be made.
 68      *
 69      * The mongod or mongos process corresponding to this mongobridge process may need to connect to
 70      * itself through the mongobridge process, e.g. when running the _isSelf command. This means
 71      * the mongobridge process needs to be running prior to the other process. However, to avoid
 72      * spurious failures during situations where the mongod or mongos process is not ready to accept
 73      * connections, connections to the mongobridge process should only be made after the other
 74      * process is known to be reachable:
 75      *
 76      *     var bridge = new MongoBridge(...);
 77      *     var conn = MongoRunner.runMongoXX(...);
 78      *     assert.neq(null, conn);
 79      *     bridge.connectToBridge();
 80      */
 81     this.connectToBridge = function connectToBridge() {
 82         var failedToStart = false;
 83         assert.soon(() => {
 84             if (!checkProgram(pid)) {
 85                 failedToStart = true;
 86                 return true;
 87             }
 88 
 89             try {
 90                 userConn = new Mongo(hostName + ':' + this.port);
 91             } catch (e) {
 92                 return false;
 93             }
 94             return true;
 95         }, 'failed to connect to the mongobridge on port ' + this.port);
 96         assert(!failedToStart, 'mongobridge failed to start on port ' + this.port);
 97 
 98         // The MongoRunner.runMongoXX() functions define a 'name' property on the returned
 99         // connection object that is equivalent to its 'host' property. Certain functions in
100         // ReplSetTest and ShardingTest use the 'name' property instead of the 'host' property, so
101         // we define it here for consistency.
102         Object.defineProperty(userConn, 'name', {
103             enumerable: true,
104             get: function() {
105                 return this.host;
106             },
107         });
108 
109         controlConn = new Mongo(hostName + ':' + this.port);
110     };
111 
112     /**
113      * Terminates the mongobridge process.
114      */
115     this.stop = function stop() {
116         _stopMongoProgram(this.port);
117     };
118 
119     // Throws an error if 'obj' is not a MongoBridge instance.
120     function throwErrorIfNotMongoBridgeInstance(obj) {
121         if (!(obj instanceof MongoBridge)) {
122             throw new Error('Expected MongoBridge instance, but got ' + tojson(obj));
123         }
124     }
125 
126     // Runs a command intended to configure the mongobridge.
127     function runBridgeCommand(conn, cmdName, cmdArgs) {
128         // The wire version of this mongobridge is detected as the wire version of the corresponding
129         // mongod or mongos process because the message is simply forwarded to that process.
130         // Commands to configure the mongobridge process must support being sent as an OP_QUERY
131         // message in order to handle when the mongobridge is a proxy for a mongos process or when
132         // --readMode=legacy is passed to the mongo shell. Create a new Object with 'cmdName' as the
133         // first key and $forBridge=true.
134         var cmdObj = {};
135         cmdObj[cmdName] = 1;
136         cmdObj.$forBridge = true;
137         Object.extend(cmdObj, cmdArgs);
138 
139         var dbName = 'test';
140         var noQueryOptions = 0;
141         return conn.runCommand(dbName, cmdObj, noQueryOptions);
142     }
143 
144     /**
145      * Allows communication between 'this.dest' and the 'dest' of each of the 'bridges'.
146      *
147      * Configures 'this' bridge to accept new connections from the 'dest' of each of the 'bridges'.
148      * Additionally configures each of the 'bridges' to accept new connections from 'this.dest'.
149      *
150      * @param {(MongoBridge|MongoBridge[])} bridges
151      */
152     this.reconnect = function reconnect(bridges) {
153         if (!Array.isArray(bridges)) {
154             bridges = [bridges];
155         }
156         bridges.forEach(throwErrorIfNotMongoBridgeInstance);
157 
158         this.acceptConnectionsFrom(bridges);
159         bridges.forEach(bridge => bridge.acceptConnectionsFrom(this));
160     };
161 
162     /**
163      * Disallows communication between 'this.dest' and the 'dest' of each of the 'bridges'.
164      *
165      * Configures 'this' bridge to close existing connections and reject new connections from the
166      * 'dest' of each of the 'bridges'. Additionally configures each of the 'bridges' to close
167      * existing connections and reject new connections from 'this.dest'.
168      *
169      * @param {(MongoBridge|MongoBridge[])} bridges
170      */
171     this.disconnect = function disconnect(bridges) {
172         if (!Array.isArray(bridges)) {
173             bridges = [bridges];
174         }
175         bridges.forEach(throwErrorIfNotMongoBridgeInstance);
176 
177         this.rejectConnectionsFrom(bridges);
178         bridges.forEach(bridge => bridge.rejectConnectionsFrom(this));
179     };
180 
181     /**
182      * Configures 'this' bridge to accept new connections from the 'dest' of each of the 'bridges'.
183      *
184      * @param {(MongoBridge|MongoBridge[])} bridges
185      */
186     this.acceptConnectionsFrom = function acceptConnectionsFrom(bridges) {
187         if (!Array.isArray(bridges)) {
188             bridges = [bridges];
189         }
190         bridges.forEach(throwErrorIfNotMongoBridgeInstance);
191 
192         bridges.forEach(bridge => {
193             var res = runBridgeCommand(controlConn, 'acceptConnectionsFrom', {host: bridge.dest});
194             assert.commandWorked(res,
195                                  'failed to configure the mongobridge listening on port ' +
196                                      this.port + ' to accept new connections from ' + bridge.dest);
197         });
198     };
199 
200     /**
201      * Configures 'this' bridge to close existing connections and reject new connections from the
202      * 'dest' of each of the 'bridges'.
203      *
204      * @param {(MongoBridge|MongoBridge[])} bridges
205      */
206     this.rejectConnectionsFrom = function rejectConnectionsFrom(bridges) {
207         if (!Array.isArray(bridges)) {
208             bridges = [bridges];
209         }
210         bridges.forEach(throwErrorIfNotMongoBridgeInstance);
211 
212         bridges.forEach(bridge => {
213             var res = runBridgeCommand(controlConn, 'rejectConnectionsFrom', {host: bridge.dest});
214             assert.commandWorked(res,
215                                  'failed to configure the mongobridge listening on port ' +
216                                      this.port + ' to hang up connections from ' + bridge.dest);
217         });
218     };
219 
220     /**
221      * Configures 'this' bridge to delay forwarding requests from the 'dest' of each of the
222      * 'bridges' to 'this.dest' by the specified amount.
223      *
224      * @param {(MongoBridge|MongoBridge[])} bridges
225      * @param {number} delay - The delay to apply in milliseconds.
226      */
227     this.delayMessagesFrom = function delayMessagesFrom(bridges, delay) {
228         if (!Array.isArray(bridges)) {
229             bridges = [bridges];
230         }
231         bridges.forEach(throwErrorIfNotMongoBridgeInstance);
232 
233         bridges.forEach(bridge => {
234             var res = runBridgeCommand(controlConn, 'delayMessagesFrom', {
235                 host: bridge.dest,
236                 delay: delay,
237             });
238             assert.commandWorked(res,
239                                  'failed to configure the mongobridge listening on port ' +
240                                      this.port + ' to delay messages from ' + bridge.dest + ' by ' +
241                                      delay + ' milliseconds');
242         });
243     };
244 
245     /**
246      * Configures 'this' bridge to uniformly discard requests from the 'dest' of each of the
247      * 'bridges' to 'this.dest' with probability 'lossProbability'.
248      *
249      * @param {(MongoBridge|MongoBridge[])} bridges
250      * @param {number} lossProbability
251      */
252     this.discardMessagesFrom = function discardMessagesFrom(bridges, lossProbability) {
253         if (!Array.isArray(bridges)) {
254             bridges = [bridges];
255         }
256         bridges.forEach(throwErrorIfNotMongoBridgeInstance);
257 
258         bridges.forEach(bridge => {
259             var res = runBridgeCommand(controlConn, 'discardMessagesFrom', {
260                 host: bridge.dest,
261                 loss: lossProbability,
262             });
263             assert.commandWorked(res,
264                                  'failed to configure the mongobridge listening on port ' +
265                                      this.port + ' to discard messages from ' + bridge.dest +
266                                      ' with probability ' + lossProbability);
267         });
268     };
269 
270     // Use a Proxy to "extend" the underlying connection object. The C++ functions, e.g.
271     // runCommand(), require that they are called on the Mongo instance itself and so typical
272     // prototypical inheritance isn't possible.
273     return new Proxy(this, {
274         get: function get(target, property, receiver) {
275             // If the property is defined on the MongoBridge instance itself, then
276             // return it.
277             // Otherwise, get the value of the property from the Mongo instance.
278             if (target.hasOwnProperty(property)) {
279                 return target[property];
280             }
281             var value = userConn[property];
282             if (typeof value === 'function') {
283                 return value.bind(userConn);
284             }
285             return value;
286         },
287 
288         set: function set(target, property, value, receiver) {
289             // Delegate setting the value of any property to the Mongo instance so
290             // that it can be
291             // accessed in functions acting on the Mongo instance directly instead of
292             // this Proxy.
293             // For example, the "slaveOk" property needs to be set on the Mongo
294             // instance in order
295             // for the query options bit to be set correctly.
296             userConn[property] = value;
297             return true;
298         },
299     });
300 }
301