|
|
/** * Module dependencies. */
var keys = require('./keys'); var hasBinary = require('has-binary2'); var sliceBuffer = require('arraybuffer.slice'); var after = require('after'); var utf8 = require('./utf8');
var base64encoder; if (typeof ArrayBuffer !== 'undefined') { base64encoder = require('base64-arraybuffer'); }
/** * Check if we are running an android browser. That requires us to use * ArrayBuffer with polling transports... * * http://ghinda.net/jpeg-blob-ajax-android/
*/
var isAndroid = typeof navigator !== 'undefined' && /Android/i.test(navigator.userAgent);
/** * Check if we are running in PhantomJS. * Uploading a Blob with PhantomJS does not work correctly, as reported here: * https://github.com/ariya/phantomjs/issues/11395
* @type boolean */ var isPhantomJS = typeof navigator !== 'undefined' && /PhantomJS/i.test(navigator.userAgent);
/** * When true, avoids using Blobs to encode payloads. * @type boolean */ var dontSendBlobs = isAndroid || isPhantomJS;
/** * Current protocol version. */
exports.protocol = 3;
/** * Packet types. */
var packets = exports.packets = { open: 0 // non-ws
, close: 1 // non-ws
, ping: 2 , pong: 3 , message: 4 , upgrade: 5 , noop: 6 };
var packetslist = keys(packets);
/** * Premade error packet. */
var err = { type: 'error', data: 'parser error' };
/** * Create a blob api even for blob builder when vendor prefixes exist */
var Blob = require('blob');
/** * Encodes a packet. * * <packet type id> [ <data> ] * * Example: * * 5hello world * 3 * 4 * * Binary is encoded in an identical principle * * @api private */
exports.encodePacket = function (packet, supportsBinary, utf8encode, callback) { if (typeof supportsBinary === 'function') { callback = supportsBinary; supportsBinary = false; }
if (typeof utf8encode === 'function') { callback = utf8encode; utf8encode = null; }
var data = (packet.data === undefined) ? undefined : packet.data.buffer || packet.data;
if (typeof ArrayBuffer !== 'undefined' && data instanceof ArrayBuffer) { return encodeArrayBuffer(packet, supportsBinary, callback); } else if (typeof Blob !== 'undefined' && data instanceof Blob) { return encodeBlob(packet, supportsBinary, callback); }
// might be an object with { base64: true, data: dataAsBase64String }
if (data && data.base64) { return encodeBase64Object(packet, callback); }
// Sending data as a utf-8 string
var encoded = packets[packet.type];
// data fragment is optional
if (undefined !== packet.data) { encoded += utf8encode ? utf8.encode(String(packet.data), { strict: false }) : String(packet.data); }
return callback('' + encoded);
};
function encodeBase64Object(packet, callback) { // packet data is an object { base64: true, data: dataAsBase64String }
var message = 'b' + exports.packets[packet.type] + packet.data.data; return callback(message); }
/** * Encode packet helpers for binary types */
function encodeArrayBuffer(packet, supportsBinary, callback) { if (!supportsBinary) { return exports.encodeBase64Packet(packet, callback); }
var data = packet.data; var contentArray = new Uint8Array(data); var resultBuffer = new Uint8Array(1 + data.byteLength);
resultBuffer[0] = packets[packet.type]; for (var i = 0; i < contentArray.length; i++) { resultBuffer[i+1] = contentArray[i]; }
return callback(resultBuffer.buffer); }
function encodeBlobAsArrayBuffer(packet, supportsBinary, callback) { if (!supportsBinary) { return exports.encodeBase64Packet(packet, callback); }
var fr = new FileReader(); fr.onload = function() { exports.encodePacket({ type: packet.type, data: fr.result }, supportsBinary, true, callback); }; return fr.readAsArrayBuffer(packet.data); }
function encodeBlob(packet, supportsBinary, callback) { if (!supportsBinary) { return exports.encodeBase64Packet(packet, callback); }
if (dontSendBlobs) { return encodeBlobAsArrayBuffer(packet, supportsBinary, callback); }
var length = new Uint8Array(1); length[0] = packets[packet.type]; var blob = new Blob([length.buffer, packet.data]);
return callback(blob); }
/** * Encodes a packet with binary data in a base64 string * * @param {Object} packet, has `type` and `data` * @return {String} base64 encoded message */
exports.encodeBase64Packet = function(packet, callback) { var message = 'b' + exports.packets[packet.type]; if (typeof Blob !== 'undefined' && packet.data instanceof Blob) { var fr = new FileReader(); fr.onload = function() { var b64 = fr.result.split(',')[1]; callback(message + b64); }; return fr.readAsDataURL(packet.data); }
var b64data; try { b64data = String.fromCharCode.apply(null, new Uint8Array(packet.data)); } catch (e) { // iPhone Safari doesn't let you apply with typed arrays
var typed = new Uint8Array(packet.data); var basic = new Array(typed.length); for (var i = 0; i < typed.length; i++) { basic[i] = typed[i]; } b64data = String.fromCharCode.apply(null, basic); } message += btoa(b64data); return callback(message); };
/** * Decodes a packet. Changes format to Blob if requested. * * @return {Object} with `type` and `data` (if any) * @api private */
exports.decodePacket = function (data, binaryType, utf8decode) { if (data === undefined) { return err; } // String data
if (typeof data === 'string') { if (data.charAt(0) === 'b') { return exports.decodeBase64Packet(data.substr(1), binaryType); }
if (utf8decode) { data = tryDecode(data); if (data === false) { return err; } } var type = data.charAt(0);
if (Number(type) != type || !packetslist[type]) { return err; }
if (data.length > 1) { return { type: packetslist[type], data: data.substring(1) }; } else { return { type: packetslist[type] }; } }
var asArray = new Uint8Array(data); var type = asArray[0]; var rest = sliceBuffer(data, 1); if (Blob && binaryType === 'blob') { rest = new Blob([rest]); } return { type: packetslist[type], data: rest }; };
function tryDecode(data) { try { data = utf8.decode(data, { strict: false }); } catch (e) { return false; } return data; }
/** * Decodes a packet encoded in a base64 string * * @param {String} base64 encoded message * @return {Object} with `type` and `data` (if any) */
exports.decodeBase64Packet = function(msg, binaryType) { var type = packetslist[msg.charAt(0)]; if (!base64encoder) { return { type: type, data: { base64: true, data: msg.substr(1) } }; }
var data = base64encoder.decode(msg.substr(1));
if (binaryType === 'blob' && Blob) { data = new Blob([data]); }
return { type: type, data: data }; };
/** * Encodes multiple messages (payload). * * <length>:data * * Example: * * 11:hello world2:hi * * If any contents are binary, they will be encoded as base64 strings. Base64 * encoded strings are marked with a b before the length specifier * * @param {Array} packets * @api private */
exports.encodePayload = function (packets, supportsBinary, callback) { if (typeof supportsBinary === 'function') { callback = supportsBinary; supportsBinary = null; }
var isBinary = hasBinary(packets);
if (supportsBinary && isBinary) { if (Blob && !dontSendBlobs) { return exports.encodePayloadAsBlob(packets, callback); }
return exports.encodePayloadAsArrayBuffer(packets, callback); }
if (!packets.length) { return callback('0:'); }
function setLengthHeader(message) { return message.length + ':' + message; }
function encodeOne(packet, doneCallback) { exports.encodePacket(packet, !isBinary ? false : supportsBinary, false, function(message) { doneCallback(null, setLengthHeader(message)); }); }
map(packets, encodeOne, function(err, results) { return callback(results.join('')); }); };
/** * Async array map using after */
function map(ary, each, done) { var result = new Array(ary.length); var next = after(ary.length, done);
var eachWithIndex = function(i, el, cb) { each(el, function(error, msg) { result[i] = msg; cb(error, result); }); };
for (var i = 0; i < ary.length; i++) { eachWithIndex(i, ary[i], next); } }
/* * Decodes data when a payload is maybe expected. Possible binary contents are * decoded from their base64 representation * * @param {String} data, callback method * @api public */
exports.decodePayload = function (data, binaryType, callback) { if (typeof data !== 'string') { return exports.decodePayloadAsBinary(data, binaryType, callback); }
if (typeof binaryType === 'function') { callback = binaryType; binaryType = null; }
var packet; if (data === '') { // parser error - ignoring payload
return callback(err, 0, 1); }
var length = '', n, msg;
for (var i = 0, l = data.length; i < l; i++) { var chr = data.charAt(i);
if (chr !== ':') { length += chr; continue; }
if (length === '' || (length != (n = Number(length)))) { // parser error - ignoring payload
return callback(err, 0, 1); }
msg = data.substr(i + 1, n);
if (length != msg.length) { // parser error - ignoring payload
return callback(err, 0, 1); }
if (msg.length) { packet = exports.decodePacket(msg, binaryType, false);
if (err.type === packet.type && err.data === packet.data) { // parser error in individual packet - ignoring payload
return callback(err, 0, 1); }
var ret = callback(packet, i + n, l); if (false === ret) return; }
// advance cursor
i += n; length = ''; }
if (length !== '') { // parser error - ignoring payload
return callback(err, 0, 1); }
};
/** * Encodes multiple messages (payload) as binary. * * <1 = binary, 0 = string><number from 0-9><number from 0-9>[...]<number * 255><data> * * Example: * 1 3 255 1 2 3, if the binary contents are interpreted as 8 bit integers * * @param {Array} packets * @return {ArrayBuffer} encoded payload * @api private */
exports.encodePayloadAsArrayBuffer = function(packets, callback) { if (!packets.length) { return callback(new ArrayBuffer(0)); }
function encodeOne(packet, doneCallback) { exports.encodePacket(packet, true, true, function(data) { return doneCallback(null, data); }); }
map(packets, encodeOne, function(err, encodedPackets) { var totalLength = encodedPackets.reduce(function(acc, p) { var len; if (typeof p === 'string'){ len = p.length; } else { len = p.byteLength; } return acc + len.toString().length + len + 2; // string/binary identifier + separator = 2
}, 0);
var resultArray = new Uint8Array(totalLength);
var bufferIndex = 0; encodedPackets.forEach(function(p) { var isString = typeof p === 'string'; var ab = p; if (isString) { var view = new Uint8Array(p.length); for (var i = 0; i < p.length; i++) { view[i] = p.charCodeAt(i); } ab = view.buffer; }
if (isString) { // not true binary
resultArray[bufferIndex++] = 0; } else { // true binary
resultArray[bufferIndex++] = 1; }
var lenStr = ab.byteLength.toString(); for (var i = 0; i < lenStr.length; i++) { resultArray[bufferIndex++] = parseInt(lenStr[i]); } resultArray[bufferIndex++] = 255;
var view = new Uint8Array(ab); for (var i = 0; i < view.length; i++) { resultArray[bufferIndex++] = view[i]; } });
return callback(resultArray.buffer); }); };
/** * Encode as Blob */
exports.encodePayloadAsBlob = function(packets, callback) { function encodeOne(packet, doneCallback) { exports.encodePacket(packet, true, true, function(encoded) { var binaryIdentifier = new Uint8Array(1); binaryIdentifier[0] = 1; if (typeof encoded === 'string') { var view = new Uint8Array(encoded.length); for (var i = 0; i < encoded.length; i++) { view[i] = encoded.charCodeAt(i); } encoded = view.buffer; binaryIdentifier[0] = 0; }
var len = (encoded instanceof ArrayBuffer) ? encoded.byteLength : encoded.size;
var lenStr = len.toString(); var lengthAry = new Uint8Array(lenStr.length + 1); for (var i = 0; i < lenStr.length; i++) { lengthAry[i] = parseInt(lenStr[i]); } lengthAry[lenStr.length] = 255;
if (Blob) { var blob = new Blob([binaryIdentifier.buffer, lengthAry.buffer, encoded]); doneCallback(null, blob); } }); }
map(packets, encodeOne, function(err, results) { return callback(new Blob(results)); }); };
/* * Decodes data when a payload is maybe expected. Strings are decoded by * interpreting each byte as a key code for entries marked to start with 0. See * description of encodePayloadAsBinary * * @param {ArrayBuffer} data, callback method * @api public */
exports.decodePayloadAsBinary = function (data, binaryType, callback) { if (typeof binaryType === 'function') { callback = binaryType; binaryType = null; }
var bufferTail = data; var buffers = [];
while (bufferTail.byteLength > 0) { var tailArray = new Uint8Array(bufferTail); var isString = tailArray[0] === 0; var msgLength = '';
for (var i = 1; ; i++) { if (tailArray[i] === 255) break;
// 310 = char length of Number.MAX_VALUE
if (msgLength.length > 310) { return callback(err, 0, 1); }
msgLength += tailArray[i]; }
bufferTail = sliceBuffer(bufferTail, 2 + msgLength.length); msgLength = parseInt(msgLength);
var msg = sliceBuffer(bufferTail, 0, msgLength); if (isString) { try { msg = String.fromCharCode.apply(null, new Uint8Array(msg)); } catch (e) { // iPhone Safari doesn't let you apply to typed arrays
var typed = new Uint8Array(msg); msg = ''; for (var i = 0; i < typed.length; i++) { msg += String.fromCharCode(typed[i]); } } }
buffers.push(msg); bufferTail = sliceBuffer(bufferTail, msgLength); }
var total = buffers.length; buffers.forEach(function(buffer, i) { callback(exports.decodePacket(buffer, binaryType, true), i, total); }); };
|