| /** | |
| * 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); | |
| }); | |
| }; |