You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					197 lines
				
				6.5 KiB
			
		
		
			
		
	
	
					197 lines
				
				6.5 KiB
			| 
								 
											3 years ago
										 
									 | 
							
								'use strict';
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const Duplex = require('stream').Duplex;
							 | 
						||
| 
								 | 
							
								const BufferList = require('bl');
							 | 
						||
| 
								 | 
							
								const MongoParseError = require('../core/error').MongoParseError;
							 | 
						||
| 
								 | 
							
								const decompress = require('../core/wireprotocol/compression').decompress;
							 | 
						||
| 
								 | 
							
								const Response = require('../core/connection/commands').Response;
							 | 
						||
| 
								 | 
							
								const BinMsg = require('../core/connection/msg').BinMsg;
							 | 
						||
| 
								 | 
							
								const MongoError = require('../core/error').MongoError;
							 | 
						||
| 
								 | 
							
								const OP_COMPRESSED = require('../core/wireprotocol/shared').opcodes.OP_COMPRESSED;
							 | 
						||
| 
								 | 
							
								const OP_MSG = require('../core/wireprotocol/shared').opcodes.OP_MSG;
							 | 
						||
| 
								 | 
							
								const MESSAGE_HEADER_SIZE = require('../core/wireprotocol/shared').MESSAGE_HEADER_SIZE;
							 | 
						||
| 
								 | 
							
								const COMPRESSION_DETAILS_SIZE = require('../core/wireprotocol/shared').COMPRESSION_DETAILS_SIZE;
							 | 
						||
| 
								 | 
							
								const opcodes = require('../core/wireprotocol/shared').opcodes;
							 | 
						||
| 
								 | 
							
								const compress = require('../core/wireprotocol/compression').compress;
							 | 
						||
| 
								 | 
							
								const compressorIDs = require('../core/wireprotocol/compression').compressorIDs;
							 | 
						||
| 
								 | 
							
								const uncompressibleCommands = require('../core/wireprotocol/compression').uncompressibleCommands;
							 | 
						||
| 
								 | 
							
								const Msg = require('../core/connection/msg').Msg;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const kDefaultMaxBsonMessageSize = 1024 * 1024 * 16 * 4;
							 | 
						||
| 
								 | 
							
								const kBuffer = Symbol('buffer');
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * A duplex stream that is capable of reading and writing raw wire protocol messages, with
							 | 
						||
| 
								 | 
							
								 * support for optional compression
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								class MessageStream extends Duplex {
							 | 
						||
| 
								 | 
							
								  constructor(options) {
							 | 
						||
| 
								 | 
							
								    options = options || {};
							 | 
						||
| 
								 | 
							
								    super(options);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    this.bson = options.bson;
							 | 
						||
| 
								 | 
							
								    this.maxBsonMessageSize = options.maxBsonMessageSize || kDefaultMaxBsonMessageSize;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    this[kBuffer] = new BufferList();
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  _write(chunk, _, callback) {
							 | 
						||
| 
								 | 
							
								    const buffer = this[kBuffer];
							 | 
						||
| 
								 | 
							
								    buffer.append(chunk);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    processIncomingData(this, callback);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  _read(/* size */) {
							 | 
						||
| 
								 | 
							
								    // NOTE: This implementation is empty because we explicitly push data to be read
							 | 
						||
| 
								 | 
							
								    //       when `writeMessage` is called.
							 | 
						||
| 
								 | 
							
								    return;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  writeCommand(command, operationDescription) {
							 | 
						||
| 
								 | 
							
								    // TODO: agreed compressor should live in `StreamDescription`
							 | 
						||
| 
								 | 
							
								    const shouldCompress = operationDescription && !!operationDescription.agreedCompressor;
							 | 
						||
| 
								 | 
							
								    if (!shouldCompress || !canCompress(command)) {
							 | 
						||
| 
								 | 
							
								      const data = command.toBin();
							 | 
						||
| 
								 | 
							
								      this.push(Array.isArray(data) ? Buffer.concat(data) : data);
							 | 
						||
| 
								 | 
							
								      return;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // otherwise, compress the message
							 | 
						||
| 
								 | 
							
								    const concatenatedOriginalCommandBuffer = Buffer.concat(command.toBin());
							 | 
						||
| 
								 | 
							
								    const messageToBeCompressed = concatenatedOriginalCommandBuffer.slice(MESSAGE_HEADER_SIZE);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // Extract information needed for OP_COMPRESSED from the uncompressed message
							 | 
						||
| 
								 | 
							
								    const originalCommandOpCode = concatenatedOriginalCommandBuffer.readInt32LE(12);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // Compress the message body
							 | 
						||
| 
								 | 
							
								    compress({ options: operationDescription }, messageToBeCompressed, (err, compressedMessage) => {
							 | 
						||
| 
								 | 
							
								      if (err) {
							 | 
						||
| 
								 | 
							
								        operationDescription.cb(err, null);
							 | 
						||
| 
								 | 
							
								        return;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      // Create the msgHeader of OP_COMPRESSED
							 | 
						||
| 
								 | 
							
								      const msgHeader = Buffer.alloc(MESSAGE_HEADER_SIZE);
							 | 
						||
| 
								 | 
							
								      msgHeader.writeInt32LE(
							 | 
						||
| 
								 | 
							
								        MESSAGE_HEADER_SIZE + COMPRESSION_DETAILS_SIZE + compressedMessage.length,
							 | 
						||
| 
								 | 
							
								        0
							 | 
						||
| 
								 | 
							
								      ); // messageLength
							 | 
						||
| 
								 | 
							
								      msgHeader.writeInt32LE(command.requestId, 4); // requestID
							 | 
						||
| 
								 | 
							
								      msgHeader.writeInt32LE(0, 8); // responseTo (zero)
							 | 
						||
| 
								 | 
							
								      msgHeader.writeInt32LE(opcodes.OP_COMPRESSED, 12); // opCode
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      // Create the compression details of OP_COMPRESSED
							 | 
						||
| 
								 | 
							
								      const compressionDetails = Buffer.alloc(COMPRESSION_DETAILS_SIZE);
							 | 
						||
| 
								 | 
							
								      compressionDetails.writeInt32LE(originalCommandOpCode, 0); // originalOpcode
							 | 
						||
| 
								 | 
							
								      compressionDetails.writeInt32LE(messageToBeCompressed.length, 4); // Size of the uncompressed compressedMessage, excluding the MsgHeader
							 | 
						||
| 
								 | 
							
								      compressionDetails.writeUInt8(compressorIDs[operationDescription.agreedCompressor], 8); // compressorID
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      this.push(Buffer.concat([msgHeader, compressionDetails, compressedMessage]));
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								// Return whether a command contains an uncompressible command term
							 | 
						||
| 
								 | 
							
								// Will return true if command contains no uncompressible command terms
							 | 
						||
| 
								 | 
							
								function canCompress(command) {
							 | 
						||
| 
								 | 
							
								  const commandDoc = command instanceof Msg ? command.command : command.query;
							 | 
						||
| 
								 | 
							
								  const commandName = Object.keys(commandDoc)[0];
							 | 
						||
| 
								 | 
							
								  return !uncompressibleCommands.has(commandName);
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function processIncomingData(stream, callback) {
							 | 
						||
| 
								 | 
							
								  const buffer = stream[kBuffer];
							 | 
						||
| 
								 | 
							
								  if (buffer.length < 4) {
							 | 
						||
| 
								 | 
							
								    callback();
							 | 
						||
| 
								 | 
							
								    return;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const sizeOfMessage = buffer.readInt32LE(0);
							 | 
						||
| 
								 | 
							
								  if (sizeOfMessage < 0) {
							 | 
						||
| 
								 | 
							
								    callback(new MongoParseError(`Invalid message size: ${sizeOfMessage}`));
							 | 
						||
| 
								 | 
							
								    return;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (sizeOfMessage > stream.maxBsonMessageSize) {
							 | 
						||
| 
								 | 
							
								    callback(
							 | 
						||
| 
								 | 
							
								      new MongoParseError(
							 | 
						||
| 
								 | 
							
								        `Invalid message size: ${sizeOfMessage}, max allowed: ${stream.maxBsonMessageSize}`
							 | 
						||
| 
								 | 
							
								      )
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 | 
							
								    return;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (sizeOfMessage > buffer.length) {
							 | 
						||
| 
								 | 
							
								    callback();
							 | 
						||
| 
								 | 
							
								    return;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const message = buffer.slice(0, sizeOfMessage);
							 | 
						||
| 
								 | 
							
								  buffer.consume(sizeOfMessage);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const messageHeader = {
							 | 
						||
| 
								 | 
							
								    length: message.readInt32LE(0),
							 | 
						||
| 
								 | 
							
								    requestId: message.readInt32LE(4),
							 | 
						||
| 
								 | 
							
								    responseTo: message.readInt32LE(8),
							 | 
						||
| 
								 | 
							
								    opCode: message.readInt32LE(12)
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  let ResponseType = messageHeader.opCode === OP_MSG ? BinMsg : Response;
							 | 
						||
| 
								 | 
							
								  const responseOptions = stream.responseOptions;
							 | 
						||
| 
								 | 
							
								  if (messageHeader.opCode !== OP_COMPRESSED) {
							 | 
						||
| 
								 | 
							
								    const messageBody = message.slice(MESSAGE_HEADER_SIZE);
							 | 
						||
| 
								 | 
							
								    stream.emit(
							 | 
						||
| 
								 | 
							
								      'message',
							 | 
						||
| 
								 | 
							
								      new ResponseType(stream.bson, message, messageHeader, messageBody, responseOptions)
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (buffer.length >= 4) {
							 | 
						||
| 
								 | 
							
								      processIncomingData(stream, callback);
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      callback();
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  messageHeader.fromCompressed = true;
							 | 
						||
| 
								 | 
							
								  messageHeader.opCode = message.readInt32LE(MESSAGE_HEADER_SIZE);
							 | 
						||
| 
								 | 
							
								  messageHeader.length = message.readInt32LE(MESSAGE_HEADER_SIZE + 4);
							 | 
						||
| 
								 | 
							
								  const compressorID = message[MESSAGE_HEADER_SIZE + 8];
							 | 
						||
| 
								 | 
							
								  const compressedBuffer = message.slice(MESSAGE_HEADER_SIZE + 9);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // recalculate based on wrapped opcode
							 | 
						||
| 
								 | 
							
								  ResponseType = messageHeader.opCode === OP_MSG ? BinMsg : Response;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  decompress(compressorID, compressedBuffer, (err, messageBody) => {
							 | 
						||
| 
								 | 
							
								    if (err) {
							 | 
						||
| 
								 | 
							
								      callback(err);
							 | 
						||
| 
								 | 
							
								      return;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (messageBody.length !== messageHeader.length) {
							 | 
						||
| 
								 | 
							
								      callback(
							 | 
						||
| 
								 | 
							
								        new MongoError(
							 | 
						||
| 
								 | 
							
								          'Decompressing a compressed message from the server failed. The message is corrupt.'
							 | 
						||
| 
								 | 
							
								        )
							 | 
						||
| 
								 | 
							
								      );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      return;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    stream.emit(
							 | 
						||
| 
								 | 
							
								      'message',
							 | 
						||
| 
								 | 
							
								      new ResponseType(stream.bson, message, messageHeader, messageBody, responseOptions)
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (buffer.length >= 4) {
							 | 
						||
| 
								 | 
							
								      processIncomingData(stream, callback);
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      callback();
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  });
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								module.exports = MessageStream;
							 |