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.

474 lines
11 KiB

7 years ago
  1. /**
  2. * Module dependencies.
  3. */
  4. var utf8 = require('wtf-8');
  5. var after = require('after');
  6. var keys = require('./keys');
  7. /**
  8. * Current protocol version.
  9. */
  10. exports.protocol = 3;
  11. /**
  12. * Packet types.
  13. */
  14. var packets = exports.packets = {
  15. open: 0 // non-ws
  16. , close: 1 // non-ws
  17. , ping: 2
  18. , pong: 3
  19. , message: 4
  20. , upgrade: 5
  21. , noop: 6
  22. };
  23. var packetslist = keys(packets);
  24. /**
  25. * Premade error packet.
  26. */
  27. var err = { type: 'error', data: 'parser error' };
  28. /**
  29. * Encodes a packet.
  30. *
  31. * <packet type id> [ <data> ]
  32. *
  33. * Example:
  34. *
  35. * 5hello world
  36. * 3
  37. * 4
  38. *
  39. * Binary is encoded in an identical principle
  40. *
  41. * @api private
  42. */
  43. exports.encodePacket = function (packet, supportsBinary, utf8encode, callback) {
  44. if ('function' == typeof supportsBinary) {
  45. callback = supportsBinary;
  46. supportsBinary = null;
  47. }
  48. if ('function' == typeof utf8encode ) {
  49. callback = utf8encode;
  50. utf8encode = null;
  51. }
  52. if (Buffer.isBuffer(packet.data)) {
  53. return encodeBuffer(packet, supportsBinary, callback);
  54. } else if (packet.data && (packet.data.buffer || packet.data) instanceof ArrayBuffer) {
  55. packet.data = arrayBufferToBuffer(packet.data);
  56. return encodeBuffer(packet, supportsBinary, callback);
  57. }
  58. // Sending data as a utf-8 string
  59. var encoded = packets[packet.type];
  60. // data fragment is optional
  61. if (undefined !== packet.data) {
  62. encoded += utf8encode ? utf8.encode(String(packet.data)) : String(packet.data);
  63. }
  64. return callback('' + encoded);
  65. };
  66. /**
  67. * Encode Buffer data
  68. */
  69. function encodeBuffer(packet, supportsBinary, callback) {
  70. var data = packet.data;
  71. if (!supportsBinary) {
  72. return exports.encodeBase64Packet(packet, callback);
  73. }
  74. var typeBuffer = new Buffer(1);
  75. typeBuffer[0] = packets[packet.type];
  76. return callback(Buffer.concat([typeBuffer, data]));
  77. }
  78. /**
  79. * Encodes a packet with binary data in a base64 string
  80. *
  81. * @param {Object} packet, has `type` and `data`
  82. * @return {String} base64 encoded message
  83. */
  84. exports.encodeBase64Packet = function(packet, callback){
  85. if (!Buffer.isBuffer(packet.data)) {
  86. packet.data = arrayBufferToBuffer(packet.data);
  87. }
  88. var message = 'b' + packets[packet.type];
  89. message += packet.data.toString('base64');
  90. return callback(message);
  91. };
  92. /**
  93. * Decodes a packet. Data also available as an ArrayBuffer if requested.
  94. *
  95. * @return {Object} with `type` and `data` (if any)
  96. * @api private
  97. */
  98. exports.decodePacket = function (data, binaryType, utf8decode) {
  99. if (data === undefined) {
  100. return err;
  101. }
  102. // String data
  103. if (typeof data == 'string') {
  104. if (data.charAt(0) == 'b') {
  105. return exports.decodeBase64Packet(data.substr(1), binaryType);
  106. }
  107. var type = data.charAt(0);
  108. if (utf8decode) {
  109. data = tryDecode(data);
  110. if (data === false) {
  111. return err;
  112. }
  113. }
  114. if (Number(type) != type || !packetslist[type]) {
  115. return err;
  116. }
  117. if (data.length > 1) {
  118. return { type: packetslist[type], data: data.substring(1) };
  119. } else {
  120. return { type: packetslist[type] };
  121. }
  122. }
  123. // Binary data
  124. if (binaryType === 'arraybuffer') {
  125. // wrap Buffer/ArrayBuffer data into an Uint8Array
  126. var intArray = new Uint8Array(data);
  127. var type = intArray[0];
  128. return { type: packetslist[type], data: intArray.buffer.slice(1) };
  129. }
  130. if (data instanceof ArrayBuffer) {
  131. data = arrayBufferToBuffer(data);
  132. }
  133. var type = data[0];
  134. return { type: packetslist[type], data: data.slice(1) };
  135. };
  136. function tryDecode(data) {
  137. try {
  138. data = utf8.decode(data);
  139. } catch (e) {
  140. return false;
  141. }
  142. return data;
  143. }
  144. /**
  145. * Decodes a packet encoded in a base64 string.
  146. *
  147. * @param {String} base64 encoded message
  148. * @return {Object} with `type` and `data` (if any)
  149. */
  150. exports.decodeBase64Packet = function(msg, binaryType) {
  151. var type = packetslist[msg.charAt(0)];
  152. var data = new Buffer(msg.substr(1), 'base64');
  153. if (binaryType === 'arraybuffer') {
  154. var abv = new Uint8Array(data.length);
  155. for (var i = 0; i < abv.length; i++){
  156. abv[i] = data[i];
  157. }
  158. data = abv.buffer;
  159. }
  160. return { type: type, data: data };
  161. };
  162. /**
  163. * Encodes multiple messages (payload).
  164. *
  165. * <length>:data
  166. *
  167. * Example:
  168. *
  169. * 11:hello world2:hi
  170. *
  171. * If any contents are binary, they will be encoded as base64 strings. Base64
  172. * encoded strings are marked with a b before the length specifier
  173. *
  174. * @param {Array} packets
  175. * @api private
  176. */
  177. exports.encodePayload = function (packets, supportsBinary, callback) {
  178. if (typeof supportsBinary == 'function') {
  179. callback = supportsBinary;
  180. supportsBinary = null;
  181. }
  182. if (supportsBinary) {
  183. return exports.encodePayloadAsBinary(packets, callback);
  184. }
  185. if (!packets.length) {
  186. return callback('0:');
  187. }
  188. function setLengthHeader(message) {
  189. return message.length + ':' + message;
  190. }
  191. function encodeOne(packet, doneCallback) {
  192. exports.encodePacket(packet, supportsBinary, true, function(message) {
  193. doneCallback(null, setLengthHeader(message));
  194. });
  195. }
  196. map(packets, encodeOne, function(err, results) {
  197. return callback(results.join(''));
  198. });
  199. };
  200. /**
  201. * Async array map using after
  202. */
  203. function map(ary, each, done) {
  204. var result = new Array(ary.length);
  205. var next = after(ary.length, done);
  206. var eachWithIndex = function(i, el, cb) {
  207. each(el, function(error, msg) {
  208. result[i] = msg;
  209. cb(error, result);
  210. });
  211. };
  212. for (var i = 0; i < ary.length; i++) {
  213. eachWithIndex(i, ary[i], next);
  214. }
  215. }
  216. /*
  217. * Decodes data when a payload is maybe expected. Possible binary contents are
  218. * decoded from their base64 representation
  219. *
  220. * @param {String} data, callback method
  221. * @api public
  222. */
  223. exports.decodePayload = function (data, binaryType, callback) {
  224. if ('string' != typeof data) {
  225. return exports.decodePayloadAsBinary(data, binaryType, callback);
  226. }
  227. if (typeof binaryType === 'function') {
  228. callback = binaryType;
  229. binaryType = null;
  230. }
  231. var packet;
  232. if (data == '') {
  233. // parser error - ignoring payload
  234. return callback(err, 0, 1);
  235. }
  236. var length = ''
  237. , n, msg;
  238. for (var i = 0, l = data.length; i < l; i++) {
  239. var chr = data.charAt(i);
  240. if (':' != chr) {
  241. length += chr;
  242. } else {
  243. if ('' == length || (length != (n = Number(length)))) {
  244. // parser error - ignoring payload
  245. return callback(err, 0, 1);
  246. }
  247. msg = data.substr(i + 1, n);
  248. if (length != msg.length) {
  249. // parser error - ignoring payload
  250. return callback(err, 0, 1);
  251. }
  252. if (msg.length) {
  253. packet = exports.decodePacket(msg, binaryType, true);
  254. if (err.type == packet.type && err.data == packet.data) {
  255. // parser error in individual packet - ignoring payload
  256. return callback(err, 0, 1);
  257. }
  258. var ret = callback(packet, i + n, l);
  259. if (false === ret) return;
  260. }
  261. // advance cursor
  262. i += n;
  263. length = '';
  264. }
  265. }
  266. if (length != '') {
  267. // parser error - ignoring payload
  268. return callback(err, 0, 1);
  269. }
  270. };
  271. /**
  272. *
  273. * Converts a buffer to a utf8.js encoded string
  274. *
  275. * @api private
  276. */
  277. function bufferToString(buffer) {
  278. var str = '';
  279. for (var i = 0; i < buffer.length; i++) {
  280. str += String.fromCharCode(buffer[i]);
  281. }
  282. return str;
  283. }
  284. /**
  285. *
  286. * Converts a utf8.js encoded string to a buffer
  287. *
  288. * @api private
  289. */
  290. function stringToBuffer(string) {
  291. var buf = new Buffer(string.length);
  292. for (var i = 0; i < string.length; i++) {
  293. buf.writeUInt8(string.charCodeAt(i), i);
  294. }
  295. return buf;
  296. }
  297. /**
  298. *
  299. * Converts an ArrayBuffer to a Buffer
  300. *
  301. * @api private
  302. */
  303. function arrayBufferToBuffer(data) {
  304. // data is either an ArrayBuffer or ArrayBufferView.
  305. var array = new Uint8Array(data.buffer || data);
  306. var length = data.byteLength || data.length;
  307. var offset = data.byteOffset || 0;
  308. var buffer = new Buffer(length);
  309. for (var i = 0; i < length; i++) {
  310. buffer[i] = array[offset + i];
  311. }
  312. return buffer;
  313. }
  314. /**
  315. * Encodes multiple messages (payload) as binary.
  316. *
  317. * <1 = binary, 0 = string><number from 0-9><number from 0-9>[...]<number
  318. * 255><data>
  319. *
  320. * Example:
  321. * 1 3 255 1 2 3, if the binary contents are interpreted as 8 bit integers
  322. *
  323. * @param {Array} packets
  324. * @return {Buffer} encoded payload
  325. * @api private
  326. */
  327. exports.encodePayloadAsBinary = function (packets, callback) {
  328. if (!packets.length) {
  329. return callback(new Buffer(0));
  330. }
  331. function encodeOne(p, doneCallback) {
  332. exports.encodePacket(p, true, true, function(packet) {
  333. if (typeof packet === 'string') {
  334. var encodingLength = '' + packet.length;
  335. var sizeBuffer = new Buffer(encodingLength.length + 2);
  336. sizeBuffer[0] = 0; // is a string (not true binary = 0)
  337. for (var i = 0; i < encodingLength.length; i++) {
  338. sizeBuffer[i + 1] = parseInt(encodingLength[i], 10);
  339. }
  340. sizeBuffer[sizeBuffer.length - 1] = 255;
  341. return doneCallback(null, Buffer.concat([sizeBuffer, stringToBuffer(packet)]));
  342. }
  343. var encodingLength = '' + packet.length;
  344. var sizeBuffer = new Buffer(encodingLength.length + 2);
  345. sizeBuffer[0] = 1; // is binary (true binary = 1)
  346. for (var i = 0; i < encodingLength.length; i++) {
  347. sizeBuffer[i + 1] = parseInt(encodingLength[i], 10);
  348. }
  349. sizeBuffer[sizeBuffer.length - 1] = 255;
  350. doneCallback(null, Buffer.concat([sizeBuffer, packet]));
  351. });
  352. }
  353. map(packets, encodeOne, function(err, results) {
  354. return callback(Buffer.concat(results));
  355. });
  356. };
  357. /*
  358. * Decodes data when a payload is maybe expected. Strings are decoded by
  359. * interpreting each byte as a key code for entries marked to start with 0. See
  360. * description of encodePayloadAsBinary
  361. * @param {Buffer} data, callback method
  362. * @api public
  363. */
  364. exports.decodePayloadAsBinary = function (data, binaryType, callback) {
  365. if (typeof binaryType === 'function') {
  366. callback = binaryType;
  367. binaryType = null;
  368. }
  369. var bufferTail = data;
  370. var buffers = [];
  371. while (bufferTail.length > 0) {
  372. var strLen = '';
  373. var isString = bufferTail[0] === 0;
  374. var numberTooLong = false;
  375. for (var i = 1; ; i++) {
  376. if (bufferTail[i] == 255) break;
  377. // 310 = char length of Number.MAX_VALUE
  378. if (strLen.length > 310) {
  379. numberTooLong = true;
  380. break;
  381. }
  382. strLen += '' + bufferTail[i];
  383. }
  384. if(numberTooLong) return callback(err, 0, 1);
  385. bufferTail = bufferTail.slice(strLen.length + 1);
  386. var msgLength = parseInt(strLen, 10);
  387. var msg = bufferTail.slice(1, msgLength + 1);
  388. if (isString) msg = bufferToString(msg);
  389. buffers.push(msg);
  390. bufferTail = bufferTail.slice(msgLength + 1);
  391. }
  392. var total = buffers.length;
  393. buffers.forEach(function(buffer, i) {
  394. callback(exports.decodePacket(buffer, binaryType, true), i, total);
  395. });
  396. };