mirror of
https://github.com/socketio/socket.io.git
synced 2026-04-30 03:00:39 -04:00
Backported from main: b25738c416
When a packet contains binary elements, the built-in parser does not modify them and simply sends them in their own WebSocket frame.
Example: `socket.emit("some event", Buffer.of(1,2,3))`
is encoded and transferred as:
- 1st frame: 51-["some event",{"_placeholder":true,"num":0}]
- 2nd frame: <buffer 01 02 03>
where:
- `5` is the type of the packet (binary message)
- `1` is the number of binary attachments
- `-` is the separator
- `["some event",{"_placeholder":true,"num":0}]` is the payload (including the placeholder)
On the receiving end, the parser reads the number of attachments and buffers them until they are all received.
Before this change, the built-in parser accepted any number of binary attachments, which could be exploited to make the server run out of memory.
The number of attachments is now limited to 10, which should be sufficient for most use cases.
The limit can be increased with a custom `parser`:
```js
import { Encoder, Decoder } from "socket.io-parser";
const io = new Server({
parser: {
Encoder,
Decoder: class extends Decoder {
constructor() {
super({
maxAttachments: 20
});
}
}
}
});
```
456 lines
9.0 KiB
JavaScript
456 lines
9.0 KiB
JavaScript
|
|
/**
|
|
* 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(opts) {
|
|
this.reconstructor = null;
|
|
opts = opts || {};
|
|
this.opts = {
|
|
maxAttachments: opts.maxAttachments || 10,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Mix in `Emitter` with Decoder.
|
|
*/
|
|
|
|
Emitter(Decoder.prototype);
|
|
|
|
/**
|
|
* Decodes an encoded 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') {
|
|
if (this.reconstructor) {
|
|
throw new Error("got plaintext data when reconstructing a packet");
|
|
}
|
|
packet = decodeString(obj, this.opts.maxAttachments);
|
|
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);
|
|
}
|
|
};
|
|
|
|
function isPayloadValid(type, payload) {
|
|
switch (type) {
|
|
case 0: // CONNECT
|
|
return typeof payload === "object";
|
|
case 1: // DISCONNECT
|
|
return payload === undefined;
|
|
case 4: // ERROR
|
|
return typeof payload === "string" || typeof payload === "object";
|
|
case 2: // EVENT
|
|
case 5: // BINARY_EVENT
|
|
return (
|
|
isArray(payload) &&
|
|
(typeof payload[0] === "string" || typeof payload[0] === "number")
|
|
);
|
|
case 3: // ACK
|
|
case 6: // BINARY_ACK
|
|
return isArray(payload);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decode a packet String (JSON data)
|
|
*
|
|
* @param {String} str
|
|
* @param {Number} maxAttachments - the maximum number of binary attachments
|
|
* @return {Object} packet
|
|
* @api private
|
|
*/
|
|
|
|
function decodeString(str, maxAttachments) {
|
|
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 start = i + 1;
|
|
while (str.charAt(++i) !== '-' && i != str.length) {}
|
|
var buf = str.substring(start, i);
|
|
if (buf != Number(buf) || str.charAt(i) !== '-') {
|
|
throw new Error('Illegal attachments');
|
|
}
|
|
var n = Number(buf);
|
|
if (!isInteger(n) || n < 0) {
|
|
throw new Error("Illegal attachments");
|
|
} else if (n > maxAttachments) {
|
|
throw new Error("too many attachments");
|
|
}
|
|
p.attachments = n;
|
|
}
|
|
|
|
// look up namespace (if any)
|
|
if ('/' === str.charAt(i + 1)) {
|
|
var start = i + 1;
|
|
while (++i) {
|
|
var c = str.charAt(i);
|
|
if (',' === c) break;
|
|
if (i === str.length) break;
|
|
}
|
|
p.nsp = str.substring(start, i);
|
|
} else {
|
|
p.nsp = '/';
|
|
}
|
|
|
|
// look up id
|
|
var next = str.charAt(i + 1);
|
|
if ('' !== next && Number(next) == next) {
|
|
var start = i + 1;
|
|
while (++i) {
|
|
var c = str.charAt(i);
|
|
if (null == c || Number(c) != c) {
|
|
--i;
|
|
break;
|
|
}
|
|
if (i === str.length) break;
|
|
}
|
|
p.id = Number(str.substring(start, i + 1));
|
|
}
|
|
|
|
// look up json data
|
|
if (str.charAt(++i)) {
|
|
var payload = tryParse(str.substr(i));
|
|
if (isPayloadValid(p.type, payload)) {
|
|
p.data = payload;
|
|
} else {
|
|
throw new 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
|
|
};
|
|
}
|
|
|
|
var isInteger =
|
|
Number.isInteger ||
|
|
function (value) {
|
|
return (
|
|
typeof value === "number" &&
|
|
isFinite(value) &&
|
|
Math.floor(value) === value
|
|
);
|
|
};
|