/************************************************************************ * Copyright 2010-2015 Brian McKelvey. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ***********************************************************************/ var bufferUtil = require('./BufferUtil').BufferUtil; var bufferAllocUnsafe = require('./utils').bufferAllocUnsafe; const DECODE_HEADER = 1; const WAITING_FOR_16_BIT_LENGTH = 2; const WAITING_FOR_64_BIT_LENGTH = 3; const WAITING_FOR_MASK_KEY = 4; const WAITING_FOR_PAYLOAD = 5; const COMPLETE = 6; // WebSocketConnection will pass shared buffer objects for maskBytes and // frameHeader into the constructor to avoid tons of small memory allocations // for each frame we have to parse. This is only used for parsing frames // we receive off the wire. function WebSocketFrame(maskBytes, frameHeader, config) { this.maskBytes = maskBytes; this.frameHeader = frameHeader; this.config = config; this.maxReceivedFrameSize = config.maxReceivedFrameSize; this.protocolError = false; this.frameTooLarge = false; this.invalidCloseFrameLength = false; this.parseState = DECODE_HEADER; this.closeStatus = -1; } WebSocketFrame.prototype.addData = function(bufferList) { if (this.parseState === DECODE_HEADER) { if (bufferList.length >= 2) { bufferList.joinInto(this.frameHeader, 0, 0, 2); bufferList.advance(2); var firstByte = this.frameHeader[0]; var secondByte = this.frameHeader[1]; this.fin = Boolean(firstByte & 0x80); this.rsv1 = Boolean(firstByte & 0x40); this.rsv2 = Boolean(firstByte & 0x20); this.rsv3 = Boolean(firstByte & 0x10); this.mask = Boolean(secondByte & 0x80); this.opcode = firstByte & 0x0F; this.length = secondByte & 0x7F; // Control frame sanity check if (this.opcode >= 0x08) { if (this.length > 125) { this.protocolError = true; this.dropReason = 'Illegal control frame longer than 125 bytes.'; return true; } if (!this.fin) { this.protocolError = true; this.dropReason = 'Control frames must not be fragmented.'; return true; } } if (this.length === 126) { this.parseState = WAITING_FOR_16_BIT_LENGTH; } else if (this.length === 127) { this.parseState = WAITING_FOR_64_BIT_LENGTH; } else { this.parseState = WAITING_FOR_MASK_KEY; } } } if (this.parseState === WAITING_FOR_16_BIT_LENGTH) { if (bufferList.length >= 2) { bufferList.joinInto(this.frameHeader, 2, 0, 2); bufferList.advance(2); this.length = this.frameHeader.readUInt16BE(2); this.parseState = WAITING_FOR_MASK_KEY; } } else if (this.parseState === WAITING_FOR_64_BIT_LENGTH) { if (bufferList.length >= 8) { bufferList.joinInto(this.frameHeader, 2, 0, 8); bufferList.advance(8); var lengthPair = [ this.frameHeader.readUInt32BE(2), this.frameHeader.readUInt32BE(2+4) ]; if (lengthPair[0] !== 0) { this.protocolError = true; this.dropReason = 'Unsupported 64-bit length frame received'; return true; } this.length = lengthPair[1]; this.parseState = WAITING_FOR_MASK_KEY; } } if (this.parseState === WAITING_FOR_MASK_KEY) { if (this.mask) { if (bufferList.length >= 4) { bufferList.joinInto(this.maskBytes, 0, 0, 4); bufferList.advance(4); this.parseState = WAITING_FOR_PAYLOAD; } } else { this.parseState = WAITING_FOR_PAYLOAD; } } if (this.parseState === WAITING_FOR_PAYLOAD) { if (this.length > this.maxReceivedFrameSize) { this.frameTooLarge = true; this.dropReason = 'Frame size of ' + this.length.toString(10) + ' bytes exceeds maximum accepted frame size'; return true; } if (this.length === 0) { this.binaryPayload = bufferAllocUnsafe(0); this.parseState = COMPLETE; return true; } if (bufferList.length >= this.length) { this.binaryPayload = bufferList.take(this.length); bufferList.advance(this.length); if (this.mask) { bufferUtil.unmask(this.binaryPayload, this.maskBytes); // xor(this.binaryPayload, this.maskBytes, 0); } if (this.opcode === 0x08) { // WebSocketOpcode.CONNECTION_CLOSE if (this.length === 1) { // Invalid length for a close frame. Must be zero or at least two. this.binaryPayload = bufferAllocUnsafe(0); this.invalidCloseFrameLength = true; } if (this.length >= 2) { this.closeStatus = this.binaryPayload.readUInt16BE(0); this.binaryPayload = this.binaryPayload.slice(2); } } this.parseState = COMPLETE; return true; } } return false; }; WebSocketFrame.prototype.throwAwayPayload = function(bufferList) { if (bufferList.length >= this.length) { bufferList.advance(this.length); this.parseState = COMPLETE; return true; } return false; }; WebSocketFrame.prototype.toBuffer = function(nullMask) { var maskKey; var headerLength = 2; var data; var outputPos; var firstByte = 0x00; var secondByte = 0x00; if (this.fin) { firstByte |= 0x80; } if (this.rsv1) { firstByte |= 0x40; } if (this.rsv2) { firstByte |= 0x20; } if (this.rsv3) { firstByte |= 0x10; } if (this.mask) { secondByte |= 0x80; } firstByte |= (this.opcode & 0x0F); // the close frame is a special case because the close reason is // prepended to the payload data. if (this.opcode === 0x08) { this.length = 2; if (this.binaryPayload) { this.length += this.binaryPayload.length; } data = bufferAllocUnsafe(this.length); data.writeUInt16BE(this.closeStatus, 0); if (this.length > 2) { this.binaryPayload.copy(data, 2); } } else if (this.binaryPayload) { data = this.binaryPayload; this.length = data.length; } else { this.length = 0; } if (this.length <= 125) { // encode the length directly into the two-byte frame header secondByte |= (this.length & 0x7F); } else if (this.length > 125 && this.length <= 0xFFFF) { // Use 16-bit length secondByte |= 126; headerLength += 2; } else if (this.length > 0xFFFF) { // Use 64-bit length secondByte |= 127; headerLength += 8; } var output = bufferAllocUnsafe(this.length + headerLength + (this.mask ? 4 : 0)); // write the frame header output[0] = firstByte; output[1] = secondByte; outputPos = 2; if (this.length > 125 && this.length <= 0xFFFF) { // write 16-bit length output.writeUInt16BE(this.length, outputPos); outputPos += 2; } else if (this.length > 0xFFFF) { // write 64-bit length output.writeUInt32BE(0x00000000, outputPos); output.writeUInt32BE(this.length, outputPos + 4); outputPos += 8; } if (this.mask) { maskKey = nullMask ? 0 : ((Math.random() * 0xFFFFFFFF) >>> 0); this.maskBytes.writeUInt32BE(maskKey, 0); // write the mask key this.maskBytes.copy(output, outputPos); outputPos += 4; if (data) { bufferUtil.mask(data, this.maskBytes, output, outputPos, this.length); } } else if (data) { data.copy(output, outputPos); } return output; }; WebSocketFrame.prototype.toString = function() { return 'Opcode: ' + this.opcode + ', fin: ' + this.fin + ', length: ' + this.length + ', hasPayload: ' + Boolean(this.binaryPayload) + ', masked: ' + this.mask; }; module.exports = WebSocketFrame;