From 2e37b0f803cfdb3b3e2c0b4a68e2a872c35833a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sverre=20Johan=20T=C3=B8vik?= Date: Mon, 21 Mar 2022 12:11:46 +0100 Subject: [PATCH 01/14] Add SMMP v3 option and fields to edit dialog * Added "v3" option to Version popup. * Added fields for context, username, and security settings. * Fields are shown/hidden based on e.g. the selected SNMP version. --- gapit-snmp.html | 72 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/gapit-snmp.html b/gapit-snmp.html index 27dd0e9..acb2c92 100644 --- a/gapit-snmp.html +++ b/gapit-snmp.html @@ -3,19 +3,60 @@ -
- - -
Timeout  S
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
@@ -125,6 +166,7 @@ return this.name ? "node_label_italic" : ""; }, oneditprepare: function () { + // field customization $("#node-input-custom_tags").typedInput({ type:"json", types:["json"] @@ -144,6 +186,28 @@ } ] }); + + ///// dynamic UI + // + // hide fields based on SNMP version + $("#node-input-version").on("change", function() { + let version = $("#node-input-version").val(); + let optionsClass = `.snmpoptions_v${version}`; + console.log() + $(".snmpoptions").hide(); + $(optionsClass).show(); + + }); + + // hide fields based on SNMP security level + $("#node-input-security-level").on("change", function() { + let securityLevel = $("#node-input-security-level").val(); + let levelClass = `.snmpoptions_v3_sec_${securityLevel}`; + console.log() + $(".snmpoptions_v3_sec").hide(); + $(levelClass).show(); + }); + }, }); From e7068792df7bfef2ab79d9864b3a5196b469928a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sverre=20Johan=20T=C3=B8vik?= Date: Mon, 21 Mar 2022 13:42:24 +0100 Subject: [PATCH 02/14] Fix input names --- gapit-snmp.html | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/gapit-snmp.html b/gapit-snmp.html index acb2c92..3015a77 100644 --- a/gapit-snmp.html +++ b/gapit-snmp.html @@ -26,27 +26,27 @@
- -
- -
- - + +
- - @@ -54,8 +54,8 @@
- - + +
From 550f9d40678767b31866ceb2c32ff46b4073e781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sverre=20Johan=20T=C3=B8vik?= Date: Mon, 21 Mar 2022 14:18:59 +0100 Subject: [PATCH 03/14] Define username and keys as credentials Define username, auth_key and priv_key as credentials. Credentials are stored separately, and are not included with flow exports. --- gapit-snmp.html | 5 +++++ gapit-snmp.js | 8 +++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/gapit-snmp.html b/gapit-snmp.html index 3015a77..34103a7 100644 --- a/gapit-snmp.html +++ b/gapit-snmp.html @@ -140,6 +140,11 @@ RED.nodes.registerType('gapit-snmp', { category: 'network-input', color: "YellowGreen", + credentials: { + username: {type:"text"}, + auth_key: {type:"password"}, + priv_key: {type:"password"} + }, defaults: { host: { value: "127.0.0.1" }, community: { value: "public" }, diff --git a/gapit-snmp.js b/gapit-snmp.js index b8e1590..bab6bf5 100644 --- a/gapit-snmp.js +++ b/gapit-snmp.js @@ -724,5 +724,11 @@ module.exports = function (RED) { } }); } - RED.nodes.registerType("gapit-snmp", GapitSnmpNode); + RED.nodes.registerType("gapit-snmp", GapitSnmpNode, { + credentials: { + username: {type:"text"}, + auth_key: {type:"password"}, + priv_key: {type:"password"} + } + }); }; From 6ec86416c81e12d96cab9d8ea74086289b035922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sverre=20Johan=20T=C3=B8vik?= Date: Mon, 21 Mar 2022 16:54:10 +0100 Subject: [PATCH 04/14] Add new edit fields to defaults, to add to config Fields are not added to the node config unless listed in "defaults". --- gapit-snmp.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gapit-snmp.html b/gapit-snmp.html index 34103a7..a371709 100644 --- a/gapit-snmp.html +++ b/gapit-snmp.html @@ -149,6 +149,10 @@ host: { value: "127.0.0.1" }, community: { value: "public" }, version: { value: "1", required: true }, + context: { value: "" }, + security_level: { value: "authPriv" }, + auth_protocol: { value: "sha" }, + priv_protocol: { value: "des" }, tagname_device_name: { value: "device_name", required: true }, tagvalue_device_name: { value: "default_name", required: true }, minion_ids: { value: "" }, From c867510bd669082eab7723d18f70ef4c4837fdd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sverre=20Johan=20T=C3=B8vik?= Date: Mon, 21 Mar 2022 16:55:42 +0100 Subject: [PATCH 05/14] Update field name for security_level "on change" --- gapit-snmp.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gapit-snmp.html b/gapit-snmp.html index a371709..e7d5ff1 100644 --- a/gapit-snmp.html +++ b/gapit-snmp.html @@ -209,8 +209,8 @@ }); // hide fields based on SNMP security level - $("#node-input-security-level").on("change", function() { - let securityLevel = $("#node-input-security-level").val(); + $("#node-input-security_level").on("change", function() { + let securityLevel = $("#node-input-security_level").val(); let levelClass = `.snmpoptions_v3_sec_${securityLevel}`; console.log() $(".snmpoptions_v3_sec").hide(); From 76a20e1484b9a9e32dd6ff2c40a438e81b25eccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sverre=20Johan=20T=C3=B8vik?= Date: Mon, 21 Mar 2022 16:57:49 +0100 Subject: [PATCH 06/14] Remove cruft * Unused/ignored option "selected". * NOOP console.logs --- gapit-snmp.html | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/gapit-snmp.html b/gapit-snmp.html index e7d5ff1..bf56483 100644 --- a/gapit-snmp.html +++ b/gapit-snmp.html @@ -37,7 +37,7 @@
@@ -202,17 +202,14 @@ $("#node-input-version").on("change", function() { let version = $("#node-input-version").val(); let optionsClass = `.snmpoptions_v${version}`; - console.log() $(".snmpoptions").hide(); $(optionsClass).show(); - }); // hide fields based on SNMP security level $("#node-input-security_level").on("change", function() { let securityLevel = $("#node-input-security_level").val(); let levelClass = `.snmpoptions_v3_sec_${securityLevel}`; - console.log() $(".snmpoptions_v3_sec").hide(); $(levelClass).show(); }); From 92309540cb4e5ee9eb30c2e3ed2d666b865a9a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sverre=20Johan=20T=C3=B8vik?= Date: Mon, 21 Mar 2022 17:00:25 +0100 Subject: [PATCH 07/14] Trigger "on change" in oneditprepare, to fix form --- gapit-snmp.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gapit-snmp.html b/gapit-snmp.html index bf56483..52b4101 100644 --- a/gapit-snmp.html +++ b/gapit-snmp.html @@ -214,6 +214,12 @@ $(levelClass).show(); }); + // Trigger change events for initial form setup. + // Doesn't seem to work when called immediately, + // so use a timer (even 1 msec seems to work). + setTimeout(function() { + $("#node-input-version").triggerHandler("change"); + }, 1); }, }); From 719105563ba515a9a1714e8059ddf8779dacd355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sverre=20Johan=20T=C3=B8vik?= Date: Mon, 21 Mar 2022 17:26:30 +0100 Subject: [PATCH 08/14] Set SNMP version by lookup instead of if stmt --- gapit-snmp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gapit-snmp.js b/gapit-snmp.js index bab6bf5..17ac6b7 100644 --- a/gapit-snmp.js +++ b/gapit-snmp.js @@ -259,7 +259,7 @@ module.exports = function (RED) { this.blockTuningSteps = 10; this.config = config; this.multi_device_separator = ";"; - this.version = (config.version === "2c") ? snmp.Version2c : snmp.Version1; + this.version = snmp.Version[config.version]; this.tagname_device_name = config.tagname_device_name.trim(); this.tagvalue_device_name = config.tagvalue_device_name.trim(); // split device_name from config into an array From b7f1b9100b32182b24ea19f1dc3c66626dc7b2d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sverre=20Johan=20T=C3=B8vik?= Date: Mon, 21 Mar 2022 18:13:14 +0100 Subject: [PATCH 09/14] Refactor getSession into a node method The sessions object is still global, so sessions are still shared between different nodes connecting to the same combined host, community and version. --- gapit-snmp.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/gapit-snmp.js b/gapit-snmp.js index 17ac6b7..14432c2 100644 --- a/gapit-snmp.js +++ b/gapit-snmp.js @@ -5,20 +5,6 @@ module.exports = function (RED) { var sessions = {}; - function getSession(host, community, version, timeout) { - var sessionKey = host + ":" + community + ":" + version; - var port = 161; - if (host.indexOf(":") !== -1) { - port = host.split(":")[1]; - host = host.split(":")[0]; - } - if (!(sessionKey in sessions)) { - sessions[sessionKey] = snmp.createSession(host, community, { port:port, version:version, timeout:(timeout || 5000) }); - } - return sessions[sessionKey]; - } - - function varbindsParseCounter64Buffers(varbinds, convertToNumbers=true) { // Counter64 is not directly supported by net-snmp, // but returned as a buffer (array). This function @@ -345,6 +331,20 @@ module.exports = function (RED) { console.info("initializing nonexistent_oids in context (set to empty Array)") nodeContext.set("nonexistent_oids", Array()); + this.getSession = function () { + let host = node.config.host; + var sessionKey = host + ":" + node.config.community + ":" + node.version; + var port = 161; + if (host.indexOf(":") !== -1) { + port = host.split(":")[1]; + host = host.split(":")[0]; + } + if (!(sessionKey in sessions)) { + sessions[sessionKey] = snmp.createSession(host, node.config.community, { port:port, version:node.version, timeout:(node.timeout || 5000) }); + } + return sessions[sessionKey]; + } + this.processVarbinds = function (msg, varbinds) { // parse Counter64 values in varbinds varbindsParseCounter64Buffers(varbinds, node.config.convert_counter64_bigint_to_number); @@ -589,7 +589,7 @@ module.exports = function (RED) { } this.tuneSnmpBlockSize = function (host, community, oids, msg, blockSize) { - getSession(host, community, node.version, node.timeout).get(oids.slice(0, blockSize), function (error, varbinds) { + node.getSession().get(oids.slice(0, blockSize), function (error, varbinds) { if (error) { // error object has .name, .message and, optionally, .status // error.status is only set for RequestFailed, so check @@ -644,7 +644,7 @@ module.exports = function (RED) { for (let blockStart = 0; blockStart < oids.length; blockStart=blockStart+blockSize) { let oidBlock = oids.slice(blockStart, blockStart+blockSize); - getSession(host, community, node.version, node.timeout).get(oidBlock, function (error, blockVarbinds) { + node.getSession().get(oidBlock, function (error, blockVarbinds) { msg.totalBlockResponseCount += oidBlock.length; if (error) { // error object has .name, .message and, optionally, .status From b02159c494efda8ff44a4ea8caff7d124f3edb2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sverre=20Johan=20T=C3=B8vik?= Date: Wed, 23 Mar 2022 10:16:22 +0100 Subject: [PATCH 10/14] "on change" for Security Level in oneditprepare --- gapit-snmp.html | 1 + 1 file changed, 1 insertion(+) diff --git a/gapit-snmp.html b/gapit-snmp.html index 52b4101..91c5213 100644 --- a/gapit-snmp.html +++ b/gapit-snmp.html @@ -219,6 +219,7 @@ // so use a timer (even 1 msec seems to work). setTimeout(function() { $("#node-input-version").triggerHandler("change"); + $("#node-input-security_level").triggerHandler("change"); }, 1); }, }); From 10157db6a50975a471b8966c4538791408369d4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sverre=20Johan=20T=C3=B8vik?= Date: Wed, 23 Mar 2022 10:50:03 +0100 Subject: [PATCH 11/14] Create SNMPv3 sessions when v3 is chosen sessionKey modified to use community or username, depending of the SNMP version used. Options is defined before create[V3]Session, for use with both v3 and v2c/v1 sessions. Context, if configured, is added to options for v3 sessions. New method createV3UserObject creates a user object to the spec of the net-snmp module. --- gapit-snmp.js | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/gapit-snmp.js b/gapit-snmp.js index 14432c2..d7aa1bf 100644 --- a/gapit-snmp.js +++ b/gapit-snmp.js @@ -333,18 +333,49 @@ module.exports = function (RED) { this.getSession = function () { let host = node.config.host; - var sessionKey = host + ":" + node.config.community + ":" + node.version; + let communityOrUsername = (node.version === snmp.Version3) ? node.credentials.username : node.config.community; + var sessionKey = host + ":" + communityOrUsername + ":" + node.version; var port = 161; if (host.indexOf(":") !== -1) { port = host.split(":")[1]; host = host.split(":")[0]; } if (!(sessionKey in sessions)) { - sessions[sessionKey] = snmp.createSession(host, node.config.community, { port:port, version:node.version, timeout:(node.timeout || 5000) }); + console.info(`creating session for ${sessionKey}`); + let options = { port:port, version:node.version, timeout:(node.timeout || 5000) } + if (node.version === snmp.Version3) { + if (node.config.context.trim() != "") { + options.context = node.config.context.trim(); + } + let user = node.createV3UserObject(); + sessions[sessionKey] = snmp.createV3Session(host, user, options); + } + else { + sessions[sessionKey] = snmp.createSession(host, node.config.community, options); + } } return sessions[sessionKey]; } + this.createV3UserObject = function() { + // set up user object for SNMPv3 + // empty object if other version + let user = {} + if (node.version == snmp.Version3) { + user.name = node.credentials.username; + user.level = snmp.SecurityLevel[node.config.security_level]; + if (user.level == snmp.SecurityLevel.authNoPriv || user.level == snmp.SecurityLevel.authPriv) { + user.authProtocol = snmp.AuthProtocols[node.config.auth_protocol]; + user.authKey = node.credentials.auth_key; + } + if (user.level == snmp.SecurityLevel.authPriv) { + user.privProtocol = snmp.PrivProtocols[node.config.priv_protocol]; + user.privKey = node.credentials.priv_key; + } + } + return user; + } + this.processVarbinds = function (msg, varbinds) { // parse Counter64 values in varbinds varbindsParseCounter64Buffers(varbinds, node.config.convert_counter64_bigint_to_number); From 33c22e633539f63cd70c512c51f39948a0019e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sverre=20Johan=20T=C3=B8vik?= Date: Fri, 29 Jul 2022 15:23:10 +0200 Subject: [PATCH 12/14] Cache session key in getSession, more unique v3 keys * Cache generated session key to a node property, don't generate again if this is set and a session exists for the key. * Add a hash of all SNMPv3 options (except username) to the session key for v3 sessions (uses the last 8 characters of a SHA256 hash). --- gapit-snmp.js | 48 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/gapit-snmp.js b/gapit-snmp.js index d7aa1bf..dea815b 100644 --- a/gapit-snmp.js +++ b/gapit-snmp.js @@ -2,6 +2,7 @@ module.exports = function (RED) { "use strict"; var snmp = require("net-snmp"); + var crypto = require("crypto"); var sessions = {}; @@ -332,28 +333,47 @@ module.exports = function (RED) { nodeContext.set("nonexistent_oids", Array()); this.getSession = function () { + // check if session already exists, return if it does + if (node.sessionKey !== undefined && node.sessionKey in sessions) { + console.debug(`Found existing session for ${node.sessionKey}`); + return sessions[node.sessionKey]; + } + let host = node.config.host; - let communityOrUsername = (node.version === snmp.Version3) ? node.credentials.username : node.config.community; - var sessionKey = host + ":" + communityOrUsername + ":" + node.version; + let sessionKey; + if (node.version === snmp.Version3) { + // include (a hash of) all v3 options to create a unique session key + let v3OptionsHash = crypto.createHash('sha256').update( + node.config.security_level + + node.config.auth_protocol + + node.credentials.auth_key + + node.config.priv_protocol + + node.credentials.priv_key).digest('hex').slice(-8) + sessionKey = host + ":" + node.credentials.username + ":" + node.version + ":" + v3OptionsHash; + } + else { // not SNMPv3 + sessionKey = host + ":" + node.config.community + ":" + node.version; + } var port = 161; if (host.indexOf(":") !== -1) { port = host.split(":")[1]; host = host.split(":")[0]; } - if (!(sessionKey in sessions)) { - console.info(`creating session for ${sessionKey}`); - let options = { port:port, version:node.version, timeout:(node.timeout || 5000) } - if (node.version === snmp.Version3) { - if (node.config.context.trim() != "") { - options.context = node.config.context.trim(); - } - let user = node.createV3UserObject(); - sessions[sessionKey] = snmp.createV3Session(host, user, options); - } - else { - sessions[sessionKey] = snmp.createSession(host, node.config.community, options); + + console.info(`Creating session for ${sessionKey}`); + let options = { port:port, version:node.version, timeout:(node.timeout || 5000) } + if (node.version === snmp.Version3) { + if (node.config.context.trim() != "") { + options.context = node.config.context.trim(); } + let user = node.createV3UserObject(); + sessions[sessionKey] = snmp.createV3Session(host, user, options); } + else { + sessions[sessionKey] = snmp.createSession(host, node.config.community, options); + } + + node.sessionKey = sessionKey; return sessions[sessionKey]; } From 32b977ffa3e4a2e104ca9640c3f3bf8ab7023807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sverre=20Johan=20T=C3=B8vik?= Date: Fri, 29 Jul 2022 15:31:48 +0200 Subject: [PATCH 13/14] Close session when node closes On deploy, if an SNMP node's config has changed, the sessions for all nodes in the same flow will be closed. These sessions _might_ also be used by other nodes, but new sessions would just be created by those nodes. --- gapit-snmp.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/gapit-snmp.js b/gapit-snmp.js index dea815b..be90c65 100644 --- a/gapit-snmp.js +++ b/gapit-snmp.js @@ -396,6 +396,14 @@ module.exports = function (RED) { return user; } + this.closeSession = function () { + if (node.sessionKey !== undefined && node.sessionKey in sessions) { + console.debug(`Closing session ${node.sessionKey}`); + sessions[node.sessionKey].close(); + delete sessions[node.sessionKey]; + } + } + this.processVarbinds = function (msg, varbinds) { // parse Counter64 values in varbinds varbindsParseCounter64Buffers(varbinds, node.config.convert_counter64_bigint_to_number); @@ -774,6 +782,10 @@ module.exports = function (RED) { node.warn("No oid(s) to search for"); } }); + + this.on("close", function() { + node.closeSession(); + }); } RED.nodes.registerType("gapit-snmp", GapitSnmpNode, { credentials: { From 1dd749ca2530f8abc097c5a79ce733e6ec8a2b79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sverre=20Johan=20T=C3=B8vik?= Date: Fri, 29 Jul 2022 15:33:34 +0200 Subject: [PATCH 14/14] Add session error handler in getSession --- gapit-snmp.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gapit-snmp.js b/gapit-snmp.js index be90c65..8f7e8ba 100644 --- a/gapit-snmp.js +++ b/gapit-snmp.js @@ -373,6 +373,11 @@ module.exports = function (RED) { sessions[sessionKey] = snmp.createSession(host, node.config.community, options); } + sessions[sessionKey].on("error", function (error) { + console.log ("Session error: " + error.toString()); + node.closeSession(); + }) + node.sessionKey = sessionKey; return sessions[sessionKey]; }