browser.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  1. /**
  2. * Module dependencies.
  3. */
  4. var keys = require('./keys');
  5. var hasBinary = require('has-binary2');
  6. var sliceBuffer = require('arraybuffer.slice');
  7. var after = require('after');
  8. var utf8 = require('./utf8');
  9. var base64encoder;
  10. if (typeof ArrayBuffer !== 'undefined') {
  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 (typeof supportsBinary === 'function') {
  74. callback = supportsBinary;
  75. supportsBinary = false;
  76. }
  77. if (typeof utf8encode === 'function') {
  78. callback = utf8encode;
  79. utf8encode = null;
  80. }
  81. var data = (packet.data === undefined)
  82. ? undefined
  83. : packet.data.buffer || packet.data;
  84. if (typeof ArrayBuffer !== 'undefined' && data instanceof ArrayBuffer) {
  85. return encodeArrayBuffer(packet, supportsBinary, callback);
  86. } else if (typeof Blob !== 'undefined' && data instanceof 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), { strict: false }) : 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. exports.encodePacket({ type: packet.type, data: fr.result }, supportsBinary, true, callback);
  129. };
  130. return fr.readAsArrayBuffer(packet.data);
  131. }
  132. function encodeBlob(packet, supportsBinary, callback) {
  133. if (!supportsBinary) {
  134. return exports.encodeBase64Packet(packet, callback);
  135. }
  136. if (dontSendBlobs) {
  137. return encodeBlobAsArrayBuffer(packet, supportsBinary, callback);
  138. }
  139. var length = new Uint8Array(1);
  140. length[0] = packets[packet.type];
  141. var blob = new Blob([length.buffer, packet.data]);
  142. return callback(blob);
  143. }
  144. /**
  145. * Encodes a packet with binary data in a base64 string
  146. *
  147. * @param {Object} packet, has `type` and `data`
  148. * @return {String} base64 encoded message
  149. */
  150. exports.encodeBase64Packet = function(packet, callback) {
  151. var message = 'b' + exports.packets[packet.type];
  152. if (typeof Blob !== 'undefined' && packet.data instanceof Blob) {
  153. var fr = new FileReader();
  154. fr.onload = function() {
  155. var b64 = fr.result.split(',')[1];
  156. callback(message + b64);
  157. };
  158. return fr.readAsDataURL(packet.data);
  159. }
  160. var b64data;
  161. try {
  162. b64data = String.fromCharCode.apply(null, new Uint8Array(packet.data));
  163. } catch (e) {
  164. // iPhone Safari doesn't let you apply with typed arrays
  165. var typed = new Uint8Array(packet.data);
  166. var basic = new Array(typed.length);
  167. for (var i = 0; i < typed.length; i++) {
  168. basic[i] = typed[i];
  169. }
  170. b64data = String.fromCharCode.apply(null, basic);
  171. }
  172. message += btoa(b64data);
  173. return callback(message);
  174. };
  175. /**
  176. * Decodes a packet. Changes format to Blob if requested.
  177. *
  178. * @return {Object} with `type` and `data` (if any)
  179. * @api private
  180. */
  181. exports.decodePacket = function (data, binaryType, utf8decode) {
  182. if (data === undefined) {
  183. return err;
  184. }
  185. // String data
  186. if (typeof data === 'string') {
  187. if (data.charAt(0) === 'b') {
  188. return exports.decodeBase64Packet(data.substr(1), binaryType);
  189. }
  190. if (utf8decode) {
  191. data = tryDecode(data);
  192. if (data === false) {
  193. return err;
  194. }
  195. }
  196. var type = data.charAt(0);
  197. if (Number(type) != type || !packetslist[type]) {
  198. return err;
  199. }
  200. if (data.length > 1) {
  201. return { type: packetslist[type], data: data.substring(1) };
  202. } else {
  203. return { type: packetslist[type] };
  204. }
  205. }
  206. var asArray = new Uint8Array(data);
  207. var type = asArray[0];
  208. var rest = sliceBuffer(data, 1);
  209. if (Blob && binaryType === 'blob') {
  210. rest = new Blob([rest]);
  211. }
  212. return { type: packetslist[type], data: rest };
  213. };
  214. function tryDecode(data) {
  215. try {
  216. data = utf8.decode(data, { strict: false });
  217. } catch (e) {
  218. return false;
  219. }
  220. return data;
  221. }
  222. /**
  223. * Decodes a packet encoded in a base64 string
  224. *
  225. * @param {String} base64 encoded message
  226. * @return {Object} with `type` and `data` (if any)
  227. */
  228. exports.decodeBase64Packet = function(msg, binaryType) {
  229. var type = packetslist[msg.charAt(0)];
  230. if (!base64encoder) {
  231. return { type: type, data: { base64: true, data: msg.substr(1) } };
  232. }
  233. var data = base64encoder.decode(msg.substr(1));
  234. if (binaryType === 'blob' && Blob) {
  235. data = new Blob([data]);
  236. }
  237. return { type: type, data: data };
  238. };
  239. /**
  240. * Encodes multiple messages (payload).
  241. *
  242. * <length>:data
  243. *
  244. * Example:
  245. *
  246. * 11:hello world2:hi
  247. *
  248. * If any contents are binary, they will be encoded as base64 strings. Base64
  249. * encoded strings are marked with a b before the length specifier
  250. *
  251. * @param {Array} packets
  252. * @api private
  253. */
  254. exports.encodePayload = function (packets, supportsBinary, callback) {
  255. if (typeof supportsBinary === 'function') {
  256. callback = supportsBinary;
  257. supportsBinary = null;
  258. }
  259. var isBinary = hasBinary(packets);
  260. if (supportsBinary && isBinary) {
  261. if (Blob && !dontSendBlobs) {
  262. return exports.encodePayloadAsBlob(packets, callback);
  263. }
  264. return exports.encodePayloadAsArrayBuffer(packets, callback);
  265. }
  266. if (!packets.length) {
  267. return callback('0:');
  268. }
  269. function setLengthHeader(message) {
  270. return message.length + ':' + message;
  271. }
  272. function encodeOne(packet, doneCallback) {
  273. exports.encodePacket(packet, !isBinary ? false : supportsBinary, false, function(message) {
  274. doneCallback(null, setLengthHeader(message));
  275. });
  276. }
  277. map(packets, encodeOne, function(err, results) {
  278. return callback(results.join(''));
  279. });
  280. };
  281. /**
  282. * Async array map using after
  283. */
  284. function map(ary, each, done) {
  285. var result = new Array(ary.length);
  286. var next = after(ary.length, done);
  287. var eachWithIndex = function(i, el, cb) {
  288. each(el, function(error, msg) {
  289. result[i] = msg;
  290. cb(error, result);
  291. });
  292. };
  293. for (var i = 0; i < ary.length; i++) {
  294. eachWithIndex(i, ary[i], next);
  295. }
  296. }
  297. /*
  298. * Decodes data when a payload is maybe expected. Possible binary contents are
  299. * decoded from their base64 representation
  300. *
  301. * @param {String} data, callback method
  302. * @api public
  303. */
  304. exports.decodePayload = function (data, binaryType, callback) {
  305. if (typeof data !== 'string') {
  306. return exports.decodePayloadAsBinary(data, binaryType, callback);
  307. }
  308. if (typeof binaryType === 'function') {
  309. callback = binaryType;
  310. binaryType = null;
  311. }
  312. var packet;
  313. if (data === '') {
  314. // parser error - ignoring payload
  315. return callback(err, 0, 1);
  316. }
  317. var length = '', n, msg;
  318. for (var i = 0, l = data.length; i < l; i++) {
  319. var chr = data.charAt(i);
  320. if (chr !== ':') {
  321. length += chr;
  322. continue;
  323. }
  324. if (length === '' || (length != (n = Number(length)))) {
  325. // parser error - ignoring payload
  326. return callback(err, 0, 1);
  327. }
  328. msg = data.substr(i + 1, n);
  329. if (length != msg.length) {
  330. // parser error - ignoring payload
  331. return callback(err, 0, 1);
  332. }
  333. if (msg.length) {
  334. packet = exports.decodePacket(msg, binaryType, false);
  335. if (err.type === packet.type && err.data === packet.data) {
  336. // parser error in individual packet - ignoring payload
  337. return callback(err, 0, 1);
  338. }
  339. var ret = callback(packet, i + n, l);
  340. if (false === ret) return;
  341. }
  342. // advance cursor
  343. i += n;
  344. length = '';
  345. }
  346. if (length !== '') {
  347. // parser error - ignoring payload
  348. return callback(err, 0, 1);
  349. }
  350. };
  351. /**
  352. * Encodes multiple messages (payload) as binary.
  353. *
  354. * <1 = binary, 0 = string><number from 0-9><number from 0-9>[...]<number
  355. * 255><data>
  356. *
  357. * Example:
  358. * 1 3 255 1 2 3, if the binary contents are interpreted as 8 bit integers
  359. *
  360. * @param {Array} packets
  361. * @return {ArrayBuffer} encoded payload
  362. * @api private
  363. */
  364. exports.encodePayloadAsArrayBuffer = function(packets, callback) {
  365. if (!packets.length) {
  366. return callback(new ArrayBuffer(0));
  367. }
  368. function encodeOne(packet, doneCallback) {
  369. exports.encodePacket(packet, true, true, function(data) {
  370. return doneCallback(null, data);
  371. });
  372. }
  373. map(packets, encodeOne, function(err, encodedPackets) {
  374. var totalLength = encodedPackets.reduce(function(acc, p) {
  375. var len;
  376. if (typeof p === 'string'){
  377. len = p.length;
  378. } else {
  379. len = p.byteLength;
  380. }
  381. return acc + len.toString().length + len + 2; // string/binary identifier + separator = 2
  382. }, 0);
  383. var resultArray = new Uint8Array(totalLength);
  384. var bufferIndex = 0;
  385. encodedPackets.forEach(function(p) {
  386. var isString = typeof p === 'string';
  387. var ab = p;
  388. if (isString) {
  389. var view = new Uint8Array(p.length);
  390. for (var i = 0; i < p.length; i++) {
  391. view[i] = p.charCodeAt(i);
  392. }
  393. ab = view.buffer;
  394. }
  395. if (isString) { // not true binary
  396. resultArray[bufferIndex++] = 0;
  397. } else { // true binary
  398. resultArray[bufferIndex++] = 1;
  399. }
  400. var lenStr = ab.byteLength.toString();
  401. for (var i = 0; i < lenStr.length; i++) {
  402. resultArray[bufferIndex++] = parseInt(lenStr[i]);
  403. }
  404. resultArray[bufferIndex++] = 255;
  405. var view = new Uint8Array(ab);
  406. for (var i = 0; i < view.length; i++) {
  407. resultArray[bufferIndex++] = view[i];
  408. }
  409. });
  410. return callback(resultArray.buffer);
  411. });
  412. };
  413. /**
  414. * Encode as Blob
  415. */
  416. exports.encodePayloadAsBlob = function(packets, callback) {
  417. function encodeOne(packet, doneCallback) {
  418. exports.encodePacket(packet, true, true, function(encoded) {
  419. var binaryIdentifier = new Uint8Array(1);
  420. binaryIdentifier[0] = 1;
  421. if (typeof encoded === 'string') {
  422. var view = new Uint8Array(encoded.length);
  423. for (var i = 0; i < encoded.length; i++) {
  424. view[i] = encoded.charCodeAt(i);
  425. }
  426. encoded = view.buffer;
  427. binaryIdentifier[0] = 0;
  428. }
  429. var len = (encoded instanceof ArrayBuffer)
  430. ? encoded.byteLength
  431. : encoded.size;
  432. var lenStr = len.toString();
  433. var lengthAry = new Uint8Array(lenStr.length + 1);
  434. for (var i = 0; i < lenStr.length; i++) {
  435. lengthAry[i] = parseInt(lenStr[i]);
  436. }
  437. lengthAry[lenStr.length] = 255;
  438. if (Blob) {
  439. var blob = new Blob([binaryIdentifier.buffer, lengthAry.buffer, encoded]);
  440. doneCallback(null, blob);
  441. }
  442. });
  443. }
  444. map(packets, encodeOne, function(err, results) {
  445. return callback(new Blob(results));
  446. });
  447. };
  448. /*
  449. * Decodes data when a payload is maybe expected. Strings are decoded by
  450. * interpreting each byte as a key code for entries marked to start with 0. See
  451. * description of encodePayloadAsBinary
  452. *
  453. * @param {ArrayBuffer} data, callback method
  454. * @api public
  455. */
  456. exports.decodePayloadAsBinary = function (data, binaryType, callback) {
  457. if (typeof binaryType === 'function') {
  458. callback = binaryType;
  459. binaryType = null;
  460. }
  461. var bufferTail = data;
  462. var buffers = [];
  463. while (bufferTail.byteLength > 0) {
  464. var tailArray = new Uint8Array(bufferTail);
  465. var isString = tailArray[0] === 0;
  466. var msgLength = '';
  467. for (var i = 1; ; i++) {
  468. if (tailArray[i] === 255) break;
  469. // 310 = char length of Number.MAX_VALUE
  470. if (msgLength.length > 310) {
  471. return callback(err, 0, 1);
  472. }
  473. msgLength += tailArray[i];
  474. }
  475. bufferTail = sliceBuffer(bufferTail, 2 + msgLength.length);
  476. msgLength = parseInt(msgLength);
  477. var msg = sliceBuffer(bufferTail, 0, msgLength);
  478. if (isString) {
  479. try {
  480. msg = String.fromCharCode.apply(null, new Uint8Array(msg));
  481. } catch (e) {
  482. // iPhone Safari doesn't let you apply to typed arrays
  483. var typed = new Uint8Array(msg);
  484. msg = '';
  485. for (var i = 0; i < typed.length; i++) {
  486. msg += String.fromCharCode(typed[i]);
  487. }
  488. }
  489. }
  490. buffers.push(msg);
  491. bufferTail = sliceBuffer(bufferTail, msgLength);
  492. }
  493. var total = buffers.length;
  494. buffers.forEach(function(buffer, i) {
  495. callback(exports.decodePacket(buffer, binaryType, true), i, total);
  496. });
  497. };