twitst4tz

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

browser.js (14692B)


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