twitst4tz

twitter statistics web application
Log | Files | Refs | README | LICENSE

index.js (10823B)


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