lib/net/ber.rb in net-ldap-0.0.5 vs lib/net/ber.rb in net-ldap-0.1.0

- old
+ new

@@ -1,7 +1,5 @@ -# $Id$ -# # NET::BER # Mixes ASN.1/BER convenience methods into several standard classes. # Also provides BER parsing functionality. # #---------------------------------------------------------------------------- @@ -23,535 +21,75 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # #--------------------------------------------------------------------------- -# -# - module Net - module BER - - class BerError < StandardError; end - - - class BerIdentifiedString < String - attr_accessor :ber_identifier - def initialize args - super args - end - end - - class BerIdentifiedArray < Array - attr_accessor :ber_identifier - def initialize - super - end - end - - class BerIdentifiedNull - attr_accessor :ber_identifier - def to_ber - "\005\000" - end - end - - class BerIdentifiedOid - attr_accessor :ber_identifier - def initialize oid - if oid.is_a?(String) - oid = oid.split(/\./).map {|s| s.to_i } - end - @value = oid - end - def to_ber - # Provisional implementation. - # We ASSUME that our incoming value is an array, and we - # use the Array#to_ber_oid method defined below. - # We probably should obsolete that method, actually, in - # and move the code here. - # WE ARE NOT CURRENTLY ENCODING THE BER-IDENTIFIER. - # This implementation currently hardcodes 6, the universal OID tag. - ary = @value.dup - first = ary.shift - raise Net::BER::BerError.new(" invalid OID" ) unless [0,1,2].include?(first) - first = first * 40 + ary.shift - ary.unshift first - oid = ary.pack("w*") - [6, oid.length].pack("CC") + oid - end - end - - #-- - # This condenses our nicely self-documenting ASN hashes down - # to an array for fast lookups. - # Scoped to be called as a module method, but not intended for - # user code to call. - # - def self.compile_syntax syn - out = [nil] * 256 - syn.each {|tclass,tclasses| - tagclass = {:universal=>0, :application=>64, :context_specific=>128, :private=>192} [tclass] - tclasses.each {|codingtype,codings| - encoding = {:primitive=>0, :constructed=>32} [codingtype] - codings.each {|tag,objtype| - out[tagclass + encoding + tag] = objtype - } - } - } - out - end - - # This module is for mixing into IO and IO-like objects. - module BERParser - - # The order of these follows the class-codes in BER. - # Maybe this should have been a hash. - TagClasses = [:universal, :application, :context_specific, :private] - - BuiltinSyntax = BER.compile_syntax( { - :universal => { - :primitive => { - 1 => :boolean, - 2 => :integer, - 4 => :string, - 5 => :null, - 6 => :oid, - 10 => :integer, - 13 => :string # (relative OID) - }, - :constructed => { - 16 => :array, - 17 => :array - } - }, - :context_specific => { - :primitive => { - 10 => :integer - } - } - }) - - # - # read_ber - # TODO: clean this up so it works properly with partial - # packets coming from streams that don't block when - # we ask for more data (like StringIOs). At it is, - # this can throw TypeErrors and other nasties. + VERSION = '0.1.0' + #-- - # BEWARE, this violates DRY and is largely equal in functionality to - # read_ber_from_string. Eventually that method may subsume the functionality - # of this one. - # - def read_ber syntax=nil - # don't bother with this line, since IO#getbyte by definition returns nil on eof. - #return nil if eof? - - id = getbyte or return nil # don't trash this value, we'll use it later - #tag = id & 31 - #tag < 31 or raise BerError.new( "unsupported tag encoding: #{id}" ) - #tagclass = TagClasses[ id >> 6 ] - #encoding = (id & 0x20 != 0) ? :constructed : :primitive - - n = getbyte - lengthlength,contentlength = if n <= 127 - [1,n] - else - # Replaced the inject because it profiles hot. - #j = (0...(n & 127)).inject(0) {|mem,x| mem = (mem << 8) + getbyte} - j = 0 - read( n & 127 ).each_byte {|n1| j = (j << 8) + n1} - [1 + (n & 127), j] - end - - newobj = read contentlength - - # This exceptionally clever and clear bit of code is verrrry slow. - objtype = (syntax && syntax[id]) || BuiltinSyntax[id] - - - # == is expensive so sort this if/else so the common cases are at the top. - obj = if objtype == :string - #(newobj || "").dup - s = BerIdentifiedString.new( newobj || "" ) - s.ber_identifier = id - s - elsif objtype == :integer - j = 0 - newobj.each_byte {|b| j = (j << 8) + b} - j - elsif objtype == :oid - # cf X.690 pgh 8.19 for an explanation of this algorithm. - # Potentially not good enough. We may need a BerIdentifiedOid - # as a subclass of BerIdentifiedArray, to get the ber identifier - # and also a to_s method that produces the familiar dotted notation. - oid = newobj.unpack("w*") - f = oid.shift - g = if f < 40 - [0, f] - elsif f < 80 - [1, f-40] - else - [2, f-80] # f-80 can easily be > 80. What a weird optimization. - end - oid.unshift g.last - oid.unshift g.first - oid - elsif objtype == :array - #seq = [] - seq = BerIdentifiedArray.new - seq.ber_identifier = id - sio = StringIO.new( newobj || "" ) - # Interpret the subobject, but note how the loop - # is built: nil ends the loop, but false (a valid - # BER value) does not! - while (e = sio.read_ber(syntax)) != nil - seq << e + # This condenses our nicely self-documenting ASN hashes down + # to an array for fast lookups. + # Scoped to be called as a module method, but not intended for + # user code to call. + # + def self.compile_syntax(syn) + out = [nil] * 256 + syn.each do |tclass, tclasses| + tagclass = {:universal=>0, :application=>64, :context_specific=>128, :private=>192} [tclass] + tclasses.each do |codingtype,codings| + encoding = {:primitive=>0, :constructed=>32} [codingtype] + codings.each {|tag, objtype| out[tagclass + encoding + tag] = objtype } end - seq - elsif objtype == :boolean - newobj != "\000" - elsif objtype == :null - n = BerIdentifiedNull.new - n.ber_identifier = id - n - else - #raise BerError.new( "unsupported object type: class=#{tagclass}, encoding=#{encoding}, tag=#{tag}" ) - raise BerError.new( "unsupported object type: id=#{id}" ) end - - # Add the identifier bits into the object if it's a String or an Array. - # We can't add extra stuff to Fixnums and booleans, not that it makes much sense anyway. - # Replaced this mechanism with subclasses because the instance_eval profiled too hot. - #obj and ([String,Array].include? obj.class) and obj.instance_eval "def ber_identifier; #{id}; end" - #obj.ber_identifier = id if obj.respond_to?(:ber_identifier) - obj - + out end - #-- - # Violates DRY! This replicates the functionality of #read_ber. - # Eventually this method may replace that one. - # This version of #read_ber behaves properly in the face of incomplete - # data packets. If a full BER object is detected, we return an array containing - # the detected object and the number of bytes consumed from the string. - # If we don't detect a complete packet, return nil. - # - # Observe that weirdly we recursively call the original #read_ber in here. - # That needs to be fixed if we ever obsolete the original method in favor of this one. - def read_ber_from_string str, syntax=nil - id = str[0] or return nil - n = str[1] or return nil - n_consumed = 2 - lengthlength,contentlength = if n <= 127 - [1,n] - else - n1 = n & 127 - return nil unless str.length >= (n_consumed + n1) - j = 0 - n1.times { - j = (j << 8) + str[n_consumed] - n_consumed += 1 - } - [1 + (n1), j] - end - - return nil unless str.length >= (n_consumed + contentlength) - newobj = str[n_consumed...(n_consumed + contentlength)] - n_consumed += contentlength - - objtype = (syntax && syntax[id]) || BuiltinSyntax[id] - - # == is expensive so sort this if/else so the common cases are at the top. - obj = if objtype == :array - seq = BerIdentifiedArray.new - seq.ber_identifier = id - sio = StringIO.new( newobj || "" ) - # Interpret the subobject, but note how the loop - # is built: nil ends the loop, but false (a valid - # BER value) does not! - # Also, we can use the standard read_ber method because - # we know for sure we have enough data. (Although this - # might be faster than the standard method.) - while (e = sio.read_ber(syntax)) != nil - seq << e - end - seq - elsif objtype == :string - s = BerIdentifiedString.new( newobj || "" ) - s.ber_identifier = id - s - elsif objtype == :integer - j = 0 - newobj.each_byte {|b| j = (j << 8) + b} - j - elsif objtype == :oid - # cf X.690 pgh 8.19 for an explanation of this algorithm. - # Potentially not good enough. We may need a BerIdentifiedOid - # as a subclass of BerIdentifiedArray, to get the ber identifier - # and also a to_s method that produces the familiar dotted notation. - oid = newobj.unpack("w*") - f = oid.shift - g = if f < 40 - [0,f] - elsif f < 80 - [1, f-40] - else - [2, f-80] # f-80 can easily be > 80. What a weird optimization. - end - oid.unshift g.last - oid.unshift g.first - oid - elsif objtype == :boolean - newobj != "\000" - elsif objtype == :null - n = BerIdentifiedNull.new - n.ber_identifier = id - n - else - raise BerError.new( "unsupported object type: id=#{id}" ) - end - - [obj, n_consumed] - end - - end # module BERParser - end # module BER - -end # module Net - - -class IO - include Net::BER::BERParser -end - -require "stringio" -class StringIO - include Net::BER::BERParser -end - -begin - require 'openssl' - class OpenSSL::SSL::SSLSocket - include Net::BER::BERParser - end -rescue LoadError -# Ignore LoadError. -# DON'T ignore NameError, which means the SSLSocket class -# is somehow unavailable on this implementation of Ruby's openssl. -# This may be WRONG, however, because we don't yet know how Ruby's -# openssl behaves on machines with no OpenSSL library. I suppose -# it's possible they do not fail to require 'openssl' but do not -# create the classes. So this code is provisional. -# Also, you might think that OpenSSL::SSL::SSLSocket inherits from -# IO so we'd pick it up above. But you'd be wrong. -end - - - -class String - include Net::BER::BERParser - def read_ber syntax=nil - StringIO.new(self).read_ber(syntax) + def to_ber + # Provisional implementation. + # We ASSUME that our incoming value is an array, and we + # use the Array#to_ber_oid method defined below. + # We probably should obsolete that method, actually, in + # and move the code here. + # WE ARE NOT CURRENTLY ENCODING THE BER-IDENTIFIER. + # This implementation currently hardcodes 6, the universal OID tag. + ary = @value.dup + first = ary.shift + raise Net::BER::BerError.new(" invalid OID" ) unless [0,1,2].include?(first) + first = first * 40 + ary.shift + ary.unshift first + oid = ary.pack("w*") + [6, oid.length].pack("CC") + oid end - def read_ber! syntax=nil - obj,n_consumed = read_ber_from_string(self, syntax) - if n_consumed - self.slice!(0...n_consumed) - obj - else - nil - end - end -end - -#---------------------------------------------- - - -class FalseClass - # - # to_ber - # - def to_ber - "\001\001\000" end end - -class TrueClass - # - # to_ber - # - def to_ber - "\001\001\001" - end -end - - - -class Fixnum - # - # to_ber - # - def to_ber - "\002" + to_ber_internal - end - - # - # to_ber_enumerated - # - def to_ber_enumerated - "\012" + to_ber_internal - end - - # - # to_ber_length_encoding - # - def to_ber_length_encoding - if self <= 127 - [self].pack('C') - else - i = [self].pack('N').sub(/^[\0]+/,"") - [0x80 + i.length].pack('C') + i +module Net + module BER + class BerError < StandardError; end + + class BerIdentifiedString < String + attr_accessor :ber_identifier + def initialize args + super args + end end - end - - # Generate a BER-encoding for an application-defined INTEGER. - # Example: SNMP's Counter, Gauge, and TimeTick types. - # - def to_ber_application tag - [0x40 + tag].pack("C") + to_ber_internal - end - - #-- - # Called internally to BER-encode the length and content bytes of a Fixnum. - # The caller will prepend the tag byte. - def to_ber_internal - # PLEASE optimize this code path. It's awfully ugly and probably slow. - # It also doesn't understand negative numbers yet. - raise Net::BER::BerError.new( "range error in fixnum" ) unless self >= 0 - z = [self].pack("N") - zlen = if self < 0x80 - 1 - elsif self < 0x8000 - 2 - elsif self < 0x800000 - 3 - else - 4 + + class BerIdentifiedArray < Array + attr_accessor :ber_identifier + def initialize(*args) + super + end end - [zlen].pack("C") + z[0-zlen,zlen] - end - private :to_ber_internal - -end # class Fixnum - - -class Bignum - - def to_ber - #i = [self].pack('w') - #i.length > 126 and raise Net::BER::BerError.new( "range error in bignum" ) - #[2, i.length].pack("CC") + i - - # Ruby represents Bignums as two's-complement numbers so we may actually be - # good as far as representing negatives goes. - # I'm sure this implementation can be improved performance-wise if necessary. - # Ruby's Bignum#size returns the number of bytes in the internal representation - # of the number, but it can and will include leading zero bytes on at least - # some implementations. Evidently Ruby stores these as sets of quadbytes. - # It's not illegal in BER to encode all of the leading zeroes but let's strip - # them out anyway. - # - sz = self.size - out = "\000" * sz - (sz*8).times {|bit| - if self[bit] == 1 - out[bit/8] += (1 << (bit % 8)) - end - } - - while out.length > 1 and out[-1] == 0 - out.slice!(-1,1) + + class BerIdentifiedNull + attr_accessor :ber_identifier + def to_ber + "\005\000" + end end - - [2, out.length].pack("CC") + out.reverse end - end - - -class String - # - # to_ber - # A universal octet-string is tag number 4, - # but others are possible depending on the context, so we - # let the caller give us one. - # The preferred way to do this in user code is via to_ber_application_sring - # and to_ber_contextspecific. - # - def to_ber code = 4 - [code].pack('C') + length.to_ber_length_encoding + self - end - - # - # to_ber_application_string - # - def to_ber_application_string code - to_ber( 0x40 + code ) - end - - # - # to_ber_contextspecific - # - def to_ber_contextspecific code - to_ber( 0x80 + code ) - end - -end # class String - - - -class Array - # - # to_ber_appsequence - # An application-specific sequence usually gets assigned - # a tag that is meaningful to the particular protocol being used. - # This is different from the universal sequence, which usually - # gets a tag value of 16. - # Now here's an interesting thing: We're adding the X.690 - # "application constructed" code at the top of the tag byte (0x60), - # but some clients, notably ldapsearch, send "context-specific - # constructed" (0xA0). The latter would appear to violate RFC-1777, - # but what do I know? We may need to change this. - # - - def to_ber id = 0; to_ber_seq_internal( 0x30 + id ); end - def to_ber_set id = 0; to_ber_seq_internal( 0x31 + id ); end - def to_ber_sequence id = 0; to_ber_seq_internal( 0x30 + id ); end - def to_ber_appsequence id = 0; to_ber_seq_internal( 0x60 + id ); end - def to_ber_contextspecific id = 0; to_ber_seq_internal( 0xA0 + id ); end - - def to_ber_oid - ary = self.dup - first = ary.shift - raise Net::BER::BerError.new( "invalid OID" ) unless [0,1,2].include?(first) - first = first * 40 + ary.shift - ary.unshift first - oid = ary.pack("w*") - [6, oid.length].pack("CC") + oid - end - - private - def to_ber_seq_internal code - s = '' - self.each{|x| s = s + x} - [code].pack('C') + s.length.to_ber_length_encoding + s - end - - -end # class Array - - +require 'net/ber/ber_parser'