You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
408 lines
7.8 KiB
408 lines
7.8 KiB
|
|
/** |
|
* Module dependencies. |
|
*/ |
|
|
|
var debug = require('debug')('socket.io-parser'); |
|
var Emitter = require('component-emitter'); |
|
var hasBin = require('has-binary2'); |
|
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() {} |
|
|
|
/** |
|
* 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){ |
|
if ((obj.type === exports.EVENT || obj.type === exports.ACK) && hasBin(obj.data)) { |
|
obj.type = obj.type === exports.EVENT ? exports.BINARY_EVENT : exports.BINARY_ACK; |
|
} |
|
|
|
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) { |
|
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 (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 |
|
}; |
|
}
|
|
|