# encoding: ascii-8bit # frozen_string_literal: true # # Copyright (c) 2004-2014 David R. Halliday # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # # # This module implements methods for encoding and decoding SNMP packets # using the ASN.1 BER (Basic Encoding Rules). # module SNMP module BER #:nodoc:all # SNMP version codes SNMP_V1 = 0 SNMP_V2C = 1 SNMP_V3 = 3 # not supported # SNMP context-specific data types # See RFC 1157 for SNMPv1 # See RFC 1905 for SNMPv2c GetRequest_PDU_TAG = 0xa0 GetNextRequest_PDU_TAG = 0xa1 Response_PDU_TAG = 0xa2 SetRequest_PDU_TAG = 0xa3 SNMPv1_Trap_PDU_TAG = 0xa4 # Note: valid for SNMPv1 only GetBulkRequest_PDU_TAG = 0xa5 InformRequest_PDU_TAG = 0xa6 SNMPv2_Trap_PDU_TAG = 0xa7 Report_PDU_TAG = 0xa8 # Note: Usage not defined - not supported # Primitive ASN.1 data types INTEGER_TAG = 0x02 OCTET_STRING_TAG = 0x04 NULL_TAG = 0x05 OBJECT_IDENTIFIER_TAG = 0x06 # Constructed ASN.1 data type SEQUENCE_TAG = 0x30 # SNMP application data types # See RFC 1155 for SNMPv1 # See RFC 1902 for SNMPv2c IpAddress_TAG = 0x40 Counter32_TAG = 0x41 # Counter in SNMPv1 Gauge32_TAG = 0x42 # Gauge in SNMPv1 Unsigned32_TAG = 0x42 # Note: same as Gauge32 TimeTicks_TAG = 0x43 Opaque_TAG = 0x44 Counter64_TAG = 0x46 # VarBind response exceptions NoSuchObject_TAG = 0x80 NoSuchInstance_TAG = 0x81 EndOfMibView_TAG = 0x82 # Exceptions thrown in this module class OutOfData < RuntimeError; end class InvalidLength < RuntimeError; end class InvalidTag < RuntimeError; end class InvalidObjectId < RuntimeError; end class InvalidLength < RuntimeError; end module Decode def assert_no_remainder(remainder) raise ParseError, remainder.inspect if remainder != "" end # # Decode tag-length-value data. The data is assumed to be a string of # bytes in network byte order. This format is returned by Socket#recv. # # Returns a tuple containing the tag, the value, and any remaining # unprocessed data. # # The data is not interpretted by this method. Use one of the other # decoding methods to interpret the data. # # Note that ASN.1 supports an indefinite length format where the end of # content is marked by a pair of 0 octets. SNMP does not support this # format, so only the two definite forms are implemented (single byte and # multi-byte). # def decode_tlv(data) raise OutOfData if (data.length == 2 && data[1].ord != 0) || data.length < 2 tag = data[0].ord length = data[1].ord if length < 0x80 value = data[2, length] remainder = data[length+2..-1] else # ASN.1 says this octet can't be 0xff raise InvalidLength, length.to_s if length == 0xff num_octets = length & 0x7f length = build_integer(data, 2, num_octets) value = data[num_octets+2, length] remainder = data[num_octets+2+length..-1] end return tag, value, remainder end # # Decode TLV data for an ASN.1 integer. # # Throws an InvalidTag exception if the tag is incorrect. # # Returns a tuple containing an integer and any remaining unprocessed data. # def decode_integer(data) tag, value, remainder = decode_tlv(data) raise InvalidTag, tag.to_s if tag != INTEGER_TAG return decode_integer_value(value), remainder end def decode_timeticks(data) tag, value, remainder = decode_tlv(data) raise InvalidTag, tag.to_s if tag != TimeTicks_TAG return decode_uinteger_value(value), remainder end def decode_integer_value(value) result = build_integer(value, 0, value.length) if value[0].ord[7] == 1 result -= (1 << (8 * value.length)) end result end ## # Decode an integer, ignoring the sign bit. Some agents insist on # encoding 32 bit unsigned integers with four bytes even though it # should be 5 bytes (at least the way I read it). # def decode_uinteger_value(value) build_integer(value, 0, value.length) end def build_integer(data, start, num_octets) number = 0 num_octets.times { |i| number = number<<8 | data[start+i].ord } return number end # # Decode TLV data for an ASN.1 octet string. # # Throws an InvalidTag exception if the tag is incorrect. # # Returns a tuple containing a string and any remaining unprocessed data. # def decode_octet_string(data) tag, value, remainder = decode_tlv(data) raise InvalidTag, tag.to_s if tag != OCTET_STRING_TAG return value, remainder end def decode_ip_address(data) tag, value, remainder = decode_tlv(data) raise InvalidTag, tag.to_s if tag != IpAddress_TAG raise InvalidLength, tag.to_s if value.length != 4 return value, remainder end # # Decode TLV data for an ASN.1 sequence. # # Throws an InvalidTag exception if the tag is incorrect. # # Returns a tuple containing the sequence data and any remaining # unprocessed data that follows the sequence. # def decode_sequence(data) tag, value, remainder = decode_tlv(data) raise InvalidTag, tag.to_s if tag != SEQUENCE_TAG return value, remainder end # # Unwrap TLV data for an ASN.1 object identifier. This method extracts # the OID value as a character string but does not decode it further. # # Throws an InvalidTag exception if the tag is incorrect. # # Returns a tuple containing the object identifier (OID) and any # remaining unprocessed data. The OID is represented as an array # of integers. # def decode_object_id(data) tag, value, remainder = decode_tlv(data) raise InvalidTag, tag.to_s if tag != OBJECT_IDENTIFIER_TAG return decode_object_id_value(value), remainder end def decode_object_id_value(value) if value.length == 0 object_id = [] else value0 = value[0].ord if value0 == 0x2b object_id = [1,3] else second = value0 % 40 first = (value0 - second) / 40 raise InvalidObjectId, value.to_s if first > 2 object_id = [first, second] end n = 0 for i in 1...value.length n = (n<<7) + (value[i].ord & 0x7f) if value[i].ord < 0x80 object_id << n n = 0 end end end return object_id end end module Encode # # Encode the length field for TLV data. Returns the length octets # as a string. # def encode_length(length) raise InvalidLength, length.to_s if length < 0 if length < 0x80 length.chr else data = integer_to_octets(length) (data.size | 0x80).chr << data end end # # Encode integer # def encode_integer(value) encode_tagged_integer(INTEGER_TAG, value) end def encode_tagged_integer(tag, value) if value > 0 && value < 0x80 data = value.chr else data = integer_to_octets(value) if value > 0 && data[0].ord > 0x7f data = "\000".dup << data elsif value < 0 && data[0].ord < 0x80 data = "\377".dup << data end end encode_tlv(tag, data) end # # Helper method for encoding integer-like things. # def integer_to_octets(i) if i >= 0 done = 0 else done = -1 end octets = "" begin octets = (i & 0xff).chr << octets i = i >> 8 end until i == done octets end def encode_null NULL_TAG.chr << "\000" end # # Encode an exception. The encoding is simply the exception tag with # no data, similar to NULL. # def encode_exception(tag) tag.chr << "\000" end # # Wraps value in a tag and length. This method expects an # integer tag and a string value. # def encode_tlv(tag, value) data = tag.chr << encode_length(value.length) data = data << value if value.length > 0 data end # # Wrap string in a octet string tag and length. # def encode_octet_string(value) encode_tlv(OCTET_STRING_TAG, value) end # # Wrap value in a sequence tag and length. # def encode_sequence(value) encode_tlv(SEQUENCE_TAG, value) end # # Encode an object id. The input is assumed to be an array of integers # representing the object id. # def encode_object_id(value) raise InvalidObjectId, value.to_s if value.length < 1 raise InvalidObjectId, value.to_s if value[0] > 2 data = "".dup if (value.length > 1) raise InvalidObjectId if value[0] < 2 && value[1] > 40 data << (40 * value[0] + value[1]).chr for i in 2...value.length if value[i] < 0x80 data << value[i].chr else octets = "" n = value[i] begin octets = (n & 0x7f | 0x80).chr << octets n = n >> 7 end until n == 0 octets[-1] = (octets[-1].ord & 0x7f).chr data << octets end end elsif (value.length == 1) data << (40 * value[0]).chr end encode_tlv(OBJECT_IDENTIFIER_TAG, data) end end end end