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.

566 lines
13 KiB

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