browser.js 14 KB

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