You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
293 lines
9.1 KiB
293 lines
9.1 KiB
import { Emitter } from "@socket.io/component-emitter"; |
|
import { deconstructPacket, reconstructPacket } from "./binary.js"; |
|
import { isBinary, hasBinary } from "./is-binary.js"; |
|
import debugModule from "debug"; // debug() |
|
const debug = debugModule("socket.io-parser"); // debug() |
|
/** |
|
* Protocol version. |
|
* |
|
* @public |
|
*/ |
|
export const protocol = 5; |
|
export var PacketType; |
|
(function (PacketType) { |
|
PacketType[PacketType["CONNECT"] = 0] = "CONNECT"; |
|
PacketType[PacketType["DISCONNECT"] = 1] = "DISCONNECT"; |
|
PacketType[PacketType["EVENT"] = 2] = "EVENT"; |
|
PacketType[PacketType["ACK"] = 3] = "ACK"; |
|
PacketType[PacketType["CONNECT_ERROR"] = 4] = "CONNECT_ERROR"; |
|
PacketType[PacketType["BINARY_EVENT"] = 5] = "BINARY_EVENT"; |
|
PacketType[PacketType["BINARY_ACK"] = 6] = "BINARY_ACK"; |
|
})(PacketType || (PacketType = {})); |
|
/** |
|
* A socket.io Encoder instance |
|
*/ |
|
export class Encoder { |
|
/** |
|
* Encoder constructor |
|
* |
|
* @param {function} replacer - custom replacer to pass down to JSON.parse |
|
*/ |
|
constructor(replacer) { |
|
this.replacer = replacer; |
|
} |
|
/** |
|
* Encode a packet as a single string if non-binary, or as a |
|
* buffer sequence, depending on packet type. |
|
* |
|
* @param {Object} obj - packet object |
|
*/ |
|
encode(obj) { |
|
debug("encoding packet %j", 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. |
|
*/ |
|
encodeAsString(obj) { |
|
// 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, this.replacer); |
|
} |
|
debug("encoded %j as %s", obj, str); |
|
return str; |
|
} |
|
/** |
|
* Encode packet as 'buffer sequence' by removing blobs, and |
|
* deconstructing packet into object with placeholders and |
|
* a list of buffers. |
|
*/ |
|
encodeAsBinary(obj) { |
|
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 Emitter { |
|
/** |
|
* Decoder constructor |
|
* |
|
* @param {function} reviver - custom reviver to pass down to JSON.stringify |
|
*/ |
|
constructor(reviver) { |
|
super(); |
|
this.reviver = reviver; |
|
} |
|
/** |
|
* Decodes an encoded packet string into packet JSON. |
|
* |
|
* @param {String} obj - encoded packet |
|
*/ |
|
add(obj) { |
|
let packet; |
|
if (typeof obj === "string") { |
|
if (this.reconstructor) { |
|
throw new Error("got plaintext data when reconstructing a packet"); |
|
} |
|
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.emitReserved("decoded", packet); |
|
} |
|
} |
|
else { |
|
// non-binary full packet |
|
super.emitReserved("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.emitReserved("decoded", packet); |
|
} |
|
} |
|
} |
|
else { |
|
throw new Error("Unknown type: " + obj); |
|
} |
|
} |
|
/** |
|
* Decode a packet String (JSON data) |
|
* |
|
* @param {String} str |
|
* @return {Object} packet |
|
*/ |
|
decodeString(str) { |
|
let i = 0; |
|
// look up type |
|
const p = { |
|
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 = this.tryParse(str.substr(i)); |
|
if (Decoder.isPayloadValid(p.type, payload)) { |
|
p.data = payload; |
|
} |
|
else { |
|
throw new Error("invalid payload"); |
|
} |
|
} |
|
debug("decoded %s as %j", str, p); |
|
return p; |
|
} |
|
tryParse(str) { |
|
try { |
|
return JSON.parse(str, this.reviver); |
|
} |
|
catch (e) { |
|
return false; |
|
} |
|
} |
|
static isPayloadValid(type, payload) { |
|
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 |
|
*/ |
|
destroy() { |
|
if (this.reconstructor) { |
|
this.reconstructor.finishedReconstruction(); |
|
} |
|
} |
|
} |
|
/** |
|
* 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 { |
|
constructor(packet) { |
|
this.packet = packet; |
|
this.buffers = []; |
|
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. |
|
*/ |
|
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. |
|
*/ |
|
finishedReconstruction() { |
|
this.reconPack = null; |
|
this.buffers = []; |
|
} |
|
}
|
|
|