OverviewFilesCommitsIssuesDocsReleasesPeers

mirall-cli / node_modules / bare-ws / lib / frame.js

216 lines · javascript ref:
const crypto = require('bare-crypto')
const errors = require('./errors')

const EMPTY = Buffer.alloc(0)

const FIN = 0b10000000
const RSV1 = 0b01000000
const RSV2 = 0b00100000
const RSV3 = 0b00010000
const OPCODE = 0b00001111
const MASK = 0b10000000
const LENGTH = 0b01111111

// https://datatracker.ietf.org/doc/html/rfc6455#section-5
module.exports = exports = class Frame {
  constructor(opcode, payload = EMPTY, opts = {}) {
    if (payload && !Buffer.isBuffer(payload)) {
      opts = payload
      payload = EMPTY
    }

    const { fin = true, rsv1 = false, rsv2 = false, rsv3 = false, mask = null } = opts

    this.fin = fin
    this.rsv1 = rsv1
    this.rsv2 = rsv2
    this.rsv3 = rsv3
    this.opcode = opcode
    this.mask = mask
    this.payload = payload
  }

  toBuffer() {
    const state = { start: 0, end: 0, buffer: null }

    Frame.preencode(state, this)

    state.buffer = Buffer.allocUnsafe(state.end)

    Frame.encode(state, this)

    return state.buffer
  }
}

const Frame = exports

exports.preencode = function preencode(state, f) {
  let i = state.end

  i++

  const length = f.payload.length

  if (length <= 0x7d) i++
  else {
    if (length <= 0xffff) i += 3
    else i += 9
  }

  if (f.mask) i += 4

  i += length

  state.end = i
}

exports.encode = function encode(state, f) {
  const b = state.buffer

  let i = state.start

  const v = new DataView(b.buffer, b.byteOffset, b.byteLength)

  b[i] = f.opcode & OPCODE

  if (f.fin) b[i] |= FIN
  if (f.rsv1) b[i] |= RSV1
  if (f.rsv1) b[i] |= RSV2
  if (f.rsv1) b[i] |= RSV3

  i++

  b[i] = f.mask ? MASK : 0

  const length = f.payload.length

  if (length <= 0x7d) b[i++] |= length
  else {
    if (length <= 0xffff) {
      b[i++] |= 0x7e

      v.setUint16(i, length, false)

      i += 2
    } else {
      b[i++] |= 0x7f

      const high = Math.floor(length / 0x100000000)

      v.setUint32(i, high, false)

      i += 4

      const low = length & 0xffffffff

      v.setUint32(i, low, false)

      i += 4
    }
  }

  if (f.mask) {
    crypto.randomFill(f.mask, 0, 4)

    b.set(f.mask, i)

    i += 4

    for (let j = 0; j < length; j++) {
      b[i + j] = f.payload[j] ^ f.mask[j & 3]
    }
  } else {
    b.set(f.payload, i)
  }

  i += length

  state.start = i
}

exports.decode = function decode(state) {
  const s = state.start
  const b = state.buffer

  let i = s
  let n = b.byteLength

  if (n < 2) throw errors.INCOMPLETE_FRAME()

  const view = new DataView(b.buffer, b.byteOffset, b.byteLength)

  const fin = !!(b[i] & FIN)

  const rsv1 = !!(b[i] & RSV1)
  const rsv2 = !!(b[i] & RSV2)
  const rsv3 = !!(b[i] & RSV3)

  const opcode = b[i] & OPCODE

  i++
  n--

  const masked = !!(b[i] & MASK)

  let length = b[i] & LENGTH

  i++
  n--

  if (length === 0x7e) {
    if (n < 2) throw errors.INCOMPLETE_FRAME()

    length = view.getUint16(i, false)

    i += 2
    n -= 2
  } else if (length === 0x7f) {
    if (n < 8) throw errors.INCOMPLETE_FRAME()

    const high = view.getUint32(i, false)

    if (high >= 0x200000) throw errors.INVALID_PAYLOAD_LENGTH()

    i += 4
    n -= 4

    const low = view.getUint32(i, false)

    i += 4
    n -= 4

    length = high * 0x100000000 + low
  }

  let mask = null

  if (masked) {
    if (n < 4) throw errors.INCOMPLETE_FRAME()

    mask = b.subarray(i, i + 4)

    i += 4
    n -= 4
  }

  if (n < length) {
    throw errors.INCOMPLETE_FRAME('Incomplete frame', i - s + length)
  }

  const payload = b.subarray(i, i + length)

  i += length
  n -= length

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

  state.start = i

  return new Frame(opcode, payload, { fin, rsv1, rsv2, rsv3, mask })
}