/*! * ws: a node.js websocket client * Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com> * MIT Licensed */ var util = require('util') , Validation = require('./Validation').Validation , ErrorCodes = require('./ErrorCodes') , BufferPool = require('./BufferPool') , bufferUtil = require('./BufferUtil').BufferUtil , PerMessageDeflate = require('./PerMessageDeflate'); /** * HyBi Receiver implementation */ function Receiver (extensions,maxPayload) { if (this instanceof Receiver === false) { throw new TypeError("Classes can't be function-called"); } if(typeof extensions==='number'){ maxPayload=extensions; extensions={}; } // memory pool for fragmented messages var fragmentedPoolPrevUsed = -1; this.fragmentedBufferPool = new BufferPool(1024, function(db, length) { return db.used + length; }, function(db) { return fragmentedPoolPrevUsed = fragmentedPoolPrevUsed >= 0 ? Math.ceil((fragmentedPoolPrevUsed + db.used) / 2) : db.used; }); // memory pool for unfragmented messages var unfragmentedPoolPrevUsed = -1; this.unfragmentedBufferPool = new BufferPool(1024, function(db, length) { return db.used + length; }, function(db) { return unfragmentedPoolPrevUsed = unfragmentedPoolPrevUsed >= 0 ? Math.ceil((unfragmentedPoolPrevUsed + db.used) / 2) : db.used; }); this.extensions = extensions || {}; this.maxPayload = maxPayload || 0; this.currentPayloadLength = 0; this.state = { activeFragmentedOperation: null, lastFragment: false, masked: false, opcode: 0, fragmentedOperation: false }; this.overflow = []; this.headerBuffer = new Buffer(10); this.expectOffset = 0; this.expectBuffer = null; this.expectHandler = null; this.currentMessage = []; this.currentMessageLength = 0; this.messageHandlers = []; this.expectHeader(2, this.processPacket); this.dead = false; this.processing = false; this.onerror = function() {}; this.ontext = function() {}; this.onbinary = function() {}; this.onclose = function() {}; this.onping = function() {}; this.onpong = function() {}; } module.exports = Receiver; /** * Add new data to the parser. * * @api public */ Receiver.prototype.add = function(data) { if (this.dead) return; var dataLength = data.length; if (dataLength == 0) return; if (this.expectBuffer == null) { this.overflow.push(data); return; } var toRead = Math.min(dataLength, this.expectBuffer.length - this.expectOffset); fastCopy(toRead, data, this.expectBuffer, this.expectOffset); this.expectOffset += toRead; if (toRead < dataLength) { this.overflow.push(data.slice(toRead)); } while (this.expectBuffer && this.expectOffset == this.expectBuffer.length) { var bufferForHandler = this.expectBuffer; this.expectBuffer = null; this.expectOffset = 0; this.expectHandler.call(this, bufferForHandler); } }; /** * Releases all resources used by the receiver. * * @api public */ Receiver.prototype.cleanup = function() { this.dead = true; this.overflow = null; this.headerBuffer = null; this.expectBuffer = null; this.expectHandler = null; this.unfragmentedBufferPool = null; this.fragmentedBufferPool = null; this.state = null; this.currentMessage = null; this.onerror = null; this.ontext = null; this.onbinary = null; this.onclose = null; this.onping = null; this.onpong = null; }; /** * Waits for a certain amount of header bytes to be available, then fires a callback. * * @api private */ Receiver.prototype.expectHeader = function(length, handler) { if (length == 0) { handler(null); return; } this.expectBuffer = this.headerBuffer.slice(this.expectOffset, this.expectOffset + length); this.expectHandler = handler; var toRead = length; while (toRead > 0 && this.overflow.length > 0) { var fromOverflow = this.overflow.pop(); if (toRead < fromOverflow.length) this.overflow.push(fromOverflow.slice(toRead)); var read = Math.min(fromOverflow.length, toRead); fastCopy(read, fromOverflow, this.expectBuffer, this.expectOffset); this.expectOffset += read; toRead -= read; } }; /** * Waits for a certain amount of data bytes to be available, then fires a callback. * * @api private */ Receiver.prototype.expectData = function(length, handler) { if (length == 0) { handler(null); return; } this.expectBuffer = this.allocateFromPool(length, this.state.fragmentedOperation); this.expectHandler = handler; var toRead = length; while (toRead > 0 && this.overflow.length > 0) { var fromOverflow = this.overflow.pop(); if (toRead < fromOverflow.length) this.overflow.push(fromOverflow.slice(toRead)); var read = Math.min(fromOverflow.length, toRead); fastCopy(read, fromOverflow, this.expectBuffer, this.expectOffset); this.expectOffset += read; toRead -= read; } }; /** * Allocates memory from the buffer pool. * * @api private */ Receiver.prototype.allocateFromPool = function(length, isFragmented) { return (isFragmented ? this.fragmentedBufferPool : this.unfragmentedBufferPool).get(length); }; /** * Start processing a new packet. * * @api private */ Receiver.prototype.processPacket = function (data) { if (this.extensions[PerMessageDeflate.extensionName]) { if ((data[0] & 0x30) != 0) { this.error('reserved fields (2, 3) must be empty', 1002); return; } } else { if ((data[0] & 0x70) != 0) { this.error('reserved fields must be empty', 1002); return; } } this.state.lastFragment = (data[0] & 0x80) == 0x80; this.state.masked = (data[1] & 0x80) == 0x80; var compressed = (data[0] & 0x40) == 0x40; var opcode = data[0] & 0xf; if (opcode === 0) { if (compressed) { this.error('continuation frame cannot have the Per-message Compressed bits', 1002); return; } // continuation frame this.state.fragmentedOperation = true; this.state.opcode = this.state.activeFragmentedOperation; if (!(this.state.opcode == 1 || this.state.opcode == 2)) { this.error('continuation frame cannot follow current opcode', 1002); return; } } else { if (opcode < 3 && this.state.activeFragmentedOperation != null) { this.error('data frames after the initial data frame must have opcode 0', 1002); return; } if (opcode >= 8 && compressed) { this.error('control frames cannot have the Per-message Compressed bits', 1002); return; } this.state.compressed = compressed; this.state.opcode = opcode; if (this.state.lastFragment === false) { this.state.fragmentedOperation = true; this.state.activeFragmentedOperation = opcode; } else this.state.fragmentedOperation = false; } var handler = opcodes[this.state.opcode]; if (typeof handler == 'undefined') this.error('no handler for opcode ' + this.state.opcode, 1002); else { handler.start.call(this, data); } }; /** * Endprocessing a packet. * * @api private */ Receiver.prototype.endPacket = function() { if (this.dead) return; if (!this.state.fragmentedOperation) this.unfragmentedBufferPool.reset(true); else if (this.state.lastFragment) this.fragmentedBufferPool.reset(true); this.expectOffset = 0; this.expectBuffer = null; this.expectHandler = null; if (this.state.lastFragment && this.state.opcode === this.state.activeFragmentedOperation) { // end current fragmented operation this.state.activeFragmentedOperation = null; } this.currentPayloadLength = 0; this.state.lastFragment = false; this.state.opcode = this.state.activeFragmentedOperation != null ? this.state.activeFragmentedOperation : 0; this.state.masked = false; this.expectHeader(2, this.processPacket); }; /** * Reset the parser state. * * @api private */ Receiver.prototype.reset = function() { if (this.dead) return; this.state = { activeFragmentedOperation: null, lastFragment: false, masked: false, opcode: 0, fragmentedOperation: false }; this.fragmentedBufferPool.reset(true); this.unfragmentedBufferPool.reset(true); this.expectOffset = 0; this.expectBuffer = null; this.expectHandler = null; this.overflow = []; this.currentMessage = []; this.currentMessageLength = 0; this.messageHandlers = []; this.currentPayloadLength = 0; }; /** * Unmask received data. * * @api private */ Receiver.prototype.unmask = function (mask, buf, binary) { if (mask != null && buf != null) bufferUtil.unmask(buf, mask); if (binary) return buf; return buf != null ? buf.toString('utf8') : ''; }; /** * Handles an error * * @api private */ Receiver.prototype.error = function (reason, protocolErrorCode) { if (this.dead) return; this.reset(); if(typeof reason == 'string'){ this.onerror(new Error(reason), protocolErrorCode); } else if(reason.constructor == Error){ this.onerror(reason, protocolErrorCode); } else{ this.onerror(new Error("An error occured"),protocolErrorCode); } return this; }; /** * Execute message handler buffers * * @api private */ Receiver.prototype.flush = function() { if (this.processing || this.dead) return; var handler = this.messageHandlers.shift(); if (!handler) return; this.processing = true; var self = this; handler(function() { self.processing = false; self.flush(); }); }; /** * Apply extensions to message * * @api private */ Receiver.prototype.applyExtensions = function(messageBuffer, fin, compressed, callback) { var self = this; if (compressed) { this.extensions[PerMessageDeflate.extensionName].decompress(messageBuffer, fin, function(err, buffer) { if (self.dead) return; if (err) { callback(new Error('invalid compressed data')); return; } callback(null, buffer); }); } else { callback(null, messageBuffer); } }; /** * Checks payload size, disconnects socket when it exceeds maxPayload * * @api private */ Receiver.prototype.maxPayloadExceeded = function(length) { if (this.maxPayload=== undefined || this.maxPayload === null || this.maxPayload < 1) { return false; } var fullLength = this.currentPayloadLength + length; if (fullLength < this.maxPayload) { this.currentPayloadLength = fullLength; return false; } this.error('payload cannot exceed ' + this.maxPayload + ' bytes', 1009); this.messageBuffer=[]; this.cleanup(); return true; }; /** * Buffer utilities */ function readUInt16BE(start) { return (this[start]<<8) + this[start+1]; } function readUInt32BE(start) { return (this[start]<<24) + (this[start+1]<<16) + (this[start+2]<<8) + this[start+3]; } function fastCopy(length, srcBuffer, dstBuffer, dstOffset) { switch (length) { default: srcBuffer.copy(dstBuffer, dstOffset, 0, length); break; case 16: dstBuffer[dstOffset+15] = srcBuffer[15]; case 15: dstBuffer[dstOffset+14] = srcBuffer[14]; case 14: dstBuffer[dstOffset+13] = srcBuffer[13]; case 13: dstBuffer[dstOffset+12] = srcBuffer[12]; case 12: dstBuffer[dstOffset+11] = srcBuffer[11]; case 11: dstBuffer[dstOffset+10] = srcBuffer[10]; case 10: dstBuffer[dstOffset+9] = srcBuffer[9]; case 9: dstBuffer[dstOffset+8] = srcBuffer[8]; case 8: dstBuffer[dstOffset+7] = srcBuffer[7]; case 7: dstBuffer[dstOffset+6] = srcBuffer[6]; case 6: dstBuffer[dstOffset+5] = srcBuffer[5]; case 5: dstBuffer[dstOffset+4] = srcBuffer[4]; case 4: dstBuffer[dstOffset+3] = srcBuffer[3]; case 3: dstBuffer[dstOffset+2] = srcBuffer[2]; case 2: dstBuffer[dstOffset+1] = srcBuffer[1]; case 1: dstBuffer[dstOffset] = srcBuffer[0]; } } function clone(obj) { var cloned = {}; for (var k in obj) { if (obj.hasOwnProperty(k)) { cloned[k] = obj[k]; } } return cloned; } /** * Opcode handlers */ var opcodes = { // text '1': { start: function(data) { var self = this; // decode length var firstLength = data[1] & 0x7f; if (firstLength < 126) { if (self.maxPayloadExceeded(firstLength)){ self.error('Maximumpayload exceeded in compressed text message. Aborting...', 1009); return; } opcodes['1'].getData.call(self, firstLength); } else if (firstLength == 126) { self.expectHeader(2, function(data) { var length = readUInt16BE.call(data, 0); if (self.maxPayloadExceeded(length)){ self.error('Maximumpayload exceeded in compressed text message. Aborting...', 1009); return; } opcodes['1'].getData.call(self, length); }); } else if (firstLength == 127) { self.expectHeader(8, function(data) { if (readUInt32BE.call(data, 0) != 0) { self.error('packets with length spanning more than 32 bit is currently not supported', 1008); return; } var length = readUInt32BE.call(data, 4); if (self.maxPayloadExceeded(length)){ self.error('Maximumpayload exceeded in compressed text message. Aborting...', 1009); return; } opcodes['1'].getData.call(self, readUInt32BE.call(data, 4)); }); } }, getData: function(length) { var self = this; if (self.state.masked) { self.expectHeader(4, function(data) { var mask = data; self.expectData(length, function(data) { opcodes['1'].finish.call(self, mask, data); }); }); } else { self.expectData(length, function(data) { opcodes['1'].finish.call(self, null, data); }); } }, finish: function(mask, data) { var self = this; var packet = this.unmask(mask, data, true) || new Buffer(0); var state = clone(this.state); this.messageHandlers.push(function(callback) { self.applyExtensions(packet, state.lastFragment, state.compressed, function(err, buffer) { if (err) { if(err.type===1009){ return self.error('Maximumpayload exceeded in compressed text message. Aborting...', 1009); } return self.error(err.message, 1007); } if (buffer != null) { if( self.maxPayload==0 || (self.maxPayload > 0 && (self.currentMessageLength + buffer.length) < self.maxPayload) ){ self.currentMessage.push(buffer); } else{ self.currentMessage=null; self.currentMessage = []; self.currentMessageLength = 0; self.error(new Error('Maximum payload exceeded. maxPayload: '+self.maxPayload), 1009); return; } self.currentMessageLength += buffer.length; } if (state.lastFragment) { var messageBuffer = Buffer.concat(self.currentMessage); self.currentMessage = []; self.currentMessageLength = 0; if (!Validation.isValidUTF8(messageBuffer)) { self.error('invalid utf8 sequence', 1007); return; } self.ontext(messageBuffer.toString('utf8'), {masked: state.masked, buffer: messageBuffer}); } callback(); }); }); this.flush(); this.endPacket(); } }, // binary '2': { start: function(data) { var self = this; // decode length var firstLength = data[1] & 0x7f; if (firstLength < 126) { if (self.maxPayloadExceeded(firstLength)){ self.error('Max payload exceeded in compressed text message. Aborting...', 1009); return; } opcodes['2'].getData.call(self, firstLength); } else if (firstLength == 126) { self.expectHeader(2, function(data) { var length = readUInt16BE.call(data, 0); if (self.maxPayloadExceeded(length)){ self.error('Max payload exceeded in compressed text message. Aborting...', 1009); return; } opcodes['2'].getData.call(self, length); }); } else if (firstLength == 127) { self.expectHeader(8, function(data) { if (readUInt32BE.call(data, 0) != 0) { self.error('packets with length spanning more than 32 bit is currently not supported', 1008); return; } var length = readUInt32BE.call(data, 4, true); if (self.maxPayloadExceeded(length)){ self.error('Max payload exceeded in compressed text message. Aborting...', 1009); return; } opcodes['2'].getData.call(self, length); }); } }, getData: function(length) { var self = this; if (self.state.masked) { self.expectHeader(4, function(data) { var mask = data; self.expectData(length, function(data) { opcodes['2'].finish.call(self, mask, data); }); }); } else { self.expectData(length, function(data) { opcodes['2'].finish.call(self, null, data); }); } }, finish: function(mask, data) { var self = this; var packet = this.unmask(mask, data, true) || new Buffer(0); var state = clone(this.state); this.messageHandlers.push(function(callback) { self.applyExtensions(packet, state.lastFragment, state.compressed, function(err, buffer) { if (err) { if(err.type===1009){ return self.error('Max payload exceeded in compressed binary message. Aborting...', 1009); } return self.error(err.message, 1007); } if (buffer != null) { if( self.maxPayload==0 || (self.maxPayload > 0 && (self.currentMessageLength + buffer.length) < self.maxPayload) ){ self.currentMessage.push(buffer); } else{ self.currentMessage=null; self.currentMessage = []; self.currentMessageLength = 0; self.error(new Error('Maximum payload exceeded'), 1009); return; } self.currentMessageLength += buffer.length; } if (state.lastFragment) { var messageBuffer = Buffer.concat(self.currentMessage); self.currentMessage = []; self.currentMessageLength = 0; self.onbinary(messageBuffer, {masked: state.masked, buffer: messageBuffer}); } callback(); }); }); this.flush(); this.endPacket(); } }, // close '8': { start: function(data) { var self = this; if (self.state.lastFragment == false) { self.error('fragmented close is not supported', 1002); return; } // decode length var firstLength = data[1] & 0x7f; if (firstLength < 126) { opcodes['8'].getData.call(self, firstLength); } else { self.error('control frames cannot have more than 125 bytes of data', 1002); } }, getData: function(length) { var self = this; if (self.state.masked) { self.expectHeader(4, function(data) { var mask = data; self.expectData(length, function(data) { opcodes['8'].finish.call(self, mask, data); }); }); } else { self.expectData(length, function(data) { opcodes['8'].finish.call(self, null, data); }); } }, finish: function(mask, data) { var self = this; data = self.unmask(mask, data, true); var state = clone(this.state); this.messageHandlers.push(function() { if (data && data.length == 1) { self.error('close packets with data must be at least two bytes long', 1002); return; } var code = data && data.length > 1 ? readUInt16BE.call(data, 0) : 1000; if (!ErrorCodes.isValidErrorCode(code)) { self.error('invalid error code', 1002); return; } var message = ''; if (data && data.length > 2) { var messageBuffer = data.slice(2); if (!Validation.isValidUTF8(messageBuffer)) { self.error('invalid utf8 sequence', 1007); return; } message = messageBuffer.toString('utf8'); } self.onclose(code, message, {masked: state.masked}); self.reset(); }); this.flush(); }, }, // ping '9': { start: function(data) { var self = this; if (self.state.lastFragment == false) { self.error('fragmented ping is not supported', 1002); return; } // decode length var firstLength = data[1] & 0x7f; if (firstLength < 126) { opcodes['9'].getData.call(self, firstLength); } else { self.error('control frames cannot have more than 125 bytes of data', 1002); } }, getData: function(length) { var self = this; if (self.state.masked) { self.expectHeader(4, function(data) { var mask = data; self.expectData(length, function(data) { opcodes['9'].finish.call(self, mask, data); }); }); } else { self.expectData(length, function(data) { opcodes['9'].finish.call(self, null, data); }); } }, finish: function(mask, data) { var self = this; data = this.unmask(mask, data, true); var state = clone(this.state); this.messageHandlers.push(function(callback) { self.onping(data, {masked: state.masked, binary: true}); callback(); }); this.flush(); this.endPacket(); } }, // pong '10': { start: function(data) { var self = this; if (self.state.lastFragment == false) { self.error('fragmented pong is not supported', 1002); return; } // decode length var firstLength = data[1] & 0x7f; if (firstLength < 126) { opcodes['10'].getData.call(self, firstLength); } else { self.error('control frames cannot have more than 125 bytes of data', 1002); } }, getData: function(length) { var self = this; if (this.state.masked) { this.expectHeader(4, function(data) { var mask = data; self.expectData(length, function(data) { opcodes['10'].finish.call(self, mask, data); }); }); } else { this.expectData(length, function(data) { opcodes['10'].finish.call(self, null, data); }); } }, finish: function(mask, data) { var self = this; data = self.unmask(mask, data, true); var state = clone(this.state); this.messageHandlers.push(function(callback) { self.onpong(data, {masked: state.masked, binary: true}); callback(); }); this.flush(); this.endPacket(); } } }