text: /************************************************************************
   *  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;
  