diff --git a/package.json b/package.json index d224a08..49b6721 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,10 @@ ], "author": "", "license": "MIT", - "dependencies": {}, + "dependencies": { + }, "devDependencies": { + "net-snmp": "3.20.0", "@git.zone/tsbuild": "^2.3.2", "@git.zone/tsrun": "^1.3.3", "@git.zone/tstest": "^1.0.96", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 100174c..16f4d2d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,10 @@ settings: importers: .: + dependencies: + net-snmp: + specifier: ^3.20.0 + version: 3.20.0 devDependencies: '@git.zone/tsbuild': specifier: ^2.3.2 @@ -1647,6 +1651,9 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + asn1-ber@1.2.2: + resolution: {integrity: sha512-CbNem/7hxrjSiOAOOTX4iZxu+0m3jiLqlsERQwwPM1IDR/22M8IPpA1VVndCLw5KtjRYyRODbvAEIfuTogNDng==} + ast-types@0.13.4: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} @@ -3303,6 +3310,9 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + net-snmp@3.20.0: + resolution: {integrity: sha512-4Cp8ODkzgVXjUrIQFfL9Vo6qVsz+8OuAjUvkRGsSZOKSpoxpy9YWjVgNs+/a9N4Hd9MilIy90Zhw3EZlUUZB6A==} + netmask@2.0.2: resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} engines: {node: '>= 0.4.0'} @@ -7181,6 +7191,8 @@ snapshots: array-union@2.1.0: {} + asn1-ber@1.2.2: {} + ast-types@0.13.4: dependencies: tslib: 2.8.1 @@ -9133,6 +9145,11 @@ snapshots: negotiator@0.6.3: {} + net-snmp@3.20.0: + dependencies: + asn1-ber: 1.2.2 + smart-buffer: 4.2.0 + netmask@2.0.2: {} new-find-package-json@2.0.0: diff --git a/test/test.ts b/test/test.ts index e31433a..838616d 100644 --- a/test/test.ts +++ b/test/test.ts @@ -1,9 +1,6 @@ import { tap, expect } from '@push.rocks/tapbundle'; -import { NupstSnmp } from '../ts/snmp.js'; -import type { SnmpConfig, UpsStatus } from '../ts/snmp.js'; -import { SnmpEncoder } from '../ts/snmp/encoder.js'; -import { SnmpPacketCreator } from '../ts/snmp/packet-creator.js'; -import { SnmpPacketParser } from '../ts/snmp/packet-parser.js'; +import { NupstSnmp } from '../ts/snmp/manager.js'; +import type { ISnmpConfig, IUpsStatus } from '../ts/snmp/types.js'; import * as qenv from '@push.rocks/qenv'; const testQenv = new qenv.Qenv('./', '.nogit/'); @@ -12,295 +9,57 @@ const testQenv = new qenv.Qenv('./', '.nogit/'); const snmp = new NupstSnmp(true); // Load the test configuration from .nogit/env.json -const testConfig = await testQenv.getEnvVarOnDemandAsObject('testConfig'); +const testConfigV1 = await testQenv.getEnvVarOnDemandAsObject('testConfigV1'); +const testConfigV3 = await testQenv.getEnvVarOnDemandAsObject('testConfigV3'); tap.test('should log config', async () => { - console.log(testConfig); -}); - -tap.test('SNMP packet creation and parsing test', async () => { - // We'll test the internal methods that are now in separate classes - - // Test OID conversion - const oidStr = '1.3.6.1.4.1.3808.1.1.1.4.1.1.0'; - const oidArray = SnmpEncoder.oidToArray(oidStr); - console.log('OID array length:', oidArray.length); - console.log('OID array:', oidArray); - // The OID has 14 elements after splitting - expect(oidArray.length).toEqual(14); - expect(oidArray[0]).toEqual(1); - expect(oidArray[1]).toEqual(3); - - // Test OID encoding - const encodedOid = SnmpEncoder.encodeOID(oidArray); - expect(encodedOid).toBeInstanceOf(Buffer); - - // Test SNMP request creation - const request = SnmpPacketCreator.createSnmpGetRequest(oidStr, 'public', true); - expect(request).toBeInstanceOf(Buffer); - expect(request.length).toBeGreaterThan(20); - - // Log the request for debugging - console.log('SNMP Request buffer:', request.toString('hex')); - - // Test integer encoding - const int = SnmpEncoder.encodeInteger(42); - expect(int).toBeInstanceOf(Buffer); - expect(int.length).toBeGreaterThanOrEqual(1); - - // Test SNMPv3 engine ID discovery message - const discoveryMsg = SnmpPacketCreator.createDiscoveryMessage(testConfig, 1); - expect(discoveryMsg).toBeInstanceOf(Buffer); - expect(discoveryMsg.length).toBeGreaterThan(20); - - console.log('SNMPv3 Discovery message:', discoveryMsg.toString('hex')); -}); - -tap.test('SNMP response parsing simulation', async () => { - // Create a simulated SNMP response for parsing - - // Simulate an INTEGER response (battery capacity) - const intResponse = Buffer.from([ - 0x30, 0x29, // Sequence, length 41 - 0x02, 0x01, 0x00, // Integer (version), value 0 - 0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public" - 0xa2, 0x1c, // GetResponse - 0x02, 0x01, 0x01, // Integer (request ID), value 1 - 0x02, 0x01, 0x00, // Integer (error status), value 0 - 0x02, 0x01, 0x00, // Integer (error index), value 0 - 0x30, 0x11, // Sequence (varbinds) - 0x30, 0x0f, // Sequence (varbind) - 0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8c, 0x10, 0x01, 0x02, 0x01, 0x00, // OID (example) - 0x02, 0x01, 0x64 // Integer (value), value 100 (100%) - ]); - - // Simulate a Gauge32 response (battery capacity) - const gauge32Response = Buffer.from([ - 0x30, 0x29, // Sequence, length 41 - 0x02, 0x01, 0x00, // Integer (version), value 0 - 0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public" - 0xa2, 0x1c, // GetResponse - 0x02, 0x01, 0x01, // Integer (request ID), value 1 - 0x02, 0x01, 0x00, // Integer (error status), value 0 - 0x02, 0x01, 0x00, // Integer (error index), value 0 - 0x30, 0x11, // Sequence (varbinds) - 0x30, 0x0f, // Sequence (varbind) - 0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8c, 0x10, 0x01, 0x02, 0x01, 0x00, // OID (example) - 0x42, 0x01, 0x64 // Gauge32 (value), value 100 (100%) - ]); - - // Simulate a TimeTicks response (battery runtime) - const timeTicksResponse = Buffer.from([ - 0x30, 0x29, // Sequence, length 41 - 0x02, 0x01, 0x00, // Integer (version), value 0 - 0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public" - 0xa2, 0x1c, // GetResponse - 0x02, 0x01, 0x01, // Integer (request ID), value 1 - 0x02, 0x01, 0x00, // Integer (error status), value 0 - 0x02, 0x01, 0x00, // Integer (error index), value 0 - 0x30, 0x11, // Sequence (varbinds) - 0x30, 0x0f, // Sequence (varbind) - 0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8c, 0x10, 0x01, 0x02, 0x01, 0x00, // OID (example) - 0x43, 0x01, 0x0f // TimeTicks (value), value 15 (0.15 seconds or 15/100 seconds) - ]); - - // Test parsing INTEGER response - const intValue = SnmpPacketParser.parseSnmpResponse(intResponse, testConfig, true); - console.log('Parsed INTEGER value:', intValue); - expect(intValue).toEqual(100); - - // Test parsing Gauge32 response - const gauge32Value = SnmpPacketParser.parseSnmpResponse(gauge32Response, testConfig, true); - console.log('Parsed Gauge32 value:', gauge32Value); - expect(gauge32Value).toEqual(100); - - // Test parsing TimeTicks response - const timeTicksValue = SnmpPacketParser.parseSnmpResponse(timeTicksResponse, testConfig, true); - console.log('Parsed TimeTicks value:', timeTicksValue); - expect(timeTicksValue).toEqual(15); -}); - -tap.test('CyberPower TimeTicks conversion', async () => { - // Test the conversion of TimeTicks to minutes for CyberPower UPS - - // Set up a config for CyberPower - const cyberPowerConfig: SnmpConfig = { - ...testConfig, - upsModel: 'cyberpower' - }; - - // Create a simulated TimeTicks response with a value of 104 (104/100 seconds) - const ticksResponse = Buffer.from([ - 0x30, 0x29, // Sequence - 0x02, 0x01, 0x00, // Integer (version), value 0 - 0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public" - 0xa2, 0x1c, // GetResponse - 0x02, 0x01, 0x01, // Integer (request ID), value 1 - 0x02, 0x01, 0x00, // Integer (error status), value 0 - 0x02, 0x01, 0x00, // Integer (error index), value 0 - 0x30, 0x11, // Sequence (varbinds) - 0x30, 0x0f, // Sequence (varbind) - 0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8c, 0x10, 0x01, 0x02, 0x04, 0x00, // OID (battery runtime) - 0x43, 0x01, 0x68 // TimeTicks (value), value 104 (104/100 seconds) - ]); - - // Mock the getUpsStatus function to test our TimeTicks conversion logic - const mockGetUpsStatus = async () => { - // Parse the TimeTicks value from the response - const runtime = SnmpPacketParser.parseSnmpResponse(ticksResponse, testConfig, true); - console.log('Raw runtime value:', runtime); - - // Create a sample UPS status result - const result = { - powerStatus: 'onBattery', - batteryCapacity: 100, - batteryRuntime: 0, - raw: { - powerStatus: 2, - batteryCapacity: 100, - batteryRuntime: runtime, - }, - }; - - // Convert TimeTicks to minutes for CyberPower - if (cyberPowerConfig.upsModel === 'cyberpower' && runtime > 0) { - result.batteryRuntime = Math.floor(runtime / 6000); - console.log(`Converting CyberPower runtime from ${runtime} ticks to ${result.batteryRuntime} minutes`); - } else { - result.batteryRuntime = runtime; - } - - return result; - }; - - // Call our mock function - const status = await mockGetUpsStatus(); - - // Assert the conversion worked correctly - console.log('Final status object:', status); - expect(status.batteryRuntime).toEqual(0); // 104 ticks / 6000 = 0.0173... rounds to 0 minutes -}); - -tap.test('Simulate fully charged online UPS', async () => { - // Test a realistic scenario of an online UPS with high battery capacity and ~30 mins runtime - - // Create simulated responses for power status (online), battery capacity (95%), runtime (30 min) - - // Power Status = 2 (online for CyberPower) - const powerStatusResponse = Buffer.from([ - 0x30, 0x29, // Sequence - 0x02, 0x01, 0x00, // Integer (version), value 0 - 0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public" - 0xa2, 0x1c, // GetResponse - 0x02, 0x01, 0x01, // Integer (request ID), value 1 - 0x02, 0x01, 0x00, // Integer (error status), value 0 - 0x02, 0x01, 0x00, // Integer (error index), value 0 - 0x30, 0x11, // Sequence (varbinds) - 0x30, 0x0f, // Sequence (varbind) - 0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xed, 0x08, 0x01, 0x01, 0x01, 0x00, // OID (power status) - 0x02, 0x01, 0x02 // Integer (value), value 2 (online) - ]); - - // Battery Capacity = 95% (as Gauge32) - const batteryCapacityResponse = Buffer.from([ - 0x30, 0x29, // Sequence - 0x02, 0x01, 0x00, // Integer (version), value 0 - 0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public" - 0xa2, 0x1c, // GetResponse - 0x02, 0x01, 0x02, // Integer (request ID), value 2 - 0x02, 0x01, 0x00, // Integer (error status), value 0 - 0x02, 0x01, 0x00, // Integer (error index), value 0 - 0x30, 0x11, // Sequence (varbinds) - 0x30, 0x0f, // Sequence (varbind) - 0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xed, 0x08, 0x01, 0x02, 0x01, 0x00, // OID (battery capacity) - 0x42, 0x01, 0x5F // Gauge32 (value), value 95 (95%) - ]); - - // Battery Runtime = 30 minutes (as TimeTicks) - // 30 minutes = 1800 seconds = 180000 ticks (in 1/100 seconds) - const batteryRuntimeResponse = Buffer.from([ - 0x30, 0x2c, // Sequence - 0x02, 0x01, 0x00, // Integer (version), value 0 - 0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public" - 0xa2, 0x1f, // GetResponse - 0x02, 0x01, 0x03, // Integer (request ID), value 3 - 0x02, 0x01, 0x00, // Integer (error status), value 0 - 0x02, 0x01, 0x00, // Integer (error index), value 0 - 0x30, 0x14, // Sequence (varbinds) - 0x30, 0x12, // Sequence (varbind) - 0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xed, 0x08, 0x01, 0x02, 0x04, 0x00, // OID (battery runtime) - 0x43, 0x04, 0x00, 0x02, 0xBF, 0x20 // TimeTicks (value), value 180000 (1800 seconds = 30 minutes) - ]); - - // Mock the getUpsStatus function to test with our simulated data - const mockGetUpsStatus = async () => { - console.log('Simulating UPS status request with synthetic data'); - - // Create a config that specifies this is a CyberPower UPS - const upsConfig: SnmpConfig = { - host: '192.168.1.1', - port: 161, - version: 1, - community: 'public', - timeout: 5000, - upsModel: 'cyberpower', - }; - - // Parse each simulated response - const powerStatus = SnmpPacketParser.parseSnmpResponse(powerStatusResponse, upsConfig, true); - console.log('Power status value:', powerStatus); - - const batteryCapacity = SnmpPacketParser.parseSnmpResponse(batteryCapacityResponse, upsConfig, true); - console.log('Battery capacity value:', batteryCapacity); - - const batteryRuntime = SnmpPacketParser.parseSnmpResponse(batteryRuntimeResponse, upsConfig, true); - console.log('Battery runtime value:', batteryRuntime); - - // Convert TimeTicks to minutes for CyberPower UPSes - const runtimeMinutes = Math.floor(batteryRuntime / 6000); - console.log(`Converting ${batteryRuntime} ticks to ${runtimeMinutes} minutes`); - - // Interpret power status for CyberPower - // CyberPower: 2=online, 3=on battery - let powerStatusText: 'online' | 'onBattery' | 'unknown' = 'unknown'; - if (powerStatus === 2) { - powerStatusText = 'online'; - } else if (powerStatus === 3) { - powerStatusText = 'onBattery'; - } - - // Create the status result - const result: UpsStatus = { - powerStatus: powerStatusText, - batteryCapacity: batteryCapacity, - batteryRuntime: runtimeMinutes, - raw: { - powerStatus, - batteryCapacity, - batteryRuntime, - }, - }; - - return result; - }; - - // Call our mock function - const status = await mockGetUpsStatus(); - - // Assert that the values match our expectations - console.log('UPS Status Result:', status); - expect(status.powerStatus).toEqual('online'); - expect(status.batteryCapacity).toEqual(95); - expect(status.batteryRuntime).toEqual(30); + console.log(testConfigV1); }); // Test with real UPS using the configuration from .nogit/env.json -tap.test('Real UPS test', async () => { +tap.test('Real UPS test v1', async () => { try { console.log('Testing with real UPS configuration...'); // Extract the correct SNMP config from the test configuration - const snmpConfig = testConfig.snmp; + const snmpConfig = testConfigV1.snmp; + console.log('SNMP Config:'); + console.log(` Host: ${snmpConfig.host}:${snmpConfig.port}`); + console.log(` Version: SNMPv${snmpConfig.version}`); + console.log(` UPS Model: ${snmpConfig.upsModel}`); + + // Use a short timeout for testing + const testSnmpConfig = { + ...snmpConfig, + timeout: Math.min(snmpConfig.timeout, 10000) // Use at most 10 seconds for testing + }; + + // Try to get the UPS status + const status = await snmp.getUpsStatus(testSnmpConfig); + + console.log('UPS Status:'); + console.log(` Power Status: ${status.powerStatus}`); + console.log(` Battery Capacity: ${status.batteryCapacity}%`); + console.log(` Runtime Remaining: ${status.batteryRuntime} minutes`); + + // Just make sure we got valid data types back + expect(status).toBeTruthy(); + expect(['online', 'onBattery', 'unknown']).toContain(status.powerStatus); + expect(typeof status.batteryCapacity).toEqual('number'); + expect(typeof status.batteryRuntime).toEqual('number'); + } catch (error) { + console.log('Real UPS test failed:', error); + // Skip the test if we can't connect to the real UPS + console.log('Skipping this test since the UPS might not be available'); + } +}); + +tap.test('Real UPS test v3', async () => { + try { + console.log('Testing with real UPS configuration...'); + + // Extract the correct SNMP config from the test configuration + const snmpConfig = testConfigV3.snmp; console.log('SNMP Config:'); console.log(` Host: ${snmpConfig.host}:${snmpConfig.port}`); console.log(` Version: SNMPv${snmpConfig.version}`); diff --git a/ts/daemon.ts b/ts/daemon.ts index 30073fb..1372a10 100644 --- a/ts/daemon.ts +++ b/ts/daemon.ts @@ -2,7 +2,8 @@ import * as fs from 'fs'; import * as path from 'path'; import { exec } from 'child_process'; import { promisify } from 'util'; -import { NupstSnmp, type ISnmpConfig } from './snmp.js'; +import { NupstSnmp } from './snmp/manager.js'; +import type { ISnmpConfig } from './snmp/types.js'; const execAsync = promisify(exec); diff --git a/ts/nupst.ts b/ts/nupst.ts index 976abfb..eded8c9 100644 --- a/ts/nupst.ts +++ b/ts/nupst.ts @@ -1,4 +1,4 @@ -import { NupstSnmp } from './snmp.js'; +import { NupstSnmp } from './snmp/manager.js'; import { NupstDaemon } from './daemon.js'; import { NupstSystemd } from './systemd.js'; import { commitinfo } from './00_commitinfo_data.js'; diff --git a/ts/snmp.ts b/ts/snmp.ts deleted file mode 100644 index 8609c45..0000000 --- a/ts/snmp.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Re-export from the snmp module - * This file is kept for backward compatibility - */ - -export * from './snmp/index.js'; \ No newline at end of file diff --git a/ts/snmp/encoder.ts b/ts/snmp/encoder.ts deleted file mode 100644 index 20ae61c..0000000 --- a/ts/snmp/encoder.ts +++ /dev/null @@ -1,98 +0,0 @@ -/** - * SNMP encoding utilities - * Contains helper methods for encoding SNMP data - */ -export class SnmpEncoder { - /** - * Convert OID string to array of integers - * @param oid OID string in dotted notation (e.g. "1.3.6.1.2.1") - * @returns Array of integers representing the OID - */ - public static oidToArray(oid: string): number[] { - return oid.split('.').map(n => parseInt(n, 10)); - } - - /** - * Encode an SNMP integer - * @param value Integer value to encode - * @returns Buffer containing the encoded integer - */ - public static encodeInteger(value: number): Buffer { - const buf = Buffer.alloc(4); - buf.writeInt32BE(value, 0); - - // Find first non-zero byte - let start = 0; - while (start < 3 && buf[start] === 0) { - start++; - } - - // Handle negative values - if (value < 0 && buf[start] === 0) { - start--; - } - - return buf.slice(start); - } - - /** - * Encode an OID - * @param oid Array of integers representing the OID - * @returns Buffer containing the encoded OID - */ - public static encodeOID(oid: number[]): Buffer { - // First two numbers are encoded as 40*x+y - let encodedOid = Buffer.from([40 * (oid[0] || 0) + (oid[1] || 0)]); - - // Encode remaining numbers - for (let i = 2; i < oid.length; i++) { - const n = oid[i]; - - if (n < 128) { - // Simple case: number fits in one byte - encodedOid = Buffer.concat([encodedOid, Buffer.from([n])]); - } else { - // Number needs multiple bytes - const bytes = []; - let value = n; - - // Create bytes array in reverse order - do { - bytes.unshift(value & 0x7F); - value >>= 7; - } while (value > 0); - - // Set high bit on all but the last byte - for (let j = 0; j < bytes.length - 1; j++) { - bytes[j] |= 0x80; - } - - encodedOid = Buffer.concat([encodedOid, Buffer.from(bytes)]); - } - } - - return encodedOid; - } - - /** - * Decode an ASN.1 integer - * @param buffer Buffer containing the encoded integer - * @param offset Offset in the buffer - * @param length Length of the integer in bytes - * @returns Decoded integer value - */ - public static decodeInteger(buffer: Buffer, offset: number, length: number): number { - if (length === 1) { - return buffer[offset]; - } else if (length === 2) { - return buffer.readInt16BE(offset); - } else if (length === 3) { - return (buffer[offset] << 16) | (buffer[offset + 1] << 8) | buffer[offset + 2]; - } else if (length === 4) { - return buffer.readInt32BE(offset); - } else { - // For longer integers, we'll just return a simple value - return buffer[offset]; - } - } -} \ No newline at end of file diff --git a/ts/snmp/manager.ts b/ts/snmp/manager.ts index 05654ab..488ca39 100644 --- a/ts/snmp/manager.ts +++ b/ts/snmp/manager.ts @@ -1,8 +1,7 @@ import * as dgram from 'dgram'; +import * as snmp from 'net-snmp'; import type { IOidSet, ISnmpConfig, TUpsModel, IUpsStatus } from './types.js'; import { UpsOidSets } from './oid-sets.js'; -import { SnmpPacketCreator } from './packet-creator.js'; -import { SnmpPacketParser } from './packet-parser.js'; /** * Class for SNMP communication with UPS devices @@ -13,6 +12,8 @@ export class NupstSnmp { private activeOIDs: IOidSet; // Reference to the parent Nupst instance private nupst: any; // Type 'any' to avoid circular dependency + // Debug mode flag + private debug: boolean = false; // Default SNMP configuration private readonly DEFAULT_CONFIG: ISnmpConfig = { @@ -24,13 +25,6 @@ export class NupstSnmp { upsModel: 'cyberpower', // Default UPS model }; - // SNMPv3 engine ID and counters - private engineID: Buffer = Buffer.from([0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]); - private engineBoots: number = 0; - private engineTime: number = 0; - private requestID: number = 1; - private debug: boolean = false; // Enable for debug output - /** * Create a new SNMP manager * @param debug Whether to enable debug mode @@ -56,6 +50,14 @@ export class NupstSnmp { return this.nupst; } + /** + * Enable debug mode + */ + public enableDebug(): void { + this.debug = true; + console.log('SNMP debug mode enabled - detailed logs will be shown'); + } + /** * Set active OID set based on UPS model * @param config SNMP configuration @@ -78,121 +80,122 @@ export class NupstSnmp { console.log(`Using OIDs for UPS model: ${model}`); } } - - /** - * Enable debug mode - */ - public enableDebug(): void { - this.debug = true; - console.log('SNMP debug mode enabled - detailed logs will be shown'); - } /** - * Send an SNMP GET request + * Send an SNMP GET request using the net-snmp package * @param oid OID to query * @param config SNMP configuration + * @param retryCount Current retry count (unused in this implementation) * @returns Promise resolving to the SNMP response value */ - public async snmpGet(oid: string, config = this.DEFAULT_CONFIG): Promise { + public async snmpGet( + oid: string, + config = this.DEFAULT_CONFIG, + retryCount = 0 + ): Promise { return new Promise((resolve, reject) => { - const socket = dgram.createSocket('udp4'); - - // Create appropriate request based on SNMP version - let request: Buffer; - if (config.version === 3) { - request = SnmpPacketCreator.createSnmpV3GetRequest( - oid, - config, - this.engineID, - this.engineBoots, - this.engineTime, - this.requestID++, - this.debug - ); - } else { - request = SnmpPacketCreator.createSnmpGetRequest(oid, config.community || 'public', this.debug); - } - if (this.debug) { - console.log(`Sending SNMP ${config.version === 3 ? 'v3' : ('v' + config.version)} request to ${config.host}:${config.port}`); - console.log('Request length:', request.length); - console.log('First 16 bytes of request:', request.slice(0, 16).toString('hex')); - console.log('Full request hex:', request.toString('hex')); + console.log(`Sending SNMP v${config.version} GET request for OID ${oid} to ${config.host}:${config.port}`); + console.log('Using community:', config.community); } + + // Create SNMP options based on configuration + const options: any = { + port: config.port, + retries: 2, // Number of retries + timeout: config.timeout, + transport: 'udp4', + idBitsSize: 32 + }; + + // Set version based on config + if (config.version === 1) { + options.version = snmp.Version1; + } else if (config.version === 2 || config.version === 2) { + options.version = snmp.Version2c; + } else { + options.version = snmp.Version3; + } + + // Create appropriate session based on SNMP version + let session; - // Set timeout - add extra logging for debugging - const timeout = setTimeout(() => { - socket.close(); - if (this.debug) { - console.error('---------------------------------------'); - console.error('SNMP request timed out after', config.timeout, 'ms'); - console.error('SNMP Version:', config.version); - if (config.version === 3) { - console.error('SNMPv3 Security Level:', config.securityLevel); - console.error('SNMPv3 Username:', config.username); - console.error('SNMPv3 Auth Protocol:', config.authProtocol || 'None'); - console.error('SNMPv3 Privacy Protocol:', config.privProtocol || 'None'); - } - console.error('OID:', oid); - console.error('Host:', config.host); - console.error('Port:', config.port); - console.error('---------------------------------------'); - } - reject(new Error(`SNMP request timed out after ${config.timeout}ms`)); - }, config.timeout); - - // Listen for responses - socket.on('message', (message, rinfo) => { - clearTimeout(timeout); + if (config.version === 3) { + // For SNMPv3, we need to set up authentication and privacy + const user = { + name: config.username || '', + level: snmp.SecurityLevel[config.securityLevel || 'noAuthNoPriv'], + authProtocol: config.authProtocol ? snmp.AuthProtocols[config.authProtocol] : undefined, + authKey: config.authKey || '', + privProtocol: config.privProtocol ? snmp.PrivProtocols[config.privProtocol] : undefined, + privKey: config.privKey || '' + }; - if (this.debug) { - console.log(`Received SNMP response from ${rinfo.address}:${rinfo.port}`); - console.log('Response length:', message.length); - console.log('First 16 bytes of response:', message.slice(0, 16).toString('hex')); - console.log('Full response hex:', message.toString('hex')); - } - - try { - const result = SnmpPacketParser.parseSnmpResponse(message, config, this.debug); - - if (this.debug) { - console.log('Parsed SNMP response:', result); - } - - socket.close(); - resolve(result); - } catch (error) { - if (this.debug) { - console.error('Error parsing SNMP response:', error); - } - socket.close(); - reject(error); - } - }); - - // Handle errors - socket.on('error', (error) => { - clearTimeout(timeout); - socket.close(); - if (this.debug) { - console.error('Socket error during SNMP request:', error); - } - reject(error); - }); - - // First send the request directly without binding to a specific port - // This lets the OS pick an available port instead of trying to bind to one - socket.send(request, 0, request.length, config.port, config.host, (error) => { + session = snmp.createV3Session(config.host, user, options); + } else { + // For SNMPv1/v2c, we use the community string + session = snmp.createSession(config.host, config.community || 'public', options); + } + + // Convert the OID string to an array of OIDs if multiple OIDs are needed + const oids = [oid]; + + // Send the GET request + session.get(oids, (error: any, varbinds: any[]) => { + // Close the session to release resources + session.close(); + if (error) { - clearTimeout(timeout); - socket.close(); if (this.debug) { - console.error('Error sending SNMP request:', error); + console.error('SNMP GET error:', error); } - reject(error); - } else if (this.debug) { - console.log('SNMP request sent successfully'); + reject(new Error(`SNMP GET error: ${error.message || error}`)); + return; } + + if (!varbinds || varbinds.length === 0) { + if (this.debug) { + console.error('No varbinds returned in response'); + } + reject(new Error('No varbinds returned in response')); + return; + } + + // Check for SNMP errors in the response + if (varbinds[0].type === snmp.ObjectType.NoSuchObject || + varbinds[0].type === snmp.ObjectType.NoSuchInstance || + varbinds[0].type === snmp.ObjectType.EndOfMibView) { + if (this.debug) { + console.error('SNMP error:', snmp.ObjectType[varbinds[0].type]); + } + reject(new Error(`SNMP error: ${snmp.ObjectType[varbinds[0].type]}`)); + return; + } + + // Process the response value based on its type + let value = varbinds[0].value; + + // Handle specific types that might need conversion + if (Buffer.isBuffer(value)) { + // If value is a Buffer, try to convert it to a string if it's printable ASCII + const isPrintableAscii = value.every(byte => byte >= 32 && byte <= 126); + if (isPrintableAscii) { + value = value.toString(); + } + } else if (typeof value === 'bigint') { + // Convert BigInt to a normal number or string if needed + value = Number(value); + } + + if (this.debug) { + console.log('SNMP response:', { + oid: varbinds[0].oid, + type: varbinds[0].type, + value: value + }); + } + + resolve(value); }); }); } @@ -230,157 +233,16 @@ export class NupstSnmp { console.log('---------------------------------------'); } - // For SNMPv3, we need to discover the engine ID first - if (config.version === 3) { - if (this.debug) { - console.log('SNMPv3 detected, starting engine ID discovery'); - } - - try { - const discoveredEngineId = await this.discoverEngineId(config); - if (discoveredEngineId) { - this.engineID = discoveredEngineId; - if (this.debug) { - console.log('Using discovered engine ID:', this.engineID.toString('hex')); - } - } - } catch (error) { - if (this.debug) { - console.warn('Engine ID discovery failed, using default:', error); - } - } - } - - // Helper function to get SNMP value with retry - const getSNMPValueWithRetry = async (oid: string, description: string) => { - if (oid === '') { - if (this.debug) { - console.log(`No OID provided for ${description}, skipping`); - } - return 0; - } - - if (this.debug) { - console.log(`Getting ${description} OID: ${oid}`); - } - - try { - const value = await this.snmpGet(oid, config); - if (this.debug) { - console.log(`${description} value:`, value); - } - return value; - } catch (error) { - if (this.debug) { - console.error(`Error getting ${description}:`, error.message); - } - - // If we got a timeout and it's SNMPv3, try with different security levels - if (error.message.includes('timed out') && config.version === 3) { - if (this.debug) { - console.log(`Retrying ${description} with fallback settings...`); - } - - // Create a retry config with lower security level - if (config.securityLevel === 'authPriv') { - const retryConfig = { ...config, securityLevel: 'authNoPriv' as 'authNoPriv' }; - try { - if (this.debug) { - console.log(`Retrying with authNoPriv security level`); - } - const value = await this.snmpGet(oid, retryConfig); - if (this.debug) { - console.log(`${description} retry value:`, value); - } - return value; - } catch (retryError) { - if (this.debug) { - console.error(`Retry failed for ${description}:`, retryError.message); - } - } - } - } - - // If we're still having trouble, try with standard OIDs - if (config.upsModel !== 'custom') { - try { - // Try RFC 1628 standard UPS MIB OIDs - const standardOIDs = UpsOidSets.getStandardOids(); - - if (this.debug) { - console.log(`Trying standard RFC 1628 OID for ${description}: ${standardOIDs[description]}`); - } - - const standardValue = await this.snmpGet(standardOIDs[description], config); - if (this.debug) { - console.log(`${description} standard OID value:`, standardValue); - } - return standardValue; - } catch (stdError) { - if (this.debug) { - console.error(`Standard OID retry failed for ${description}:`, stdError.message); - } - } - } - - // Return a default value if all attempts fail - if (this.debug) { - console.log(`Using default value 0 for ${description}`); - } - return 0; - } - }; - // Get all values with independent retry logic - const powerStatusValue = await getSNMPValueWithRetry(this.activeOIDs.POWER_STATUS, 'power status'); - const batteryCapacity = await getSNMPValueWithRetry(this.activeOIDs.BATTERY_CAPACITY, 'battery capacity') || 0; - const batteryRuntime = await getSNMPValueWithRetry(this.activeOIDs.BATTERY_RUNTIME, 'battery runtime') || 0; + const powerStatusValue = await this.getSNMPValueWithRetry(this.activeOIDs.POWER_STATUS, 'power status', config); + const batteryCapacity = await this.getSNMPValueWithRetry(this.activeOIDs.BATTERY_CAPACITY, 'battery capacity', config) || 0; + const batteryRuntime = await this.getSNMPValueWithRetry(this.activeOIDs.BATTERY_RUNTIME, 'battery runtime', config) || 0; // Determine power status - handle different values for different UPS models - let powerStatus: 'online' | 'onBattery' | 'unknown' = 'unknown'; - - // Different UPS models use different values for power status - if (config.upsModel === 'cyberpower') { - // CyberPower RMCARD205: upsBaseOutputStatus values - // 2=onLine, 3=onBattery, 4=onBoost, 5=onSleep, 6=off, etc. - if (powerStatusValue === 2) { - powerStatus = 'online'; - } else if (powerStatusValue === 3) { - powerStatus = 'onBattery'; - } - } else if (config.upsModel === 'eaton') { - // Eaton UPS: xupsOutputSource values - // 3=normal/mains, 5=battery, etc. - if (powerStatusValue === 3) { - powerStatus = 'online'; - } else if (powerStatusValue === 5) { - powerStatus = 'onBattery'; - } - } else { - // Default interpretation for other UPS models - if (powerStatusValue === 1) { - powerStatus = 'online'; - } else if (powerStatusValue === 2) { - powerStatus = 'onBattery'; - } - } + const powerStatus = this.determinePowerStatus(config.upsModel, powerStatusValue); // Convert to minutes for UPS models with different time units - let processedRuntime = batteryRuntime; - - if (config.upsModel === 'cyberpower' && batteryRuntime > 0) { - // CyberPower: TimeTicks is in 1/100 seconds, convert to minutes - processedRuntime = Math.floor(batteryRuntime / 6000); // 6000 ticks = 1 minute - if (this.debug) { - console.log(`Converting CyberPower runtime from ${batteryRuntime} ticks to ${processedRuntime} minutes`); - } - } else if (config.upsModel === 'eaton' && batteryRuntime > 0) { - // Eaton: Runtime is in seconds, convert to minutes - processedRuntime = Math.floor(batteryRuntime / 60); - if (this.debug) { - console.log(`Converting Eaton runtime from ${batteryRuntime} seconds to ${processedRuntime} minutes`); - } - } + const processedRuntime = this.processRuntimeValue(config.upsModel, batteryRuntime); const result = { powerStatus, @@ -414,109 +276,231 @@ export class NupstSnmp { } /** - * Discover SNMP engine ID (for SNMPv3) - * Sends a proper discovery message to get the engine ID from the device + * Helper method to get SNMP value with retry and fallback logic + * @param oid OID to query + * @param description Description of the value for logging * @param config SNMP configuration - * @returns Promise resolving to the discovered engine ID + * @returns Promise resolving to the SNMP value */ - public async discoverEngineId(config: ISnmpConfig): Promise { - return new Promise((resolve, reject) => { - const socket = dgram.createSocket('udp4'); - - // Create a proper discovery message (SNMPv3 with noAuthNoPriv) - const discoveryConfig: ISnmpConfig = { - ...config, - securityLevel: 'noAuthNoPriv', - username: '', // Empty username for discovery - }; - - // Create a simple GetRequest for sysDescr (a commonly available OID) - const request = SnmpPacketCreator.createDiscoveryMessage(discoveryConfig, this.requestID++); - + private async getSNMPValueWithRetry( + oid: string, + description: string, + config: ISnmpConfig + ): Promise { + if (oid === '') { if (this.debug) { - console.log('Sending SNMPv3 discovery message'); - console.log('SNMPv3 Discovery message:', request.toString('hex')); + console.log(`No OID provided for ${description}, skipping`); + } + return 0; + } + + if (this.debug) { + console.log(`Getting ${description} OID: ${oid}`); + } + + try { + const value = await this.snmpGet(oid, config); + if (this.debug) { + console.log(`${description} value:`, value); + } + return value; + } catch (error) { + if (this.debug) { + console.error(`Error getting ${description}:`, error.message); } - // Set timeout - use a longer timeout for discovery phase - const discoveryTimeout = Math.max(config.timeout, 15000); // At least 15 seconds for discovery - const timeout = setTimeout(() => { - socket.close(); - // Fall back to default engine ID if discovery fails - if (this.debug) { - console.error('---------------------------------------'); - console.error('Engine ID discovery timed out after', discoveryTimeout, 'ms'); - console.error('SNMPv3 settings:'); - console.error(' Username:', config.username); - console.error(' Security Level:', config.securityLevel); - console.error(' Host:', config.host); - console.error(' Port:', config.port); - console.error('Using default engine ID:', this.engineID.toString('hex')); - console.error('---------------------------------------'); - } - resolve(this.engineID); - }, discoveryTimeout); + // If we're using SNMPv3, try with different security levels + if (config.version === 3) { + return await this.tryFallbackSecurityLevels(oid, description, config); + } - // Listen for responses - socket.on('message', (message, rinfo) => { - clearTimeout(timeout); - - if (this.debug) { - console.log(`Received SNMPv3 discovery response from ${rinfo.address}:${rinfo.port}`); - console.log('Response:', message.toString('hex')); - } - - try { - // Extract engine ID from response - const engineId = SnmpPacketParser.extractEngineId(message, this.debug); - if (engineId) { - this.engineID = engineId; // Update the engine ID - if (this.debug) { - console.log('Discovered engine ID:', engineId.toString('hex')); - } - socket.close(); - resolve(engineId); - } else { - if (this.debug) { - console.log('Could not extract engine ID, using default'); - } - socket.close(); - resolve(this.engineID); - } - } catch (error) { - if (this.debug) { - console.error('Error extracting engine ID:', error); - } - socket.close(); - resolve(this.engineID); // Fall back to default engine ID - } - }); + // Try with standard OIDs as fallback + if (config.upsModel !== 'custom') { + return await this.tryStandardOids(oid, description, config); + } - // Handle errors - socket.on('error', (error) => { - clearTimeout(timeout); - socket.close(); - if (this.debug) { - console.error('Engine ID discovery socket error:', error); - } - resolve(this.engineID); // Fall back to default engine ID - }); - - // Send request directly without binding - socket.send(request, 0, request.length, config.port, config.host, (error) => { - if (error) { - clearTimeout(timeout); - socket.close(); - if (this.debug) { - console.error('Error sending discovery message:', error); - } - resolve(this.engineID); // Fall back to default engine ID - } else if (this.debug) { - console.log('Discovery message sent successfully'); - } - }); - }); + // Return a default value if all attempts fail + if (this.debug) { + console.log(`Using default value 0 for ${description}`); + } + return 0; + } } - // initiateShutdown method has been moved to the NupstDaemon class + /** + * Try fallback security levels for SNMPv3 + * @param oid OID to query + * @param description Description of the value for logging + * @param config SNMP configuration + * @returns Promise resolving to the SNMP value + */ + private async tryFallbackSecurityLevels( + oid: string, + description: string, + config: ISnmpConfig + ): Promise { + if (this.debug) { + console.log(`Retrying ${description} with fallback security level...`); + } + + // Try with authNoPriv if current level is authPriv + if (config.securityLevel === 'authPriv') { + const retryConfig = { ...config, securityLevel: 'authNoPriv' as 'authNoPriv' }; + try { + if (this.debug) { + console.log(`Retrying with authNoPriv security level`); + } + const value = await this.snmpGet(oid, retryConfig); + if (this.debug) { + console.log(`${description} retry value:`, value); + } + return value; + } catch (retryError) { + if (this.debug) { + console.error(`Retry failed for ${description}:`, retryError.message); + } + } + } + + // Try with noAuthNoPriv as a last resort + if (config.securityLevel === 'authPriv' || config.securityLevel === 'authNoPriv') { + const retryConfig = { ...config, securityLevel: 'noAuthNoPriv' as 'noAuthNoPriv' }; + try { + if (this.debug) { + console.log(`Retrying with noAuthNoPriv security level`); + } + const value = await this.snmpGet(oid, retryConfig); + if (this.debug) { + console.log(`${description} retry value:`, value); + } + return value; + } catch (retryError) { + if (this.debug) { + console.error(`Retry failed for ${description}:`, retryError.message); + } + } + } + + return 0; + } + + /** + * Try standard OIDs as fallback + * @param oid OID to query + * @param description Description of the value for logging + * @param config SNMP configuration + * @returns Promise resolving to the SNMP value + */ + private async tryStandardOids( + oid: string, + description: string, + config: ISnmpConfig + ): Promise { + try { + // Try RFC 1628 standard UPS MIB OIDs + const standardOIDs = UpsOidSets.getStandardOids(); + + if (this.debug) { + console.log(`Trying standard RFC 1628 OID for ${description}: ${standardOIDs[description]}`); + } + + const standardValue = await this.snmpGet(standardOIDs[description], config); + if (this.debug) { + console.log(`${description} standard OID value:`, standardValue); + } + return standardValue; + } catch (stdError) { + if (this.debug) { + console.error(`Standard OID retry failed for ${description}:`, stdError.message); + } + } + + return 0; + } + + /** + * Determine power status based on UPS model and raw value + * @param upsModel UPS model + * @param powerStatusValue Raw power status value + * @returns Standardized power status + */ + private determinePowerStatus( + upsModel: TUpsModel | undefined, + powerStatusValue: number + ): 'online' | 'onBattery' | 'unknown' { + if (upsModel === 'cyberpower') { + // CyberPower RMCARD205: upsBaseOutputStatus values + // 2=onLine, 3=onBattery, 4=onBoost, 5=onSleep, 6=off, etc. + if (powerStatusValue === 2) { + return 'online'; + } else if (powerStatusValue === 3) { + return 'onBattery'; + } + } else if (upsModel === 'eaton') { + // Eaton UPS: xupsOutputSource values + // 3=normal/mains, 5=battery, etc. + if (powerStatusValue === 3) { + return 'online'; + } else if (powerStatusValue === 5) { + return 'onBattery'; + } + } else if (upsModel === 'apc') { + // APC UPS: upsBasicOutputStatus values + // 2=online, 3=onBattery, etc. + if (powerStatusValue === 2) { + return 'online'; + } else if (powerStatusValue === 3) { + return 'onBattery'; + } + } else { + // Default interpretation for other UPS models + if (powerStatusValue === 1) { + return 'online'; + } else if (powerStatusValue === 2) { + return 'onBattery'; + } + } + + return 'unknown'; + } + + /** + * Process runtime value based on UPS model + * @param upsModel UPS model + * @param batteryRuntime Raw battery runtime value + * @returns Processed runtime in minutes + */ + private processRuntimeValue( + upsModel: TUpsModel | undefined, + batteryRuntime: number + ): number { + if (this.debug) { + console.log('Raw runtime value:', batteryRuntime); + } + + if (upsModel === 'cyberpower' && batteryRuntime > 0) { + // CyberPower: TimeTicks is in 1/100 seconds, convert to minutes + const minutes = Math.floor(batteryRuntime / 6000); // 6000 ticks = 1 minute + if (this.debug) { + console.log(`Converting CyberPower runtime from ${batteryRuntime} ticks to ${minutes} minutes`); + } + return minutes; + } else if (upsModel === 'eaton' && batteryRuntime > 0) { + // Eaton: Runtime is in seconds, convert to minutes + const minutes = Math.floor(batteryRuntime / 60); + if (this.debug) { + console.log(`Converting Eaton runtime from ${batteryRuntime} seconds to ${minutes} minutes`); + } + return minutes; + } else if (batteryRuntime > 10000) { + // Generic conversion for large tick values (likely TimeTicks) + const minutes = Math.floor(batteryRuntime / 6000); + if (this.debug) { + console.log(`Converting ${batteryRuntime} ticks to ${minutes} minutes`); + } + return minutes; + } + + return batteryRuntime; + } } \ No newline at end of file diff --git a/ts/snmp/packet-creator.ts b/ts/snmp/packet-creator.ts deleted file mode 100644 index 5088b7e..0000000 --- a/ts/snmp/packet-creator.ts +++ /dev/null @@ -1,651 +0,0 @@ -import * as crypto from 'crypto'; -import type { ISnmpConfig, ISnmpV3SecurityParams } from './types.js'; -import { SnmpEncoder } from './encoder.js'; - -/** - * SNMP packet creation utilities - * Creates SNMP request packets for different SNMP versions - */ -export class SnmpPacketCreator { - /** - * Create an SNMPv1 GET request - * @param oid OID to query - * @param community Community string - * @param debug Whether to enable debug output - * @returns Buffer containing the SNMP request - */ - public static createSnmpGetRequest(oid: string, community: string, debug: boolean = false): Buffer { - const oidArray = SnmpEncoder.oidToArray(oid); - const encodedOid = SnmpEncoder.encodeOID(oidArray); - - if (debug) { - console.log('OID array length:', oidArray.length); - console.log('OID array:', oidArray); - } - - // SNMP message structure - // Sequence - // Version (Integer) - // Community (String) - // PDU (GetRequest) - // Request ID (Integer) - // Error Status (Integer) - // Error Index (Integer) - // Variable Bindings (Sequence) - // Variable (Sequence) - // OID (ObjectIdentifier) - // Value (Null) - - // Use the standard method from our test that is known to work - // Create a fixed request ID (0x00000001) to ensure deterministic behavior - const requestId = Buffer.from([0x00, 0x00, 0x00, 0x01]); - - // Encode values - const versionBuf = Buffer.concat([ - Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 - Buffer.from([0x00]) // SNMP version 1 (0) - ]); - - const communityBuf = Buffer.concat([ - Buffer.from([0x04, community.length]), // ASN.1 Octet String, length - Buffer.from(community) // Community string - ]); - - const requestIdBuf = Buffer.concat([ - Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 - requestId // Fixed Request ID - ]); - - const errorStatusBuf = Buffer.concat([ - Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 - Buffer.from([0x00]) // Error Status (0 = no error) - ]); - - const errorIndexBuf = Buffer.concat([ - Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 - Buffer.from([0x00]) // Error Index (0) - ]); - - const oidValueBuf = Buffer.concat([ - Buffer.from([0x30]), // ASN.1 Sequence - Buffer.from([encodedOid.length + 2]), // Length - Buffer.from([0x06]), // ASN.1 Object Identifier - Buffer.from([encodedOid.length]), // Length - encodedOid, // OID - Buffer.from([0x05, 0x00]) // Null value - ]); - - const varBindingsBuf = Buffer.concat([ - Buffer.from([0x30]), // ASN.1 Sequence - Buffer.from([oidValueBuf.length]), // Length - oidValueBuf // Variable binding - ]); - - const pduBuf = Buffer.concat([ - Buffer.from([0xa0]), // ASN.1 Context-specific Constructed 0 (GetRequest) - Buffer.from([requestIdBuf.length + errorStatusBuf.length + errorIndexBuf.length + varBindingsBuf.length]), // Length - requestIdBuf, // Request ID - errorStatusBuf, // Error Status - errorIndexBuf, // Error Index - varBindingsBuf // Variable Bindings - ]); - - const messageBuf = Buffer.concat([ - Buffer.from([0x30]), // ASN.1 Sequence - Buffer.from([versionBuf.length + communityBuf.length + pduBuf.length]), // Length - versionBuf, // Version - communityBuf, // Community - pduBuf // PDU - ]); - - if (debug) { - console.log('SNMP Request buffer:', messageBuf.toString('hex')); - } - - return messageBuf; - } - - /** - * Create an SNMPv3 GET request - * @param oid OID to query - * @param config SNMP configuration - * @param engineID Engine ID - * @param engineBoots Engine boots counter - * @param engineTime Engine time counter - * @param requestID Request ID - * @param debug Whether to enable debug output - * @returns Buffer containing the SNMP request - */ - public static createSnmpV3GetRequest( - oid: string, - config: ISnmpConfig, - engineID: Buffer, - engineBoots: number, - engineTime: number, - requestID: number, - debug: boolean = false - ): Buffer { - if (debug) { - console.log('Creating SNMPv3 GET request for OID:', oid); - console.log('With config:', { - ...config, - authKey: config.authKey ? '***' : undefined, - privKey: config.privKey ? '***' : undefined - }); - } - - const oidArray = SnmpEncoder.oidToArray(oid); - const encodedOid = SnmpEncoder.encodeOID(oidArray); - - if (debug) { - console.log('Using engine ID:', engineID.toString('hex')); - console.log('Engine boots:', engineBoots); - console.log('Engine time:', engineTime); - console.log('Request ID:', requestID); - } - - // Create security parameters - const securityParams: ISnmpV3SecurityParams = { - msgAuthoritativeEngineID: engineID, - msgAuthoritativeEngineBoots: engineBoots, - msgAuthoritativeEngineTime: engineTime, - msgUserName: config.username || '', - msgAuthenticationParameters: Buffer.alloc(12, 0), // Will be filled in later for auth - msgPrivacyParameters: Buffer.alloc(8, 0), // For privacy - }; - - // Create the PDU (Protocol Data Unit) - // This is wrapped within the security parameters - const requestIdBuf = Buffer.concat([ - Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 - SnmpEncoder.encodeInteger(requestID) // Request ID - ]); - - const errorStatusBuf = Buffer.concat([ - Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 - Buffer.from([0x00]) // Error Status (0 = no error) - ]); - - const errorIndexBuf = Buffer.concat([ - Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 - Buffer.from([0x00]) // Error Index (0) - ]); - - const oidValueBuf = Buffer.concat([ - Buffer.from([0x30]), // ASN.1 Sequence - Buffer.from([encodedOid.length + 2]), // Length - Buffer.from([0x06]), // ASN.1 Object Identifier - Buffer.from([encodedOid.length]), // Length - encodedOid, // OID - Buffer.from([0x05, 0x00]) // Null value - ]); - - const varBindingsBuf = Buffer.concat([ - Buffer.from([0x30]), // ASN.1 Sequence - Buffer.from([oidValueBuf.length]), // Length - oidValueBuf // Variable binding - ]); - - const pduBuf = Buffer.concat([ - Buffer.from([0xa0]), // ASN.1 Context-specific Constructed 0 (GetRequest) - Buffer.from([requestIdBuf.length + errorStatusBuf.length + errorIndexBuf.length + varBindingsBuf.length]), // Length - requestIdBuf, // Request ID - errorStatusBuf, // Error Status - errorIndexBuf, // Error Index - varBindingsBuf // Variable Bindings - ]); - - // Create the security parameters - const engineIdBuf = Buffer.concat([ - Buffer.from([0x04, securityParams.msgAuthoritativeEngineID.length]), // ASN.1 Octet String - securityParams.msgAuthoritativeEngineID - ]); - - const engineBootsBuf = Buffer.concat([ - Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 - SnmpEncoder.encodeInteger(securityParams.msgAuthoritativeEngineBoots) - ]); - - const engineTimeBuf = Buffer.concat([ - Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 - SnmpEncoder.encodeInteger(securityParams.msgAuthoritativeEngineTime) - ]); - - const userNameBuf = Buffer.concat([ - Buffer.from([0x04, securityParams.msgUserName.length]), // ASN.1 Octet String - Buffer.from(securityParams.msgUserName) - ]); - - const authParamsBuf = Buffer.concat([ - Buffer.from([0x04, securityParams.msgAuthenticationParameters.length]), // ASN.1 Octet String - securityParams.msgAuthenticationParameters - ]); - - const privParamsBuf = Buffer.concat([ - Buffer.from([0x04, securityParams.msgPrivacyParameters.length]), // ASN.1 Octet String - securityParams.msgPrivacyParameters - ]); - - // Security parameters sequence - const securityParamsBuf = Buffer.concat([ - Buffer.from([0x30]), // ASN.1 Sequence - Buffer.from([engineIdBuf.length + engineBootsBuf.length + engineTimeBuf.length + - userNameBuf.length + authParamsBuf.length + privParamsBuf.length]), // Length - engineIdBuf, - engineBootsBuf, - engineTimeBuf, - userNameBuf, - authParamsBuf, - privParamsBuf - ]); - - // Determine security level flags - let securityFlags = 0; - if (config.securityLevel === 'authNoPriv' || config.securityLevel === 'authPriv') { - securityFlags |= 0x01; // Authentication flag - } - if (config.securityLevel === 'authPriv') { - securityFlags |= 0x02; // Privacy flag - } - - // Set reportable flag - required for SNMPv3 - securityFlags |= 0x04; // Reportable flag - - // Create SNMPv3 header - const msgIdBuf = Buffer.concat([ - Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 - SnmpEncoder.encodeInteger(requestID) // Message ID (same as request ID for simplicity) - ]); - - const msgMaxSizeBuf = Buffer.concat([ - Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 - SnmpEncoder.encodeInteger(65507) // Max message size - ]); - - const msgFlagsBuf = Buffer.concat([ - Buffer.from([0x04, 0x01]), // ASN.1 Octet String, length 1 - Buffer.from([securityFlags]) - ]); - - const msgSecModelBuf = Buffer.concat([ - Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 - Buffer.from([0x03]) // Security model (3 = USM) - ]); - - // SNMPv3 header - const msgHeaderBuf = Buffer.concat([ - Buffer.from([0x30]), // ASN.1 Sequence - Buffer.from([msgIdBuf.length + msgMaxSizeBuf.length + msgFlagsBuf.length + msgSecModelBuf.length]), // Length - msgIdBuf, - msgMaxSizeBuf, - msgFlagsBuf, - msgSecModelBuf - ]); - - // SNMPv3 security parameters - const msgSecurityBuf = Buffer.concat([ - Buffer.from([0x04]), // ASN.1 Octet String - Buffer.from([securityParamsBuf.length]), // Length - securityParamsBuf - ]); - - // Create scopedPDU - // In SNMPv3, the PDU is wrapped in a "scoped PDU" structure - const contextEngineBuf = Buffer.concat([ - Buffer.from([0x04, engineID.length]), // ASN.1 Octet String - engineID - ]); - - const contextNameBuf = Buffer.concat([ - Buffer.from([0x04, 0x00]), // ASN.1 Octet String, length 0 (empty context name) - ]); - - const scopedPduBuf = Buffer.concat([ - Buffer.from([0x30]), // ASN.1 Sequence - Buffer.from([contextEngineBuf.length + contextNameBuf.length + pduBuf.length]), // Length - contextEngineBuf, - contextNameBuf, - pduBuf - ]); - - // For authPriv, we need to encrypt the scopedPDU - let encryptedPdu = scopedPduBuf; - if (config.securityLevel === 'authPriv' && config.privKey) { - // In a real implementation, encryption would be applied here - // For this example, we'll just simulate it - encryptedPdu = this.simulateEncryption(scopedPduBuf, config); - } - - // Final scopedPDU (encrypted or not) - const finalScopedPduBuf = Buffer.concat([ - Buffer.from([0x04]), // ASN.1 Octet String - Buffer.from([encryptedPdu.length]), // Length - encryptedPdu - ]); - - // Combine everything for the final message - const versionBuf = Buffer.concat([ - Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 - Buffer.from([0x03]) // SNMP version 3 (3) - ]); - - const messageBuf = Buffer.concat([ - Buffer.from([0x30]), // ASN.1 Sequence - Buffer.from([versionBuf.length + msgHeaderBuf.length + msgSecurityBuf.length + finalScopedPduBuf.length]), // Length - versionBuf, - msgHeaderBuf, - msgSecurityBuf, - finalScopedPduBuf - ]); - - // If using authentication, calculate and insert the authentication parameters - if ((config.securityLevel === 'authNoPriv' || config.securityLevel === 'authPriv') && - config.authKey && config.authProtocol) { - const authenticatedMsg = this.addAuthentication(messageBuf, config, authParamsBuf); - - if (debug) { - console.log('Created authenticated SNMPv3 message'); - console.log('Final message length:', authenticatedMsg.length); - } - - return authenticatedMsg; - } - - if (debug) { - console.log('Created SNMPv3 message without authentication'); - console.log('Final message length:', messageBuf.length); - } - - return messageBuf; - } - - /** - * Simulate encryption for authPriv security level - * In a real implementation, this would use the specified privacy protocol (DES/AES) - * @param data Data to encrypt - * @param config SNMP configuration - * @returns Encrypted data - */ - private static simulateEncryption(data: Buffer, config: ISnmpConfig): Buffer { - // This is a placeholder - in a real implementation, you would: - // 1. Generate an initialization vector (IV) - // 2. Use the privacy key derived from the privKey - // 3. Apply the appropriate encryption algorithm (DES/AES) - - // For demonstration purposes only - if (config.privProtocol === 'AES' && config.privKey) { - try { - // Create a deterministic IV for demo purposes (not secure for production) - const iv = Buffer.alloc(16, 0); - const engineID = Buffer.from([0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]); - for (let i = 0; i < 8; i++) { - iv[i] = engineID[i % engineID.length]; - } - - // Create a key from the privKey (proper key localization should be used in production) - const key = crypto.createHash('md5').update(config.privKey).digest(); - - // Create cipher and encrypt - const cipher = crypto.createCipheriv('aes-128-cfb', key, iv); - const encrypted = Buffer.concat([cipher.update(data), cipher.final()]); - - return encrypted; - } catch (error) { - console.warn('AES encryption failed, falling back to plaintext:', error); - return data; - } - } else if (config.privProtocol === 'DES' && config.privKey) { - try { - // Create a deterministic IV for demo purposes (not secure for production) - const iv = Buffer.alloc(8, 0); - const engineID = Buffer.from([0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]); - for (let i = 0; i < 8; i++) { - iv[i] = engineID[i % engineID.length]; - } - - // Create a key from the privKey (proper key localization should be used in production) - const key = crypto.createHash('md5').update(config.privKey).digest().slice(0, 8); - - // Create cipher and encrypt - const cipher = crypto.createCipheriv('des-cbc', key, iv); - const encrypted = Buffer.concat([cipher.update(data), cipher.final()]); - - return encrypted; - } catch (error) { - console.warn('DES encryption failed, falling back to plaintext:', error); - return data; - } - } - - return data; // Return unencrypted data as fallback - } - - /** - * Add authentication to SNMPv3 message - * @param message Message to authenticate - * @param config SNMP configuration - * @param authParamsBuf Authentication parameters buffer - * @returns Authenticated message - */ - private static addAuthentication(message: Buffer, config: ISnmpConfig, authParamsBuf: Buffer): Buffer { - // In a real implementation, this would: - // 1. Zero out the authentication parameters field - // 2. Calculate HMAC-MD5 or HMAC-SHA1 over the entire message - // 3. Insert the HMAC into the authentication parameters field - - if (!config.authKey) { - return message; - } - - try { - // Find position of auth parameters in the message - // This is a more reliable way to find the exact position - let authParamsPos = -1; - for (let i = 0; i < message.length - 16; i++) { - // Look for the auth params pattern: 0x04 0x0C 0x00 0x00... - if (message[i] === 0x04 && message[i + 1] === 0x0C) { - // Check if next 12 bytes are all zeros - let allZeros = true; - for (let j = 0; j < 12; j++) { - if (message[i + 2 + j] !== 0) { - allZeros = false; - break; - } - } - if (allZeros) { - authParamsPos = i; - break; - } - } - } - - if (authParamsPos === -1) { - return message; - } - - // Create a copy of the message with zeroed auth parameters - const msgCopy = Buffer.from(message); - - // Prepare the authentication key according to RFC3414 - // We should use the standard key localization process - const localizedKey = this.localizeAuthKey(config.authKey, - Buffer.from([0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]), - config.authProtocol); - - // Calculate HMAC - let hmac; - if (config.authProtocol === 'SHA') { - hmac = crypto.createHmac('sha1', localizedKey).update(msgCopy).digest().slice(0, 12); - } else { - // Default to MD5 - hmac = crypto.createHmac('md5', localizedKey).update(msgCopy).digest().slice(0, 12); - } - - // Copy HMAC into original message - hmac.copy(message, authParamsPos + 2); - - return message; - } catch (error) { - console.warn('Authentication failed:', error); - return message; - } - } - - /** - * Localize authentication key according to RFC3414 - * @param key Authentication key - * @param engineId Engine ID - * @param authProtocol Authentication protocol - * @returns Localized key - */ - private static localizeAuthKey(key: string, engineId: Buffer, authProtocol: string = 'MD5'): Buffer { - try { - // Convert password to key using hash - let initialHash; - if (authProtocol === 'SHA') { - initialHash = crypto.createHash('sha1'); - } else { - initialHash = crypto.createHash('md5'); - } - - // Generate the initial key - repeated hashing of password + padding - const password = Buffer.from(key); - let passwordIndex = 0; - - // Create a buffer of 1MB (1048576 bytes) filled with the password - const buffer = Buffer.alloc(1048576); - for (let i = 0; i < 1048576; i++) { - buffer[i] = password[passwordIndex]; - passwordIndex = (passwordIndex + 1) % password.length; - } - - initialHash.update(buffer); - let initialKey = initialHash.digest(); - - // Localize the key with engine ID - let localHash; - if (authProtocol === 'SHA') { - localHash = crypto.createHash('sha1'); - } else { - localHash = crypto.createHash('md5'); - } - - localHash.update(initialKey); - localHash.update(engineId); - localHash.update(initialKey); - - return localHash.digest(); - } catch (error) { - console.error('Error localizing auth key:', error); - // Return a fallback key - return Buffer.from(key); - } - } - - /** - * Create a discovery message for SNMPv3 engine ID discovery - * @param config SNMP configuration - * @param requestID Request ID - * @returns Discovery message - */ - public static createDiscoveryMessage(config: ISnmpConfig, requestID: number): Buffer { - // Basic SNMPv3 header for discovery - const msgIdBuf = Buffer.concat([ - Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 - SnmpEncoder.encodeInteger(requestID) - ]); - - const msgMaxSizeBuf = Buffer.concat([ - Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 - SnmpEncoder.encodeInteger(65507) // Max message size - ]); - - const msgFlagsBuf = Buffer.concat([ - Buffer.from([0x04, 0x01]), // ASN.1 Octet String, length 1 - Buffer.from([0x00]) // No authentication or privacy - ]); - - const msgSecModelBuf = Buffer.concat([ - Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 - Buffer.from([0x03]) // Security model (3 = USM) - ]); - - // SNMPv3 header - const msgHeaderBuf = Buffer.concat([ - Buffer.from([0x30]), // ASN.1 Sequence - Buffer.from([msgIdBuf.length + msgMaxSizeBuf.length + msgFlagsBuf.length + msgSecModelBuf.length]), // Length - msgIdBuf, - msgMaxSizeBuf, - msgFlagsBuf, - msgSecModelBuf - ]); - - // Simple security parameters for discovery - const securityBuf = Buffer.concat([ - Buffer.from([0x04, 0x00]), // Empty octet string - ]); - - // Simple Get request for discovery - const requestIdBuf = Buffer.concat([ - Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 - SnmpEncoder.encodeInteger(requestID + 1) - ]); - - const errorStatusBuf = Buffer.concat([ - Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 - Buffer.from([0x00]) // Error Status (0 = no error) - ]); - - const errorIndexBuf = Buffer.concat([ - Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 - Buffer.from([0x00]) // Error Index (0) - ]); - - // Empty varbinds for discovery - const varBindingsBuf = Buffer.concat([ - Buffer.from([0x30, 0x00]), // Empty sequence - ]); - - const pduBuf = Buffer.concat([ - Buffer.from([0xa0]), // GetRequest - Buffer.from([requestIdBuf.length + errorStatusBuf.length + errorIndexBuf.length + varBindingsBuf.length]), - requestIdBuf, - errorStatusBuf, - errorIndexBuf, - varBindingsBuf - ]); - - // Context data - const contextEngineBuf = Buffer.concat([ - Buffer.from([0x04, 0x00]), // Empty octet string - ]); - - const contextNameBuf = Buffer.concat([ - Buffer.from([0x04, 0x00]), // Empty octet string - ]); - - const scopedPduBuf = Buffer.concat([ - Buffer.from([0x30]), - Buffer.from([contextEngineBuf.length + contextNameBuf.length + pduBuf.length]), - contextEngineBuf, - contextNameBuf, - pduBuf - ]); - - // Version - const versionBuf = Buffer.concat([ - Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 - Buffer.from([0x03]) // SNMP version 3 (3) - ]); - - // Complete message - return Buffer.concat([ - Buffer.from([0x30]), - Buffer.from([versionBuf.length + msgHeaderBuf.length + securityBuf.length + scopedPduBuf.length]), - versionBuf, - msgHeaderBuf, - securityBuf, - scopedPduBuf - ]); - } -} \ No newline at end of file diff --git a/ts/snmp/packet-parser.ts b/ts/snmp/packet-parser.ts deleted file mode 100644 index 1d3b20c..0000000 --- a/ts/snmp/packet-parser.ts +++ /dev/null @@ -1,553 +0,0 @@ -import type { ISnmpConfig } from './types.js'; -import { SnmpEncoder } from './encoder.js'; - -/** - * SNMP packet parsing utilities - * Parses SNMP response packets - */ -export class SnmpPacketParser { - /** - * Parse an SNMP response - * @param buffer Response buffer - * @param config SNMP configuration - * @param debug Whether to enable debug output - * @returns Parsed value or null if parsing failed - */ - public static parseSnmpResponse(buffer: Buffer, config: ISnmpConfig, debug: boolean = false): any { - // Check if we have a response packet - if (buffer[0] !== 0x30) { - throw new Error('Invalid SNMP response format'); - } - - // For SNMPv3, we need to handle the message differently - if (config.version === 3) { - return this.parseSnmpV3Response(buffer, debug); - } - - if (debug) { - console.log('Parsing SNMPv1/v2 response: ', buffer.toString('hex')); - } - - try { - // Enhanced structured parsing approach - // SEQUENCE header - let pos = 0; - if (buffer[pos] !== 0x30) { - throw new Error('Missing SEQUENCE at start of response'); - } - // Skip SEQUENCE header - assume length is in single byte for simplicity - // In a more robust implementation, we'd handle multi-byte lengths - pos += 2; - - // VERSION - if (buffer[pos] !== 0x02) { - throw new Error('Missing INTEGER for version'); - } - const versionLength = buffer[pos + 1]; - pos += 2 + versionLength; - - // COMMUNITY STRING - if (buffer[pos] !== 0x04) { - throw new Error('Missing OCTET STRING for community'); - } - const communityLength = buffer[pos + 1]; - pos += 2 + communityLength; - - // PDU TYPE - should be RESPONSE (0xA2) - if (buffer[pos] !== 0xA2) { - throw new Error(`Unexpected PDU type: 0x${buffer[pos].toString(16)}, expected 0xA2`); - } - // Skip PDU header - pos += 2; - - // REQUEST ID - if (buffer[pos] !== 0x02) { - throw new Error('Missing INTEGER for request ID'); - } - const requestIdLength = buffer[pos + 1]; - pos += 2 + requestIdLength; - - // ERROR STATUS - if (buffer[pos] !== 0x02) { - throw new Error('Missing INTEGER for error status'); - } - const errorStatusLength = buffer[pos + 1]; - const errorStatus = SnmpEncoder.decodeInteger(buffer, pos + 2, errorStatusLength); - - if (errorStatus !== 0) { - throw new Error(`SNMP error status: ${errorStatus}`); - } - pos += 2 + errorStatusLength; - - // ERROR INDEX - if (buffer[pos] !== 0x02) { - throw new Error('Missing INTEGER for error index'); - } - const errorIndexLength = buffer[pos + 1]; - pos += 2 + errorIndexLength; - - // VARBIND LIST - if (buffer[pos] !== 0x30) { - throw new Error('Missing SEQUENCE for varbind list'); - } - // Skip varbind list header - pos += 2; - - // VARBIND - if (buffer[pos] !== 0x30) { - throw new Error('Missing SEQUENCE for varbind'); - } - // Skip varbind header - pos += 2; - - // OID - if (buffer[pos] !== 0x06) { - throw new Error('Missing OBJECT IDENTIFIER for OID'); - } - const oidLength = buffer[pos + 1]; - pos += 2 + oidLength; - - // VALUE - this is what we want - const valueType = buffer[pos]; - const valueLength = buffer[pos + 1]; - - if (debug) { - console.log(`Found value type: 0x${valueType.toString(16)}, length: ${valueLength}`); - } - - return this.parseValueByType(valueType, valueLength, buffer, pos, debug); - } catch (error) { - if (debug) { - console.error('Error in structured parsing:', error); - console.error('Falling back to scan-based parsing method'); - } - - return this.scanBasedParsing(buffer, debug); - } - } - - /** - * Parse value by ASN.1 type - * @param valueType ASN.1 type - * @param valueLength Value length - * @param buffer Buffer containing the value - * @param pos Position of the value in the buffer - * @param debug Whether to enable debug output - * @returns Parsed value - */ - private static parseValueByType( - valueType: number, - valueLength: number, - buffer: Buffer, - pos: number, - debug: boolean - ): any { - switch (valueType) { - case 0x02: // INTEGER - { - const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength); - if (debug) { - console.log('Parsed INTEGER value:', value); - } - return value; - } - - case 0x04: // OCTET STRING - { - const value = buffer.slice(pos + 2, pos + 2 + valueLength).toString(); - if (debug) { - console.log('Parsed OCTET STRING value:', value); - } - return value; - } - - case 0x05: // NULL - if (debug) { - console.log('Parsed NULL value'); - } - return null; - - case 0x06: // OBJECT IDENTIFIER (rare in a value position) - { - // Usually this would be encoded as a string representation - const value = buffer.slice(pos + 2, pos + 2 + valueLength).toString('hex'); - if (debug) { - console.log('Parsed OBJECT IDENTIFIER value (hex):', value); - } - return value; - } - - case 0x40: // IP ADDRESS - { - if (valueLength !== 4) { - throw new Error(`Invalid IP address length: ${valueLength}, expected 4`); - } - const octets = []; - for (let i = 0; i < 4; i++) { - octets.push(buffer[pos + 2 + i]); - } - const value = octets.join('.'); - if (debug) { - console.log('Parsed IP ADDRESS value:', value); - } - return value; - } - - case 0x41: // COUNTER - case 0x42: // GAUGE32 - case 0x43: // TIMETICKS - case 0x44: // OPAQUE - { - // All these are essentially unsigned 32-bit integers - const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength); - if (debug) { - console.log(`Parsed ${valueType === 0x41 ? 'COUNTER' - : valueType === 0x42 ? 'GAUGE32' - : valueType === 0x43 ? 'TIMETICKS' - : 'OPAQUE'} value:`, value); - } - return value; - } - - default: - if (debug) { - console.log(`Unknown value type: 0x${valueType.toString(16)}`); - } - return null; - } - } - - /** - * Fallback scan-based parsing method - * @param buffer Buffer containing the SNMP response - * @param debug Whether to enable debug output - * @returns Parsed value or null if parsing failed - */ - private static scanBasedParsing(buffer: Buffer, debug: boolean): any { - // Look for various data types in the response - // The value is near the end of the packet after the OID - - // We're looking for one of these: - // 0x02 - Integer - can be at the end of a varbind - // 0x04 - OctetString - // 0x05 - Null - // 0x42 - Gauge32 - special type for unsigned 32-bit integers - // 0x43 - Timeticks - special type for time values - - // This algorithm performs a thorough search for data types - // by iterating from the start and watching for varbind structures - - // Walk through the buffer looking for varbinds - let i = 0; - - // First, find the varbinds section (0x30 sequence) - while (i < buffer.length - 2) { - // Look for a varbinds sequence - if (buffer[i] === 0x30) { - const varbindsLength = buffer[i + 1]; - const varbindsEnd = i + 2 + varbindsLength; - - // Now search within the varbinds for the value - let j = i + 2; - while (j < varbindsEnd - 2) { - // Look for a varbind (0x30 sequence) - if (buffer[j] === 0x30) { - const varbindLength = buffer[j + 1]; - const varbindEnd = j + 2 + varbindLength; - - // Skip over the OID and find the value within this varbind - let k = j + 2; - while (k < varbindEnd - 1) { - // First find the OID - if (buffer[k] === 0x06) { // OID - const oidLength = buffer[k + 1]; - k += 2 + oidLength; // Skip past the OID - - // We should now be at the value - // Check what type it is - if (k < varbindEnd - 1) { - return this.parseValueAtPosition(buffer, k, debug); - } - - // If we didn't find a value, move to next byte - k++; - } else { - // Move to next byte - k++; - } - } - - // Move to next varbind - j = varbindEnd; - } else { - // Move to next byte - j++; - } - } - - // Move to next sequence - i = varbindsEnd; - } else { - // Move to next byte - i++; - } - } - - if (debug) { - console.log('No valid value found in SNMP response'); - } - return null; - } - - /** - * Parse value at a specific position in the buffer - * @param buffer Buffer containing the SNMP response - * @param pos Position of the value in the buffer - * @param debug Whether to enable debug output - * @returns Parsed value or null if parsing failed - */ - private static parseValueAtPosition(buffer: Buffer, pos: number, debug: boolean): any { - if (buffer[pos] === 0x02) { // Integer - const valueLength = buffer[pos + 1]; - const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength); - if (debug) { - console.log('Found Integer value:', value); - } - return value; - } else if (buffer[pos] === 0x42) { // Gauge32 - const valueLength = buffer[pos + 1]; - const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength); - if (debug) { - console.log('Found Gauge32 value:', value); - } - return value; - } else if (buffer[pos] === 0x43) { // TimeTicks - const valueLength = buffer[pos + 1]; - const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength); - if (debug) { - console.log('Found Timeticks value:', value); - } - return value; - } else if (buffer[pos] === 0x04) { // OctetString - const valueLength = buffer[pos + 1]; - if (debug) { - console.log('Found OctetString value'); - } - // Just return the string value as-is - return buffer.slice(pos + 2, pos + 2 + valueLength).toString(); - } else if (buffer[pos] === 0x05) { // Null - if (debug) { - console.log('Found Null value'); - } - return null; - } - - return null; - } - - /** - * Parse an SNMPv3 response - * @param buffer Buffer containing the SNMP response - * @param debug Whether to enable debug output - * @returns Parsed value or null if parsing failed - */ - public static parseSnmpV3Response(buffer: Buffer, debug: boolean = false): any { - // SNMPv3 parsing is complex. In a real implementation, we would: - // 1. Parse the header and get the security parameters - // 2. Verify authentication if used - // 3. Decrypt the PDU if privacy was used - // 4. Extract the PDU and parse it - - if (debug) { - console.log('Parsing SNMPv3 response: ', buffer.toString('hex')); - } - - // Find the scopedPDU - it should be the last OCTET STRING in the message - let scopedPduPos = -1; - for (let i = buffer.length - 50; i >= 0; i--) { - if (buffer[i] === 0x04 && buffer[i + 1] > 10) { // OCTET STRING with reasonable length - scopedPduPos = i; - break; - } - } - - if (scopedPduPos === -1) { - if (debug) { - console.log('Could not find scoped PDU in SNMPv3 response'); - } - return null; - } - - // Skip to the PDU content - let pduContent = buffer.slice(scopedPduPos + 2); // Skip OCTET STRING header - - // This improved algorithm performs a more thorough search for varbinds - // in the scoped PDU - - // First, look for the response PDU (sequence with tag 0xa2) - let responsePdu = null; - for (let i = 0; i < pduContent.length - 3; i++) { - if (pduContent[i] === 0xa2) { - // Found the response PDU - const pduLength = pduContent[i + 1]; - responsePdu = pduContent.slice(i, i + 2 + pduLength); - break; - } - } - - if (!responsePdu) { - // Try to find the varbinds directly - for (let i = 0; i < pduContent.length - 3; i++) { - if (pduContent[i] === 0x30) { - const seqLength = pduContent[i + 1]; - if (i + 2 + seqLength <= pduContent.length) { - // Check if this sequence might be the varbinds - const possibleVarbinds = pduContent.slice(i, i + 2 + seqLength); - - // Look for varbind structure inside - for (let j = 0; j < possibleVarbinds.length - 3; j++) { - if (possibleVarbinds[j] === 0x30) { - // Might be a varbind - look for an OID inside - for (let k = j; k < j + 10 && k < possibleVarbinds.length - 1; k++) { - if (possibleVarbinds[k] === 0x06) { - // Found an OID, so this is likely the varbinds sequence - responsePdu = possibleVarbinds; - break; - } - } - - if (responsePdu) break; - } - } - - if (responsePdu) break; - } - } - } - } - - if (!responsePdu) { - if (debug) { - console.log('Could not find response PDU in SNMPv3 response'); - } - return null; - } - - // Now that we have the response PDU, search for varbinds - // Skip the first few bytes to get past the header fields - let varbindsPos = -1; - for (let i = 10; i < responsePdu.length - 3; i++) { - if (responsePdu[i] === 0x30) { - // Check if this is the start of the varbinds - // by seeing if it contains a varbind sequence - for (let j = i + 2; j < i + 10 && j < responsePdu.length - 3; j++) { - if (responsePdu[j] === 0x30) { - varbindsPos = i; - break; - } - } - - if (varbindsPos !== -1) break; - } - } - - if (varbindsPos === -1) { - if (debug) { - console.log('Could not find varbinds in SNMPv3 response'); - } - return null; - } - - // Get the varbinds - const varbindsLength = responsePdu[varbindsPos + 1]; - const varbinds = responsePdu.slice(varbindsPos, varbindsPos + 2 + varbindsLength); - - // Now search for values inside the varbinds - for (let i = 2; i < varbinds.length - 3; i++) { - // Look for a varbind sequence - if (varbinds[i] === 0x30) { - const varbindLength = varbinds[i + 1]; - const varbind = varbinds.slice(i, i + 2 + varbindLength); - - // Inside the varbind, look for the OID and then the value - for (let j = 0; j < varbind.length - 3; j++) { - if (varbind[j] === 0x06) { // OID - const oidLength = varbind[j + 1]; - - // The value should be right after the OID - const valuePos = j + 2 + oidLength; - if (valuePos < varbind.length - 1) { - // Check what type of value it is - if (varbind[valuePos] === 0x02) { // INTEGER - const valueLength = varbind[valuePos + 1]; - const value = SnmpEncoder.decodeInteger(varbind, valuePos + 2, valueLength); - if (debug) { - console.log('Found INTEGER value in SNMPv3 response:', value); - } - return value; - } else if (varbind[valuePos] === 0x42) { // Gauge32 - const valueLength = varbind[valuePos + 1]; - const value = SnmpEncoder.decodeInteger(varbind, valuePos + 2, valueLength); - if (debug) { - console.log('Found Gauge32 value in SNMPv3 response:', value); - } - return value; - } else if (varbind[valuePos] === 0x43) { // TimeTicks - const valueLength = varbind[valuePos + 1]; - const value = SnmpEncoder.decodeInteger(varbind, valuePos + 2, valueLength); - if (debug) { - console.log('Found TimeTicks value in SNMPv3 response:', value); - } - return value; - } else if (varbind[valuePos] === 0x04) { // OctetString - const valueLength = varbind[valuePos + 1]; - const value = varbind.slice(valuePos + 2, valuePos + 2 + valueLength).toString(); - if (debug) { - console.log('Found OctetString value in SNMPv3 response:', value); - } - return value; - } - } - } - } - } - } - - if (debug) { - console.log('No valid value found in SNMPv3 response'); - } - return null; - } - - /** - * Extract engine ID from SNMPv3 response - * @param buffer Buffer containing the SNMP response - * @param debug Whether to enable debug output - * @returns Extracted engine ID or null if extraction failed - */ - public static extractEngineId(buffer: Buffer, debug: boolean = false): Buffer | null { - try { - // Simple parsing to find the engine ID - // Look for the first octet string with appropriate length - for (let i = 0; i < buffer.length - 10; i++) { - if (buffer[i] === 0x04) { // Octet string - const len = buffer[i + 1]; - if (len >= 5 && len <= 32) { // Engine IDs are typically 5-32 bytes - // Verify this looks like an engine ID (usually starts with 0x80) - if (buffer[i + 2] === 0x80) { - if (debug) { - console.log('Found engine ID at position', i); - console.log('Engine ID:', buffer.slice(i + 2, i + 2 + len).toString('hex')); - } - return buffer.slice(i + 2, i + 2 + len); - } - } - } - } - return null; - } catch (error) { - console.error('Error extracting engine ID:', error); - return null; - } - } -} \ No newline at end of file diff --git a/ts/snmp/types.ts b/ts/snmp/types.ts index a9ddb65..32508f4 100644 --- a/ts/snmp/types.ts +++ b/ts/snmp/types.ts @@ -45,6 +45,8 @@ export interface ISnmpConfig { version: number; /** Timeout in milliseconds */ timeout: number; + + context?: string; // SNMPv1/v2c /** Community string for SNMPv1/v2c */