index.js (10823B)
1 /** 2 * Module dependencies. 3 */ 4 5 var utf8 = require('./utf8'); 6 var hasBinary = require('has-binary2'); 7 var after = require('after'); 8 var keys = require('./keys'); 9 10 /** 11 * Current protocol version. 12 */ 13 exports.protocol = 3; 14 15 /** 16 * Packet types. 17 */ 18 19 var packets = exports.packets = { 20 open: 0 // non-ws 21 , close: 1 // non-ws 22 , ping: 2 23 , pong: 3 24 , message: 4 25 , upgrade: 5 26 , noop: 6 27 }; 28 29 var packetslist = keys(packets); 30 31 /** 32 * Premade error packet. 33 */ 34 35 var err = { type: 'error', data: 'parser error' }; 36 37 const EMPTY_BUFFER = Buffer.concat([]); 38 39 /** 40 * Encodes a packet. 41 * 42 * <packet type id> [ <data> ] 43 * 44 * Example: 45 * 46 * 5hello world 47 * 3 48 * 4 49 * 50 * Binary is encoded in an identical principle 51 * 52 * @api private 53 */ 54 55 exports.encodePacket = function (packet, supportsBinary, utf8encode, callback) { 56 if (typeof supportsBinary === 'function') { 57 callback = supportsBinary; 58 supportsBinary = null; 59 } 60 61 if (typeof utf8encode === 'function') { 62 callback = utf8encode; 63 utf8encode = null; 64 } 65 66 if (Buffer.isBuffer(packet.data)) { 67 return encodeBuffer(packet, supportsBinary, callback); 68 } else if (packet.data && (packet.data.buffer || packet.data) instanceof ArrayBuffer) { 69 return encodeBuffer({ type: packet.type, data: arrayBufferToBuffer(packet.data) }, supportsBinary, callback); 70 } 71 72 // Sending data as a utf-8 string 73 var encoded = packets[packet.type]; 74 75 // data fragment is optional 76 if (undefined !== packet.data) { 77 encoded += utf8encode ? utf8.encode(String(packet.data), { strict: false }) : String(packet.data); 78 } 79 80 return callback('' + encoded); 81 }; 82 83 /** 84 * Encode Buffer data 85 */ 86 87 function encodeBuffer(packet, supportsBinary, callback) { 88 if (!supportsBinary) { 89 return exports.encodeBase64Packet(packet, callback); 90 } 91 92 var data = packet.data; 93 var typeBuffer = Buffer.allocUnsafe(1); 94 typeBuffer[0] = packets[packet.type]; 95 return callback(Buffer.concat([typeBuffer, data])); 96 } 97 98 /** 99 * Encodes a packet with binary data in a base64 string 100 * 101 * @param {Object} packet, has `type` and `data` 102 * @return {String} base64 encoded message 103 */ 104 105 exports.encodeBase64Packet = function(packet, callback){ 106 var data = Buffer.isBuffer(packet.data) ? packet.data : arrayBufferToBuffer(packet.data); 107 var message = 'b' + packets[packet.type]; 108 message += data.toString('base64'); 109 return callback(message); 110 }; 111 112 /** 113 * Decodes a packet. Data also available as an ArrayBuffer if requested. 114 * 115 * @return {Object} with `type` and `data` (if any) 116 * @api private 117 */ 118 119 exports.decodePacket = function (data, binaryType, utf8decode) { 120 if (data === undefined) { 121 return err; 122 } 123 124 var type; 125 126 // String data 127 if (typeof data === 'string') { 128 129 type = data.charAt(0); 130 131 if (type === 'b') { 132 return exports.decodeBase64Packet(data.substr(1), binaryType); 133 } 134 135 if (utf8decode) { 136 data = tryDecode(data); 137 if (data === false) { 138 return err; 139 } 140 } 141 142 if (Number(type) != type || !packetslist[type]) { 143 return err; 144 } 145 146 if (data.length > 1) { 147 return { type: packetslist[type], data: data.substring(1) }; 148 } else { 149 return { type: packetslist[type] }; 150 } 151 } 152 153 // Binary data 154 if (binaryType === 'arraybuffer') { 155 // wrap Buffer/ArrayBuffer data into an Uint8Array 156 var intArray = new Uint8Array(data); 157 type = intArray[0]; 158 return { type: packetslist[type], data: intArray.buffer.slice(1) }; 159 } 160 161 if (data instanceof ArrayBuffer) { 162 data = arrayBufferToBuffer(data); 163 } 164 type = data[0]; 165 return { type: packetslist[type], data: data.slice(1) }; 166 }; 167 168 function tryDecode(data) { 169 try { 170 data = utf8.decode(data, { strict: false }); 171 } catch (e) { 172 return false; 173 } 174 return data; 175 } 176 177 /** 178 * Decodes a packet encoded in a base64 string. 179 * 180 * @param {String} base64 encoded message 181 * @return {Object} with `type` and `data` (if any) 182 */ 183 184 exports.decodeBase64Packet = function(msg, binaryType) { 185 var type = packetslist[msg.charAt(0)]; 186 var data = Buffer.from(msg.substr(1), 'base64'); 187 if (binaryType === 'arraybuffer') { 188 var abv = new Uint8Array(data.length); 189 for (var i = 0; i < abv.length; i++){ 190 abv[i] = data[i]; 191 } 192 data = abv.buffer; 193 } 194 return { type: type, data: data }; 195 }; 196 197 /** 198 * Encodes multiple messages (payload). 199 * 200 * <length>:data 201 * 202 * Example: 203 * 204 * 11:hello world2:hi 205 * 206 * If any contents are binary, they will be encoded as base64 strings. Base64 207 * encoded strings are marked with a b before the length specifier 208 * 209 * @param {Array} packets 210 * @api private 211 */ 212 213 exports.encodePayload = function (packets, supportsBinary, callback) { 214 if (typeof supportsBinary === 'function') { 215 callback = supportsBinary; 216 supportsBinary = null; 217 } 218 219 if (supportsBinary && hasBinary(packets)) { 220 return exports.encodePayloadAsBinary(packets, callback); 221 } 222 223 if (!packets.length) { 224 return callback('0:'); 225 } 226 227 function encodeOne(packet, doneCallback) { 228 exports.encodePacket(packet, supportsBinary, false, function(message) { 229 doneCallback(null, setLengthHeader(message)); 230 }); 231 } 232 233 map(packets, encodeOne, function(err, results) { 234 return callback(results.join('')); 235 }); 236 }; 237 238 function setLengthHeader(message) { 239 return message.length + ':' + message; 240 } 241 242 /** 243 * Async array map using after 244 */ 245 246 function map(ary, each, done) { 247 var result = new Array(ary.length); 248 var next = after(ary.length, done); 249 250 for (var i = 0; i < ary.length; i++) { 251 each(ary[i], function(error, msg) { 252 result[i] = msg; 253 next(error, result); 254 }); 255 } 256 } 257 258 /* 259 * Decodes data when a payload is maybe expected. Possible binary contents are 260 * decoded from their base64 representation 261 * 262 * @param {String} data, callback method 263 * @api public 264 */ 265 266 exports.decodePayload = function (data, binaryType, callback) { 267 if (typeof data !== 'string') { 268 return exports.decodePayloadAsBinary(data, binaryType, callback); 269 } 270 271 if (typeof binaryType === 'function') { 272 callback = binaryType; 273 binaryType = null; 274 } 275 276 if (data === '') { 277 // parser error - ignoring payload 278 return callback(err, 0, 1); 279 } 280 281 var length = '', n, msg, packet; 282 283 for (var i = 0, l = data.length; i < l; i++) { 284 var chr = data.charAt(i); 285 286 if (chr !== ':') { 287 length += chr; 288 continue; 289 } 290 291 if (length === '' || (length != (n = Number(length)))) { 292 // parser error - ignoring payload 293 return callback(err, 0, 1); 294 } 295 296 msg = data.substr(i + 1, n); 297 298 if (length != msg.length) { 299 // parser error - ignoring payload 300 return callback(err, 0, 1); 301 } 302 303 if (msg.length) { 304 packet = exports.decodePacket(msg, binaryType, false); 305 306 if (err.type === packet.type && err.data === packet.data) { 307 // parser error in individual packet - ignoring payload 308 return callback(err, 0, 1); 309 } 310 311 var more = callback(packet, i + n, l); 312 if (false === more) return; 313 } 314 315 // advance cursor 316 i += n; 317 length = ''; 318 } 319 320 if (length !== '') { 321 // parser error - ignoring payload 322 return callback(err, 0, 1); 323 } 324 325 }; 326 327 /** 328 * 329 * Converts a buffer to a utf8.js encoded string 330 * 331 * @api private 332 */ 333 334 function bufferToString(buffer) { 335 var str = ''; 336 for (var i = 0, l = buffer.length; i < l; i++) { 337 str += String.fromCharCode(buffer[i]); 338 } 339 return str; 340 } 341 342 /** 343 * 344 * Converts a utf8.js encoded string to a buffer 345 * 346 * @api private 347 */ 348 349 function stringToBuffer(string) { 350 var buf = Buffer.allocUnsafe(string.length); 351 for (var i = 0, l = string.length; i < l; i++) { 352 buf.writeUInt8(string.charCodeAt(i), i); 353 } 354 return buf; 355 } 356 357 /** 358 * 359 * Converts an ArrayBuffer to a Buffer 360 * 361 * @api private 362 */ 363 364 function arrayBufferToBuffer(data) { 365 // data is either an ArrayBuffer or ArrayBufferView. 366 var length = data.byteLength || data.length; 367 var offset = data.byteOffset || 0; 368 369 return Buffer.from(data.buffer || data, offset, length); 370 } 371 372 /** 373 * Encodes multiple messages (payload) as binary. 374 * 375 * <1 = binary, 0 = string><number from 0-9><number from 0-9>[...]<number 376 * 255><data> 377 * 378 * Example: 379 * 1 3 255 1 2 3, if the binary contents are interpreted as 8 bit integers 380 * 381 * @param {Array} packets 382 * @return {Buffer} encoded payload 383 * @api private 384 */ 385 386 exports.encodePayloadAsBinary = function (packets, callback) { 387 if (!packets.length) { 388 return callback(EMPTY_BUFFER); 389 } 390 391 map(packets, encodeOneBinaryPacket, function(err, results) { 392 return callback(Buffer.concat(results)); 393 }); 394 }; 395 396 function encodeOneBinaryPacket(p, doneCallback) { 397 398 function onBinaryPacketEncode(packet) { 399 400 var encodingLength = '' + packet.length; 401 var sizeBuffer; 402 403 if (typeof packet === 'string') { 404 sizeBuffer = Buffer.allocUnsafe(encodingLength.length + 2); 405 sizeBuffer[0] = 0; // is a string (not true binary = 0) 406 for (var i = 0; i < encodingLength.length; i++) { 407 sizeBuffer[i + 1] = parseInt(encodingLength[i], 10); 408 } 409 sizeBuffer[sizeBuffer.length - 1] = 255; 410 return doneCallback(null, Buffer.concat([sizeBuffer, stringToBuffer(packet)])); 411 } 412 413 sizeBuffer = Buffer.allocUnsafe(encodingLength.length + 2); 414 sizeBuffer[0] = 1; // is binary (true binary = 1) 415 for (var i = 0; i < encodingLength.length; i++) { 416 sizeBuffer[i + 1] = parseInt(encodingLength[i], 10); 417 } 418 sizeBuffer[sizeBuffer.length - 1] = 255; 419 420 doneCallback(null, Buffer.concat([sizeBuffer, packet])); 421 } 422 423 exports.encodePacket(p, true, true, onBinaryPacketEncode); 424 425 } 426 427 428 /* 429 * Decodes data when a payload is maybe expected. Strings are decoded by 430 * interpreting each byte as a key code for entries marked to start with 0. See 431 * description of encodePayloadAsBinary 432 433 * @param {Buffer} data, callback method 434 * @api public 435 */ 436 437 exports.decodePayloadAsBinary = function (data, binaryType, callback) { 438 if (typeof binaryType === 'function') { 439 callback = binaryType; 440 binaryType = null; 441 } 442 443 var bufferTail = data; 444 var buffers = []; 445 var i; 446 447 while (bufferTail.length > 0) { 448 var strLen = ''; 449 var isString = bufferTail[0] === 0; 450 for (i = 1; ; i++) { 451 if (bufferTail[i] === 255) break; 452 // 310 = char length of Number.MAX_VALUE 453 if (strLen.length > 310) { 454 return callback(err, 0, 1); 455 } 456 strLen += '' + bufferTail[i]; 457 } 458 bufferTail = bufferTail.slice(strLen.length + 1); 459 460 var msgLength = parseInt(strLen, 10); 461 462 var msg = bufferTail.slice(1, msgLength + 1); 463 if (isString) msg = bufferToString(msg); 464 buffers.push(msg); 465 bufferTail = bufferTail.slice(msgLength + 1); 466 } 467 468 var total = buffers.length; 469 for (i = 0; i < total; i++) { 470 var buffer = buffers[i]; 471 callback(exports.decodePacket(buffer, binaryType, true), i, total); 472 } 473 };