This commit is contained in:
2025-03-25 09:06:23 +00:00
commit 1e74fbe367
27 changed files with 14700 additions and 0 deletions

98
ts/snmp/encoder.ts Normal file
View File

@ -0,0 +1,98 @@
/**
* 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];
}
}
}

10
ts/snmp/index.ts Normal file
View File

@ -0,0 +1,10 @@
/**
* Main module entry point for SNMP functionality
* Re-exports public types and classes
*/
// Re-export all public types
export type { UpsStatus, OIDSet, UpsModel, SnmpConfig } from './types.js';
// Re-export the SNMP manager class
export { NupstSnmp } from './manager.js';

514
ts/snmp/manager.ts Normal file
View File

@ -0,0 +1,514 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import * as dgram from 'dgram';
import type { OIDSet, SnmpConfig, UpsModel, UpsStatus } from './types.js';
import { UpsOidSets } from './oid-sets.js';
import { SnmpPacketCreator } from './packet-creator.js';
import { SnmpPacketParser } from './packet-parser.js';
const execAsync = promisify(exec);
/**
* Class for SNMP communication with UPS devices
* Main entry point for SNMP functionality
*/
export class NupstSnmp {
// Active OID set
private activeOIDs: OIDSet;
// Default SNMP configuration
private readonly DEFAULT_CONFIG: SnmpConfig = {
host: '127.0.0.1', // Default to localhost
port: 161, // Default SNMP port
community: 'public', // Default community string for v1/v2c
version: 1, // SNMPv1
timeout: 5000, // 5 seconds timeout
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
*/
constructor(debug = false) {
this.debug = debug;
// Set default OID set
this.activeOIDs = UpsOidSets.getOidSet('cyberpower');
}
/**
* Set active OID set based on UPS model
* @param config SNMP configuration
*/
private setActiveOIDs(config: SnmpConfig): void {
// If custom OIDs are provided, use them
if (config.upsModel === 'custom' && config.customOIDs) {
this.activeOIDs = config.customOIDs;
if (this.debug) {
console.log('Using custom OIDs:', this.activeOIDs);
}
return;
}
// Use OIDs for the specified UPS model or default to Cyberpower
const model = config.upsModel || 'cyberpower';
this.activeOIDs = UpsOidSets.getOidSet(model);
if (this.debug) {
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
* @param oid OID to query
* @param config SNMP configuration
* @returns Promise resolving to the SNMP response value
*/
public async snmpGet(oid: string, config = this.DEFAULT_CONFIG): Promise<any> {
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'));
}
// 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 (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) => {
if (error) {
clearTimeout(timeout);
socket.close();
if (this.debug) {
console.error('Error sending SNMP request:', error);
}
reject(error);
} else if (this.debug) {
console.log('SNMP request sent successfully');
}
});
});
}
/**
* Get the current status of the UPS
* @param config SNMP configuration
* @returns Promise resolving to the UPS status
*/
public async getUpsStatus(config = this.DEFAULT_CONFIG): Promise<UpsStatus> {
try {
// Set active OID set based on UPS model in config
this.setActiveOIDs(config);
if (this.debug) {
console.log('---------------------------------------');
console.log('Getting UPS status with config:');
console.log(' Host:', config.host);
console.log(' Port:', config.port);
console.log(' Version:', config.version);
console.log(' Timeout:', config.timeout, 'ms');
console.log(' UPS Model:', config.upsModel || 'cyberpower');
if (config.version === 1 || config.version === 2) {
console.log(' Community:', config.community);
} else if (config.version === 3) {
console.log(' Security Level:', config.securityLevel);
console.log(' Username:', config.username);
console.log(' Auth Protocol:', config.authProtocol || 'None');
console.log(' Privacy Protocol:', config.privProtocol || 'None');
}
console.log('Using OIDs:');
console.log(' Power Status:', this.activeOIDs.POWER_STATUS);
console.log(' Battery Capacity:', this.activeOIDs.BATTERY_CAPACITY);
console.log(' Battery Runtime:', this.activeOIDs.BATTERY_RUNTIME);
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;
// 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 {
// Default interpretation for other UPS models
if (powerStatusValue === 1) {
powerStatus = 'online';
} else if (powerStatusValue === 2) {
powerStatus = 'onBattery';
}
}
// Convert TimeTicks to minutes for CyberPower runtime (value is in 1/100 seconds)
let processedRuntime = batteryRuntime;
if (config.upsModel === 'cyberpower' && batteryRuntime > 0) {
// 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`);
}
}
const result = {
powerStatus,
batteryCapacity,
batteryRuntime: processedRuntime,
raw: {
powerStatus: powerStatusValue,
batteryCapacity,
batteryRuntime,
},
};
if (this.debug) {
console.log('---------------------------------------');
console.log('UPS status result:');
console.log(' Power Status:', result.powerStatus);
console.log(' Battery Capacity:', result.batteryCapacity + '%');
console.log(' Battery Runtime:', result.batteryRuntime, 'minutes');
console.log('---------------------------------------');
}
return result;
} catch (error) {
if (this.debug) {
console.error('---------------------------------------');
console.error('Error getting UPS status:', error.message);
console.error('---------------------------------------');
}
throw new Error(`Failed to get UPS status: ${error.message}`);
}
}
/**
* Discover SNMP engine ID (for SNMPv3)
* Sends a proper discovery message to get the engine ID from the device
* @param config SNMP configuration
* @returns Promise resolving to the discovered engine ID
*/
public async discoverEngineId(config: SnmpConfig): Promise<Buffer> {
return new Promise((resolve, reject) => {
const socket = dgram.createSocket('udp4');
// Create a proper discovery message (SNMPv3 with noAuthNoPriv)
const discoveryConfig: SnmpConfig = {
...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++);
if (this.debug) {
console.log('Sending SNMPv3 discovery message');
console.log('SNMPv3 Discovery message:', request.toString('hex'));
}
// 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);
// 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
}
});
// 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');
}
});
});
}
/**
* Initiate system shutdown
* @param reason Reason for shutdown
*/
public async initiateShutdown(reason: string): Promise<void> {
console.log(`Initiating system shutdown due to: ${reason}`);
try {
// Execute shutdown command
const { stdout } = await execAsync('shutdown -h +1 "UPS battery critical, shutting down in 1 minute"');
console.log('Shutdown initiated:', stdout);
} catch (error) {
console.error('Failed to initiate shutdown:', error);
// Try a different method if first one fails
try {
console.log('Trying alternative shutdown method...');
await execAsync('poweroff --force');
} catch (innerError) {
console.error('All shutdown methods failed:', innerError);
}
}
}
}

75
ts/snmp/oid-sets.ts Normal file
View File

@ -0,0 +1,75 @@
import type { OIDSet, UpsModel } from './types.js';
/**
* OID sets for different UPS models
* Contains mappings between UPS models and their SNMP OIDs
*/
export class UpsOidSets {
/**
* OID sets for different UPS models
*/
private static readonly UPS_OID_SETS: Record<UpsModel, OIDSet> = {
// Cyberpower OIDs for RMCARD205 (based on CyberPower_MIB_v2.11)
cyberpower: {
POWER_STATUS: '1.3.6.1.4.1.3808.1.1.1.4.1.1.0', // upsBaseOutputStatus (2=online, 3=on battery)
BATTERY_CAPACITY: '1.3.6.1.4.1.3808.1.1.1.2.2.1.0', // upsAdvanceBatteryCapacity (percentage)
BATTERY_RUNTIME: '1.3.6.1.4.1.3808.1.1.1.2.2.4.0', // upsAdvanceBatteryRunTimeRemaining (TimeTicks)
},
// APC OIDs
apc: {
POWER_STATUS: '1.3.6.1.4.1.318.1.1.1.4.1.1.0', // Power status (1=online, 2=on battery)
BATTERY_CAPACITY: '1.3.6.1.4.1.318.1.1.1.2.2.1.0', // Battery capacity in percentage
BATTERY_RUNTIME: '1.3.6.1.4.1.318.1.1.1.2.2.3.0', // Remaining runtime in minutes
},
// Eaton OIDs
eaton: {
POWER_STATUS: '1.3.6.1.4.1.534.1.1.2.0', // Power status
BATTERY_CAPACITY: '1.3.6.1.4.1.534.1.2.4.0', // Battery capacity in percentage
BATTERY_RUNTIME: '1.3.6.1.4.1.534.1.2.1.0', // Remaining runtime in minutes
},
// TrippLite OIDs
tripplite: {
POWER_STATUS: '1.3.6.1.4.1.850.1.1.3.1.1.1.0', // Power status
BATTERY_CAPACITY: '1.3.6.1.4.1.850.1.1.3.2.4.1.0', // Battery capacity in percentage
BATTERY_RUNTIME: '1.3.6.1.4.1.850.1.1.3.2.2.1.0', // Remaining runtime in minutes
},
// Liebert/Vertiv OIDs
liebert: {
POWER_STATUS: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.2.1', // Power status
BATTERY_CAPACITY: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.4.1', // Battery capacity in percentage
BATTERY_RUNTIME: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.5.1', // Remaining runtime in minutes
},
// Custom OIDs (to be provided by the user)
custom: {
POWER_STATUS: '',
BATTERY_CAPACITY: '',
BATTERY_RUNTIME: '',
}
};
/**
* Get OID set for a specific UPS model
* @param model UPS model name
* @returns OID set for the model
*/
public static getOidSet(model: UpsModel): OIDSet {
return this.UPS_OID_SETS[model];
}
/**
* Get standard RFC 1628 OID set as fallback
* @returns Standard OID set
*/
public static getStandardOids(): Record<string, string> {
return {
'power status': '1.3.6.1.2.1.33.1.4.1.0', // upsOutputSource
'battery capacity': '1.3.6.1.2.1.33.1.2.4.0', // upsEstimatedChargeRemaining
'battery runtime': '1.3.6.1.2.1.33.1.2.3.0' // upsEstimatedMinutesRemaining
};
}
}

651
ts/snmp/packet-creator.ts Normal file
View File

@ -0,0 +1,651 @@
import * as crypto from 'crypto';
import type { SnmpConfig, SnmpV3SecurityParams } 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: SnmpConfig,
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: SnmpV3SecurityParams = {
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: SnmpConfig): 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: SnmpConfig, 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: SnmpConfig, 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
]);
}
}

553
ts/snmp/packet-parser.ts Normal file
View File

@ -0,0 +1,553 @@
import type { SnmpConfig } 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: SnmpConfig, 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;
}
}
}

90
ts/snmp/types.ts Normal file
View File

@ -0,0 +1,90 @@
/**
* Type definitions for SNMP module
*/
/**
* UPS status interface
*/
export interface UpsStatus {
/** Current power status */
powerStatus: 'online' | 'onBattery' | 'unknown';
/** Battery capacity percentage */
batteryCapacity: number;
/** Remaining runtime in minutes */
batteryRuntime: number;
/** Raw values from SNMP responses */
raw: Record<string, any>;
}
/**
* SNMP OID Sets for different UPS brands
*/
export interface OIDSet {
/** OID for power status */
POWER_STATUS: string;
/** OID for battery capacity */
BATTERY_CAPACITY: string;
/** OID for battery runtime */
BATTERY_RUNTIME: string;
}
/**
* Supported UPS model types
*/
export type UpsModel = 'cyberpower' | 'apc' | 'eaton' | 'tripplite' | 'liebert' | 'custom';
/**
* SNMP Configuration interface
*/
export interface SnmpConfig {
/** SNMP server host */
host: string;
/** SNMP server port (default 161) */
port: number;
/** SNMP version (1, 2, or 3) */
version: number;
/** Timeout in milliseconds */
timeout: number;
// SNMPv1/v2c
/** Community string for SNMPv1/v2c */
community?: string;
// SNMPv3
/** Security level for SNMPv3 */
securityLevel?: 'noAuthNoPriv' | 'authNoPriv' | 'authPriv';
/** Username for SNMPv3 authentication */
username?: string;
/** Authentication protocol for SNMPv3 */
authProtocol?: 'MD5' | 'SHA';
/** Authentication key for SNMPv3 */
authKey?: string;
/** Privacy protocol for SNMPv3 */
privProtocol?: 'DES' | 'AES';
/** Privacy key for SNMPv3 */
privKey?: string;
// UPS model and custom OIDs
/** UPS model for OID selection */
upsModel?: UpsModel;
/** Custom OIDs when using custom UPS model */
customOIDs?: OIDSet;
}
/**
* SNMPv3 security parameters
*/
export interface SnmpV3SecurityParams {
/** Engine ID for the SNMP server */
msgAuthoritativeEngineID: Buffer;
/** Engine boots counter */
msgAuthoritativeEngineBoots: number;
/** Engine time counter */
msgAuthoritativeEngineTime: number;
/** Username for authentication */
msgUserName: string;
/** Authentication parameters */
msgAuthenticationParameters: Buffer;
/** Privacy parameters */
msgPrivacyParameters: Buffer;
}