553 lines
18 KiB
TypeScript
553 lines
18 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
} |