index.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  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",
  9. "connect_error",
  10. "disconnect",
  11. "disconnecting",
  12. "newListener",
  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. // see https://stackoverflow.com/questions/8511281/check-if-a-value-is-an-object-in-javascript
  104. function isObject(value) {
  105. return Object.prototype.toString.call(value) === "[object Object]";
  106. }
  107. /**
  108. * A socket.io Decoder instance
  109. *
  110. * @return {Object} decoder
  111. */
  112. export class Decoder extends Emitter {
  113. /**
  114. * Decoder constructor
  115. *
  116. * @param {function} reviver - custom reviver to pass down to JSON.stringify
  117. */
  118. constructor(reviver) {
  119. super();
  120. this.reviver = reviver;
  121. }
  122. /**
  123. * Decodes an encoded packet string into packet JSON.
  124. *
  125. * @param {String} obj - encoded packet
  126. */
  127. add(obj) {
  128. let packet;
  129. if (typeof obj === "string") {
  130. if (this.reconstructor) {
  131. throw new Error("got plaintext data when reconstructing a packet");
  132. }
  133. packet = this.decodeString(obj);
  134. const isBinaryEvent = packet.type === PacketType.BINARY_EVENT;
  135. if (isBinaryEvent || packet.type === PacketType.BINARY_ACK) {
  136. packet.type = isBinaryEvent ? PacketType.EVENT : PacketType.ACK;
  137. // binary packet's json
  138. this.reconstructor = new BinaryReconstructor(packet);
  139. // no attachments, labeled binary but no binary data to follow
  140. if (packet.attachments === 0) {
  141. super.emitReserved("decoded", packet);
  142. }
  143. }
  144. else {
  145. // non-binary full packet
  146. super.emitReserved("decoded", packet);
  147. }
  148. }
  149. else if (isBinary(obj) || obj.base64) {
  150. // raw binary data
  151. if (!this.reconstructor) {
  152. throw new Error("got binary data when not reconstructing a packet");
  153. }
  154. else {
  155. packet = this.reconstructor.takeBinaryData(obj);
  156. if (packet) {
  157. // received final buffer
  158. this.reconstructor = null;
  159. super.emitReserved("decoded", packet);
  160. }
  161. }
  162. }
  163. else {
  164. throw new Error("Unknown type: " + obj);
  165. }
  166. }
  167. /**
  168. * Decode a packet String (JSON data)
  169. *
  170. * @param {String} str
  171. * @return {Object} packet
  172. */
  173. decodeString(str) {
  174. let i = 0;
  175. // look up type
  176. const p = {
  177. type: Number(str.charAt(0)),
  178. };
  179. if (PacketType[p.type] === undefined) {
  180. throw new Error("unknown packet type " + p.type);
  181. }
  182. // look up attachments if type binary
  183. if (p.type === PacketType.BINARY_EVENT ||
  184. p.type === PacketType.BINARY_ACK) {
  185. const start = i + 1;
  186. while (str.charAt(++i) !== "-" && i != str.length) { }
  187. const buf = str.substring(start, i);
  188. if (buf != Number(buf) || str.charAt(i) !== "-") {
  189. throw new Error("Illegal attachments");
  190. }
  191. p.attachments = Number(buf);
  192. }
  193. // look up namespace (if any)
  194. if ("/" === str.charAt(i + 1)) {
  195. const start = i + 1;
  196. while (++i) {
  197. const c = str.charAt(i);
  198. if ("," === c)
  199. break;
  200. if (i === str.length)
  201. break;
  202. }
  203. p.nsp = str.substring(start, i);
  204. }
  205. else {
  206. p.nsp = "/";
  207. }
  208. // look up id
  209. const next = str.charAt(i + 1);
  210. if ("" !== next && Number(next) == next) {
  211. const start = i + 1;
  212. while (++i) {
  213. const c = str.charAt(i);
  214. if (null == c || Number(c) != c) {
  215. --i;
  216. break;
  217. }
  218. if (i === str.length)
  219. break;
  220. }
  221. p.id = Number(str.substring(start, i + 1));
  222. }
  223. // look up json data
  224. if (str.charAt(++i)) {
  225. const payload = this.tryParse(str.substr(i));
  226. if (Decoder.isPayloadValid(p.type, payload)) {
  227. p.data = payload;
  228. }
  229. else {
  230. throw new Error("invalid payload");
  231. }
  232. }
  233. return p;
  234. }
  235. tryParse(str) {
  236. try {
  237. return JSON.parse(str, this.reviver);
  238. }
  239. catch (e) {
  240. return false;
  241. }
  242. }
  243. static isPayloadValid(type, payload) {
  244. switch (type) {
  245. case PacketType.CONNECT:
  246. return isObject(payload);
  247. case PacketType.DISCONNECT:
  248. return payload === undefined;
  249. case PacketType.CONNECT_ERROR:
  250. return typeof payload === "string" || isObject(payload);
  251. case PacketType.EVENT:
  252. case PacketType.BINARY_EVENT:
  253. return (Array.isArray(payload) &&
  254. (typeof payload[0] === "number" ||
  255. (typeof payload[0] === "string" &&
  256. RESERVED_EVENTS.indexOf(payload[0]) === -1)));
  257. case PacketType.ACK:
  258. case PacketType.BINARY_ACK:
  259. return Array.isArray(payload);
  260. }
  261. }
  262. /**
  263. * Deallocates a parser's resources
  264. */
  265. destroy() {
  266. if (this.reconstructor) {
  267. this.reconstructor.finishedReconstruction();
  268. this.reconstructor = null;
  269. }
  270. }
  271. }
  272. /**
  273. * A manager of a binary event's 'buffer sequence'. Should
  274. * be constructed whenever a packet of type BINARY_EVENT is
  275. * decoded.
  276. *
  277. * @param {Object} packet
  278. * @return {BinaryReconstructor} initialized reconstructor
  279. */
  280. class BinaryReconstructor {
  281. constructor(packet) {
  282. this.packet = packet;
  283. this.buffers = [];
  284. this.reconPack = packet;
  285. }
  286. /**
  287. * Method to be called when binary data received from connection
  288. * after a BINARY_EVENT packet.
  289. *
  290. * @param {Buffer | ArrayBuffer} binData - the raw binary data received
  291. * @return {null | Object} returns null if more binary data is expected or
  292. * a reconstructed packet object if all buffers have been received.
  293. */
  294. takeBinaryData(binData) {
  295. this.buffers.push(binData);
  296. if (this.buffers.length === this.reconPack.attachments) {
  297. // done with buffer list
  298. const packet = reconstructPacket(this.reconPack, this.buffers);
  299. this.finishedReconstruction();
  300. return packet;
  301. }
  302. return null;
  303. }
  304. /**
  305. * Cleans up binary packet reconstruction variables.
  306. */
  307. finishedReconstruction() {
  308. this.reconPack = null;
  309. this.buffers = [];
  310. }
  311. }