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() /** * These strings must not be used as event names, as they have a special meaning. */ const RESERVED_EVENTS = [ "connect", "connect_error", "disconnect", "disconnecting", "newListener", "removeListener", // used by the Node.js EventEmitter ]; /** * 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)) { return this.encodeAsBinary({ type: obj.type === PacketType.EVENT ? PacketType.BINARY_EVENT : PacketType.BINARY_ACK, nsp: obj.nsp, data: obj.data, id: obj.id, }); } } 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 } } // see https://stackoverflow.com/questions/8511281/check-if-a-value-is-an-object-in-javascript function isObject(value) { return Object.prototype.toString.call(value) === "[object Object]"; } /** * 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); const isBinaryEvent = packet.type === PacketType.BINARY_EVENT; if (isBinaryEvent || packet.type === PacketType.BINARY_ACK) { packet.type = isBinaryEvent ? PacketType.EVENT : PacketType.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 isObject(payload); case PacketType.DISCONNECT: return payload === undefined; case PacketType.CONNECT_ERROR: return typeof payload === "string" || isObject(payload); case PacketType.EVENT: case PacketType.BINARY_EVENT: return (Array.isArray(payload) && (typeof payload[0] === "number" || (typeof payload[0] === "string" && RESERVED_EVENTS.indexOf(payload[0]) === -1))); case PacketType.ACK: case PacketType.BINARY_ACK: return Array.isArray(payload); } } /** * Deallocates a parser's resources */ destroy() { if (this.reconstructor) { this.reconstructor.finishedReconstruction(); this.reconstructor = null; } } } /** * 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 = []; } }