import * as crypto from 'crypto';
import { EventEmitter } from 'events';
import * as http from 'http';
import * as url from 'url';

/* eslint-disable no-bitwise */

const OPCODES = {
  CONTINUATION: 0,
  TEXT: 1,
  BINARY: 2,
  TERMINATE: 8,
  PING: 9,
  PONG: 10,
};

const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';

function isCompleteFrame(frame) {
  return Buffer.byteLength(frame.payload) >= frame.payloadLength;
}

function unmaskPayload(payload, mask, offset) {
  if (mask === undefined) {
    return payload;
  }

  for (let i = 0; i < payload.length; i++) {
    payload[i] ^= mask[(offset + i) & 3];
  }

  return payload;
}

function buildFrame(opts) {
  const { opcode, fin, data } = opts;

  let offset = 6;
  let dataLength = data.length;

  if (dataLength >= 65536) {
    offset += 8;
    dataLength = 127;
  } else if (dataLength > 125) {
    offset += 2;
    dataLength = 126;
  }

  const head = Buffer.allocUnsafe(offset);

  head[0] = fin ? opcode | 128 : opcode;
  head[1] = dataLength;

  if (dataLength === 126) {
    head.writeUInt16BE(data.length, 2);
  } else if (dataLength === 127) {
    head.writeUInt32BE(0, 2);
    head.writeUInt32BE(data.length, 6);
  }

  const mask = crypto.randomBytes(4);
  head[1] |= 128;
  head[offset - 4] = mask[0];
  head[offset - 3] = mask[1];
  head[offset - 2] = mask[2];
  head[offset - 1] = mask[3];

  const masked = Buffer.alloc(dataLength);
  for (let i = 0; i < dataLength; ++i) {
    masked[i] = data[i] ^ mask[i & 3];
  }

  return Buffer.concat([head, masked]);
}

function parseFrame(buffer) {
  const firstByte = buffer.readUInt8(0);
  const isFinalFrame = Boolean((firstByte >>> 7) & 1);
  const opcode = firstByte & 15;

  const secondByte = buffer.readUInt8(1);
  const isMasked = Boolean((secondByte >>> 7) & 1);

  // Keep track of our current position as we advance through the buffer
  let currentOffset = 2;
  let payloadLength = secondByte & 127;
  if (payloadLength > 125) {
    if (payloadLength === 126) {
      payloadLength = buffer.readUInt16BE(currentOffset);
      currentOffset += 2;
    } else if (payloadLength === 127) {
      const leftPart = buffer.readUInt32BE(currentOffset);
      currentOffset += 4;

      // The maximum safe integer in JavaScript is 2^53 - 1. An error is returned

      // if payload length is greater than this number.
      if (leftPart >= Number.MAX_SAFE_INTEGER) {
        throw new Error('Unsupported WebSocket frame: payload length > 2^53 - 1');
      }

      const rightPart = buffer.readUInt32BE(currentOffset);
      currentOffset += 4;

      payloadLength = leftPart * Math.pow(2, 32) + rightPart;
    } else {
      throw new Error('Unknown payload length');
    }
  }

  // Get the masking key if one exists
  let mask;
  if (isMasked) {
    mask = buffer.slice(currentOffset, currentOffset + 4);
    currentOffset += 4;
  }

  const payload = unmaskPayload(buffer.slice(currentOffset), mask, 0);

  return {
    fin: isFinalFrame,
    opcode,
    mask,
    payload,
    payloadLength,
  };
}

function createKey(key) {
  return crypto.createHash('sha1').update(`${key}${GUID}`).digest('base64');
}

class WebSocketInterface extends EventEmitter {

   constructor(socket) {
    super();
    // When a frame is set here then any additional continuation frames payloads will be appended
    this._unfinishedFrame = undefined;

    // When a frame is set here, all additional chunks will be appended until we reach the correct payloadLength
    this._incompleteFrame = undefined;

    this._socket = socket;
    this._alive = true;

    socket.on('data', buff => {
      this._addBuffer(buff);
    });

    socket.on('error', (err) => {
      if (err.code === 'ECONNRESET') {
        this.emit('close');
      } else {
        this.emit('error');
      }
    });

    socket.on('close', () => {
      this.end();
    });
  }

   end() {
    if (!this._alive) {
      return;
    }

    this._alive = false;
    this.emit('close');
    this._socket.end();
  }

   send(buff) {
    this._sendFrame({
      opcode: OPCODES.TEXT,
      fin: true,
      data: Buffer.from(buff),
    });
  }

   _sendFrame(frameOpts) {
    this._socket.write(buildFrame(frameOpts));
  }

   _completeFrame(frame) {
    // If we have an unfinished frame then only allow continuations
    const { _unfinishedFrame: unfinishedFrame } = this;
    if (unfinishedFrame !== undefined) {
      if (frame.opcode === OPCODES.CONTINUATION) {
        unfinishedFrame.payload = Buffer.concat([
          unfinishedFrame.payload,
          unmaskPayload(frame.payload, unfinishedFrame.mask, unfinishedFrame.payload.length),
        ]);

        if (frame.fin) {
          this._unfinishedFrame = undefined;
          this._completeFrame(unfinishedFrame);
        }
        return;
      } else {
        // Silently ignore the previous frame...
        this._unfinishedFrame = undefined;
      }
    }

    if (frame.fin) {
      if (frame.opcode === OPCODES.PING) {
        this._sendFrame({
          opcode: OPCODES.PONG,
          fin: true,
          data: frame.payload,
        });
      } else {
        // Trim off any excess payload
        let excess;
        if (frame.payload.length > frame.payloadLength) {
          excess = frame.payload.slice(frame.payloadLength);
          frame.payload = frame.payload.slice(0, frame.payloadLength);
        }

        this.emit('message', frame.payload);

        if (excess !== undefined) {
          this._addBuffer(excess);
        }
      }
    } else {
      this._unfinishedFrame = frame;
    }
  }

   _addBufferToIncompleteFrame(incompleteFrame, buff) {
    incompleteFrame.payload = Buffer.concat([
      incompleteFrame.payload,
      unmaskPayload(buff, incompleteFrame.mask, incompleteFrame.payload.length),
    ]);

    if (isCompleteFrame(incompleteFrame)) {
      this._incompleteFrame = undefined;
      this._completeFrame(incompleteFrame);
    }
  }

   _addBuffer(buff) {
    // Check if we're still waiting for the rest of a payload
    const { _incompleteFrame: incompleteFrame } = this;
    if (incompleteFrame !== undefined) {
      this._addBufferToIncompleteFrame(incompleteFrame, buff);
      return;
    }

    // There needs to be atleast two values in the buffer for us to parse
    // a frame from it.
    // See: https://github.com/getsentry/sentry-javascript/issues/9307
    if (buff.length <= 1) {
      return;
    }

    const frame = parseFrame(buff);

    if (isCompleteFrame(frame)) {
      // Frame has been completed!
      this._completeFrame(frame);
    } else {
      this._incompleteFrame = frame;
    }
  }
}

/**
 * Creates a WebSocket client
 */
async function createWebSocketClient(rawUrl) {
  const parts = url.parse(rawUrl);

  return new Promise((resolve, reject) => {
    const key = crypto.randomBytes(16).toString('base64');
    const digest = createKey(key);

    const req = http.request({
      hostname: parts.hostname,
      port: parts.port,
      path: parts.path,
      method: 'GET',
      headers: {
        Connection: 'Upgrade',
        Upgrade: 'websocket',
        'Sec-WebSocket-Key': key,
        'Sec-WebSocket-Version': '13',
      },
    });

    req.on('response', (res) => {
      if (res.statusCode && res.statusCode >= 400) {
        process.stderr.write(`Unexpected HTTP code: ${res.statusCode}\n`);
        res.pipe(process.stderr);
      } else {
        res.pipe(process.stderr);
      }
    });

    req.on('upgrade', (res, socket) => {
      if (res.headers['sec-websocket-accept'] !== digest) {
        socket.end();
        reject(new Error(`Digest mismatch ${digest} !== ${res.headers['sec-websocket-accept']}`));
        return;
      }

      const client = new WebSocketInterface(socket);
      resolve(client);
    });

    req.on('error', err => {
      reject(err);
    });

    req.end();
  });
}

export { createWebSocketClient };
//# sourceMappingURL=websocket.js.map
