index.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. import { Emitter } from "@socket.io/component-emitter";
  2. import { deconstructPacket, reconstructPacket } from "./binary.js";
  3. import { isBinary, hasBinary } from "./is-binary.js";
  4. /**
  5. * These strings must not be used as event names, as they have a special meaning.
  6. */
  7. const RESERVED_EVENTS = [
  8. "connect", // used on the client side
  9. "connect_error", // used on the client side
  10. "disconnect", // used on both sides
  11. "disconnecting", // used on the server side
  12. "newListener", // used by the Node.js EventEmitter
  13. "removeListener", // used by the Node.js EventEmitter
  14. ];
  15. /**
  16. * Protocol version.
  17. *
  18. * @public
  19. */
  20. export const protocol = 5;
  21. export var PacketType;
  22. (function (PacketType) {
  23. PacketType[PacketType["CONNECT"] = 0] = "CONNECT";
  24. PacketType[PacketType["DISCONNECT"] = 1] = "DISCONNECT";
  25. PacketType[PacketType["EVENT"] = 2] = "EVENT";
  26. PacketType[PacketType["ACK"] = 3] = "ACK";
  27. PacketType[PacketType["CONNECT_ERROR"] = 4] = "CONNECT_ERROR";
  28. PacketType[PacketType["BINARY_EVENT"] = 5] = "BINARY_EVENT";
  29. PacketType[PacketType["BINARY_ACK"] = 6] = "BINARY_ACK";
  30. })(PacketType || (PacketType = {}));
  31. /**
  32. * A socket.io Encoder instance
  33. */
  34. export class Encoder {
  35. /**
  36. * Encoder constructor
  37. *
  38. * @param {function} replacer - custom replacer to pass down to JSON.parse
  39. */
  40. constructor(replacer) {
  41. this.replacer = replacer;
  42. }
  43. /**
  44. * Encode a packet as a single string if non-binary, or as a
  45. * buffer sequence, depending on packet type.
  46. *
  47. * @param {Object} obj - packet object
  48. */
  49. encode(obj) {
  50. if (obj.type === PacketType.EVENT || obj.type === PacketType.ACK) {
  51. if (hasBinary(obj)) {
  52. return this.encodeAsBinary({
  53. type: obj.type === PacketType.EVENT
  54. ? PacketType.BINARY_EVENT
  55. : PacketType.BINARY_ACK,
  56. nsp: obj.nsp,
  57. data: obj.data,
  58. id: obj.id,
  59. });
  60. }
  61. }
  62. return [this.encodeAsString(obj)];
  63. }
  64. /**
  65. * Encode packet as string.
  66. */
  67. encodeAsString(obj) {
  68. // first is type
  69. let str = "" + obj.type;
  70. // attachments if we have them
  71. if (obj.type === PacketType.BINARY_EVENT ||
  72. obj.type === PacketType.BINARY_ACK) {
  73. str += obj.attachments + "-";
  74. }
  75. // if we have a namespace other than `/`
  76. // we append it followed by a comma `,`
  77. if (obj.nsp && "/" !== obj.nsp) {
  78. str += obj.nsp + ",";
  79. }
  80. // immediately followed by the id
  81. if (null != obj.id) {
  82. str += obj.id;
  83. }
  84. // json data
  85. if (null != obj.data) {
  86. str += JSON.stringify(obj.data, this.replacer);
  87. }
  88. return str;
  89. }
  90. /**
  91. * Encode packet as 'buffer sequence' by removing blobs, and
  92. * deconstructing packet into object with placeholders and
  93. * a list of buffers.
  94. */
  95. encodeAsBinary(obj) {
  96. const deconstruction = deconstructPacket(obj);
  97. const pack = this.encodeAsString(deconstruction.packet);
  98. const buffers = deconstruction.buffers;
  99. buffers.unshift(pack); // add packet info to beginning of data list
  100. return buffers; // write all the buffers
  101. }
  102. }
  103. /**
  104. * A socket.io Decoder instance
  105. *
  106. * @return {Object} decoder
  107. */
  108. export class Decoder extends Emitter {
  109. /**
  110. * Decoder constructor
  111. */
  112. constructor(opts) {
  113. super();
  114. this.opts = Object.assign({
  115. reviver: undefined,
  116. maxAttachments: 10,
  117. }, typeof opts === "function" ? { reviver: opts } : opts);
  118. }
  119. /**
  120. * Decodes an encoded packet string into packet JSON.
  121. *
  122. * @param {String} obj - encoded packet
  123. */
  124. add(obj) {
  125. let packet;
  126. if (typeof obj === "string") {
  127. if (this.reconstructor) {
  128. throw new Error("got plaintext data when reconstructing a packet");
  129. }
  130. packet = this.decodeString(obj);
  131. const isBinaryEvent = packet.type === PacketType.BINARY_EVENT;
  132. if (isBinaryEvent || packet.type === PacketType.BINARY_ACK) {
  133. packet.type = isBinaryEvent ? PacketType.EVENT : PacketType.ACK;
  134. // binary packet's json
  135. this.reconstructor = new BinaryReconstructor(packet);
  136. // no attachments, labeled binary but no binary data to follow
  137. if (packet.attachments === 0) {
  138. super.emitReserved("decoded", packet);
  139. }
  140. }
  141. else {
  142. // non-binary full packet
  143. super.emitReserved("decoded", packet);
  144. }
  145. }
  146. else if (isBinary(obj) || obj.base64) {
  147. // raw binary data
  148. if (!this.reconstructor) {
  149. throw new Error("got binary data when not reconstructing a packet");
  150. }
  151. else {
  152. packet = this.reconstructor.takeBinaryData(obj);
  153. if (packet) {
  154. // received final buffer
  155. this.reconstructor = null;
  156. super.emitReserved("decoded", packet);
  157. }
  158. }
  159. }
  160. else {
  161. throw new Error("Unknown type: " + obj);
  162. }
  163. }
  164. /**
  165. * Decode a packet String (JSON data)
  166. *
  167. * @param {String} str
  168. * @return {Object} packet
  169. */
  170. decodeString(str) {
  171. let i = 0;
  172. // look up type
  173. const p = {
  174. type: Number(str.charAt(0)),
  175. };
  176. if (PacketType[p.type] === undefined) {
  177. throw new Error("unknown packet type " + p.type);
  178. }
  179. // look up attachments if type binary
  180. if (p.type === PacketType.BINARY_EVENT ||
  181. p.type === PacketType.BINARY_ACK) {
  182. const start = i + 1;
  183. while (str.charAt(++i) !== "-" && i != str.length) { }
  184. const buf = str.substring(start, i);
  185. if (buf != Number(buf) || str.charAt(i) !== "-") {
  186. throw new Error("Illegal attachments");
  187. }
  188. const n = Number(buf);
  189. if (!isInteger(n) || n < 0) {
  190. throw new Error("Illegal attachments");
  191. }
  192. else if (n > this.opts.maxAttachments) {
  193. throw new Error("too many attachments");
  194. }
  195. p.attachments = n;
  196. }
  197. // look up namespace (if any)
  198. if ("/" === str.charAt(i + 1)) {
  199. const start = i + 1;
  200. while (++i) {
  201. const c = str.charAt(i);
  202. if ("," === c)
  203. break;
  204. if (i === str.length)
  205. break;
  206. }
  207. p.nsp = str.substring(start, i);
  208. }
  209. else {
  210. p.nsp = "/";
  211. }
  212. // look up id
  213. const next = str.charAt(i + 1);
  214. if ("" !== next && Number(next) == next) {
  215. const start = i + 1;
  216. while (++i) {
  217. const c = str.charAt(i);
  218. if (null == c || Number(c) != c) {
  219. --i;
  220. break;
  221. }
  222. if (i === str.length)
  223. break;
  224. }
  225. p.id = Number(str.substring(start, i + 1));
  226. }
  227. // look up json data
  228. if (str.charAt(++i)) {
  229. const payload = this.tryParse(str.substr(i));
  230. if (Decoder.isPayloadValid(p.type, payload)) {
  231. p.data = payload;
  232. }
  233. else {
  234. throw new Error("invalid payload");
  235. }
  236. }
  237. return p;
  238. }
  239. tryParse(str) {
  240. try {
  241. return JSON.parse(str, this.opts.reviver);
  242. }
  243. catch (e) {
  244. return false;
  245. }
  246. }
  247. static isPayloadValid(type, payload) {
  248. switch (type) {
  249. case PacketType.CONNECT:
  250. return isObject(payload);
  251. case PacketType.DISCONNECT:
  252. return payload === undefined;
  253. case PacketType.CONNECT_ERROR:
  254. return typeof payload === "string" || isObject(payload);
  255. case PacketType.EVENT:
  256. case PacketType.BINARY_EVENT:
  257. return (Array.isArray(payload) &&
  258. (typeof payload[0] === "number" ||
  259. (typeof payload[0] === "string" &&
  260. RESERVED_EVENTS.indexOf(payload[0]) === -1)));
  261. case PacketType.ACK:
  262. case PacketType.BINARY_ACK:
  263. return Array.isArray(payload);
  264. }
  265. }
  266. /**
  267. * Deallocates a parser's resources
  268. */
  269. destroy() {
  270. if (this.reconstructor) {
  271. this.reconstructor.finishedReconstruction();
  272. this.reconstructor = null;
  273. }
  274. }
  275. }
  276. /**
  277. * A manager of a binary event's 'buffer sequence'. Should
  278. * be constructed whenever a packet of type BINARY_EVENT is
  279. * decoded.
  280. *
  281. * @param {Object} packet
  282. * @return {BinaryReconstructor} initialized reconstructor
  283. */
  284. class BinaryReconstructor {
  285. constructor(packet) {
  286. this.packet = packet;
  287. this.buffers = [];
  288. this.reconPack = packet;
  289. }
  290. /**
  291. * Method to be called when binary data received from connection
  292. * after a BINARY_EVENT packet.
  293. *
  294. * @param {Buffer | ArrayBuffer} binData - the raw binary data received
  295. * @return {null | Object} returns null if more binary data is expected or
  296. * a reconstructed packet object if all buffers have been received.
  297. */
  298. takeBinaryData(binData) {
  299. this.buffers.push(binData);
  300. if (this.buffers.length === this.reconPack.attachments) {
  301. // done with buffer list
  302. const packet = reconstructPacket(this.reconPack, this.buffers);
  303. this.finishedReconstruction();
  304. return packet;
  305. }
  306. return null;
  307. }
  308. /**
  309. * Cleans up binary packet reconstruction variables.
  310. */
  311. finishedReconstruction() {
  312. this.reconPack = null;
  313. this.buffers = [];
  314. }
  315. }
  316. function isNamespaceValid(nsp) {
  317. return typeof nsp === "string";
  318. }
  319. // see https://caniuse.com/mdn-javascript_builtins_number_isinteger
  320. const isInteger = Number.isInteger ||
  321. function (value) {
  322. return (typeof value === "number" &&
  323. isFinite(value) &&
  324. Math.floor(value) === value);
  325. };
  326. function isAckIdValid(id) {
  327. return id === undefined || isInteger(id);
  328. }
  329. // see https://stackoverflow.com/questions/8511281/check-if-a-value-is-an-object-in-javascript
  330. function isObject(value) {
  331. return Object.prototype.toString.call(value) === "[object Object]";
  332. }
  333. function isDataValid(type, payload) {
  334. switch (type) {
  335. case PacketType.CONNECT:
  336. return payload === undefined || isObject(payload);
  337. case PacketType.DISCONNECT:
  338. return payload === undefined;
  339. case PacketType.EVENT:
  340. return (Array.isArray(payload) &&
  341. (typeof payload[0] === "number" ||
  342. (typeof payload[0] === "string" &&
  343. RESERVED_EVENTS.indexOf(payload[0]) === -1)));
  344. case PacketType.ACK:
  345. return Array.isArray(payload);
  346. case PacketType.CONNECT_ERROR:
  347. return typeof payload === "string" || isObject(payload);
  348. default:
  349. return false;
  350. }
  351. }
  352. export function isPacketValid(packet) {
  353. return (isNamespaceValid(packet.nsp) &&
  354. isAckIdValid(packet.id) &&
  355. isDataValid(packet.type, packet.data));
  356. }