From 473a777faef50b61b673370fe1241e96e48c5ad1 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 16 Apr 2006 09:38:52 +0000 Subject: [PATCH] Moved the netxxx scripts down to net/xxx. --- lib/net/ber.rb | 282 ++++++++++++++++++++++++++++++++++++++++++ lib/net/ldap.rb | 2 - lib/net/ldapfilter.rb | 185 +++++++++++++++++++++++++++ lib/net/ldappdu.rb | 154 +++++++++++++++++++++++ 4 files changed, 621 insertions(+), 2 deletions(-) create mode 100644 lib/net/ber.rb create mode 100644 lib/net/ldapfilter.rb create mode 100644 lib/net/ldappdu.rb diff --git a/lib/net/ber.rb b/lib/net/ber.rb new file mode 100644 index 0000000..21f4dcc --- /dev/null +++ b/lib/net/ber.rb @@ -0,0 +1,282 @@ +# $Id$ +# +# NET::BER +# Mixes ASN.1/BER convenience methods into several standard classes. +# Also provides BER parsing functionality. +# +#---------------------------------------------------------------------------- +# +# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved. +# +# Gmail: garbagecat10 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# 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 < Exception; 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 = { + :universal => { + :primitive => { + 1 => :boolean, + 2 => :integer, + 4 => :string, + 10 => :integer, + }, + :constructed => { + 16 => :array, + 17 => :array + } + } + } + + # + # 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. + # + def read_ber syntax=nil + eof? and return nil + + id = getc # 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 = getc + lengthlength,contentlength = if n <= 127 + [1,n] + else + j = (0...(n & 127)).inject(0) {|mem,x| mem = (mem << 8) + getc} + [1 + (n & 127), j] + end + + newobj = read contentlength + + objtype = nil + [syntax, BuiltinSyntax].each {|syn| + if syn && (ot = syn[tagclass]) && (ot = ot[encoding]) && ot[tag] + objtype = ot[tag] + break + end + } + + obj = case objtype + when :boolean + newobj != "\000" + when :string + (newobj || "").dup + when :integer + j = 0 + newobj.each_byte {|b| j = (j << 8) + b} + j + when :array + seq = [] + sio = StringIO.new( newobj || "" ) + while e = sio.read_ber(syntax); seq << e; end + seq + else + raise BerError.new( "unsupported object type: class=#{tagclass}, encoding=#{encoding}, tag=#{tag}" ) + 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. + obj and ([String,Array].include? obj.class) and obj.instance_eval "def ber_identifier; #{id}; end" + obj + + 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 + + +class String + def read_ber syntax=nil + StringIO.new(self).read_ber(syntax) + 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 + i = [self].pack('w') + [2, i.length].pack("CC") + i + end + + # + # to_ber_enumerated + # + def to_ber_enumerated + i = [self].pack('w') + [10, i.length].pack("CC") + i + 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 + end + end + +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 + 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. + # + def to_ber code = 4 + [code].pack('C') + length.to_ber_length_encoding + self + end + + # + # to_ber_application_string + # TODO. WARNING, IS THIS WRONG? Shouldn't app-specific string + # have a prefix of 0x40? + # + def to_ber_application_string code + to_ber( 0x80 + 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 + + private + def to_ber_seq_internal code + s = self.to_s + [code].pack('C') + s.length.to_ber_length_encoding + s + end + +end # class Array + + + +#---------------------------------------------- + +if __FILE__ == $0 + puts "No default action" +end + + + diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 8d8a73e..2ac179f 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -39,8 +39,6 @@ # -#require 'rubygems' -#require_gem "eventmachine", ">= 0.3.1" require 'socket' diff --git a/lib/net/ldapfilter.rb b/lib/net/ldapfilter.rb new file mode 100644 index 0000000..97c4e86 --- /dev/null +++ b/lib/net/ldapfilter.rb @@ -0,0 +1,185 @@ +# $Id$ +# +# +#---------------------------------------------------------------------------- +# +# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved. +# +# Gmail: garbagecat10 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# 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 +class LDAP + +class Filter + + def initialize op, a, b + @op = op + @left = a + @right = b + end + + def Filter::eq a, b; Filter.new :eq, a, b; end + def Filter::ne a, b; Filter.new :ne, a, b; end + def Filter::gt a, b; Filter.new :gt, a, b; end + def Filter::lt a, b; Filter.new :lt, a, b; end + def Filter::ge a, b; Filter.new :ge, a, b; end + def Filter::le a, b; Filter.new :le, a, b; end + + def & a; Filter.new :and, self, a; end + def | a; Filter.new :or, self, a; end + + # This operator can't be !, evidently. Try it. + def ~@; Filter.new :not, self, nil; end + + def to_s + case @op + when :ne + "(!(#{@left}=#{@right}))" + when :eq + "(#{@left}=#{@right})" + when :gt + "#{@left}>#{@right}" + when :lt + "#{@left}<#{@right}" + when :ge + "#{@left}>=#{@right}" + when :le + "#{@left}<=#{@right}" + when :and + "(&(#{@left})(#{@right}))" + when :or + "(|(#{@left})(#{@right}))" + when :not + "(!(#{@left}))" + else + raise "invalid or unsupported operator in LDAP Filter" + end + end + + + # + # to_ber + # Filter ::= + # CHOICE { + # and [0] SET OF Filter, + # or [1] SET OF Filter, + # not [2] Filter, + # equalityMatch [3] AttributeValueAssertion, + # substrings [4] SubstringFilter, + # greaterOrEqual [5] AttributeValueAssertion, + # lessOrEqual [6] AttributeValueAssertion, + # present [7] AttributeType, + # approxMatch [8] AttributeValueAssertion + # } + # + # SubstringFilter + # SEQUENCE { + # type AttributeType, + # SEQUENCE OF CHOICE { + # initial [0] LDAPString, + # any [1] LDAPString, + # final [2] LDAPString + # } + # } + # + # Parsing substrings is a little tricky. + # We use the split method to break a string into substrings + # delimited by the * (star) character. But we also need + # to know whether there is a star at the head and tail + # of the string. A Ruby particularity comes into play here: + # if you split on * and the first character of the string is + # a star, then split will return an array whose first element + # is an _empty_ string. But if the _last_ character of the + # string is star, then split will return an array that does + # _not_ add an empty string at the end. So we have to deal + # with all that specifically. + # + def to_ber + case @op + when :eq + if @right == "*" # present + @left.to_ber_application_string 7 + elsif @right =~ /[\*]/ #substring + ary = @right.split( /[\*]+/ ) + final_star = @right =~ /[\*]$/ + initial_star = ary.first == "" and ary.shift + + seq = [] + unless initial_star + seq << ary.shift.to_ber_contextspecific(0) + end + n_any_strings = ary.length - (final_star ? 0 : 1) + p n_any_strings + n_any_strings.times { + seq << ary.shift.to_ber_contextspecific(1) + } + unless final_star + seq << ary.shift.to_ber_contextspecific(2) + end + [@left.to_ber, seq.to_ber].to_ber_contextspecific 4 + else #equality + [@left.to_ber, @right.to_ber].to_ber_contextspecific 3 + end + when :and + ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten + ary.map {|a| a.to_ber}.to_ber_contextspecific( 0 ) + when :or + ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten + ary.map {|a| a.to_ber}.to_ber_contextspecific( 1 ) + when :not + [@left.to_ber].to_ber_contextspecific 2 + else + # ERROR, we'll return objectclass=* to keep things from blowing up, + # but that ain't a good answer and we need to kick out an error of some kind. + raise "unimplemented search filter" + end + end + + # + # coalesce + # This is a private helper method for dealing with chains of ANDs and ORs + # that are longer than two. If BOTH of our branches are of the specified + # type of joining operator, then return both of them as an array (calling + # coalesce recursively). If they're not, then return an array consisting + # only of self. + # + def coalesce operator + if @op == operator + [@left.coalesce( operator ), @right.coalesce( operator )] + else + [self] + end + end + + +end # class Net::LDAP::Filter + +end # class Net::LDAP +end # module Net + + +#----------------------------------- + +if __FILE__ == $0 + puts "No default action" +end + diff --git a/lib/net/ldappdu.rb b/lib/net/ldappdu.rb new file mode 100644 index 0000000..236fb75 --- /dev/null +++ b/lib/net/ldappdu.rb @@ -0,0 +1,154 @@ +# $Id$ +# +# LDAP PDU support classes +# +# +#---------------------------------------------------------------------------- +# +# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved. +# +# Gmail: garbagecat10 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# 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 + + +class LdapPduError < Exception; end + + +class LdapPdu + + BindResult = 1 + SearchReturnedData = 4 + SearchResult = 5 + ModifyResponse = 7 + AddResponse = 9 + ModifyRDNResponse = 13 + + attr_reader :msg_id, :app_tag + attr_reader :search_dn, :search_attributes + + # + # initialize + # An LDAP PDU always looks like a BerSequence with + # two elements: an integer (message-id number), and + # an application-specific sequence. + # The application-specific tag in the sequence tells + # us what kind of packet it is, and each kind has its + # own format, defined in RFC-1777. + # Observe that many clients (such as ldapsearch) + # do not necessarily enforce the expected application + # tags on received protocol packets. This implementation + # does interpret the RFC strictly in this regard, and + # it remains to be seen whether there are servers out + # there that will not work well with our approach. + # + def initialize ber_object + begin + @msg_id = ber_object[0].to_i + @app_tag = ber_object[1].ber_identifier - 0x60 + rescue + # any error becomes a data-format error + raise LdapPduError.new( "ldap-pdu format error" ) + end + + case @app_tag + when BindResult + parse_ldap_result ber_object[1] + when SearchReturnedData + parse_search_return ber_object[1] + when SearchResult + parse_ldap_result ber_object[1] + when ModifyResponse + parse_ldap_result ber_object[1] + when AddResponse + parse_ldap_result ber_object[1] + when ModifyRDNResponse + parse_ldap_result ber_object[1] + else + raise LdapPduError.new( "unknown pdu-type: #{@app_tag}" ) + end + end + + # + # result_code + # This returns an LDAP result code taken from the PDU, + # but it will be nil if there wasn't a result code. + # That can easily happen depending on the type of packet. + # + def result_code code = :resultCode + @ldap_result and @ldap_result[code] + end + + + private + + # + # parse_ldap_result + # + def parse_ldap_result sequence + sequence.length >= 3 or raise LdapPduError + @ldap_result = {:resultCode => sequence[0], :matchedDN => sequence[1], :errorMessage => sequence[2]} + end + + # + # parse_search_return + # Definition from RFC 1777 (we're handling application-4 here) + # + # Search Response ::= + # CHOICE { + # entry [APPLICATION 4] SEQUENCE { + # objectName LDAPDN, + # attributes SEQUENCE OF SEQUENCE { + # AttributeType, + # SET OF AttributeValue + # } + # }, + # resultCode [APPLICATION 5] LDAPResult + # } + # + # We concoct a search response that is a hash of the returned attribute values. + # NOW OBSERVE CAREFULLY: WE ARE DOWNCASING THE RETURNED ATTRIBUTE NAMES. + # This is to make them more predictable for user programs, but it + # may not be a good idea. Maybe this should be configurable. + # + def parse_search_return sequence + sequence.length >= 2 or raise LdapPduError + @search_dn = sequence[0] + @search_attributes = {} + sequence[1].each {|seq| + @search_attributes[seq[0].downcase.intern] = seq[1] + } + end + + +end + + +end # module Net + +#------------------------------------------- + +if __FILE__ == $0 + puts "No default action for this file" +end + +