UNPKG

snmp-native

Version:

A native Javascript SNMP implementation for Node.js

788 lines (654 loc) 26.5 kB
// Introduction // ----- // This is `node-snmp-native`, a native (Javascript) implementation of an SNMP // client library targeted at Node.js. It's MIT licensed and available at // https://github.com/calmh/node-snmp-native // // (c) 2012 Jakob Borg, Nym Networks "use strict"; // Code // ----- // This file implements a structure representing an SNMP message // and routines for converting to and from the network representation. // Define our external dependencies. var assert = require('assert'); var dgram = require('dgram'); var events = require('events'); // We also need our ASN.1 BER en-/decoding routines. var asn1ber = require('./asn1ber'); exports.PduTypes = asn1ber.pduTypes; exports.DataTypes = asn1ber.types; exports.Errors = asn1ber.errors; var versions = { SNMPv1: 0, SNMPv2c: 1 }; exports.Versions = versions; // Basic structures // ---- // A `VarBind` is the innermost structure, containing an OID-Value pair. function VarBind() { this.type = 5; this.value = null; } // The `PDU` contains the SNMP request or response fields and a list of `VarBinds`. function PDU() { this.type = asn1ber.pduTypes.GetRequestPDU; this.reqid = 1; this.error = 0; this.errorIndex = 0; this.varbinds = [ new VarBind() ]; } // The `Packet` contains the SNMP version and community and the `PDU`. function Packet() { this.version = versions.SNMPv2c; this.community = 'public'; this.pdu = new PDU(); } // Allow consumers to create packet structures from scratch. exports.Packet = Packet; // Private helper functions // ---- // Concatenate several buffers to one. function concatBuffers(buffers) { var total, cur = 0, buf; // First we calculate the total length, total = buffers.reduce(function (tot, b) { return tot + b.length; }, 0); // then we allocate a new Buffer large enough to contain all data, buf = new Buffer(total); buffers.forEach(function (buffer) { // finally we copy the data into the new larger buffer. buffer.copy(buf, cur, 0); cur += buffer.length; }); return buf; } // Clear a pending packet when it times out or is successfully received. function clearRequest(reqs, reqid) { var self = this; var entry = reqs[reqid]; if (entry) { if (entry.timeout) { clearTimeout(entry.timeout); } delete reqs[reqid]; } } // Convert a string formatted OID to an array, leaving anything non-string alone. function parseSingleOid(oid) { if (typeof oid !== 'string') { return oid; } if (oid[0] !== '.') { throw new Error('Invalid OID format'); } oid = oid.split('.') .filter(function (s) { return s.length > 0; }) .map(function (s) { return parseInt(s, 10); }); return oid; } // Fix any OIDs in the 'oid' or 'oids' objects that are passed as strings. function parseOids(options) { if (options.oid) { options.oid = parseSingleOid(options.oid); } if (options.oids) { options.oids = options.oids.map(parseSingleOid); } } // Update targ with attributes from _defs. // Any existing attributes on targ are untouched. function defaults(targ, _defs) { [].slice.call(arguments, 1).forEach(function (def) { Object.keys(def).forEach(function (key) { if (!targ.hasOwnProperty(key)) { targ[key] = def[key]; } }); }); } // Encode structure to ASN.1 BER // ---- // Return an ASN.1 BER encoding of a Packet structure. // This is suitable for transmission on a UDP socket. function encode(pkt) { var version, community, reqid, err, erridx, vbs, pdu, message; // We only support SNMPv1 and SNMPv2c, so enforce those version stamps. if (pkt.version !== versions.SNMPv1 && pkt.version !== versions.SNMPv2c) { throw new Error('Only SNMPv1 and SNMPv2c are supported.'); } // Encode the message header fields. version = asn1ber.encodeInteger(pkt.version); community = asn1ber.encodeOctetString(pkt.community); // Encode the PDU header fields. reqid = asn1ber.encodeInteger(pkt.pdu.reqid); err = asn1ber.encodeInteger(pkt.pdu.error); erridx = asn1ber.encodeInteger(pkt.pdu.errorIndex); // Encode the PDU varbinds. vbs = []; pkt.pdu.varbinds.forEach(function (vb) { var oid = asn1ber.encodeOid(vb.oid), val; if (vb.type === asn1ber.types.Null || vb.value === null) { val = asn1ber.encodeNull(); } else if (vb.type === asn1ber.types.Integer) { val = asn1ber.encodeInteger(vb.value); } else if (vb.type === asn1ber.types.Gauge) { val = asn1ber.encodeGauge(vb.value); } else if (vb.type === asn1ber.types.IpAddress) { val = asn1ber.encodeIpAddress(vb.value); } else if (vb.type === asn1ber.types.OctetString) { val = asn1ber.encodeOctetString(vb.value); } else if (vb.type === asn1ber.types.ObjectIdentifier) { val = asn1ber.encodeOid(vb.value, true); } else if (vb.type === asn1ber.types.Counter) { val = asn1ber.encodeCounter(vb.value); } else if (vb.type === asn1ber.types.TimeTicks) { val = asn1ber.encodeTimeTicks(vb.value); } else if (vb.type === asn1ber.types.NoSuchObject) { val = asn1ber.encodeNoSuchObject(); } else if (vb.type === asn1ber.types.NoSuchInstance) { val = asn1ber.encodeNoSuchInstance(); } else if (vb.type === asn1ber.types.EndOfMibView) { val = asn1ber.encodeEndOfMibView(); } else { throw new Error('Unknown varbind type "' + vb.type + '" in encoding.'); } vbs.push(asn1ber.encodeSequence(concatBuffers([oid, val]))); }); // Concatenate all the varbinds together. vbs = asn1ber.encodeSequence(concatBuffers(vbs)); // Create the PDU by concatenating the inner fields and adding a request structure around it. pdu = asn1ber.encodeRequest(pkt.pdu.type, concatBuffers([reqid, err, erridx, vbs])); // Create the message by concatenating the header fields and the PDU. message = asn1ber.encodeSequence(concatBuffers([version, community, pdu])); return message; } exports.encode = encode; // Parse ASN.1 BER into a structure // ----- // Parse an SNMP packet into its component fields. // We don't do a lot of validation so a malformed packet will probably just // make us blow up. function parse(buf) { var pkt, oid, bvb, vb, hdr, vbhdr; pkt = new Packet(); // First we have a sequence marker (two bytes). // We don't care about those, so cut them off. hdr = asn1ber.typeAndLength(buf); assert.equal(asn1ber.types.Sequence, hdr.type); buf = buf.slice(hdr.header); // Then comes the version field (integer). Parse it and slice it. pkt.version = asn1ber.parseInteger(buf.slice(0, buf[1] + 2)); buf = buf.slice(2 + buf[1]); // We then get the community. Parse and slice. pkt.community = asn1ber.parseOctetString(buf.slice(0, buf[1] + 2)); buf = buf.slice(2 + buf[1]); // Here's the PDU structure. We're interested in the type. Slice the rest. hdr = asn1ber.typeAndLength(buf); assert.ok(hdr.type >= 0xA0); pkt.pdu.type = hdr.type - 0xA0; buf = buf.slice(hdr.header); // The request id field. pkt.pdu.reqid = asn1ber.parseInteger(buf.slice(0, buf[1] + 2)); buf = buf.slice(2 + buf[1]); // The error field. pkt.pdu.error = asn1ber.parseInteger(buf.slice(0, buf[1] + 2)); buf = buf.slice(2 + buf[1]); // The error index field. pkt.pdu.errorIndex = asn1ber.parseInteger(buf.slice(0, buf[1] + 2)); buf = buf.slice(2 + buf[1]); // Here's the varbind list. Not interested. hdr = asn1ber.typeAndLength(buf); assert.equal(asn1ber.types.Sequence, hdr.type); buf = buf.slice(hdr.header); // Now comes the varbinds. There might be many, so we loop for as long as we have data. pkt.pdu.varbinds = []; while (buf[0] === asn1ber.types.Sequence) { vb = new VarBind(); // Slice off the sequence header. hdr = asn1ber.typeAndLength(buf); assert.equal(asn1ber.types.Sequence, hdr.type); bvb = buf.slice(hdr.header, hdr.len + hdr.header); // Parse and save the ObjectIdentifier. vb.oid = asn1ber.parseOid(bvb); // Parse the value. We use the type marker to figure out // what kind of value it is and call the appropriate parser // routine. For the SNMPv2c error types, we simply set the // value to a text representation of the error and leave handling // up to the user. var vb_name_hdr = asn1ber.typeAndLength(bvb); bvb = bvb.slice(vb_name_hdr.header + vb_name_hdr.len); var vb_value_hdr = asn1ber.typeAndLength(bvb); vb.type = vb_value_hdr.type; if (vb.type === asn1ber.types.Null) { // Null type. vb.value = null; } else if (vb.type === asn1ber.types.OctetString) { // Octet string type. vb.value = asn1ber.parseOctetString(bvb); } else if (vb.type === asn1ber.types.Integer || vb.type === asn1ber.types.Counter || vb.type === asn1ber.types.Counter64 || vb.type === asn1ber.types.TimeTicks || vb.type === asn1ber.types.Gauge) { // Integer type and it's derivatives that behave in the same manner. vb.value = asn1ber.parseInteger(bvb); } else if (vb.type === asn1ber.types.ObjectIdentifier) { // Object identifier type. vb.value = asn1ber.parseOid(bvb); } else if (vb.type === asn1ber.types.IpAddress) { // IP Address type. vb.value = asn1ber.parseArray(bvb); } else if (vb.type === asn1ber.types.Opaque) { // Opaque type. The 'parsing' here is very light; basically we return a // string representation of the raw bytes in hex. vb.value = asn1ber.parseOpaque(bvb); } else if (vb.type === asn1ber.types.EndOfMibView) { // End of MIB view error, returned when attempting to GetNext beyond the end // of the current view. vb.value = 'endOfMibView'; } else if (vb.type === asn1ber.types.NoSuchObject) { // No such object error, returned when attempting to Get/GetNext an OID that doesn't exist. vb.value = 'noSuchObject'; } else if (vb.type === asn1ber.types.NoSuchInstance) { // No such instance error, returned when attempting to Get/GetNext an instance // that doesn't exist in a given table. vb.value = 'noSuchInstance'; } else { // Something else that we can't handle, so throw an error. // The error will be caught and presented in a useful manner on stderr, // with a dump of the message causing it. throw new Error('Unrecognized value type ' + vb.type); } // Take the raw octet string value and preseve it as a buffer and hex string. vb.valueRaw = bvb.slice(vb_value_hdr.header, vb_value_hdr.header + vb_value_hdr.len); vb.valueHex = vb.valueRaw.toString('hex'); // Add the request id to the varbind (even though it doesn't really belong) // so that it will be availble to the end user. vb.requestId = pkt.pdu.reqid; // Push whatever we parsed to the varbind list. pkt.pdu.varbinds.push(vb); // Go fetch the next varbind, if there seems to be any. if (buf.length > hdr.header + hdr.len) { buf = buf.slice(hdr.header + hdr.len); } else { break; } } return pkt; } exports.parse = parse; // Utility functions // ----- // Compare two OIDs, returning -1, 0 or +1 depending on the relation between // oidA and oidB. function compareOids (oidA, oidB) { var mlen, i; // The undefined OID, if there is any, is deemed lesser. if (typeof oidA === 'undefined' && typeof oidB !== 'undefined') { return 1; } else if (typeof oidA !== 'undefined' && typeof oidB === 'undefined') { return -1; } // Check each number part of the OIDs individually, and if there is any // position where one OID is larger than the other, return accordingly. // This will only check up to the minimum length of both OIDs. mlen = Math.min(oidA.length, oidB.length); for (i = 0; i < mlen; i++) { if (oidA[i] > oidB[i]) { return -1; } else if (oidB[i] > oidA[i]) { return 1; } } // If there is one OID that is longer than the other after the above comparison, // consider the shorter OID to be lesser. if (oidA.length > oidB.length) { return -1; } else if (oidB.length > oidA.length) { return 1; } else { // The OIDs are obviously equal. return 0; } } exports.compareOids = compareOids; // Communication functions // ----- // This is called for when we receive a message. function msgReceived(msg, rinfo) { var self = this, now = Date.now(), pkt, entry; if (msg.length === 0) { // Not sure why we sometimes receive an empty message. // As far as I'm concerned it shouldn't happen, but we'll ignore it // and if it's necessary a retransmission of the request will be // made later. return; } // Parse the packet, or call the informative // parse error display if we fail. try { pkt = parse(msg); } catch (error) { return self.emit('error', error); } // If this message's request id matches one we've sent, // cancel any outstanding timeout and call the registered // callback. entry = self.reqs[pkt.pdu.reqid]; if (entry) { clearRequest(self.reqs, pkt.pdu.reqid); if (typeof entry.callback === 'function') { if (pkt.pdu.error !== 0) { // An error response should be reported as an error to the callback. // We try to find the error description, or in worst case call it // just "Unknown Error <number>". var errorDescr = Object.keys(asn1ber.errors).filter(function (key) { return asn1ber.errors[key] === pkt.pdu.error; })[0] || 'Unknown Error ' + pkt.pdu.error; return entry.callback(new Error(errorDescr)); } pkt.pdu.varbinds.forEach(function (vb) { vb.receiveStamp = now; vb.sendStamp = entry.sendStamp; }); entry.callback(null, pkt.pdu.varbinds); } } } // Default options for new sessions and operations. exports.defaultOptions = { host: 'localhost', port: 161, bindPort: 0, community: 'public', family: 'udp4', timeouts: [ 5000, 5000, 5000, 5000 ], version: versions.SNMPv2c }; // This creates a new SNMP session. function Session(options) { var self = this; self.options = options || {}; defaults(self.options, exports.defaultOptions); self.reqs = {}; self.socket = dgram.createSocket(self.options.family); self.socket.on('message', self.options.msgReceived || msgReceived.bind(self)); self.socket.on('close', function () { // Remove the socket so we don't try to send a message on // it when it's closed. self.socket = undefined; }); self.socket.on('error', function () { // Errors will be emitted here as well as on the callback to the send function. // We handle them there, so doing anything here is unnecessary. // But having no error handler trips up the test suite. }); // If exclusive is false (default), then cluster workers will use the same underlying handle, // allowing connection handling duties to be shared. // When exclusive is true, the handle is not shared, and attempted port sharing results in an error. self.socket.bind({ port: self.options.bindPort, // unless otherwise specified, get a random port automatically exclusive: true // you should not share the same port, otherwise yours packages will be screwed up between workers }); } // We inherit from EventEmitter so that we can emit error events // on fatal errors. Session.prototype = Object.create(events.EventEmitter.prototype); exports.Session = Session; // Generate a request ID. It's best kept within a signed 32 bit integer. // Uses the current time in ms, shifted left ten bits, plus a counter. // This gives us space for 1 transmit every microsecond and wraps every // ~1000 seconds. This is OK since we only need to keep unique ID:s for in // flight packets and they should be safely timed out by then. Session.prototype.requestId = function () { var self = this, now = Date.now(); if (!self.prevTs) { self.prevTs = now; self.counter = 0; } if (now === self.prevTs) { self.counter += 1; if (self.counter > 1023) { throw new Error('Request ID counter overflow. Adjust algorithm.'); } } else { self.prevTs = now; self.counter = 0; } return ((now & 0x1fffff) << 10) + self.counter; }; // Send a message. Can be used after manually constructing a correct Packet structure. Session.prototype.sendMsg = function (pkt, options, callback) { var self = this, buf, reqid, retrans = 0; defaults(options, self.options); reqid = self.requestId(); pkt.pdu.reqid = reqid; buf = encode(pkt); function transmit() { if (!self.socket || !self.reqs[reqid]) { // The socket has already been closed, perhaps due to an error that ocurred while a timeout // was scheduled. We can't do anything about it now. clearRequest(self.reqs, reqid); return; } else if (!options.timeouts[retrans]){ // If there is no other configured retransmission attempt, we raise a final timeout error clearRequest(self.reqs, reqid); return callback(new Error('Timeout')); } // Send the message. self.socket.send(buf, 0, buf.length, options.port, options.host, function (err, bytes) { var entry = self.reqs[reqid]; if (err) { clearRequest(self.reqs, reqid); return callback(err); } else if (entry) { // Set timeout and record the timer so that we can (attempt to) cancel it when we receive the reply. entry.sendStamp = Date.now(); entry.timeout = setTimeout(transmit, options.timeouts[retrans]); retrans += 1; } }); } // Register the callback to call when we receive a reply. self.reqs[reqid] = { callback: callback }; // Transmit the message. transmit(); }; // Shortcut to create a GetRequest and send it, while registering a callback. // Needs `options.oid` to be an OID in array form. Session.prototype.get = function (options, callback) { var self = this, pkt; defaults(options, self.options); parseOids(options); if (!options.oid) { return callback(null, []); } pkt = new Packet(); pkt.community = options.community; pkt.version = options.version; pkt.pdu.varbinds[0].oid = options.oid; self.sendMsg(pkt, options, callback); }; // Shortcut to create a SetRequest and send it, while registering a callback. // Needs `options.oid` to be an OID in array form, `options.value` to be an // integer and `options.type` to be asn1ber.T.Integer (2). Session.prototype.set = function (options, callback) { var self = this, pkt; defaults(options, self.options); parseOids(options); if (!options.oid) { throw new Error('Missing required option `oid`.'); } else if (options.value === undefined) { throw new Error('Missing required option `value`.'); } else if (!options.type) { throw new Error('Missing required option `type`.'); } pkt = new Packet(); pkt.community = options.community; pkt.version = options.version; pkt.pdu.type = asn1ber.pduTypes.SetRequestPDU; pkt.pdu.varbinds[0].oid = options.oid; pkt.pdu.varbinds[0].type = options.type; pkt.pdu.varbinds[0].value = options.value; self.sendMsg(pkt, options, callback); }; // Shortcut to get all OIDs in the `options.oids` array sequentially. The // callback is called when the entire operation is completed. If // options.abortOnError is truish, an error while getting any of the values // will cause the callback to be called with error status. When // `options.abortOnError` is falsish (the default), any errors will be ignored // and any successfully retrieved values sent to the callback. Session.prototype.getAll = function (options, callback) { var self = this, results = [], combinedTimeoutTimer = null, combinedTimeoutExpired = false; defaults(options, self.options, { abortOnError: false }); parseOids(options); if (!options.oids || options.oids.length === 0) { return callback(null, []); } function getOne(c) { var oid, pkt, m, vb; pkt = new Packet(); pkt.community = options.community; pkt.version = options.version; pkt.pdu.varbinds = []; // Push up to 16 varbinds in the same message. // The number 16 isn't really that magical, it's just a nice round // number that usually seems to fit withing a single packet and gets // accepted by the switches I've tested it on. for (m = 0; m < 16 && c < options.oids.length; m++) { vb = new VarBind(); vb.oid = options.oids[c]; pkt.pdu.varbinds.push(vb); c++; } self.sendMsg(pkt, options, function (err, varbinds) { if (combinedTimeoutExpired) { return; } if (options.abortOnError && err) { clearTimeout(combinedTimeoutTimer); callback(err); } else { if (varbinds) { results = results.concat(varbinds); } if (c < options.oids.length) { getOne(c); } else { clearTimeout(combinedTimeoutTimer); callback(null, results); } } }); } if (options.combinedTimeout) { var combinedTimeoutEvent = function() { combinedTimeoutExpired = true; return callback(new Error('Timeout'), results); }; combinedTimeoutTimer = setTimeout(combinedTimeoutEvent, options.combinedTimeout); } getOne(0); }; // Shortcut to create a GetNextRequest and send it, while registering a callback. // Needs `options.oid` to be an OID in array form. Session.prototype.getNext = function (options, callback) { var self = this, pkt; defaults(options, self.options); parseOids(options); if (!options.oid) { return callback(null, []); } pkt = new Packet(); pkt.community = options.community; pkt.version = options.version; pkt.pdu.type = 1; pkt.pdu.varbinds[0].oid = options.oid; self.sendMsg(pkt, options, callback); }; // Shortcut to get all entries below the specified OID. // The callback will be called once with the list of // varbinds that was collected, or with an error object. // Needs `options.oid` to be an OID in array form. Session.prototype.getSubtree = function (options, callback) { var self = this, vbs = [], combinedTimeoutTimer = null, combinedTimeoutExpired = false; defaults(options, self.options); parseOids(options); if (!options.oid) { return callback(null, []); } options.startOid = options.oid; // Helper to check whether `oid` in inside the tree rooted at // `root` or not. function inTree(root, oid) { var i; if (oid.length <= root.length) { return false; } for (i = 0; i < root.length; i++) { if (oid[i] !== root[i]) { return false; } } return true; } // Helper to handle the result of getNext and call the user's callback // as appropriate. The callback will see one of the following patterns: // - callback([an Error object], undefined) -- an error ocurred. // - callback(null, [a Packet object]) -- data from under the tree. // - callback(null, null) -- end of tree. function result(error, varbinds) { if (combinedTimeoutExpired) { return; } if (error) { clearTimeout(combinedTimeoutTimer); callback(error); } else { if (inTree(options.startOid, varbinds[0].oid)) { if (varbinds[0].value === 'endOfMibView' || varbinds[0].value === 'noSuchObject' || varbinds[0].value === 'noSuchInstance') { clearTimeout(combinedTimeoutTimer); callback(null, vbs); } else if (vbs.length && compareOids(vbs.slice(-1)[0].oid, varbinds[0].oid) !== 1) { return callback(new Error('OID not increasing')); } else { vbs.push(varbinds[0]); var next = { oid: varbinds[0].oid }; defaults(next, options); self.getNext(next, result); } } else { clearTimeout(combinedTimeoutTimer); callback(null, vbs); } } } if (options.combinedTimeout) { var combinedTimeoutEvent = function() { combinedTimeoutExpired = true; return callback(new Error('Timeout'), vbs); }; combinedTimeoutTimer = setTimeout(combinedTimeoutEvent, options.combinedTimeout); } self.getNext(options, result); }; // Close the socket. Necessary to finish the event loop and exit the program. Session.prototype.close = function () { var self = this; for (var reqid in self.reqs) { if (self.reqs[reqid].callback) { self.reqs[reqid].callback(new Error('Cancelled')); } clearRequest(self.reqs, reqid); } this.socket.close(); };