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.

467 lines
11 KiB

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