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.

409 lines
11 KiB

'use strict';
var events = require('events');
var util = require('util');
var net = require('net');
var tls = require('tls');
var fs = require('fs');
var debug = require('./debug');
var jspack = require('../jspack').jspack;
var AMQPTypes = require('./constants').AMQPTypes;
var Indicators = require('./constants').Indicators;
var FrameType = require('./constants').FrameType;
var definitions = require('./definitions');
var methodTable = definitions.methodTable;
var classes = definitions.classes;
// parser
var MAX_FRAME_BUFFER_DEFAULT = 131072; // 128k, same as rabbitmq (which was
// copying qpid)
// An interruptible AMQP parser.
//
// type is either 'server' or 'client'
// version is '0-9-1'.
//
// Instances of this class have several callbacks
// - onMethod(channel, method, args);
// - onHeartBeat()
// - onContent(channel, buffer);
// - onContentHeader(channel, class, weight, properties, size);
//
// This class does not subclass EventEmitter, in order to reduce the speed
// of emitting the callbacks. Since this is an internal class, that should
// be fine.
var AMQPParser = module.exports = function AMQPParser (version, type) {
this.isClient = (type == 'client');
this.state = this.isClient ? 'frameHeader' : 'protocolHeader';
this.maxFrameBuffer = MAX_FRAME_BUFFER_DEFAULT;
if (version != '0-9-1') this.throwError("Unsupported protocol version");
var frameHeader = new Buffer(7);
frameHeader.used = 0;
var frameBuffer, frameType, frameChannel;
var self = this;
function header(data) {
var fh = frameHeader;
var needed = fh.length - fh.used;
data.copy(fh, fh.used, 0, data.length);
fh.used += data.length; // sloppy
if (fh.used >= fh.length) {
fh.read = 0;
frameType = fh[fh.read++];
frameChannel = parseInt(fh, 2);
var frameSize = parseInt(fh, 4);
fh.used = 0; // for reuse
if (frameSize > self.maxFrameBuffer) {
self.throwError("Oversized frame " + frameSize);
}
frameBuffer = new Buffer(frameSize);
frameBuffer.used = 0;
return frame(data.slice(needed));
}
else { // need more!
return header;
}
}
function frame(data) {
var fb = frameBuffer;
var needed = fb.length - fb.used;
var sourceEnd = (fb.length > data.length) ? data.length : fb.length;
data.copy(fb, fb.used, 0, sourceEnd);
fb.used += data.length;
if (data.length > needed) {
return frameEnd(data.slice(needed));
}
else if (data.length == needed) {
return frameEnd;
}
else {
return frame;
}
}
function frameEnd(data) {
if (data.length > 0) {
if (data[0] === Indicators.FRAME_END) {
switch (frameType) {
case FrameType.METHOD:
self._parseMethodFrame(frameChannel, frameBuffer);
break;
case FrameType.HEADER:
self._parseHeaderFrame(frameChannel, frameBuffer);
break;
case FrameType.BODY:
if (self.onContent) {
self.onContent(frameChannel, frameBuffer);
}
break;
case FrameType.HEARTBEAT:
debug && debug("heartbeat");
if (self.onHeartBeat) self.onHeartBeat();
break;
default:
self.throwError("Unhandled frame type " + frameType);
break;
}
return header(data.slice(1));
}
else {
self.throwError("Missing frame end marker");
}
}
else {
return frameEnd;
}
}
self.parse = header;
}
// If there's an error in the parser, call the onError handler or throw
AMQPParser.prototype.throwError = function (error) {
if (this.onError) this.onError(error);
else throw new Error(error);
};
// Everytime data is recieved on the socket, pass it to this function for
// parsing.
AMQPParser.prototype.execute = function (data) {
// This function only deals with dismantling and buffering the frames.
// It delegates to other functions for parsing the frame-body.
debug && debug('execute: ' + data.toString('hex'));
this.parse = this.parse(data);
};
/**
* Set the maximum frame buffer size in bytes. The connection needs to change this
* if the server responds with a connection tune event where the maxFrameBuffer
* was changed in the server config.
*
* @param maxFrameBuffer the maximum frame buffer size in bytes
*/
AMQPParser.prototype.setMaxFrameBuffer = function(maxFrameBuffer) {
this.maxFrameBuffer = maxFrameBuffer;
};
// parse Network Byte Order integers. size can be 1,2,4,8
function parseInt (buffer, size) {
switch (size) {
case 1:
return buffer[buffer.read++];
case 2:
return (buffer[buffer.read++] << 8) + buffer[buffer.read++];
case 4:
return (buffer[buffer.read++] << 24) + (buffer[buffer.read++] << 16) +
(buffer[buffer.read++] << 8) + buffer[buffer.read++];
case 8:
return (buffer[buffer.read++] << 56) + (buffer[buffer.read++] << 48) +
(buffer[buffer.read++] << 40) + (buffer[buffer.read++] << 32) +
(buffer[buffer.read++] << 24) + (buffer[buffer.read++] << 16) +
(buffer[buffer.read++] << 8) + buffer[buffer.read++];
default:
throw new Error("cannot parse ints of that size");
}
}
function parseShortString (buffer) {
var length = buffer[buffer.read++];
var s = buffer.toString('utf8', buffer.read, buffer.read+length);
buffer.read += length;
return s;
}
function parseLongString (buffer) {
var length = parseInt(buffer, 4);
var s = buffer.slice(buffer.read, buffer.read + length);
buffer.read += length;
return s.toString();
}
function parseSignedInteger (buffer) {
var int = parseInt(buffer, 4);
if (int & 0x80000000) {
int |= 0xEFFFFFFF;
int = -int;
}
return int;
}
function parseValue (buffer) {
switch (buffer[buffer.read++]) {
case AMQPTypes.STRING:
return parseLongString(buffer);
case AMQPTypes.INTEGER:
return parseInt(buffer, 4);
case AMQPTypes.DECIMAL:
var dec = parseInt(buffer, 1);
var num = parseInt(buffer, 4);
return num / (dec * 10);
case AMQPTypes._64BIT_FLOAT:
var b = [];
for (var i = 0; i < 8; ++i)
b[i] = buffer[buffer.read++];
return (new jspack(true)).Unpack('d', b);
case AMQPTypes._32BIT_FLOAT:
var b = [];
for (var i = 0; i < 4; ++i)
b[i] = buffer[buffer.read++];
return (new jspack(true)).Unpack('f', b);
case AMQPTypes.TIME:
var int = parseInt(buffer, 8);
return (new Date()).setTime(int * 1000);
case AMQPTypes.HASH:
return parseTable(buffer);
case AMQPTypes.SIGNED_64BIT:
return parseInt(buffer, 8);
case AMQPTypes.SIGNED_8BIT:
return parseInt(buffer, 1);
case AMQPTypes.BOOLEAN:
return (parseInt(buffer, 1) > 0);
case AMQPTypes.BYTE_ARRAY:
var len = parseInt(buffer, 4);
var buf = new Buffer(len);
buffer.copy(buf, 0, buffer.read, buffer.read + len);
buffer.read += len;
return buf;
case AMQPTypes.ARRAY:
var len = parseInt(buffer, 4);
var end = buffer.read + len;
var arr = [];
while (buffer.read < end) {
arr.push(parseValue(buffer));
}
return arr;
case AMQPTypes.VOID:
return null;
default:
throw new Error("Unknown field value type " + buffer[buffer.read-1]);
}
}
function parseTable (buffer) {
var length = buffer.read + parseInt(buffer, 4);
var table = {};
while (buffer.read < length) {
table[parseShortString(buffer)] = parseValue(buffer);
}
return table;
}
function parseFields (buffer, fields) {
var args = {};
var bitIndex = 0;
var value;
for (var i = 0; i < fields.length; i++) {
var field = fields[i];
//debug && debug("parsing field " + field.name + " of type " + field.domain);
switch (field.domain) {
case 'bit':
// 8 bits can be packed into one octet.
// XXX check if bitIndex greater than 7?
value = (buffer[buffer.read] & (1 << bitIndex)) ? true : false;
if (fields[i+1] && fields[i+1].domain == 'bit') {
bitIndex++;
} else {
bitIndex = 0;
buffer.read++;
}
break;
case 'octet':
value = buffer[buffer.read++];
break;
case 'short':
value = parseInt(buffer, 2);
break;
case 'long':
value = parseInt(buffer, 4);
break;
// In a previous version this shared code with 'longlong', which caused problems when passed Date
// integers. Nobody expects to pass a Buffer here, 53 bits is still 28 million years after 1970, we'll be fine.
case 'timestamp':
value = parseInt(buffer, 8);
break;
// JS doesn't support 64-bit Numbers, so we expect if you're using 'longlong' that you've
// used a Buffer instead
case 'longlong':
value = new Buffer(8);
for (var j = 0; j < 8; j++) {
value[j] = buffer[buffer.read++];
}
break;
case 'shortstr':
value = parseShortString(buffer);
break;
case 'longstr':
value = parseLongString(buffer);
break;
case 'table':
value = parseTable(buffer);
break;
default:
throw new Error("Unhandled parameter type " + field.domain);
}
//debug && debug("got " + value);
args[field.name] = value;
}
return args;
}
AMQPParser.prototype._parseMethodFrame = function (channel, buffer) {
buffer.read = 0;
var classId = parseInt(buffer, 2),
methodId = parseInt(buffer, 2);
// Make sure that this is a method that we understand.
if (!methodTable[classId] || !methodTable[classId][methodId]) {
this.throwError("Received unknown [classId, methodId] pair [" +
classId + ", " + methodId + "]");
}
var method = methodTable[classId][methodId];
if (!method) this.throwError("bad method?");
var args = parseFields(buffer, method.fields);
if (this.onMethod) {
debug && debug("Executing method", channel, method, args);
this.onMethod(channel, method, args);
}
};
AMQPParser.prototype._parseHeaderFrame = function (channel, buffer) {
buffer.read = 0;
var classIndex = parseInt(buffer, 2);
var weight = parseInt(buffer, 2);
var size = parseInt(buffer, 8);
var classInfo = classes[classIndex];
if (classInfo.fields.length > 15) {
this.throwError("TODO: support more than 15 properties");
}
var propertyFlags = parseInt(buffer, 2);
var fields = [];
for (var i = 0; i < classInfo.fields.length; i++) {
var field = classInfo.fields[i];
// groan.
if (propertyFlags & (1 << (15-i))) fields.push(field);
}
var properties = parseFields(buffer, fields);
if (this.onContentHeader) {
this.onContentHeader(channel, classInfo, weight, properties, size);
}
};