// imported from https://github.com/socketio/engine.io-parser/tree/2.2.x /** * Module dependencies. */ var utf8 = require('./utf8') /** * Current protocol version. */ export const protocol = 3 const hasBinary = (packets) => { for (const packet of packets) { if (packet.data instanceof ArrayBuffer || ArrayBuffer.isView(packet.data)) { return true } } return false } /** * Packet types. */ export const packets = { open: 0 // non-ws , close: 1 // non-ws , ping: 2 , pong: 3 , message: 4 , upgrade: 5 , noop: 6 } var packetslist = Object.keys(packets) /** * Premade error packet. */ var err = { type: 'error', data: 'parser error' } const EMPTY_BUFFER = Buffer.concat([]) /** * Encodes a packet. * * [ ] * * Example: * * 5hello world * 3 * 4 * * Binary is encoded in an identical principle * * @api private */ export function encodePacket(packet, supportsBinary, utf8encode, callback) { if (typeof supportsBinary === 'function') { callback = supportsBinary supportsBinary = null } if (typeof utf8encode === 'function') { callback = utf8encode utf8encode = null } if (Buffer.isBuffer(packet.data)) { return encodeBuffer(packet, supportsBinary, callback) } else if (packet.data && (packet.data.buffer || packet.data) instanceof ArrayBuffer) { return encodeBuffer({ type: packet.type, data: arrayBufferToBuffer(packet.data) }, supportsBinary, 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) }; /** * Encode Buffer data */ function encodeBuffer(packet, supportsBinary, callback) { if (!supportsBinary) { return encodeBase64Packet(packet, callback) } var data = packet.data var typeBuffer = Buffer.allocUnsafe(1) typeBuffer[0] = packets[packet.type] return callback(Buffer.concat([typeBuffer, data])) } /** * Encodes a packet with binary data in a base64 string * * @param {Object} packet, has `type` and `data` * @return {String} base64 encoded message */ export function encodeBase64Packet(packet, callback) { var data = Buffer.isBuffer(packet.data) ? packet.data : arrayBufferToBuffer(packet.data) var message = 'b' + packets[packet.type] message += data.toString('base64') return callback(message) }; /** * Decodes a packet. Data also available as an ArrayBuffer if requested. * * @return {Object} with `type` and `data` (if any) * @api private */ export function decodePacket(data, binaryType, utf8decode) { if (data === undefined) { return err } var type // String data if (typeof data === 'string') { type = data.charAt(0) if (type === 'b') { return decodeBase64Packet(data.slice(1), binaryType) } if (utf8decode) { data = tryDecode(data) if (data === false) { return err } } if (Number(type) != type || !packetslist[type]) { return err } if (data.length > 1) { return { type: packetslist[type], data: data.slice(1) } } else { return { type: packetslist[type] } } } // Binary data if (binaryType === 'arraybuffer') { // wrap Buffer/ArrayBuffer data into an Uint8Array var intArray = new Uint8Array(data) type = intArray[0] return { type: packetslist[type], data: intArray.buffer.slice(1) } } if (data instanceof ArrayBuffer) { data = arrayBufferToBuffer(data) } type = data[0] return { type: packetslist[type], data: data.slice(1) } }; 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) */ export function decodeBase64Packet(msg, binaryType) { var type = packetslist[msg.charAt(0)] var data = Buffer.from(msg.slice(1), 'base64') if (binaryType === 'arraybuffer') { var abv = new Uint8Array(data.length) for (var i = 0; i < abv.length; i++) { abv[i] = data[i] } // @ts-ignore data = abv.buffer } return { type: type, data: data } }; /** * Encodes multiple messages (payload). * * :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 */ export function encodePayload(packets, supportsBinary, callback) { if (typeof supportsBinary === 'function') { callback = supportsBinary supportsBinary = null } if (supportsBinary && hasBinary(packets)) { return encodePayloadAsBinary(packets, callback) } if (!packets.length) { return callback('0:') } function encodeOne(packet, doneCallback) { encodePacket(packet, supportsBinary, false, function (message) { doneCallback(null, setLengthHeader(message)) }) } map(packets, encodeOne, function (err, results) { return callback(results.join('')) }) }; function setLengthHeader(message) { return message.length + ':' + message } /** * Async array map using after */ function map(ary, each, done) { const results = new Array(ary.length) let count = 0 for (let i = 0; i < ary.length; i++) { each(ary[i], (error, msg) => { results[i] = msg if (++count === ary.length) { done(null, results) } }) } } /* * Decodes data when a payload is maybe expected. Possible binary contents are * decoded from their base64 representation * * @param {String} data, callback method * @api public */ export function decodePayload(data, binaryType, callback) { if (typeof data !== 'string') { return decodePayloadAsBinary(data, binaryType, callback) } if (typeof binaryType === 'function') { callback = binaryType binaryType = null } if (data === '') { // parser error - ignoring payload return callback(err, 0, 1) } var length = '', n, msg, packet for (var i = 0, l = data.length; i < l; i++) { var chr = data.charAt(i) if (chr !== ':') { length += chr continue } // @ts-ignore if (length === '' || (length != (n = Number(length)))) { // parser error - ignoring payload return callback(err, 0, 1) } msg = data.slice(i + 1, i + 1 + n) if (length != msg.length) { // parser error - ignoring payload return callback(err, 0, 1) } if (msg.length) { packet = 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 more = callback(packet, i + n, l) if (false === more) return } // advance cursor i += n length = '' } if (length !== '') { // parser error - ignoring payload return callback(err, 0, 1) } }; /** * * Converts a buffer to a utf8.js encoded string * * @api private */ function bufferToString(buffer) { var str = '' for (var i = 0, l = buffer.length; i < l; i++) { str += String.fromCharCode(buffer[i]) } return str } /** * * Converts a utf8.js encoded string to a buffer * * @api private */ function stringToBuffer(string) { var buf = Buffer.allocUnsafe(string.length) for (var i = 0, l = string.length; i < l; i++) { buf.writeUInt8(string.charCodeAt(i), i) } return buf } /** * * Converts an ArrayBuffer to a Buffer * * @api private */ function arrayBufferToBuffer(data) { // data is either an ArrayBuffer or ArrayBufferView. var length = data.byteLength || data.length var offset = data.byteOffset || 0 return Buffer.from(data.buffer || data, offset, length) } /** * Encodes multiple messages (payload) as binary. * * <1 = binary, 0 = string>[...] * * Example: * 1 3 255 1 2 3, if the binary contents are interpreted as 8 bit integers * * @param {Array} packets * @return {Buffer} encoded payload * @api private */ export function encodePayloadAsBinary(packets, callback) { if (!packets.length) { return callback(EMPTY_BUFFER) } map(packets, encodeOneBinaryPacket, function (err, results) { return callback(Buffer.concat(results)) }) }; function encodeOneBinaryPacket(p, doneCallback) { function onBinaryPacketEncode(packet) { var encodingLength = '' + packet.length var sizeBuffer if (typeof packet === 'string') { sizeBuffer = Buffer.allocUnsafe(encodingLength.length + 2) sizeBuffer[0] = 0 // is a string (not true binary = 0) for (var i = 0; i < encodingLength.length; i++) { sizeBuffer[i + 1] = parseInt(encodingLength[i], 10) } sizeBuffer[sizeBuffer.length - 1] = 255 return doneCallback(null, Buffer.concat([sizeBuffer, stringToBuffer(packet)])) } sizeBuffer = Buffer.allocUnsafe(encodingLength.length + 2) sizeBuffer[0] = 1 // is binary (true binary = 1) for (var i = 0; i < encodingLength.length; i++) { sizeBuffer[i + 1] = parseInt(encodingLength[i], 10) } sizeBuffer[sizeBuffer.length - 1] = 255 doneCallback(null, Buffer.concat([sizeBuffer, packet])) } encodePacket(p, true, true, onBinaryPacketEncode) } /* * 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 {Buffer} data, callback method * @api public */ export function decodePayloadAsBinary(data, binaryType, callback) { if (typeof binaryType === 'function') { callback = binaryType binaryType = null } var bufferTail = data var buffers = [] var i while (bufferTail.length > 0) { var strLen = '' var isString = bufferTail[0] === 0 for (i = 1; ; i++) { if (bufferTail[i] === 255) break // 310 = char length of Number.MAX_VALUE if (strLen.length > 310) { return callback(err, 0, 1) } strLen += '' + bufferTail[i] } bufferTail = bufferTail.slice(strLen.length + 1) var msgLength = parseInt(strLen, 10) var msg = bufferTail.slice(1, msgLength + 1) if (isString) msg = bufferToString(msg) buffers.push(msg) bufferTail = bufferTail.slice(msgLength + 1) } var total = buffers.length for (i = 0; i < total; i++) { var buffer = buffers[i] callback(decodePacket(buffer, binaryType, true), i, total) } }