|
|
/**
|
|
* Module dependencies.
|
|
*/
|
|
|
|
var debug = require('debug')('socket.io-parser');
|
|
var json = require('json3');
|
|
var isArray = require('isarray');
|
|
var Emitter = require('component-emitter');
|
|
var binary = require('./binary');
|
|
var isBuf = require('./is-buffer');
|
|
|
|
/**
|
|
* Protocol version.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
exports.protocol = 4;
|
|
|
|
/**
|
|
* Packet types.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
exports.types = [
|
|
'CONNECT',
|
|
'DISCONNECT',
|
|
'EVENT',
|
|
'BINARY_EVENT',
|
|
'ACK',
|
|
'BINARY_ACK',
|
|
'ERROR'
|
|
];
|
|
|
|
/**
|
|
* 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() {}
|
|
|
|
/**
|
|
* 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) {
|
|
var str = '';
|
|
var nsp = false;
|
|
|
|
// first is type
|
|
str += obj.type;
|
|
|
|
// attachments if we have them
|
|
if (exports.BINARY_EVENT == obj.type || exports.BINARY_ACK == obj.type) {
|
|
str += obj.attachments;
|
|
str += '-';
|
|
}
|
|
|
|
// if we have a namespace other than `/`
|
|
// we append it followed by a comma `,`
|
|
if (obj.nsp && '/' != obj.nsp) {
|
|
nsp = true;
|
|
str += obj.nsp;
|
|
}
|
|
|
|
// immediately followed by the id
|
|
if (null != obj.id) {
|
|
if (nsp) {
|
|
str += ',';
|
|
nsp = false;
|
|
}
|
|
str += obj.id;
|
|
}
|
|
|
|
// json data
|
|
if (null != obj.data) {
|
|
if (nsp) str += ',';
|
|
str += json.stringify(obj.data);
|
|
}
|
|
|
|
debug('encoded %j as %s', obj, str);
|
|
return str;
|
|
}
|
|
|
|
/**
|
|
* 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 ('string' == typeof obj) {
|
|
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 p = {};
|
|
var i = 0;
|
|
|
|
// look up type
|
|
p.type = Number(str.charAt(0));
|
|
if (null == exports.types[p.type]) return error();
|
|
|
|
// look up attachments if type binary
|
|
if (exports.BINARY_EVENT == p.type || exports.BINARY_ACK == p.type) {
|
|
p.attachments = '';
|
|
while (str.charAt(++i) != '-') {
|
|
p.attachments += str.charAt(i);
|
|
}
|
|
p.attachments = Number(p.attachments);
|
|
}
|
|
|
|
// 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 + 1 == 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 + 1 == str.length) break;
|
|
}
|
|
p.id = Number(p.id);
|
|
}
|
|
|
|
// look up json data
|
|
if (str.charAt(++i)) {
|
|
try {
|
|
p.data = json.parse(str.substr(i));
|
|
} catch(e){
|
|
return error();
|
|
}
|
|
}
|
|
|
|
debug('decoded %s as %j', str, p);
|
|
return p;
|
|
}
|
|
|
|
/**
|
|
* 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(data){
|
|
return {
|
|
type: exports.ERROR,
|
|
data: 'parser error'
|
|
};
|
|
}
|