initial
This commit is contained in:
98
ts/snmp/encoder.ts
Normal file
98
ts/snmp/encoder.ts
Normal 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
10
ts/snmp/index.ts
Normal 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
514
ts/snmp/manager.ts
Normal 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
75
ts/snmp/oid-sets.ts
Normal 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
651
ts/snmp/packet-creator.ts
Normal 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
553
ts/snmp/packet-parser.ts
Normal 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
90
ts/snmp/types.ts
Normal 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;
|
||||
}
|
Reference in New Issue
Block a user