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.

609 lines
14 KiB

7 years ago
  1. /**
  2. * Module dependencies.
  3. */
  4. var keys = require('./keys');
  5. var hasBinary = require('has-binary');
  6. var sliceBuffer = require('arraybuffer.slice');
  7. var after = require('after');
  8. var utf8 = require('wtf-8');
  9. var base64encoder;
  10. if (global && global.ArrayBuffer) {
  11. base64encoder = require('base64-arraybuffer');
  12. }
  13. /**
  14. * Check if we are running an android browser. That requires us to use
  15. * ArrayBuffer with polling transports...
  16. *
  17. * http://ghinda.net/jpeg-blob-ajax-android/
  18. */
  19. var isAndroid = typeof navigator !== 'undefined' && /Android/i.test(navigator.userAgent);
  20. /**
  21. * Check if we are running in PhantomJS.
  22. * Uploading a Blob with PhantomJS does not work correctly, as reported here:
  23. * https://github.com/ariya/phantomjs/issues/11395
  24. * @type boolean
  25. */
  26. var isPhantomJS = typeof navigator !== 'undefined' && /PhantomJS/i.test(navigator.userAgent);
  27. /**
  28. * When true, avoids using Blobs to encode payloads.
  29. * @type boolean
  30. */
  31. var dontSendBlobs = isAndroid || isPhantomJS;
  32. /**
  33. * Current protocol version.
  34. */
  35. exports.protocol = 3;
  36. /**
  37. * Packet types.
  38. */
  39. var packets = exports.packets = {
  40. open: 0 // non-ws
  41. , close: 1 // non-ws
  42. , ping: 2
  43. , pong: 3
  44. , message: 4
  45. , upgrade: 5
  46. , noop: 6
  47. };
  48. var packetslist = keys(packets);
  49. /**
  50. * Premade error packet.
  51. */
  52. var err = { type: 'error', data: 'parser error' };
  53. /**
  54. * Create a blob api even for blob builder when vendor prefixes exist
  55. */
  56. var Blob = require('blob');
  57. /**
  58. * Encodes a packet.
  59. *
  60. * <packet type id> [ <data> ]
  61. *
  62. * Example:
  63. *
  64. * 5hello world
  65. * 3
  66. * 4
  67. *
  68. * Binary is encoded in an identical principle
  69. *
  70. * @api private
  71. */
  72. exports.encodePacket = function (packet, supportsBinary, utf8encode, callback) {
  73. if ('function' == typeof supportsBinary) {
  74. callback = supportsBinary;
  75. supportsBinary = false;
  76. }
  77. if ('function' == typeof utf8encode) {
  78. callback = utf8encode;
  79. utf8encode = null;
  80. }
  81. var data = (packet.data === undefined)
  82. ? undefined
  83. : packet.data.buffer || packet.data;
  84. if (global.ArrayBuffer && data instanceof ArrayBuffer) {
  85. return encodeArrayBuffer(packet, supportsBinary, callback);
  86. } else if (Blob && data instanceof global.Blob) {
  87. return encodeBlob(packet, supportsBinary, callback);
  88. }
  89. // might be an object with { base64: true, data: dataAsBase64String }
  90. if (data && data.base64) {
  91. return encodeBase64Object(packet, callback);
  92. }
  93. // Sending data as a utf-8 string
  94. var encoded = packets[packet.type];
  95. // data fragment is optional
  96. if (undefined !== packet.data) {
  97. encoded += utf8encode ? utf8.encode(String(packet.data)) : String(packet.data);
  98. }
  99. return callback('' + encoded);
  100. };
  101. function encodeBase64Object(packet, callback) {
  102. // packet data is an object { base64: true, data: dataAsBase64String }
  103. var message = 'b' + exports.packets[packet.type] + packet.data.data;
  104. return callback(message);
  105. }
  106. /**
  107. * Encode packet helpers for binary types
  108. */
  109. function encodeArrayBuffer(packet, supportsBinary, callback) {
  110. if (!supportsBinary) {
  111. return exports.encodeBase64Packet(packet, callback);
  112. }
  113. var data = packet.data;
  114. var contentArray = new Uint8Array(data);
  115. var resultBuffer = new Uint8Array(1 + data.byteLength);
  116. resultBuffer[0] = packets[packet.type];
  117. for (var i = 0; i < contentArray.length; i++) {
  118. resultBuffer[i+1] = contentArray[i];
  119. }
  120. return callback(resultBuffer.buffer);
  121. }
  122. function encodeBlobAsArrayBuffer(packet, supportsBinary, callback) {
  123. if (!supportsBinary) {
  124. return exports.encodeBase64Packet(packet, callback);
  125. }
  126. var fr = new FileReader();
  127. fr.onload = function() {
  128. packet.data = fr.result;
  129. exports.encodePacket(packet, supportsBinary, true, callback);
  130. };
  131. return fr.readAsArrayBuffer(packet.data);
  132. }
  133. function encodeBlob(packet, supportsBinary, callback) {
  134. if (!supportsBinary) {
  135. return exports.encodeBase64Packet(packet, callback);
  136. }
  137. if (dontSendBlobs) {
  138. return encodeBlobAsArrayBuffer(packet, supportsBinary, callback);
  139. }
  140. var length = new Uint8Array(1);
  141. length[0] = packets[packet.type];
  142. var blob = new Blob([length.buffer, packet.data]);
  143. return callback(blob);
  144. }
  145. /**
  146. * Encodes a packet with binary data in a base64 string
  147. *
  148. * @param {Object} packet, has `type` and `data`
  149. * @return {String} base64 encoded message
  150. */
  151. exports.encodeBase64Packet = function(packet, callback) {
  152. var message = 'b' + exports.packets[packet.type];
  153. if (Blob && packet.data instanceof global.Blob) {
  154. var fr = new FileReader();
  155. fr.onload = function() {
  156. var b64 = fr.result.split(',')[1];
  157. callback(message + b64);
  158. };
  159. return fr.readAsDataURL(packet.data);
  160. }
  161. var b64data;
  162. try {
  163. b64data = String.fromCharCode.apply(null, new Uint8Array(packet.data));
  164. } catch (e) {
  165. // iPhone Safari doesn't let you apply with typed arrays
  166. var typed = new Uint8Array(packet.data);
  167. var basic = new Array(typed.length);
  168. for (var i = 0; i < typed.length; i++) {
  169. basic[i] = typed[i];
  170. }
  171. b64data = String.fromCharCode.apply(null, basic);
  172. }
  173. message += global.btoa(b64data);
  174. return callback(message);
  175. };
  176. /**
  177. * Decodes a packet. Changes format to Blob if requested.
  178. *
  179. * @return {Object} with `type` and `data` (if any)
  180. * @api private
  181. */
  182. exports.decodePacket = function (data, binaryType, utf8decode) {
  183. if (data === undefined) {
  184. return err;
  185. }
  186. // String data
  187. if (typeof data == 'string') {
  188. if (data.charAt(0) == 'b') {
  189. return exports.decodeBase64Packet(data.substr(1), binaryType);
  190. }
  191. if (utf8decode) {
  192. data = tryDecode(data);
  193. if (data === false) {
  194. return err;
  195. }
  196. }
  197. var type = data.charAt(0);
  198. if (Number(type) != type || !packetslist[type]) {
  199. return err;
  200. }
  201. if (data.length > 1) {
  202. return { type: packetslist[type], data: data.substring(1) };
  203. } else {
  204. return { type: packetslist[type] };
  205. }
  206. }
  207. var asArray = new Uint8Array(data);
  208. var type = asArray[0];
  209. var rest = sliceBuffer(data, 1);
  210. if (Blob && binaryType === 'blob') {
  211. rest = new Blob([rest]);
  212. }
  213. return { type: packetslist[type], data: rest };
  214. };
  215. function tryDecode(data) {
  216. try {
  217. data = utf8.decode(data);
  218. } catch (e) {
  219. return false;
  220. }
  221. return data;
  222. }
  223. /**
  224. * Decodes a packet encoded in a base64 string
  225. *
  226. * @param {String} base64 encoded message
  227. * @return {Object} with `type` and `data` (if any)
  228. */
  229. exports.decodeBase64Packet = function(msg, binaryType) {
  230. var type = packetslist[msg.charAt(0)];
  231. if (!base64encoder) {
  232. return { type: type, data: { base64: true, data: msg.substr(1) } };
  233. }
  234. var data = base64encoder.decode(msg.substr(1));
  235. if (binaryType === 'blob' && Blob) {
  236. data = new Blob([data]);
  237. }
  238. return { type: type, data: data };
  239. };
  240. /**
  241. * Encodes multiple messages (payload).
  242. *
  243. * <length>:data
  244. *
  245. * Example:
  246. *
  247. * 11:hello world2:hi
  248. *
  249. * If any contents are binary, they will be encoded as base64 strings. Base64
  250. * encoded strings are marked with a b before the length specifier
  251. *
  252. * @param {Array} packets
  253. * @api private
  254. */
  255. exports.encodePayload = function (packets, supportsBinary, callback) {
  256. if (typeof supportsBinary == 'function') {
  257. callback = supportsBinary;
  258. supportsBinary = null;
  259. }
  260. var isBinary = hasBinary(packets);
  261. if (supportsBinary && isBinary) {
  262. if (Blob && !dontSendBlobs) {
  263. return exports.encodePayloadAsBlob(packets, callback);
  264. }
  265. return exports.encodePayloadAsArrayBuffer(packets, callback);
  266. }
  267. if (!packets.length) {
  268. return callback('0:');
  269. }
  270. function setLengthHeader(message) {
  271. return message.length + ':' + message;
  272. }
  273. function encodeOne(packet, doneCallback) {
  274. exports.encodePacket(packet, !isBinary ? false : supportsBinary, true, function(message) {
  275. doneCallback(null, setLengthHeader(message));
  276. });
  277. }
  278. map(packets, encodeOne, function(err, results) {
  279. return callback(results.join(''));
  280. });
  281. };
  282. /**
  283. * Async array map using after
  284. */
  285. function map(ary, each, done) {
  286. var result = new Array(ary.length);
  287. var next = after(ary.length, done);
  288. var eachWithIndex = function(i, el, cb) {
  289. each(el, function(error, msg) {
  290. result[i] = msg;
  291. cb(error, result);
  292. });
  293. };
  294. for (var i = 0; i < ary.length; i++) {
  295. eachWithIndex(i, ary[i], next);
  296. }
  297. }
  298. /*
  299. * Decodes data when a payload is maybe expected. Possible binary contents are
  300. * decoded from their base64 representation
  301. *
  302. * @param {String} data, callback method
  303. * @api public
  304. */
  305. exports.decodePayload = function (data, binaryType, callback) {
  306. if (typeof data != 'string') {
  307. return exports.decodePayloadAsBinary(data, binaryType, callback);
  308. }
  309. if (typeof binaryType === 'function') {
  310. callback = binaryType;
  311. binaryType = null;
  312. }
  313. var packet;
  314. if (data == '') {
  315. // parser error - ignoring payload
  316. return callback(err, 0, 1);
  317. }
  318. var length = ''
  319. , n, msg;
  320. for (var i = 0, l = data.length; i < l; i++) {
  321. var chr = data.charAt(i);
  322. if (':' != chr) {
  323. length += chr;
  324. } else {
  325. if ('' == length || (length != (n = Number(length)))) {
  326. // parser error - ignoring payload
  327. return callback(err, 0, 1);
  328. }
  329. msg = data.substr(i + 1, n);
  330. if (length != msg.length) {
  331. // parser error - ignoring payload
  332. return callback(err, 0, 1);
  333. }
  334. if (msg.length) {
  335. packet = exports.decodePacket(msg, binaryType, true);
  336. if (err.type == packet.type && err.data == packet.data) {
  337. // parser error in individual packet - ignoring payload
  338. return callback(err, 0, 1);
  339. }
  340. var ret = callback(packet, i + n, l);
  341. if (false === ret) return;
  342. }
  343. // advance cursor
  344. i += n;
  345. length = '';
  346. }
  347. }
  348. if (length != '') {
  349. // parser error - ignoring payload
  350. return callback(err, 0, 1);
  351. }
  352. };
  353. /**
  354. * Encodes multiple messages (payload) as binary.
  355. *
  356. * <1 = binary, 0 = string><number from 0-9><number from 0-9>[...]<number
  357. * 255><data>
  358. *
  359. * Example:
  360. * 1 3 255 1 2 3, if the binary contents are interpreted as 8 bit integers
  361. *
  362. * @param {Array} packets
  363. * @return {ArrayBuffer} encoded payload
  364. * @api private
  365. */
  366. exports.encodePayloadAsArrayBuffer = function(packets, callback) {
  367. if (!packets.length) {
  368. return callback(new ArrayBuffer(0));
  369. }
  370. function encodeOne(packet, doneCallback) {
  371. exports.encodePacket(packet, true, true, function(data) {
  372. return doneCallback(null, data);
  373. });
  374. }
  375. map(packets, encodeOne, function(err, encodedPackets) {
  376. var totalLength = encodedPackets.reduce(function(acc, p) {
  377. var len;
  378. if (typeof p === 'string'){
  379. len = p.length;
  380. } else {
  381. len = p.byteLength;
  382. }
  383. return acc + len.toString().length + len + 2; // string/binary identifier + separator = 2
  384. }, 0);
  385. var resultArray = new Uint8Array(totalLength);
  386. var bufferIndex = 0;
  387. encodedPackets.forEach(function(p) {
  388. var isString = typeof p === 'string';
  389. var ab = p;
  390. if (isString) {
  391. var view = new Uint8Array(p.length);
  392. for (var i = 0; i < p.length; i++) {
  393. view[i] = p.charCodeAt(i);
  394. }
  395. ab = view.buffer;
  396. }
  397. if (isString) { // not true binary
  398. resultArray[bufferIndex++] = 0;
  399. } else { // true binary
  400. resultArray[bufferIndex++] = 1;
  401. }
  402. var lenStr = ab.byteLength.toString();
  403. for (var i = 0; i < lenStr.length; i++) {
  404. resultArray[bufferIndex++] = parseInt(lenStr[i]);
  405. }
  406. resultArray[bufferIndex++] = 255;
  407. var view = new Uint8Array(ab);
  408. for (var i = 0; i < view.length; i++) {
  409. resultArray[bufferIndex++] = view[i];
  410. }
  411. });
  412. return callback(resultArray.buffer);
  413. });
  414. };
  415. /**
  416. * Encode as Blob
  417. */
  418. exports.encodePayloadAsBlob = function(packets, callback) {
  419. function encodeOne(packet, doneCallback) {
  420. exports.encodePacket(packet, true, true, function(encoded) {
  421. var binaryIdentifier = new Uint8Array(1);
  422. binaryIdentifier[0] = 1;
  423. if (typeof encoded === 'string') {
  424. var view = new Uint8Array(encoded.length);
  425. for (var i = 0; i < encoded.length; i++) {
  426. view[i] = encoded.charCodeAt(i);
  427. }
  428. encoded = view.buffer;
  429. binaryIdentifier[0] = 0;
  430. }
  431. var len = (encoded instanceof ArrayBuffer)
  432. ? encoded.byteLength
  433. : encoded.size;
  434. var lenStr = len.toString();
  435. var lengthAry = new Uint8Array(lenStr.length + 1);
  436. for (var i = 0; i < lenStr.length; i++) {
  437. lengthAry[i] = parseInt(lenStr[i]);
  438. }
  439. lengthAry[lenStr.length] = 255;
  440. if (Blob) {
  441. var blob = new Blob([binaryIdentifier.buffer, lengthAry.buffer, encoded]);
  442. doneCallback(null, blob);
  443. }
  444. });
  445. }
  446. map(packets, encodeOne, function(err, results) {
  447. return callback(new Blob(results));
  448. });
  449. };
  450. /*
  451. * Decodes data when a payload is maybe expected. Strings are decoded by
  452. * interpreting each byte as a key code for entries marked to start with 0. See
  453. * description of encodePayloadAsBinary
  454. *
  455. * @param {ArrayBuffer} data, callback method
  456. * @api public
  457. */
  458. exports.decodePayloadAsBinary = function (data, binaryType, callback) {
  459. if (typeof binaryType === 'function') {
  460. callback = binaryType;
  461. binaryType = null;
  462. }
  463. var bufferTail = data;
  464. var buffers = [];
  465. var numberTooLong = false;
  466. while (bufferTail.byteLength > 0) {
  467. var tailArray = new Uint8Array(bufferTail);
  468. var isString = tailArray[0] === 0;
  469. var msgLength = '';
  470. for (var i = 1; ; i++) {
  471. if (tailArray[i] == 255) break;
  472. if (msgLength.length > 310) {
  473. numberTooLong = true;
  474. break;
  475. }
  476. msgLength += tailArray[i];
  477. }
  478. if(numberTooLong) return callback(err, 0, 1);
  479. bufferTail = sliceBuffer(bufferTail, 2 + msgLength.length);
  480. msgLength = parseInt(msgLength);
  481. var msg = sliceBuffer(bufferTail, 0, msgLength);
  482. if (isString) {
  483. try {
  484. msg = String.fromCharCode.apply(null, new Uint8Array(msg));
  485. } catch (e) {
  486. // iPhone Safari doesn't let you apply to typed arrays
  487. var typed = new Uint8Array(msg);
  488. msg = '';
  489. for (var i = 0; i < typed.length; i++) {
  490. msg += String.fromCharCode(typed[i]);
  491. }
  492. }
  493. }
  494. buffers.push(msg);
  495. bufferTail = sliceBuffer(bufferTail, msgLength);
  496. }
  497. var total = buffers.length;
  498. buffers.forEach(function(buffer, i) {
  499. callback(exports.decodePacket(buffer, binaryType, true), i, total);
  500. });
  501. };