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 };