You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

312 lines
9.7 KiB
JavaScript

7 months ago
import { Emitter } from "@socket.io/component-emitter";
import { deconstructPacket, reconstructPacket } from "./binary.js";
import { isBinary, hasBinary } from "./is-binary.js";
/**
* 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) {
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);
}
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");
}
}
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 = [];
}
}