lib/diameter-codec.js
'use strict';
var _ = require('lodash');
var diameterTypes = require('./diameter-types');
var diameterUtil = require('./diameter-util');
var dictionary = require('./diameter-dictionary');
var DIAMETER_MESSAGE_HEADER_LENGTH_IN_BYTES = 20;
var DIAMETER_MESSAGE_AVP_HEADER_LENGTH_IN_BYTES = 8;
var DIAMETER_MESSAGE_VENDOR_ID_LENGTH_IN_BYTES = 4;
// Byte positions for header fields
var DIAMETER_MESSAGE_HEADER_VERSION = 0;
var DIAMETER_MESSAGE_HEADER_LENGTH = 1;
var DIAMETER_MESSAGE_HEADER_COMMAND_CODE = 5;
var DIAMETER_MESSAGE_HEADER_FLAGS = 4;
var DIAMETER_MESSAGE_HEADER_FLAG_REQUEST = 0;
var DIAMETER_MESSAGE_HEADER_FLAG_PROXIABLE = 1;
var DIAMETER_MESSAGE_HEADER_FLAG_ERROR = 2;
var DIAMETER_MESSAGE_HEADER_FLAG_POTENTIALLY_RETRANSMITTED = 3;
var DIAMETER_MESSAGE_HEADER_APPLICATION_ID = 8;
var DIAMETER_MESSAGE_HEADER_HOP_BY_HOP_ID = 12;
var DIAMETER_MESSAGE_HEADER_END_TO_END_ID = 16;
// Byte postions for AVP fields
var DIAMETER_MESSAGE_AVP_CODE = 0;
var DIAMETER_MESSAGE_AVP_FLAGS = 4;
var DIAMETER_MESSAGE_AVP_FLAG_VENDOR = 0;
var DIAMETER_MESSAGE_AVP_FLAG_MANDATORY = 1;
var DIAMETER_MESSAGE_AVP_LENGTH = 5;
var DIAMETER_MESSAGE_AVP_VENDOR_ID = 8;
var DIAMETER_MESSAGE_AVP_VENDOR_ID_DATA = 12;
var DIAMETER_MESSAGE_AVP_NO_VENDOR_ID_DATA = 8;
var readUInt24BE = function(buffer, offset) {
return buffer.readUInt8(offset) * 256 * 256 + buffer.readUInt8(offset + 1) * 256 + buffer.readUInt8(offset + 2);
};
// wow, this is terrible.. must change
var writeUInt24BE = function(buffer, offset, value) {
var i = Math.floor(value / (256 * 256));
buffer.writeUInt8(i, offset);
value = value % (256 * 256);
i = Math.floor(value / (256));
buffer.writeUInt8(Math.floor(value / 256), offset + 1);
value = value % 256;
buffer.writeUInt8(value, offset + 2);
};
var getBit = function(num, bit) {
return ((num >> (7 - bit)) % 2 !== 0);
};
// another beauty..
var getIntFromBits = function(array) {
var s = '';
_.each(array, function(bit) {
s += bit ? '1' : '0';
});
return parseInt(s, 2);
};
exports.decodeMessageHeader = function(buffer) {
var message = {
_timeReceived: _.now(),
header: {},
body: []
};
message.header.version = buffer.readUInt8(DIAMETER_MESSAGE_HEADER_VERSION);
message.header.length = readUInt24BE(buffer, DIAMETER_MESSAGE_HEADER_LENGTH);
message.header.commandCode = readUInt24BE(buffer, DIAMETER_MESSAGE_HEADER_COMMAND_CODE);
var flags = buffer.readUInt8(DIAMETER_MESSAGE_HEADER_FLAGS);
message.header.flags = {
request: getBit(flags, DIAMETER_MESSAGE_HEADER_FLAG_REQUEST),
proxiable: getBit(flags, DIAMETER_MESSAGE_HEADER_FLAG_PROXIABLE),
error: getBit(flags, DIAMETER_MESSAGE_HEADER_FLAG_ERROR),
potentiallyRetransmitted: getBit(flags, DIAMETER_MESSAGE_HEADER_FLAG_POTENTIALLY_RETRANSMITTED)
};
message.header.applicationId = buffer.readUInt32BE(DIAMETER_MESSAGE_HEADER_APPLICATION_ID);
message.header.hopByHopId = buffer.readUInt32BE(DIAMETER_MESSAGE_HEADER_HOP_BY_HOP_ID);
message.header.endToEndId = buffer.readUInt32BE(DIAMETER_MESSAGE_HEADER_END_TO_END_ID);
return message;
};
var inflateMessageHeader = function(message) {
var command = dictionary.getCommandByCode(message.header.commandCode);
if (command == null) {
throw new Error('Can\'t find command with code ' + message.header.commandCode);
}
message.command = command.name;
var application = dictionary.getApplicationById(message.header.applicationId);
if (application == null) {
throw new Error('Can\'t find application with ID ' + message.header.applicationId);
}
message.header.application = application.name;
};
var findApplication = function(applicationName) {
var application;
if (!_.isNumber(applicationName)) {
application = dictionary.getApplicationByName(applicationName);
} else {
application = dictionary.getApplicationById(applicationName);
}
return application;
};
var findCommand = function(commandName) {
var command;
if (!_.isNumber(commandName)) {
command = dictionary.getCommandByName(commandName);
} else {
command = dictionary.getCommandByCode(commandName);
}
return command;
};
exports.constructRequest = function(applicationName, commandName, sessionId) {
var application = findApplication(applicationName);
if (application == undefined) {
throw new Error('Application ' + applicationName + ' not found in dictionary. ');
}
var command = findCommand(commandName);
if (command === undefined) {
throw new Error('Command ' + commandName + ' not found in dictionary. ');
}
var request = {
header: {
version: 1,
commandCode: _.parseInt(command.code),
flags: {
request: true,
proxiable: false,
error: false,
potentiallyRetransmitted: false
},
applicationId: _.parseInt(application.code),
application: application.name,
hopByHopId: -1, // needs to be set by client
endToEndId: diameterUtil.random32BitNumber()
},
body: [],
command: command.name
};
request.body.push(['Session-Id', sessionId.toString()]);
return request;
};
exports.constructResponse = function(message) {
var response = {
header: {
version: message.header.version,
commandCode: message.header.commandCode,
flags: {
request: false,
proxiable: message.header.flags.proxiable,
error: false,
potentiallyRetransmitted: message.header.flags.potentiallyRetransmitted
},
applicationId: message.header.applicationId,
application: message.header.application,
hopByHopId: message.header.hopByHopId,
endToEndId: message.header.endToEndId
},
body: [],
command: message.command
};
var sessionId = _.find(message.body, function(avp) {
return avp[0] === 'Session-Id';
});
if (sessionId) {
response.body.push(['Session-Id', sessionId[1]]);
}
return response;
};
var decodeAvpHeader = function(buffer, start) {
var avp = {};
avp.codeInt = buffer.readUInt32BE(start + DIAMETER_MESSAGE_AVP_CODE);
var flags = buffer.readUInt8(start + DIAMETER_MESSAGE_AVP_FLAGS);
avp.flags = {
vendor: getBit(flags, DIAMETER_MESSAGE_AVP_FLAG_VENDOR),
mandatory: getBit(flags, DIAMETER_MESSAGE_AVP_FLAG_MANDATORY)
};
avp.length = readUInt24BE(buffer, start + DIAMETER_MESSAGE_AVP_LENGTH);
return avp;
};
var decodeAvp = function(buffer, start, appId) {
var avp = decodeAvpHeader(buffer, start);
var hasVendorId = avp.flags.vendor;
if (hasVendorId) {
avp.vendorId = buffer.readUInt32BE(start + DIAMETER_MESSAGE_AVP_VENDOR_ID);
} else {
avp.vendorId = 0;
}
try {
var avpTag = dictionary.getAvpByCodeAndVendorId(avp.codeInt, avp.vendorId);
if (avpTag == null) {
throw new Error('Unable to find AVP for code ' + avp.codeInt + ' and vendor id ' + avp.vendorId + ', for app ' + appId);
}
avp.code = avpTag.name;
var dataPosition = hasVendorId ? DIAMETER_MESSAGE_AVP_VENDOR_ID_DATA : DIAMETER_MESSAGE_AVP_NO_VENDOR_ID_DATA;
avp.dataRaw = buffer.slice(start + dataPosition, start + avp.length);
if (avpTag.type === 'Grouped') {
avp.avps = decodeAvps(avp.dataRaw, 0, avp.dataRaw.length, appId);
} else {
avp.data = diameterTypes.decode(avpTag.type, avp.dataRaw);
if (avpTag.type === 'AppId') {
var application = dictionary.getApplicationById(avp.data);
if (application == null) {
throw new Error('Can\'t find application with ID ' + avp.data);
}
avp.data = application.name;
} else if (avpTag.enums) {
var enumValue = _.find(avpTag.enums, {
code: avp.data
});
if (enumValue == null) {
throw new Error('No enum value found for ' + avp.code + ' code ' + avp.data);
}
avp.data = enumValue.name;
}
}
} catch (err) {
if (avp.flags.mandatory) {
throw err;
}
}
return avp;
};
var decodeAvps = function(buffer, start, end, appId) {
var avps = [];
var cursor = start;
while (cursor < end) {
var avp = decodeAvp(buffer, cursor, appId);
avps.push(avp);
cursor += avp.length;
if (cursor % 4 !== 0) {
cursor += 4 - cursor % 4; // round to next 32 bit
}
}
return avps;
};
// Converts avp objects to array form, e.g. [['key', 'value'], ['key', 'value']]
var avpsToArrayForm = function(avps) {
return _.map(avps, function(avp) {
if (avp.avps) {
return [avp.code, avpsToArrayForm(avp.avps)];
}
return [avp.code, avp.data];
});
};
exports.decodeMessage = function(buffer) {
var message = exports.decodeMessageHeader(buffer);
var avps = decodeAvps(buffer, DIAMETER_MESSAGE_HEADER_LENGTH_IN_BYTES,
message.header.length, message.header.applicationId);
inflateMessageHeader(message);
message.body = avpsToArrayForm(avps);
message._timeProcessed = _.now();
return message;
};
var encodeAvps = function(avps, appId) {
var avpBuffers = _.map(avps, function(avp) {
return encodeAvp(avp, appId);
});
return Buffer.concat(avpBuffers);
};
var encodeAvp = function(avp, appId) {
var avpTag;
if (!_.isNumber(avp[0])) {
avpTag = dictionary.getAvpByName(avp[0]);
} else {
avpTag = dictionary.getAvpByCode(avp[0]);
}
if (avpTag == null) {
throw new Error('Unknown AVP code ' + avp[0] + ' for app ' + appId);
}
var value = avp[1];
var avpDataBuffer;
if (avpTag.type === 'Grouped') {
avpDataBuffer = encodeAvps(value, appId);
} else {
if (avpTag.enums) {
var enumCode;
if (!_.isNumber(value)) {
enumCode = _.find(avpTag.enums, {
name: value
});
} else {
enumCode = _.find(avpTag.enums, {
code: value
});
}
if (enumCode == null) {
throw new Error('Invalid enum value ' + value + ' for ' + avpTag.name);
}
value = enumCode.code;
} else if (avpTag.type === 'AppId') {
var enumCode;
if (!_.isNumber(value)) {
enumCode = dictionary.getApplicationByName(value);
} else {
enumCode = dictionary.getApplicationById(value);
}
if (enumCode == null) {
throw new Error('Invalid application ID value ' + value + ' for ' + avpTag.name);
}
value = enumCode.code;
}
avpDataBuffer = diameterTypes.encode(avpTag.type, value);
}
var avpHeaderLength = DIAMETER_MESSAGE_AVP_HEADER_LENGTH_IN_BYTES;
if (avpTag.flags.vendorBit) {
// 4 extra bytes for vendor id
avpHeaderLength += DIAMETER_MESSAGE_VENDOR_ID_LENGTH_IN_BYTES;
}
var avpHeaderBuffer = new Buffer(avpHeaderLength);
avpHeaderBuffer.writeUInt32BE(_.parseInt(avpTag.code), DIAMETER_MESSAGE_AVP_CODE);
var flags = [avpTag.flags.vendorBit, avpTag.flags.mandatory, avpTag.flags['protected']];
var flagsInt = getIntFromBits(_.flatten([flags, [false, false, false, false, false]]));
avpHeaderBuffer.writeUInt8(flagsInt, DIAMETER_MESSAGE_AVP_FLAGS);
writeUInt24BE(avpHeaderBuffer, DIAMETER_MESSAGE_AVP_LENGTH, avpDataBuffer.length + avpHeaderBuffer.length);
if (avpTag.vendorId > 0) {
avpHeaderBuffer.writeUInt32BE(_.parseInt(avpTag.vendorId), DIAMETER_MESSAGE_AVP_VENDOR_ID);
}
if (avpDataBuffer.length % 4 !== 0) {
var filler = new Buffer(4 - avpDataBuffer.length % 4);
filler.fill(0);
avpDataBuffer = Buffer.concat([avpDataBuffer, filler]);
}
return Buffer.concat([avpHeaderBuffer, avpDataBuffer]);
};
exports.encodeMessage = function(message) {
var buffer = new Buffer(DIAMETER_MESSAGE_HEADER_LENGTH_IN_BYTES);
buffer.writeUInt8(message.header.version, DIAMETER_MESSAGE_HEADER_VERSION);
writeUInt24BE(buffer, DIAMETER_MESSAGE_HEADER_COMMAND_CODE, message.header.commandCode);
var flags = _.values(message.header.flags);
var flagsInt = getIntFromBits(_.flatten([flags, [false, false, false, false]]));
buffer.writeUInt8(flagsInt, DIAMETER_MESSAGE_HEADER_FLAGS);
buffer.writeUInt32BE(message.header.applicationId, DIAMETER_MESSAGE_HEADER_APPLICATION_ID);
buffer.writeUInt32BE(message.header.hopByHopId, DIAMETER_MESSAGE_HEADER_HOP_BY_HOP_ID);
buffer.writeUInt32BE(message.header.endToEndId, DIAMETER_MESSAGE_HEADER_END_TO_END_ID);
var avpBuffers = _.map(message.body, function(avp) {
return encodeAvp(avp, message.header.applicationId);
});
buffer = Buffer.concat(_.flatten([buffer, avpBuffers]));
writeUInt24BE(buffer, DIAMETER_MESSAGE_HEADER_LENGTH, buffer.length);
return buffer;
};
exports.encodeAvp = encodeAvp;
exports.decodeAvp = decodeAvp;