snmp-native
Version:
A native Javascript SNMP implementation for Node.js
512 lines (412 loc) • 13.6 kB
JavaScript
// This file implements a minimal subset of Abstract Syntax Notation One (**ASN.1**)
// Basic Encoding Rules (**BER**), namely the parts that are necessary for sending
// and receiving SNMPv2c messages.
//
// (c) 2012 Jakob Borg, Nym Networks
;
// We define constants for the commonly used ASN.1 types in SNMP.
var T = {
Integer: 0x02,
OctetString: 0x04,
Null: 0x05,
ObjectIdentifier: 0x06,
Sequence: 0x30,
IpAddress: 0x40,
Counter: 0x41,
Gauge: 0x42,
TimeTicks: 0x43,
Opaque: 0x44,
NsapAddress: 0x45,
Counter64: 0x46,
NoSuchObject: 0x80,
NoSuchInstance: 0x81,
EndOfMibView: 0x82,
PDUBase: 0xA0
};
var P = {
GetRequestPDU: 0x00,
GetNextRequestPDU: 0x01,
GetResponsePDU: 0x02,
SetRequestPDU: 0x03
};
var E = {
NoError: 0,
TooBig: 1,
NoSuchName: 2,
BadValue: 3,
ReadOnly: 4,
GenErr: 5,
NoAccess: 6,
WrongType: 7,
WrongLength: 8,
WrongEncoding: 9,
WrongValue: 10,
NoCreation: 11,
InconsistentValue: 12,
ResourceUnavailable: 13,
CommitFailed: 14,
UndoFailed: 15,
AuthorizationError: 16,
NotWritable: 17,
InconsistentName: 18
};
var LOG256 = Math.log(256);
// The types are also available for consumers of the library.
exports.types = T;
exports.pduTypes = P;
exports.errors = E;
exports.unittest = {};
// Private helper functions
// -----
// Encode a length as it should be encoded.
function lengthArray(len) {
var arr = [];
if (len <= 127) {
// Return a single byte if the value is 127 or less.
return [ len ];
} else {
// Otherwise encode it as a MSB base-256 integer.
while (len > 0) {
arr.push(len % 256);
len = parseInt(len / 256, 10);
}
// Add a length byte in front and set the high bit to indicate
// that this is a longer value than one byte.
arr.push(128 + arr.length);
arr.reverse();
return arr;
}
}
exports.unittest.lengthArray = lengthArray;
// Return a wrapped copy of the passed `contents`, with the specified wrapper type.
// This is used for Sequence and other constructed types.
function wrapper(type, contents) {
var buf, len, i;
// Get the encoded length of the contents
len = lengthArray(contents.length);
// Set up a buffer with the type and length bytes plus a straight copy of the content.
buf = new Buffer(1 + contents.length + len.length);
buf[0] = type;
for (i = 1; i < len.length + 1; i++) {
buf[i] = len[i - 1];
}
contents.copy(buf, len.length + 1, 0);
return buf;
}
// Get the encoded representation of a number in an OID.
// If the number is less than 128, pass it as it is.
// Otherwise return an array of base-128 digits, most significant first,
// with the high bit set on all octets except the last one.
// This encoding should be used for all number in an OID except the first
// two (.1.3) which are handled specially.
function oidInt(val) {
var bytes = [];
bytes.push(val % 128);
val = parseInt(val / 128, 10);
while (val > 127) {
bytes.push(128 + val % 128);
val = parseInt(val / 128, 10);
}
bytes.push(val + 128);
return bytes.reverse();
}
// Encode an OID. The first two number are encoded specially
// in the first octet, then the rest are encoded as one octet per number
// unless the number exceeds 127. If so, it's encoded as several base-127
// octets with the high bit set to indicate continuation.
function oidArray(oid) {
var bytes, i, val;
// Enforce some minimum requirements on the OID.
if (oid.length < 2) {
throw new Error("Minimum OID length is two.");
} else if (oid[0] > 2) {
throw new Error("Invalid OID");
} else if (oid[0] == 0 && oid[1] > 39) {
throw new Error("Invalid OID");
} else if (oid[0] == 1 && oid[1] > 39) {
throw new Error("Invalid OID");
} else if (oid[0] == 2 && oid[1] > 79) {
throw new Error("Invalid OID");
}
// Calculate the first byte of the encoded OID according to the 'special' rule.
bytes = [ 40 * oid[0] + oid[1] ];
// For the rest of the OID, encode each number individually and add the
// resulting bytes to the buffer.
for (i = 2; i < oid.length; i++) {
val = oid[i];
if (val > 127) {
bytes = bytes.concat(oidInt(val));
} else {
bytes.push(val);
}
}
return bytes;
}
// Divide an integer into base-256 bytes.
// Most significant byte first.
function intArray(val) {
var array = [], encVal = val, bytes;
if (val === 0) {
array.push(0);
} else {
if (val < 0) {
bytes = Math.floor(1 + Math.log(-val) / LOG256);
// Encode negatives as 32-bit two's complement. Let's hope that fits.
encVal += Math.pow(2, 8 * bytes);
}
while (encVal > 0) {
array.push(encVal % 256);
encVal = parseInt(encVal / 256, 10);
}
}
// Do not produce integers that look negative (high bit
// of first byte set).
if (val > 0 && array[array.length - 1] >= 0x80) {
array.push(0);
}
return array.reverse();
}
// Functions to encode ASN.1 from native objects
// -----
// Encode a simple integer.
// Integers are encoded as a simple base-256 byte sequence, most significant byte first,
// prefixed with a length byte. In principle we support arbitrary integer sizes, in practice
// Javascript doesn't even **have** integers so some precision might get lost.
function encodeIntegerish(val, type) {
var i, arr, buf;
// Get the bytes that we're going to encode.
arr = intArray(val);
// Now that we know the length, we allocate a buffer of the required size.
// We set the type and length bytes appropriately.
buf = new Buffer(2 + arr.length);
buf[0] = type;
buf[1] = arr.length;
// Copy the bytes into the array.
for (i = 0; i < arr.length; i++) {
buf[i + 2] = arr[i];
}
return buf;
}
// Integer type, 0x02
exports.encodeInteger = function (val) {
return(encodeIntegerish(val, T.Integer));
};
// Gauge type, 0x42
exports.encodeGauge = function (val) {
return(encodeIntegerish(val, T.Gauge));
};
// Counter type, 0x41
exports.encodeCounter = function (val) {
return(encodeIntegerish(val, T.Counter));
};
// TimeTicks type, 0x43
exports.encodeTimeTicks = function (val) {
return(encodeIntegerish(val, T.TimeTicks));
};
// Create the representation of a Null, `05 00`.
exports.encodeNull = function () {
var buf = new Buffer(2);
buf[0] = T.Null;
buf[1] = 0;
return buf;
};
// NoSuchObject type, 0x80
exports.encodeNoSuchObject = function() {
var buf = new Buffer(2);
buf[0] = T.NoSuchObject;
buf[1] = 0;
return buf;
};
// NoSuchInstance type, 0x81
exports.encodeNoSuchInstance = function() {
var buf = new Buffer(2);
buf[0] = T.NoSuchInstance;
buf[1] = 0;
return buf;
};
// EndOfMibView type, 0x82
exports.encodeEndOfMibView = function() {
var buf = new Buffer(2);
buf[0] = T.EndOfMibView;
buf[1] = 0;
return buf;
};
// Encode a Sequence, which is a wrapper of type `30`.
exports.encodeSequence = function (contents) {
return wrapper(T.Sequence, contents);
};
// Encode an OctetString, which is a wrapper of type `04`.
exports.encodeOctetString = function (string) {
var buf, contents;
if (typeof string === 'string') {
contents = new Buffer(string);
} else if (Buffer.isBuffer(string)) {
contents = string;
} else {
throw new Error('Only Buffer and string types are acceptable as OctetString.');
}
return wrapper(T.OctetString, contents);
};
// Encode an IpAddress, which is a wrapper of type `40`.
exports.encodeIpAddress = function (address) {
var contents, octets, value = [];
if (typeof address !== 'string' && !Buffer.isBuffer(address)) {
throw new Error('Only Buffer and string types are acceptable as OctetString.');
}
// assume that the string is in dotted decimal format ipv4
// also, use toString in case a buffer was passed in.
octets = address.toString().split('.');
if (octets.length !== 4) {
throw new Error('IP Addresses must be specified in dotted decimal format.');
}
octets.forEach(function (octet) {
var octetValue = parseInt(octet, 10);
if (octet < 0 || octet > 255) {
throw new Error('IP Address octets must be between 0 and 255 inclusive.' + JSON.stringify(octets));
}
value.push(octetValue);
});
contents = new Buffer(value);
return wrapper(T.IpAddress, contents);
};
// Encode an ObjectId.
exports.encodeOid = function (oid) {
var buf, bytes, i, len;
// Get the encoded format of the OID.
bytes = oidArray(oid);
// Get the encoded format of the length
len = lengthArray(bytes.length);
// Fill in the buffer with type, length and OID data.
buf = new Buffer(1 + bytes.length + len.length);
buf[0] = T.ObjectIdentifier;
for (i = 1; i < len.length + 1; i++) {
buf[i] = len[i - 1];
}
for (i = len.length + 1; i < bytes.length + len.length + 1; i++) {
buf[i] = bytes[i - len.length - 1];
}
return buf;
};
// Encode an SNMP request with specified `contents`.
// The `type` code is 0 for `GetRequest`, 1 for `GetNextRequest`.
exports.encodeRequest = function (type, contents) {
return wrapper(T.PDUBase + type, contents);
};
// Functions to parse ASN.1 to native objects
// -----
// Parse and return type, data length and header length.
function typeAndLength(buf) {
var res, len, i;
res = { type: buf[0], len: 0, header: 1 };
if (buf[1] < 128) {
// If bit 8 is zero, this byte indicates the content length (up to 127 bytes).
res.len = buf[1];
res.header += 1;
} else {
// If bit 8 is 1, bits 0 to 7 indicate the number of following legth bytes.
// These bytes are a simple msb base-256 integer indicating the content length.
for (i = 0; i < buf[1] - 128; i++) {
res.len *= 256;
res.len += buf[i + 2];
}
res.header += buf[1] - 128 + 1;
}
return res;
}
exports.typeAndLength = typeAndLength;
// Parse a buffer containing a representation of an integer.
// Verifies the type, then multiplies in each byte as it comes.
exports.parseInteger = function (buf) {
var i, val, type, len;
type = buf[0];
len = buf[1];
if (type !== T.Integer && type !== T.Counter &&
type !== T.Counter64 && type !== T.Gauge &&
type !== T.TimeTicks) {
throw new Error('Buffer ' + buf.toString('hex') + ' does not appear to be an Integer');
}
val = 0;
for (i = 0; i < len; i++) {
val *= 256;
val += buf[i + 2];
}
if (buf[2] > 127 && type === T.Integer) {
return val - Math.pow(2, 8 * buf[1]);
} else {
return val;
}
};
// Parse a buffer containing a representation of an OctetString.
// Verify the type, then just grab the string out of the buffer.
exports.parseOctetString = function (buf) {
var i, len, lenBytes = 0;
if (buf[0] !== T.OctetString) {
throw new Error('Buffer does not appear to be an OctetString');
}
// SNMP doesn't specify an encoding so I pick UTF-8 as the 'most standard'
// encoding. We'll see if that assumption survives contact with actual reality.
len = buf[1];
if (len > 128) {
// Multi byte length encoding
lenBytes = len - 128;
len = 0;
for (i = 0; i < lenBytes; i++) {
len *= 256;
len += buf[2+i];
}
}
return buf.toString('utf-8', 2 + lenBytes, 2 + lenBytes + len);
};
// Parse a buffer containing a representation of an ObjectIdentifier.
// Verify the type, then apply the relevent encoding rules.
exports.parseOid = function (buf) {
var oid, val, i, o1, o2;
if (buf[0] !== T.ObjectIdentifier) {
throw new Error('Buffer does not appear to be an ObjectIdentifier');
}
// The first byte contains the first two numbers in the OID. They're
// magical! They're compactly encoded in a special way! KILL ME NOW!
o1 = parseInt(buf[2] / 40, 10);
if (o1 > 2) {
o1 = 2;
}
o2 = buf[2] - 40 * o1;
oid = [o1, o2];
// The rest of the data is a base-128-encoded OID
for (i = 0; i < buf[1] - 1; i++) {
val = 0;
while (buf[i + 3] >= 128) {
val += buf[i + 3] - 128;
val *= 128;
i++;
}
val += buf[i + 3];
oid.push(val);
}
return oid;
};
// Parse a buffer containing a representation of an array type.
// This is for example an IpAddress.
exports.parseArray = function (buf) {
var i, nelem, array;
if (buf[0] !== T.IpAddress) {
throw new Error('Buffer does not appear to be an array type.');
}
nelem = buf[1];
array = [];
for (i = 0; i < buf[1]; i++) {
array.push(buf[i + 2]);
}
return array;
};
// Parse a buffer containing a representation of an opaque type.
// This is for example an IpAddress.
exports.parseOpaque = function (buf) {
var hdr;
hdr = typeAndLength(buf);
if (hdr.type !== T.Opaque) {
throw new Error('Buffer does not appear to be an opaque type.');
}
return '0x' + buf.slice(hdr.header).toString('hex');
};
/*globals exports: false*/