| |
| /** |
| * Module dependencies. |
| */ |
| |
| var debug = require('debug')('socket.io-parser'); |
| var Emitter = require('component-emitter'); |
| var binary = require('./binary'); |
| var isArray = require('isarray'); |
| var isBuf = require('./is-buffer'); |
| |
| /** |
| * Protocol version. |
| * |
| * @api public |
| */ |
| |
| exports.protocol = 4; |
| |
| /** |
| * Packet types. |
| * |
| * @api public |
| */ |
| |
| exports.types = [ |
| 'CONNECT', |
| 'DISCONNECT', |
| 'EVENT', |
| 'ACK', |
| 'ERROR', |
| 'BINARY_EVENT', |
| 'BINARY_ACK' |
| ]; |
| |
| /** |
| * Packet type `connect`. |
| * |
| * @api public |
| */ |
| |
| exports.CONNECT = 0; |
| |
| /** |
| * Packet type `disconnect`. |
| * |
| * @api public |
| */ |
| |
| exports.DISCONNECT = 1; |
| |
| /** |
| * Packet type `event`. |
| * |
| * @api public |
| */ |
| |
| exports.EVENT = 2; |
| |
| /** |
| * Packet type `ack`. |
| * |
| * @api public |
| */ |
| |
| exports.ACK = 3; |
| |
| /** |
| * Packet type `error`. |
| * |
| * @api public |
| */ |
| |
| exports.ERROR = 4; |
| |
| /** |
| * Packet type 'binary event' |
| * |
| * @api public |
| */ |
| |
| exports.BINARY_EVENT = 5; |
| |
| /** |
| * Packet type `binary ack`. For acks with binary arguments. |
| * |
| * @api public |
| */ |
| |
| exports.BINARY_ACK = 6; |
| |
| /** |
| * Encoder constructor. |
| * |
| * @api public |
| */ |
| |
| exports.Encoder = Encoder; |
| |
| /** |
| * Decoder constructor. |
| * |
| * @api public |
| */ |
| |
| exports.Decoder = Decoder; |
| |
| /** |
| * A socket.io Encoder instance |
| * |
| * @api public |
| */ |
| |
| function Encoder() {} |
| |
| var ERROR_PACKET = exports.ERROR + '"encode error"'; |
| |
| /** |
| * Encode a packet as a single string if non-binary, or as a |
| * buffer sequence, depending on packet type. |
| * |
| * @param {Object} obj - packet object |
| * @param {Function} callback - function to handle encodings (likely engine.write) |
| * @return Calls callback with Array of encodings |
| * @api public |
| */ |
| |
| Encoder.prototype.encode = function(obj, callback){ |
| debug('encoding packet %j', obj); |
| |
| if (exports.BINARY_EVENT === obj.type || exports.BINARY_ACK === obj.type) { |
| encodeAsBinary(obj, callback); |
| } else { |
| var encoding = encodeAsString(obj); |
| callback([encoding]); |
| } |
| }; |
| |
| /** |
| * Encode packet as string. |
| * |
| * @param {Object} packet |
| * @return {String} encoded |
| * @api private |
| */ |
| |
| function encodeAsString(obj) { |
| |
| // first is type |
| var str = '' + obj.type; |
| |
| // attachments if we have them |
| if (exports.BINARY_EVENT === obj.type || exports.BINARY_ACK === obj.type) { |
| str += obj.attachments + '-'; |
| } |
| |
| // if we have a namespace other than `/` |
| // we append it followed by a comma `,` |
| if (obj.nsp && '/' !== obj.nsp) { |
| str += obj.nsp + ','; |
| } |
| |
| // immediately followed by the id |
| if (null != obj.id) { |
| str += obj.id; |
| } |
| |
| // json data |
| if (null != obj.data) { |
| var payload = tryStringify(obj.data); |
| if (payload !== false) { |
| str += payload; |
| } else { |
| return ERROR_PACKET; |
| } |
| } |
| |
| debug('encoded %j as %s', obj, str); |
| return str; |
| } |
| |
| function tryStringify(str) { |
| try { |
| return JSON.stringify(str); |
| } catch(e){ |
| return false; |
| } |
| } |
| |
| /** |
| * Encode packet as 'buffer sequence' by removing blobs, and |
| * deconstructing packet into object with placeholders and |
| * a list of buffers. |
| * |
| * @param {Object} packet |
| * @return {Buffer} encoded |
| * @api private |
| */ |
| |
| function encodeAsBinary(obj, callback) { |
| |
| function writeEncoding(bloblessData) { |
| var deconstruction = binary.deconstructPacket(bloblessData); |
| var pack = encodeAsString(deconstruction.packet); |
| var buffers = deconstruction.buffers; |
| |
| buffers.unshift(pack); // add packet info to beginning of data list |
| callback(buffers); // write all the buffers |
| } |
| |
| binary.removeBlobs(obj, writeEncoding); |
| } |
| |
| /** |
| * A socket.io Decoder instance |
| * |
| * @return {Object} decoder |
| * @api public |
| */ |
| |
| function Decoder() { |
| this.reconstructor = null; |
| } |
| |
| /** |
| * Mix in `Emitter` with Decoder. |
| */ |
| |
| Emitter(Decoder.prototype); |
| |
| /** |
| * Decodes an ecoded packet string into packet JSON. |
| * |
| * @param {String} obj - encoded packet |
| * @return {Object} packet |
| * @api public |
| */ |
| |
| Decoder.prototype.add = function(obj) { |
| var packet; |
| if (typeof obj === 'string') { |
| packet = decodeString(obj); |
| if (exports.BINARY_EVENT === packet.type || exports.BINARY_ACK === packet.type) { // binary packet's json |
| this.reconstructor = new BinaryReconstructor(packet); |
| |
| // no attachments, labeled binary but no binary data to follow |
| if (this.reconstructor.reconPack.attachments === 0) { |
| this.emit('decoded', packet); |
| } |
| } else { // non-binary full packet |
| this.emit('decoded', packet); |
| } |
| } |
| else if (isBuf(obj) || obj.base64) { // raw binary data |
| if (!this.reconstructor) { |
| throw new Error('got binary data when not reconstructing a packet'); |
| } else { |
| packet = this.reconstructor.takeBinaryData(obj); |
| if (packet) { // received final buffer |
| this.reconstructor = null; |
| this.emit('decoded', packet); |
| } |
| } |
| } |
| else { |
| throw new Error('Unknown type: ' + obj); |
| } |
| }; |
| |
| /** |
| * Decode a packet String (JSON data) |
| * |
| * @param {String} str |
| * @return {Object} packet |
| * @api private |
| */ |
| |
| function decodeString(str) { |
| var i = 0; |
| // look up type |
| var p = { |
| type: Number(str.charAt(0)) |
| }; |
| |
| if (null == exports.types[p.type]) { |
| return error('unknown packet type ' + p.type); |
| } |
| |
| // look up attachments if type binary |
| if (exports.BINARY_EVENT === p.type || exports.BINARY_ACK === p.type) { |
| var buf = ''; |
| while (str.charAt(++i) !== '-') { |
| buf += str.charAt(i); |
| if (i == str.length) break; |
| } |
| if (buf != Number(buf) || str.charAt(i) !== '-') { |
| throw new Error('Illegal attachments'); |
| } |
| p.attachments = Number(buf); |
| } |
| |
| // look up namespace (if any) |
| if ('/' === str.charAt(i + 1)) { |
| p.nsp = ''; |
| while (++i) { |
| var c = str.charAt(i); |
| if (',' === c) break; |
| p.nsp += c; |
| if (i === str.length) break; |
| } |
| } else { |
| p.nsp = '/'; |
| } |
| |
| // look up id |
| var next = str.charAt(i + 1); |
| if ('' !== next && Number(next) == next) { |
| p.id = ''; |
| while (++i) { |
| var c = str.charAt(i); |
| if (null == c || Number(c) != c) { |
| --i; |
| break; |
| } |
| p.id += str.charAt(i); |
| if (i === str.length) break; |
| } |
| p.id = Number(p.id); |
| } |
| |
| // look up json data |
| if (str.charAt(++i)) { |
| var payload = tryParse(str.substr(i)); |
| var isPayloadValid = payload !== false && (p.type === exports.ERROR || isArray(payload)); |
| if (isPayloadValid) { |
| p.data = payload; |
| } else { |
| return error('invalid payload'); |
| } |
| } |
| |
| debug('decoded %s as %j', str, p); |
| return p; |
| } |
| |
| function tryParse(str) { |
| try { |
| return JSON.parse(str); |
| } catch(e){ |
| return false; |
| } |
| } |
| |
| /** |
| * Deallocates a parser's resources |
| * |
| * @api public |
| */ |
| |
| Decoder.prototype.destroy = function() { |
| if (this.reconstructor) { |
| this.reconstructor.finishedReconstruction(); |
| } |
| }; |
| |
| /** |
| * A manager of a binary event's 'buffer sequence'. Should |
| * be constructed whenever a packet of type BINARY_EVENT is |
| * decoded. |
| * |
| * @param {Object} packet |
| * @return {BinaryReconstructor} initialized reconstructor |
| * @api private |
| */ |
| |
| function BinaryReconstructor(packet) { |
| this.reconPack = packet; |
| this.buffers = []; |
| } |
| |
| /** |
| * Method to be called when binary data received from connection |
| * after a BINARY_EVENT packet. |
| * |
| * @param {Buffer | ArrayBuffer} binData - the raw binary data received |
| * @return {null | Object} returns null if more binary data is expected or |
| * a reconstructed packet object if all buffers have been received. |
| * @api private |
| */ |
| |
| BinaryReconstructor.prototype.takeBinaryData = function(binData) { |
| this.buffers.push(binData); |
| if (this.buffers.length === this.reconPack.attachments) { // done with buffer list |
| var packet = binary.reconstructPacket(this.reconPack, this.buffers); |
| this.finishedReconstruction(); |
| return packet; |
| } |
| return null; |
| }; |
| |
| /** |
| * Cleans up binary packet reconstruction variables. |
| * |
| * @api private |
| */ |
| |
| BinaryReconstructor.prototype.finishedReconstruction = function() { |
| this.reconPack = null; |
| this.buffers = []; |
| }; |
| |
| function error(msg) { |
| return { |
| type: exports.ERROR, |
| data: 'parser error: ' + msg |
| }; |
| } |