606 lines
14 KiB
JavaScript
606 lines
14 KiB
JavaScript
|
/**
|
||
|
* 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);
|
||
|
});
|
||
|
};
|