walve
Version:
stream based WebSocket implementation
146 lines (115 loc) • 3.76 kB
JavaScript
const {
Transform
} = require('readable-stream')
const FIN = 0x80
const RSV1 = 0x40
const RSV2 = 0x20
const RSV3 = 0x10
const RSV_ALL = RSV1 | RSV2 | RSV3
const OPCODE = 0x0f
const MASKED = 0x80
const LENGTH = 0x7f
const LENGTH_EXT1 = 0x7e
const LENGTH_EXT2 = 0x7f
class Incoming extends Transform {
constructor(options) {
super(options)
this.header = null
this.inHeader = true
this.needsHeadBase = true
this.needsHeadExtended = false
this.rawHead = []
// TODO: move this.headLength to headBytesRead
this.totalBytesRead = 0
this.payloadBytesRead = 0
this.payloadBytesToRead = 0
}
_cache(chunk) {
this.rawHead.push(chunk)
return Buffer.concat(this.rawHead)
}
_transform(chunk, encoding, done) {
var header = this.header = this.header || {}
if (this.inHeader) {
chunk = this._cache(chunk)
// buffer until we have two bytes to parse
// and to calculated total head byte length
if (chunk.length >= 2 && this.needsHeadBase) {
if (RSV_ALL & chunk[0]) {
this.emit('error', new Error('RSV flag should not be set.'))
return
}
header.final = !!(FIN & chunk[0])
header.masked = !!(MASKED & chunk[1])
header.opcode = OPCODE & chunk[0]
this.length = LENGTH & chunk[1]
this.headLength = 2
if (header.masked) {
this.headLength += 4
}
if (this.length === LENGTH_EXT1) {
this.headLength += 2
}
if (this.length === LENGTH_EXT2) {
this.headLength += 8
}
this.totalBytesRead += 2
this.needsHeadBase = false
this.needsHeadExtended = true
}
// buffer until we have all required head bytes and parse
if (chunk.length >= this.headLength && this.needsHeadExtended) {
header.length = this.length
if (header.masked) {
header.masking = chunk.slice(this.headLength - 4)
}
switch (this.length) {
case LENGTH_EXT1:
header.length = chunk.readUInt16BE(2, 2)
break
case LENGTH_EXT2:
if (chunk.readUInt32BE(2, 4)) {
this.emit('error', new Error('Payload length is too big.'))
return
}
header.length = chunk.readUInt32BE(6, 4)
break
}
this.totalBytesRead += chunk.length
this.payloadBytesToRead += header.length
this.inHeader = false
this.needsHeadExtended = false
this.emit('header', header)
}
// if we are finished with head bytes slice payload if in head length
// else set chunk to null to avoid being consumed by stream
if (!this.needsHeadBase && !this.needsHeadExtended) {
chunk = chunk.slice(this.headLength)
}
}
if (!this.inHeader) {
// if we have more chunk then we need to read we probably got two frames
// glued to each other. In this case we will flush them away.
if (chunk.length > this.payloadBytesToRead) {
this.emit('flush', chunk.slice(this.payloadBytesToRead))
chunk = chunk.slice(0, this.payloadBytesToRead)
}
// unmask the payload
if (header.masked) {
for (var i = 0; i < chunk.length; i++) {
chunk[i] = chunk[i] ^ header.masking[(i + this.payloadBytesRead) % 4]
}
}
this.push(chunk)
// update all counters
this.totalBytesRead += chunk.length
this.payloadBytesRead += chunk.length
this.payloadBytesToRead -= chunk.length
if (!this.payloadBytesToRead) {
this.push(null)
}
}
done()
}
}
module.exports = Incoming