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; } } }