78
packages/websocket/src/socket.io-parser/binary.ts
Normal file
78
packages/websocket/src/socket.io-parser/binary.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { isBinary } from "./is-binary"
|
||||
|
||||
/**
|
||||
* Replaces every Buffer | ArrayBuffer | Blob | File in packet with a numbered placeholder.
|
||||
*
|
||||
* @param {Object} packet - socket.io event packet
|
||||
* @return {Object} with deconstructed packet and list of buffers
|
||||
* @public
|
||||
*/
|
||||
|
||||
export function deconstructPacket(packet) {
|
||||
const buffers = []
|
||||
const packetData = packet.data
|
||||
const pack = packet
|
||||
pack.data = _deconstructPacket(packetData, buffers)
|
||||
pack.attachments = buffers.length // number of binary 'attachments'
|
||||
return { packet: pack, buffers: buffers }
|
||||
}
|
||||
|
||||
function _deconstructPacket(data, buffers) {
|
||||
if (!data) return data
|
||||
|
||||
if (isBinary(data)) {
|
||||
const placeholder = { _placeholder: true, num: buffers.length }
|
||||
buffers.push(data)
|
||||
return placeholder
|
||||
} else if (Array.isArray(data)) {
|
||||
const newData = new Array(data.length)
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
newData[i] = _deconstructPacket(data[i], buffers)
|
||||
}
|
||||
return newData
|
||||
} else if (typeof data === "object" && !(data instanceof Date)) {
|
||||
const newData = {}
|
||||
for (const key in data) {
|
||||
if (data.hasOwnProperty(key)) {
|
||||
newData[key] = _deconstructPacket(data[key], buffers)
|
||||
}
|
||||
}
|
||||
return newData
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstructs a binary packet from its placeholder packet and buffers
|
||||
*
|
||||
* @param {Object} packet - event packet with placeholders
|
||||
* @param {Array} buffers - binary buffers to put in placeholder positions
|
||||
* @return {Object} reconstructed packet
|
||||
* @public
|
||||
*/
|
||||
|
||||
export function reconstructPacket(packet, buffers) {
|
||||
packet.data = _reconstructPacket(packet.data, buffers)
|
||||
packet.attachments = undefined // no longer useful
|
||||
return packet
|
||||
}
|
||||
|
||||
function _reconstructPacket(data, buffers) {
|
||||
if (!data) return data
|
||||
|
||||
if (data && data._placeholder) {
|
||||
return buffers[data.num] // appropriate buffer (should be natural order anyway)
|
||||
} else if (Array.isArray(data)) {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
data[i] = _reconstructPacket(data[i], buffers)
|
||||
}
|
||||
} else if (typeof data === "object") {
|
||||
for (const key in data) {
|
||||
if (data.hasOwnProperty(key)) {
|
||||
data[key] = _reconstructPacket(data[key], buffers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
316
packages/websocket/src/socket.io-parser/index.ts
Normal file
316
packages/websocket/src/socket.io-parser/index.ts
Normal file
@ -0,0 +1,316 @@
|
||||
import EventEmitter = require("events")
|
||||
import { deconstructPacket, reconstructPacket } from "./binary"
|
||||
import { isBinary, hasBinary } from "./is-binary"
|
||||
|
||||
// const debug = require("debug")("socket.io-parser")
|
||||
|
||||
/**
|
||||
* Protocol version.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
|
||||
export const protocol: number = 5
|
||||
|
||||
export enum PacketType {
|
||||
CONNECT,
|
||||
DISCONNECT,
|
||||
EVENT,
|
||||
ACK,
|
||||
CONNECT_ERROR,
|
||||
BINARY_EVENT,
|
||||
BINARY_ACK,
|
||||
}
|
||||
|
||||
export interface Packet {
|
||||
type: PacketType
|
||||
nsp: string
|
||||
data?: any
|
||||
id?: number
|
||||
attachments?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* A socket.io Encoder instance
|
||||
*/
|
||||
|
||||
export class Encoder {
|
||||
/**
|
||||
* Encode a packet as a single string if non-binary, or as a
|
||||
* buffer sequence, depending on packet type.
|
||||
*
|
||||
* @param {Object} obj - packet object
|
||||
*/
|
||||
public encode(obj: Packet) {
|
||||
console.trace("encoding packet", JSON.stringify(obj))
|
||||
|
||||
if (obj.type === PacketType.EVENT || obj.type === PacketType.ACK) {
|
||||
if (hasBinary(obj)) {
|
||||
obj.type =
|
||||
obj.type === PacketType.EVENT
|
||||
? PacketType.BINARY_EVENT
|
||||
: PacketType.BINARY_ACK
|
||||
return this.encodeAsBinary(obj)
|
||||
}
|
||||
}
|
||||
return [this.encodeAsString(obj)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode packet as string.
|
||||
*/
|
||||
|
||||
private encodeAsString(obj: Packet) {
|
||||
// first is type
|
||||
let str = "" + obj.type
|
||||
|
||||
// attachments if we have them
|
||||
if (
|
||||
obj.type === PacketType.BINARY_EVENT ||
|
||||
obj.type === PacketType.BINARY_ACK
|
||||
) {
|
||||
str += obj.attachments + "-"
|
||||
}
|
||||
|
||||
// if we have a namespace other than `/`
|
||||
// we append it followed by a comma `,`
|
||||
if (obj.nsp && "/" !== obj.nsp) {
|
||||
str += obj.nsp + ","
|
||||
}
|
||||
|
||||
// immediately followed by the id
|
||||
if (null != obj.id) {
|
||||
str += obj.id
|
||||
}
|
||||
|
||||
// json data
|
||||
if (null != obj.data) {
|
||||
str += JSON.stringify(obj.data)
|
||||
}
|
||||
|
||||
console.trace("encoded", JSON.stringify(obj), "as", str)
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode packet as 'buffer sequence' by removing blobs, and
|
||||
* deconstructing packet into object with placeholders and
|
||||
* a list of buffers.
|
||||
*/
|
||||
|
||||
private encodeAsBinary(obj: Packet) {
|
||||
const deconstruction = deconstructPacket(obj)
|
||||
const pack = this.encodeAsString(deconstruction.packet)
|
||||
const buffers = deconstruction.buffers
|
||||
|
||||
buffers.unshift(pack) // add packet info to beginning of data list
|
||||
return buffers // write all the buffers
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A socket.io Decoder instance
|
||||
*
|
||||
* @return {Object} decoder
|
||||
*/
|
||||
export class Decoder extends EventEmitter {
|
||||
private reconstructor: BinaryReconstructor
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes an encoded packet string into packet JSON.
|
||||
*
|
||||
* @param {String} obj - encoded packet
|
||||
*/
|
||||
|
||||
public add(obj: any) {
|
||||
let packet
|
||||
if (typeof obj === "string") {
|
||||
packet = this.decodeString(obj)
|
||||
if (
|
||||
packet.type === PacketType.BINARY_EVENT ||
|
||||
packet.type === PacketType.BINARY_ACK
|
||||
) {
|
||||
// binary packet's json
|
||||
this.reconstructor = new BinaryReconstructor(packet)
|
||||
|
||||
// no attachments, labeled binary but no binary data to follow
|
||||
if (packet.attachments === 0) {
|
||||
super.emit("decoded", packet)
|
||||
}
|
||||
} else {
|
||||
// non-binary full packet
|
||||
super.emit("decoded", packet)
|
||||
}
|
||||
} else if (isBinary(obj) || obj.base64) {
|
||||
// raw binary data
|
||||
if (!this.reconstructor) {
|
||||
throw new Error("got binary data when not reconstructing a packet")
|
||||
} else {
|
||||
packet = this.reconstructor.takeBinaryData(obj)
|
||||
if (packet) {
|
||||
// received final buffer
|
||||
this.reconstructor = null
|
||||
super.emit("decoded", packet)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error("Unknown type: " + obj)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a packet String (JSON data)
|
||||
*
|
||||
* @param {String} str
|
||||
* @return {Object} packet
|
||||
*/
|
||||
private decodeString(str): Packet {
|
||||
let i = 0
|
||||
// look up type
|
||||
const p: any = {
|
||||
type: Number(str.charAt(0)),
|
||||
}
|
||||
|
||||
if (PacketType[p.type] === undefined) {
|
||||
throw new Error("unknown packet type " + p.type)
|
||||
}
|
||||
|
||||
// look up attachments if type binary
|
||||
if (
|
||||
p.type === PacketType.BINARY_EVENT ||
|
||||
p.type === PacketType.BINARY_ACK
|
||||
) {
|
||||
const start = i + 1
|
||||
while (str.charAt(++i) !== "-" && i != str.length) { }
|
||||
const buf = str.substring(start, i)
|
||||
if (buf != Number(buf) || str.charAt(i) !== "-") {
|
||||
throw new Error("Illegal attachments")
|
||||
}
|
||||
p.attachments = Number(buf)
|
||||
}
|
||||
|
||||
// look up namespace (if any)
|
||||
if ("/" === str.charAt(i + 1)) {
|
||||
const start = i + 1
|
||||
while (++i) {
|
||||
const c = str.charAt(i)
|
||||
if ("," === c) break
|
||||
if (i === str.length) break
|
||||
}
|
||||
p.nsp = str.substring(start, i)
|
||||
} else {
|
||||
p.nsp = "/"
|
||||
}
|
||||
|
||||
// look up id
|
||||
const next = str.charAt(i + 1)
|
||||
if ("" !== next && Number(next) == next) {
|
||||
const start = i + 1
|
||||
while (++i) {
|
||||
const c = str.charAt(i)
|
||||
if (null == c || Number(c) != c) {
|
||||
--i
|
||||
break
|
||||
}
|
||||
if (i === str.length) break
|
||||
}
|
||||
p.id = Number(str.substring(start, i + 1))
|
||||
}
|
||||
|
||||
// look up json data
|
||||
if (str.charAt(++i)) {
|
||||
const payload = tryParse(str.substr(i))
|
||||
if (Decoder.isPayloadValid(p.type, payload)) {
|
||||
p.data = payload
|
||||
} else {
|
||||
throw new Error("invalid payload")
|
||||
}
|
||||
}
|
||||
|
||||
console.trace("decoded", str, "as", p)
|
||||
return p
|
||||
}
|
||||
|
||||
private static isPayloadValid(type: PacketType, payload: any): boolean {
|
||||
switch (type) {
|
||||
case PacketType.CONNECT:
|
||||
return typeof payload === "object"
|
||||
case PacketType.DISCONNECT:
|
||||
return payload === undefined
|
||||
case PacketType.CONNECT_ERROR:
|
||||
return typeof payload === "string" || typeof payload === "object"
|
||||
case PacketType.EVENT:
|
||||
case PacketType.BINARY_EVENT:
|
||||
return Array.isArray(payload) && payload.length > 0
|
||||
case PacketType.ACK:
|
||||
case PacketType.BINARY_ACK:
|
||||
return Array.isArray(payload)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deallocates a parser's resources
|
||||
*/
|
||||
public destroy() {
|
||||
if (this.reconstructor) {
|
||||
this.reconstructor.finishedReconstruction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function tryParse(str) {
|
||||
try {
|
||||
return JSON.parse(str)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A manager of a binary event's 'buffer sequence'. Should
|
||||
* be constructed whenever a packet of type BINARY_EVENT is
|
||||
* decoded.
|
||||
*
|
||||
* @param {Object} packet
|
||||
* @return {BinaryReconstructor} initialized reconstructor
|
||||
*/
|
||||
|
||||
class BinaryReconstructor {
|
||||
private reconPack
|
||||
private buffers: Array<Buffer | ArrayBuffer> = [];
|
||||
|
||||
constructor(readonly packet: Packet) {
|
||||
this.reconPack = packet
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to be called when binary data received from connection
|
||||
* after a BINARY_EVENT packet.
|
||||
*
|
||||
* @param {Buffer | ArrayBuffer} binData - the raw binary data received
|
||||
* @return {null | Object} returns null if more binary data is expected or
|
||||
* a reconstructed packet object if all buffers have been received.
|
||||
*/
|
||||
public takeBinaryData(binData) {
|
||||
this.buffers.push(binData)
|
||||
if (this.buffers.length === this.reconPack.attachments) {
|
||||
// done with buffer list
|
||||
const packet = reconstructPacket(this.reconPack, this.buffers)
|
||||
this.finishedReconstruction()
|
||||
return packet
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up binary packet reconstruction variables.
|
||||
*/
|
||||
public finishedReconstruction() {
|
||||
this.reconPack = null
|
||||
this.buffers = []
|
||||
}
|
||||
}
|
65
packages/websocket/src/socket.io-parser/is-binary.ts
Normal file
65
packages/websocket/src/socket.io-parser/is-binary.ts
Normal file
@ -0,0 +1,65 @@
|
||||
const withNativeArrayBuffer: boolean = typeof ArrayBuffer === "function"
|
||||
|
||||
const isView = (obj: any) => {
|
||||
return typeof ArrayBuffer.isView === "function"
|
||||
? ArrayBuffer.isView(obj)
|
||||
: obj.buffer instanceof ArrayBuffer
|
||||
}
|
||||
|
||||
const toString = Object.prototype.toString
|
||||
const withNativeBlob = false
|
||||
// typeof Blob === "function" ||
|
||||
// (typeof Blob !== "undefined" &&
|
||||
// toString.call(Blob) === "[object BlobConstructor]")
|
||||
const withNativeFile = false
|
||||
// typeof File === "function" ||
|
||||
// (typeof File !== "undefined" &&
|
||||
// toString.call(File) === "[object FileConstructor]")
|
||||
|
||||
/**
|
||||
* Returns true if obj is a Buffer, an ArrayBuffer, a Blob or a File.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
export function isBinary(obj: any) {
|
||||
return (
|
||||
(withNativeArrayBuffer && (obj instanceof ArrayBuffer || isView(obj)))
|
||||
// || (withNativeBlob && obj instanceof Blob) || (withNativeFile && obj instanceof File)
|
||||
)
|
||||
}
|
||||
|
||||
export function hasBinary(obj: any, toJSON?: boolean) {
|
||||
if (!obj || typeof obj !== "object") {
|
||||
return false
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
for (let i = 0, l = obj.length; i < l; i++) {
|
||||
if (hasBinary(obj[i])) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if (isBinary(obj)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (
|
||||
obj.toJSON &&
|
||||
typeof obj.toJSON === "function" &&
|
||||
arguments.length === 1
|
||||
) {
|
||||
return hasBinary(obj.toJSON(), true)
|
||||
}
|
||||
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key) && hasBinary(obj[key])) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
Reference in New Issue
Block a user