From eb3ab200f8d51f424286dab24efd10ae362fd8e5 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 14 Apr 2006 23:23:35 +0000 Subject: [PATCH 001/231] Ooops, I added my code in the top level. Provisionally added this branch to keep from messing things up. --- COPYING | 281 +++++++++++++++++++++++++++++++ README | 6 + RELEASE_NOTES | 6 + Rakefile | 33 ++++ lib/ldappdu.rb | 148 +++++++++++++++++ lib/netber.rb | 265 ++++++++++++++++++++++++++++++ lib/netldap.rb | 382 +++++++++++++++++++++++++++++++++++++++++++ lib/netldapfilter.rb | 185 +++++++++++++++++++++ tests/testber.rb | 43 +++++ tests/testem.rb | 8 + 10 files changed, 1357 insertions(+) create mode 100644 COPYING create mode 100644 README create mode 100644 RELEASE_NOTES create mode 100644 Rakefile create mode 100644 lib/ldappdu.rb create mode 100644 lib/netber.rb create mode 100644 lib/netldap.rb create mode 100644 lib/netldapfilter.rb create mode 100644 tests/testber.rb create mode 100644 tests/testem.rb diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..3b70c5b --- /dev/null +++ b/COPYING @@ -0,0 +1,281 @@ +. + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your + freedom to share and change it. By contrast, the GNU General Public + License is intended to guarantee your freedom to share and change free + software--to make sure the software is free for all its users. This + General Public License applies to most of the Free Software + Foundation's software and to any other program whose authors commit to + using it. (Some other Free Software Foundation software is covered by + the GNU Lesser General Public License instead.) You can apply it to + your programs, too. + + When we speak of free software, we are referring to freedom, not + price. Our General Public Licenses are designed to make sure that you + have the freedom to distribute copies of free software (and charge for + this service if you wish), that you receive source code or can get it + if you want it, that you can change the software or use pieces of it + in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid + anyone to deny you these rights or to ask you to surrender the rights. + These restrictions translate to certain responsibilities for you if you + distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether + gratis or for a fee, you must give the recipients all the rights that + you have. You must make sure that they, too, receive or can get the + source code. And you must show them these terms so they know their + rights. + + We protect your rights with two steps: (1) copyright the software, and + (2) offer you this license which gives you legal permission to copy, + distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain + that everyone understands that there is no warranty for this free + software. If the software is modified by someone else and passed on, we + want its recipients to know that what they have is not the original, so + that any problems introduced by others will not reflect on the original + authors' reputations. + + Finally, any free program is threatened constantly by software + patents. We wish to avoid the danger that redistributors of a free + program will individually obtain patent licenses, in effect making the + program proprietary. To prevent this, we have made it clear that any + patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and + modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains + a notice placed by the copyright holder saying it may be distributed + under the terms of this General Public License. The "Program", below, + refers to any such program or work, and a "work based on the Program" + means either the Program or any derivative work under copyright law: + that is to say, a work containing the Program or a portion of it, + either verbatim or with modifications and/or translated into another + language. (Hereinafter, translation is included without limitation in + the term "modification".) Each licensee is addressed as "you". + + Activities other than copying, distribution and modification are not + covered by this License; they are outside its scope. The act of + running the Program is not restricted, and the output from the Program + is covered only if its contents constitute a work based on the + Program (independent of having been made by running the Program). + Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's + source code as you receive it, in any medium, provided that you + conspicuously and appropriately publish on each copy an appropriate + copyright notice and disclaimer of warranty; keep intact all the + notices that refer to this License and to the absence of any warranty; + and give any other recipients of the Program a copy of this License + along with the Program. + + You may charge a fee for the physical act of transferring a copy, and + you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion + of it, thus forming a work based on the Program, and copy and + distribute such modifications or work under the terms of Section 1 + above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + + These requirements apply to the modified work as a whole. If + identifiable sections of that work are not derived from the Program, + and can be reasonably considered independent and separate works in + themselves, then this License, and its terms, do not apply to those + sections when you distribute them as separate works. But when you + distribute the same sections as part of a whole which is a work based + on the Program, the distribution of the whole must be on the terms of + this License, whose permissions for other licensees extend to the + entire whole, and thus to each and every part regardless of who wrote it. + + Thus, it is not the intent of this section to claim rights or contest + your rights to work written entirely by you; rather, the intent is to + exercise the right to control the distribution of derivative or + collective works based on the Program. + + In addition, mere aggregation of another work not based on the Program + with the Program (or with a work based on the Program) on a volume of + a storage or distribution medium does not bring the other work under + the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, + under Section 2) in object code or executable form under the terms of + Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + + The source code for a work means the preferred form of the work for + making modifications to it. For an executable work, complete source + code means all the source code for all modules it contains, plus any + associated interface definition files, plus the scripts used to + control compilation and installation of the executable. However, as a + special exception, the source code distributed need not include + anything that is normally distributed (in either source or binary + form) with the major components (compiler, kernel, and so on) of the + operating system on which the executable runs, unless that component + itself accompanies the executable. + + If distribution of executable or object code is made by offering + access to copy from a designated place, then offering equivalent + access to copy the source code from the same place counts as + distribution of the source code, even though third parties are not + compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program + except as expressly provided under this License. Any attempt + otherwise to copy, modify, sublicense or distribute the Program is + void, and will automatically terminate your rights under this License. + However, parties who have received copies, or rights, from you under + this License will not have their licenses terminated so long as such + parties remain in full compliance. + + 5. You are not required to accept this License, since you have not + signed it. However, nothing else grants you permission to modify or + distribute the Program or its derivative works. These actions are + prohibited by law if you do not accept this License. Therefore, by + modifying or distributing the Program (or any work based on the + Program), you indicate your acceptance of this License to do so, and + all its terms and conditions for copying, distributing or modifying + the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the + Program), the recipient automatically receives a license from the + original licensor to copy, distribute or modify the Program subject to + these terms and conditions. You may not impose any further + restrictions on the recipients' exercise of the rights granted herein. + You are not responsible for enforcing compliance by third parties to + this License. + + 7. If, as a consequence of a court judgment or allegation of patent + infringement or for any other reason (not limited to patent issues), + conditions are imposed on you (whether by court order, agreement or + otherwise) that contradict the conditions of this License, they do not + excuse you from the conditions of this License. If you cannot + distribute so as to satisfy simultaneously your obligations under this + License and any other pertinent obligations, then as a consequence you + may not distribute the Program at all. For example, if a patent + license would not permit royalty-free redistribution of the Program by + all those who receive copies directly or indirectly through you, then + the only way you could satisfy both it and this License would be to + refrain entirely from distribution of the Program. + + If any portion of this section is held invalid or unenforceable under + any particular circumstance, the balance of the section is intended to + apply and the section as a whole is intended to apply in other + circumstances. + + It is not the purpose of this section to induce you to infringe any + patents or other property right claims or to contest validity of any + such claims; this section has the sole purpose of protecting the + integrity of the free software distribution system, which is + implemented by public license practices. Many people have made + generous contributions to the wide range of software distributed + through that system in reliance on consistent application of that + system; it is up to the author/donor to decide if he or she is willing + to distribute software through any other system and a licensee cannot + impose that choice. + + This section is intended to make thoroughly clear what is believed to + be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in + certain countries either by patents or by copyrighted interfaces, the + original copyright holder who places the Program under this License + may add an explicit geographical distribution limitation excluding + those countries, so that distribution is permitted only in or among + countries not thus excluded. In such case, this License incorporates + the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions + of the General Public License from time to time. Such new versions will + be similar in spirit to the present version, but may differ in detail to + address new problems or concerns. + + Each version is given a distinguishing version number. If the Program + specifies a version number of this License which applies to it and "any + later version", you have the option of following the terms and conditions + either of that version or of any later version published by the Free + Software Foundation. If the Program does not specify a version number of + this License, you may choose any version ever published by the Free Software + Foundation. + + 10. If you wish to incorporate parts of the Program into other free + programs whose distribution conditions are different, write to the author + to ask for permission. For software which is copyrighted by the Free + Software Foundation, write to the Free Software Foundation; we sometimes + make exceptions for this. Our decision will be guided by the two goals + of preserving the free status of all derivatives of our free software and + of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY + FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN + OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES + PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED + OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS + TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE + PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, + REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING + WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR + REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, + INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING + OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED + TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY + YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER + PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE + POSSIBILITY OF SUCH DAMAGES. + diff --git a/README b/README new file mode 100644 index 0000000..c02bd34 --- /dev/null +++ b/README @@ -0,0 +1,6 @@ +# $Id$ +# +# + +README stub for Net::LDAP + diff --git a/RELEASE_NOTES b/RELEASE_NOTES new file mode 100644 index 0000000..bc54d10 --- /dev/null +++ b/RELEASE_NOTES @@ -0,0 +1,6 @@ +# $Id$ +# +# + +Release notes for Net::LDAP. + diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..2060bfa --- /dev/null +++ b/Rakefile @@ -0,0 +1,33 @@ +# $Id$ +# Rakefile for the netldap ruby gem. +# + + +require 'rubygems' +Gem::manage_gems +require 'rake/gempackagetask' + +em_version = "0.0.1" + +spec = Gem::Specification.new {|s| + s.name = "netldap" + s.version = em_version + s.author = "Francis Cianfrocca" + s.email = "garbagecat10@gmail.com" + s.homepage = "netldap@rubyforge.org" + s.summary = "Net::LDAP library" + s.files = FileList["{bin,tests,lib}/**/*"].exclude("rdoc").to_a + s.require_paths = ["lib"] + s.autorequire = "netldap" + s.test_file = "tests/testem.rb" + s.has_rdoc = true + s.extra_rdoc_files = ["README", "RELEASE_NOTES", "COPYING"] +} + +Rake::GemPackageTask.new( spec ) {|pkg| + pkg.need_tar = true +} + + +task :default => ["pkg/netldap-#{em_version}.gem"] + diff --git a/lib/ldappdu.rb b/lib/ldappdu.rb new file mode 100644 index 0000000..eb18f34 --- /dev/null +++ b/lib/ldappdu.rb @@ -0,0 +1,148 @@ +# $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. + # + 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. Should make this somehow a configurable parameter. + # + 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 + + diff --git a/lib/netber.rb b/lib/netber.rb new file mode 100644 index 0000000..ec61cd7 --- /dev/null +++ b/lib/netber.rb @@ -0,0 +1,265 @@ +# $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: garbagecat20 +# +# 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 + + TagClasses = [:universal, :application, :context_specific, :private] + + # This module is for mixing into IO and IO-like objects. + module BERParser + + BuiltinSyntax = { + :universal => { + 1 => :boolean, + 2 => :integer, + 4 => :string, + 10 => :integer, + 16 => :array, + 17 => :array, + } + } + + # + # read_ber + # + 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 ] + constructed = (id & 0x20 != 0) + + 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 = (ot = BuiltinSyntax[tagclass]) && ot[tag] + objtype = objtype || (syntax && (ot = syntax[tagclass]) && ot[tag]) + obj = case objtype + when :boolean + raise BerError.new( "boolean unimplemented- fix this now, dummy" ) + 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}, 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 + 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/netldap.rb b/lib/netldap.rb new file mode 100644 index 0000000..09acf6a --- /dev/null +++ b/lib/netldap.rb @@ -0,0 +1,382 @@ +# $Id$ +# +# Net::LDAP for Ruby +# +# +# +# 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 +# +# +# == Miscellaneous +# +# For reasons relating to the source-code layout, this file doesn't +# require all the outboard stuff it actually needs, like netber. +# Until we figure out how to do that without damaging the directory +# structure, we're reliant on user programs to explicitly require +# everything, and in the correct order too! +# +# == BUGS: +# +# Try querying the objectGUID attribute from an A/D. It's a binary value +# which we're reading correctly, but we need to make sure it gets base64-encoded +# if we're going to put it out to an LDIF. +# + + +#require 'rubygems' +#require_gem "eventmachine", ">= 0.3.1" + +require 'socket' + + +module Net + + + # + # class LDAP + # + class LDAP + + class LdapError < Exception; end + + AsnSyntax = { + :application => { + 0 => :array, # BindRequest + 1 => :array, # BindResponse + 2 => :array, # UnbindRequest + 3 => :array, # SearchRequest + 4 => :array, # SearchData + 5 => :array, # SearchResult + 6 => :array, # ModifyRequest + 7 => :array, # ModifyResponse + 8 => :array, # AddRequest + 9 => :array, # AddResponse + 10 => :array, # DelRequest + 11 => :array, # DelResponse + 12 => :array, # ModifyRdnRequest + 13 => :array, # ModifyRdnResponse + 14 => :array, # CompareRequest + 15 => :array, # CompareResponse + 16 => :array, # AbandonRequest + }, + :context_specific => { + 0 => :string, # password + 1 => :string, # Kerberos v4 + 2 => :string, # Kerberos v5 + } + } + + DefaultHost = "127.0.0.1" + DefaultPort = 389 + DefaultAuth = {:method => :anonymous} + + + ResultStrings = { + 0 => "Success", + 1 => "Operations Error", + 16 => "No Such Attribute", + 17 => "Undefined Attribute Type", + 20 => "Attribute or Value Exists", + 32 => "No Such Object", + 34 => "Invalid DN Syntax", + 48 => "Invalid DN Syntax", + 48 => "Inappropriate Authentication", + 49 => "Invalid Credentials", + 50 => "Insufficient Access Rights", + 51 => "Busy", + 52 => "Unavailable", + 53 => "Unwilling to perform", + 68 => "Entry Already Exists" + } + + # + # LDAP::result2string + # + def LDAP::result2string code + ResultStrings[code] || "unknown result (#{code})" + end + + # + # initialize + # + def initialize args + @host = args[:host] || DefaultHost + @port = args[:port] || DefaultPort + @verbose = false # Make this configurable with a switch on the class. + @auth = args[:auth] || DefaultAuth + + # This variable is only set when we are created with LDAP::open. + # All of our internal methods will connect using it, or else + # they will create their own. + @open_connection = nil + end + + # + # open + # + def LDAP::open + end + + # + # search + # + def search args + conn = Connection.new( :host => @host, :port => @port ) + # TODO, hardcoded Ldap result code in next line + (rc = conn.bind @auth) == 0 or return rc + result_code = conn.search( args ) {|values| + block_given? and yield( values ) + } + result_code + end + + # + # bind + # Bind and unbind. + # Can serve as a connectivity test as well as an auth test. + # + def bind + conn = Connection.new( :host => @host, :port => @port ) + conn.bind @auth + end + + # + # bind_as + # This is for testing authentication credentials. + # Most likely a "standard" name (like a CN or an email + # address) will be presented along with a password. + # We'll bind with the main credential given in the + # constructor, query the full DN of the user given + # to us as a parameter, then unbind and rebind as the + # new user. + # + def bind_as + end + + # + # add + # Add a full RDN to the remote DIS. + # + def add args + conn = Connection.new( :host => @host, :port => @port ) + # TODO, hardcoded Ldap result code in next line + (rc = conn.bind @auth) == 0 or return rc + conn.add( args ) + end + + + # + # modify + # Modify the attributes of an entry on the remote DIS. + # + def modify args + conn = Connection.new( :host => @host, :port => @port ) + # TODO, hardcoded Ldap result code in next line + (rc = conn.bind @auth) == 0 or return rc + conn.modify( args ) + end + + # + # rename + # Rename an entry on the remote DIS by changing the last RDN of its DN. + # + def rename args + conn = Connection.new( :host => @host, :port => @port ) + # TODO, hardcoded Ldap result code in next line + (rc = conn.bind @auth) == 0 or return rc + conn.rename( args ) + end + + end # class LDAP + + + + class LDAP + class Connection + + LdapVersion = 3 + + + # + # initialize + # + def initialize server + begin + @conn = TCPsocket.new( server[:host], server[:port] ) + rescue + raise LdapError.new( "no connection to server" ) + end + + block_given? and yield self + end + + # + # next_msgid + # + def next_msgid + @msgid ||= 0 + @msgid += 1 + end + + + # + # bind + # + def bind auth + user,psw = case auth[:method] + when :anonymous + ["",""] + when :simple + [auth[:username] || auth[:dn], auth[:password]] + end + raise LdapError.new( "invalid binding information" ) unless (user && psw) + + msgid = next_msgid.to_ber + request = [LdapVersion.to_ber, user.to_ber, psw.to_ber_contextspecific(0)].to_ber_appsequence(0) + request_pkt = [msgid, request].to_ber_sequence + @conn.write request_pkt + + (be = @conn.read_ber(AsnSyntax) and pdu = Net::LdapPdu.new( be )) or raise LdapError.new( "no bind result" ) + pdu.result_code + end + + # + # search + # TODO, certain search parameters are hardcoded. + # TODO, if we mis-parse the server results or the results are wrong, we can block + # forever. That's because we keep reading results until we get a type-5 packet, + # which might never come. We need to support the time-limit in the protocol. + # + def search args + search_filter = (args && args[:filter]) || Filter.eq( "objectclass", "*" ) + search_base = (args && args[:base]) || "dc=example,dc=com" + search_attributes = ((args && args[:attributes]) || []).map {|attr| attr.to_s.to_ber} + request = [ + search_base.to_ber, + 2.to_ber_enumerated, + 0.to_ber_enumerated, + 0.to_ber, + 0.to_ber, + false.to_ber, + search_filter.to_ber, + search_attributes.to_ber_sequence + ].to_ber_appsequence(3) + pkt = [next_msgid.to_ber, request].to_ber_sequence + @conn.write pkt + + search_results = {} + result_code = 0 + + while (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) + case pdu.app_tag + when 4 # search-data + search_results [pdu.search_dn] = pdu.search_attributes + when 5 # search-result + result_code = pdu.result_code + block_given? and yield( search_results ) + break + else + raise LdapError.new( "invalid response-type in search: #{pdu.app_tag}" ) + end + end + + result_code + end + + # + # modify + # TODO, need to support a time limit, in case the server fails to respond. + # TODO!!! We're throwing an exception here on empty DN. + # Should return a proper error instead, probaby from farther up the chain. + # TODO!!! If the user specifies a bogus opcode, we'll throw a + # confusing error here ("to_ber_enumerated is not defined on nil"). + # + def modify args + modify_dn = args[:dn] or raise "Unable to modify empty DN" + modify_ops = [] + a = args[:operations] and a.each {|op, attr, values| + # TODO, fix the following line, which gives a bogus error + # if the opcode is invalid. + op_1 = {:add => 0, :delete => 1, :replace => 2} [op.to_sym].to_ber_enumerated + modify_ops << [op_1, [attr.to_s.to_ber, values.to_a.map {|v| v.to_ber}.to_ber_set].to_ber_sequence].to_ber_sequence + } + + request = [modify_dn.to_ber, modify_ops.to_ber_sequence].to_ber_appsequence(6) + pkt = [next_msgid.to_ber, request].to_ber_sequence + @conn.write pkt + + (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 7) or raise LdapError.new( "response missing or invalid" ) + pdu.result_code + end + + + # + # add + # TODO, need to support a time limit, in case the server fails to respond. + # + def add args + add_dn = args[:dn] or raise LdapError.new("Unable to add empty DN") + add_attrs = [] + a = args[:attributes] and a.each {|k,v| + add_attrs << [ k.to_s.to_ber, v.to_a.map {|m| m.to_ber}.to_ber_set ].to_ber_sequence + } + + request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(8) + pkt = [next_msgid.to_ber, request].to_ber_sequence + @conn.write pkt + + (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 9) or raise LdapError.new( "response missing or invalid" ) + pdu.result_code + end + + + # + # rename + # TODO, need to support a time limit, in case the server fails to respond. + # + def rename args + old_dn = args[:olddn] or raise "Unable to rename empty DN" + new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN" + delete_attrs = args[:delete_attributes] ? true : false + + request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber].to_ber_appsequence(12) + pkt = [next_msgid.to_ber, request].to_ber_sequence + @conn.write pkt + + (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 13) or raise LdapError.new( "response missing or invalid" ) + pdu.result_code + end + + + end # class Connection + end # class LDAP + + +end # module Net + + +#------------------------------------------------------ + +if __FILE__ == $0 + puts "No default action" +end + + + + diff --git a/lib/netldapfilter.rb b/lib/netldapfilter.rb new file mode 100644 index 0000000..97c4e86 --- /dev/null +++ b/lib/netldapfilter.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/tests/testber.rb b/tests/testber.rb new file mode 100644 index 0000000..562c968 --- /dev/null +++ b/tests/testber.rb @@ -0,0 +1,43 @@ +# $Id$ +# +# + + +require 'lib/netber' +require 'lib/netldap' +require 'lib/ldappdu' +require 'lib/netldapfilter' +require 'stringio' + + +class TestBer < Test::Unit::TestCase + + def setup + end + + # TODO: Add some much bigger numbers + # 5000000000 is a Bignum, which hits different code. + def test_ber_integers + assert_equal( "\002\001\005", 5.to_ber ) + assert_equal( "\002\002\203t", 500.to_ber ) + assert_equal( "\002\003\203\206P", 50000.to_ber ) + assert_equal( "\002\005\222\320\227\344\000", 5000000000.to_ber ) + end + + def test_ber_parsing + assert_equal( 6, "\002\001\006".read_ber( Net::LDAP::AsnSyntax )) + assert_equal( "testing", "\004\007testing".read_ber( Net::LDAP::AsnSyntax )) + end + + + def test_ber_parser_on_ldap_bind_request + s = StringIO.new "0$\002\001\001`\037\002\001\003\004\rAdministrator\200\vad_is_bogus" + assert_equal( [1, [3, "Administrator", "ad_is_bogus"]], s.read_ber( Net::LDAP::AsnSyntax )) + end + + + + +end + + diff --git a/tests/testem.rb b/tests/testem.rb new file mode 100644 index 0000000..3249a26 --- /dev/null +++ b/tests/testem.rb @@ -0,0 +1,8 @@ +# $Id$ +# +# + +require 'test/unit' +require 'tests/testber' + + From 9b59d3fd93feb532d5240073a4a0cf3e73bd2818 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 15 Apr 2006 02:19:55 +0000 Subject: [PATCH 002/231] Fun little LDAP server to testing Net::LDAP --- testserver/ldapserver.rb | 201 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 testserver/ldapserver.rb diff --git a/testserver/ldapserver.rb b/testserver/ldapserver.rb new file mode 100644 index 0000000..4504159 --- /dev/null +++ b/testserver/ldapserver.rb @@ -0,0 +1,201 @@ +# $Id$ +# +# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved. +# Gmail account: garbagecat10. +# +# This is an LDAP server intended for unit testing of Net::LDAP. +# It implements as much of the protocol as we have the stomach +# to implement but serves static data. Use ldapsearch to test +# this server! +# +# To make this easier to write, we use the Ruby/EventMachine +# reactor library. +# + + +require 'stringio' + +#------------------------------------------------ + +class String + def read_ber! syntax=nil + s = StringIO.new self + pdu = s.read_ber(syntax) + if pdu + if s.eof? + slice!(0, length) + else + slice!(0, length - s.read.length) + end + end + pdu + end +end + + +module LdapServer + + LdapServerAsnSyntax = { + :application => { + :constructed => { + 0 => :array, # LDAP BindRequest + 3 => :array # LDAP SearchRequest + }, + :primitive => { + 2 => :string # ldapsearch sends this to unbind + } + }, + :context_specific => { + :primitive => { + 0 => :string # simple auth (password) + }, + } + } + + def post_init + $logger.info "Accepted LDAP connection" + @authenticated = false + end + + def receive_data data + @data ||= ""; @data << data + while pdu = @data.read_ber!(LdapServerAsnSyntax) + begin + handle_ldap_pdu pdu + rescue + $logger.error "closing connection due to error #{$!}" + close_connection + end + end + end + + def handle_ldap_pdu pdu + tag_id = pdu[1].ber_identifier + case tag_id + when 0x60 + handle_bind_request pdu + when 0x63 + handle_search_request pdu + when 0x42 + # bizarre thing, it's a null object (primitive application-2) + # sent by ldapsearch to request an unbind (or a kiss-off, not sure which) + close_connection_after_writing + else + $logger.error "received unknown packet-type #{tag_id}" + close_connection_after_writing + end + end + + def handle_bind_request pdu + # TODO, return a proper LDAP error instead of blowing up on version error + if pdu[1][0] != 3 + send_ldap_response 0, pdu[0].to_i, 2, "", "We only support version 3" + elsif pdu[1][1] != "cn=bigshot,dc=bayshorenetworks,dc=com" + send_ldap_response 0, pdu[0].to_i, 48, "", "Who are you?" + elsif pdu[1][2].ber_identifier != 0x80 + send_ldap_response 0, pdu[0].to_i, 7, "", "Keep it simple, man" + elsif pdu[1][2] != "opensesame" + send_ldap_response 0, pdu[0].to_i, 49, "", "Make my day" + else + @authenticated = true + send_ldap_response 0, pdu[0].to_i, 0, pdu[1][1], "I'll take it" + end + end + + def handle_search_request pdu + unless @authenticated + send_ldap_response 5, pdu[0].to_i, 50, "", "Who did you say you were?" + return + end + + treebase = pdu[1][0] + if treebase != "dc=bigdomain,dc=com" + send_ldap_response 5, pdu[0].to_i, 32, "", "unknown treebase" + return + end + +=begin +Search Response ::= + CHOICE { + entry [APPLICATION 4] SEQUENCE { + objectName LDAPDN, + attributes SEQUENCE OF SEQUENCE { + AttributeType, + SET OF AttributeValue + } + }, + resultCode [APPLICATION 5] LDAPResult + } +=end + + send_data( [ + pdu[0].to_i.to_ber, [ + "abcdefghijklmnopqrstuvwxyz".to_ber, [ + + [ + "mail".to_ber, ["aaa".to_ber, "bbb".to_ber, "ccc".to_ber].to_ber_set + ].to_ber_sequence, + [ + "objectclass".to_ber, ["111".to_ber, "222".to_ber, "333".to_ber].to_ber_set + ].to_ber_sequence, + [ + "cn".to_ber, ["CNCNCNCN".to_ber].to_ber_set + ].to_ber_sequence, + + ].to_ber_sequence + ].to_ber_appsequence(4) + ].to_ber_sequence) + + send_data( [ + pdu[0].to_i.to_ber, [ + "ABCDEFGHIJKLMNOPQRSTUVWXYZ".to_ber, [ + + [ + "mail".to_ber, ["aaa".to_ber, "bbb".to_ber, "ccc".to_ber].to_ber_set + ].to_ber_sequence, + [ + "objectclass".to_ber, ["111".to_ber, "222".to_ber, "333".to_ber].to_ber_set + ].to_ber_sequence, + [ + "cn".to_ber, ["CNCNCNCN".to_ber].to_ber_set + ].to_ber_sequence, + + ].to_ber_sequence + ].to_ber_appsequence(4) + ].to_ber_sequence) + + send_ldap_response 5, pdu[0].to_i, 0, "", "Was that what you wanted?" + end + + def send_ldap_response pkt_tag, msgid, code, dn, text + send_data( [msgid.to_ber, [code.to_ber, dn.to_ber, text.to_ber].to_ber_appsequence(pkt_tag) ].to_ber ) + end + +end + + +#------------------------------------------------ + +if __FILE__ == $0 + + require 'rubygems' + require 'eventmachine' + + require 'logger' + $logger = Logger.new $stderr + + $logger.info "adding ../lib to loadpath, to pick up dev version of Net::LDAP." + $:.unshift "../lib" + + require 'netber' + require 'ldappdu' + require 'netldap' + require 'netldapfilter' + + EventMachine.run { + $logger.info "starting LDAP server on 127.0.0.1 port 3890" + EventMachine.start_server "127.0.0.1", 3890, LdapServer + EventMachine.add_periodic_timer 60, proc {$logger.info "heartbeat"} + } +end + From 2f56d3e258d9ae67c0a74613e668f0bb871ef44a Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 15 Apr 2006 02:22:38 +0000 Subject: [PATCH 003/231] Filled in a few blanks, and reworked the mechanism for handling application-specific ASN.1 syntaxes. We now allow applications to define application tag numbers that are the same, as long as one is constructed and the other primitive. --- lib/netber.rb | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/lib/netber.rb b/lib/netber.rb index ec61cd7..b2a3f15 100644 --- a/lib/netber.rb +++ b/lib/netber.rb @@ -44,17 +44,25 @@ module Net BuiltinSyntax = { :universal => { - 1 => :boolean, - 2 => :integer, - 4 => :string, - 10 => :integer, - 16 => :array, - 17 => :array, + :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 @@ -63,7 +71,7 @@ module Net tag = id & 31 tag < 31 or raise BerError.new( "unsupported tag encoding: #{id}" ) tagclass = TagClasses[ id >> 6 ] - constructed = (id & 0x20 != 0) + encoding = (id & 0x20 != 0) ? :constructed : :primitive n = getc lengthlength,contentlength = if n <= 127 @@ -75,24 +83,30 @@ module Net newobj = read contentlength - objtype = (ot = BuiltinSyntax[tagclass]) && ot[tag] - objtype = objtype || (syntax && (ot = syntax[tagclass]) && ot[tag]) + 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 - raise BerError.new( "boolean unimplemented- fix this now, dummy" ) + newobj != "\000" when :string - newobj.dup + (newobj || "").dup when :integer j = 0 newobj.each_byte {|b| j = (j << 8) + b} j when :array seq = [] - sio = StringIO.new newobj + sio = StringIO.new( newobj || "" ) while e = sio.read_ber(syntax); seq << e; end seq else - raise BerError.new( "unsupported object type: class=#{tagclass}, tag=#{tag}" ) + 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. From 0d4ec9b2d51661a656eb02f5e02f07e3fac1b3d1 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 15 Apr 2006 04:16:20 +0000 Subject: [PATCH 004/231] Fixed a regression that happened when the Asn-syntax format changed. --- lib/netldap.rb | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/lib/netldap.rb b/lib/netldap.rb index 09acf6a..1c6eb20 100644 --- a/lib/netldap.rb +++ b/lib/netldap.rb @@ -57,28 +57,32 @@ module Net AsnSyntax = { :application => { - 0 => :array, # BindRequest - 1 => :array, # BindResponse - 2 => :array, # UnbindRequest - 3 => :array, # SearchRequest - 4 => :array, # SearchData - 5 => :array, # SearchResult - 6 => :array, # ModifyRequest - 7 => :array, # ModifyResponse - 8 => :array, # AddRequest - 9 => :array, # AddResponse - 10 => :array, # DelRequest - 11 => :array, # DelResponse - 12 => :array, # ModifyRdnRequest - 13 => :array, # ModifyRdnResponse - 14 => :array, # CompareRequest - 15 => :array, # CompareResponse - 16 => :array, # AbandonRequest + :constructed => { + 0 => :array, # BindRequest + 1 => :array, # BindResponse + 2 => :array, # UnbindRequest + 3 => :array, # SearchRequest + 4 => :array, # SearchData + 5 => :array, # SearchResult + 6 => :array, # ModifyRequest + 7 => :array, # ModifyResponse + 8 => :array, # AddRequest + 9 => :array, # AddResponse + 10 => :array, # DelRequest + 11 => :array, # DelResponse + 12 => :array, # ModifyRdnRequest + 13 => :array, # ModifyRdnResponse + 14 => :array, # CompareRequest + 15 => :array, # CompareResponse + 16 => :array, # AbandonRequest + } }, :context_specific => { - 0 => :string, # password - 1 => :string, # Kerberos v4 - 2 => :string, # Kerberos v5 + :primitive => { + 0 => :string, # password + 1 => :string, # Kerberos v4 + 2 => :string, # Kerberos v5 + } } } From 9147cf1254f1587c6480446a039a5655d4030510 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 15 Apr 2006 04:43:36 +0000 Subject: [PATCH 005/231] Started adding some LDAP-client test cases --- tests/testem.rb | 1 + tests/testldap.rb | 63 ++++++++++++++++++++++++++++++++++++++++ testserver/ldapserver.rb | 10 +++---- 3 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 tests/testldap.rb diff --git a/tests/testem.rb b/tests/testem.rb index 3249a26..ca38ff7 100644 --- a/tests/testem.rb +++ b/tests/testem.rb @@ -4,5 +4,6 @@ require 'test/unit' require 'tests/testber' +require 'tests/testldap' diff --git a/tests/testldap.rb b/tests/testldap.rb new file mode 100644 index 0000000..41c556d --- /dev/null +++ b/tests/testldap.rb @@ -0,0 +1,63 @@ +# $Id$ +# +# + + +require 'lib/netber' +require 'lib/netldap' +require 'lib/ldappdu' +require 'lib/netldapfilter' +require 'stringio' + + +class TestLdapClient < Test::Unit::TestCase + + def setup + @host = "127.0.0.1" + @port = 3890 + @auth = { + :method => :simple, + :username => "cn=bigshot,dc=bayshorenetworks,dc=com", + :password => "opensesame" + } + + end + + # Binding tests. + # Need tests for all kinds of network failures and incorrect auth. + # TODO: Implement a class-level timeout for operations like bind. + # Search has a timeout defined at the protocol level, other ops do not. + # TODO, use constants for the LDAP result codes, rather than hardcoding them. + def test_bind + ldap = Net::LDAP.new :host => @host, :port => @port, :auth => @auth + assert_equal( 0, ldap.bind ) + + bad_username = @auth.merge( {:username => "cn=badguy,dc=imposters,dc=com"} ) + ldap = Net::LDAP.new :host => @host, :port => @port, :auth => bad_username + assert_equal( 48, ldap.bind ) + + bad_password = @auth.merge( {:password => "cornhusk"} ) + ldap = Net::LDAP.new :host => @host, :port => @port, :auth => bad_password + assert_equal( 49, ldap.bind ) + end + + def test_search + ldap = Net::LDAP.new :host => @host, :port => @port, :auth => @auth + + search = {:base => "dc=smalldomain,dc=com"} + assert_equal( 32, ldap.search( search )) + + search = {:base => "dc=bigdomain,dc=com"} + assert_equal( 0, ldap.search( search )) + + ldap.search( search ) {|res| + p res + } + + + end + + +end + + diff --git a/testserver/ldapserver.rb b/testserver/ldapserver.rb index 4504159..e215dd5 100644 --- a/testserver/ldapserver.rb +++ b/testserver/ldapserver.rb @@ -89,16 +89,16 @@ module LdapServer def handle_bind_request pdu # TODO, return a proper LDAP error instead of blowing up on version error if pdu[1][0] != 3 - send_ldap_response 0, pdu[0].to_i, 2, "", "We only support version 3" + send_ldap_response 1, pdu[0].to_i, 2, "", "We only support version 3" elsif pdu[1][1] != "cn=bigshot,dc=bayshorenetworks,dc=com" - send_ldap_response 0, pdu[0].to_i, 48, "", "Who are you?" + send_ldap_response 1, pdu[0].to_i, 48, "", "Who are you?" elsif pdu[1][2].ber_identifier != 0x80 - send_ldap_response 0, pdu[0].to_i, 7, "", "Keep it simple, man" + send_ldap_response 1, pdu[0].to_i, 7, "", "Keep it simple, man" elsif pdu[1][2] != "opensesame" - send_ldap_response 0, pdu[0].to_i, 49, "", "Make my day" + send_ldap_response 1, pdu[0].to_i, 49, "", "Make my day" else @authenticated = true - send_ldap_response 0, pdu[0].to_i, 0, pdu[1][1], "I'll take it" + send_ldap_response 1, pdu[0].to_i, 0, pdu[1][1], "I'll take it" end end From 0c1310ea4aeb1acc624d868a5b1f0d57e8ca76e3 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 16 Apr 2006 01:29:05 +0000 Subject: [PATCH 006/231] Comments. --- lib/ldappdu.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/ldappdu.rb b/lib/ldappdu.rb index eb18f34..236fb75 100644 --- a/lib/ldappdu.rb +++ b/lib/ldappdu.rb @@ -54,6 +54,12 @@ class LdapPdu # 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 @@ -122,7 +128,7 @@ class LdapPdu # 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. Should make this somehow a configurable parameter. + # may not be a good idea. Maybe this should be configurable. # def parse_search_return sequence sequence.length >= 2 or raise LdapPduError From c9fb4256acd7b15b40b405e099b307855b16195f Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 16 Apr 2006 01:35:30 +0000 Subject: [PATCH 007/231] comments and minor cleanup. --- lib/netber.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/netber.rb b/lib/netber.rb index b2a3f15..a143527 100644 --- a/lib/netber.rb +++ b/lib/netber.rb @@ -8,7 +8,7 @@ # # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved. # -# Gmail: garbagecat20 +# 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 @@ -37,11 +37,14 @@ module Net class BerError < Exception; end - TagClasses = [:universal, :application, :context_specific, :private] # 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 => { From 777028007ed66c78df851e4cd803540c03621180 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 16 Apr 2006 09:16:14 +0000 Subject: [PATCH 008/231] Fixed bug in String#to_ber. Thanks Austin. --- lib/netber.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/netber.rb b/lib/netber.rb index a143527..21f4dcc 100644 --- a/lib/netber.rb +++ b/lib/netber.rb @@ -137,7 +137,7 @@ end class String def read_ber syntax=nil - StringIO.new(self).read_ber + StringIO.new(self).read_ber(syntax) end end From 7b049aad4aee72c55e5b83d6f9b3a8b624d0861b Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 16 Apr 2006 09:23:12 +0000 Subject: [PATCH 009/231] Added net subdirectory and started refactor to net/ldap. Thanks for the suggestion, Austin. --- lib/net/ldap.rb | 387 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 387 insertions(+) create mode 100644 lib/net/ldap.rb diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb new file mode 100644 index 0000000..8d8a73e --- /dev/null +++ b/lib/net/ldap.rb @@ -0,0 +1,387 @@ +# $Id$ +# +# Net::LDAP for Ruby +# +# +# +# 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 +# +# +# == Miscellaneous +# +# For reasons relating to the source-code layout, this file doesn't +# require all the outboard stuff it actually needs, like netber. +# Until we figure out how to do that without damaging the directory +# structure, we're reliant on user programs to explicitly require +# everything, and in the correct order too! +# +# == BUGS: +# +# Try querying the objectGUID attribute from an A/D. It's a binary value +# which we're reading correctly, but we need to make sure it gets base64-encoded +# if we're going to put it out to an LDIF. +# + + +#require 'rubygems' +#require_gem "eventmachine", ">= 0.3.1" + +require 'socket' + + +module Net + + + # + # class LDAP + # + class LDAP + + class LdapError < Exception; end + + AsnSyntax = { + :application => { + :constructed => { + 0 => :array, # BindRequest + 1 => :array, # BindResponse + 2 => :array, # UnbindRequest + 3 => :array, # SearchRequest + 4 => :array, # SearchData + 5 => :array, # SearchResult + 6 => :array, # ModifyRequest + 7 => :array, # ModifyResponse + 8 => :array, # AddRequest + 9 => :array, # AddResponse + 10 => :array, # DelRequest + 11 => :array, # DelResponse + 12 => :array, # ModifyRdnRequest + 13 => :array, # ModifyRdnResponse + 14 => :array, # CompareRequest + 15 => :array, # CompareResponse + 16 => :array, # AbandonRequest + } + }, + :context_specific => { + :primitive => { + 0 => :string, # password + 1 => :string, # Kerberos v4 + 2 => :string, # Kerberos v5 + } + } + } + + DefaultHost = "127.0.0.1" + DefaultPort = 389 + DefaultAuth = {:method => :anonymous} + + + ResultStrings = { + 0 => "Success", + 1 => "Operations Error", + 16 => "No Such Attribute", + 17 => "Undefined Attribute Type", + 20 => "Attribute or Value Exists", + 32 => "No Such Object", + 34 => "Invalid DN Syntax", + 48 => "Invalid DN Syntax", + 48 => "Inappropriate Authentication", + 49 => "Invalid Credentials", + 50 => "Insufficient Access Rights", + 51 => "Busy", + 52 => "Unavailable", + 53 => "Unwilling to perform", + 68 => "Entry Already Exists" + } + + # + # LDAP::result2string + # + def LDAP::result2string code + ResultStrings[code] || "unknown result (#{code})" + end + + # + # initialize + # + def initialize args + @host = args[:host] || DefaultHost + @port = args[:port] || DefaultPort + @verbose = false # Make this configurable with a switch on the class. + @auth = args[:auth] || DefaultAuth + + # This variable is only set when we are created with LDAP::open. + # All of our internal methods will connect using it, or else + # they will create their own. + @open_connection = nil + end + + # + # open + # + def LDAP::open + end + + # + # search + # + def search args + conn = Connection.new( :host => @host, :port => @port ) + # TODO, hardcoded Ldap result code in next line + (rc = conn.bind @auth) == 0 or return rc + result_code = conn.search( args ) {|values| + block_given? and yield( values ) + } + result_code + end + + # + # bind + # Bind and unbind. + # Can serve as a connectivity test as well as an auth test. + # + def bind + conn = Connection.new( :host => @host, :port => @port ) + conn.bind @auth + end + + # + # bind_as + # This is for testing authentication credentials. + # Most likely a "standard" name (like a CN or an email + # address) will be presented along with a password. + # We'll bind with the main credential given in the + # constructor, query the full DN of the user given + # to us as a parameter, then unbind and rebind as the + # new user. + # + def bind_as + end + + # + # add + # Add a full RDN to the remote DIS. + # + def add args + conn = Connection.new( :host => @host, :port => @port ) + # TODO, hardcoded Ldap result code in next line + (rc = conn.bind @auth) == 0 or return rc + conn.add( args ) + end + + + # + # modify + # Modify the attributes of an entry on the remote DIS. + # + def modify args + conn = Connection.new( :host => @host, :port => @port ) + # TODO, hardcoded Ldap result code in next line + (rc = conn.bind @auth) == 0 or return rc + conn.modify( args ) + end + + # + # rename + # Rename an entry on the remote DIS by changing the last RDN of its DN. + # + def rename args + conn = Connection.new( :host => @host, :port => @port ) + # TODO, hardcoded Ldap result code in next line + (rc = conn.bind @auth) == 0 or return rc + conn.rename( args ) + end + + end # class LDAP + + + + class LDAP + class Connection + + LdapVersion = 3 + + + # + # initialize + # + def initialize server + begin + @conn = TCPsocket.new( server[:host], server[:port] ) + rescue + raise LdapError.new( "no connection to server" ) + end + + block_given? and yield self + end + + # + # next_msgid + # + def next_msgid + @msgid ||= 0 + @msgid += 1 + end + + + # + # bind + # + def bind auth + user,psw = case auth[:method] + when :anonymous + ["",""] + when :simple + [auth[:username] || auth[:dn], auth[:password]] + end + raise LdapError.new( "invalid binding information" ) unless (user && psw) + + msgid = next_msgid.to_ber + request = [LdapVersion.to_ber, user.to_ber, psw.to_ber_contextspecific(0)].to_ber_appsequence(0) + request_pkt = [msgid, request].to_ber_sequence + @conn.write request_pkt + + (be = @conn.read_ber(AsnSyntax) and pdu = Net::LdapPdu.new( be )) or raise LdapError.new( "no bind result" ) + pdu.result_code + end + + # + # search + # TODO, certain search parameters are hardcoded. + # TODO, if we mis-parse the server results or the results are wrong, we can block + # forever. That's because we keep reading results until we get a type-5 packet, + # which might never come. We need to support the time-limit in the protocol. + # + def search args + search_filter = (args && args[:filter]) || Filter.eq( "objectclass", "*" ) + search_base = (args && args[:base]) || "dc=example,dc=com" + search_attributes = ((args && args[:attributes]) || []).map {|attr| attr.to_s.to_ber} + request = [ + search_base.to_ber, + 2.to_ber_enumerated, + 0.to_ber_enumerated, + 0.to_ber, + 0.to_ber, + false.to_ber, + search_filter.to_ber, + search_attributes.to_ber_sequence + ].to_ber_appsequence(3) + pkt = [next_msgid.to_ber, request].to_ber_sequence + @conn.write pkt + + search_results = {} + result_code = 0 + + while (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) + case pdu.app_tag + when 4 # search-data + search_results [pdu.search_dn] = pdu.search_attributes + when 5 # search-result + result_code = pdu.result_code + block_given? and yield( search_results ) + break + else + raise LdapError.new( "invalid response-type in search: #{pdu.app_tag}" ) + end + end + + result_code + end + + # + # modify + # TODO, need to support a time limit, in case the server fails to respond. + # TODO!!! We're throwing an exception here on empty DN. + # Should return a proper error instead, probaby from farther up the chain. + # TODO!!! If the user specifies a bogus opcode, we'll throw a + # confusing error here ("to_ber_enumerated is not defined on nil"). + # + def modify args + modify_dn = args[:dn] or raise "Unable to modify empty DN" + modify_ops = [] + a = args[:operations] and a.each {|op, attr, values| + # TODO, fix the following line, which gives a bogus error + # if the opcode is invalid. + op_1 = {:add => 0, :delete => 1, :replace => 2} [op.to_sym].to_ber_enumerated + modify_ops << [op_1, [attr.to_s.to_ber, values.to_a.map {|v| v.to_ber}.to_ber_set].to_ber_sequence].to_ber_sequence + } + + request = [modify_dn.to_ber, modify_ops.to_ber_sequence].to_ber_appsequence(6) + pkt = [next_msgid.to_ber, request].to_ber_sequence + @conn.write pkt + + (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 7) or raise LdapError.new( "response missing or invalid" ) + pdu.result_code + end + + + # + # add + # TODO, need to support a time limit, in case the server fails to respond. + # + def add args + add_dn = args[:dn] or raise LdapError.new("Unable to add empty DN") + add_attrs = [] + a = args[:attributes] and a.each {|k,v| + add_attrs << [ k.to_s.to_ber, v.to_a.map {|m| m.to_ber}.to_ber_set ].to_ber_sequence + } + + request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(8) + pkt = [next_msgid.to_ber, request].to_ber_sequence + @conn.write pkt + + (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 9) or raise LdapError.new( "response missing or invalid" ) + pdu.result_code + end + + + # + # rename + # TODO, need to support a time limit, in case the server fails to respond. + # + def rename args + old_dn = args[:olddn] or raise "Unable to rename empty DN" + new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN" + delete_attrs = args[:delete_attributes] ? true : false + + request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber].to_ber_appsequence(12) + pkt = [next_msgid.to_ber, request].to_ber_sequence + @conn.write pkt + + (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 13) or raise LdapError.new( "response missing or invalid" ) + pdu.result_code + end + + + end # class Connection + end # class LDAP + + +end # module Net + + +#------------------------------------------------------ + +if __FILE__ == $0 + puts "No default action" +end + + + + + From 473a777faef50b61b673370fe1241e96e48c5ad1 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 16 Apr 2006 09:38:52 +0000 Subject: [PATCH 010/231] 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 + + From 8920805bb2e1fcf5e1ee1a7b28db530c144f11ef Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 16 Apr 2006 09:40:01 +0000 Subject: [PATCH 011/231] Changed the includes to pick up the new net/xxx script locations. --- tests/testber.rb | 10 ++++++---- tests/testldap.rb | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/testber.rb b/tests/testber.rb index 562c968..997499d 100644 --- a/tests/testber.rb +++ b/tests/testber.rb @@ -3,10 +3,12 @@ # -require 'lib/netber' -require 'lib/netldap' -require 'lib/ldappdu' -require 'lib/netldapfilter' +$:.unshift "lib" + +require 'net/ber' +require 'net/ldap' +require 'net/ldappdu' +require 'net/ldapfilter' require 'stringio' diff --git a/tests/testldap.rb b/tests/testldap.rb index 41c556d..abffc6b 100644 --- a/tests/testldap.rb +++ b/tests/testldap.rb @@ -3,10 +3,12 @@ # -require 'lib/netber' -require 'lib/netldap' -require 'lib/ldappdu' -require 'lib/netldapfilter' +$:.unshift "lib" + +require 'net/ber' +require 'net/ldap' +require 'net/ldappdu' +require 'net/ldapfilter' require 'stringio' From fbacb5f8e06845dedbbad1ba99eaf0eaa2f6f5d0 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 16 Apr 2006 11:10:17 +0000 Subject: [PATCH 012/231] started an attribute test --- tests/testldap.rb | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/testldap.rb b/tests/testldap.rb index abffc6b..fc9f65f 100644 --- a/tests/testldap.rb +++ b/tests/testldap.rb @@ -14,6 +14,9 @@ require 'stringio' class TestLdapClient < Test::Unit::TestCase + # TODO: these tests crash and burn if the associated + # LDAP testserver isn't up and running. + def setup @host = "127.0.0.1" @port = 3890 @@ -53,13 +56,36 @@ class TestLdapClient < Test::Unit::TestCase assert_equal( 0, ldap.search( search )) ldap.search( search ) {|res| - p res + # STUB. + #p res } + end + def test_search_attributes + ldap = Net::LDAP.new :host => @host, :port => @port, :auth => @auth + assert_equal( 0, ldap.bind ) + + search = { + :base => "dc=bigdomain,dc=com", + :attributes => ["mail"] + } + assert_equal( 0, ldap.search( search )) + + ldap.search( search ) {|res| + # STUB. + p res + } end + def test_search_filters + end + + + + + end From 33e248cdc366bf3194a89f3986ce99e6124161cf Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 16 Apr 2006 11:52:37 +0000 Subject: [PATCH 013/231] removed files that now live in lib/net. --- lib/ldappdu.rb | 154 ----------------- lib/netber.rb | 282 ------------------------------- lib/netldap.rb | 386 ------------------------------------------- lib/netldapfilter.rb | 185 --------------------- 4 files changed, 1007 deletions(-) delete mode 100644 lib/ldappdu.rb delete mode 100644 lib/netber.rb delete mode 100644 lib/netldap.rb delete mode 100644 lib/netldapfilter.rb diff --git a/lib/ldappdu.rb b/lib/ldappdu.rb deleted file mode 100644 index 236fb75..0000000 --- a/lib/ldappdu.rb +++ /dev/null @@ -1,154 +0,0 @@ -# $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 - - diff --git a/lib/netber.rb b/lib/netber.rb deleted file mode 100644 index 21f4dcc..0000000 --- a/lib/netber.rb +++ /dev/null @@ -1,282 +0,0 @@ -# $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/netldap.rb b/lib/netldap.rb deleted file mode 100644 index 1c6eb20..0000000 --- a/lib/netldap.rb +++ /dev/null @@ -1,386 +0,0 @@ -# $Id$ -# -# Net::LDAP for Ruby -# -# -# -# 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 -# -# -# == Miscellaneous -# -# For reasons relating to the source-code layout, this file doesn't -# require all the outboard stuff it actually needs, like netber. -# Until we figure out how to do that without damaging the directory -# structure, we're reliant on user programs to explicitly require -# everything, and in the correct order too! -# -# == BUGS: -# -# Try querying the objectGUID attribute from an A/D. It's a binary value -# which we're reading correctly, but we need to make sure it gets base64-encoded -# if we're going to put it out to an LDIF. -# - - -#require 'rubygems' -#require_gem "eventmachine", ">= 0.3.1" - -require 'socket' - - -module Net - - - # - # class LDAP - # - class LDAP - - class LdapError < Exception; end - - AsnSyntax = { - :application => { - :constructed => { - 0 => :array, # BindRequest - 1 => :array, # BindResponse - 2 => :array, # UnbindRequest - 3 => :array, # SearchRequest - 4 => :array, # SearchData - 5 => :array, # SearchResult - 6 => :array, # ModifyRequest - 7 => :array, # ModifyResponse - 8 => :array, # AddRequest - 9 => :array, # AddResponse - 10 => :array, # DelRequest - 11 => :array, # DelResponse - 12 => :array, # ModifyRdnRequest - 13 => :array, # ModifyRdnResponse - 14 => :array, # CompareRequest - 15 => :array, # CompareResponse - 16 => :array, # AbandonRequest - } - }, - :context_specific => { - :primitive => { - 0 => :string, # password - 1 => :string, # Kerberos v4 - 2 => :string, # Kerberos v5 - } - } - } - - DefaultHost = "127.0.0.1" - DefaultPort = 389 - DefaultAuth = {:method => :anonymous} - - - ResultStrings = { - 0 => "Success", - 1 => "Operations Error", - 16 => "No Such Attribute", - 17 => "Undefined Attribute Type", - 20 => "Attribute or Value Exists", - 32 => "No Such Object", - 34 => "Invalid DN Syntax", - 48 => "Invalid DN Syntax", - 48 => "Inappropriate Authentication", - 49 => "Invalid Credentials", - 50 => "Insufficient Access Rights", - 51 => "Busy", - 52 => "Unavailable", - 53 => "Unwilling to perform", - 68 => "Entry Already Exists" - } - - # - # LDAP::result2string - # - def LDAP::result2string code - ResultStrings[code] || "unknown result (#{code})" - end - - # - # initialize - # - def initialize args - @host = args[:host] || DefaultHost - @port = args[:port] || DefaultPort - @verbose = false # Make this configurable with a switch on the class. - @auth = args[:auth] || DefaultAuth - - # This variable is only set when we are created with LDAP::open. - # All of our internal methods will connect using it, or else - # they will create their own. - @open_connection = nil - end - - # - # open - # - def LDAP::open - end - - # - # search - # - def search args - conn = Connection.new( :host => @host, :port => @port ) - # TODO, hardcoded Ldap result code in next line - (rc = conn.bind @auth) == 0 or return rc - result_code = conn.search( args ) {|values| - block_given? and yield( values ) - } - result_code - end - - # - # bind - # Bind and unbind. - # Can serve as a connectivity test as well as an auth test. - # - def bind - conn = Connection.new( :host => @host, :port => @port ) - conn.bind @auth - end - - # - # bind_as - # This is for testing authentication credentials. - # Most likely a "standard" name (like a CN or an email - # address) will be presented along with a password. - # We'll bind with the main credential given in the - # constructor, query the full DN of the user given - # to us as a parameter, then unbind and rebind as the - # new user. - # - def bind_as - end - - # - # add - # Add a full RDN to the remote DIS. - # - def add args - conn = Connection.new( :host => @host, :port => @port ) - # TODO, hardcoded Ldap result code in next line - (rc = conn.bind @auth) == 0 or return rc - conn.add( args ) - end - - - # - # modify - # Modify the attributes of an entry on the remote DIS. - # - def modify args - conn = Connection.new( :host => @host, :port => @port ) - # TODO, hardcoded Ldap result code in next line - (rc = conn.bind @auth) == 0 or return rc - conn.modify( args ) - end - - # - # rename - # Rename an entry on the remote DIS by changing the last RDN of its DN. - # - def rename args - conn = Connection.new( :host => @host, :port => @port ) - # TODO, hardcoded Ldap result code in next line - (rc = conn.bind @auth) == 0 or return rc - conn.rename( args ) - end - - end # class LDAP - - - - class LDAP - class Connection - - LdapVersion = 3 - - - # - # initialize - # - def initialize server - begin - @conn = TCPsocket.new( server[:host], server[:port] ) - rescue - raise LdapError.new( "no connection to server" ) - end - - block_given? and yield self - end - - # - # next_msgid - # - def next_msgid - @msgid ||= 0 - @msgid += 1 - end - - - # - # bind - # - def bind auth - user,psw = case auth[:method] - when :anonymous - ["",""] - when :simple - [auth[:username] || auth[:dn], auth[:password]] - end - raise LdapError.new( "invalid binding information" ) unless (user && psw) - - msgid = next_msgid.to_ber - request = [LdapVersion.to_ber, user.to_ber, psw.to_ber_contextspecific(0)].to_ber_appsequence(0) - request_pkt = [msgid, request].to_ber_sequence - @conn.write request_pkt - - (be = @conn.read_ber(AsnSyntax) and pdu = Net::LdapPdu.new( be )) or raise LdapError.new( "no bind result" ) - pdu.result_code - end - - # - # search - # TODO, certain search parameters are hardcoded. - # TODO, if we mis-parse the server results or the results are wrong, we can block - # forever. That's because we keep reading results until we get a type-5 packet, - # which might never come. We need to support the time-limit in the protocol. - # - def search args - search_filter = (args && args[:filter]) || Filter.eq( "objectclass", "*" ) - search_base = (args && args[:base]) || "dc=example,dc=com" - search_attributes = ((args && args[:attributes]) || []).map {|attr| attr.to_s.to_ber} - request = [ - search_base.to_ber, - 2.to_ber_enumerated, - 0.to_ber_enumerated, - 0.to_ber, - 0.to_ber, - false.to_ber, - search_filter.to_ber, - search_attributes.to_ber_sequence - ].to_ber_appsequence(3) - pkt = [next_msgid.to_ber, request].to_ber_sequence - @conn.write pkt - - search_results = {} - result_code = 0 - - while (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) - case pdu.app_tag - when 4 # search-data - search_results [pdu.search_dn] = pdu.search_attributes - when 5 # search-result - result_code = pdu.result_code - block_given? and yield( search_results ) - break - else - raise LdapError.new( "invalid response-type in search: #{pdu.app_tag}" ) - end - end - - result_code - end - - # - # modify - # TODO, need to support a time limit, in case the server fails to respond. - # TODO!!! We're throwing an exception here on empty DN. - # Should return a proper error instead, probaby from farther up the chain. - # TODO!!! If the user specifies a bogus opcode, we'll throw a - # confusing error here ("to_ber_enumerated is not defined on nil"). - # - def modify args - modify_dn = args[:dn] or raise "Unable to modify empty DN" - modify_ops = [] - a = args[:operations] and a.each {|op, attr, values| - # TODO, fix the following line, which gives a bogus error - # if the opcode is invalid. - op_1 = {:add => 0, :delete => 1, :replace => 2} [op.to_sym].to_ber_enumerated - modify_ops << [op_1, [attr.to_s.to_ber, values.to_a.map {|v| v.to_ber}.to_ber_set].to_ber_sequence].to_ber_sequence - } - - request = [modify_dn.to_ber, modify_ops.to_ber_sequence].to_ber_appsequence(6) - pkt = [next_msgid.to_ber, request].to_ber_sequence - @conn.write pkt - - (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 7) or raise LdapError.new( "response missing or invalid" ) - pdu.result_code - end - - - # - # add - # TODO, need to support a time limit, in case the server fails to respond. - # - def add args - add_dn = args[:dn] or raise LdapError.new("Unable to add empty DN") - add_attrs = [] - a = args[:attributes] and a.each {|k,v| - add_attrs << [ k.to_s.to_ber, v.to_a.map {|m| m.to_ber}.to_ber_set ].to_ber_sequence - } - - request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(8) - pkt = [next_msgid.to_ber, request].to_ber_sequence - @conn.write pkt - - (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 9) or raise LdapError.new( "response missing or invalid" ) - pdu.result_code - end - - - # - # rename - # TODO, need to support a time limit, in case the server fails to respond. - # - def rename args - old_dn = args[:olddn] or raise "Unable to rename empty DN" - new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN" - delete_attrs = args[:delete_attributes] ? true : false - - request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber].to_ber_appsequence(12) - pkt = [next_msgid.to_ber, request].to_ber_sequence - @conn.write pkt - - (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 13) or raise LdapError.new( "response missing or invalid" ) - pdu.result_code - end - - - end # class Connection - end # class LDAP - - -end # module Net - - -#------------------------------------------------------ - -if __FILE__ == $0 - puts "No default action" -end - - - - diff --git a/lib/netldapfilter.rb b/lib/netldapfilter.rb deleted file mode 100644 index 97c4e86..0000000 --- a/lib/netldapfilter.rb +++ /dev/null @@ -1,185 +0,0 @@ -# $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 - From 1decab1aafe500ecca81d0e65e0604de7dccc4d9 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 16 Apr 2006 23:07:50 +0000 Subject: [PATCH 014/231] Ditched autorequire from Rakefile --- Rakefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Rakefile b/Rakefile index 2060bfa..004f88b 100644 --- a/Rakefile +++ b/Rakefile @@ -18,7 +18,6 @@ spec = Gem::Specification.new {|s| s.summary = "Net::LDAP library" s.files = FileList["{bin,tests,lib}/**/*"].exclude("rdoc").to_a s.require_paths = ["lib"] - s.autorequire = "netldap" s.test_file = "tests/testem.rb" s.has_rdoc = true s.extra_rdoc_files = ["README", "RELEASE_NOTES", "COPYING"] From 1b8bce9051dddef5494ed3abb18579d2bb1e283a Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 17 Apr 2006 00:32:24 +0000 Subject: [PATCH 015/231] Fixed two bugs: 1) We were incorrectly halting sequence-parses when the sequence contained a boolean FALSE value; 2) We were generating application strings with a tag class of 0x80 (context-specific) rather than 0x40. --- lib/net/ber.rb | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index 21f4dcc..cf64bca 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -106,7 +106,12 @@ module Net when :array seq = [] sio = StringIO.new( newobj || "" ) - while e = sio.read_ber(syntax); seq << e; end + # 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 + end seq else raise BerError.new( "unsupported object type: class=#{tagclass}, encoding=#{encoding}, tag=#{tag}" ) @@ -224,11 +229,9 @@ class String # # 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 ) + to_ber( 0x40 + code ) end # From ff01c2640102d1dcb949fb4497fdcd6bfc1057e4 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 17 Apr 2006 00:33:55 +0000 Subject: [PATCH 016/231] Fixed bug: we were generating an application string rather than a context-specific one when constructing "present" filters. --- lib/net/ldapfilter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/net/ldapfilter.rb b/lib/net/ldapfilter.rb index 97c4e86..4ad9abf 100644 --- a/lib/net/ldapfilter.rb +++ b/lib/net/ldapfilter.rb @@ -117,7 +117,7 @@ class Filter case @op when :eq if @right == "*" # present - @left.to_ber_application_string 7 + @left.to_ber_contextspecific 7 elsif @right =~ /[\*]/ #substring ary = @right.split( /[\*]+/ ) final_star = @right =~ /[\*]$/ From a25e74e1e3be401f1452cb711fc89ac9b12efa50 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 17 Apr 2006 00:35:42 +0000 Subject: [PATCH 017/231] added the ASN.1 syntax definition for a "present" filter. --- testserver/ldapserver.rb | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/testserver/ldapserver.rb b/testserver/ldapserver.rb index e215dd5..64a495f 100644 --- a/testserver/ldapserver.rb +++ b/testserver/ldapserver.rb @@ -42,12 +42,13 @@ module LdapServer 3 => :array # LDAP SearchRequest }, :primitive => { - 2 => :string # ldapsearch sends this to unbind + 2 => :string, # ldapsearch sends this to unbind } }, :context_specific => { :primitive => { - 0 => :string # simple auth (password) + 0 => :string, # simple auth (password) + 7 => :string # present filter }, } } @@ -114,6 +115,8 @@ module LdapServer return end + # pdu[1][7] is the attributes. It's an empty array to signify ALL attributes. + puts "WARNING, not interpreting attributes specifier" =begin Search Response ::= CHOICE { @@ -187,10 +190,10 @@ if __FILE__ == $0 $logger.info "adding ../lib to loadpath, to pick up dev version of Net::LDAP." $:.unshift "../lib" - require 'netber' - require 'ldappdu' - require 'netldap' - require 'netldapfilter' + require 'net/ber' + require 'net/ldappdu' + require 'net/ldap' + require 'net/ldapfilter' EventMachine.run { $logger.info "starting LDAP server on 127.0.0.1 port 3890" From 4c9532f43230ac36236dd5159a5b3fe22a1b86ca Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 17 Apr 2006 00:41:49 +0000 Subject: [PATCH 018/231] Refactored ldap/pdu and ldap/filter scripts --- lib/net/ldap.rb | 3 +++ lib/net/{ldapfilter.rb => ldap/filter.rb} | 0 lib/net/{ldappdu.rb => ldap/pdu.rb} | 0 tests/testber.rb | 6 +++--- tests/testldap.rb | 6 +++--- testserver/ldapserver.rb | 6 +++--- 6 files changed, 12 insertions(+), 9 deletions(-) rename lib/net/{ldapfilter.rb => ldap/filter.rb} (100%) rename lib/net/{ldappdu.rb => ldap/pdu.rb} (100%) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 2ac179f..cfe10ee 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -41,6 +41,9 @@ require 'socket' +require 'net/ber' +require 'net/ldap/pdu' +require 'net/ldap/filter' module Net diff --git a/lib/net/ldapfilter.rb b/lib/net/ldap/filter.rb similarity index 100% rename from lib/net/ldapfilter.rb rename to lib/net/ldap/filter.rb diff --git a/lib/net/ldappdu.rb b/lib/net/ldap/pdu.rb similarity index 100% rename from lib/net/ldappdu.rb rename to lib/net/ldap/pdu.rb diff --git a/tests/testber.rb b/tests/testber.rb index 997499d..a9a6aa7 100644 --- a/tests/testber.rb +++ b/tests/testber.rb @@ -5,10 +5,10 @@ $:.unshift "lib" -require 'net/ber' +#require 'net/ber' require 'net/ldap' -require 'net/ldappdu' -require 'net/ldapfilter' +#require 'net/ldap/pdu' +#require 'net/ldap/filter' require 'stringio' diff --git a/tests/testldap.rb b/tests/testldap.rb index fc9f65f..4352b29 100644 --- a/tests/testldap.rb +++ b/tests/testldap.rb @@ -5,10 +5,10 @@ $:.unshift "lib" -require 'net/ber' +#require 'net/ber' require 'net/ldap' -require 'net/ldappdu' -require 'net/ldapfilter' +#require 'net/ldap/pdu' +#require 'net/ldap/filter' require 'stringio' diff --git a/testserver/ldapserver.rb b/testserver/ldapserver.rb index 64a495f..e1e946b 100644 --- a/testserver/ldapserver.rb +++ b/testserver/ldapserver.rb @@ -190,10 +190,10 @@ if __FILE__ == $0 $logger.info "adding ../lib to loadpath, to pick up dev version of Net::LDAP." $:.unshift "../lib" - require 'net/ber' - require 'net/ldappdu' + #require 'net/ber' require 'net/ldap' - require 'net/ldapfilter' + #require 'net/ldap/pdu' + #require 'net/ldap/filter' EventMachine.run { $logger.info "starting LDAP server on 127.0.0.1 port 3890" From 825594ade25232c8bcf032b3773d641d0f9f3130 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 17 Apr 2006 00:42:47 +0000 Subject: [PATCH 019/231] Removed superfluous requires. Now, all you have to do is require net/ldap and you'll pick up all the needed modules. --- tests/testldap.rb | 3 --- testserver/ldapserver.rb | 3 --- 2 files changed, 6 deletions(-) diff --git a/tests/testldap.rb b/tests/testldap.rb index 4352b29..f3ff534 100644 --- a/tests/testldap.rb +++ b/tests/testldap.rb @@ -5,10 +5,7 @@ $:.unshift "lib" -#require 'net/ber' require 'net/ldap' -#require 'net/ldap/pdu' -#require 'net/ldap/filter' require 'stringio' diff --git a/testserver/ldapserver.rb b/testserver/ldapserver.rb index e1e946b..8bc5aef 100644 --- a/testserver/ldapserver.rb +++ b/testserver/ldapserver.rb @@ -190,10 +190,7 @@ if __FILE__ == $0 $logger.info "adding ../lib to loadpath, to pick up dev version of Net::LDAP." $:.unshift "../lib" - #require 'net/ber' require 'net/ldap' - #require 'net/ldap/pdu' - #require 'net/ldap/filter' EventMachine.run { $logger.info "starting LDAP server on 127.0.0.1 port 3890" From 2ccd60bfc2d018323469ecff1af3f2ba5eb88ce6 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 17 Apr 2006 02:54:00 +0000 Subject: [PATCH 020/231] Test LDIF --- testserver/testdata.ldif | 101 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 testserver/testdata.ldif diff --git a/testserver/testdata.ldif b/testserver/testdata.ldif new file mode 100644 index 0000000..d43add0 --- /dev/null +++ b/testserver/testdata.ldif @@ -0,0 +1,101 @@ +# $Id$ +# +# This is test-data for an LDAP server in LDIF format. +# +dn: dc=bayshorenetworks,dc=com +objectClass: dcObject +objectClass: organization +o: Bayshore Networks LLC +dc: bayshorenetworks + +dn: cn=Manager,dc=bayshorenetworks,dc=com +objectClass: organizationalrole +cn: Manager + +dn: ou=people,dc=bayshorenetworks,dc=com +objectClass: organizationalunit +ou: people + +dn: ou=privileges,dc=bayshorenetworks,dc=com +objectClass: organizationalunit +ou: privileges + +dn: ou=roles,dc=bayshorenetworks,dc=com +objectClass: organizationalunit +ou: roles + +dn: ou=office,dc=bayshorenetworks,dc=com +objectClass: organizationalunit +ou: office + +dn: mail=nogoodnik@steamheat.net,ou=people,dc=bayshorenetworks,dc=com +cn: Bob Fosse +mail: nogoodnik@steamheat.net +sn: Fosse +ou: people +objectClass: top +objectClass: inetorgperson +objectClass: authorizedperson +hasAccessRole: uniqueIdentifier=engineer,ou=roles +hasAccessRole: uniqueIdentifier=ldapadmin,ou=roles +hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles +hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles +hasAccessRole: uniqueIdentifier=ogilvy_eagle_user,ou=roles +hasAccessRole: uniqueIdentifier=greenplug_user,ou=roles +hasAccessRole: uniqueIdentifier=brandplace_logging_user,ou=roles +hasAccessRole: uniqueIdentifier=brandplace_report_user,ou=roles +hasAccessRole: uniqueIdentifier=workorder_user,ou=roles +hasAccessRole: uniqueIdentifier=bayshore_eagle_user,ou=roles +hasAccessRole: uniqueIdentifier=bayshore_eagle_superuser,ou=roles +hasAccessRole: uniqueIdentifier=kledaras_user,ou=roles + +dn: mail=elephant@steamheat.net,ou=people,dc=bayshorenetworks,dc=com +cn: Gwen Verdon +mail: elephant@steamheat.net +sn: Verdon +ou: people +objectClass: top +objectClass: inetorgperson +objectClass: authorizedperson +hasAccessRole: uniqueIdentifier=brandplace_report_user,ou=roles +hasAccessRole: uniqueIdentifier=engineer,ou=roles +hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles +hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles +hasAccessRole: uniqueIdentifier=ldapadmin,ou=roles + +dn: uniqueIdentifier=engineering,ou=privileges,dc=bayshorenetworks,dc=com +uniqueIdentifier: engineering +ou: privileges +objectClass: accessPrivilege + +dn: uniqueIdentifier=engineer,ou=roles,dc=bayshorenetworks,dc=com +uniqueIdentifier: engineer +ou: roles +objectClass: accessRole +hasAccessPrivilege: uniqueIdentifier=engineering,ou=privileges + +dn: uniqueIdentifier=ldapadmin,ou=roles,dc=bayshorenetworks,dc=com +uniqueIdentifier: ldapadmin +ou: roles +objectClass: accessRole + +dn: uniqueIdentifier=ldapsuperadmin,ou=roles,dc=bayshorenetworks,dc=com +uniqueIdentifier: ldapsuperadmin +ou: roles +objectClass: accessRole + +dn: mail=catperson@steamheat.net,ou=people,dc=bayshorenetworks,dc=com +cn: Sid Sorokin +mail: catperson@steamheat.net +sn: Sorokin +ou: people +objectClass: top +objectClass: inetorgperson +objectClass: authorizedperson +hasAccessRole: uniqueIdentifier=engineer,ou=roles +hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles +hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles +hasAccessRole: uniqueIdentifier=ogilvy_eagle_user,ou=roles +hasAccessRole: uniqueIdentifier=greenplug_user,ou=roles +hasAccessRole: uniqueIdentifier=workorder_user,ou=roles + From f8440cdf36a22a17648511ca370b06fa3fbea2b8 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 17 Apr 2006 03:44:55 +0000 Subject: [PATCH 021/231] incorporated test data into the test LDAP server. --- tests/testldap.rb | 4 ++-- testserver/ldapserver.rb | 43 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/tests/testldap.rb b/tests/testldap.rb index f3ff534..d5b04e6 100644 --- a/tests/testldap.rb +++ b/tests/testldap.rb @@ -49,7 +49,7 @@ class TestLdapClient < Test::Unit::TestCase search = {:base => "dc=smalldomain,dc=com"} assert_equal( 32, ldap.search( search )) - search = {:base => "dc=bigdomain,dc=com"} + search = {:base => "dc=bayshorenetworks,dc=com"} assert_equal( 0, ldap.search( search )) ldap.search( search ) {|res| @@ -64,7 +64,7 @@ class TestLdapClient < Test::Unit::TestCase assert_equal( 0, ldap.bind ) search = { - :base => "dc=bigdomain,dc=com", + :base => "dc=bayshorenetworks,dc=com", :attributes => ["mail"] } assert_equal( 0, ldap.search( search )) diff --git a/testserver/ldapserver.rb b/testserver/ldapserver.rb index 8bc5aef..499331d 100644 --- a/testserver/ldapserver.rb +++ b/testserver/ldapserver.rb @@ -110,11 +110,26 @@ module LdapServer end treebase = pdu[1][0] - if treebase != "dc=bigdomain,dc=com" + if treebase != "dc=bayshorenetworks,dc=com" send_ldap_response 5, pdu[0].to_i, 32, "", "unknown treebase" return end + msgid = pdu[0].to_i.to_ber + + $ldif.each {|dn, entry| + + attrs = [] + entry.each {|k, v| + attrvals = v.map {|v1| v1.to_ber}.to_ber_set + attrs << [k.to_ber, attrvals].to_ber_sequence + } + + appseq = [dn.to_ber, attrs.to_ber_sequence].to_ber_appsequence(4) + pkt = [msgid.to_ber, appseq].to_ber_sequence + send_data pkt + } + # pdu[1][7] is the attributes. It's an empty array to signify ALL attributes. puts "WARNING, not interpreting attributes specifier" =begin @@ -131,6 +146,7 @@ Search Response ::= } =end +=begin send_data( [ pdu[0].to_i.to_ber, [ "abcdefghijklmnopqrstuvwxyz".to_ber, [ @@ -166,6 +182,7 @@ Search Response ::= ].to_ber_sequence ].to_ber_appsequence(4) ].to_ber_sequence) +=end send_ldap_response 5, pdu[0].to_i, 0, "", "Was that what you wanted?" end @@ -177,6 +194,28 @@ Search Response ::= end +#------------------------------------------------ + +# Rather bogus, a global method, which reads a HARDCODED filename +# parses out LDIF data. It will be used to serve LDAP queries out of this server. +# +def load_test_data + ary = File.readlines( "./testdata.ldif" ) + hash = {} + while line = ary.shift and line.chomp! + if line =~ /^dn:[\s]*/i + dn = $' + hash[dn] = {} + while attr = ary.shift and attr.chomp! and attr =~ /^([\w]+)[\s]*:[\s]*/ + hash[dn][$1.downcase] ||= [] + hash[dn][$1.downcase] << $' + end + end + end + hash +end + + #------------------------------------------------ if __FILE__ == $0 @@ -190,6 +229,8 @@ if __FILE__ == $0 $logger.info "adding ../lib to loadpath, to pick up dev version of Net::LDAP." $:.unshift "../lib" + $ldif = load_test_data + require 'net/ldap' EventMachine.run { From 7d08543d2478450e121f4add6906e121443ca86a Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 17 Apr 2006 12:41:50 +0000 Subject: [PATCH 022/231] ldapserver now correctly selects which attributes to return in a search, based on the client's request. --- testserver/ldapserver.rb | 84 ++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 55 deletions(-) diff --git a/testserver/ldapserver.rb b/testserver/ldapserver.rb index 499331d..8954e02 100644 --- a/testserver/ldapserver.rb +++ b/testserver/ldapserver.rb @@ -103,6 +103,20 @@ module LdapServer end end + + + #-- + # Search Response ::= + # CHOICE { + # entry [APPLICATION 4] SEQUENCE { + # objectName LDAPDN, + # attributes SEQUENCE OF SEQUENCE { + # AttributeType, + # SET OF AttributeValue + # } + # }, + # resultCode [APPLICATION 5] LDAPResult + # } def handle_search_request pdu unless @authenticated send_ldap_response 5, pdu[0].to_i, 50, "", "Who did you say you were?" @@ -117,12 +131,23 @@ module LdapServer msgid = pdu[0].to_i.to_ber + # pdu[1][7] is the list of requested attributes. + # If it's an empty array, that means that *all* attributes were requested. + requested_attrs = if pdu[1][7].length > 0 + pdu[1][7].map {|a| a.downcase} + else + :all + end + + $ldif.each {|dn, entry| attrs = [] entry.each {|k, v| - attrvals = v.map {|v1| v1.to_ber}.to_ber_set - attrs << [k.to_ber, attrvals].to_ber_sequence + if requested_attrs == :all or requested_attrs.include?(k.downcase) + attrvals = v.map {|v1| v1.to_ber}.to_ber_set + attrs << [k.to_ber, attrvals].to_ber_sequence + end } appseq = [dn.to_ber, attrs.to_ber_sequence].to_ber_appsequence(4) @@ -130,63 +155,12 @@ module LdapServer send_data pkt } - # pdu[1][7] is the attributes. It's an empty array to signify ALL attributes. - puts "WARNING, not interpreting attributes specifier" -=begin -Search Response ::= - CHOICE { - entry [APPLICATION 4] SEQUENCE { - objectName LDAPDN, - attributes SEQUENCE OF SEQUENCE { - AttributeType, - SET OF AttributeValue - } - }, - resultCode [APPLICATION 5] LDAPResult - } -=end - -=begin - send_data( [ - pdu[0].to_i.to_ber, [ - "abcdefghijklmnopqrstuvwxyz".to_ber, [ - - [ - "mail".to_ber, ["aaa".to_ber, "bbb".to_ber, "ccc".to_ber].to_ber_set - ].to_ber_sequence, - [ - "objectclass".to_ber, ["111".to_ber, "222".to_ber, "333".to_ber].to_ber_set - ].to_ber_sequence, - [ - "cn".to_ber, ["CNCNCNCN".to_ber].to_ber_set - ].to_ber_sequence, - - ].to_ber_sequence - ].to_ber_appsequence(4) - ].to_ber_sequence) - - send_data( [ - pdu[0].to_i.to_ber, [ - "ABCDEFGHIJKLMNOPQRSTUVWXYZ".to_ber, [ - - [ - "mail".to_ber, ["aaa".to_ber, "bbb".to_ber, "ccc".to_ber].to_ber_set - ].to_ber_sequence, - [ - "objectclass".to_ber, ["111".to_ber, "222".to_ber, "333".to_ber].to_ber_set - ].to_ber_sequence, - [ - "cn".to_ber, ["CNCNCNCN".to_ber].to_ber_set - ].to_ber_sequence, - - ].to_ber_sequence - ].to_ber_appsequence(4) - ].to_ber_sequence) -=end send_ldap_response 5, pdu[0].to_i, 0, "", "Was that what you wanted?" end + + def send_ldap_response pkt_tag, msgid, code, dn, text send_data( [msgid.to_ber, [code.to_ber, dn.to_ber, text.to_ber].to_ber_appsequence(pkt_tag) ].to_ber ) end From ca38bfc8030720e86e34caefa99061028082454c Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 17 Apr 2006 17:57:33 +0000 Subject: [PATCH 023/231] additional unit tests and partial support for filters in the test server. --- tests/testdata.ldif | 101 +++++++++++++++++++++++++++++++++++++++ tests/testldap.rb | 84 +++++++++++++++++++++++++++++--- testserver/ldapserver.rb | 7 +++ 3 files changed, 185 insertions(+), 7 deletions(-) create mode 100644 tests/testdata.ldif diff --git a/tests/testdata.ldif b/tests/testdata.ldif new file mode 100644 index 0000000..d43add0 --- /dev/null +++ b/tests/testdata.ldif @@ -0,0 +1,101 @@ +# $Id$ +# +# This is test-data for an LDAP server in LDIF format. +# +dn: dc=bayshorenetworks,dc=com +objectClass: dcObject +objectClass: organization +o: Bayshore Networks LLC +dc: bayshorenetworks + +dn: cn=Manager,dc=bayshorenetworks,dc=com +objectClass: organizationalrole +cn: Manager + +dn: ou=people,dc=bayshorenetworks,dc=com +objectClass: organizationalunit +ou: people + +dn: ou=privileges,dc=bayshorenetworks,dc=com +objectClass: organizationalunit +ou: privileges + +dn: ou=roles,dc=bayshorenetworks,dc=com +objectClass: organizationalunit +ou: roles + +dn: ou=office,dc=bayshorenetworks,dc=com +objectClass: organizationalunit +ou: office + +dn: mail=nogoodnik@steamheat.net,ou=people,dc=bayshorenetworks,dc=com +cn: Bob Fosse +mail: nogoodnik@steamheat.net +sn: Fosse +ou: people +objectClass: top +objectClass: inetorgperson +objectClass: authorizedperson +hasAccessRole: uniqueIdentifier=engineer,ou=roles +hasAccessRole: uniqueIdentifier=ldapadmin,ou=roles +hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles +hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles +hasAccessRole: uniqueIdentifier=ogilvy_eagle_user,ou=roles +hasAccessRole: uniqueIdentifier=greenplug_user,ou=roles +hasAccessRole: uniqueIdentifier=brandplace_logging_user,ou=roles +hasAccessRole: uniqueIdentifier=brandplace_report_user,ou=roles +hasAccessRole: uniqueIdentifier=workorder_user,ou=roles +hasAccessRole: uniqueIdentifier=bayshore_eagle_user,ou=roles +hasAccessRole: uniqueIdentifier=bayshore_eagle_superuser,ou=roles +hasAccessRole: uniqueIdentifier=kledaras_user,ou=roles + +dn: mail=elephant@steamheat.net,ou=people,dc=bayshorenetworks,dc=com +cn: Gwen Verdon +mail: elephant@steamheat.net +sn: Verdon +ou: people +objectClass: top +objectClass: inetorgperson +objectClass: authorizedperson +hasAccessRole: uniqueIdentifier=brandplace_report_user,ou=roles +hasAccessRole: uniqueIdentifier=engineer,ou=roles +hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles +hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles +hasAccessRole: uniqueIdentifier=ldapadmin,ou=roles + +dn: uniqueIdentifier=engineering,ou=privileges,dc=bayshorenetworks,dc=com +uniqueIdentifier: engineering +ou: privileges +objectClass: accessPrivilege + +dn: uniqueIdentifier=engineer,ou=roles,dc=bayshorenetworks,dc=com +uniqueIdentifier: engineer +ou: roles +objectClass: accessRole +hasAccessPrivilege: uniqueIdentifier=engineering,ou=privileges + +dn: uniqueIdentifier=ldapadmin,ou=roles,dc=bayshorenetworks,dc=com +uniqueIdentifier: ldapadmin +ou: roles +objectClass: accessRole + +dn: uniqueIdentifier=ldapsuperadmin,ou=roles,dc=bayshorenetworks,dc=com +uniqueIdentifier: ldapsuperadmin +ou: roles +objectClass: accessRole + +dn: mail=catperson@steamheat.net,ou=people,dc=bayshorenetworks,dc=com +cn: Sid Sorokin +mail: catperson@steamheat.net +sn: Sorokin +ou: people +objectClass: top +objectClass: inetorgperson +objectClass: authorizedperson +hasAccessRole: uniqueIdentifier=engineer,ou=roles +hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles +hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles +hasAccessRole: uniqueIdentifier=ogilvy_eagle_user,ou=roles +hasAccessRole: uniqueIdentifier=greenplug_user,ou=roles +hasAccessRole: uniqueIdentifier=workorder_user,ou=roles + diff --git a/tests/testldap.rb b/tests/testldap.rb index d5b04e6..0c2c206 100644 --- a/tests/testldap.rb +++ b/tests/testldap.rb @@ -13,6 +13,11 @@ class TestLdapClient < Test::Unit::TestCase # TODO: these tests crash and burn if the associated # LDAP testserver isn't up and running. + # We rely on being able to read a file with test data + # in LDIF format. + # TODO, WARNING: for the moment, this data is in a file + # whose name and location are HARDCODED into the + # instance method load_test_data. def setup @host = "127.0.0.1" @@ -23,8 +28,34 @@ class TestLdapClient < Test::Unit::TestCase :password => "opensesame" } + @ldif = load_test_data end + + + # Get some test data which will be used to validate + # the responses from the test LDAP server we will + # connect to. + # TODO, Bogus: we are HARDCODING the location of the file for now. + # + def load_test_data + ary = File.readlines( "tests/testdata.ldif" ) + hash = {} + while line = ary.shift and line.chomp! + if line =~ /^dn:[\s]*/i + dn = $' + hash[dn] = {} + while attr = ary.shift and attr.chomp! and attr =~ /^([\w]+)[\s]*:[\s]*/ + hash[dn][$1.downcase.intern] ||= [] + hash[dn][$1.downcase.intern] << $' + end + end + end + hash + end + + + # Binding tests. # Need tests for all kinds of network failures and incorrect auth. # TODO: Implement a class-level timeout for operations like bind. @@ -43,6 +74,8 @@ class TestLdapClient < Test::Unit::TestCase assert_equal( 49, ldap.bind ) end + + def test_search ldap = Net::LDAP.new :host => @host, :port => @port, :auth => @auth @@ -53,30 +86,67 @@ class TestLdapClient < Test::Unit::TestCase assert_equal( 0, ldap.search( search )) ldap.search( search ) {|res| - # STUB. - #p res + assert_equal( res, @ldif ) } end - def test_search_attributes + + + # This is a helper routine for test_search_attributes. + def internal_test_search_attributes attrs_to_search ldap = Net::LDAP.new :host => @host, :port => @port, :auth => @auth assert_equal( 0, ldap.bind ) search = { :base => "dc=bayshorenetworks,dc=com", - :attributes => ["mail"] + :attributes => attrs_to_search } - assert_equal( 0, ldap.search( search )) + ldif = @ldif + ldif.each {|dn,entry| + entry.delete_if {|attr,value| + ! attrs_to_search.include?(attr) + } + } + + assert_equal( 0, ldap.search( search )) ldap.search( search ) {|res| - # STUB. - p res + res_keys = res.keys.sort + ldif_keys = ldif.keys.sort + assert( res_keys, ldif_keys ) + res.keys.each {|rk| + assert( res[rk], ldif[rk] ) + } } end + def test_search_attributes + internal_test_search_attributes [:mail] + internal_test_search_attributes [:cn] + internal_test_search_attributes [:ou] + internal_test_search_attributes [:hasaccessprivilege] + internal_test_search_attributes ["mail"] + internal_test_search_attributes ["cn"] + internal_test_search_attributes ["ou"] + internal_test_search_attributes ["hasaccessrole"] + + internal_test_search_attributes [:mail, :cn, :ou, :hasaccessrole] + internal_test_search_attributes [:mail, "cn", :ou, "hasaccessrole"] + end + + def test_search_filters + ldap = Net::LDAP.new :host => @host, :port => @port, :auth => @auth + search = { + :base => "dc=bayshorenetworks,dc=com", + :filter => Net::LDAP::Filter.eq( "sn", "Verdon" ) + } + + ldap.search( search ) {|res| + p res + } end diff --git a/testserver/ldapserver.rb b/testserver/ldapserver.rb index 8954e02..c7e7911 100644 --- a/testserver/ldapserver.rb +++ b/testserver/ldapserver.rb @@ -50,6 +50,9 @@ module LdapServer 0 => :string, # simple auth (password) 7 => :string # present filter }, + :constructed => { + 3 => :array # equality filter + }, } } @@ -139,6 +142,10 @@ module LdapServer :all end + filters = pdu[1][6] + if filters.length > 0 + p filters.ber_identifier + end $ldif.each {|dn, entry| From 497e2be4e725a8807bcec05bfc3386de914d6abf Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 17 Apr 2006 20:39:54 +0000 Subject: [PATCH 024/231] Added unit tests for search-filters --- lib/net/ldap/filter.rb | 35 +++++++++++++++++++++++++++++++++++ tests/testldap.rb | 2 +- testserver/ldapserver.rb | 32 +++++++++++++++++++------------- 3 files changed, 55 insertions(+), 14 deletions(-) diff --git a/lib/net/ldap/filter.rb b/lib/net/ldap/filter.rb index 4ad9abf..4293ef5 100644 --- a/lib/net/ldap/filter.rb +++ b/lib/net/ldap/filter.rb @@ -171,6 +171,41 @@ class Filter end + + # We get a Ruby object which comes from parsing an RFC-1777 "Filter" + # object. Convert it to a Net::LDAP::Filter. + # TODO, we're hardcoding the RFC-1777 BER-encodings of the various + # filter types. Could pull them out into a constant. + # + def Filter::parse_ldap_filter obj + case obj.ber_identifier + when 0x87 # present. context-specific primitive 7. + Filter.eq( obj.to_s, "*" ) + when 0xa3 # equalityMatch. context-specific constructed 3. + Filter.eq( obj[0], obj[1] ) + else + raise LdapError.new( "unknown ldap search-filter type: #{obj.ber_identifier}" ) + end + end + + + # We got a hash of attribute values. + # Do we match the attributes? + # Return T/F, and call match recursively as necessary. + def match entry + case @op + when :eq + if @right == "*" + l = entry[@left] and l.length > 0 + else + l = entry[@left] and l = l.to_a and l.index(@right) + end + else + raise LdapError.new( "unknown filter type in match: #{@op}" ) + end + end + + end # class Net::LDAP::Filter end # class Net::LDAP diff --git a/tests/testldap.rb b/tests/testldap.rb index 0c2c206..2f86d89 100644 --- a/tests/testldap.rb +++ b/tests/testldap.rb @@ -141,7 +141,7 @@ class TestLdapClient < Test::Unit::TestCase ldap = Net::LDAP.new :host => @host, :port => @port, :auth => @auth search = { :base => "dc=bayshorenetworks,dc=com", - :filter => Net::LDAP::Filter.eq( "sn", "Verdon" ) + :filter => Net::LDAP::Filter.eq( "sn", "Fosse" ) } ldap.search( search ) {|res| diff --git a/testserver/ldapserver.rb b/testserver/ldapserver.rb index c7e7911..60b8572 100644 --- a/testserver/ldapserver.rb +++ b/testserver/ldapserver.rb @@ -122,6 +122,7 @@ module LdapServer # } def handle_search_request pdu unless @authenticated + # NOTE, early exit. send_ldap_response 5, pdu[0].to_i, 50, "", "Who did you say you were?" return end @@ -143,23 +144,28 @@ module LdapServer end filters = pdu[1][6] - if filters.length > 0 - p filters.ber_identifier + if filters.length == 0 + # NOTE, early exit. + send_ldap_response 5, pdu[0].to_i, 53, "", "No filter specified" end + # TODO, what if this returns nil? + filter = Net::LDAP::Filter.parse_ldap_filter( filters ) + $ldif.each {|dn, entry| + if filter.match( entry ) + attrs = [] + entry.each {|k, v| + if requested_attrs == :all or requested_attrs.include?(k.downcase) + attrvals = v.map {|v1| v1.to_ber}.to_ber_set + attrs << [k.to_ber, attrvals].to_ber_sequence + end + } - attrs = [] - entry.each {|k, v| - if requested_attrs == :all or requested_attrs.include?(k.downcase) - attrvals = v.map {|v1| v1.to_ber}.to_ber_set - attrs << [k.to_ber, attrvals].to_ber_sequence - end - } - - appseq = [dn.to_ber, attrs.to_ber_sequence].to_ber_appsequence(4) - pkt = [msgid.to_ber, appseq].to_ber_sequence - send_data pkt + appseq = [dn.to_ber, attrs.to_ber_sequence].to_ber_appsequence(4) + pkt = [msgid.to_ber, appseq].to_ber_sequence + send_data pkt + end } From ee51696d137d3be6a11c13cdc39399c6a2a2994f Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 17 Apr 2006 20:42:45 +0000 Subject: [PATCH 025/231] Added an LDIF class (just a stub for now) --- lib/net/ldif.rb | 50 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 lib/net/ldif.rb diff --git a/lib/net/ldif.rb b/lib/net/ldif.rb new file mode 100644 index 0000000..8acc6cb --- /dev/null +++ b/lib/net/ldif.rb @@ -0,0 +1,50 @@ +# $Id$ +# +# Net::LDIF for Ruby +# +# +# +# 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 +# +# + +# THIS FILE IS A STUB. + +module Net + + class LDIF + + + end # class LDIF + + +end # module Net + + +#------------------------------------------------------ + +if __FILE__ == $0 + puts "No default action" +end + + + + + + From 2dc2f238a6f9ba7193f12dd5a4dbc938c609cfdb Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 18 Apr 2006 00:10:28 +0000 Subject: [PATCH 026/231] Added a Search object --- lib/net/ldap/search.rb | 49 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 lib/net/ldap/search.rb diff --git a/lib/net/ldap/search.rb b/lib/net/ldap/search.rb new file mode 100644 index 0000000..fdebd65 --- /dev/null +++ b/lib/net/ldap/search.rb @@ -0,0 +1,49 @@ +# $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 Dataset + + + +end # Dataset + +end #LDAP +end Net + + +#----------------------------------- + +if __FILE__ == $0 + puts "No default action" +end + + + From 39810f40ce6d702fa8698a0f2df22d5547662b46 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 18 Apr 2006 00:11:05 +0000 Subject: [PATCH 027/231] renamed the search object to Dataset --- lib/net/ldap/{search.rb => dataset.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/net/ldap/{search.rb => dataset.rb} (100%) diff --git a/lib/net/ldap/search.rb b/lib/net/ldap/dataset.rb similarity index 100% rename from lib/net/ldap/search.rb rename to lib/net/ldap/dataset.rb From c0a4c72e66abdbd62da121bb051b7aec86f6a062 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 18 Apr 2006 00:11:36 +0000 Subject: [PATCH 028/231] added a dataset include --- lib/net/ldap.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index cfe10ee..ab1f763 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -44,6 +44,7 @@ require 'socket' require 'net/ber' require 'net/ldap/pdu' require 'net/ldap/filter' +require 'net/ldap/dataset' module Net From 974652f34dd5e1c05bc6f4f94ae7095b9ff95ae1 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 18 Apr 2006 00:12:28 +0000 Subject: [PATCH 029/231] fixed small error --- lib/net/ldap/dataset.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/net/ldap/dataset.rb b/lib/net/ldap/dataset.rb index fdebd65..51b1659 100644 --- a/lib/net/ldap/dataset.rb +++ b/lib/net/ldap/dataset.rb @@ -35,8 +35,8 @@ class Dataset end # Dataset -end #LDAP -end Net +end # LDAP +end # Net #----------------------------------- From dac184da9154b09ee067f12764f0c569f20d7f3b Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 18 Apr 2006 00:18:48 +0000 Subject: [PATCH 030/231] removed dead code --- tests/testber.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/testber.rb b/tests/testber.rb index a9a6aa7..5c63ffc 100644 --- a/tests/testber.rb +++ b/tests/testber.rb @@ -5,10 +5,7 @@ $:.unshift "lib" -#require 'net/ber' require 'net/ldap' -#require 'net/ldap/pdu' -#require 'net/ldap/filter' require 'stringio' From e6d6a5261a6ee1ac1a3f84902398b74874ab4a59 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 18 Apr 2006 15:42:10 +0000 Subject: [PATCH 031/231] Started a Dataset implementation that will be used to hold search-results and LDIFs. --- lib/net/ldap/dataset.rb | 47 ++++++++++++++++++++++++++++++++- tests/testem.rb | 1 + tests/testldif.rb | 57 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 tests/testldif.rb diff --git a/lib/net/ldap/dataset.rb b/lib/net/ldap/dataset.rb index 51b1659..dd5048a 100644 --- a/lib/net/ldap/dataset.rb +++ b/lib/net/ldap/dataset.rb @@ -26,11 +26,56 @@ # +require 'base64' + + module Net class LDAP -class Dataset +class Dataset < Hash + attr_reader :comments + + + def Dataset::read_ldif io + ds = Dataset.new + + line = io.gets && chomp + dn = nil + + while line + io.gets and chomp + if $_ =~ /^[\s]+/ + line << " " << $' + else + nextline = $_ + + if line =~ /^\#/ + ds.comments << line + elsif line =~ /^dn:[\s]*/i + dn = $' + ds[dn] = Hash.new {|k,v| k[v] = []} + elsif line.length == 0 + dn = nil + elsif line =~ /^([^:]+):([\:]?)[\s]*/ + # $1 is the attribute name + # $2 is a colon iff the attr-value is base-64 encoded + # $' is the attr-value + attrvalue = ($2 == ":") ? Base64.decode64($') : $' + ds[dn][$1.downcase.intern] << attrvalue + end + + line = nextline + end + end + + ds + end + + + def initialize + @comments = [] + end end # Dataset diff --git a/tests/testem.rb b/tests/testem.rb index ca38ff7..a78f24a 100644 --- a/tests/testem.rb +++ b/tests/testem.rb @@ -4,6 +4,7 @@ require 'test/unit' require 'tests/testber' +require 'tests/testldif' require 'tests/testldap' diff --git a/tests/testldif.rb b/tests/testldif.rb new file mode 100644 index 0000000..d42c44f --- /dev/null +++ b/tests/testldif.rb @@ -0,0 +1,57 @@ +# $Id$ +# +# + + +$:.unshift "lib" + +require 'net/ldap' +require 'net/ldif' + +require 'sha1' +require 'base64' + +class TestLdif < Test::Unit::TestCase + + TestLdifFilename = "tests/testdata.ldif" + + def test_empty_ldif + ds = Net::LDAP::Dataset::read_ldif( StringIO.new ) + assert_equal( true, ds.empty? ) + end + + def test_ldif_with_comments + str = ["# Hello from LDIF-land", "# This is an unterminated comment"] + io = StringIO.new( str[0] + "\r\n" + str[1] ) + ds = Net::LDAP::Dataset::read_ldif( io ) + assert_equal( str, ds.comments ) + end + + def test_ldif_with_password + psw = "goldbricks" + hashed_psw = "{SHA}" + Base64::encode64( SHA1.new(psw).digest ).chomp + + ldif_encoded = Base64::encode64( hashed_psw ).chomp + ds = Net::LDAP::Dataset::read_ldif( StringIO.new( "dn: Goldbrick\r\nuserPassword:: #{ldif_encoded}\r\n\r\n" )) + recovered_psw = ds["Goldbrick"][:userpassword].shift + assert_equal( hashed_psw, recovered_psw ) + end + + def test_ldif_with_continuation_lines + ds = Net::LDAP::Dataset::read_ldif( StringIO.new( "dn: abcdefg\r\n hijklmn\r\n\r\n" )) + assert_equal( true, ds.has_key?( "abcdefg hijklmn" )) + end + + # TODO, INADEQUATE. We need some more tests + # to verify the content. + def test_ldif + File.open( TestLdifFilename, "r" ) {|f| + ds = Net::LDAP::Dataset::read_ldif( f ) + assert_equal( 13, ds.length ) + } + end + + +end + + From c423ced9e2c1c658a4a4a96fb295a0bee8ef0042 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 18 Apr 2006 16:27:39 +0000 Subject: [PATCH 032/231] Removed dependency on Base64, which doesn't seem to appear on all Ruby versions. --- lib/net/ldap/dataset.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/net/ldap/dataset.rb b/lib/net/ldap/dataset.rb index dd5048a..db01ce4 100644 --- a/lib/net/ldap/dataset.rb +++ b/lib/net/ldap/dataset.rb @@ -26,7 +26,6 @@ # -require 'base64' module Net @@ -61,7 +60,8 @@ class Dataset < Hash # $1 is the attribute name # $2 is a colon iff the attr-value is base-64 encoded # $' is the attr-value - attrvalue = ($2 == ":") ? Base64.decode64($') : $' + #attrvalue = ($2 == ":") ? Base64.decode64($') : $' + attrvalue = ($2 == ":") ? $'.unpack('m').shift : $' ds[dn][$1.downcase.intern] << attrvalue end From 7a0da7c9bcfbe550c850f76b80490062a158f0b7 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 18 Apr 2006 19:46:47 +0000 Subject: [PATCH 033/231] Added Dataset::to_ldif --- lib/net/ldap/dataset.rb | 24 +++++++++++++++++++++++- tests/testldif.rb | 15 +++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/lib/net/ldap/dataset.rb b/lib/net/ldap/dataset.rb index db01ce4..a1c80a7 100644 --- a/lib/net/ldap/dataset.rb +++ b/lib/net/ldap/dataset.rb @@ -60,7 +60,7 @@ class Dataset < Hash # $1 is the attribute name # $2 is a colon iff the attr-value is base-64 encoded # $' is the attr-value - #attrvalue = ($2 == ":") ? Base64.decode64($') : $' + # Avoid the Base64 class because not all Ruby versions have it. attrvalue = ($2 == ":") ? $'.unpack('m').shift : $' ds[dn][$1.downcase.intern] << attrvalue end @@ -78,6 +78,28 @@ class Dataset < Hash end + def to_ldif + ary = [] + ary += (@comments || []) + + keys.sort.each {|dn| + ary << "dn: #{dn}" + + self[dn].keys.map {|sym| sym.to_s}.sort.each {|attr| + self[dn][attr.intern].each {|val| + ary << "#{attr}: #{val}" + } + } + + ary << "" + } + + block_given? and ary.each {|line| yield line} + + ary + end + + end # Dataset end # LDAP diff --git a/tests/testldif.rb b/tests/testldif.rb index d42c44f..02604e5 100644 --- a/tests/testldif.rb +++ b/tests/testldif.rb @@ -5,6 +5,11 @@ $:.unshift "lib" +require 'test/unit' +require 'tests/testber' +require 'tests/testldif' +require 'tests/testldap' + require 'net/ldap' require 'net/ldif' @@ -51,6 +56,16 @@ class TestLdif < Test::Unit::TestCase } end + # TODO, need some tests. + # Must test folded lines and base64-encoded lines as well as normal ones. + def test_to_ldif + File.open( TestLdifFilename, "r" ) {|f| + ds = Net::LDAP::Dataset::read_ldif( f ) + ds.to_ldif + assert_equal( true, false ) # REMOVE WHEN WE HAVE SOME TESTS HERE. + } + end + end From 36526cd66ac7ec9e7c3df8206b373d87357bbd86 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 18 Apr 2006 20:55:55 +0000 Subject: [PATCH 034/231] Refactored the Net::LDAP methods to prepare for the implementation of Net::LDAP::open --- lib/net/ldap.rb | 126 ++++++++++++++++++++++++++++++++++++++-------- tests/testldap.rb | 10 ++++ tests/testldif.rb | 3 -- 3 files changed, 114 insertions(+), 25 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index ab1f763..d56fc33 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -136,30 +136,79 @@ module Net # # open # - def LDAP::open + def LDAP::open args + #ldap = LDAP.new args + #ldap.connect + #yield ldap + #ldap.disconnect end + + # This method opens a network connection to the server and then + # passes self to the caller-supplied block. The connection is + # closed when the block completes. It's for executing multiple + # LDAP operations without requiring a separate network connection + # (and authentication) for each one. + # + # + def open + conn = connect + yield self + disconnect + end + + # # search + #-- + # If an open call is in progress (@open_connection will be non-nil), + # then ASSUME a bind has been performed and accepted, and just + # execute the search. + # If @open_connection is nil, then we have to connect, bind, + # search, and then disconnect. (The disconnect is not strictly + # necessary but it's friendlier to the network to do it here + # rather than waiting for Ruby's GC.) + # Note that in the standalone case, we're permitting the caller + # to modify the auth parms. # def search args - conn = Connection.new( :host => @host, :port => @port ) - # TODO, hardcoded Ldap result code in next line - (rc = conn.bind @auth) == 0 or return rc - result_code = conn.search( args ) {|values| - block_given? and yield( values ) - } - result_code + if @open_connection + result_code = @open_connection.search( args ) {|values| + block_given? and yield( values ) + } + result_code + else + result_code = 0 + conn = Connection.new( :host => @host, :port => @port ) + if (result_code = conn.bind( args[:auth] || @auth )) == 0 + result_code = conn.search( args ) {|values| + block_given? and yield( values ) + } + end + conn.close + result_code + end + end # # bind # Bind and unbind. # Can serve as a connectivity test as well as an auth test. + #-- + # If there is an @open_connection, then perform the bind + # on it. Otherwise, connect, bind, and disconnect. + # The latter operation is obviously useful only as an auth check. # def bind - conn = Connection.new( :host => @host, :port => @port ) - conn.bind @auth + if @open_connection + @open_connection.bind @auth + else + conn = Connection.new( :host => @host, :port => @port ) + result = conn.bind @auth + conn.close + result + end end # @@ -180,10 +229,17 @@ module Net # Add a full RDN to the remote DIS. # def add args - conn = Connection.new( :host => @host, :port => @port ) - # TODO, hardcoded Ldap result code in next line - (rc = conn.bind @auth) == 0 or return rc - conn.add( args ) + if @open_connection + @open_connection.add( args ) + else + result_code = 0 + conn = Connection.new( :host => @host, :port => @port ) + if (result_code = conn.bind( args[:auth] || @auth )) == 0 + result_code = conn.add( args ) + end + conn.close + result_code + end end @@ -192,10 +248,17 @@ module Net # Modify the attributes of an entry on the remote DIS. # def modify args - conn = Connection.new( :host => @host, :port => @port ) - # TODO, hardcoded Ldap result code in next line - (rc = conn.bind @auth) == 0 or return rc - conn.modify( args ) + if @open_connection + @open_connection.modify( args ) + else + result_code = 0 + conn = Connection.new( :host => @host, :port => @port ) + if (result_code = conn.bind( args[:auth] || @auth )) == 0 + result_code = conn.modify( args ) + end + conn.close + result_code + end end # @@ -203,10 +266,17 @@ module Net # Rename an entry on the remote DIS by changing the last RDN of its DN. # def rename args - conn = Connection.new( :host => @host, :port => @port ) - # TODO, hardcoded Ldap result code in next line - (rc = conn.bind @auth) == 0 or return rc - conn.rename( args ) + if @open_connection + @open_connection.rename( args ) + else + result_code = 0 + conn = Connection.new( :host => @host, :port => @port ) + if (result_code = conn.bind( args[:auth] || @auth )) == 0 + result_code = conn.rename( args ) + end + conn.close + result_code + end end end # class LDAP @@ -232,6 +302,18 @@ module Net block_given? and yield self end + + # + # close + # This is provided as a convenience method to make + # sure a connection object gets closed without waiting + # for a GC to happen. Clients shouldn't have to call it, + # but perhaps it will come in handy someday. + def close + @conn.close + @conn = nil + end + # # next_msgid # diff --git a/tests/testldap.rb b/tests/testldap.rb index 2f86d89..e166442 100644 --- a/tests/testldap.rb +++ b/tests/testldap.rb @@ -5,6 +5,8 @@ $:.unshift "lib" +require 'test/unit' + require 'net/ldap' require 'stringio' @@ -151,6 +153,14 @@ class TestLdapClient < Test::Unit::TestCase + def test_open + Net::LDAP.open( :host => @host, :port => @port, :auth => @auth ) { + p "NO TESTS!!!" + } + end + + + end diff --git a/tests/testldif.rb b/tests/testldif.rb index 02604e5..7bd76fb 100644 --- a/tests/testldif.rb +++ b/tests/testldif.rb @@ -6,9 +6,6 @@ $:.unshift "lib" require 'test/unit' -require 'tests/testber' -require 'tests/testldif' -require 'tests/testldap' require 'net/ldap' require 'net/ldif' From 4bc667ffb29bbf590b5cf84752ff379637e809b3 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 18 Apr 2006 21:11:33 +0000 Subject: [PATCH 035/231] Implemented Net::LDAP::open --- lib/net/ldap.rb | 20 ++++++++++++-------- tests/testldap.rb | 18 ++++++++++++++++-- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index d56fc33..17fe391 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -137,10 +137,8 @@ module Net # open # def LDAP::open args - #ldap = LDAP.new args - #ldap.connect - #yield ldap - #ldap.disconnect + ldap = LDAP.new args + ldap.open {|ldap1| yield ldap1 } end @@ -149,12 +147,18 @@ module Net # closed when the block completes. It's for executing multiple # LDAP operations without requiring a separate network connection # (and authentication) for each one. - # - # + #-- + # First we make a connection and then a binding, but we don't + # do anything with the bind results. + # We then pass self to the caller's block, where he will execute + # his LDAP operations. Of course they will all generate auth failures + # if the bind was unsuccessful. def open - conn = connect + raise LdapError.new( "open already in progress" ) if @open_connection + @open_connection = Connection.new( :host => @host, :port => @port ) + @open_connection.bind @auth yield self - disconnect + @open_connection.close end diff --git a/tests/testldap.rb b/tests/testldap.rb index e166442..9d9848a 100644 --- a/tests/testldap.rb +++ b/tests/testldap.rb @@ -154,8 +154,22 @@ class TestLdapClient < Test::Unit::TestCase def test_open - Net::LDAP.open( :host => @host, :port => @port, :auth => @auth ) { - p "NO TESTS!!!" + ldap = Net::LDAP.new :host => @host, :port => @port, :auth => @auth + ldap.open {|ldap| + 10.times { + rc = ldap.search( :base => "dc=bayshorenetworks,dc=com" ) + assert_equal( 0, rc ) + } + } + end + + + def test_ldap_open + Net::LDAP.open( :host => @host, :port => @port, :auth => @auth ) {|ldap| + 10.times { + rc = ldap.search( :base => "dc=bayshorenetworks,dc=com" ) + assert_equal( 0, rc ) + } } end From 0117d386c0d6c09a5081834f252a960becc057e8 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 23 Apr 2006 01:09:13 +0000 Subject: [PATCH 036/231] Converted LDAP#bind to return T/F and added LDAP::get_operation_result to retrieve extended error information. --- lib/net/ldap.rb | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 17fe391..5ff968f 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -41,6 +41,7 @@ require 'socket' +require 'ostruct' require 'net/ber' require 'net/ldap/pdu' require 'net/ldap/filter' @@ -141,6 +142,27 @@ module Net ldap.open {|ldap1| yield ldap1 } end + # This method will return a meaningful result any time after + # a protocol operation (bind, search, add, modify, rename, delete) + # has completed. + # It returns an OpenStruct containing an LDAP result code (0 means success), + # and a human-readable string. + # unless ldap.bind + # puts "Result: #{ldap.get_operation_result.code}" + # puts "Message: #{ldap.get_operation_result.message}" + # end + # + def get_operation_result + os = OpenStruct.new + if @result + os.code = @result + else + os.code = 0 + end + os.message = LDAP.result2string( os.code ) + os + end + # This method opens a network connection to the server and then # passes self to the caller-supplied block. The connection is @@ -206,13 +228,14 @@ module Net # def bind if @open_connection - @open_connection.bind @auth + @result = @open_connection.bind @auth else conn = Connection.new( :host => @host, :port => @port ) - result = conn.bind @auth + @result = conn.bind @auth conn.close - result end + + @result == 0 end # From 030bb9c6d40e148252a13645de63e8b168046140 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 23 Apr 2006 01:13:41 +0000 Subject: [PATCH 037/231] Fixed tests to handle the new error return from LDAP#bind. --- tests/testldap.rb | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/testldap.rb b/tests/testldap.rb index 9d9848a..e615005 100644 --- a/tests/testldap.rb +++ b/tests/testldap.rb @@ -65,15 +65,21 @@ class TestLdapClient < Test::Unit::TestCase # TODO, use constants for the LDAP result codes, rather than hardcoding them. def test_bind ldap = Net::LDAP.new :host => @host, :port => @port, :auth => @auth - assert_equal( 0, ldap.bind ) + assert_equal( true, ldap.bind ) + assert_equal( 0, ldap.get_operation_result.code ) + assert_equal( "Success", ldap.get_operation_result.message ) bad_username = @auth.merge( {:username => "cn=badguy,dc=imposters,dc=com"} ) ldap = Net::LDAP.new :host => @host, :port => @port, :auth => bad_username - assert_equal( 48, ldap.bind ) + assert_equal( false, ldap.bind ) + assert_equal( 48, ldap.get_operation_result.code ) + assert_equal( "Inappropriate Authentication", ldap.get_operation_result.message ) bad_password = @auth.merge( {:password => "cornhusk"} ) ldap = Net::LDAP.new :host => @host, :port => @port, :auth => bad_password - assert_equal( 49, ldap.bind ) + assert_equal( false, ldap.bind ) + assert_equal( 49, ldap.get_operation_result.code ) + assert_equal( "Invalid Credentials", ldap.get_operation_result.message ) end @@ -98,7 +104,7 @@ class TestLdapClient < Test::Unit::TestCase # This is a helper routine for test_search_attributes. def internal_test_search_attributes attrs_to_search ldap = Net::LDAP.new :host => @host, :port => @port, :auth => @auth - assert_equal( 0, ldap.bind ) + assert( ldap.bind ) search = { :base => "dc=bayshorenetworks,dc=com", From f8649bc54eefe175748426a79f2ce6dc82016c1b Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 23 Apr 2006 01:17:49 +0000 Subject: [PATCH 038/231] Changed LDAP#search to return T/F instead of numeric LDAP code, and adjusted the tests accordingly. --- lib/net/ldap.rb | 11 +++++------ tests/testldap.rb | 12 +++++++----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 5ff968f..2e2f21c 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -199,22 +199,21 @@ module Net # def search args if @open_connection - result_code = @open_connection.search( args ) {|values| + @result = @open_connection.search( args ) {|values| block_given? and yield( values ) } - result_code else - result_code = 0 + @result = 0 conn = Connection.new( :host => @host, :port => @port ) - if (result_code = conn.bind( args[:auth] || @auth )) == 0 - result_code = conn.search( args ) {|values| + if (@result = conn.bind( args[:auth] || @auth )) == 0 + @result = conn.search( args ) {|values| block_given? and yield( values ) } end conn.close - result_code end + @result == 0 end # diff --git a/tests/testldap.rb b/tests/testldap.rb index e615005..a401476 100644 --- a/tests/testldap.rb +++ b/tests/testldap.rb @@ -88,10 +88,12 @@ class TestLdapClient < Test::Unit::TestCase ldap = Net::LDAP.new :host => @host, :port => @port, :auth => @auth search = {:base => "dc=smalldomain,dc=com"} - assert_equal( 32, ldap.search( search )) + assert_equal( false, ldap.search( search )) + assert_equal( 32, ldap.get_operation_result.code ) search = {:base => "dc=bayshorenetworks,dc=com"} - assert_equal( 0, ldap.search( search )) + assert_equal( true, ldap.search( search )) + assert_equal( 0, ldap.get_operation_result.code ) ldap.search( search ) {|res| assert_equal( res, @ldif ) @@ -118,7 +120,7 @@ class TestLdapClient < Test::Unit::TestCase } } - assert_equal( 0, ldap.search( search )) + assert_equal( true, ldap.search( search )) ldap.search( search ) {|res| res_keys = res.keys.sort ldif_keys = ldif.keys.sort @@ -164,7 +166,7 @@ class TestLdapClient < Test::Unit::TestCase ldap.open {|ldap| 10.times { rc = ldap.search( :base => "dc=bayshorenetworks,dc=com" ) - assert_equal( 0, rc ) + assert_equal( true, rc ) } } end @@ -174,7 +176,7 @@ class TestLdapClient < Test::Unit::TestCase Net::LDAP.open( :host => @host, :port => @port, :auth => @auth ) {|ldap| 10.times { rc = ldap.search( :base => "dc=bayshorenetworks,dc=com" ) - assert_equal( 0, rc ) + assert_equal( true, rc ) } } end From ee488f9384e2d7a6f6b3b70513d272568eebb78f Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 23 Apr 2006 01:21:34 +0000 Subject: [PATCH 039/231] Changed LDAP#add, LDAP#modify and LDAP#delete to return T/F instead of a numeric LDAP code. --- lib/net/ldap.rb | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 2e2f21c..46b103a 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -256,16 +256,16 @@ module Net # def add args if @open_connection - @open_connection.add( args ) + @result = @open_connection.add( args ) else - result_code = 0 + @result = 0 conn = Connection.new( :host => @host, :port => @port ) - if (result_code = conn.bind( args[:auth] || @auth )) == 0 - result_code = conn.add( args ) + if (@result = conn.bind( args[:auth] || @auth )) == 0 + @result = conn.add( args ) end conn.close - result_code end + @result == 0 end @@ -275,16 +275,16 @@ module Net # def modify args if @open_connection - @open_connection.modify( args ) + @result = @open_connection.modify( args ) else - result_code = 0 + @result = 0 conn = Connection.new( :host => @host, :port => @port ) - if (result_code = conn.bind( args[:auth] || @auth )) == 0 - result_code = conn.modify( args ) + if (@result = conn.bind( args[:auth] || @auth )) == 0 + @result = conn.modify( args ) end conn.close - result_code end + @result == 0 end # @@ -293,16 +293,16 @@ module Net # def rename args if @open_connection - @open_connection.rename( args ) + @result = @open_connection.rename( args ) else - result_code = 0 + @result = 0 conn = Connection.new( :host => @host, :port => @port ) - if (result_code = conn.bind( args[:auth] || @auth )) == 0 - result_code = conn.rename( args ) + if (@result = conn.bind( args[:auth] || @auth )) == 0 + @result = conn.rename( args ) end conn.close - result_code end + @result == 0 end end # class LDAP From 28efeadd947456578cbb3914b915c976e62bdaff Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 23 Apr 2006 01:23:09 +0000 Subject: [PATCH 040/231] Added LDAP#modify_rdn as a synonym for LDAP#rename. --- lib/net/ldap.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 46b103a..db23bdd 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -305,6 +305,11 @@ module Net @result == 0 end + # modify_rdn is an alias for rename. + def modify_rdn args + rename args + end + end # class LDAP From cf680bc8c10b25e003f6ab03f7d6c39cdf16f4bd Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 23 Apr 2006 17:34:18 +0000 Subject: [PATCH 041/231] readme text --- README | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README b/README index c02bd34..76b3f4b 100644 --- a/README +++ b/README @@ -2,5 +2,10 @@ # # -README stub for Net::LDAP +Net::LDAP is an LDAP support library written in pure Ruby. +It supports all LDAP client features, and a subset of server features as well. + +NOTE: Net::LDAP is currently released under GPL but we expect that it will +be changed to the Ruby license before being released in a "stable" version. + From 4f18bcb1e0218a5a42b6425558d0bad8dc25f373 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 23 Apr 2006 19:05:06 +0000 Subject: [PATCH 042/231] documentation --- lib/net/ldap.rb | 203 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 173 insertions(+), 30 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index db23bdd..a3cd55a 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -3,41 +3,17 @@ # Net::LDAP for Ruby # # -# # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved. # -# Gmail: garbagecat10 +# Written and maintained by Francis Cianfrocca, 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 +# This program is free software. +# You may re-distribute and/or modify this program under the same terms +# as Ruby itself: Ruby Distribution License or GNU General Public License. # # -# == Miscellaneous +# See Net::LDAP for documentation and usage samples. # -# For reasons relating to the source-code layout, this file doesn't -# require all the outboard stuff it actually needs, like netber. -# Until we figure out how to do that without damaging the directory -# structure, we're reliant on user programs to explicitly require -# everything, and in the correct order too! -# -# == BUGS: -# -# Try querying the objectGUID attribute from an A/D. It's a binary value -# which we're reading correctly, but we need to make sure it gets base64-encoded -# if we're going to put it out to an LDIF. -# - require 'socket' @@ -51,9 +27,176 @@ require 'net/ldap/dataset' module Net + # == Net::LDAP # - # class LDAP + # This library provides a pure-Ruby implementation of the + # LDAP client protocol, per RFC-1777. + # It can be used to access any server which implements the + # LDAP protocol. # + # Net::LDAP is intended to provide full LDAP functionality + # while hiding the more arcane aspects + # the LDAP protocol itself, and thus presenting as Ruby-like + # a programming interface as possible. + # + # == Quick introduction to LDAP + # + # We're going to provide a quick and highly informal introduction to LDAP + # terminology and + # typical operations. If you're comfortable with this material, skip + # ahead to "How to use Net::LDAP." If you want a more rigorous treatment + # of this material, we recommend you start with the various IETF and ITU + # standards that control LDAP. + # + # === Entities + # LDAP is an Internet-standard protocol used to access directory servers. + # The basic search unit is the entity, which corresponds to + # a person or other domain-specific object. + # A directory service which supports the LDAP protocol typically + # stores information about a number of entities. + # + # === Principals + # LDAP servers are typically used to access information about people, + # but also very often about such items as printers, computers, and other + # resources. To reflect this, LDAP uses the term entity, or less + # commonly, principal, to denote its basic data-storage unit. + # + # + # === Distinguished Names + # In LDAP's view of the world, + # an entity is uniquely identified by a globally-unique text string + # called a Distinguished Name, originally defined in the X.400 + # standards from which LDAP is ultimately derived. + # Much like a DNS hostname, a DN is a "flattened" text representation + # of a string of tree nodes. Also like DNS (and unlike Java package + # names), a DN expresses a chain of tree-nodes written from left to right + # in order from the most-resolved node to the most-general one. + # + # If you know the DN of a person or other entity, then you can query + # an LDAP-enabled directory for information (attributes) about the entity. + # Alternatively, you can query the directory for a list of DNs matching + # a set of criteria that you supply. + # + # === Attributes + # + # In the LDAP view of the world, a DN uniquely identifies an entity. + # Information about the entity is stored as a set of Attributes. + # An attribute is a text string which is associated with zero or more + # values. Most LDAP-enabled directories store a well-standardized + # range of attributes, and constrain their values according to standard + # rules. + # + # A good example of an attribute is cn, which stands for "Common Name." + # In many directories, this attribute is used to store a string consisting of + # a person's first and last names. Most directories enforce the convention that + # an entity's cn attribute have exactly one value. In LDAP + # jargon, that means that cn must be present and + # single-valued. + # + # Another attribute is mail, which is used to store email addresses. + # (No, there is no attribute called "email," perhaps because X.400 terminology + # predates the invention of the term email.) mail differs + # from cn in that most directories permit any number of values for the + # mail attribute, including zero. + # + # + # === Tree-Base + # We said above that X.400 Distinguished Names are globally unique. + # In a manner reminiscent of DNS, LDAP supposes that each directory server + # contains authoritative attribute data for a set of DNs corresponding + # to a specific sub-tree of the (notional) global directory tree. + # This subtree is generally configured into a directory server when it is + # created. It matters for this discussion because most servers will not + # allow you to query them unless you specify a correct tree-base. + # + # Let's say you work for the engineering department of Big Company, Inc., + # whose internet domain is bigcompany.com. You may find that your departmental + # directory is stored in a server with a defined tree-base of + # ou=engineering,dc=bigcompany,dc=com + # You will need to supply this string as the tree-base when querying this + # directory. (Ou is a very old X.400 term meaning "organizational unit." + # Dc is a more recent term meaning "domain component.") + # + # === LDAP Versions + # (stub, discuss v2 and v3) + # + # === LDAP Operations + # The essential operations are: bind, search, add, modify, delete, and rename. + # ==== Bind + # Bind supplies a user's authentication credentials to a server, which in turn verifies + # or rejects them. There is a range of possibilities for credentials, but most directories + # support a simple username and password authentication. + # + # Taken by itself, the bind operation can be used to authenticate a user against information + # stored in a directory, for example to permit or deny access to some other resource. + # In terms of the other LDAP operations, most directories require a successful bind to + # be performed before the other operations will be permitted. Some servers permit certain + # operations to be performed with an "anonymous" binding, meaning that no credentials are + # presented by the user. (We're glossing over a lot of platform-specific detail here.) + # + # ==== Search + # Searching the directory involves specifying a treebase, a set of search filters, + # and a list of attribute values. + # The filters specify ranges of possible values for particular attributes. Multiple + # filters can be joined together with AND, OR, and NOT operators. + # A server will respond to a search by returning a list of matching DNs together with a + # set of attribute values for each entity, depending on what attributes the search requested. + # + # ==== Add + # An add operation specifies a new DN and an initial set of attribute values. If the operation + # succeeds, a new entity with the corresponding DN and attributes is added to the directory. + # + # ==== Modify + # Modify specifies an entity DN, and a list of attribute operations. Modify is used to change + # the attribute values stored in the directory for a particular entity. + # Modify may add or delete attributes (which are lists of values) or it change attributes by + # adding to or deleting from their values. + # + # ==== Delete + # The delete operation specifies an entity DN. If it succeeds, the entity and all its attributes + # is removed from the directory. + # + # ==== Rename (or Modify RDN) + # Rename (or Modify RDN) is an operation added to version 3 of the LDAP protocol. It responds to + # the often-arising need to change the DN of an entity without discarding its attribute values. + # In earlier LDAP versions, the only way to do this was to delete the whole entity and add it + # again with a different DN. + # + # Rename works by taking an "old" DN (the one to change) and a "new RDN," which is the left-most + # part of the DN string. If successful, rename changes the entity DN so that its left-most + # node corresponds to the new RDN given in the request. (RDN, or "relative distinguished name," + # denotes a single tree-node as expressed in a DN, which is a chain of tree nodes.) + # + # == How to use Net::LDAP + # + # This is how to access Net::LDAP functionality in your Ruby programs + # (note that at present, Net::LDAP is provided as a gem): + # + # require 'rubygems' + # require 'net/ldap' + # + # Most operations with Net::LDAP start by instantiating a Net::LDAP object. + # The constructor for this object takes arguments specifying the network location + # (address and port) of the LDAP server, and also the binding (authentication) + # credentials, typically a username and password. + # Given an object of class Net:LDAP, you can then perform LDAP operations by calling + # instance methods on the object. These are documented with usage examples below. + # + # The Net::LDAP library is designed to be very disciplined about how it makes network + # connections to servers. This is different from many of the standard native-code + # libraries that are provided on most platforms, and that share bloodlines with the + # original Netscape/Michigan LDAP client implementations. These libraries sought to + # insulate user code from the workings of the network. This is a good idea of course, + # but the practical effect has been confusing and many difficult bugs have been caused + # by the opacity of the native libraries, and their variable behavior across platforms. + # + # In general, Net::LDAP instance methods which invoke server operations make a connection + # to the server when the method is called. They execute the operation (typically binding first) + # and then disconnect from the server. The exception is Net::LDAP#open, which makes a connection + # to the server and then keeps it open while it executes a user-supplied block. Net::LDAP#open + # closes the connection on completion of the block. + # + class LDAP class LdapError < Exception; end From b5799f66d069e969396fbe492cc82175a070a424 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 23 Apr 2006 19:43:37 +0000 Subject: [PATCH 043/231] documents --- lib/net/ldap.rb | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index a3cd55a..3733a69 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -39,6 +39,36 @@ module Net # the LDAP protocol itself, and thus presenting as Ruby-like # a programming interface as possible. # + # === Quick-start for the Impatient + # require 'rubygems' + # require 'net/ldap' + # + # ldap = Net::LDAP.new :host => server_ip_address, + # :port => 389, + # :auth => { + # :method => :simple, + # :username => "cn=manager,dc=example,dc=com", + # :password => "opensesame" + # } + # + # filter = Net::LDAP::Filter.eq?( "cn", "George*" ) + # treebase = "dc=example,dc=com" + # + # ldap.search( :base => treebase, :filter => filter ) do |result| + # result.each do |dn, attrs| + # puts "DN: #{dn}" + # attrs.each do |attr, values| + # puts "***Attr: #{attr}" + # values.each do |value| + # puts " #{value}" + # end + # end + # end + # end + # + # p ldap.get_operation_result + # + # # == Quick introduction to LDAP # # We're going to provide a quick and highly informal introduction to LDAP From bf9e230a09a473d78b7c2e6d3c2768f6f603835e Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 24 Apr 2006 16:27:30 +0000 Subject: [PATCH 044/231] added an error message string (65) --- lib/net/ldap.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 3733a69..0e32998 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -282,6 +282,7 @@ module Net 51 => "Busy", 52 => "Unavailable", 53 => "Unwilling to perform", + 65 => "Object Class Violation", 68 => "Entry Already Exists" } From 6f2d527c24e121a09f38abfa620895cb3d9f32db Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 24 Apr 2006 21:58:14 +0000 Subject: [PATCH 045/231] Added a password-hash generator. --- lib/net/ldap.rb | 1 + lib/net/ldap/psw.rb | 62 +++++++++++++++++++++++++++++++++++++++++++++ tests/testem.rb | 1 + tests/testpsw.rb | 28 ++++++++++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 lib/net/ldap/psw.rb create mode 100644 tests/testpsw.rb diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 0e32998..feaf205 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -22,6 +22,7 @@ require 'net/ber' require 'net/ldap/pdu' require 'net/ldap/filter' require 'net/ldap/dataset' +require 'net/ldap/psw' module Net diff --git a/lib/net/ldap/psw.rb b/lib/net/ldap/psw.rb new file mode 100644 index 0000000..dd7d917 --- /dev/null +++ b/lib/net/ldap/psw.rb @@ -0,0 +1,62 @@ +# $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 Password + class << self + + # Generate a password-hash suitable for inclusion in an LDAP attribute. + # STUB: This is here to fulfill the requirements of an RFC, which one? + # TODO, gotta do salted-sha and (maybe) salted-md5. + # Should we provide sha1 as a synonym for sha1? I vote no because then + # should you also provide ssha1 for symmetry? + def generate( type, str ) + case type + when :md5 + require 'md5' + "{MD5}#{ [MD5.new( str.to_s ).digest].pack("m").chomp }" + when :sha + require 'sha1' + "{SHA}#{ [SHA1.new( str.to_s ).digest].pack("m").chomp }" + # when ssha + else + raise Net::LDAP::LdapError.new( "unsupported password-hash type (#{type})" ) + end + end + + end +end + + +end # class LDAP +end # module Net + + diff --git a/tests/testem.rb b/tests/testem.rb index a78f24a..64d8613 100644 --- a/tests/testem.rb +++ b/tests/testem.rb @@ -6,5 +6,6 @@ require 'test/unit' require 'tests/testber' require 'tests/testldif' require 'tests/testldap' +require 'tests/testpsw' diff --git a/tests/testpsw.rb b/tests/testpsw.rb new file mode 100644 index 0000000..e816450 --- /dev/null +++ b/tests/testpsw.rb @@ -0,0 +1,28 @@ +# $Id$ +# +# + + +$:.unshift "lib" + +require 'net/ldap' +require 'stringio' + + +class TestPassword < Test::Unit::TestCase + + def setup + end + + + def test_psw + assert_equal( "{MD5}xq8jwrcfibi0sZdZYNkSng==", Net::LDAP::Password.generate( :md5, "cashflow" )) + assert_equal( "{SHA}YE4eGkN4BvwNN1f5R7CZz0kFn14=", Net::LDAP::Password.generate( :sha, "cashflow" )) + end + + + + +end + + From 193f76e53079f4ebdbeb52e2740e836ad33f3777 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 24 Apr 2006 21:59:35 +0000 Subject: [PATCH 046/231] documentation --- lib/net/ldap/psw.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/net/ldap/psw.rb b/lib/net/ldap/psw.rb index dd7d917..b4f0442 100644 --- a/lib/net/ldap/psw.rb +++ b/lib/net/ldap/psw.rb @@ -34,6 +34,8 @@ class Password class << self # Generate a password-hash suitable for inclusion in an LDAP attribute. + # Pass a hash type (currently supported: :md5 and :sha) and a plaintext + # password. This function will return a hashed representation. # STUB: This is here to fulfill the requirements of an RFC, which one? # TODO, gotta do salted-sha and (maybe) salted-md5. # Should we provide sha1 as a synonym for sha1? I vote no because then From 7be5474f0611ecfac3b8cc13589112432f3e68b9 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 25 Apr 2006 14:03:11 +0000 Subject: [PATCH 047/231] Significant mods to LDAP#search --- lib/net/ldap.rb | 117 ++++++++++++++++++++++++++++++++++++------ lib/net/ldap/entry.rb | 71 +++++++++++++++++++++++++ lib/net/ldap/pdu.rb | 7 ++- 3 files changed, 178 insertions(+), 17 deletions(-) create mode 100644 lib/net/ldap/entry.rb diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index feaf205..9d38702 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -23,6 +23,7 @@ require 'net/ldap/pdu' require 'net/ldap/filter' require 'net/ldap/dataset' require 'net/ldap/psw' +require 'net/ldap/entry' module Net @@ -372,17 +373,17 @@ module Net # Note that in the standalone case, we're permitting the caller # to modify the auth parms. # - def search args + def searchx args if @open_connection - @result = @open_connection.search( args ) {|values| - block_given? and yield( values ) + @result = @open_connection.searchx( args ) {|values| + yield( values ) if block_given? } else @result = 0 conn = Connection.new( :host => @host, :port => @port ) if (@result = conn.bind( args[:auth] || @auth )) == 0 - @result = conn.search( args ) {|values| - block_given? and yield( values ) + @result = conn.searchx( args ) {|values| + yield( values ) if block_given? } end conn.close @@ -391,6 +392,49 @@ module Net @result == 0 end + #-- + # This is a re-implementation of search that replaces the + # original one (now renamed searchx and possibly destined to go away). + # The difference is that we return a dataset (or nil) from the + # call, and pass _each entry_ as it is received from the server + # to the caler-supplied block. This will probably make things + # far faster as we can do useful work during the network latency + # of the search. The downside is that we have no access to the + # whole set while processing the blocks, so we can't do stuff + # like sort the DNs until after the call completes. + # It's also possible that this interacts badly with server timeouts. + # We'll have to ensure that something reasonable happens if + # the caller has processed half a result set when we throw a timeout + # error. + # Another important difference is that we return a result set from + # this method rather than a T/F indication. + # Since this can be very heavy-weight, we define an argument flag + # that the caller can set to suppress the return of a result set, + # if he's planning to process every entry as it comes from the server. + # + def search args + result_set = (args and args[:return_result] == false) ? nil : {} + + if @open_connection + @result = @open_connection.search( args ) {|entry| + result_set[entry.dn] = entry if result_set + yield( entry ) if block_given? + } + else + @result = 0 + conn = Connection.new( :host => @host, :port => @port ) + if (@result = conn.bind( args[:auth] || @auth )) == 0 + @result = conn.search( args ) {|entry| + result_set[entry.dn] = entry if result_set + yield( entry ) if block_given? + } + end + conn.close + end + + @result == 0 and result_set + end + # # bind # Bind and unbind. @@ -505,7 +549,7 @@ module Net raise LdapError.new( "no connection to server" ) end - block_given? and yield self + yield self if block_given? end @@ -552,15 +596,66 @@ module Net # # search + # Alternate implementation, this yields each search entry to the caller + # as it are received. # TODO, certain search parameters are hardcoded. # TODO, if we mis-parse the server results or the results are wrong, we can block # forever. That's because we keep reading results until we get a type-5 packet, # which might never come. We need to support the time-limit in the protocol. + #-- + # WARNING: this code substantially recapitulates the searchx method. # def search args search_filter = (args && args[:filter]) || Filter.eq( "objectclass", "*" ) search_base = (args && args[:base]) || "dc=example,dc=com" search_attributes = ((args && args[:attributes]) || []).map {|attr| attr.to_s.to_ber} + + request = [ + search_base.to_ber, + 2.to_ber_enumerated, + 0.to_ber_enumerated, + 0.to_ber, + 0.to_ber, + false.to_ber, + search_filter.to_ber, + search_attributes.to_ber_sequence + ].to_ber_appsequence(3) + pkt = [next_msgid.to_ber, request].to_ber_sequence + @conn.write pkt + + result_code = 0 + + while (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) + case pdu.app_tag + when 4 # search-data + yield( pdu.search_entry ) if block_given? + when 5 # search-result + result_code = pdu.result_code + break + else + raise LdapError.new( "invalid response-type in search: #{pdu.app_tag}" ) + end + end + + result_code + end + + + # + # searchx + # Original implementation, this doesn't return until all data have been + # received from the server. + # TODO, certain search parameters are hardcoded. + # TODO, if we mis-parse the server results or the results are wrong, we can block + # forever. That's because we keep reading results until we get a type-5 packet, + # which might never come. We need to support the time-limit in the protocol. + #-- + # WARNING: this code substantially recapitulates the search method. + # + def searchx args + search_filter = (args && args[:filter]) || Filter.eq( "objectclass", "*" ) + search_base = (args && args[:base]) || "dc=example,dc=com" + search_attributes = ((args && args[:attributes]) || []).map {|attr| attr.to_s.to_ber} request = [ search_base.to_ber, 2.to_ber_enumerated, @@ -665,13 +760,3 @@ module Net end # module Net -#------------------------------------------------------ - -if __FILE__ == $0 - puts "No default action" -end - - - - - diff --git a/lib/net/ldap/entry.rb b/lib/net/ldap/entry.rb new file mode 100644 index 0000000..8abb3a3 --- /dev/null +++ b/lib/net/ldap/entry.rb @@ -0,0 +1,71 @@ +# $Id$ +# +# LDAP Entry (search-result) 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 LDAP + + + class Entry + + def initialize dn = nil + @myhash = Hash.new {|k,v| k[v] = [] } + self[:dn] = [dn] + end + + + def []= name, value + sym = name.to_s.downcase.intern + @myhash[sym] = value + end + + def [] name + unless name.is_a?(Symbol) + name = name.to_s.downcase.intern + end + @myhash[name] + end + + def dn + self[:dn].shift + end + + def attribute_names + @myhash.keys + end + + + end # class Entry + + +end # class LDAP +end # module Net + + diff --git a/lib/net/ldap/pdu.rb b/lib/net/ldap/pdu.rb index 236fb75..5dadc0c 100644 --- a/lib/net/ldap/pdu.rb +++ b/lib/net/ldap/pdu.rb @@ -44,7 +44,7 @@ class LdapPdu ModifyRDNResponse = 13 attr_reader :msg_id, :app_tag - attr_reader :search_dn, :search_attributes + attr_reader :search_dn, :search_attributes, :search_entry # # initialize @@ -129,12 +129,17 @@ class LdapPdu # 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. + # ALTERNATE IMPLEMENTATION: In addition to @search_dn and @search_attributes, + # we also return @search_entry, which is an LDAP::Entry object. + # If that works out well, then we'll remove the first two. # def parse_search_return sequence sequence.length >= 2 or raise LdapPduError + @search_entry = LDAP::Entry.new( sequence[0] ) @search_dn = sequence[0] @search_attributes = {} sequence[1].each {|seq| + @search_entry[seq[0]] = seq[1] @search_attributes[seq[0].downcase.intern] = seq[1] } end From 28185fdf0f692f65594a5d8315b65cc779c9aaff Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 25 Apr 2006 20:15:46 +0000 Subject: [PATCH 048/231] Improved the search-result handling --- lib/net/ldap.rb | 18 ++++++++---------- lib/net/ldap/entry.rb | 17 ++++++++++++----- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 9d38702..9c1a7a0 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -53,17 +53,15 @@ module Net # :password => "opensesame" # } # - # filter = Net::LDAP::Filter.eq?( "cn", "George*" ) + # filter = Net::LDAP::Filter.eq( "cn", "George*" ) # treebase = "dc=example,dc=com" # - # ldap.search( :base => treebase, :filter => filter ) do |result| - # result.each do |dn, attrs| - # puts "DN: #{dn}" - # attrs.each do |attr, values| - # puts "***Attr: #{attr}" - # values.each do |value| - # puts " #{value}" - # end + # ldap.search( :base => treebase, :filter => filter ) do |entry| + # puts "DN: #{entry.dn}" + # entry.each do |attribute, values| + # puts " #{attribute}:" + # values.each do |value| + # puts " --->#{value}" # end # end # end @@ -425,7 +423,7 @@ module Net conn = Connection.new( :host => @host, :port => @port ) if (@result = conn.bind( args[:auth] || @auth )) == 0 @result = conn.search( args ) {|entry| - result_set[entry.dn] = entry if result_set + (result_set[entry.dn] = entry) if result_set yield( entry ) if block_given? } end diff --git a/lib/net/ldap/entry.rb b/lib/net/ldap/entry.rb index 8abb3a3..fc6bf18 100644 --- a/lib/net/ldap/entry.rb +++ b/lib/net/ldap/entry.rb @@ -37,7 +37,7 @@ class LDAP def initialize dn = nil @myhash = Hash.new {|k,v| k[v] = [] } - self[:dn] = [dn] + @myhash[:dn] = [dn] end @@ -47,20 +47,27 @@ class LDAP end def [] name - unless name.is_a?(Symbol) - name = name.to_s.downcase.intern - end + #unless name.is_a?(Symbol) + # name = name.to_s.downcase.intern + #end @myhash[name] end def dn - self[:dn].shift + self[:dn][0] end def attribute_names @myhash.keys end + def each + if block_given? + attribute_names.each {|a| yield a, self[a] } + end + end + + alias_method :each_attribute, :each end # class Entry From e231a5359eb82d816d55f57db10c3ba78eb1a87e Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 26 Apr 2006 01:43:57 +0000 Subject: [PATCH 049/231] documentation --- lib/net/ldap.rb | 158 ++++++++++++++++++++++++++++++----------- lib/net/ldap/filter.rb | 63 +++++++++++++--- 2 files changed, 170 insertions(+), 51 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 9c1a7a0..cdb0583 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -151,49 +151,49 @@ module Net # (stub, discuss v2 and v3) # # === LDAP Operations - # The essential operations are: bind, search, add, modify, delete, and rename. + # The essential operations are: #bind, #search, #add, #modify, #delete, and #rename. # ==== Bind - # Bind supplies a user's authentication credentials to a server, which in turn verifies + # #bind supplies a user's authentication credentials to a server, which in turn verifies # or rejects them. There is a range of possibilities for credentials, but most directories # support a simple username and password authentication. # - # Taken by itself, the bind operation can be used to authenticate a user against information + # Taken by itself, #bind can be used to authenticate a user against information # stored in a directory, for example to permit or deny access to some other resource. - # In terms of the other LDAP operations, most directories require a successful bind to + # In terms of the other LDAP operations, most directories require a successful #bind to # be performed before the other operations will be permitted. Some servers permit certain # operations to be performed with an "anonymous" binding, meaning that no credentials are # presented by the user. (We're glossing over a lot of platform-specific detail here.) # # ==== Search - # Searching the directory involves specifying a treebase, a set of search filters, + # Calling #search against the directory involves specifying a treebase, a set of search filters, # and a list of attribute values. # The filters specify ranges of possible values for particular attributes. Multiple # filters can be joined together with AND, OR, and NOT operators. - # A server will respond to a search by returning a list of matching DNs together with a + # A server will respond to a #search by returning a list of matching DNs together with a # set of attribute values for each entity, depending on what attributes the search requested. # # ==== Add - # An add operation specifies a new DN and an initial set of attribute values. If the operation + # #add operation specifies a new DN and an initial set of attribute values. If the operation # succeeds, a new entity with the corresponding DN and attributes is added to the directory. # # ==== Modify - # Modify specifies an entity DN, and a list of attribute operations. Modify is used to change + # #modify specifies an entity DN, and a list of attribute operations. #modify is used to change # the attribute values stored in the directory for a particular entity. - # Modify may add or delete attributes (which are lists of values) or it change attributes by + # #modify may add or delete attributes (which are lists of values) or it change attributes by # adding to or deleting from their values. # # ==== Delete - # The delete operation specifies an entity DN. If it succeeds, the entity and all its attributes + # #delete operation specifies an entity DN. If it succeeds, the entity and all its attributes # is removed from the directory. # # ==== Rename (or Modify RDN) - # Rename (or Modify RDN) is an operation added to version 3 of the LDAP protocol. It responds to + # #rename (or #modify_rdn) is an operation added to version 3 of the LDAP protocol. It responds to # the often-arising need to change the DN of an entity without discarding its attribute values. # In earlier LDAP versions, the only way to do this was to delete the whole entity and add it # again with a different DN. # - # Rename works by taking an "old" DN (the one to change) and a "new RDN," which is the left-most - # part of the DN string. If successful, rename changes the entity DN so that its left-most + # #rename works by taking an "old" DN (the one to change) and a "new RDN," which is the left-most + # part of the DN string. If successful, #rename changes the entity DN so that its left-most # node corresponds to the new RDN given in the request. (RDN, or "relative distinguished name," # denotes a single tree-node as expressed in a DN, which is a chain of tree nodes.) # @@ -293,10 +293,20 @@ module Net ResultStrings[code] || "unknown result (#{code})" end + # Instantiate an object of type Net::LDAP to perform directory operations. + # This constructor takes a hash containing arguments. The following arguments + # are supported: + # * :host => the LDAP server's IP-address (default 127.0.0.1) + # * :port => the LDAP server's TCP port (default 389) + # * :auth => a hash containing authorization parameters. Currently supported values include: + # {:method => :anonymous} and + # {:method => :simple, :username => your_user_name, :password => your_password } # - # initialize + # Instantiating a Net::LDAP object does not result in network traffic to + # the LDAP server. It simply stores the connection and binding parameters in the + # object. # - def initialize args + def initialize args = {} @host = args[:host] || DefaultHost @port = args[:port] || DefaultPort @verbose = false # Make this configurable with a switch on the class. @@ -308,18 +318,29 @@ module Net @open_connection = nil end + # #open takes the same parameters as #new. #open makes a network connection to the + # LDAP server and then passes a newly-created Net::LDAP object to the caller-supplied block. + # Within the block, you can call any of the instance methods of Net::LDAP to + # perform operations against the LDAP directory. #open will perform all the + # operations in the user-supplied block on the same network connection, which + # will be closed automatically when the block finishes. # - # open + # auth = {:method => :simple, :username => username, :password => password} + # Net::LDAP.open( :host => ipaddress, :port => 389, :auth => auth ) do |ldap| + # ldap.search( ... ) + # ldap.add( ... ) + # ldap.modify( ... ) + # end # def LDAP::open args - ldap = LDAP.new args - ldap.open {|ldap1| yield ldap1 } + ldap1 = LDAP.new args + ldap1.open {|ldap| yield ldap } end - # This method will return a meaningful result any time after - # a protocol operation (bind, search, add, modify, rename, delete) + # Returns a meaningful result any time after + # a protocol operation (#bind, #search, #add, #modify, #rename, #delete) # has completed. - # It returns an OpenStruct containing an LDAP result code (0 means success), + # It returns an #OpenStruct containing an LDAP result code (0 means success), # and a human-readable string. # unless ldap.bind # puts "Result: #{ldap.get_operation_result.code}" @@ -338,11 +359,22 @@ module Net end - # This method opens a network connection to the server and then - # passes self to the caller-supplied block. The connection is - # closed when the block completes. It's for executing multiple + # Opens a network connection to the server and then + # passes self to the caller-supplied block. The connection is + # closed when the block completes. Used for executing multiple # LDAP operations without requiring a separate network connection # (and authentication) for each one. + # Note: You do not need to log-in or "bind" to the server. This will + # be done for you automatically. + # For an even simpler approach, see the class method Net::LDAP#open. + # + # auth = {:method => :simple, :username => username, :password => password} + # ldap = Net::LDAP.new( :host => ipaddress, :port => 389, :auth => auth ) + # ldap.open do |ldap| + # ldap.search( ... ) + # ldap.add( ... ) + # ldap.modify( ... ) + # end #-- # First we make a connection and then a binding, but we don't # do anything with the bind results. @@ -358,8 +390,8 @@ module Net end - # - # search + # DEPRECATED. Performs an LDAP search, waits for the operation to complete, and + # passes a result set to the caller-supplied block. #-- # If an open call is in progress (@open_connection will be non-nil), # then ASSUME a bind has been performed and accepted, and just @@ -390,6 +422,46 @@ module Net @result == 0 end + # Searches the LDAP directory for directory entries. + # Takes a hash argument with parameters. Supported parameters include: + # * :base (a string specifying the tree-base for the search); + # * :filter (an object of type Net::LDAP::Filter, defaults to objectclass=*); + # * :attributes (a string or array of strings specifying the LDAP attributes to return from the server); + # * :return_result (a boolean specifying whether to return a result set). + # + # #search queries the LDAP server and passes each entry to the + # caller-supplied block, as an object of type Net::LDAP::Entry. + # If the search returns 1000 entries, the block will + # be called 1000 times. If the search returns no entries, the block will + # not be called. + # + # #search returns either a result-set or a boolean, depending on the + # value of the :return_result argument. The default behavior is to return + # a result set, which is a hash. Each key in the hash is a string specifying + # the DN of an entry. The corresponding value for each key is a Net::LDAP::Entry object. + # If you request a result set and #search fails with an error, it will return nil. + # Call #get_operation_result to get the error information returned by + # the LDAP server. + # + # When :return_result => false, #search will + # return only a Boolean, to indicate whether the operation succeeded. This can improve performance + # with very large result sets, because the library can discard each entry from memory after + # your block processes it. + # + # + # treebase = "dc=example,dc=com" + # filter = Net::LDAP::Filter.eq( "mail", "a*.com" ) + # attrs = ["mail", "cn", "sn", "objectclass"] + # ldap.search( :base => treebase, :filter => filter, :attributes => attrs, :return_result => false ) do |entry| + # puts "DN: #{entry.dn}" + # entry.each do |attr, values| + # puts ".......#{attr}:" + # values.each do |value| + # puts " #{value}" + # end + # end + # end + # #-- # This is a re-implementation of search that replaces the # original one (now renamed searchx and possibly destined to go away). @@ -433,10 +505,12 @@ module Net @result == 0 and result_set end - # - # bind - # Bind and unbind. - # Can serve as a connectivity test as well as an auth test. + # #bind connects to the LDAP server and requests authentication + # based on the :auth parameter passed to #open or #new. + # It takes no parameters. + # User code generally will not call #bind. It will be called + # implicitly by the library whenever an LDAP operation is + # requested. #bind can be useful to test authentication. #-- # If there is an @open_connection, then perform the bind # on it. Otherwise, connect, bind, and disconnect. @@ -455,8 +529,7 @@ module Net end # - # bind_as - # This is for testing authentication credentials. + # #bind_as is for testing authentication credentials. # Most likely a "standard" name (like a CN or an email # address) will be presented along with a password. # We'll bind with the main credential given in the @@ -464,6 +537,8 @@ module Net # to us as a parameter, then unbind and rebind as the # new user. # + # This method is currently an unimplemented stub. + # def bind_as end @@ -532,12 +607,13 @@ module Net class LDAP + # This is a private class used internally by the library. It should not be called by user code. class Connection LdapVersion = 3 - # + #-- # initialize # def initialize server @@ -551,7 +627,7 @@ module Net end - # + #-- # close # This is provided as a convenience method to make # sure a connection object gets closed without waiting @@ -562,7 +638,7 @@ module Net @conn = nil end - # + #-- # next_msgid # def next_msgid @@ -571,7 +647,7 @@ module Net end - # + #-- # bind # def bind auth @@ -592,7 +668,7 @@ module Net pdu.result_code end - # + #-- # search # Alternate implementation, this yields each search entry to the caller # as it are received. @@ -639,7 +715,7 @@ module Net end - # + #-- # searchx # Original implementation, this doesn't return until all data have been # received from the server. @@ -686,7 +762,7 @@ module Net result_code end - # + #-- # modify # TODO, need to support a time limit, in case the server fails to respond. # TODO!!! We're throwing an exception here on empty DN. @@ -713,7 +789,7 @@ module Net end - # + #-- # add # TODO, need to support a time limit, in case the server fails to respond. # @@ -733,7 +809,7 @@ module Net end - # + #-- # rename # TODO, need to support a time limit, in case the server fails to respond. # diff --git a/lib/net/ldap/filter.rb b/lib/net/ldap/filter.rb index 4293ef5..96be82d 100644 --- a/lib/net/ldap/filter.rb +++ b/lib/net/ldap/filter.rb @@ -29,6 +29,23 @@ module Net class LDAP + +# Class Net::LDAP::Filter is used to constrain +# LDAP searches. An object of this class is +# passed to Net::LDAP#search in the parameter :filter. +# +# Net::LDAP::Filter supports the complete set of search filters +# available in LDAP, including conjunction, disjunction and negation +# (AND, OR, and NOT). This class supplants the (infamous) RFC-2254 +# standard notation for specifying LDAP search filters. +# +# Here's how to code the familiar "objectclass is present" filter: +# f = Net::LDAP::Filter.pres( "objectclass" ) +# The object returned by this code can be passed directly to +# the :filter parameter of Net::LDAP#search. +# +# See the individual class and instance methods below for more examples. +# class Filter def initialize op, a, b @@ -37,16 +54,40 @@ class Filter @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 + # #eq creates a filter object indicating that the value of + # a paticular attribute must be either present or must + # match a particular string. + # + # To specify that an attribute is "present" means that only + # directory entries which contain a value for the particular + # attribute will be selected by the filter. This is useful + # in case of optional attributes such as mail. + # Presence is indicated by giving the value "*" in the second + # parameter to #eq. This example selects only entries that have + # one or more values for sAMAccountName: + # f = Net::LDAP::Filter.eq( "sAMAccountName", "*" ) + # + # To match a particular range of values, pass a string as the + # second parameter to #eq. The string may contain one or more + # "*" characters as wildcards: these match zero or more occurrences + # of any character. Full regular-expressions are not supported + # due to limitations in the underlying LDAP protocol. + # This example selects any entry with a mail value containing + # the substring "anderson": + # f = Net::LDAP::Filter.eq( "mail", "*anderson*" ) + # + def Filter::eq attribute, value; Filter.new :eq, attribute, value; end + def Filter::ne attribute, value; Filter.new :ne, attribute, value; end + def Filter::gt attribute, value; Filter.new :gt, attribute, value; end + def Filter::lt attribute, value; Filter.new :lt, attribute, value; end + def Filter::ge attribute, value; Filter.new :ge, attribute, value; end + def Filter::le attribute, value; Filter.new :le, attribute, value; end - def & a; Filter.new :and, self, a; end - def | a; Filter.new :or, self, a; end + def & filter; Filter.new :and, self, filter; end + def | filter; Filter.new :or, self, filter; end + # + #-- # This operator can't be !, evidently. Try it. def ~@; Filter.new :not, self, nil; end @@ -76,7 +117,7 @@ class Filter end - # + #-- # to_ber # Filter ::= # CHOICE { @@ -154,7 +195,7 @@ class 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 @@ -172,6 +213,7 @@ class Filter + #-- # We get a Ruby object which comes from parsing an RFC-1777 "Filter" # object. Convert it to a Net::LDAP::Filter. # TODO, we're hardcoding the RFC-1777 BER-encodings of the various @@ -189,6 +231,7 @@ class Filter end + #-- # We got a hash of attribute values. # Do we match the attributes? # Return T/F, and call match recursively as necessary. From b4c28477f9592d48d30df12dc956e5195136e951 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 26 Apr 2006 02:10:34 +0000 Subject: [PATCH 050/231] documentation --- lib/net/ldap/filter.rb | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/net/ldap/filter.rb b/lib/net/ldap/filter.rb index 96be82d..e16a42e 100644 --- a/lib/net/ldap/filter.rb +++ b/lib/net/ldap/filter.rb @@ -83,14 +83,36 @@ class Filter def Filter::ge attribute, value; Filter.new :ge, attribute, value; end def Filter::le attribute, value; Filter.new :le, attribute, value; end + # #pres( attribute ) is a synonym for #eq( attribute, "*" ) + # + def Filter::pres attribute; Filter.eq attribute, "*"; end + + # operator & ("AND") is used to conjoin two or more filters. + # This expression will select only entries that have an objectclass + # attribute AND have a mail attribute that begins with "George": + # f = Net::LDAP::Filter.pres( "objectclass" ) & Net::LDAP::Filter.eq( "mail", "George*" ) + # def & filter; Filter.new :and, self, filter; end + + # operator | ("OR") is used to disjoin two or more filters. + # This expression will select entries that have either an objectclass + # attribute OR a mail attribute that begins with "George": + # f = Net::LDAP::Filter.pres( "objectclass" ) | Net::LDAP::Filter.eq( "mail", "George*" ) + # def | filter; Filter.new :or, self, filter; end + + # + # operator ~ ("NOT") is used to negate a filter. + # This expression will select only entries that do not have an objectclass + # attribute: + # f = ~ Net::LDAP::Filter.pres( "objectclass" ) # #-- # This operator can't be !, evidently. Try it. def ~@; Filter.new :not, self, nil; end + def to_s case @op when :ne From 90616a3ca932d4b3a472b6a5d6ac29c68599f637 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 26 Apr 2006 02:57:34 +0000 Subject: [PATCH 051/231] lost superfluous default methods --- lib/net/ber.rb | 9 --------- lib/net/ldap/dataset.rb | 8 -------- lib/net/ldap/filter.rb | 6 ------ lib/net/ldap/pdu.rb | 7 ------- lib/net/ldif.rb | 11 ----------- 5 files changed, 41 deletions(-) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index cf64bca..7737de3 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -274,12 +274,3 @@ class Array end # class Array - -#---------------------------------------------- - -if __FILE__ == $0 - puts "No default action" -end - - - diff --git a/lib/net/ldap/dataset.rb b/lib/net/ldap/dataset.rb index a1c80a7..3fc60d0 100644 --- a/lib/net/ldap/dataset.rb +++ b/lib/net/ldap/dataset.rb @@ -106,11 +106,3 @@ end # LDAP end # Net -#----------------------------------- - -if __FILE__ == $0 - puts "No default action" -end - - - diff --git a/lib/net/ldap/filter.rb b/lib/net/ldap/filter.rb index e16a42e..66848a4 100644 --- a/lib/net/ldap/filter.rb +++ b/lib/net/ldap/filter.rb @@ -277,9 +277,3 @@ end # class Net::LDAP end # module Net -#----------------------------------- - -if __FILE__ == $0 - puts "No default action" -end - diff --git a/lib/net/ldap/pdu.rb b/lib/net/ldap/pdu.rb index 5dadc0c..ceabf2d 100644 --- a/lib/net/ldap/pdu.rb +++ b/lib/net/ldap/pdu.rb @@ -150,10 +150,3 @@ end end # module Net -#------------------------------------------- - -if __FILE__ == $0 - puts "No default action for this file" -end - - diff --git a/lib/net/ldif.rb b/lib/net/ldif.rb index 8acc6cb..be95bcd 100644 --- a/lib/net/ldif.rb +++ b/lib/net/ldif.rb @@ -37,14 +37,3 @@ module Net end # module Net -#------------------------------------------------------ - -if __FILE__ == $0 - puts "No default action" -end - - - - - - From 65102e44d92c7d4d8245798450da9b96de3995a5 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 26 Apr 2006 03:08:32 +0000 Subject: [PATCH 052/231] spelling error --- lib/net/ldap.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index cdb0583..dac2b0c 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -467,7 +467,7 @@ module Net # original one (now renamed searchx and possibly destined to go away). # The difference is that we return a dataset (or nil) from the # call, and pass _each entry_ as it is received from the server - # to the caler-supplied block. This will probably make things + # to the caller-supplied block. This will probably make things # far faster as we can do useful work during the network latency # of the search. The downside is that we have no access to the # whole set while processing the blocks, so we can't do stuff From 2faf22a2afe49edef1f0a3fc3f2b56688b26adbc Mon Sep 17 00:00:00 2001 From: austin Date: Sun, 30 Apr 2006 03:34:34 +0000 Subject: [PATCH 053/231] Added the framework for release. I haven't actually verified this yet for Net::LDAP, but I used the same framework for the recent release of MIME::Types. Some pieces of the code may need to be moved around to better handle the testing framework in the Rakefile and the pre-setup.rb file. To make this work, you will need the meta_project, gmailer, and archive-tar-minitar gems. --- COPYING | 469 +++++++-------- ChangeLog | 15 + Install | 21 + LICENCE | 55 ++ README | 35 +- RELEASE_NOTES | 6 - Rakefile | 244 +++++++- Release-Announcement | 33 + net-ldap.gemspec | 13 + pre-setup.rb | 46 ++ setup.rb | 1366 ++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 2025 insertions(+), 278 deletions(-) create mode 100644 ChangeLog create mode 100644 Install create mode 100644 LICENCE delete mode 100644 RELEASE_NOTES create mode 100644 Release-Announcement create mode 100644 net-ldap.gemspec create mode 100644 pre-setup.rb create mode 100644 setup.rb diff --git a/COPYING b/COPYING index 3b70c5b..2ff629a 100644 --- a/COPYING +++ b/COPYING @@ -1,281 +1,272 @@ -. + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 +Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street, +Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and +distribute verbatim copies of this license document, but changing it is not +allowed. - Copyright (C) 1989, 1991 Free Software Foundation, Inc. - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. + Preamble - Preamble +The licenses for most software are designed to take away your freedom to +share and change it. By contrast, the GNU General Public License is +intended to guarantee your freedom to share and change free software--to +make sure the software is free for all its users. This General Public +License applies to most of the Free Software Foundation's software and to +any other program whose authors commit to using it. (Some other Free +Software Foundation software is covered by the GNU Lesser General Public +License instead.) You can apply it to your programs, too. - The licenses for most software are designed to take away your - freedom to share and change it. By contrast, the GNU General Public - License is intended to guarantee your freedom to share and change free - software--to make sure the software is free for all its users. This - General Public License applies to most of the Free Software - Foundation's software and to any other program whose authors commit to - using it. (Some other Free Software Foundation software is covered by - the GNU Lesser General Public License instead.) You can apply it to - your programs, too. +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish), that you receive source code or can get it if you want it, that you +can change the software or use pieces of it in new free programs; and that +you know you can do these things. - When we speak of free software, we are referring to freedom, not - price. Our General Public Licenses are designed to make sure that you - have the freedom to distribute copies of free software (and charge for - this service if you wish), that you receive source code or can get it - if you want it, that you can change the software or use pieces of it - in new free programs; and that you know you can do these things. +To protect your rights, we need to make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These +restrictions translate to certain responsibilities for you if you distribute +copies of the software, or if you modify it. - To protect your rights, we need to make restrictions that forbid - anyone to deny you these rights or to ask you to surrender the rights. - These restrictions translate to certain responsibilities for you if you - distribute copies of the software, or if you modify it. +For example, if you distribute copies of such a program, whether gratis or +for a fee, you must give the recipients all the rights that you have. You +must make sure that they, too, receive or can get the source code. And you +must show them these terms so they know their rights. - For example, if you distribute copies of such a program, whether - gratis or for a fee, you must give the recipients all the rights that - you have. You must make sure that they, too, receive or can get the - source code. And you must show them these terms so they know their - rights. +We protect your rights with two steps: (1) copyright the software, and (2) +offer you this license which gives you legal permission to copy, distribute +and/or modify the software. - We protect your rights with two steps: (1) copyright the software, and - (2) offer you this license which gives you legal permission to copy, - distribute and/or modify the software. +Also, for each author's protection and ours, we want to make certain that +everyone understands that there is no warranty for this free software. If +the software is modified by someone else and passed on, we want its +recipients to know that what they have is not the original, so that any +problems introduced by others will not reflect on the original authors' +reputations. - Also, for each author's protection and ours, we want to make certain - that everyone understands that there is no warranty for this free - software. If the software is modified by someone else and passed on, we - want its recipients to know that what they have is not the original, so - that any problems introduced by others will not reflect on the original - authors' reputations. +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that redistributors of a free program will +individually obtain patent licenses, in effect making the program +proprietary. To prevent this, we have made it clear that any patent must be +licensed for everyone's free use or not licensed at all. - Finally, any free program is threatened constantly by software - patents. We wish to avoid the danger that redistributors of a free - program will individually obtain patent licenses, in effect making the - program proprietary. To prevent this, we have made it clear that any - patent must be licensed for everyone's free use or not licensed at all. +The precise terms and conditions for copying, distribution and modification +follow. - The precise terms and conditions for copying, distribution and - modification follow. + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION +0. This License applies to any program or other work which contains a notice + placed by the copyright holder saying it may be distributed under the + terms of this General Public License. The "Program", below, refers to + any such program or work, and a "work based on the Program" means either + the Program or any derivative work under copyright law: that is to say, a + work containing the Program or a portion of it, either verbatim or with + modifications and/or translated into another language. (Hereinafter, + translation is included without limitation in the term "modification".) + Each licensee is addressed as "you". - 0. This License applies to any program or other work which contains - a notice placed by the copyright holder saying it may be distributed - under the terms of this General Public License. The "Program", below, - refers to any such program or work, and a "work based on the Program" - means either the Program or any derivative work under copyright law: - that is to say, a work containing the Program or a portion of it, - either verbatim or with modifications and/or translated into another - language. (Hereinafter, translation is included without limitation in - the term "modification".) Each licensee is addressed as "you". + Activities other than copying, distribution and modification are not + covered by this License; they are outside its scope. The act of running + the Program is not restricted, and the output from the Program is covered + only if its contents constitute a work based on the Program (independent + of having been made by running the Program). Whether that is true depends + on what the Program does. - Activities other than copying, distribution and modification are not - covered by this License; they are outside its scope. The act of - running the Program is not restricted, and the output from the Program - is covered only if its contents constitute a work based on the - Program (independent of having been made by running the Program). - Whether that is true depends on what the Program does. +1. You may copy and distribute verbatim copies of the Program's source code + as you receive it, in any medium, provided that you conspicuously and + appropriately publish on each copy an appropriate copyright notice and + disclaimer of warranty; keep intact all the notices that refer to this + License and to the absence of any warranty; and give any other recipients + of the Program a copy of this License along with the Program. - 1. You may copy and distribute verbatim copies of the Program's - source code as you receive it, in any medium, provided that you - conspicuously and appropriately publish on each copy an appropriate - copyright notice and disclaimer of warranty; keep intact all the - notices that refer to this License and to the absence of any warranty; - and give any other recipients of the Program a copy of this License - along with the Program. + You may charge a fee for the physical act of transferring a copy, and you + may at your option offer warranty protection in exchange for a fee. - You may charge a fee for the physical act of transferring a copy, and - you may at your option offer warranty protection in exchange for a fee. +2. You may modify your copy or copies of the Program or any portion of it, + thus forming a work based on the Program, and copy and distribute such + modifications or work under the terms of Section 1 above, provided that + you also meet all of these conditions: - 2. You may modify your copy or copies of the Program or any portion - of it, thus forming a work based on the Program, and copy and - distribute such modifications or work under the terms of Section 1 - above, provided that you also meet all of these conditions: + a) You must cause the modified files to carry prominent notices stating + that you changed the files and the date of any change. - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. + b) You must cause any work that you distribute or publish, that in whole + or in part contains or is derived from the Program or any part + thereof, to be licensed as a whole at no charge to all third parties + under the terms of this License. - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. + c) If the modified program normally reads commands interactively when + run, you must cause it, when started running for such interactive use + in the most ordinary way, to print or display an announcement + including an appropriate copyright notice and a notice that there is + no warranty (or else, saying that you provide a warranty) and that + users may redistribute the program under these conditions, and telling + the user how to view a copy of this License. (Exception: if the + Program itself is interactive but does not normally print such an + announcement, your work based on the Program is not required to print + an announcement.) - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) + These requirements apply to the modified work as a whole. If + identifiable sections of that work are not derived from the Program, and + can be reasonably considered independent and separate works in + themselves, then this License, and its terms, do not apply to those + sections when you distribute them as separate works. But when you + distribute the same sections as part of a whole which is a work based on + the Program, the distribution of the whole must be on the terms of this + License, whose permissions for other licensees extend to the entire + whole, and thus to each and every part regardless of who wrote it. - These requirements apply to the modified work as a whole. If - identifiable sections of that work are not derived from the Program, - and can be reasonably considered independent and separate works in - themselves, then this License, and its terms, do not apply to those - sections when you distribute them as separate works. But when you - distribute the same sections as part of a whole which is a work based - on the Program, the distribution of the whole must be on the terms of - this License, whose permissions for other licensees extend to the - entire whole, and thus to each and every part regardless of who wrote it. + Thus, it is not the intent of this section to claim rights or contest + your rights to work written entirely by you; rather, the intent is to + exercise the right to control the distribution of derivative or + collective works based on the Program. - Thus, it is not the intent of this section to claim rights or contest - your rights to work written entirely by you; rather, the intent is to - exercise the right to control the distribution of derivative or - collective works based on the Program. + In addition, mere aggregation of another work not based on the Program + with the Program (or with a work based on the Program) on a volume of a + storage or distribution medium does not bring the other work under the + scope of this License. - In addition, mere aggregation of another work not based on the Program - with the Program (or with a work based on the Program) on a volume of - a storage or distribution medium does not bring the other work under - the scope of this License. +3. You may copy and distribute the Program (or a work based on it, under + Section 2) in object code or executable form under the terms of Sections + 1 and 2 above provided that you also do one of the following: - 3. You may copy and distribute the Program (or a work based on it, - under Section 2) in object code or executable form under the terms of - Sections 1 and 2 above provided that you also do one of the following: + a) Accompany it with the complete corresponding machine-readable source + code, which must be distributed under the terms of Sections 1 and 2 + above on a medium customarily used for software interchange; or, - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, + b) Accompany it with a written offer, valid for at least three years, to + give any third party, for a charge no more than your cost of + physically performing source distribution, a complete machine-readable + copy of the corresponding source code, to be distributed under the + terms of Sections 1 and 2 above on a medium customarily used for + software interchange; or, - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, + c) Accompany it with the information you received as to the offer to + distribute corresponding source code. (This alternative is allowed + only for noncommercial distribution and only if you received the + program in object code or executable form with such an offer, in + accord with Subsection b above.) - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) + The source code for a work means the preferred form of the work for + making modifications to it. For an executable work, complete source code + means all the source code for all modules it contains, plus any + associated interface definition files, plus the scripts used to control + compilation and installation of the executable. However, as a special + exception, the source code distributed need not include anything that is + normally distributed (in either source or binary form) with the major + components (compiler, kernel, and so on) of the operating system on which + the executable runs, unless that component itself accompanies the + executable. - The source code for a work means the preferred form of the work for - making modifications to it. For an executable work, complete source - code means all the source code for all modules it contains, plus any - associated interface definition files, plus the scripts used to - control compilation and installation of the executable. However, as a - special exception, the source code distributed need not include - anything that is normally distributed (in either source or binary - form) with the major components (compiler, kernel, and so on) of the - operating system on which the executable runs, unless that component - itself accompanies the executable. + If distribution of executable or object code is made by offering access + to copy from a designated place, then offering equivalent access to copy + the source code from the same place counts as distribution of the source + code, even though third parties are not compelled to copy the source + along with the object code. - If distribution of executable or object code is made by offering - access to copy from a designated place, then offering equivalent - access to copy the source code from the same place counts as - distribution of the source code, even though third parties are not - compelled to copy the source along with the object code. +4. You may not copy, modify, sublicense, or distribute the Program except as + expressly provided under this License. Any attempt otherwise to copy, + modify, sublicense or distribute the Program is void, and will + automatically terminate your rights under this License. However, parties + who have received copies, or rights, from you under this License will not + have their licenses terminated so long as such parties remain in full + compliance. - 4. You may not copy, modify, sublicense, or distribute the Program - except as expressly provided under this License. Any attempt - otherwise to copy, modify, sublicense or distribute the Program is - void, and will automatically terminate your rights under this License. - However, parties who have received copies, or rights, from you under - this License will not have their licenses terminated so long as such - parties remain in full compliance. +5. You are not required to accept this License, since you have not signed + it. However, nothing else grants you permission to modify or distribute + the Program or its derivative works. These actions are prohibited by law + if you do not accept this License. Therefore, by modifying or + distributing the Program (or any work based on the Program), you indicate + your acceptance of this License to do so, and all its terms and + conditions for copying, distributing or modifying the Program or works + based on it. - 5. You are not required to accept this License, since you have not - signed it. However, nothing else grants you permission to modify or - distribute the Program or its derivative works. These actions are - prohibited by law if you do not accept this License. Therefore, by - modifying or distributing the Program (or any work based on the - Program), you indicate your acceptance of this License to do so, and - all its terms and conditions for copying, distributing or modifying - the Program or works based on it. +6. Each time you redistribute the Program (or any work based on the + Program), the recipient automatically receives a license from the + original licensor to copy, distribute or modify the Program subject to + these terms and conditions. You may not impose any further restrictions + on the recipients' exercise of the rights granted herein. You are not + responsible for enforcing compliance by third parties to this License. - 6. Each time you redistribute the Program (or any work based on the - Program), the recipient automatically receives a license from the - original licensor to copy, distribute or modify the Program subject to - these terms and conditions. You may not impose any further - restrictions on the recipients' exercise of the rights granted herein. - You are not responsible for enforcing compliance by third parties to - this License. +7. If, as a consequence of a court judgment or allegation of patent + infringement or for any other reason (not limited to patent issues), + conditions are imposed on you (whether by court order, agreement or + otherwise) that contradict the conditions of this License, they do not + excuse you from the conditions of this License. If you cannot distribute + so as to satisfy simultaneously your obligations under this License and + any other pertinent obligations, then as a consequence you may not + distribute the Program at all. For example, if a patent license would + not permit royalty-free redistribution of the Program by all those who + receive copies directly or indirectly through you, then the only way you + could satisfy both it and this License would be to refrain entirely from + distribution of the Program. - 7. If, as a consequence of a court judgment or allegation of patent - infringement or for any other reason (not limited to patent issues), - conditions are imposed on you (whether by court order, agreement or - otherwise) that contradict the conditions of this License, they do not - excuse you from the conditions of this License. If you cannot - distribute so as to satisfy simultaneously your obligations under this - License and any other pertinent obligations, then as a consequence you - may not distribute the Program at all. For example, if a patent - license would not permit royalty-free redistribution of the Program by - all those who receive copies directly or indirectly through you, then - the only way you could satisfy both it and this License would be to - refrain entirely from distribution of the Program. + If any portion of this section is held invalid or unenforceable under any + particular circumstance, the balance of the section is intended to apply + and the section as a whole is intended to apply in other circumstances. - If any portion of this section is held invalid or unenforceable under - any particular circumstance, the balance of the section is intended to - apply and the section as a whole is intended to apply in other - circumstances. + It is not the purpose of this section to induce you to infringe any + patents or other property right claims or to contest validity of any such + claims; this section has the sole purpose of protecting the integrity of + the free software distribution system, which is implemented by public + license practices. Many people have made generous contributions to the + wide range of software distributed through that system in reliance on + consistent application of that system; it is up to the author/donor to + decide if he or she is willing to distribute software through any other + system and a licensee cannot impose that choice. - It is not the purpose of this section to induce you to infringe any - patents or other property right claims or to contest validity of any - such claims; this section has the sole purpose of protecting the - integrity of the free software distribution system, which is - implemented by public license practices. Many people have made - generous contributions to the wide range of software distributed - through that system in reliance on consistent application of that - system; it is up to the author/donor to decide if he or she is willing - to distribute software through any other system and a licensee cannot - impose that choice. + This section is intended to make thoroughly clear what is believed to be + a consequence of the rest of this License. - This section is intended to make thoroughly clear what is believed to - be a consequence of the rest of this License. +8. If the distribution and/or use of the Program is restricted in certain + countries either by patents or by copyrighted interfaces, the original + copyright holder who places the Program under this License may add an + explicit geographical distribution limitation excluding those countries, + so that distribution is permitted only in or among countries not thus + excluded. In such case, this License incorporates the limitation as if + written in the body of this License. - 8. If the distribution and/or use of the Program is restricted in - certain countries either by patents or by copyrighted interfaces, the - original copyright holder who places the Program under this License - may add an explicit geographical distribution limitation excluding - those countries, so that distribution is permitted only in or among - countries not thus excluded. In such case, this License incorporates - the limitation as if written in the body of this License. +9. The Free Software Foundation may publish revised and/or new versions of + the General Public License from time to time. Such new versions will be + similar in spirit to the present version, but may differ in detail to + address new problems or concerns. - 9. The Free Software Foundation may publish revised and/or new versions - of the General Public License from time to time. Such new versions will - be similar in spirit to the present version, but may differ in detail to - address new problems or concerns. + Each version is given a distinguishing version number. If the Program + specifies a version number of this License which applies to it and "any + later version", you have the option of following the terms and conditions + either of that version or of any later version published by the Free + Software Foundation. If the Program does not specify a version number of + this License, you may choose any version ever published by the Free + Software Foundation. - Each version is given a distinguishing version number. If the Program - specifies a version number of this License which applies to it and "any - later version", you have the option of following the terms and conditions - either of that version or of any later version published by the Free - Software Foundation. If the Program does not specify a version number of - this License, you may choose any version ever published by the Free Software - Foundation. +10. If you wish to incorporate parts of the Program into other free programs + whose distribution conditions are different, write to the author to ask + for permission. For software which is copyrighted by the Free Software + Foundation, write to the Free Software Foundation; we sometimes make + exceptions for this. Our decision will be guided by the two goals of + preserving the free status of all derivatives of our free software and + of promoting the sharing and reuse of software generally. - 10. If you wish to incorporate parts of the Program into other free - programs whose distribution conditions are different, write to the author - to ask for permission. For software which is copyrighted by the Free - Software Foundation, write to the Free Software Foundation; we sometimes - make exceptions for this. Our decision will be guided by the two goals - of preserving the free status of all derivatives of our free software and - of promoting the sharing and reuse of software generally. + NO WARRANTY - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY - FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN - OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES - PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED - OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS - TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE - PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, - REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING - WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR - REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, - INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING - OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED - TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY - YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER - PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE - POSSIBILITY OF SUCH DAMAGES. +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR + THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN + OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES + PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER + EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE + ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH + YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL + NECESSARY SERVICING, REPAIR OR CORRECTION. +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING + WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR + REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR + DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL + DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM + (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED + INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF + THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR + OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..9ad6049 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,15 @@ += Net::LDAP Changelong + +== Net::LDAP 0.0.1: April 30, 2006 + +#-- +# Net::LDAP for Ruby. +# http://rubyforge.org/projects/net-ldap/ +# Copyright 2006 Francis Cianfrocca +# +# Available under the same terms as Ruby. See LICENCE in the main +# distribution for full licensing information. +# +# $Id: ChangeLog,v 1.17.2.4 2005/09/09 12:36:42 austin Exp $ +#++ +# vim: sts=2 sw=2 ts=4 et ai tw=77 diff --git a/Install b/Install new file mode 100644 index 0000000..a8263d9 --- /dev/null +++ b/Install @@ -0,0 +1,21 @@ +Net::LDAP is a pure Ruby LDAP client. It does not as yet require any external +libraries. It can be installed with: + + % ruby setup.rb + +Alternatively, you can use the RubyGems version of Net::LDAP availalble as +ruby-net-ldap-0.0.1.gem from the usual sources. + +Net::LDAP:: http://rubyforge.org/projects/net-ldap/ + +#-- +# Net::LDAP for Ruby. +# http://rubyforge.org/projects/net-ldap/ +# Copyright 2006 Francis Cianfrocca +# +# Available under the same terms as Ruby. See LICENCE in the main +# distribution for full licensing information. +# +# $Id: ChangeLog,v 1.17.2.4 2005/09/09 12:36:42 austin Exp $ +#++ +# vim: sts=2 sw=2 ts=4 et ai tw=77 diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..a879f66 --- /dev/null +++ b/LICENCE @@ -0,0 +1,55 @@ +Net::LDAP is copyrighted free software by Francis Gianfrocca +. You can redistribute it and/or modify it under either +the terms of the GPL (see the file COPYING), or the conditions below: + +1. You may make and give away verbatim copies of the source form of the + software without restriction, provided that you duplicate all of the + original copyright notices and associated disclaimers. + +2. You may modify your copy of the software in any way, provided that you do + at least ONE of the following: + + a) place your modifications in the Public Domain or otherwise make them + Freely Available, such as by posting said modifications to Usenet or + an equivalent medium, or by allowing the author to include your + modifications in the software. + + b) use the modified software only within your corporation or + organization. + + c) rename any non-standard executables so the names do not conflict with + standard executables, which must also be provided. + + d) make other distribution arrangements with the author. + +3. You may distribute the software in object code or executable form, + provided that you do at least ONE of the following: + + a) distribute the executables and library files of the software, together + with instructions (in the manual page or equivalent) on where to get + the original distribution. + + b) accompany the distribution with the machine-readable source of the + software. + + c) give non-standard executables non-standard names, with instructions on + where to get the original software distribution. + + d) make other distribution arrangements with the author. + +4. You may modify and include the part of the software into any other + software (possibly commercial). But some files in the distribution are + not written by the author, so that they are not under this terms. + + They are gc.c(partly), utils.c(partly), regex.[ch], st.[ch] and some + files under the ./missing directory. See each file for the copying + condition. + +5. The scripts and library files supplied as input to or produced as output + from the software do not automatically fall under the copyright of the + software, but belong to whomever generated them, and may be sold + commercially, and may be aggregated with this software. + +6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED + WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. diff --git a/README b/README index 76b3f4b..6b84df1 100644 --- a/README +++ b/README @@ -1,11 +1,26 @@ += Net::LDAP for Ruby +Net::LDAP is an LDAP support library written in pure Ruby. It supports all +LDAP client features, and a subset of server features as well. + +Homepage:: http://rubyforge.org/projects/net-ldap/ +Copyright:: 2006, Francis Cianfrocca + +== LICENCE NOTES +Please read the file LICENCE for licensing restrictions on this library. In +it simplest terms, this library is available under the same terms as Ruby +itself. + +== Requirements +PDF::Writer requires Ruby 1.8.2 or better. + +#-- +# Net::LDAP for Ruby. +# http://rubyforge.org/projects/net-ldap/ +# Copyright 2006 Francis Cianfrocca +# +# Available under the same terms as Ruby. See LICENCE in the main +# distribution for full licensing information. +# # $Id$ -# -# - -Net::LDAP is an LDAP support library written in pure Ruby. -It supports all LDAP client features, and a subset of server features as well. - -NOTE: Net::LDAP is currently released under GPL but we expect that it will -be changed to the Ruby license before being released in a "stable" version. - - +#++ +# vim: sts=2 sw=2 ts=4 et ai tw=77 diff --git a/RELEASE_NOTES b/RELEASE_NOTES deleted file mode 100644 index bc54d10..0000000 --- a/RELEASE_NOTES +++ /dev/null @@ -1,6 +0,0 @@ -# $Id$ -# -# - -Release notes for Net::LDAP. - diff --git a/Rakefile b/Rakefile index 004f88b..c54230e 100644 --- a/Rakefile +++ b/Rakefile @@ -1,32 +1,230 @@ -# $Id$ -# Rakefile for the netldap ruby gem. +#! /usr/bin/env rake +#-- +# Net::LDAP for Ruby. +# http://rubyforge.org/projects/net-ldap/ +# Copyright 2006 Francis Cianfrocca # +# Available under the same terms as Ruby. See LICENCE in the main +# distribution for full licensing information. +# +# $Id$ +#++ - -require 'rubygems' -Gem::manage_gems +require 'meta_project' require 'rake/gempackagetask' +require 'rake/contrib/xforge' +require 'rake/clean' -em_version = "0.0.1" +$can_gmail = false +begin + require 'gmailer' + $can_gmail = true +rescue LoadError +end -spec = Gem::Specification.new {|s| - s.name = "netldap" - s.version = em_version - s.author = "Francis Cianfrocca" - s.email = "garbagecat10@gmail.com" - s.homepage = "netldap@rubyforge.org" - s.summary = "Net::LDAP library" - s.files = FileList["{bin,tests,lib}/**/*"].exclude("rdoc").to_a - s.require_paths = ["lib"] - s.test_file = "tests/testem.rb" - s.has_rdoc = true - s.extra_rdoc_files = ["README", "RELEASE_NOTES", "COPYING"] -} +$can_minitar = false +begin + require 'archive/tar/minitar' + require 'zlib' + $can_minitar = true +rescue LoadError +end -Rake::GemPackageTask.new( spec ) {|pkg| - pkg.need_tar = true -} +$LOAD_PATH.unshift "lib" +require 'net/ldap' +$version = Net::LDAP::VERSION +$name = Net::LDAP.to_s +$project = MetaProject::Project::XForge::RubyForge.new('net-ldap') +$distdir = "ruby-net-ldap-#$version" +$tardist = "../#$distdir.tar.gz" -task :default => ["pkg/netldap-#{em_version}.gem"] +$release_date = nil +$release_date = Time.parse(ENV['RELEASE_DATE']) if ENV['RELEASE_DATE'] + +desc "Run the tests for #$name." +task :test do |t| + require 'test/unit/testsuite' + require 'test/unit/ui/console/testrunner' + + runner = Test::Unit::UI::Console::TestRunner + + $LOAD_PATH.unshift('tests') + $stderr.puts "Checking for test cases:" if t.verbose + Dir['tests/tc_*.rb'].each do |testcase| + $stderr.puts "\t#{testcase}" if t.verbose + load testcase + end + + suite = Test::Unit::TestSuite.new($name) + + ObjectSpace.each_object(Class) do |testcase| + suite << testcase.suite if testcase < Test::Unit::TestCase + end + + runner.run(suite) +end + +spec = eval(File.read("net-ldap.gemspec")) +spec.version = $version +desc "Build the RubyGem for #$name." +task :gem => [ :test ] +Rake::GemPackageTask.new(spec) do |g| + if $can_minitar + g.need_tar = false + g.need_zip = false + end + g.package_dir = ".." +end + +if $can_minitar + desc "Build a #$name .tar.gz distribution." + task :tar => [ $tardist ] + file $tardist => [ :test ] do |t| + current = File.basename(Dir.pwd) + Dir.chdir("..") do + begin + files = %W(bin/**/* lib/**/* tests/**/* ChangeLog README LICENCE + COPYING Rakefile net-ldap.gemspec setup.rb pre-setup.rb) + files = FileList[files.map { |file| File.join(current, file) }].to_a + files.map! do |dd| + ddnew = dd.gsub(/^#{current}/, $distdir) + mtime = $release_date || File.stat(dd).mtime + if File.directory?(dd) + { :name => ddnew, :mode => 0755, :dir => true, :mtime => mtime } + else + if dd =~ %r{bin/} + mode = 0755 + else + mode = 0644 + end + data = File.open(dd, "rb") { |ff| ff.read } + { :name => ddnew, :mode => mode, :data => data, :size => + data.size, :mtime => mtime } + end + end + + ff = File.open(t.name.gsub(%r{^\.\./}o, ''), "wb") + gz = Zlib::GzipWriter.new(ff) + tw = Archive::Tar::Minitar::Writer.new(gz) + + files.each do |entry| + if entry[:dir] + tw.mkdir(entry[:name], entry) + else + tw.add_file_simple(entry[:name], entry) { |os| os.write(entry[:data]) } + end + end + ensure + tw.close if tw + gz.finish if gz + ff.close + end + end + end + task $tardist => [ :test ] +end + +desc "Build the RDoc documentation for #$name." +task :docs do + require 'rdoc/rdoc' + rdoc_options = %W(--title #$name --main README --line-numbers) + files = FileList[*%w(README LICENCE ChangeLog COPYING LICENCE bin/**/*.rb lib/**/*.rb)] + rdoc_options += files.to_a + RDoc::RDoc.new.document(rdoc_options) +end + +task :verify_rubyforge do + raise "RUBYFORGE_USER environment variable not set!" unless ENV['RUBYFORGE_USER'] + raise "RUBYFORGE_PASSWORD environment variable not set!" unless ENV['RUBYFORGE_PASSWORD'] +end + +if $can_gmail + task :verify_gmail do + raise "GMAIL_USER environment variable not set!" unless ENV['GMAIL_USER'] + raise "GMAIL_PASSWORD environment variable not set!" unless ENV['GMAIL_PASSWORD'] + end + + desc "Post a release announcement via GMail." + task :email_announcement => [ :verify_gmail ] do + GMailer.connect(ENV["GMAIL_USER"], ENV["GMAIL_PASSWORD"]) do |gmail| + msg = { + :to => "ruby-talk@ruby-lang.org, #{ENV['GMAIL_USER']}@gmail.com", + :subject => "[ANN] #$name #$version", + :body => File.read("Release-Announcement"), + } + gmail.send msg + end + end +end + +desc "Release files on RubyForge." +task :release_files => [ :verify_rubyforge, :gem ] do + release_files = FileList[$tardist, "../#$distdir.gem"] + Rake::XForge::Release.new($project) do |release| + release.user_name = ENV['RUBYFORGE_USER'] + release.password = ENV['RUBYFORGE_PASSWORD'] + release.files = release_files.to_a + release.release_name = "#$name #$version" + release.package_name = "mime-types" + + notes = [] + File.open("README") do |file| + file.each do |line| + line.chomp! + line.gsub!(/^#.*$/, '') and next + notes << line + end + end + release.release_notes = notes.join("\n") + + changes = [] + File.open("ChangeLog") do |file| + current = true + + file.each do |line| + line.chomp! + current = false if current and line =~ /^==/ + break if line.empty? and not current + changes << line + end + end + release.release_changes = changes.join("\n") + end +end + +desc "Publish news on RubyForge" +task :publish_news => [ :verify_rubyforge, :gem ] do + Rake::XForge::NewsPublisher.new($project) do |news| + news.user_name = ENV['RUBYFORGE_USER'] + news.password = ENV['RUBYFORGE_PASSWORD'] + news.subject = "#$name #$version Released" + news.changes_file = nil + + details = [] + File.open("Release-Announcement") do |file| + file.each do |line| + line.chomp! + break if line =~ /^=/ + details << line + end + end + news.details = details.join("\n") + end +end + +desc "Release the latest version." +task :release => [ :verify_rubyforge, :release_files, :publish_news, :docs ] +if $can_gmail + task :release => [ :verify_gmail, :email_announcment ] +end + +desc "Build everything." +task :default => [ :gem ] + +if $can_minitar + task :release_files => :tar + task :publish_news => :tar + task :default => :tar +end diff --git a/Release-Announcement b/Release-Announcement new file mode 100644 index 0000000..5c4f80a --- /dev/null +++ b/Release-Announcement @@ -0,0 +1,33 @@ +It is with great excitement that I announce the first release of the pure +Ruby LDAP library, Net::LDAP. + += What is Net::LDAP for Ruby? +[...] + +Homepage:: http://ruby-pdf.rubyforge.org/net-ldap/ +Download:: http://rubyforge.org/frs/?**** +Copyright:: 2006 Francis Cianfrocca + +This software is based on RFC***, describing the Lightweight Directory +Access Protocol. + +== LICENCE NOTES +[...] + +== Requirements and Installation +Net::LDAP requires Ruby 1.8.2 or better. + +Net::LDAP can be installed with: + + % ruby setup.rb + +Alternatively, you can use the RubyGems version of Net::LDAP available +as ruby-net-ldap-0.0.1.gem from the usual sources. + +== Whet your appetite: +[...] + +== Net::LDAP 0.0.1: April 30, 2006 +[...] + +[signature here] diff --git a/net-ldap.gemspec b/net-ldap.gemspec new file mode 100644 index 0000000..16c8a5a --- /dev/null +++ b/net-ldap.gemspec @@ -0,0 +1,13 @@ +spec = Gem::Specification.new {|s| + s.name = "netldap" + s.version = em_version + s.author = "Francis Cianfrocca" + s.email = "garbagecat10@gmail.com" + s.homepage = "netldap@rubyforge.org" + s.summary = "Net::LDAP library" + s.files = FileList["{bin,tests,lib}/**/*"].exclude("rdoc").to_a + s.require_paths = ["lib"] + s.test_file = "tests/testem.rb" + s.has_rdoc = true + s.extra_rdoc_files = ["README", "RELEASE_NOTES", "COPYING"] +} diff --git a/pre-setup.rb b/pre-setup.rb new file mode 100644 index 0000000..88f6938 --- /dev/null +++ b/pre-setup.rb @@ -0,0 +1,46 @@ +require 'rdoc/rdoc' +## +# Build the rdoc documentation. Also, try to build the RI documentation. +# +def build_rdoc(options) + RDoc::RDoc.new.document(options) +rescue RDoc::RDocError => e + $stderr.puts e.message +rescue Exception => e + $stderr.puts "Couldn't build RDoc documentation\n#{e.message}" +end + +def build_ri(files) + RDoc::RDoc.new(["--ri-site", "--merge"] + files) +rescue RDoc::RDocError => e + $stderr.puts e.message +rescue Exception => e + $stderr.puts "Couldn't build Ri documentation\n#{e.message}" +end + +def run_tests(test_list) + return if test_list.empty? + + require 'test/unit/ui/console/testrunner' + $:.unshift "lib" + test_list.each do |test| + next if File.directory?(test) + require test + end + + tests = [] + ObjectSpace.each_object { |o| tests << o if o.kind_of?(Class) } + tests.delete_if { |o| !o.ancestors.include?(Test::Unit::TestCase) } + tests.delete_if { |o| o == Test::Unit::TestCase } + + tests.each { |test| Test::Unit::UI::Console::TestRunner.run(test) } + $:.shift +end + +rdoc = %w(--main README --line-numbers + --title MIME::Types) +ri = %w(--ri-site --merge) +dox = %w(README ChangeLog lib) +build_rdoc rdoc + dox +build_ri ri + dox +run_tests Dir["tests/**/*"] diff --git a/setup.rb b/setup.rb new file mode 100644 index 0000000..0673386 --- /dev/null +++ b/setup.rb @@ -0,0 +1,1366 @@ +# +# setup.rb +# +# Copyright (c) 2000-2004 Minero Aoki +# +# This program is free software. +# You can distribute/modify this program under the terms of +# the GNU LGPL, Lesser General Public License version 2.1. +# + +unless Enumerable.method_defined?(:map) # Ruby 1.4.6 + module Enumerable + alias map collect + end +end + +unless File.respond_to?(:read) # Ruby 1.6 + def File.read(fname) + open(fname) {|f| + return f.read + } + end +end + +def File.binread(fname) + open(fname, 'rb') {|f| + return f.read + } +end + +# for corrupted windows stat(2) +def File.dir?(path) + File.directory?((path[-1,1] == '/') ? path : path + '/') +end + + +class SetupError < StandardError; end + +def setup_rb_error(msg) + raise SetupError, msg +end + +# +# Config +# + +if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg } + ARGV.delete(arg) + require arg.split(/=/, 2)[1] + $".push 'rbconfig.rb' +else + require 'rbconfig' +end + +def multipackage_install? + FileTest.directory?(File.dirname($0) + '/packages') +end + + +class ConfigItem + def initialize(name, template, default, desc) + @name = name.freeze + @template = template + @value = default + @default = default.dup.freeze + @description = desc + end + + attr_reader :name + attr_reader :description + + attr_accessor :default + alias help_default default + + def help_opt + "--#{@name}=#{@template}" + end + + def value + @value + end + + def eval(table) + @value.gsub(%r<\$([^/]+)>) { table[$1] } + end + + def set(val) + @value = check(val) + end + + private + + def check(val) + setup_rb_error "config: --#{name} requires argument" unless val + val + end +end + +class BoolItem < ConfigItem + def config_type + 'bool' + end + + def help_opt + "--#{@name}" + end + + private + + def check(val) + return 'yes' unless val + unless /\A(y(es)?|n(o)?|t(rue)?|f(alse))\z/i =~ val + setup_rb_error "config: --#{@name} accepts only yes/no for argument" + end + (/\Ay(es)?|\At(rue)/i =~ value) ? 'yes' : 'no' + end +end + +class PathItem < ConfigItem + def config_type + 'path' + end + + private + + def check(path) + setup_rb_error "config: --#{@name} requires argument" unless path + path[0,1] == '$' ? path : File.expand_path(path) + end +end + +class ProgramItem < ConfigItem + def config_type + 'program' + end +end + +class SelectItem < ConfigItem + def initialize(name, template, default, desc) + super + @ok = template.split('/') + end + + def config_type + 'select' + end + + private + + def check(val) + unless @ok.include?(val.strip) + setup_rb_error "config: use --#{@name}=#{@template} (#{val})" + end + val.strip + end +end + +class PackageSelectionItem < ConfigItem + def initialize(name, template, default, help_default, desc) + super name, template, default, desc + @help_default = help_default + end + + attr_reader :help_default + + def config_type + 'package' + end + + private + + def check(val) + unless File.dir?("packages/#{val}") + setup_rb_error "config: no such package: #{val}" + end + val + end +end + +class ConfigTable_class + + def initialize(items) + @items = items + @table = {} + items.each do |i| + @table[i.name] = i + end + ALIASES.each do |ali, name| + @table[ali] = @table[name] + end + @script_extensions = ['rb'] + end + + attr_accessor :script_extensions + + include Enumerable + + def each(&block) + @items.each(&block) + end + + def key?(name) + @table.key?(name) + end + + def lookup(name) + @table[name] or raise ArgumentError, "no such config item: #{name}" + end + + def add(item) + @items.push item + @table[item.name] = item + end + + def remove(name) + item = lookup(name) + @items.delete_if {|i| i.name == name } + @table.delete_if {|name, i| i.name == name } + item + end + + def new + dup() + end + + def savefile + '.config' + end + + def load + begin + t = dup() + File.foreach(savefile()) do |line| + k, v = *line.split(/=/, 2) + t[k] = v.strip + end + t + rescue Errno::ENOENT + setup_rb_error $!.message + "#{File.basename($0)} config first" + end + end + + def save + @items.each {|i| i.value } + File.open(savefile(), 'w') {|f| + @items.each do |i| + f.printf "%s=%s\n", i.name, i.value if i.value + end + } + end + + def [](key) + lookup(key).eval(self) + end + + def []=(key, val) + lookup(key).set val + end + +end + +c = ::Config::CONFIG + +rubypath = c['bindir'] + '/' + c['ruby_install_name'] + +major = c['MAJOR'].to_i +minor = c['MINOR'].to_i +teeny = c['TEENY'].to_i +version = "#{major}.#{minor}" + +# ruby ver. >= 1.4.4? +newpath_p = ((major >= 2) or + ((major == 1) and + ((minor >= 5) or + ((minor == 4) and (teeny >= 4))))) + +if c['rubylibdir'] + # V < 1.6.3 + _stdruby = c['rubylibdir'] + _siteruby = c['sitedir'] + _siterubyver = c['sitelibdir'] + _siterubyverarch = c['sitearchdir'] +elsif newpath_p + # 1.4.4 <= V <= 1.6.3 + _stdruby = "$prefix/lib/ruby/#{version}" + _siteruby = c['sitedir'] + _siterubyver = "$siteruby/#{version}" + _siterubyverarch = "$siterubyver/#{c['arch']}" +else + # V < 1.4.4 + _stdruby = "$prefix/lib/ruby/#{version}" + _siteruby = "$prefix/lib/ruby/#{version}/site_ruby" + _siterubyver = _siteruby + _siterubyverarch = "$siterubyver/#{c['arch']}" +end +libdir = '-* dummy libdir *-' +stdruby = '-* dummy rubylibdir *-' +siteruby = '-* dummy site_ruby *-' +siterubyver = '-* dummy site_ruby version *-' +parameterize = lambda {|path| + path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix')\ + .sub(/\A#{Regexp.quote(libdir)}/, '$libdir')\ + .sub(/\A#{Regexp.quote(stdruby)}/, '$stdruby')\ + .sub(/\A#{Regexp.quote(siteruby)}/, '$siteruby')\ + .sub(/\A#{Regexp.quote(siterubyver)}/, '$siterubyver') +} +libdir = parameterize.call(c['libdir']) +stdruby = parameterize.call(_stdruby) +siteruby = parameterize.call(_siteruby) +siterubyver = parameterize.call(_siterubyver) +siterubyverarch = parameterize.call(_siterubyverarch) + +if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg } + makeprog = arg.sub(/'/, '').split(/=/, 2)[1] +else + makeprog = 'make' +end + +common_conf = [ + PathItem.new('prefix', 'path', c['prefix'], + 'path prefix of target environment'), + PathItem.new('bindir', 'path', parameterize.call(c['bindir']), + 'the directory for commands'), + PathItem.new('libdir', 'path', libdir, + 'the directory for libraries'), + PathItem.new('datadir', 'path', parameterize.call(c['datadir']), + 'the directory for shared data'), + PathItem.new('mandir', 'path', parameterize.call(c['mandir']), + 'the directory for man pages'), + PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']), + 'the directory for man pages'), + PathItem.new('stdruby', 'path', stdruby, + 'the directory for standard ruby libraries'), + PathItem.new('siteruby', 'path', siteruby, + 'the directory for version-independent aux ruby libraries'), + PathItem.new('siterubyver', 'path', siterubyver, + 'the directory for aux ruby libraries'), + PathItem.new('siterubyverarch', 'path', siterubyverarch, + 'the directory for aux ruby binaries'), + PathItem.new('rbdir', 'path', '$siterubyver', + 'the directory for ruby scripts'), + PathItem.new('sodir', 'path', '$siterubyverarch', + 'the directory for ruby extentions'), + PathItem.new('rubypath', 'path', rubypath, + 'the path to set to #! line'), + ProgramItem.new('rubyprog', 'name', rubypath, + 'the ruby program using for installation'), + ProgramItem.new('makeprog', 'name', makeprog, + 'the make program to compile ruby extentions'), + SelectItem.new('shebang', 'all/ruby/never', 'ruby', + 'shebang line (#!) editing mode'), + BoolItem.new('without-ext', 'yes/no', 'no', + 'does not compile/install ruby extentions') +] +class ConfigTable_class # open again + ALIASES = { + 'std-ruby' => 'stdruby', + 'site-ruby-common' => 'siteruby', # For backward compatibility + 'site-ruby' => 'siterubyver', # For backward compatibility + 'bin-dir' => 'bindir', + 'bin-dir' => 'bindir', + 'rb-dir' => 'rbdir', + 'so-dir' => 'sodir', + 'data-dir' => 'datadir', + 'ruby-path' => 'rubypath', + 'ruby-prog' => 'rubyprog', + 'ruby' => 'rubyprog', + 'make-prog' => 'makeprog', + 'make' => 'makeprog' + } +end +multipackage_conf = [ + PackageSelectionItem.new('with', 'name,name...', '', 'ALL', + 'package names that you want to install'), + PackageSelectionItem.new('without', 'name,name...', '', 'NONE', + 'package names that you do not want to install') +] +if multipackage_install? + ConfigTable = ConfigTable_class.new(common_conf + multipackage_conf) +else + ConfigTable = ConfigTable_class.new(common_conf) +end + + +module MetaConfigAPI + + def eval_file_ifexist(fname) + instance_eval File.read(fname), fname, 1 if File.file?(fname) + end + + def config_names + ConfigTable.map {|i| i.name } + end + + def config?(name) + ConfigTable.key?(name) + end + + def bool_config?(name) + ConfigTable.lookup(name).config_type == 'bool' + end + + def path_config?(name) + ConfigTable.lookup(name).config_type == 'path' + end + + def value_config?(name) + case ConfigTable.lookup(name).config_type + when 'bool', 'path' + true + else + false + end + end + + def add_config(item) + ConfigTable.add item + end + + def add_bool_config(name, default, desc) + ConfigTable.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc) + end + + def add_path_config(name, default, desc) + ConfigTable.add PathItem.new(name, 'path', default, desc) + end + + def set_config_default(name, default) + ConfigTable.lookup(name).default = default + end + + def remove_config(name) + ConfigTable.remove(name) + end + + def add_script_extension(ext) + ConfigTable.script_extensions << ext + end +end + + +# +# File Operations +# + +module FileOperations + + def mkdir_p(dirname, prefix = nil) + dirname = prefix + File.expand_path(dirname) if prefix + $stderr.puts "mkdir -p #{dirname}" if verbose? + return if no_harm? + + # does not check '/'... it's too abnormal case + dirs = File.expand_path(dirname).split(%r<(?=/)>) + if /\A[a-z]:\z/i =~ dirs[0] + disk = dirs.shift + dirs[0] = disk + dirs[0] + end + dirs.each_index do |idx| + path = dirs[0..idx].join('') + Dir.mkdir path unless File.dir?(path) + end + end + + def rm_f(fname) + $stderr.puts "rm -f #{fname}" if verbose? + return if no_harm? + + if File.exist?(fname) or File.symlink?(fname) + File.chmod 0777, fname + File.unlink fname + end + end + + def rm_rf(dn) + $stderr.puts "rm -rf #{dn}" if verbose? + return if no_harm? + + Dir.chdir dn + Dir.foreach('.') do |fn| + next if fn == '.' + next if fn == '..' + if File.dir?(fn) + verbose_off { + rm_rf fn + } + else + verbose_off { + rm_f fn + } + end + end + Dir.chdir '..' + Dir.rmdir dn + end + + def move_file(src, dest) + File.unlink dest if File.exist?(dest) + begin + File.rename src, dest + rescue + File.open(dest, 'wb') {|f| f.write File.binread(src) } + File.chmod File.stat(src).mode, dest + File.unlink src + end + end + + def install(from, dest, mode, prefix = nil) + $stderr.puts "install #{from} #{dest}" if verbose? + return if no_harm? + + realdest = prefix ? prefix + File.expand_path(dest) : dest + realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest) + str = File.binread(from) + if diff?(str, realdest) + verbose_off { + rm_f realdest if File.exist?(realdest) + } + File.open(realdest, 'wb') {|f| + f.write str + } + File.chmod mode, realdest + + File.open("#{objdir_root()}/InstalledFiles", 'a') {|f| + if prefix + f.puts realdest.sub(prefix, '') + else + f.puts realdest + end + } + end + end + + def diff?(new_content, path) + return true unless File.exist?(path) + new_content != File.binread(path) + end + + def command(str) + $stderr.puts str if verbose? + system str or raise RuntimeError, "'system #{str}' failed" + end + + def ruby(str) + command config('rubyprog') + ' ' + str + end + + def make(task = '') + command config('makeprog') + ' ' + task + end + + def extdir?(dir) + File.exist?(dir + '/MANIFEST') + end + + def all_files_in(dirname) + Dir.open(dirname) {|d| + return d.select {|ent| File.file?("#{dirname}/#{ent}") } + } + end + + REJECT_DIRS = %w( + CVS SCCS RCS CVS.adm .svn + ) + + def all_dirs_in(dirname) + Dir.open(dirname) {|d| + return d.select {|n| File.dir?("#{dirname}/#{n}") } - %w(. ..) - REJECT_DIRS + } + end + +end + + +# +# Main Installer +# + +module HookUtils + + def run_hook(name) + try_run_hook "#{curr_srcdir()}/#{name}" or + try_run_hook "#{curr_srcdir()}/#{name}.rb" + end + + def try_run_hook(fname) + return false unless File.file?(fname) + begin + instance_eval File.read(fname), fname, 1 + rescue + setup_rb_error "hook #{fname} failed:\n" + $!.message + end + true + end + +end + + +module HookScriptAPI + + def get_config(key) + @config[key] + end + + alias config get_config + + def set_config(key, val) + @config[key] = val + end + + # + # srcdir/objdir (works only in the package directory) + # + + #abstract srcdir_root + #abstract objdir_root + #abstract relpath + + def curr_srcdir + "#{srcdir_root()}/#{relpath()}" + end + + def curr_objdir + "#{objdir_root()}/#{relpath()}" + end + + def srcfile(path) + "#{curr_srcdir()}/#{path}" + end + + def srcexist?(path) + File.exist?(srcfile(path)) + end + + def srcdirectory?(path) + File.dir?(srcfile(path)) + end + + def srcfile?(path) + File.file? srcfile(path) + end + + def srcentries(path = '.') + Dir.open("#{curr_srcdir()}/#{path}") {|d| + return d.to_a - %w(. ..) + } + end + + def srcfiles(path = '.') + srcentries(path).select {|fname| + File.file?(File.join(curr_srcdir(), path, fname)) + } + end + + def srcdirectories(path = '.') + srcentries(path).select {|fname| + File.dir?(File.join(curr_srcdir(), path, fname)) + } + end + +end + + +class ToplevelInstaller + + Version = '3.3.1' + Copyright = 'Copyright (c) 2000-2004 Minero Aoki' + + TASKS = [ + [ 'all', 'do config, setup, then install' ], + [ 'config', 'saves your configurations' ], + [ 'show', 'shows current configuration' ], + [ 'setup', 'compiles ruby extentions and others' ], + [ 'install', 'installs files' ], + [ 'clean', "does `make clean' for each extention" ], + [ 'distclean',"does `make distclean' for each extention" ] + ] + + def ToplevelInstaller.invoke + instance().invoke + end + + @singleton = nil + + def ToplevelInstaller.instance + @singleton ||= new(File.dirname($0)) + @singleton + end + + include MetaConfigAPI + + def initialize(ardir_root) + @config = nil + @options = { 'verbose' => true } + @ardir = File.expand_path(ardir_root) + end + + def inspect + "#<#{self.class} #{__id__()}>" + end + + def invoke + run_metaconfigs + case task = parsearg_global() + when nil, 'all' + @config = load_config('config') + parsearg_config + init_installers + exec_config + exec_setup + exec_install + else + @config = load_config(task) + __send__ "parsearg_#{task}" + init_installers + __send__ "exec_#{task}" + end + end + + def run_metaconfigs + eval_file_ifexist "#{@ardir}/metaconfig" + end + + def load_config(task) + case task + when 'config' + ConfigTable.new + when 'clean', 'distclean' + if File.exist?(ConfigTable.savefile) + then ConfigTable.load + else ConfigTable.new + end + else + ConfigTable.load + end + end + + def init_installers + @installer = Installer.new(@config, @options, @ardir, File.expand_path('.')) + end + + # + # Hook Script API bases + # + + def srcdir_root + @ardir + end + + def objdir_root + '.' + end + + def relpath + '.' + end + + # + # Option Parsing + # + + def parsearg_global + valid_task = /\A(?:#{TASKS.map {|task,desc| task }.join '|'})\z/ + + while arg = ARGV.shift + case arg + when /\A\w+\z/ + setup_rb_error "invalid task: #{arg}" unless valid_task =~ arg + return arg + + when '-q', '--quiet' + @options['verbose'] = false + + when '--verbose' + @options['verbose'] = true + + when '-h', '--help' + print_usage $stdout + exit 0 + + when '-v', '--version' + puts "#{File.basename($0)} version #{Version}" + exit 0 + + when '--copyright' + puts Copyright + exit 0 + + else + setup_rb_error "unknown global option '#{arg}'" + end + end + + nil + end + + + def parsearg_no_options + unless ARGV.empty? + setup_rb_error "#{task}: unknown options: #{ARGV.join ' '}" + end + end + + alias parsearg_show parsearg_no_options + alias parsearg_setup parsearg_no_options + alias parsearg_clean parsearg_no_options + alias parsearg_distclean parsearg_no_options + + def parsearg_config + re = /\A--(#{ConfigTable.map {|i| i.name }.join('|')})(?:=(.*))?\z/ + @options['config-opt'] = [] + + while i = ARGV.shift + if /\A--?\z/ =~ i + @options['config-opt'] = ARGV.dup + break + end + m = re.match(i) or setup_rb_error "config: unknown option #{i}" + name, value = *m.to_a[1,2] + @config[name] = value + end + end + + def parsearg_install + @options['no-harm'] = false + @options['install-prefix'] = '' + while a = ARGV.shift + case a + when /\A--no-harm\z/ + @options['no-harm'] = true + when /\A--prefix=(.*)\z/ + path = $1 + path = File.expand_path(path) unless path[0,1] == '/' + @options['install-prefix'] = path + else + setup_rb_error "install: unknown option #{a}" + end + end + end + + def print_usage(out) + out.puts 'Typical Installation Procedure:' + out.puts " $ ruby #{File.basename $0} config" + out.puts " $ ruby #{File.basename $0} setup" + out.puts " # ruby #{File.basename $0} install (may require root privilege)" + out.puts + out.puts 'Detailed Usage:' + out.puts " ruby #{File.basename $0} " + out.puts " ruby #{File.basename $0} [] []" + + fmt = " %-24s %s\n" + out.puts + out.puts 'Global options:' + out.printf fmt, '-q,--quiet', 'suppress message outputs' + out.printf fmt, ' --verbose', 'output messages verbosely' + out.printf fmt, '-h,--help', 'print this message' + out.printf fmt, '-v,--version', 'print version and quit' + out.printf fmt, ' --copyright', 'print copyright and quit' + out.puts + out.puts 'Tasks:' + TASKS.each do |name, desc| + out.printf fmt, name, desc + end + + fmt = " %-24s %s [%s]\n" + out.puts + out.puts 'Options for CONFIG or ALL:' + ConfigTable.each do |item| + out.printf fmt, item.help_opt, item.description, item.help_default + end + out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's" + out.puts + out.puts 'Options for INSTALL:' + out.printf fmt, '--no-harm', 'only display what to do if given', 'off' + out.printf fmt, '--prefix=path', 'install path prefix', '$prefix' + out.puts + end + + # + # Task Handlers + # + + def exec_config + @installer.exec_config + @config.save # must be final + end + + def exec_setup + @installer.exec_setup + end + + def exec_install + @installer.exec_install + end + + def exec_show + ConfigTable.each do |i| + printf "%-20s %s\n", i.name, i.value + end + end + + def exec_clean + @installer.exec_clean + end + + def exec_distclean + @installer.exec_distclean + end + +end + + +class ToplevelInstallerMulti < ToplevelInstaller + + include HookUtils + include HookScriptAPI + include FileOperations + + def initialize(ardir) + super + @packages = all_dirs_in("#{@ardir}/packages") + raise 'no package exists' if @packages.empty? + end + + def run_metaconfigs + eval_file_ifexist "#{@ardir}/metaconfig" + @packages.each do |name| + eval_file_ifexist "#{@ardir}/packages/#{name}/metaconfig" + end + end + + def init_installers + @installers = {} + @packages.each do |pack| + @installers[pack] = Installer.new(@config, @options, + "#{@ardir}/packages/#{pack}", + "packages/#{pack}") + end + + with = extract_selection(config('with')) + without = extract_selection(config('without')) + @selected = @installers.keys.select {|name| + (with.empty? or with.include?(name)) \ + and not without.include?(name) + } + end + + def extract_selection(list) + a = list.split(/,/) + a.each do |name| + setup_rb_error "no such package: #{name}" unless @installers.key?(name) + end + a + end + + def print_usage(f) + super + f.puts 'Inluded packages:' + f.puts ' ' + @packages.sort.join(' ') + f.puts + end + + # + # multi-package metaconfig API + # + + attr_reader :packages + + def declare_packages(list) + raise 'package list is empty' if list.empty? + list.each do |name| + raise "directory packages/#{name} does not exist"\ + unless File.dir?("#{@ardir}/packages/#{name}") + end + @packages = list + end + + # + # Task Handlers + # + + def exec_config + run_hook 'pre-config' + each_selected_installers {|inst| inst.exec_config } + run_hook 'post-config' + @config.save # must be final + end + + def exec_setup + run_hook 'pre-setup' + each_selected_installers {|inst| inst.exec_setup } + run_hook 'post-setup' + end + + def exec_install + run_hook 'pre-install' + each_selected_installers {|inst| inst.exec_install } + run_hook 'post-install' + end + + def exec_clean + rm_f ConfigTable.savefile + run_hook 'pre-clean' + each_selected_installers {|inst| inst.exec_clean } + run_hook 'post-clean' + end + + def exec_distclean + rm_f ConfigTable.savefile + run_hook 'pre-distclean' + each_selected_installers {|inst| inst.exec_distclean } + run_hook 'post-distclean' + end + + # + # lib + # + + def each_selected_installers + Dir.mkdir 'packages' unless File.dir?('packages') + @selected.each do |pack| + $stderr.puts "Processing the package `#{pack}' ..." if @options['verbose'] + Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}") + Dir.chdir "packages/#{pack}" + yield @installers[pack] + Dir.chdir '../..' + end + end + + def verbose? + @options['verbose'] + end + + def no_harm? + @options['no-harm'] + end + +end + + +class Installer + + FILETYPES = %w( bin lib ext data ) + + include HookScriptAPI + include HookUtils + include FileOperations + + def initialize(config, opt, srcroot, objroot) + @config = config + @options = opt + @srcdir = File.expand_path(srcroot) + @objdir = File.expand_path(objroot) + @currdir = '.' + end + + def inspect + "#<#{self.class} #{File.basename(@srcdir)}>" + end + + # + # Hook Script API base methods + # + + def srcdir_root + @srcdir + end + + def objdir_root + @objdir + end + + def relpath + @currdir + end + + # + # configs/options + # + + def no_harm? + @options['no-harm'] + end + + def verbose? + @options['verbose'] + end + + def verbose_off + begin + save, @options['verbose'] = @options['verbose'], false + yield + ensure + @options['verbose'] = save + end + end + + # + # TASK config + # + + def exec_config + exec_task_traverse 'config' + end + + def config_dir_bin(rel) + end + + def config_dir_lib(rel) + end + + def config_dir_ext(rel) + extconf if extdir?(curr_srcdir()) + end + + def extconf + opt = @options['config-opt'].join(' ') + command "#{config('rubyprog')} #{curr_srcdir()}/extconf.rb #{opt}" + end + + def config_dir_data(rel) + end + + # + # TASK setup + # + + def exec_setup + exec_task_traverse 'setup' + end + + def setup_dir_bin(rel) + all_files_in(curr_srcdir()).each do |fname| + adjust_shebang "#{curr_srcdir()}/#{fname}" + end + end + + def adjust_shebang(path) + return if no_harm? + tmpfile = File.basename(path) + '.tmp' + begin + File.open(path, 'rb') {|r| + first = r.gets + return unless File.basename(config('rubypath')) == 'ruby' + return unless File.basename(first.sub(/\A\#!/, '').split[0]) == 'ruby' + $stderr.puts "adjusting shebang: #{File.basename(path)}" if verbose? + File.open(tmpfile, 'wb') {|w| + w.print first.sub(/\A\#!\s*\S+/, '#! ' + config('rubypath')) + w.write r.read + } + move_file tmpfile, File.basename(path) + } + ensure + File.unlink tmpfile if File.exist?(tmpfile) + end + end + + def setup_dir_lib(rel) + end + + def setup_dir_ext(rel) + make if extdir?(curr_srcdir()) + end + + def setup_dir_data(rel) + end + + # + # TASK install + # + + def exec_install + rm_f 'InstalledFiles' + exec_task_traverse 'install' + end + + def install_dir_bin(rel) + install_files collect_filenames_auto(), "#{config('bindir')}/#{rel}", 0755 + end + + def install_dir_lib(rel) + install_files ruby_scripts(), "#{config('rbdir')}/#{rel}", 0644 + end + + def install_dir_ext(rel) + return unless extdir?(curr_srcdir()) + install_files ruby_extentions('.'), + "#{config('sodir')}/#{File.dirname(rel)}", + 0555 + end + + def install_dir_data(rel) + install_files collect_filenames_auto(), "#{config('datadir')}/#{rel}", 0644 + end + + def install_files(list, dest, mode) + mkdir_p dest, @options['install-prefix'] + list.each do |fname| + install fname, dest, mode, @options['install-prefix'] + end + end + + def ruby_scripts + collect_filenames_auto().select {|n| /\.(#{ConfigTable.script_extensions.join('|')})\z/ =~ n } + end + + # picked up many entries from cvs-1.11.1/src/ignore.c + reject_patterns = %w( + core RCSLOG tags TAGS .make.state + .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb + *~ *.old *.bak *.BAK *.orig *.rej _$* *$ + + *.org *.in .* + ) + mapping = { + '.' => '\.', + '$' => '\$', + '#' => '\#', + '*' => '.*' + } + REJECT_PATTERNS = Regexp.new('\A(?:' + + reject_patterns.map {|pat| + pat.gsub(/[\.\$\#\*]/) {|ch| mapping[ch] } + }.join('|') + + ')\z') + + def collect_filenames_auto + mapdir((existfiles() - hookfiles()).reject {|fname| + REJECT_PATTERNS =~ fname + }) + end + + def existfiles + all_files_in(curr_srcdir()) | all_files_in('.') + end + + def hookfiles + %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt| + %w( config setup install clean ).map {|t| sprintf(fmt, t) } + }.flatten + end + + def mapdir(filelist) + filelist.map {|fname| + if File.exist?(fname) # objdir + fname + else # srcdir + File.join(curr_srcdir(), fname) + end + } + end + + def ruby_extentions(dir) + Dir.open(dir) {|d| + ents = d.select {|fname| /\.#{::Config::CONFIG['DLEXT']}\z/ =~ fname } + if ents.empty? + setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first" + end + return ents + } + end + + # + # TASK clean + # + + def exec_clean + exec_task_traverse 'clean' + rm_f ConfigTable.savefile + rm_f 'InstalledFiles' + end + + def clean_dir_bin(rel) + end + + def clean_dir_lib(rel) + end + + def clean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'clean' if File.file?('Makefile') + end + + def clean_dir_data(rel) + end + + # + # TASK distclean + # + + def exec_distclean + exec_task_traverse 'distclean' + rm_f ConfigTable.savefile + rm_f 'InstalledFiles' + end + + def distclean_dir_bin(rel) + end + + def distclean_dir_lib(rel) + end + + def distclean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'distclean' if File.file?('Makefile') + end + + # + # lib + # + + def exec_task_traverse(task) + run_hook "pre-#{task}" + FILETYPES.each do |type| + if config('without-ext') == 'yes' and type == 'ext' + $stderr.puts 'skipping ext/* by user option' if verbose? + next + end + traverse task, type, "#{task}_dir_#{type}" + end + run_hook "post-#{task}" + end + + def traverse(task, rel, mid) + dive_into(rel) { + run_hook "pre-#{task}" + __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '') + all_dirs_in(curr_srcdir()).each do |d| + traverse task, "#{rel}/#{d}", mid + end + run_hook "post-#{task}" + } + end + + def dive_into(rel) + return unless File.dir?("#{@srcdir}/#{rel}") + + dir = File.basename(rel) + Dir.mkdir dir unless File.dir?(dir) + prevdir = Dir.pwd + Dir.chdir dir + $stderr.puts '---> ' + rel if verbose? + @currdir = rel + yield + Dir.chdir prevdir + $stderr.puts '<--- ' + rel if verbose? + @currdir = File.dirname(rel) + end + +end + + +if $0 == __FILE__ + begin + if multipackage_install? + ToplevelInstallerMulti.invoke + else + ToplevelInstaller.invoke + end + rescue SetupError + raise if $DEBUG + $stderr.puts $!.message + $stderr.puts "Try 'ruby #{$0} --help' for detailed usage." + exit 1 + end +end From 12f7d6efec409cd0f99ed490a487c2cc1aee0e0c Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 30 Apr 2006 11:32:52 +0000 Subject: [PATCH 054/231] Added version number. Removed RELEASE_NOTES in favor of ChangeLog. Removed COPYING in favor of LICENCE. --- net-ldap.gemspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/net-ldap.gemspec b/net-ldap.gemspec index 16c8a5a..502ec1e 100644 --- a/net-ldap.gemspec +++ b/net-ldap.gemspec @@ -1,6 +1,6 @@ spec = Gem::Specification.new {|s| s.name = "netldap" - s.version = em_version + s.version = "0.0.1" s.author = "Francis Cianfrocca" s.email = "garbagecat10@gmail.com" s.homepage = "netldap@rubyforge.org" @@ -9,5 +9,5 @@ spec = Gem::Specification.new {|s| s.require_paths = ["lib"] s.test_file = "tests/testem.rb" s.has_rdoc = true - s.extra_rdoc_files = ["README", "RELEASE_NOTES", "COPYING"] + s.extra_rdoc_files = ["README", "ChangeLog", "LICENCE"] } From 3a8151061104f4895077499285f8ea89c6f5bb79 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 30 Apr 2006 11:36:18 +0000 Subject: [PATCH 055/231] verbiage tweak and acknowledged Austin. --- README | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README b/README index 6b84df1..2e4798f 100644 --- a/README +++ b/README @@ -3,7 +3,10 @@ Net::LDAP is an LDAP support library written in pure Ruby. It supports all LDAP client features, and a subset of server features as well. Homepage:: http://rubyforge.org/projects/net-ldap/ -Copyright:: 2006, Francis Cianfrocca +Copyright:: 2006 by Francis Cianfrocca + +Original developer: Francis Cianfrocca +Contributions by Austin Ziegler gratefully acknowledged. == LICENCE NOTES Please read the file LICENCE for licensing restrictions on this library. In @@ -11,12 +14,12 @@ it simplest terms, this library is available under the same terms as Ruby itself. == Requirements -PDF::Writer requires Ruby 1.8.2 or better. +Net::LDAP requires Ruby 1.8.2 or better. #-- # Net::LDAP for Ruby. # http://rubyforge.org/projects/net-ldap/ -# Copyright 2006 Francis Cianfrocca +# Copyright (C) 2006 by Francis Cianfrocca # # Available under the same terms as Ruby. See LICENCE in the main # distribution for full licensing information. From 62e24ae8937fa09d4243672954699ae3b4fa5f81 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 30 Apr 2006 11:37:59 +0000 Subject: [PATCH 056/231] minor verbiage tweaks. Removed COPYING which only makes sense for GPL. --- COPYING | 272 ------------------------------------------------------ ChangeLog | 9 +- Install | 2 +- Rakefile | 2 +- 4 files changed, 7 insertions(+), 278 deletions(-) delete mode 100644 COPYING diff --git a/COPYING b/COPYING deleted file mode 100644 index 2ff629a..0000000 --- a/COPYING +++ /dev/null @@ -1,272 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - -Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street, -Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and -distribute verbatim copies of this license document, but changing it is not -allowed. - - Preamble - -The licenses for most software are designed to take away your freedom to -share and change it. By contrast, the GNU General Public License is -intended to guarantee your freedom to share and change free software--to -make sure the software is free for all its users. This General Public -License applies to most of the Free Software Foundation's software and to -any other program whose authors commit to using it. (Some other Free -Software Foundation software is covered by the GNU Lesser General Public -License instead.) You can apply it to your programs, too. - -When we speak of free software, we are referring to freedom, not price. Our -General Public Licenses are designed to make sure that you have the freedom -to distribute copies of free software (and charge for this service if you -wish), that you receive source code or can get it if you want it, that you -can change the software or use pieces of it in new free programs; and that -you know you can do these things. - -To protect your rights, we need to make restrictions that forbid anyone to -deny you these rights or to ask you to surrender the rights. These -restrictions translate to certain responsibilities for you if you distribute -copies of the software, or if you modify it. - -For example, if you distribute copies of such a program, whether gratis or -for a fee, you must give the recipients all the rights that you have. You -must make sure that they, too, receive or can get the source code. And you -must show them these terms so they know their rights. - -We protect your rights with two steps: (1) copyright the software, and (2) -offer you this license which gives you legal permission to copy, distribute -and/or modify the software. - -Also, for each author's protection and ours, we want to make certain that -everyone understands that there is no warranty for this free software. If -the software is modified by someone else and passed on, we want its -recipients to know that what they have is not the original, so that any -problems introduced by others will not reflect on the original authors' -reputations. - -Finally, any free program is threatened constantly by software patents. We -wish to avoid the danger that redistributors of a free program will -individually obtain patent licenses, in effect making the program -proprietary. To prevent this, we have made it clear that any patent must be -licensed for everyone's free use or not licensed at all. - -The precise terms and conditions for copying, distribution and modification -follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - -0. This License applies to any program or other work which contains a notice - placed by the copyright holder saying it may be distributed under the - terms of this General Public License. The "Program", below, refers to - any such program or work, and a "work based on the Program" means either - the Program or any derivative work under copyright law: that is to say, a - work containing the Program or a portion of it, either verbatim or with - modifications and/or translated into another language. (Hereinafter, - translation is included without limitation in the term "modification".) - Each licensee is addressed as "you". - - Activities other than copying, distribution and modification are not - covered by this License; they are outside its scope. The act of running - the Program is not restricted, and the output from the Program is covered - only if its contents constitute a work based on the Program (independent - of having been made by running the Program). Whether that is true depends - on what the Program does. - -1. You may copy and distribute verbatim copies of the Program's source code - as you receive it, in any medium, provided that you conspicuously and - appropriately publish on each copy an appropriate copyright notice and - disclaimer of warranty; keep intact all the notices that refer to this - License and to the absence of any warranty; and give any other recipients - of the Program a copy of this License along with the Program. - - You may charge a fee for the physical act of transferring a copy, and you - may at your option offer warranty protection in exchange for a fee. - -2. You may modify your copy or copies of the Program or any portion of it, - thus forming a work based on the Program, and copy and distribute such - modifications or work under the terms of Section 1 above, provided that - you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices stating - that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in whole - or in part contains or is derived from the Program or any part - thereof, to be licensed as a whole at no charge to all third parties - under the terms of this License. - - c) If the modified program normally reads commands interactively when - run, you must cause it, when started running for such interactive use - in the most ordinary way, to print or display an announcement - including an appropriate copyright notice and a notice that there is - no warranty (or else, saying that you provide a warranty) and that - users may redistribute the program under these conditions, and telling - the user how to view a copy of this License. (Exception: if the - Program itself is interactive but does not normally print such an - announcement, your work based on the Program is not required to print - an announcement.) - - These requirements apply to the modified work as a whole. If - identifiable sections of that work are not derived from the Program, and - can be reasonably considered independent and separate works in - themselves, then this License, and its terms, do not apply to those - sections when you distribute them as separate works. But when you - distribute the same sections as part of a whole which is a work based on - the Program, the distribution of the whole must be on the terms of this - License, whose permissions for other licensees extend to the entire - whole, and thus to each and every part regardless of who wrote it. - - Thus, it is not the intent of this section to claim rights or contest - your rights to work written entirely by you; rather, the intent is to - exercise the right to control the distribution of derivative or - collective works based on the Program. - - In addition, mere aggregation of another work not based on the Program - with the Program (or with a work based on the Program) on a volume of a - storage or distribution medium does not bring the other work under the - scope of this License. - -3. You may copy and distribute the Program (or a work based on it, under - Section 2) in object code or executable form under the terms of Sections - 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable source - code, which must be distributed under the terms of Sections 1 and 2 - above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three years, to - give any third party, for a charge no more than your cost of - physically performing source distribution, a complete machine-readable - copy of the corresponding source code, to be distributed under the - terms of Sections 1 and 2 above on a medium customarily used for - software interchange; or, - - c) Accompany it with the information you received as to the offer to - distribute corresponding source code. (This alternative is allowed - only for noncommercial distribution and only if you received the - program in object code or executable form with such an offer, in - accord with Subsection b above.) - - The source code for a work means the preferred form of the work for - making modifications to it. For an executable work, complete source code - means all the source code for all modules it contains, plus any - associated interface definition files, plus the scripts used to control - compilation and installation of the executable. However, as a special - exception, the source code distributed need not include anything that is - normally distributed (in either source or binary form) with the major - components (compiler, kernel, and so on) of the operating system on which - the executable runs, unless that component itself accompanies the - executable. - - If distribution of executable or object code is made by offering access - to copy from a designated place, then offering equivalent access to copy - the source code from the same place counts as distribution of the source - code, even though third parties are not compelled to copy the source - along with the object code. - -4. You may not copy, modify, sublicense, or distribute the Program except as - expressly provided under this License. Any attempt otherwise to copy, - modify, sublicense or distribute the Program is void, and will - automatically terminate your rights under this License. However, parties - who have received copies, or rights, from you under this License will not - have their licenses terminated so long as such parties remain in full - compliance. - -5. You are not required to accept this License, since you have not signed - it. However, nothing else grants you permission to modify or distribute - the Program or its derivative works. These actions are prohibited by law - if you do not accept this License. Therefore, by modifying or - distributing the Program (or any work based on the Program), you indicate - your acceptance of this License to do so, and all its terms and - conditions for copying, distributing or modifying the Program or works - based on it. - -6. Each time you redistribute the Program (or any work based on the - Program), the recipient automatically receives a license from the - original licensor to copy, distribute or modify the Program subject to - these terms and conditions. You may not impose any further restrictions - on the recipients' exercise of the rights granted herein. You are not - responsible for enforcing compliance by third parties to this License. - -7. If, as a consequence of a court judgment or allegation of patent - infringement or for any other reason (not limited to patent issues), - conditions are imposed on you (whether by court order, agreement or - otherwise) that contradict the conditions of this License, they do not - excuse you from the conditions of this License. If you cannot distribute - so as to satisfy simultaneously your obligations under this License and - any other pertinent obligations, then as a consequence you may not - distribute the Program at all. For example, if a patent license would - not permit royalty-free redistribution of the Program by all those who - receive copies directly or indirectly through you, then the only way you - could satisfy both it and this License would be to refrain entirely from - distribution of the Program. - - If any portion of this section is held invalid or unenforceable under any - particular circumstance, the balance of the section is intended to apply - and the section as a whole is intended to apply in other circumstances. - - It is not the purpose of this section to induce you to infringe any - patents or other property right claims or to contest validity of any such - claims; this section has the sole purpose of protecting the integrity of - the free software distribution system, which is implemented by public - license practices. Many people have made generous contributions to the - wide range of software distributed through that system in reliance on - consistent application of that system; it is up to the author/donor to - decide if he or she is willing to distribute software through any other - system and a licensee cannot impose that choice. - - This section is intended to make thoroughly clear what is believed to be - a consequence of the rest of this License. - -8. If the distribution and/or use of the Program is restricted in certain - countries either by patents or by copyrighted interfaces, the original - copyright holder who places the Program under this License may add an - explicit geographical distribution limitation excluding those countries, - so that distribution is permitted only in or among countries not thus - excluded. In such case, this License incorporates the limitation as if - written in the body of this License. - -9. The Free Software Foundation may publish revised and/or new versions of - the General Public License from time to time. Such new versions will be - similar in spirit to the present version, but may differ in detail to - address new problems or concerns. - - Each version is given a distinguishing version number. If the Program - specifies a version number of this License which applies to it and "any - later version", you have the option of following the terms and conditions - either of that version or of any later version published by the Free - Software Foundation. If the Program does not specify a version number of - this License, you may choose any version ever published by the Free - Software Foundation. - -10. If you wish to incorporate parts of the Program into other free programs - whose distribution conditions are different, write to the author to ask - for permission. For software which is copyrighted by the Free Software - Foundation, write to the Free Software Foundation; we sometimes make - exceptions for this. Our decision will be guided by the two goals of - preserving the free status of all derivatives of our free software and - of promoting the sharing and reuse of software generally. - - NO WARRANTY - -11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR - THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN - OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES - PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER - EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE - ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH - YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL - NECESSARY SERVICING, REPAIR OR CORRECTION. - -12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING - WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR - REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR - DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL - DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM - (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED - INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF - THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR - OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. diff --git a/ChangeLog b/ChangeLog index 9ad6049..f473e39 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,7 +1,3 @@ -= Net::LDAP Changelong - -== Net::LDAP 0.0.1: April 30, 2006 - #-- # Net::LDAP for Ruby. # http://rubyforge.org/projects/net-ldap/ @@ -13,3 +9,8 @@ # $Id: ChangeLog,v 1.17.2.4 2005/09/09 12:36:42 austin Exp $ #++ # vim: sts=2 sw=2 ts=4 et ai tw=77 + += Net::LDAP Changelong + +== Net::LDAP 0.0.1: April 30, 2006 +Initial release. diff --git a/Install b/Install index a8263d9..21db14d 100644 --- a/Install +++ b/Install @@ -11,7 +11,7 @@ Net::LDAP:: http://rubyforge.org/projects/net-ldap/ #-- # Net::LDAP for Ruby. # http://rubyforge.org/projects/net-ldap/ -# Copyright 2006 Francis Cianfrocca +# Copyright (C) 2006 by Francis Cianfrocca # # Available under the same terms as Ruby. See LICENCE in the main # distribution for full licensing information. diff --git a/Rakefile b/Rakefile index c54230e..c646488 100644 --- a/Rakefile +++ b/Rakefile @@ -2,7 +2,7 @@ #-- # Net::LDAP for Ruby. # http://rubyforge.org/projects/net-ldap/ -# Copyright 2006 Francis Cianfrocca +# Copyright (C) 2006 by Francis Cianfrocca # # Available under the same terms as Ruby. See LICENCE in the main # distribution for full licensing information. From 5c18a3008841113d7f73cb6ca3dad3544bc952fd Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 30 Apr 2006 13:08:55 +0000 Subject: [PATCH 057/231] documentation tweaks --- lib/net/ldap.rb | 45 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index dac2b0c..20965a9 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -199,8 +199,13 @@ module Net # # == How to use Net::LDAP # - # This is how to access Net::LDAP functionality in your Ruby programs - # (note that at present, Net::LDAP is provided as a gem): + # To access Net::LDAP functionality in your Ruby programs, start by requiring + # the library: + # + # require 'net/ldap' + # + # If you installed the Gem version of Net::LDAP, and depending on your version of + # Ruby and rubygems, you _may_ also need to require rubygems explicitly: # # require 'rubygems' # require 'net/ldap' @@ -214,7 +219,7 @@ module Net # # The Net::LDAP library is designed to be very disciplined about how it makes network # connections to servers. This is different from many of the standard native-code - # libraries that are provided on most platforms, and that share bloodlines with the + # libraries that are provided on most platforms, which share bloodlines with the # original Netscape/Michigan LDAP client implementations. These libraries sought to # insulate user code from the workings of the network. This is a good idea of course, # but the practical effect has been confusing and many difficult bugs have been caused @@ -294,11 +299,11 @@ module Net end # Instantiate an object of type Net::LDAP to perform directory operations. - # This constructor takes a hash containing arguments. The following arguments + # This constructor takes a Hash containing arguments. The following arguments # are supported: # * :host => the LDAP server's IP-address (default 127.0.0.1) # * :port => the LDAP server's TCP port (default 389) - # * :auth => a hash containing authorization parameters. Currently supported values include: + # * :auth => a Hash containing authorization parameters. Currently supported values include: # {:method => :anonymous} and # {:method => :simple, :username => your_user_name, :password => your_password } # @@ -325,6 +330,7 @@ module Net # operations in the user-supplied block on the same network connection, which # will be closed automatically when the block finishes. # + # # (PSEUDOCODE) # auth = {:method => :simple, :username => username, :password => password} # Net::LDAP.open( :host => ipaddress, :port => 389, :auth => auth ) do |ldap| # ldap.search( ... ) @@ -368,6 +374,7 @@ module Net # be done for you automatically. # For an even simpler approach, see the class method Net::LDAP#open. # + # # (PSEUDOCODE) # auth = {:method => :simple, :username => username, :password => password} # ldap = Net::LDAP.new( :host => ipaddress, :port => 389, :auth => auth ) # ldap.open do |ldap| @@ -542,9 +549,29 @@ module Net def bind_as end + # Adds a new entry to the remote LDAP server. + # Supported arguments: + # :dn :: Full DN of the new entry + # :attributes :: Attributes of the new entry. # - # add - # Add a full RDN to the remote DIS. + # The attributes argument is supplied as a Hash keyed by Strings or Symbols + # giving the attribute name, and mapping to Strings or Arrays of Strings + # giving the actual attribute values. Observe that most LDAP directories + # enforce schema constraints on the attributes contained in entries. + # #add will fail with a server-generated error if your attributes violate + # the server-specific constraints. + # Here's an example: + # + # dn = "cn=George Smith,ou=people,dc=example,dc=com" + # attr = { + # :cn => "George Smith", + # :objectclass => ["top", "inetorgperson"], + # :sn => "Smith", + # :mail => "gsmith@example.com" + # } + # Net::LDAP.open (:host => host) do |ldap| + # ldap.add( :dn => dn, :attributes => attr ) + # end # def add args if @open_connection @@ -597,7 +624,7 @@ module Net @result == 0 end - # modify_rdn is an alias for rename. + # modify_rdn is an alias for #rename. def modify_rdn args rename args end @@ -608,7 +635,7 @@ module Net class LDAP # This is a private class used internally by the library. It should not be called by user code. - class Connection + class Connection # :nodoc: LdapVersion = 3 From 5eaacf1ac32167dc45022dd780a220c3018bd16d Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 30 Apr 2006 16:31:08 +0000 Subject: [PATCH 058/231] supported LDAP delete --- lib/net/ber.rb | 2 +- lib/net/ldap.rb | 43 +++++++++++++++++++++++++++++++++++++++++++ lib/net/ldap/pdu.rb | 3 +++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index 7737de3..d9c5c8d 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -68,7 +68,7 @@ module Net # this can throw TypeErrors and other nasties. # def read_ber syntax=nil - eof? and return nil + return nil if eof? id = getc # don't trash this value, we'll use it later tag = id & 31 diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 20965a9..f80a039 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -256,6 +256,7 @@ module Net 14 => :array, # CompareRequest 15 => :array, # CompareResponse 16 => :array, # AbandonRequest + 24 => :array, # Unsolicited Notification } }, :context_specific => { @@ -275,6 +276,7 @@ module Net ResultStrings = { 0 => "Success", 1 => "Operations Error", + 2 => "Protocol Error", 16 => "No Such Attribute", 17 => "Undefined Attribute Type", 20 => "Attribute or Value Exists", @@ -629,6 +631,31 @@ module Net rename args end + # Delete an entry from the LDAP directory. + # Takes a hash of arguments. + # The only supported argument is :dn, which must + # give the complete DN of the entry to be deleted. + # Returns True or False to indicate whether the delete + # succeeded. Extended status information is available by + # calling #get_operation_result. + # + # dn = "mail=deleteme@example.com,ou=people,dc=example,dc=com" + # ldap.delete :dn => dn + # + def delete args + if @open_connection + @result = @open_connection.delete( args ) + else + @result = 0 + conn = Connection.new( :host => @host, :port => @port ) + if (@result = conn.bind( args[:auth] || @auth )) == 0 + @result = conn.delete( args ) + end + conn.close + end + @result == 0 + end + end # class LDAP @@ -854,6 +881,22 @@ module Net end + #-- + # delete + # TODO, need to support a time limit, in case the server fails to respond. + # + def delete args + dn = args[:dn] or raise "Unable to delete empty DN" + + request = dn.to_s.to_ber_application_string(10) + pkt = [next_msgid.to_ber, request].to_ber_sequence + @conn.write pkt + + (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 11) or raise LdapError.new( "response missing or invalid" ) + pdu.result_code + end + + end # class Connection end # class LDAP diff --git a/lib/net/ldap/pdu.rb b/lib/net/ldap/pdu.rb index ceabf2d..aebe9fb 100644 --- a/lib/net/ldap/pdu.rb +++ b/lib/net/ldap/pdu.rb @@ -41,6 +41,7 @@ class LdapPdu SearchResult = 5 ModifyResponse = 7 AddResponse = 9 + DeleteResponse = 11 ModifyRDNResponse = 13 attr_reader :msg_id, :app_tag @@ -81,6 +82,8 @@ class LdapPdu parse_ldap_result ber_object[1] when AddResponse parse_ldap_result ber_object[1] + when DeleteResponse + parse_ldap_result ber_object[1] when ModifyRDNResponse parse_ldap_result ber_object[1] else From fdc41e0fd9cca84988a2571ffc5c65da992be847 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 30 Apr 2006 17:17:20 +0000 Subject: [PATCH 059/231] supported search scopes and attribute-only searches. --- lib/net/ldap.rb | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index f80a039..9869e13 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -236,6 +236,11 @@ module Net class LdapError < Exception; end + SearchScope_BaseObject = 0 + SearchScope_SingleLevel = 1 + SearchScope_WholeSubtree = 2 + SearchScopes = [SearchScope_BaseObject, SearchScope_SingleLevel, SearchScope_WholeSubtree] + AsnSyntax = { :application => { :constructed => { @@ -437,6 +442,8 @@ module Net # * :filter (an object of type Net::LDAP::Filter, defaults to objectclass=*); # * :attributes (a string or array of strings specifying the LDAP attributes to return from the server); # * :return_result (a boolean specifying whether to return a result set). + # * :attributes_only (a boolean flag, defaults false) + # * :scope (one of: Net::LDAP::SearchScope_BaseObject, Net::LDAP::SearchScope_SingleLevel, Net::LDAP::SearchScope_WholeSubtree. Default is WholeSubtree.) # # #search queries the LDAP server and passes each entry to the # caller-supplied block, as an object of type Net::LDAP::Entry. @@ -733,18 +740,22 @@ module Net #-- # WARNING: this code substantially recapitulates the searchx method. # - def search args + def search args = {} search_filter = (args && args[:filter]) || Filter.eq( "objectclass", "*" ) search_base = (args && args[:base]) || "dc=example,dc=com" search_attributes = ((args && args[:attributes]) || []).map {|attr| attr.to_s.to_ber} + attributes_only = (args and args[:attributes_only] == true) + scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree + raise LdapError.new( "invalid search scope" ) unless SearchScopes.include?(scope) + request = [ search_base.to_ber, - 2.to_ber_enumerated, + scope.to_ber_enumerated, 0.to_ber_enumerated, 0.to_ber, 0.to_ber, - false.to_ber, + attributes_only.to_ber, search_filter.to_ber, search_attributes.to_ber_sequence ].to_ber_appsequence(3) From 63ecf9c06552c8b24718d34f892c84276dc03df0 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 30 Apr 2006 18:19:38 +0000 Subject: [PATCH 060/231] documentation for modify --- lib/net/ldap.rb | 70 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 9869e13..e38e874 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -597,9 +597,72 @@ module Net end + # Modify the attribute values of a particular entry on the LDAP directory. + # Takes a hash with arguments. Supported arguments are: + # :dn :: (the full DN of the entry whose attributes are to be modified) + # :operations :: (the modifications to be performed, detailed next) + # + # This method returns True or False to indicate whether the operation + # succeeded or failed, with extended information available by calling + # #get_operation_result. + # + # The LDAP protocol provides a full and well thought-out set of operations + # for changing the values of attributes, but they are necessarily somewhat complex + # and not always intuitive. If these instructions are confusing or incomplete, + # please send us email or create a bug report on rubyforge. + # + # The :operations parameter to #modify takes an array of operation-descriptors. + # Each individual operation is specified in one element of the array, and + # most LDAP servers will attempt to perform the operations in order. + # + # Each of the operations appearing in the Array must itself be an Array + # with exactly three elements: + # an operator:: must be :add, :replace, or :delete + # an attribute name:: the attribute name (string or symbol) to modify + # a value:: either a string or an array of strings. + # + # The :add operator will, unsurprisingly, add the specified values to + # the specified attribute. If the attribute does not already exist, + # :add will create it. Most LDAP servers will generate an error if you + # to add a value that already exists. + # + # :replace will erase the current value(s) for the specified attribute, + # if there are any, and replace them with the specified value(s). + # + # :delete will remove the specified value(s) from the specified attribute. + # If you pass nil, an empty string, or an empty array as the value parameter + # to a :delete operation, the _entire_ _attribute_ will be deleted. + # + # For example: + # + # dn = "mail=modifyme@example.com,ou=people,dc=example,dc=com" + # ops = [ + # [:add, :mail, "aliasaddress@example.com"], + # [:replace, :mail, ["newaddress@example.com", "newalias@example.com"]], + # [:delete, :sn, nil] + # ] + # ldap.modify :dn => dn, :operations => ops + # + # (This example is contrived since you probably wouldn't add a mail + # value right before replacing the whole attribute, but it shows that order + # of execution matters. Also, many LDAP servers won't let you delete SN + # because it would be a schema violation.) + # + # It's essential to keep in mind that if you specify more than one operation in + # a call to #modify, most LDAP servers will attempt to perform all of the operations + # in the order you gave them. + # This matters because you may specify operations on the + # same attribute which must be performed in a certain order. + # Most LDAP servers will _stop_ processing your modifications if one of them + # causes an error on the server (such as a schema-constraint violation). + # If this happens, you will probably get a result code from the server that + # reflects only the operation that failed, and you may or may not get extended + # information that will tell you which one failed. #modify has no notion + # of an atomic transaction. If you specify a chain of modifications in one + # call to #modify, and one of them fails, the preceding ones will usually + # not be "rolled back," resulting in a partial update. This is a limitation + # of the LDAP protocol, not of Net::LDAP. # - # modify - # Modify the attributes of an entry on the remote DIS. # def modify args if @open_connection @@ -615,9 +678,8 @@ module Net @result == 0 end - # - # rename # Rename an entry on the remote DIS by changing the last RDN of its DN. + # _Documentation_ _stub_ # def rename args if @open_connection From bae0b92211ea99d54bd2a216d5fea0222a4fdb0c Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 30 Apr 2006 18:36:24 +0000 Subject: [PATCH 061/231] added VERSION constant, otherwise rake was picking it up from Ruby's version. --- lib/net/ldap.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index e38e874..105e8ca 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -236,6 +236,9 @@ module Net class LdapError < Exception; end + VERSION = "0.0.1" + + SearchScope_BaseObject = 0 SearchScope_SingleLevel = 1 SearchScope_WholeSubtree = 2 From 6e3e95c2370a57c46ce92fa47e1c4ebf479b642c Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 30 Apr 2006 18:44:45 +0000 Subject: [PATCH 062/231] Removed COPYING from distro list. This GPL file is replaced by LICENCE. --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index c646488..938bc72 100644 --- a/Rakefile +++ b/Rakefile @@ -85,7 +85,7 @@ if $can_minitar Dir.chdir("..") do begin files = %W(bin/**/* lib/**/* tests/**/* ChangeLog README LICENCE - COPYING Rakefile net-ldap.gemspec setup.rb pre-setup.rb) + Rakefile net-ldap.gemspec setup.rb pre-setup.rb) files = FileList[files.map { |file| File.join(current, file) }].to_a files.map! do |dd| ddnew = dd.gsub(/^#{current}/, $distdir) From 1fec1f6abf7902f51f6bccda30233e3c34f2db6f Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 30 Apr 2006 18:46:27 +0000 Subject: [PATCH 063/231] and removed COPYING from another place --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index 938bc72..9bb5f7d 100644 --- a/Rakefile +++ b/Rakefile @@ -129,7 +129,7 @@ desc "Build the RDoc documentation for #$name." task :docs do require 'rdoc/rdoc' rdoc_options = %W(--title #$name --main README --line-numbers) - files = FileList[*%w(README LICENCE ChangeLog COPYING LICENCE bin/**/*.rb lib/**/*.rb)] + files = FileList[*%w(README LICENCE ChangeLog LICENCE bin/**/*.rb lib/**/*.rb)] rdoc_options += files.to_a RDoc::RDoc.new.document(rdoc_options) end From 2f3802c4fdad0ef031dae25c7aaa30a58ff000ed Mon Sep 17 00:00:00 2001 From: austin Date: Mon, 1 May 2006 02:14:05 +0000 Subject: [PATCH 064/231] Tweaks to top-level files. --- COPYING | 272 +++++++++++++++++++++++++++++++++++++++++++++++ ChangeLog | 10 +- Rakefile | 3 +- net-ldap.gemspec | 56 +++++++--- pre-setup.rb | 2 +- 5 files changed, 322 insertions(+), 21 deletions(-) create mode 100644 COPYING diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..2ff629a --- /dev/null +++ b/COPYING @@ -0,0 +1,272 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street, +Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and +distribute verbatim copies of this license document, but changing it is not +allowed. + + Preamble + +The licenses for most software are designed to take away your freedom to +share and change it. By contrast, the GNU General Public License is +intended to guarantee your freedom to share and change free software--to +make sure the software is free for all its users. This General Public +License applies to most of the Free Software Foundation's software and to +any other program whose authors commit to using it. (Some other Free +Software Foundation software is covered by the GNU Lesser General Public +License instead.) You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish), that you receive source code or can get it if you want it, that you +can change the software or use pieces of it in new free programs; and that +you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These +restrictions translate to certain responsibilities for you if you distribute +copies of the software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or +for a fee, you must give the recipients all the rights that you have. You +must make sure that they, too, receive or can get the source code. And you +must show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) +offer you this license which gives you legal permission to copy, distribute +and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that +everyone understands that there is no warranty for this free software. If +the software is modified by someone else and passed on, we want its +recipients to know that what they have is not the original, so that any +problems introduced by others will not reflect on the original authors' +reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that redistributors of a free program will +individually obtain patent licenses, in effect making the program +proprietary. To prevent this, we have made it clear that any patent must be +licensed for everyone's free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification +follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice + placed by the copyright holder saying it may be distributed under the + terms of this General Public License. The "Program", below, refers to + any such program or work, and a "work based on the Program" means either + the Program or any derivative work under copyright law: that is to say, a + work containing the Program or a portion of it, either verbatim or with + modifications and/or translated into another language. (Hereinafter, + translation is included without limitation in the term "modification".) + Each licensee is addressed as "you". + + Activities other than copying, distribution and modification are not + covered by this License; they are outside its scope. The act of running + the Program is not restricted, and the output from the Program is covered + only if its contents constitute a work based on the Program (independent + of having been made by running the Program). Whether that is true depends + on what the Program does. + +1. You may copy and distribute verbatim copies of the Program's source code + as you receive it, in any medium, provided that you conspicuously and + appropriately publish on each copy an appropriate copyright notice and + disclaimer of warranty; keep intact all the notices that refer to this + License and to the absence of any warranty; and give any other recipients + of the Program a copy of this License along with the Program. + + You may charge a fee for the physical act of transferring a copy, and you + may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, + thus forming a work based on the Program, and copy and distribute such + modifications or work under the terms of Section 1 above, provided that + you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices stating + that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in whole + or in part contains or is derived from the Program or any part + thereof, to be licensed as a whole at no charge to all third parties + under the terms of this License. + + c) If the modified program normally reads commands interactively when + run, you must cause it, when started running for such interactive use + in the most ordinary way, to print or display an announcement + including an appropriate copyright notice and a notice that there is + no warranty (or else, saying that you provide a warranty) and that + users may redistribute the program under these conditions, and telling + the user how to view a copy of this License. (Exception: if the + Program itself is interactive but does not normally print such an + announcement, your work based on the Program is not required to print + an announcement.) + + These requirements apply to the modified work as a whole. If + identifiable sections of that work are not derived from the Program, and + can be reasonably considered independent and separate works in + themselves, then this License, and its terms, do not apply to those + sections when you distribute them as separate works. But when you + distribute the same sections as part of a whole which is a work based on + the Program, the distribution of the whole must be on the terms of this + License, whose permissions for other licensees extend to the entire + whole, and thus to each and every part regardless of who wrote it. + + Thus, it is not the intent of this section to claim rights or contest + your rights to work written entirely by you; rather, the intent is to + exercise the right to control the distribution of derivative or + collective works based on the Program. + + In addition, mere aggregation of another work not based on the Program + with the Program (or with a work based on the Program) on a volume of a + storage or distribution medium does not bring the other work under the + scope of this License. + +3. You may copy and distribute the Program (or a work based on it, under + Section 2) in object code or executable form under the terms of Sections + 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable source + code, which must be distributed under the terms of Sections 1 and 2 + above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three years, to + give any third party, for a charge no more than your cost of + physically performing source distribution, a complete machine-readable + copy of the corresponding source code, to be distributed under the + terms of Sections 1 and 2 above on a medium customarily used for + software interchange; or, + + c) Accompany it with the information you received as to the offer to + distribute corresponding source code. (This alternative is allowed + only for noncommercial distribution and only if you received the + program in object code or executable form with such an offer, in + accord with Subsection b above.) + + The source code for a work means the preferred form of the work for + making modifications to it. For an executable work, complete source code + means all the source code for all modules it contains, plus any + associated interface definition files, plus the scripts used to control + compilation and installation of the executable. However, as a special + exception, the source code distributed need not include anything that is + normally distributed (in either source or binary form) with the major + components (compiler, kernel, and so on) of the operating system on which + the executable runs, unless that component itself accompanies the + executable. + + If distribution of executable or object code is made by offering access + to copy from a designated place, then offering equivalent access to copy + the source code from the same place counts as distribution of the source + code, even though third parties are not compelled to copy the source + along with the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except as + expressly provided under this License. Any attempt otherwise to copy, + modify, sublicense or distribute the Program is void, and will + automatically terminate your rights under this License. However, parties + who have received copies, or rights, from you under this License will not + have their licenses terminated so long as such parties remain in full + compliance. + +5. You are not required to accept this License, since you have not signed + it. However, nothing else grants you permission to modify or distribute + the Program or its derivative works. These actions are prohibited by law + if you do not accept this License. Therefore, by modifying or + distributing the Program (or any work based on the Program), you indicate + your acceptance of this License to do so, and all its terms and + conditions for copying, distributing or modifying the Program or works + based on it. + +6. Each time you redistribute the Program (or any work based on the + Program), the recipient automatically receives a license from the + original licensor to copy, distribute or modify the Program subject to + these terms and conditions. You may not impose any further restrictions + on the recipients' exercise of the rights granted herein. You are not + responsible for enforcing compliance by third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent + infringement or for any other reason (not limited to patent issues), + conditions are imposed on you (whether by court order, agreement or + otherwise) that contradict the conditions of this License, they do not + excuse you from the conditions of this License. If you cannot distribute + so as to satisfy simultaneously your obligations under this License and + any other pertinent obligations, then as a consequence you may not + distribute the Program at all. For example, if a patent license would + not permit royalty-free redistribution of the Program by all those who + receive copies directly or indirectly through you, then the only way you + could satisfy both it and this License would be to refrain entirely from + distribution of the Program. + + If any portion of this section is held invalid or unenforceable under any + particular circumstance, the balance of the section is intended to apply + and the section as a whole is intended to apply in other circumstances. + + It is not the purpose of this section to induce you to infringe any + patents or other property right claims or to contest validity of any such + claims; this section has the sole purpose of protecting the integrity of + the free software distribution system, which is implemented by public + license practices. Many people have made generous contributions to the + wide range of software distributed through that system in reliance on + consistent application of that system; it is up to the author/donor to + decide if he or she is willing to distribute software through any other + system and a licensee cannot impose that choice. + + This section is intended to make thoroughly clear what is believed to be + a consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain + countries either by patents or by copyrighted interfaces, the original + copyright holder who places the Program under this License may add an + explicit geographical distribution limitation excluding those countries, + so that distribution is permitted only in or among countries not thus + excluded. In such case, this License incorporates the limitation as if + written in the body of this License. + +9. The Free Software Foundation may publish revised and/or new versions of + the General Public License from time to time. Such new versions will be + similar in spirit to the present version, but may differ in detail to + address new problems or concerns. + + Each version is given a distinguishing version number. If the Program + specifies a version number of this License which applies to it and "any + later version", you have the option of following the terms and conditions + either of that version or of any later version published by the Free + Software Foundation. If the Program does not specify a version number of + this License, you may choose any version ever published by the Free + Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs + whose distribution conditions are different, write to the author to ask + for permission. For software which is copyrighted by the Free Software + Foundation, write to the Free Software Foundation; we sometimes make + exceptions for this. Our decision will be guided by the two goals of + preserving the free status of all derivatives of our free software and + of promoting the sharing and reuse of software generally. + + NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR + THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN + OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES + PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER + EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE + ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH + YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL + NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING + WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR + REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR + DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL + DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM + (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED + INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF + THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR + OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. diff --git a/ChangeLog b/ChangeLog index f473e39..bf7fec2 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,8 @@ += Net::LDAP Changelog + +== Net::LDAP 0.0.1: April 30, 2006 +Initial release. + #-- # Net::LDAP for Ruby. # http://rubyforge.org/projects/net-ldap/ @@ -9,8 +14,3 @@ # $Id: ChangeLog,v 1.17.2.4 2005/09/09 12:36:42 austin Exp $ #++ # vim: sts=2 sw=2 ts=4 et ai tw=77 - -= Net::LDAP Changelong - -== Net::LDAP 0.0.1: April 30, 2006 -Initial release. diff --git a/Rakefile b/Rakefile index 9bb5f7d..c57c590 100644 --- a/Rakefile +++ b/Rakefile @@ -85,7 +85,7 @@ if $can_minitar Dir.chdir("..") do begin files = %W(bin/**/* lib/**/* tests/**/* ChangeLog README LICENCE - Rakefile net-ldap.gemspec setup.rb pre-setup.rb) + COPYING Rakefile net-ldap.gemspec setup.rb pre-setup.rb) files = FileList[files.map { |file| File.join(current, file) }].to_a files.map! do |dd| ddnew = dd.gsub(/^#{current}/, $distdir) @@ -227,4 +227,3 @@ if $can_minitar task :publish_news => :tar task :default => :tar end - diff --git a/net-ldap.gemspec b/net-ldap.gemspec index 502ec1e..3d019d0 100644 --- a/net-ldap.gemspec +++ b/net-ldap.gemspec @@ -1,13 +1,43 @@ -spec = Gem::Specification.new {|s| - s.name = "netldap" - s.version = "0.0.1" - s.author = "Francis Cianfrocca" - s.email = "garbagecat10@gmail.com" - s.homepage = "netldap@rubyforge.org" - s.summary = "Net::LDAP library" - s.files = FileList["{bin,tests,lib}/**/*"].exclude("rdoc").to_a - s.require_paths = ["lib"] - s.test_file = "tests/testem.rb" - s.has_rdoc = true - s.extra_rdoc_files = ["README", "ChangeLog", "LICENCE"] -} +#-- +# Net::LDAP for Ruby. +# http://rubyforge.org/projects/net-ldap/ +# Copyright 2006 Francis Cianfrocca +# +# Available under the same terms as Ruby. See LICENCE in the main +# distribution for full licensing information. +# +# $Id: ChangeLog,v 1.17.2.4 2005/09/09 12:36:42 austin Exp $ +#++ + +spec = Gem::Specification.new do |s| + s.name = "ruby-net-ldap" + s.version = "0.0.1" + s.summary = %q(A pure Ruby LDAP client library.) + s.platform = Gem::Platform::RUBY + + s.has_rdoc = true + s.rdoc_options = %w(--title Net::LDAP --main README --line-numbers) + s.extra_rdoc_files = %w(README ChangeLog LICENCE COPYING) + + files = %w(README LICENCE ChangeLog COPYING {bin,tests,lib}/**/*) + s.files = FileList[*files].exclude("rdoc").to_a + + s.require_paths = ["lib"] + + s.test_files = %w{tests/testem.rb} + + s.author = "Francis Cianfrocca" + s.email = "garbagecat10@gmail.com" + s.rubyforge_project = %q(net-ldap) + s.homepage = "http://rubyforge.org/projects/net-ldap" + + description = [] + File.open("README") do |file| + file.each do |line| + line.chomp! + break if line.empty? + description << "#{line.gsub(/\[\d\]/, '')}" + end + end + s.description = description[1..-1].join(" ") +end diff --git a/pre-setup.rb b/pre-setup.rb index 88f6938..1bfc1e4 100644 --- a/pre-setup.rb +++ b/pre-setup.rb @@ -43,4 +43,4 @@ ri = %w(--ri-site --merge) dox = %w(README ChangeLog lib) build_rdoc rdoc + dox build_ri ri + dox -run_tests Dir["tests/**/*"] +# run_tests Dir["tests/**/*"] From 7958315227fcde441e53b1b118519b096ff02314 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 1 May 2006 05:27:29 +0000 Subject: [PATCH 065/231] documentation tweaks --- ChangeLog | 2 +- LICENCE | 4 +-- Release-Announcement | 59 ++++++++++++++++++++++++++++++++++++++------ net-ldap.gemspec | 2 +- 4 files changed, 55 insertions(+), 12 deletions(-) diff --git a/ChangeLog b/ChangeLog index bf7fec2..f572e67 100644 --- a/ChangeLog +++ b/ChangeLog @@ -6,7 +6,7 @@ Initial release. #-- # Net::LDAP for Ruby. # http://rubyforge.org/projects/net-ldap/ -# Copyright 2006 Francis Cianfrocca +# Copyright (C) 2006 by Francis Cianfrocca # # Available under the same terms as Ruby. See LICENCE in the main # distribution for full licensing information. diff --git a/LICENCE b/LICENCE index a879f66..953ea0b 100644 --- a/LICENCE +++ b/LICENCE @@ -1,5 +1,5 @@ -Net::LDAP is copyrighted free software by Francis Gianfrocca -. You can redistribute it and/or modify it under either +Net::LDAP is copyrighted free software by Francis Cianfrocca +. You can redistribute it and/or modify it under either the terms of the GPL (see the file COPYING), or the conditions below: 1. You may make and give away verbatim copies of the source form of the diff --git a/Release-Announcement b/Release-Announcement index 5c4f80a..45d9007 100644 --- a/Release-Announcement +++ b/Release-Announcement @@ -1,18 +1,38 @@ -It is with great excitement that I announce the first release of the pure -Ruby LDAP library, Net::LDAP. +We're pleased to announce the first release of Net::LDAP, the first +pure-Ruby LDAP library. Net::LDAP intends to be a feature-complete +LDAP client which can access as much as possible of the functionality +of the most-used LDAP server implementations. + +Net::LDAP includes a full implementation of the LDAP wire-line +protocol so it can also be used in LDAP server implementations. + +Thanks for Austin Ziegler for invaluable help in reviewing the +implementation and providing the release structure. = What is Net::LDAP for Ruby? -[...] +This library provides a pure-Ruby implementation of an LDAP client. +It can be used to access any server which implements the LDAP protocol. + +Net::LDAP is intended to provide full LDAP functionality while hiding +the more arcane aspects of the LDAP protocol itself, so as to make the +programming interface as Ruby-like as possible. + +In particular, this means that there is no direct dependence on the +structure of the various "traditional" LDAP clients. This is a ground-up +rethinking of the LDAP API. + +Net::LDAP is based on RFC-1777, which specifies the Lightweight Directory +Access Protocol, as amended and extended by subsequent RFCs and by the more +widely-used directory implementations. Homepage:: http://ruby-pdf.rubyforge.org/net-ldap/ Download:: http://rubyforge.org/frs/?**** Copyright:: 2006 Francis Cianfrocca -This software is based on RFC***, describing the Lightweight Directory -Access Protocol. - == LICENCE NOTES -[...] +Please read the file LICENCE for licensing restrictions on this library. In +the simplest terms, this library is available under the same terms as Ruby +itself. == Requirements and Installation Net::LDAP requires Ruby 1.8.2 or better. @@ -25,7 +45,30 @@ Alternatively, you can use the RubyGems version of Net::LDAP available as ruby-net-ldap-0.0.1.gem from the usual sources. == Whet your appetite: -[...] + require 'net/ldap' + + ldap = Net::LDAP.new :host => server_ip_address, + :port => 389, + :auth => { + :method => :simple, + :username => "cn=manager,dc=example,dc=com", + :password => "opensesame" + } + + filter = Net::LDAP::Filter.eq( "cn", "George*" ) + treebase = "dc=example,dc=com" + + ldap.search( :base => treebase, :filter => filter ) do |entry| + puts "DN: #{entry.dn}" + entry.each do |attribute, values| + puts " #{attribute}:" + values.each do |value| + puts " --->#{value}" + end + end + end + + p ldap.get_operation_result == Net::LDAP 0.0.1: April 30, 2006 [...] diff --git a/net-ldap.gemspec b/net-ldap.gemspec index 3d019d0..107a396 100644 --- a/net-ldap.gemspec +++ b/net-ldap.gemspec @@ -1,7 +1,7 @@ #-- # Net::LDAP for Ruby. # http://rubyforge.org/projects/net-ldap/ -# Copyright 2006 Francis Cianfrocca +# Copyright (C) 2006 by Francis Cianfrocca # # Available under the same terms as Ruby. See LICENCE in the main # distribution for full licensing information. From 92c61ea89763112b48b256c8482c958277239a10 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 1 May 2006 06:40:48 +0000 Subject: [PATCH 066/231] comment --- lib/net/ber.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index d9c5c8d..69a190f 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -222,6 +222,8 @@ class String # 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 From 4d4423ec25fdb26c73fabe1a8689064eb68ab084 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 1 May 2006 07:19:12 +0000 Subject: [PATCH 067/231] Deprecated #modify. Added #add_attribute, #replace_attribute and #delete_attribute. --- lib/net/ldap.rb | 61 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 105e8ca..b090c2a 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -181,6 +181,8 @@ module Net # the attribute values stored in the directory for a particular entity. # #modify may add or delete attributes (which are lists of values) or it change attributes by # adding to or deleting from their values. + # There are three easier methods to modify an entry's attribute values: + # #add_attribute, #replace_attribute, and #delete_attribute. # # ==== Delete # #delete operation specifies an entity DN. If it succeeds, the entity and all its attributes @@ -600,7 +602,9 @@ module Net end - # Modify the attribute values of a particular entry on the LDAP directory. + # _DEPRECATED_ - Please use #add_attribute, #replace_attribute, or #delete_attribute. + # + # Modifies the attribute values of a particular entry on the LDAP directory. # Takes a hash with arguments. Supported arguments are: # :dn :: (the full DN of the entry whose attributes are to be modified) # :operations :: (the modifications to be performed, detailed next) @@ -681,6 +685,61 @@ module Net @result == 0 end + + # Add a value to an attribute. + # Takes the full DN of the entry to modify, + # the name (Symbol or String) of the attribute, and the value (String or + # Array). If the attribute does not exist (and there are no schema violations), + # #add_attribute will create it with the caller-specified values. + # If the attribute already exists (and there are no schema violations), the + # caller-specified values will be _added_ to the values already present. + # + # Returns True or False to indicate whether the operation + # succeeded or failed, with extended information available by calling + # #get_operation_result. See also #replace_attribute and #delete_attribute. + # + # dn = "cn=modifyme,dc=example,dc=com" + # ldap.add_attribute dn, :mail, "newmailaddress@example.com" + # + def add_attribute dn, attribute, value + modify :dn => dn, :operations => [[:add, attribute, value]] + end + + # Replace the value of an attribute. + # #replace_attribute can be thought of as equivalent to calling #delete_attribute + # followed by #add_attribute. It takes the full DN of the entry to modify, + # the name (Symbol or String) of the attribute, and the value (String or + # Array). If the attribute does not exist, it will be created with the + # caller-specified value(s). If the attribute does exist, its values will be + # _discarded_ and replaced with the caller-specified values. + # + # Returns True or False to indicate whether the operation + # succeeded or failed, with extended information available by calling + # #get_operation_result. See also #add_attribute and #delete_attribute. + # + # dn = "cn=modifyme,dc=example,dc=com" + # ldap.replace_attribute dn, :mail, "newmailaddress@example.com" + # + def replace_attribute dn, attribute, value + modify :dn => dn, :operations => [[:replace, attribute, value]] + end + + # Delete an attribute and all its values. + # Takes the full DN of the entry to modify, and the + # name (Symbol or String) of the attribute to delete. + # + # Returns True or False to indicate whether the operation + # succeeded or failed, with extended information available by calling + # #get_operation_result. See also #add_attribute and #replace_attribute. + # + # dn = "cn=modifyme,dc=example,dc=com" + # ldap.delete_attribute dn, :mail + # + def delete_attribute dn, attribute + modify :dn => dn, :operations => [[:delete, attribute, nil]] + end + + # Rename an entry on the remote DIS by changing the last RDN of its DN. # _Documentation_ _stub_ # From 5d349b43f644abc65d2b058574802ffa6e916664 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 1 May 2006 12:19:16 +0000 Subject: [PATCH 068/231] Fixed a small edge condition relating to adding values to Net::LDAP::Entry. --- lib/net/ldap/entry.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/net/ldap/entry.rb b/lib/net/ldap/entry.rb index fc6bf18..3d89a56 100644 --- a/lib/net/ldap/entry.rb +++ b/lib/net/ldap/entry.rb @@ -46,10 +46,14 @@ class LDAP @myhash[sym] = value end + + #-- + # We have to deal with this one as we do []= + # because this one and not the other one gets called + # in formulations like entry["CN"] << cn. + # def [] name - #unless name.is_a?(Symbol) - # name = name.to_s.downcase.intern - #end + name = name.to_s.downcase.intern unless name.is_a?(Symbol) @myhash[name] end From 648118a60bab3f8dedfa8bf0c2d1ac39c548b7d4 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 1 May 2006 17:24:58 +0000 Subject: [PATCH 069/231] changelog for 0.0.1 --- ChangeLog | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ChangeLog b/ChangeLog index f572e67..0392387 100644 --- a/ChangeLog +++ b/ChangeLog @@ -2,6 +2,18 @@ == Net::LDAP 0.0.1: April 30, 2006 Initial release. +Client functionality is near-complete, although the APIs +are not guaranteed and may change depending on feedback +from the community. +We're internally working on a Ruby-based implementation +of a full-featured, production-quality LDAP server, +which will leverage the underlying LDAP and BER functionality +in Net::LDAP. +Please tell us if you would be interested in seeing a public +release of the LDAP server. +Grateful acknowledgement to Austin Ziegler, who reviewed +this code and provided the release framework, including +minitar. #-- # Net::LDAP for Ruby. From 8054ef181f1d34c1f4d7aed95b0c6c78af6b7ad0 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 1 May 2006 17:35:31 +0000 Subject: [PATCH 070/231] added changenotes for version 0.0.1 --- Release-Announcement | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Release-Announcement b/Release-Announcement index 45d9007..408d517 100644 --- a/Release-Announcement +++ b/Release-Announcement @@ -71,6 +71,18 @@ as ruby-net-ldap-0.0.1.gem from the usual sources. p ldap.get_operation_result == Net::LDAP 0.0.1: April 30, 2006 -[...] +Initial release. +Client functionality is near-complete, although the APIs +are not guaranteed and may change depending on feedback +from the community. +We're internally working on a Ruby-based implementation +of a full-featured, production-quality LDAP server, +which will leverage the underlying LDAP and BER functionality +in Net::LDAP. +Please tell us if you would be interested in seeing a public +release of the LDAP server. +Grateful acknowledgement to Austin Ziegler, who reviewed +this code and provided the release framework, including +minitar. + -[signature here] From d144ad360f0f61a3c0ea28b0b30850c07630d40a Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 1 May 2006 17:49:34 +0000 Subject: [PATCH 071/231] doc tweaks --- ChangeLog | 28 ++++++++++++++-------------- Release-Announcement | 29 ++++++++++++++--------------- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/ChangeLog b/ChangeLog index 0392387..fcde05e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,19 +1,19 @@ = Net::LDAP Changelog -== Net::LDAP 0.0.1: April 30, 2006 -Initial release. -Client functionality is near-complete, although the APIs -are not guaranteed and may change depending on feedback -from the community. -We're internally working on a Ruby-based implementation -of a full-featured, production-quality LDAP server, -which will leverage the underlying LDAP and BER functionality -in Net::LDAP. -Please tell us if you would be interested in seeing a public -release of the LDAP server. -Grateful acknowledgement to Austin Ziegler, who reviewed -this code and provided the release framework, including -minitar. +== Net::LDAP 0.0.1: May 1, 2006 +* Initial release. +* Client functionality is near-complete, although the APIs + are not guaranteed and may change depending on feedback + from the community. +* We're internally working on a Ruby-based implementation + of a full-featured, production-quality LDAP server, + which will leverage the underlying LDAP and BER functionality + in Net::LDAP. +* Please tell us if you would be interested in seeing a public + release of the LDAP server. +* Grateful acknowledgement to Austin Ziegler, who reviewed + this code and provided the release framework, including + minitar. #-- # Net::LDAP for Ruby. diff --git a/Release-Announcement b/Release-Announcement index 408d517..c59de28 100644 --- a/Release-Announcement +++ b/Release-Announcement @@ -70,19 +70,18 @@ as ruby-net-ldap-0.0.1.gem from the usual sources. p ldap.get_operation_result -== Net::LDAP 0.0.1: April 30, 2006 -Initial release. -Client functionality is near-complete, although the APIs -are not guaranteed and may change depending on feedback -from the community. -We're internally working on a Ruby-based implementation -of a full-featured, production-quality LDAP server, -which will leverage the underlying LDAP and BER functionality -in Net::LDAP. -Please tell us if you would be interested in seeing a public -release of the LDAP server. -Grateful acknowledgement to Austin Ziegler, who reviewed -this code and provided the release framework, including -minitar. - +== Net::LDAP 0.0.1: May 1, 2006 +* Initial release. +* Client functionality is near-complete, although the APIs + are not guaranteed and may change depending on feedback + from the community. +* We're internally working on a Ruby-based implementation + of a full-featured, production-quality LDAP server, + which will leverage the underlying LDAP and BER functionality + in Net::LDAP. +* Please tell us if you would be interested in seeing a public + release of the LDAP server. +* Grateful acknowledgement to Austin Ziegler, who reviewed + this code and provided the release framework, including + minitar. From 20a83436b4c715a2251279990b6649570c799fb6 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 1 May 2006 18:07:42 +0000 Subject: [PATCH 072/231] links corrected --- Release-Announcement | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Release-Announcement b/Release-Announcement index c59de28..5a3a078 100644 --- a/Release-Announcement +++ b/Release-Announcement @@ -25,9 +25,9 @@ Net::LDAP is based on RFC-1777, which specifies the Lightweight Directory Access Protocol, as amended and extended by subsequent RFCs and by the more widely-used directory implementations. -Homepage:: http://ruby-pdf.rubyforge.org/net-ldap/ -Download:: http://rubyforge.org/frs/?**** -Copyright:: 2006 Francis Cianfrocca +Homepage:: http://rubyforge.org/projects/net-ldap/ +Download:: http://rubyforge.org/frs/?group_id=143 +Copyright:: 2006 by Francis Cianfrocca == LICENCE NOTES Please read the file LICENCE for licensing restrictions on this library. In From e0d32bb1bd2294e295d4f9d6d827961d6cd1333e Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 1 May 2006 20:19:25 +0000 Subject: [PATCH 073/231] Fixed package name --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index c57c590..cd5b007 100644 --- a/Rakefile +++ b/Rakefile @@ -166,7 +166,7 @@ task :release_files => [ :verify_rubyforge, :gem ] do release.password = ENV['RUBYFORGE_PASSWORD'] release.files = release_files.to_a release.release_name = "#$name #$version" - release.package_name = "mime-types" + release.package_name = "ruby-net-ldap" notes = [] File.open("README") do |file| From a24d607735dcfbbdb96f08aeddcd0fc684127d66 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 1 May 2006 20:31:33 +0000 Subject: [PATCH 074/231] added no-dependency statement. --- Release-Announcement | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Release-Announcement b/Release-Announcement index 5a3a078..eb48c19 100644 --- a/Release-Announcement +++ b/Release-Announcement @@ -1,7 +1,9 @@ We're pleased to announce the first release of Net::LDAP, the first pure-Ruby LDAP library. Net::LDAP intends to be a feature-complete LDAP client which can access as much as possible of the functionality -of the most-used LDAP server implementations. +of the most-used LDAP server implementations. This library does +not wrap any existing native-code LDAP libraries, creates no +Ruby extensions, and has dependencies external to Ruby. Net::LDAP includes a full implementation of the LDAP wire-line protocol so it can also be used in LDAP server implementations. From d4b093eb0755173fa3aff29a3afdfcf22a5a032a Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 1 May 2006 20:50:26 +0000 Subject: [PATCH 075/231] fixed small problem --- Release-Announcement | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Release-Announcement b/Release-Announcement index eb48c19..c839db2 100644 --- a/Release-Announcement +++ b/Release-Announcement @@ -3,7 +3,7 @@ pure-Ruby LDAP library. Net::LDAP intends to be a feature-complete LDAP client which can access as much as possible of the functionality of the most-used LDAP server implementations. This library does not wrap any existing native-code LDAP libraries, creates no -Ruby extensions, and has dependencies external to Ruby. +Ruby extensions, and has no dependencies external to Ruby. Net::LDAP includes a full implementation of the LDAP wire-line protocol so it can also be used in LDAP server implementations. From 927446601fa10538e5eb7d972993e14c0314dcee Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 1 May 2006 21:31:27 +0000 Subject: [PATCH 076/231] doc tweaks --- lib/net/ldap.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index b090c2a..7a9af92 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -116,17 +116,17 @@ module Net # range of attributes, and constrain their values according to standard # rules. # - # A good example of an attribute is cn, which stands for "Common Name." - # In many directories, this attribute is used to store a string consisting of - # a person's first and last names. Most directories enforce the convention that - # an entity's cn attribute have exactly one value. In LDAP - # jargon, that means that cn must be present and + # A good example of an attribute is sn, which stands for "Surname." + # This attribute is generally used to store a person's surname, or last name. + # Most directories enforce the standard convention that + # an entity's sn attribute have exactly one value. In LDAP + # jargon, that means that sn must be present and # single-valued. # # Another attribute is mail, which is used to store email addresses. # (No, there is no attribute called "email," perhaps because X.400 terminology # predates the invention of the term email.) mail differs - # from cn in that most directories permit any number of values for the + # from sn in that most directories permit any number of values for the # mail attribute, including zero. # # @@ -173,7 +173,7 @@ module Net # set of attribute values for each entity, depending on what attributes the search requested. # # ==== Add - # #add operation specifies a new DN and an initial set of attribute values. If the operation + # #add specifies a new DN and an initial set of attribute values. If the operation # succeeds, a new entity with the corresponding DN and attributes is added to the directory. # # ==== Modify From d85cc21fad673fa632fe946d46ad014ab4ce0b6f Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 2 May 2006 00:10:24 +0000 Subject: [PATCH 077/231] documented the Entry class --- lib/net/ldap.rb | 4 ++-- lib/net/ldap/entry.rb | 47 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 7a9af92..9e981fa 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -181,11 +181,11 @@ module Net # the attribute values stored in the directory for a particular entity. # #modify may add or delete attributes (which are lists of values) or it change attributes by # adding to or deleting from their values. - # There are three easier methods to modify an entry's attribute values: + # Net::LDAP provides three easier methods to modify an entry's attribute values: # #add_attribute, #replace_attribute, and #delete_attribute. # # ==== Delete - # #delete operation specifies an entity DN. If it succeeds, the entity and all its attributes + # #delete specifies an entity DN. If it succeeds, the entity and all its attributes # is removed from the directory. # # ==== Rename (or Modify RDN) diff --git a/lib/net/ldap/entry.rb b/lib/net/ldap/entry.rb index 3d89a56..598eb37 100644 --- a/lib/net/ldap/entry.rb +++ b/lib/net/ldap/entry.rb @@ -33,15 +33,43 @@ module Net class LDAP + # Objects of this class represent individual entries in an LDAP + # directory. User code generally does not instantiate this class. + # Net::LDAP#search provides objects of this class to user code, + # either as block parameters or as return values. + # + # In LDAP-land, an "entry" is a collection of attributes that are + # uniquely and globally identified by a DN ("Distinguished Name"). + # Attributes are identified by short, descriptive words or phrases. + # Although a directory is + # free to implement any attribute name, most of them follow rigorous + # standards so that the range of commonly-encountered attribute + # names is not large. + # + # An attribute name is case-insensitive. Most directories also + # restrict the range of characters allowed in attribute names. + # To simplify handling attribute names, Net::LDAP::Entry + # internally converts them to a standard format. Therefore, the + # methods which take attribute names can take Strings or Synmbols, + # and work correctly regardless of case or capitalization. + # + # An attribute consists of zero or more data items called + # values. An entry is the combination of a unique DN, a set of attribute + # names, and a (possibly-empty) array of values for each attribute. + # + # Class Net::LDAP::Entry provides convenience methods for dealing + # with LDAP entries. + # class Entry - def initialize dn = nil + # This constructor is not generally called by user code. + def initialize dn = nil # :nodoc: @myhash = Hash.new {|k,v| k[v] = [] } @myhash[:dn] = [dn] end - def []= name, value + def []= name, value # :nodoc: sym = name.to_s.downcase.intern @myhash[sym] = value end @@ -52,22 +80,33 @@ class LDAP # because this one and not the other one gets called # in formulations like entry["CN"] << cn. # - def [] name + def [] name # :nodoc: name = name.to_s.downcase.intern unless name.is_a?(Symbol) @myhash[name] end + # Returns the dn of the Entry as a String. def dn self[:dn][0] end + # Returns an array of the attribute names present in the Entry. def attribute_names @myhash.keys end + # Accesses each of the attributes present in the Entry. + # Calls a user-supplied block with each attribute in turn, + # passing two arguments to the block: a Symbol giving + # the name of the attribute, and a (possibly empty) + # Array of data values. + # def each if block_given? - attribute_names.each {|a| yield a, self[a] } + attribute_names.each {|a| + attr_name,values = a,self[a] + yield attr_name, values + } end end From 504aa8c71dd499db805a15dba44faa9d63c4ef50 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 2 May 2006 00:47:31 +0000 Subject: [PATCH 078/231] Bumped up version number to 0.0.2, added a missing status code, and tweaked docs. --- lib/net/ldap.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 9e981fa..063f866 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -238,7 +238,7 @@ module Net class LdapError < Exception; end - VERSION = "0.0.1" + VERSION = "0.0.2" SearchScope_BaseObject = 0 @@ -287,6 +287,8 @@ module Net 0 => "Success", 1 => "Operations Error", 2 => "Protocol Error", + 3 => "Time Limit Exceeded", + 4 => "Size Limit Exceeded", 16 => "No Such Attribute", 17 => "Undefined Attribute Type", 20 => "Attribute or Value Exists", @@ -306,7 +308,7 @@ module Net # # LDAP::result2string # - def LDAP::result2string code + def LDAP::result2string code # :nodoc: ResultStrings[code] || "unknown result (#{code})" end From 84e2ccc843f1fa14e37637adbed6668f1b22b58c Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 2 May 2006 06:55:46 +0000 Subject: [PATCH 079/231] Started adding support for LDAP rfc-2251 controls. --- lib/net/ldap.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 063f866..45e3d93 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -274,6 +274,9 @@ module Net 0 => :string, # password 1 => :string, # Kerberos v4 2 => :string, # Kerberos v5 + }, + :constructed => { + 0 => :array, # RFC-2251 Control } } } @@ -885,12 +888,27 @@ module Net search_filter.to_ber, search_attributes.to_ber_sequence ].to_ber_appsequence(3) + +=begin + controls = [ + [ + "1.2.840.113556.1.4.319".to_ber, + false.to_ber, + + [10.to_ber, "".to_ber].to_ber_sequence.to_s.to_ber + ].to_ber_sequence + + ].to_ber_contextspecific(0) + + pkt = [next_msgid.to_ber, request, controls].to_ber_sequence +=end pkt = [next_msgid.to_ber, request].to_ber_sequence @conn.write pkt result_code = 0 while (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) +#p be case pdu.app_tag when 4 # search-data yield( pdu.search_entry ) if block_given? From 91631f845def50500e44d5db6ae9a9f47475a847 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 2 May 2006 07:44:33 +0000 Subject: [PATCH 080/231] Supported RFC 2251 "controls" --- lib/net/ldap/pdu.rb | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/lib/net/ldap/pdu.rb b/lib/net/ldap/pdu.rb index aebe9fb..d80d8ea 100644 --- a/lib/net/ldap/pdu.rb +++ b/lib/net/ldap/pdu.rb @@ -50,8 +50,11 @@ class LdapPdu # # initialize # An LDAP PDU always looks like a BerSequence with - # two elements: an integer (message-id number), and + # at least two elements: an integer (message-id number), and # an application-specific sequence. + # Some LDAPv3 packets also include an optional + # third element, which is a sequence of "controls" + # (See RFC 2251, section 4.1.12). # 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. @@ -62,6 +65,10 @@ class LdapPdu # it remains to be seen whether there are servers out # there that will not work well with our approach. # + # Added a controls-processor to SearchResult. + # Didn't add it everywhere because it just _feels_ + # like it will need to be refactored. + # def initialize ber_object begin @msg_id = ber_object[0].to_i @@ -78,6 +85,7 @@ class LdapPdu parse_search_return ber_object[1] when SearchResult parse_ldap_result ber_object[1] + parse_controls(ber_object[2]) if ber_object[2] when ModifyResponse parse_ldap_result ber_object[1] when AddResponse @@ -102,7 +110,6 @@ class LdapPdu end - private # # parse_ldap_result @@ -146,6 +153,28 @@ class LdapPdu @search_attributes[seq[0].downcase.intern] = seq[1] } end + private :parse_ldap_result + + + # Per RFC 2251, an LDAP "control" is a sequence of tuples, each consisting + # of an OID, a boolean criticality flag defaulting FALSE, and an OPTIONAL + # Octet String. If only two fields are given, the second one may be + # either criticality or data, since criticality has a default value. + # Someday we may want to come back here and add support for some of + # more-widely used controls. RFC-2696 is a good example. + # + def parse_controls sequence + @ldap_controls = sequence.map do |control| + o = OpenStruct.new + o.oid,o.criticality,o.value = control[0],control[1],control[2] + if o.criticality and o.criticality.is_a?(String) + o.value = o.criticality + o.criticality = false + end + o + end + end + private :parse_controls end From b67a91edcab3a789812f2d450b6bd74db1d9b449 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 2 May 2006 09:26:55 +0000 Subject: [PATCH 081/231] implemented RFC 2696. --- lib/net/ldap.rb | 116 +++++++++++++++++++++++++++++--------------- lib/net/ldap/pdu.rb | 5 ++ 2 files changed, 83 insertions(+), 38 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 45e3d93..7826b0f 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -292,6 +292,7 @@ module Net 2 => "Protocol Error", 3 => "Time Limit Exceeded", 4 => "Size Limit Exceeded", + 12 => "Unavailable crtical extension", 16 => "No Such Attribute", 17 => "Undefined Attribute Type", 20 => "Attribute or Value Exists", @@ -308,6 +309,12 @@ module Net 68 => "Entry Already Exists" } + + module LdapControls + PagedResults = "1.2.840.113556.1.4.319" # Microsoft evil from RFC 2696 + end + + # # LDAP::result2string # @@ -869,6 +876,13 @@ module Net #-- # WARNING: this code substantially recapitulates the searchx method. # + # 02May06: Well, I added support for RFC-2696-style paged searches. + # This is used on all queries because the extension is marked non-critical. + # As far as I know, only A/D uses this, but it's required for A/D. Otherwise + # you won't get more than 1000 results back from a query. + # This implementation is kindof clunky and should probably be refactored. + # Also, is it my imagination, or are A/Ds the slowest directory servers ever??? + # def search args = {} search_filter = (args && args[:filter]) || Filter.eq( "objectclass", "*" ) search_base = (args && args[:base]) || "dc=example,dc=com" @@ -878,47 +892,73 @@ module Net scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree raise LdapError.new( "invalid search scope" ) unless SearchScopes.include?(scope) - request = [ - search_base.to_ber, - scope.to_ber_enumerated, - 0.to_ber_enumerated, - 0.to_ber, - 0.to_ber, - attributes_only.to_ber, - search_filter.to_ber, - search_attributes.to_ber_sequence - ].to_ber_appsequence(3) - -=begin - controls = [ - [ - "1.2.840.113556.1.4.319".to_ber, - false.to_ber, - - [10.to_ber, "".to_ber].to_ber_sequence.to_s.to_ber - ].to_ber_sequence - - ].to_ber_contextspecific(0) - - pkt = [next_msgid.to_ber, request, controls].to_ber_sequence -=end - pkt = [next_msgid.to_ber, request].to_ber_sequence - @conn.write pkt - + rfc2696_cookie = [739, ""] # size-limit is a funky number so we can distinguish it from errors. result_code = 0 - while (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) -#p be - case pdu.app_tag - when 4 # search-data - yield( pdu.search_entry ) if block_given? - when 5 # search-result - result_code = pdu.result_code - break - else - raise LdapError.new( "invalid response-type in search: #{pdu.app_tag}" ) + loop { + # should collect this into a private helper to clarify the structure + + request = [ + search_base.to_ber, + scope.to_ber_enumerated, + 0.to_ber_enumerated, + 0.to_ber, + 0.to_ber, + attributes_only.to_ber, + search_filter.to_ber, + search_attributes.to_ber_sequence + ].to_ber_appsequence(3) + + controls = [ + [ + LdapControls::PagedResults.to_ber, + false.to_ber, # criticality MUST be false to interoperate with normal LDAPs. + rfc2696_cookie.map{|v| v.to_ber}.to_ber_sequence.to_s.to_ber + ].to_ber_sequence + ].to_ber_contextspecific(0) + + pkt = [next_msgid.to_ber, request, controls].to_ber_sequence + @conn.write pkt + + result_code = 0 + controls = [] + + while (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) + case pdu.app_tag + when 4 # search-data + yield( pdu.search_entry ) if block_given? + when 5 # search-result + result_code = pdu.result_code + controls = pdu.result_controls + break + else + raise LdapError.new( "invalid response-type in search: #{pdu.app_tag}" ) + end end - end + + # When we get here, we have seen a type-5 response. + # If there is no error AND there is an RFC-2696 cookie, + # then query again for the next page of results. + # If not, we're done. + # Don't screw this up or we'll break every search we do. + more_pages = false + if result_code == 0 and controls + controls.each do |c| + if c.oid == LdapControls::PagedResults + more_pages = false # just in case some bogus server sends us >1 of these. + if c.value and c.value.length > 0 + cookie = c.value.read_ber[1] + if cookie and cookie.length > 0 + rfc2696_cookie[1] = cookie + more_pages = true + end + end + end + end + end + + break unless more_pages + } # loop result_code end diff --git a/lib/net/ldap/pdu.rb b/lib/net/ldap/pdu.rb index d80d8ea..6f3eac7 100644 --- a/lib/net/ldap/pdu.rb +++ b/lib/net/ldap/pdu.rb @@ -109,6 +109,11 @@ class LdapPdu @ldap_result and @ldap_result[code] end + # Return RFC-2251 Controls if any. + # Messy. Does this functionality belong somewhere else? + def result_controls + @ldap_controls || [] + end # From 0fc833e417a5bab0ea98643d25a4709a0ee9a1d0 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 3 May 2006 05:38:58 +0000 Subject: [PATCH 082/231] fixed bug in RDoc instantiation --- pre-setup.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre-setup.rb b/pre-setup.rb index 1bfc1e4..61c38e3 100644 --- a/pre-setup.rb +++ b/pre-setup.rb @@ -11,7 +11,7 @@ rescue Exception => e end def build_ri(files) - RDoc::RDoc.new(["--ri-site", "--merge"] + files) + RDoc::RDoc.new.document(["--ri-site", "--merge"] + files) rescue RDoc::RDocError => e $stderr.puts e.message rescue Exception => e From 6205656846eaddde0a0e680d4549820d719e852b Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 3 May 2006 07:22:14 +0000 Subject: [PATCH 083/231] Version 0.0.2 release notes and announcement --- ChangeLog | 5 +++++ Release-Announcement | 30 +++++++++++------------------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/ChangeLog b/ChangeLog index fcde05e..bf451cc 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,10 @@ = Net::LDAP Changelog +== Net::LDAP 0.0.2: May 3, 2006 +* Fixed malformation in distro tarball and gem. +* Improved documentation. +* Supported "paged search control." + == Net::LDAP 0.0.1: May 1, 2006 * Initial release. * Client functionality is near-complete, although the APIs diff --git a/Release-Announcement b/Release-Announcement index c839db2..e5928cd 100644 --- a/Release-Announcement +++ b/Release-Announcement @@ -1,15 +1,17 @@ -We're pleased to announce the first release of Net::LDAP, the first +We're pleased to announce version 0.0.2 of Net::LDAP, the first pure-Ruby LDAP library. Net::LDAP intends to be a feature-complete LDAP client which can access as much as possible of the functionality of the most-used LDAP server implementations. This library does not wrap any existing native-code LDAP libraries, creates no Ruby extensions, and has no dependencies external to Ruby. -Net::LDAP includes a full implementation of the LDAP wire-line -protocol so it can also be used in LDAP server implementations. +Version 0.0.2 includes an implementation of the "paged search +control" to enable queries of A/D result sets > 1000 entries. +It also fills in some holes in the documentation. -Thanks for Austin Ziegler for invaluable help in reviewing the -implementation and providing the release structure. +And finally we fixed the annoying problem that the 0.0.1 gem +and tarball had trailing garbage. Thanks to Austin for helping +track that down. = What is Net::LDAP for Ruby? This library provides a pure-Ruby implementation of an LDAP client. @@ -72,18 +74,8 @@ as ruby-net-ldap-0.0.1.gem from the usual sources. p ldap.get_operation_result -== Net::LDAP 0.0.1: May 1, 2006 -* Initial release. -* Client functionality is near-complete, although the APIs - are not guaranteed and may change depending on feedback - from the community. -* We're internally working on a Ruby-based implementation - of a full-featured, production-quality LDAP server, - which will leverage the underlying LDAP and BER functionality - in Net::LDAP. -* Please tell us if you would be interested in seeing a public - release of the LDAP server. -* Grateful acknowledgement to Austin Ziegler, who reviewed - this code and provided the release framework, including - minitar. +== Net::LDAP 0.0.2: May 3, 2006 +* Fixed malformation in distro tarball and gem. +* Improved documentation. +* Supported "paged search control." From 0719bed7f6c550ca729867228f0e48cc33fda494 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Thu, 4 May 2006 11:52:22 +0000 Subject: [PATCH 084/231] Added convenience methods and accessors so that authentication and seach parameters can be specified more easily than by passing them in an argument hash. --- lib/net/ldap.rb | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 7826b0f..21a9c88 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -284,6 +284,7 @@ module Net DefaultHost = "127.0.0.1" DefaultPort = 389 DefaultAuth = {:method => :anonymous} + DefaultTreebase = "dc=com" ResultStrings = { @@ -322,6 +323,10 @@ module Net ResultStrings[code] || "unknown result (#{code})" end + + attr_accessor :host, :port, :base + + # Instantiate an object of type Net::LDAP to perform directory operations. # This constructor takes a Hash containing arguments. The following arguments # are supported: @@ -340,6 +345,7 @@ module Net @port = args[:port] || DefaultPort @verbose = false # Make this configurable with a switch on the class. @auth = args[:auth] || DefaultAuth + @base = args[:base] || DefaultTreebase # This variable is only set when we are created with LDAP::open. # All of our internal methods will connect using it, or else @@ -347,6 +353,22 @@ module Net @open_connection = nil end + # Convenient method to specify your authentication to the LDAP + # server. Currently supports simple authentication requiring + # a username and password. Observe that on most LDAP servers, + # including A/D, the username is a complete DN. + # require 'net/ldap' + # + # ldap = Net::LDAP.new + # ldap.host = server_ip_address + # ldap.authenticate "cn=Your Username,cn=Users,dc=example,dc=com", "your_psw" + # + def authenticate username, password + @auth = {:method => :simple, :username => username, :password => password} + end + + alias_method :auth, :authenticate + # #open takes the same parameters as #new. #open makes a network connection to the # LDAP server and then passes a newly-created Net::LDAP object to the caller-supplied block. # Within the block, you can call any of the instance methods of Net::LDAP to @@ -515,7 +537,8 @@ module Net # that the caller can set to suppress the return of a result set, # if he's planning to process every entry as it comes from the server. # - def search args + def search args = {} + args[:base] ||= @base result_set = (args and args[:return_result] == false) ? nil : {} if @open_connection From c30a198a29d334abd7b93e3ae52184738eb53a23 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Thu, 4 May 2006 11:58:10 +0000 Subject: [PATCH 085/231] Provisionally removed an obsolete way of returning search results. The code is commented out so we can bring it back if the removal causes regressions. --- lib/net/ldap/pdu.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/net/ldap/pdu.rb b/lib/net/ldap/pdu.rb index 6f3eac7..3e2528e 100644 --- a/lib/net/ldap/pdu.rb +++ b/lib/net/ldap/pdu.rb @@ -148,14 +148,16 @@ class LdapPdu # we also return @search_entry, which is an LDAP::Entry object. # If that works out well, then we'll remove the first two. # + # Provisionally removed obsolete search_attributes and search_dn, 04May06. + # def parse_search_return sequence sequence.length >= 2 or raise LdapPduError @search_entry = LDAP::Entry.new( sequence[0] ) - @search_dn = sequence[0] - @search_attributes = {} + #@search_dn = sequence[0] + #@search_attributes = {} sequence[1].each {|seq| @search_entry[seq[0]] = seq[1] - @search_attributes[seq[0].downcase.intern] = seq[1] + #@search_attributes[seq[0].downcase.intern] = seq[1] } end private :parse_ldap_result From 271c685a6716e50b7badcdf29aaf208d3c61da5d Mon Sep 17 00:00:00 2001 From: blackhedd Date: Thu, 4 May 2006 12:06:34 +0000 Subject: [PATCH 086/231] Enabled the left side of a filter-specification to be a Symbol. Previously it had to be a String. --- lib/net/ldap/filter.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/net/ldap/filter.rb b/lib/net/ldap/filter.rb index 66848a4..84e8c7b 100644 --- a/lib/net/ldap/filter.rb +++ b/lib/net/ldap/filter.rb @@ -180,7 +180,7 @@ class Filter case @op when :eq if @right == "*" # present - @left.to_ber_contextspecific 7 + @left.to_s.to_ber_contextspecific 7 elsif @right =~ /[\*]/ #substring ary = @right.split( /[\*]+/ ) final_star = @right =~ /[\*]$/ @@ -198,9 +198,9 @@ class Filter unless final_star seq << ary.shift.to_ber_contextspecific(2) end - [@left.to_ber, seq.to_ber].to_ber_contextspecific 4 + [@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific 4 else #equality - [@left.to_ber, @right.to_ber].to_ber_contextspecific 3 + [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 3 end when :and ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten From c7df586c9f00fb91ce7e418e22d3a2a3dfa963ba Mon Sep 17 00:00:00 2001 From: blackhedd Date: Thu, 4 May 2006 12:19:41 +0000 Subject: [PATCH 087/231] Added a method_missing implementation which allows callers to access attributes simply by giving their name. --- lib/net/ldap/entry.rb | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/net/ldap/entry.rb b/lib/net/ldap/entry.rb index 598eb37..fb289ea 100644 --- a/lib/net/ldap/entry.rb +++ b/lib/net/ldap/entry.rb @@ -60,6 +60,11 @@ class LDAP # Class Net::LDAP::Entry provides convenience methods for dealing # with LDAP entries. # + #-- + # Ugly problem to fix someday: We key off the internal hash with + # a canonical form of the attribute name: convert to a string, + # downcase, then take the symbol. Unfortunately we do this in + # at least three places. Should do it in ONE place. class Entry # This constructor is not generally called by user code. @@ -76,7 +81,7 @@ class LDAP #-- - # We have to deal with this one as we do []= + # We have to deal with this one as we do with []= # because this one and not the other one gets called # in formulations like entry["CN"] << cn. # @@ -112,6 +117,16 @@ class LDAP alias_method :each_attribute, :each + + def method_missing *args, &block # :nodoc: + s = args[0].to_s.downcase.intern + if attribute_names.include?(s) + self[s] + else + raise NoMethodError.new( "undefined method '#{s}'" ) + end + end + end # class Entry From 013982e6728e85bb958337b9a667b798831c60d5 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Thu, 4 May 2006 12:24:57 +0000 Subject: [PATCH 088/231] Small performance improvement --- lib/net/ldap/entry.rb | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/net/ldap/entry.rb b/lib/net/ldap/entry.rb index fb289ea..be7798b 100644 --- a/lib/net/ldap/entry.rb +++ b/lib/net/ldap/entry.rb @@ -118,8 +118,19 @@ class LDAP alias_method :each_attribute, :each + #-- + # Convenience method to convert unknown method names + # to attribute references. Of course the method name + # comes to us as a symbol, so let's save a little time + # and not bother with the to_s.downcase two-step. + # Of course that means that a method name like mAIL + # won't work, but we shouldn't be encouraging that + # kind of bad behavior in the first place. + # Maybe we should thow something if the caller sends + # arguments or a block... + # def method_missing *args, &block # :nodoc: - s = args[0].to_s.downcase.intern + s = args[0] if attribute_names.include?(s) self[s] else From 999f493863e6000d05d455bd21c73a61a119bd66 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Thu, 4 May 2006 12:30:29 +0000 Subject: [PATCH 089/231] Added some documentation. --- lib/net/ldap/entry.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/net/ldap/entry.rb b/lib/net/ldap/entry.rb index be7798b..f5354de 100644 --- a/lib/net/ldap/entry.rb +++ b/lib/net/ldap/entry.rb @@ -59,6 +59,16 @@ class LDAP # # Class Net::LDAP::Entry provides convenience methods for dealing # with LDAP entries. + # In addition to the methods documented below, you may access individual + # attributes of an entry simply by giving the attribute name as + # the name of a method call. For example: + # ldap.search( ... ) do |entry| + # puts "Common name: #{entry.cn}" + # puts "Email addresses:" + # entry.mail.each {|ma| puts ma} + # end + # If you use this technique to access an attribute that is not present + # in a particular Entry object, a NoMethodError exception will be raised. # #-- # Ugly problem to fix someday: We key off the internal hash with From 6e5aba6530ae6d5334ac9e3d403e779442a42b12 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Thu, 4 May 2006 12:39:28 +0000 Subject: [PATCH 090/231] commented out the searchx methods. I don't think we'll be needing them. --- lib/net/ldap.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 21a9c88..dbb4062 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -456,6 +456,7 @@ module Net # Note that in the standalone case, we're permitting the caller # to modify the auth parms. # +=begin def searchx args if @open_connection @result = @open_connection.searchx( args ) {|values| @@ -474,6 +475,7 @@ module Net @result == 0 end +=end # Searches the LDAP directory for directory entries. # Takes a hash argument with parameters. Supported parameters include: @@ -998,6 +1000,7 @@ module Net #-- # WARNING: this code substantially recapitulates the search method. # +=begin def searchx args search_filter = (args && args[:filter]) || Filter.eq( "objectclass", "*" ) search_base = (args && args[:base]) || "dc=example,dc=com" @@ -1033,6 +1036,8 @@ module Net result_code end +=end + #-- # modify From d4c19eea44091f4ea313a57112f3c666d94b2052 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Thu, 4 May 2006 13:08:36 +0000 Subject: [PATCH 091/231] Reinterpreted search result sets as Arrays rather than Hashes. WARNING WARNING WARNING: This change breaks existing code. --- lib/net/ldap.rb | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index dbb4062..53a07ae 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -492,6 +492,8 @@ module Net # be called 1000 times. If the search returns no entries, the block will # not be called. # + #-- + # ORIGINAL TEXT, replaced 04May06. # #search returns either a result-set or a boolean, depending on the # value of the :return_result argument. The default behavior is to return # a result set, which is a hash. Each key in the hash is a string specifying @@ -499,6 +501,13 @@ module Net # If you request a result set and #search fails with an error, it will return nil. # Call #get_operation_result to get the error information returned by # the LDAP server. + #++ + # #search returns either a result-set or a boolean, depending on the + # value of the :return_result argument. The default behavior is to return + # a result set, which is an Array of objects of class Net::LDAP::Entry. + # If you request a result set and #search fails with an error, it will return nil. + # Call #get_operation_result to get the error information returned by + # the LDAP server. # # When :return_result => false, #search will # return only a Boolean, to indicate whether the operation succeeded. This can improve performance @@ -539,13 +548,20 @@ module Net # that the caller can set to suppress the return of a result set, # if he's planning to process every entry as it comes from the server. # + # REINTERPRETED the result set, 04May06. Originally this was a hash + # of entries keyed by DNs. But let's get away from making users + # handle DNs. Change it to a plain array. Eventually we may + # want to return a Dataset object that delegates to an internal + # array, so we can provide sort methods and what-not. + # def search args = {} args[:base] ||= @base - result_set = (args and args[:return_result] == false) ? nil : {} + result_set = (args and args[:return_result] == false) ? nil : [] if @open_connection @result = @open_connection.search( args ) {|entry| - result_set[entry.dn] = entry if result_set + #result_set[entry.dn] = entry if result_set + result_set << entry if result_set yield( entry ) if block_given? } else @@ -553,7 +569,8 @@ module Net conn = Connection.new( :host => @host, :port => @port ) if (@result = conn.bind( args[:auth] || @auth )) == 0 @result = conn.search( args ) {|entry| - (result_set[entry.dn] = entry) if result_set + #(result_set[entry.dn] = entry) if result_set + result_set << entry if result_set yield( entry ) if block_given? } end From 39ec12afe2566ae4ce47f80a528e0b0b4831b624 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 9 May 2006 23:01:30 +0000 Subject: [PATCH 092/231] supported Proc objects as passwords. --- lib/net/ldap.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 53a07ae..3d3c71d 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -335,6 +335,7 @@ module Net # * :auth => a Hash containing authorization parameters. Currently supported values include: # {:method => :anonymous} and # {:method => :simple, :username => your_user_name, :password => your_password } + # The password parameter may be a Proc that returns a String. # # Instantiating a Net::LDAP object does not result in network traffic to # the LDAP server. It simply stores the connection and binding parameters in the @@ -347,6 +348,10 @@ module Net @auth = args[:auth] || DefaultAuth @base = args[:base] || DefaultTreebase + if pr = @auth[:password] and pr.respond_to?(:call) + @auth[:password] = pr.call + end + # This variable is only set when we are created with LDAP::open. # All of our internal methods will connect using it, or else # they will create their own. @@ -357,6 +362,7 @@ module Net # server. Currently supports simple authentication requiring # a username and password. Observe that on most LDAP servers, # including A/D, the username is a complete DN. + # The password argument may be a Proc that returns a string. # require 'net/ldap' # # ldap = Net::LDAP.new @@ -364,6 +370,7 @@ module Net # ldap.authenticate "cn=Your Username,cn=Users,dc=example,dc=com", "your_psw" # def authenticate username, password + password = password.call if password.respond_to?(:call) @auth = {:method => :simple, :username => username, :password => password} end From fe40fa1ef9d168e03624acefcd5d72c7dd588f0b Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 15 May 2006 18:33:01 +0000 Subject: [PATCH 093/231] improved behavior of setting attributes in entries --- lib/net/ldap/entry.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/net/ldap/entry.rb b/lib/net/ldap/entry.rb index f5354de..c74fa1d 100644 --- a/lib/net/ldap/entry.rb +++ b/lib/net/ldap/entry.rb @@ -143,11 +143,19 @@ class LDAP s = args[0] if attribute_names.include?(s) self[s] + elsif s.to_s[-1] == 61 and s.to_s.length > 1 + value = args[1] or raise RuntimeError.new( "unable to set value" ) + value = [value] unless value.is_a?(Array) + name = s.to_s[0..-2].intern + self[name] = value else raise NoMethodError.new( "undefined method '#{s}'" ) end end + def write + end + end # class Entry From 8b7b74ac16f45459f4e26dfff7e74938a8748d36 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 15 May 2006 18:36:24 +0000 Subject: [PATCH 094/231] Added test module for filters --- tests/testem.rb | 1 + tests/testfilter.rb | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 tests/testfilter.rb diff --git a/tests/testem.rb b/tests/testem.rb index 64d8613..4b0f73e 100644 --- a/tests/testem.rb +++ b/tests/testem.rb @@ -7,5 +7,6 @@ require 'tests/testber' require 'tests/testldif' require 'tests/testldap' require 'tests/testpsw' +require 'tests/testfilter' diff --git a/tests/testfilter.rb b/tests/testfilter.rb new file mode 100644 index 0000000..f1ca616 --- /dev/null +++ b/tests/testfilter.rb @@ -0,0 +1,26 @@ +# $Id$ +# +# + +require 'test/unit' + +$:.unshift "lib" + +require 'net/ldap' + + +class TestFilter < Test::Unit::TestCase + + def setup + end + + + def teardown + end + + def test_rfc_2254 + end + + +end + From 7de0147b21e206bac1493ebb4abdf381052ff989 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 15 May 2006 20:03:56 +0000 Subject: [PATCH 095/231] Added Net::LDAP::Filter.from_rfc2254 --- lib/net/ldap/filter.rb | 91 ++++++++++++++++++++++++++++++++++++++++++ tests/testfilter.rb | 11 +++++ 2 files changed, 102 insertions(+) diff --git a/lib/net/ldap/filter.rb b/lib/net/ldap/filter.rb index 84e8c7b..25bf549 100644 --- a/lib/net/ldap/filter.rb +++ b/lib/net/ldap/filter.rb @@ -270,9 +270,100 @@ class Filter end end + # Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254) + # to a Net::LDAP::Filter. + def self.from_rfc2254 str + FilterParser.new(str).filter + end end # class Net::LDAP::Filter + +class FilterParser #:nodoc: + + attr_reader :filter + + def initialize str + require 'strscan' + @filter = parse( StringScanner.new( str )) or raise Net::LDAP::LdapError.new( "invalid filter syntax" ) + end + + def parse scanner + parse_filter_branch(scanner) or parse_paren_expression(scanner) + end + + def parse_paren_expression scanner + if scanner.scan /\s*\(\s*/ + b = if scanner.scan /\s*\&\s*/ + a = nil + branches = [] + while br = parse_paren_expression(scanner) + branches << br + end + if branches.length >= 2 + a = branches.shift + while branches.length > 0 + a = a & branches.shift + end + a + end + elsif scanner.scan /\s*\|\s*/ + # TODO: DRY! + a = nil + branches = [] + while br = parse_paren_expression(scanner) + branches << br + end + if branches.length >= 2 + a = branches.shift + while branches.length > 0 + a = a | branches.shift + end + a + end + elsif scanner.scan /\s*\!\s*/ + br = parse_paren_expression(scanner) + if br + ~ br + end + else + parse_filter_branch( scanner ) + end + + if b and scanner.scan( /\s*\)\s*/ ) + b + end + end + end + + def parse_filter_branch scanner + scanner.scan /\s*/ + if token = scanner.scan( /[\w\-_]+/ ) + scanner.scan /\s*/ + if op = scanner.scan( /\=|\<\=|\<|\>\=|\>|\!\=/ ) + scanner.scan /\s*/ + if value = scanner.scan( /[\w\*]+/ ) + case op + when "=" + Filter.eq( token, value ) + when "!=" + Filter.ne( token, value ) + when "<" + Filter.lt( token, value ) + when "<=" + Filter.le( token, value ) + when ">" + Filter.gt( token, value ) + when ">=" + Filter.ge( token, value ) + end + end + end + end + end + +end # class Net::LDAP::FilterParser + end # class Net::LDAP end # module Net diff --git a/tests/testfilter.rb b/tests/testfilter.rb index f1ca616..4339f26 100644 --- a/tests/testfilter.rb +++ b/tests/testfilter.rb @@ -19,6 +19,17 @@ class TestFilter < Test::Unit::TestCase end def test_rfc_2254 + p Net::LDAP::Filter.from_rfc2254( " ( uid=george* ) " ) + p Net::LDAP::Filter.from_rfc2254( "uid!=george*" ) + p Net::LDAP::Filter.from_rfc2254( "uidgeorge*" ) + p Net::LDAP::Filter.from_rfc2254( "uid>=george*" ) + p Net::LDAP::Filter.from_rfc2254( "uid!=george*" ) + + p Net::LDAP::Filter.from_rfc2254( "(& (uid!=george* ) (mail=*))" ) + p Net::LDAP::Filter.from_rfc2254( "(| (uid!=george* ) (mail=*))" ) + p Net::LDAP::Filter.from_rfc2254( "(! (mail=*))" ) end From 241d9b8fb3da144767177ca76519a42977c32244 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Thu, 18 May 2006 03:52:38 +0000 Subject: [PATCH 096/231] Took a suggestion from Andre Nathan (andre@digirati.com.br) to change the behavior of Net::LDAP::Filter#method_missing to allow case-insensitive attribute names. --- lib/net/ldap/entry.rb | 4 ++-- lib/net/ldap/filter.rb | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/net/ldap/entry.rb b/lib/net/ldap/entry.rb index c74fa1d..625c5db 100644 --- a/lib/net/ldap/entry.rb +++ b/lib/net/ldap/entry.rb @@ -50,7 +50,7 @@ class LDAP # restrict the range of characters allowed in attribute names. # To simplify handling attribute names, Net::LDAP::Entry # internally converts them to a standard format. Therefore, the - # methods which take attribute names can take Strings or Synmbols, + # methods which take attribute names can take Strings or Symbols, # and work correctly regardless of case or capitalization. # # An attribute consists of zero or more data items called @@ -140,7 +140,7 @@ class LDAP # arguments or a block... # def method_missing *args, &block # :nodoc: - s = args[0] + s = args[0].to_s.downcase.intern if attribute_names.include?(s) self[s] elsif s.to_s[-1] == 61 and s.to_s.length > 1 diff --git a/lib/net/ldap/filter.rb b/lib/net/ldap/filter.rb index 25bf549..5b48e1b 100644 --- a/lib/net/ldap/filter.rb +++ b/lib/net/ldap/filter.rb @@ -272,8 +272,14 @@ class Filter # Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254) # to a Net::LDAP::Filter. - def self.from_rfc2254 str - FilterParser.new(str).filter + def self.construct ldap_filter_string + FilterParser.new(ldap_filter_string).filter + end + + # Synonym for #construct. + # to a Net::LDAP::Filter. + def self.from_rfc2254 ldap_filter_string + construct ldap_filter_string end end # class Net::LDAP::Filter From c9f5b6a689968134a84e32d68052f9a545cdf9b7 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Thu, 18 May 2006 03:54:11 +0000 Subject: [PATCH 097/231] Notes --- ChangeLog | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ChangeLog b/ChangeLog index bf451cc..a550f4c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -4,6 +4,9 @@ * Fixed malformation in distro tarball and gem. * Improved documentation. * Supported "paged search control." +* Added a range of API improvements. +* Thanks to Andre Nathan, andre@digirati.com.br, for valuable + suggestions. == Net::LDAP 0.0.1: May 1, 2006 * Initial release. From a11e121e54f580de745a4ebb61821e5aa904c62f Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 24 May 2006 23:33:17 +0000 Subject: [PATCH 098/231] Allowed dots in filter branches. Patch provided by Andre Nathan. --- lib/net/ldap/filter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/net/ldap/filter.rb b/lib/net/ldap/filter.rb index 5b48e1b..14353f3 100644 --- a/lib/net/ldap/filter.rb +++ b/lib/net/ldap/filter.rb @@ -348,7 +348,7 @@ class FilterParser #:nodoc: scanner.scan /\s*/ if op = scanner.scan( /\=|\<\=|\<|\>\=|\>|\!\=/ ) scanner.scan /\s*/ - if value = scanner.scan( /[\w\*]+/ ) + if value = scanner.scan( /[\w\*\.]+/ ) case op when "=" Filter.eq( token, value ) From f3e05db9d37102c48f26979330920d3f5945c264 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 31 May 2006 15:55:16 +0000 Subject: [PATCH 099/231] added support for search-result referrals. --- lib/net/ldap.rb | 95 ++++++--------------------------------------- lib/net/ldap/pdu.rb | 16 +++++++- 2 files changed, 26 insertions(+), 85 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 3d3c71d..1170c97 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -266,6 +266,7 @@ module Net 14 => :array, # CompareRequest 15 => :array, # CompareResponse 16 => :array, # AbandonRequest + 19 => :array, # SearchResultReferral 24 => :array, # Unsolicited Notification } }, @@ -450,40 +451,6 @@ module Net end - # DEPRECATED. Performs an LDAP search, waits for the operation to complete, and - # passes a result set to the caller-supplied block. - #-- - # If an open call is in progress (@open_connection will be non-nil), - # then ASSUME a bind has been performed and accepted, and just - # execute the search. - # If @open_connection is nil, then we have to connect, bind, - # search, and then disconnect. (The disconnect is not strictly - # necessary but it's friendlier to the network to do it here - # rather than waiting for Ruby's GC.) - # Note that in the standalone case, we're permitting the caller - # to modify the auth parms. - # -=begin - def searchx args - if @open_connection - @result = @open_connection.searchx( args ) {|values| - yield( values ) if block_given? - } - else - @result = 0 - conn = Connection.new( :host => @host, :port => @port ) - if (@result = conn.bind( args[:auth] || @auth )) == 0 - @result = conn.searchx( args ) {|values| - yield( values ) if block_given? - } - end - conn.close - end - - @result == 0 - end -=end - # Searches the LDAP directory for directory entries. # Takes a hash argument with parameters. Supported parameters include: # * :base (a string specifying the tree-base for the search); @@ -567,7 +534,6 @@ module Net if @open_connection @result = @open_connection.search( args ) {|entry| - #result_set[entry.dn] = entry if result_set result_set << entry if result_set yield( entry ) if block_given? } @@ -576,7 +542,6 @@ module Net conn = Connection.new( :host => @host, :port => @port ) if (@result = conn.bind( args[:auth] || @auth )) == 0 @result = conn.search( args ) {|entry| - #(result_set[entry.dn] = entry) if result_set result_set << entry if result_set yield( entry ) if block_given? } @@ -936,6 +901,7 @@ module Net search_filter = (args && args[:filter]) || Filter.eq( "objectclass", "*" ) search_base = (args && args[:base]) || "dc=example,dc=com" search_attributes = ((args && args[:attributes]) || []).map {|attr| attr.to_s.to_ber} + return_referrals = args && args[:return_referrals] == true attributes_only = (args and args[:attributes_only] == true) scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree @@ -976,6 +942,15 @@ module Net case pdu.app_tag when 4 # search-data yield( pdu.search_entry ) if block_given? + when 19 # search-referral + if return_referrals + if block_given? + se = Net::LDAP::Entry.new + se[:search_referrals] = (pdu.search_referrals || []) + yield se + end + end + #p pdu.referrals when 5 # search-result result_code = pdu.result_code controls = pdu.result_controls @@ -1013,54 +988,6 @@ module Net end - #-- - # searchx - # Original implementation, this doesn't return until all data have been - # received from the server. - # TODO, certain search parameters are hardcoded. - # TODO, if we mis-parse the server results or the results are wrong, we can block - # forever. That's because we keep reading results until we get a type-5 packet, - # which might never come. We need to support the time-limit in the protocol. - #-- - # WARNING: this code substantially recapitulates the search method. - # -=begin - def searchx args - search_filter = (args && args[:filter]) || Filter.eq( "objectclass", "*" ) - search_base = (args && args[:base]) || "dc=example,dc=com" - search_attributes = ((args && args[:attributes]) || []).map {|attr| attr.to_s.to_ber} - request = [ - search_base.to_ber, - 2.to_ber_enumerated, - 0.to_ber_enumerated, - 0.to_ber, - 0.to_ber, - false.to_ber, - search_filter.to_ber, - search_attributes.to_ber_sequence - ].to_ber_appsequence(3) - pkt = [next_msgid.to_ber, request].to_ber_sequence - @conn.write pkt - - search_results = {} - result_code = 0 - - while (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) - case pdu.app_tag - when 4 # search-data - search_results [pdu.search_dn] = pdu.search_attributes - when 5 # search-result - result_code = pdu.result_code - block_given? and yield( search_results ) - break - else - raise LdapError.new( "invalid response-type in search: #{pdu.app_tag}" ) - end - end - - result_code - end -=end #-- diff --git a/lib/net/ldap/pdu.rb b/lib/net/ldap/pdu.rb index 3e2528e..f195e29 100644 --- a/lib/net/ldap/pdu.rb +++ b/lib/net/ldap/pdu.rb @@ -43,9 +43,11 @@ class LdapPdu AddResponse = 9 DeleteResponse = 11 ModifyRDNResponse = 13 + SearchResultReferral = 19 attr_reader :msg_id, :app_tag attr_reader :search_dn, :search_attributes, :search_entry + attr_reader :search_referrals # # initialize @@ -83,6 +85,8 @@ class LdapPdu parse_ldap_result ber_object[1] when SearchReturnedData parse_search_return ber_object[1] + when SearchResultReferral + parse_search_referral ber_object[1] when SearchResult parse_ldap_result ber_object[1] parse_controls(ber_object[2]) if ber_object[2] @@ -123,6 +127,7 @@ class LdapPdu sequence.length >= 3 or raise LdapPduError @ldap_result = {:resultCode => sequence[0], :matchedDN => sequence[1], :errorMessage => sequence[2]} end + private :parse_ldap_result # # parse_search_return @@ -160,7 +165,16 @@ class LdapPdu #@search_attributes[seq[0].downcase.intern] = seq[1] } end - private :parse_ldap_result + + # + # A search referral is a sequence of one or more LDAP URIs. + # Any number of search-referral replies can be returned by the server, interspersed + # with normal replies in any order. + # Until I can think of a better way to do this, we'll return the referrals as an array. + # It'll be up to higher-level handlers to expose something reasonable to the client. + def parse_search_referral uris + @search_referrals = uris + end # Per RFC 2251, an LDAP "control" is a sequence of tuples, each consisting From 50541f653d396b3df5940559c08b621fcad8c035 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 9 Jun 2006 21:43:40 +0000 Subject: [PATCH 100/231] supported filter types greaterorequal and lessorequal --- lib/net/ldap/filter.rb | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/net/ldap/filter.rb b/lib/net/ldap/filter.rb index 14353f3..8c672a4 100644 --- a/lib/net/ldap/filter.rb +++ b/lib/net/ldap/filter.rb @@ -75,11 +75,13 @@ class Filter # This example selects any entry with a mail value containing # the substring "anderson": # f = Net::LDAP::Filter.eq( "mail", "*anderson*" ) + #-- + # Removed gt and lt. They ain't in the standard! # def Filter::eq attribute, value; Filter.new :eq, attribute, value; end def Filter::ne attribute, value; Filter.new :ne, attribute, value; end - def Filter::gt attribute, value; Filter.new :gt, attribute, value; end - def Filter::lt attribute, value; Filter.new :lt, attribute, value; end + #def Filter::gt attribute, value; Filter.new :gt, attribute, value; end + #def Filter::lt attribute, value; Filter.new :lt, attribute, value; end def Filter::ge attribute, value; Filter.new :ge, attribute, value; end def Filter::le attribute, value; Filter.new :le, attribute, value; end @@ -110,6 +112,7 @@ class Filter # #-- # This operator can't be !, evidently. Try it. + # Removed GT and LT. They're not in the RFC. def ~@; Filter.new :not, self, nil; end @@ -119,10 +122,10 @@ class Filter "(!(#{@left}=#{@right}))" when :eq "(#{@left}=#{@right})" - when :gt - "#{@left}>#{@right}" - when :lt - "#{@left}<#{@right}" + #when :gt + # "#{@left}>#{@right}" + #when :lt + # "#{@left}<#{@right}" when :ge "#{@left}>=#{@right}" when :le @@ -202,6 +205,10 @@ class Filter else #equality [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 3 end + when :ge + [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 5 + when :le + [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 6 when :and ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten ary.map {|a| a.to_ber}.to_ber_contextspecific( 0 ) From bc62156f123ebec5658f6a169345bf93143f881f Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 9 Jun 2006 22:09:01 +0000 Subject: [PATCH 101/231] change notes --- ChangeLog | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog b/ChangeLog index a550f4c..1b0eae6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,8 @@ * Added a range of API improvements. * Thanks to Andre Nathan, andre@digirati.com.br, for valuable suggestions. +* Added support for LE and GE search filters. +* Added support for Search referrals. == Net::LDAP 0.0.1: May 1, 2006 * Initial release. From 5fea59a42aa8284a16d4e76cb71dd435773221d1 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 10 Jun 2006 03:25:08 +0000 Subject: [PATCH 102/231] fixed a regression caused by the introduction of RFC-2696 paged-result controls. These broke openLDAP versions 2.2.x and greater because we were using a page size of 739, while openLDAP doesn't like anything bigger than 126. --- lib/net/ldap.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 1170c97..1eaa8a4 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -907,7 +907,11 @@ module Net scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree raise LdapError.new( "invalid search scope" ) unless SearchScopes.include?(scope) - rfc2696_cookie = [739, ""] # size-limit is a funky number so we can distinguish it from errors. + # An interesting value for the size limit would be close to A/D's built-in + # page limit of 1000 records, but openLDAP newer than version 2.2.0 chokes + # on anything bigger than 126. You get a silent error that is easily visible + # by running slapd in debug mode. Go figure. + rfc2696_cookie = [126, ""] result_code = 0 loop { From eb4a7737a99412676f4734d22b46762b2c108246 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 10 Jun 2006 03:28:43 +0000 Subject: [PATCH 103/231] reported fix of RFC-2696 regression --- ChangeLog | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ChangeLog b/ChangeLog index 1b0eae6..9ccd1a4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -9,6 +9,9 @@ suggestions. * Added support for LE and GE search filters. * Added support for Search referrals. +* Fixed a regression with openldap 2.2.x and higher caused + by the introduction of RFC-2696 controls. Thanks to Andre + Nathan for reporting the problem. == Net::LDAP 0.0.1: May 1, 2006 * Initial release. From 2baf207486ace035141f9db9d8333e3938ce7290 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 23 Jun 2006 12:07:23 +0000 Subject: [PATCH 104/231] Supported constructed, context-specific tag 3 (search referral). This was missing from the earlier work that supported referrals. --- lib/net/ldap.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 1eaa8a4..af9ac98 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -278,6 +278,7 @@ module Net }, :constructed => { 0 => :array, # RFC-2251 Control + 3 => :array, # Seach referral } } } From 526c1cf08b983b73b89b1e81960328baa0b27b88 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 27 Jun 2006 17:35:18 +0000 Subject: [PATCH 105/231] Removed extraneous print statement, caught by Gary Williams, gary.williams@sas.com --- lib/net/ldap/filter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/net/ldap/filter.rb b/lib/net/ldap/filter.rb index 8c672a4..d086785 100644 --- a/lib/net/ldap/filter.rb +++ b/lib/net/ldap/filter.rb @@ -194,7 +194,7 @@ class Filter seq << ary.shift.to_ber_contextspecific(0) end n_any_strings = ary.length - (final_star ? 0 : 1) - p n_any_strings + #p n_any_strings n_any_strings.times { seq << ary.shift.to_ber_contextspecific(1) } From 4a44c80e2005ea5a16f8f9b9d6524c450ab684b3 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 12 Jul 2006 08:53:53 +0000 Subject: [PATCH 106/231] Version 0.0.2 release announcement --- Release-Announcement | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Release-Announcement b/Release-Announcement index e5928cd..5407e5a 100644 --- a/Release-Announcement +++ b/Release-Announcement @@ -9,9 +9,25 @@ Version 0.0.2 includes an implementation of the "paged search control" to enable queries of A/D result sets > 1000 entries. It also fills in some holes in the documentation. +We fixed a handful of issues that gave strange-looking errors when +working with A/D. The most important of these was support for search +referrals. + +Net::LDAP now works with standard RFC-2254 filter strings (see +Net::LDAP::Filter#construct). This is intended as an adjunct to +(not a replacement for) the existing Filter API, which is considerably +easier to use for people who are not LDAP experts. + +We added a range of new APIs, and deprecated some existing ones. +The goal is to make the Net::LDAP API as intuitive and Ruby-like +as possible. We'll be adding more improvements to the API as we +go along. + And finally we fixed the annoying problem that the 0.0.1 gem and tarball had trailing garbage. Thanks to Austin for helping -track that down. +track that down. Thanks also to Andre Nathan and others for +several valuable suggestions and notes on your experience with +the library. = What is Net::LDAP for Ruby? This library provides a pure-Ruby implementation of an LDAP client. @@ -46,7 +62,7 @@ Net::LDAP can be installed with: % ruby setup.rb Alternatively, you can use the RubyGems version of Net::LDAP available -as ruby-net-ldap-0.0.1.gem from the usual sources. +as ruby-net-ldap-0.0.2.gem from the usual sources. == Whet your appetite: require 'net/ldap' From 82aa67da7cae80931268972667b23aef635df684 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 12 Jul 2006 08:55:34 +0000 Subject: [PATCH 107/231] changelog notes --- ChangeLog | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 9ccd1a4..f82ed51 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,6 @@ = Net::LDAP Changelog -== Net::LDAP 0.0.2: May 3, 2006 +== Net::LDAP 0.0.2: July 12, 2006 * Fixed malformation in distro tarball and gem. * Improved documentation. * Supported "paged search control." @@ -12,6 +12,7 @@ * Fixed a regression with openldap 2.2.x and higher caused by the introduction of RFC-2696 controls. Thanks to Andre Nathan for reporting the problem. +* Added support for RFC-2254 filter syntax. == Net::LDAP 0.0.1: May 1, 2006 * Initial release. From 3370866f01fb2f4fcea015e0059edf2c30295b34 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 12 Jul 2006 09:07:30 +0000 Subject: [PATCH 108/231] more release notes --- Release-Announcement | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/Release-Announcement b/Release-Announcement index 5407e5a..66a0a63 100644 --- a/Release-Announcement +++ b/Release-Announcement @@ -29,6 +29,36 @@ track that down. Thanks also to Andre Nathan and others for several valuable suggestions and notes on your experience with the library. +Progress so far: Net::LDAP is now in use on a variety of production +sites. Stability has been excellent, and performance is quite good. +We've found by far that the most popular application is authentication +for users of web sites, but there has been some chatter about +authorization as well, and a few of you are building directory-enabled +applications. + += What's next on the roadmap? + +Many of you have requested support for encryption. We will soon be +adding support for STARTTLS and LDAPS to Net::LDAP, using Ruby's +built-in OpenSSL library. + +We'd like to improve the API so it presents a more search-like interface +and further masks the archaic (and arcane) native LDAP vocabulary. + +We have a project going ("Peregrine") to provide a full-featured +LDAP server in Ruby, based on the EventMachine fast network-IO library, +that you can use with your own Ruby classes. This will allow you +to serve LDAP clients (such as mailers, IM, and calendaring apps) with data +that you generate dynamically from your own code. + +Finally, we've started thinking about an authentication generator or +plugin for Rails that will work against LDAP servers. It would be even +more interesting to augment it with fine-grained authorization at the +controller level. + +If anyone wants to contribute suggestions, insights or (especially) +code, please email me at garbagecat10@gmail.com. + = What is Net::LDAP for Ruby? This library provides a pure-Ruby implementation of an LDAP client. It can be used to access any server which implements the LDAP protocol. @@ -41,7 +71,7 @@ In particular, this means that there is no direct dependence on the structure of the various "traditional" LDAP clients. This is a ground-up rethinking of the LDAP API. -Net::LDAP is based on RFC-1777, which specifies the Lightweight Directory +Net::LDAP is based on RFC-2251, which specifies the Lightweight Directory Access Protocol, as amended and extended by subsequent RFCs and by the more widely-used directory implementations. From a6d4fa1462dae13dfe24805cb86784bf6c5cb7bc Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 12 Jul 2006 09:20:05 +0000 Subject: [PATCH 109/231] more release notes --- Release-Announcement | 5 +++++ lib/net/ldap.rb | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Release-Announcement b/Release-Announcement index 66a0a63..a73b7c1 100644 --- a/Release-Announcement +++ b/Release-Announcement @@ -23,6 +23,11 @@ The goal is to make the Net::LDAP API as intuitive and Ruby-like as possible. We'll be adding more improvements to the API as we go along. +Please note that Net::LDAP is licensed under the Ruby license +or LGPL, at your option. (I'm saying it publicly right here!) +There are still many source files in the distro that contain +GPL verbiage. We will clean that up in a future release. + And finally we fixed the annoying problem that the 0.0.1 gem and tarball had trailing garbage. Thanks to Austin for helping track that down. Thanks also to Andre Nathan and others for diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index af9ac98..3bf4488 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -32,7 +32,7 @@ module Net # == Net::LDAP # # This library provides a pure-Ruby implementation of the - # LDAP client protocol, per RFC-1777. + # LDAP client protocol, per RFC-2251. # It can be used to access any server which implements the # LDAP protocol. # From b2f67b828b7501226d0618131069795013b894bd Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 12 Jul 2006 09:21:46 +0000 Subject: [PATCH 110/231] doc tweak --- Release-Announcement | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Release-Announcement b/Release-Announcement index a73b7c1..1257598 100644 --- a/Release-Announcement +++ b/Release-Announcement @@ -41,7 +41,7 @@ for users of web sites, but there has been some chatter about authorization as well, and a few of you are building directory-enabled applications. -= What's next on the roadmap? +What's next on the roadmap? Many of you have requested support for encryption. We will soon be adding support for STARTTLS and LDAPS to Net::LDAP, using Ruby's From 8a36c38ec42880d718ae0e0bce69a5a0c60f421e Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 12 Jul 2006 09:23:06 +0000 Subject: [PATCH 111/231] license note --- Release-Announcement | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Release-Announcement b/Release-Announcement index 1257598..5f7c58c 100644 --- a/Release-Announcement +++ b/Release-Announcement @@ -27,6 +27,8 @@ Please note that Net::LDAP is licensed under the Ruby license or LGPL, at your option. (I'm saying it publicly right here!) There are still many source files in the distro that contain GPL verbiage. We will clean that up in a future release. +See the file LICENCE in the distro for details of the Ruby-like +license. And finally we fixed the annoying problem that the 0.0.1 gem and tarball had trailing garbage. Thanks to Austin for helping From 7abd93bc86cfef0db97dbb0464ca3400aafe3061 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 12 Jul 2006 09:26:10 +0000 Subject: [PATCH 112/231] changed version in gemspec --- net-ldap.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/net-ldap.gemspec b/net-ldap.gemspec index 107a396..5c5e829 100644 --- a/net-ldap.gemspec +++ b/net-ldap.gemspec @@ -11,7 +11,7 @@ spec = Gem::Specification.new do |s| s.name = "ruby-net-ldap" - s.version = "0.0.1" + s.version = "0.0.2" s.summary = %q(A pure Ruby LDAP client library.) s.platform = Gem::Platform::RUBY From 3108412c1414e5b3c56843810961e6094fef0306 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 12 Jul 2006 09:46:27 +0000 Subject: [PATCH 113/231] tweak --- README | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README b/README index 2e4798f..322eecc 100644 --- a/README +++ b/README @@ -3,14 +3,14 @@ Net::LDAP is an LDAP support library written in pure Ruby. It supports all LDAP client features, and a subset of server features as well. Homepage:: http://rubyforge.org/projects/net-ldap/ -Copyright:: 2006 by Francis Cianfrocca +Copyright:: (C) 2006 by Francis Cianfrocca Original developer: Francis Cianfrocca Contributions by Austin Ziegler gratefully acknowledged. == LICENCE NOTES Please read the file LICENCE for licensing restrictions on this library. In -it simplest terms, this library is available under the same terms as Ruby +the simplest terms, this library is available under the same terms as Ruby itself. == Requirements From 43dcdec0f1e721bcc897ad8e1be51eb44783c722 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 12 Jul 2006 10:37:37 +0000 Subject: [PATCH 114/231] doc improvements --- README | 3 ++ lib/net/ldap.rb | 97 +++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 88 insertions(+), 12 deletions(-) diff --git a/README b/README index 322eecc..93499d8 100644 --- a/README +++ b/README @@ -16,6 +16,9 @@ itself. == Requirements Net::LDAP requires Ruby 1.8.2 or better. +== Documentation +See Net::LDAP for documentation and usage samples. + #-- # Net::LDAP for Ruby. # http://rubyforge.org/projects/net-ldap/ diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 3bf4488..e661592 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -41,7 +41,25 @@ module Net # the LDAP protocol itself, and thus presenting as Ruby-like # a programming interface as possible. # - # === Quick-start for the Impatient + # == Quick-start for the Impatient + # === Quick Example of a user-authentication against an LDAP directory: + # + # require 'rubygems' + # require 'net/ldap' + # + # ldap = Net::LDAP.new + # ldap.host = your_server_ip_address + # ldap.port = 389 + # ldap.auth "joe_user", "opensesame" + # if ldap.bind + # # authentication succeeded + # else + # # authentication failed + # end + # + # + # === Quick Example of a search against an LDAP directory: + # # require 'rubygems' # require 'net/ldap' # @@ -69,14 +87,14 @@ module Net # p ldap.get_operation_result # # - # == Quick introduction to LDAP + # == A Brief Introduction to LDAP # - # We're going to provide a quick and highly informal introduction to LDAP + # We're going to provide a quick, informal introduction to LDAP # terminology and # typical operations. If you're comfortable with this material, skip # ahead to "How to use Net::LDAP." If you want a more rigorous treatment # of this material, we recommend you start with the various IETF and ITU - # standards that control LDAP. + # standards that relate to LDAP. # # === Entities # LDAP is an Internet-standard protocol used to access directory servers. @@ -360,17 +378,38 @@ module Net @open_connection = nil end - # Convenient method to specify your authentication to the LDAP + # Convenience method to specify authentication credentials to the LDAP # server. Currently supports simple authentication requiring - # a username and password. Observe that on most LDAP servers, - # including A/D, the username is a complete DN. - # The password argument may be a Proc that returns a string. + # a username and password. + # + # Observe that on most LDAP servers, + # the username is a complete DN. However, with A/D, it's often possible + # to give only a user-name rather than a complete DN. In the latter + # case, beware that many A/D servers are configured to permit anonymous + # (uncredentialled) binding, and will silently accept your binding + # as anonymous if you give an unrecognized username. This is not usually + # what you want. (See #get_operation_result.) + # + # Important: The password argument may be a Proc that returns a string. + # This makes it possible for you to write client programs that solicit + # passwords from users or from other data sources without showing them + # in your code or on command lines. + # # require 'net/ldap' # # ldap = Net::LDAP.new # ldap.host = server_ip_address # ldap.authenticate "cn=Your Username,cn=Users,dc=example,dc=com", "your_psw" # + # Alternatively (with a password block): + # + # require 'net/ldap' + # + # ldap = Net::LDAP.new + # ldap.host = server_ip_address + # psw = proc { your_psw_function } + # ldap.authenticate "cn=Your Username,cn=Users,dc=example,dc=com", psw + # def authenticate username, password password = password.call if password.respond_to?(:call) @auth = {:method => :simple, :username => username, :password => password} @@ -553,12 +592,46 @@ module Net @result == 0 and result_set end - # #bind connects to the LDAP server and requests authentication + # #bind connects to an LDAP server and requests authentication # based on the :auth parameter passed to #open or #new. # It takes no parameters. - # User code generally will not call #bind. It will be called - # implicitly by the library whenever an LDAP operation is - # requested. #bind can be useful to test authentication. + # + # User code does not need to call #bind directly. It will be called + # implicitly by the library whenever you invoke an LDAP operation, + # such as #search or #add. + # + # It is useful, however, to call #bind in your own code when the + # only operation you intend to perform against the directory is + # to validate a login credential. #bind returns true or false + # to indicate whether the binding was successful. Reasons for + # failure include malformed or unrecognized usernames and + # incorrect passwords. Use #get_operation_result to find out + # what happened in case of failure. + # + # Here's a typical example using #bind to authenticate a + # credential which was (perhaps) solicited from the user of a + # web site: + # + # require 'net/ldap' + # ldap = Net::LDAP.new + # ldap.host = your_server_ip_address + # ldap.port = 389 + # ldap.auth your_user_name, your_user_password + # if ldap.bind + # # authentication succeeded + # else + # # authentication failed + # p ldap.get_operation_result + # end + # + # You don't have to create a new instance of Net::LDAP every time + # you perform a binding in this way. If you prefer, you can cache the Net::LDAP object + # and re-use it to perform subsequent bindings, provided you call + # #auth to specify a new credential before calling #bind. Otherwise, you'll + # just re-authenticate the previous user! (You don't need to re-set + # the values of #host and #port.) As noted in the documentation for #auth, + # the password parameter can be a Ruby Proc instead of a String. + # #-- # If there is an @open_connection, then perform the bind # on it. Otherwise, connect, bind, and disconnect. From 9bc533401a2fe5aa318467abbc116b967c046200 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 26 Jul 2006 12:20:33 +0000 Subject: [PATCH 115/231] Added simple TLS encryption. --- ChangeLog | 4 ++ lib/net/ber.rb | 16 ++++++++ lib/net/ldap.rb | 98 ++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 116 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index f82ed51..beee1d4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,9 @@ = Net::LDAP Changelog +== Net::LDAP 0.0.3: July xx, 2006 +* Added simple TLS encryption. + Thanks to Garett Shulman for suggestions and for helping test. + == Net::LDAP 0.0.2: July 12, 2006 * Fixed malformation in distro tarball and gem. * Improved documentation. diff --git a/lib/net/ber.rb b/lib/net/ber.rb index 69a190f..fabcb0f 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -139,6 +139,22 @@ 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 def read_ber syntax=nil diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index e661592..ff1d203 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -18,6 +18,13 @@ require 'socket' require 'ostruct' + +begin + require 'openssl' + $net_ldap_openssl_available = true +rescue LoadError +end + require 'net/ber' require 'net/ldap/pdu' require 'net/ldap/filter' @@ -348,7 +355,7 @@ module Net # Instantiate an object of type Net::LDAP to perform directory operations. - # This constructor takes a Hash containing arguments. The following arguments + # This constructor takes a Hash containing arguments, all of which are either optional or may be specified later with other methods as described below. The following arguments # are supported: # * :host => the LDAP server's IP-address (default 127.0.0.1) # * :port => the LDAP server's TCP port (default 389) @@ -356,6 +363,8 @@ module Net # {:method => :anonymous} and # {:method => :simple, :username => your_user_name, :password => your_password } # The password parameter may be a Proc that returns a String. + # * :base => a default treebase parameter for searches performed against the LDAP server. If you don't give this value, then each call to #search must specify a treebase parameter. If you do give this value, then it will be used in subsequent calls to #search that do not specify a treebase. If you give a treebase value in any particular call to #search, that value will override any treebase value you give here. + # * :encryption => specifies the encryption to be used in communicating with the LDAP server. The value is either a Hash containing additional parameters, or the Symbol :simple_tls, which is equivalent to specifying the Hash {:method => :simple_tls}. There is a fairly large range of potential values that may be given for this parameter. See #encryption for details. # # Instantiating a Net::LDAP object does not result in network traffic to # the LDAP server. It simply stores the connection and binding parameters in the @@ -367,6 +376,7 @@ module Net @verbose = false # Make this configurable with a switch on the class. @auth = args[:auth] || DefaultAuth @base = args[:base] || DefaultTreebase + encryption args[:encryption] # may be nil if pr = @auth[:password] and pr.respond_to?(:call) @auth[:password] = pr.call @@ -417,6 +427,51 @@ module Net alias_method :auth, :authenticate + # Convenience method to specify encryption characteristics for connections + # to LDAP servers. Called implicitly by #new and #open, but may also be called + # by user code if desired. + # The single argument is generally a Hash (but see below for convenience alternatives). + # This implementation is currently a stub, supporting only a few encryption + # alternatives. As additional capabilities are added, more configuration values + # will be added here. + # + # Currently, the only supported argument is {:method => :simple_tls}. + # (Equivalently, you may pass the symbol :simple_tls all by itself, without + # enclosing it in a Hash.) + # + # The :simple_tls encryption method encrypts all communications with the LDAP + # server. + # It completely establishes SSL/TLS encryption with the LDAP server + # before any LDAP-protocol data is exchanged. + # There is no plaintext negotiation and no special encryption-request controls + # are sent to the server. + # The :simple_tls option is the simplest, easiest way to encrypt communications + # between Net::LDAP and LDAP servers. + # It's intended for cases where you have an implicit level of trust in the authenticity + # of the LDAP server. No validation of the LDAP server's SSL certificate is + # performed. This means that :simple_tls will not produce errors if the LDAP + # server's encryption certificate is not signed by a well-known Certification + # Authority. + # If you get communications or protocol errors when using this option, check + # with your LDAP server administrator. Pay particular attention to the TCP port + # you are connecting to. It's impossible for an LDAP server to support plaintext + # LDAP communications and simple TLS connections on the same port. + # The standard TCP port for unencrypted LDAP connections is 389, but the standard + # port for simple-TLS encrypted connections is 636. Be sure you are using the + # correct port. + # + # [Note: a future version of Net::LDAP will support the STARTTLS LDAP control, + # which will enable encrypted communications on the same TCP port used for + # unencrypted connections.] + # + def encryption args + if args == :simple_tls + args = {:method => :simple_tls} + end + @encryption = args + end + + # #open takes the same parameters as #new. #open makes a network connection to the # LDAP server and then passes a newly-created Net::LDAP object to the caller-supplied block. # Within the block, you can call any of the instance methods of Net::LDAP to @@ -484,7 +539,7 @@ module Net # if the bind was unsuccessful. def open raise LdapError.new( "open already in progress" ) if @open_connection - @open_connection = Connection.new( :host => @host, :port => @port ) + @open_connection = Connection.new( :host => @host, :port => @port, :encryption => @encryption ) @open_connection.bind @auth yield self @open_connection.close @@ -908,10 +963,49 @@ module Net raise LdapError.new( "no connection to server" ) end + if server[:encryption] + setup_encryption server[:encryption] + end + yield self if block_given? end + #-- + # Helper method called only from new, and only after we have a successfully-opened + # @conn instance variable, which is a TCP connection. + # Depending on the received arguments, we establish SSL, potentially replacing + # the value of @conn accordingly. + # Don't generate any errors here if no encryption is requested. + # DO raise LdapError objects if encryption is requested and we have trouble setting + # it up. That includes if OpenSSL is not set up on the machine. (Question: + # how does the Ruby OpenSSL wrapper react in that case?) + # DO NOT filter exceptions raised by the OpenSSL library. Let them pass back + # to the user. That should make it easier for us to debug the problem reports. + # Presumably (hopefully?) that will also produce recognizable errors if someone + # tries to use this on a machine without OpenSSL. + # + # The simple_tls method is intended as the simplest, stupidest, easiest solution + # for people who want nothing more than encrypted comms with the LDAP server. + # It doesn't do any server-cert validation and requires nothing in the way + # of key files and root-cert files, etc etc. + # OBSERVE: WE REPLACE the value of @conn, which is presumed to be a connected + # TCPsocket object. + # + def setup_encryption args + case args[:method] + when :simple_tls + raise LdapError.new("openssl unavailable") unless $net_ldap_openssl_available + ctx = OpenSSL::SSL::SSLContext.new + @conn = OpenSSL::SSL::SSLSocket.new(@conn, ctx) + @conn.connect + @conn.sync_close = true + # additional branches requiring server validation and peer certs, etc. go here. + else + raise LdapError.new( "unsupported encryption method #{args[:method]}" ) + end + end + #-- # close # This is provided as a convenience method to make From e89a9a4d728b20acfa5fa95d83c69d8a4c220f64 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 26 Jul 2006 12:21:30 +0000 Subject: [PATCH 116/231] version bump to 0.0.3 --- lib/net/ldap.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index ff1d203..1c0450f 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -263,7 +263,7 @@ module Net class LdapError < Exception; end - VERSION = "0.0.2" + VERSION = "0.0.3" SearchScope_BaseObject = 0 From 69158b7b16033223552a4a9a42c8c7803b12c672 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 26 Jul 2006 21:35:38 +0000 Subject: [PATCH 117/231] left out some encryption calls. --- lib/net/ldap.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 1c0450f..414f02a 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -634,7 +634,7 @@ module Net } else @result = 0 - conn = Connection.new( :host => @host, :port => @port ) + conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption ) if (@result = conn.bind( args[:auth] || @auth )) == 0 @result = conn.search( args ) {|entry| result_set << entry if result_set @@ -696,7 +696,7 @@ module Net if @open_connection @result = @open_connection.bind @auth else - conn = Connection.new( :host => @host, :port => @port ) + conn = Connection.new( :host => @host, :port => @port , :encryption => @encryption) @result = conn.bind @auth conn.close end @@ -747,7 +747,7 @@ module Net @result = @open_connection.add( args ) else @result = 0 - conn = Connection.new( :host => @host, :port => @port ) + conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption) if (@result = conn.bind( args[:auth] || @auth )) == 0 @result = conn.add( args ) end @@ -831,7 +831,7 @@ module Net @result = @open_connection.modify( args ) else @result = 0 - conn = Connection.new( :host => @host, :port => @port ) + conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption ) if (@result = conn.bind( args[:auth] || @auth )) == 0 @result = conn.modify( args ) end @@ -903,7 +903,7 @@ module Net @result = @open_connection.rename( args ) else @result = 0 - conn = Connection.new( :host => @host, :port => @port ) + conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption ) if (@result = conn.bind( args[:auth] || @auth )) == 0 @result = conn.rename( args ) end @@ -933,7 +933,7 @@ module Net @result = @open_connection.delete( args ) else @result = 0 - conn = Connection.new( :host => @host, :port => @port ) + conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption ) if (@result = conn.bind( args[:auth] || @auth )) == 0 @result = conn.delete( args ) end From 77764ecc94676cbdcea01ae61f32317c6f41f03c Mon Sep 17 00:00:00 2001 From: blackhedd Date: Thu, 27 Jul 2006 04:52:41 +0000 Subject: [PATCH 118/231] changelog date --- ChangeLog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index beee1d4..44df06d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,6 @@ = Net::LDAP Changelog -== Net::LDAP 0.0.3: July xx, 2006 +== Net::LDAP 0.0.3: July 26, 2006 * Added simple TLS encryption. Thanks to Garett Shulman for suggestions and for helping test. From f96e4ff4fab4f2e39c6be93d492df4baf1ee9f01 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Thu, 27 Jul 2006 05:03:54 +0000 Subject: [PATCH 119/231] Version 0.0.3 announcement --- Release-Announcement | 82 +++++++++++++++----------------------------- 1 file changed, 27 insertions(+), 55 deletions(-) diff --git a/Release-Announcement b/Release-Announcement index 5f7c58c..effd985 100644 --- a/Release-Announcement +++ b/Release-Announcement @@ -1,70 +1,42 @@ -We're pleased to announce version 0.0.2 of Net::LDAP, the first +We're pleased to announce version 0.0.3 of Net::LDAP, the first pure-Ruby LDAP library. Net::LDAP intends to be a feature-complete LDAP client which can access as much as possible of the functionality of the most-used LDAP server implementations. This library does not wrap any existing native-code LDAP libraries, creates no Ruby extensions, and has no dependencies external to Ruby. -Version 0.0.2 includes an implementation of the "paged search -control" to enable queries of A/D result sets > 1000 entries. -It also fills in some holes in the documentation. +Version 0.0.3 adds support for encrypted communications to LDAP servers. +There is a new optional parameter for Net::LDAP#new and Net::LDAP#open +that allows you to specify encryption characteristics. Here's a quick +example: -We fixed a handful of issues that gave strange-looking errors when -working with A/D. The most important of these was support for search -referrals. + require 'net/ldap' + ldap = Net::LDAP.new( + :host => "an_ip_address", + :port => 636, + :auth => {:method => :simple, :username => "mickey", :password => "mouse" }, + :encryption => {:method => :simple_tls} + ) + ldap.bind or raise "bind failed" + ldap.search( ... ) + # etc, etc. -Net::LDAP now works with standard RFC-2254 filter strings (see -Net::LDAP::Filter#construct). This is intended as an adjunct to -(not a replacement for) the existing Filter API, which is considerably -easier to use for people who are not LDAP experts. +This release supports simple TLS encryption with no client or server +validation. Future versions will add support for the STARTTLS control, +and for certificate validation. Additional parameters will appear to +support these options. -We added a range of new APIs, and deprecated some existing ones. -The goal is to make the Net::LDAP API as intuitive and Ruby-like -as possible. We'll be adding more improvements to the API as we -go along. +Net::LDAP encryption requires Ruby's openssl library. We're not +quite sure what happens when this library is present but the underlying +OpenSSL libraries are missing or not configured appropriately, +especially on back versions of Ruby. If anyone encounters problems +using encryption in Net::LDAP, please let us know and give us the +details of your platform and Ruby build info. -Please note that Net::LDAP is licensed under the Ruby license -or LGPL, at your option. (I'm saying it publicly right here!) -There are still many source files in the distro that contain -GPL verbiage. We will clean that up in a future release. -See the file LICENCE in the distro for details of the Ruby-like -license. - -And finally we fixed the annoying problem that the 0.0.1 gem -and tarball had trailing garbage. Thanks to Austin for helping -track that down. Thanks also to Andre Nathan and others for -several valuable suggestions and notes on your experience with -the library. - -Progress so far: Net::LDAP is now in use on a variety of production -sites. Stability has been excellent, and performance is quite good. -We've found by far that the most popular application is authentication -for users of web sites, but there has been some chatter about -authorization as well, and a few of you are building directory-enabled -applications. - -What's next on the roadmap? - -Many of you have requested support for encryption. We will soon be -adding support for STARTTLS and LDAPS to Net::LDAP, using Ruby's -built-in OpenSSL library. - -We'd like to improve the API so it presents a more search-like interface -and further masks the archaic (and arcane) native LDAP vocabulary. - -We have a project going ("Peregrine") to provide a full-featured -LDAP server in Ruby, based on the EventMachine fast network-IO library, -that you can use with your own Ruby classes. This will allow you -to serve LDAP clients (such as mailers, IM, and calendaring apps) with data -that you generate dynamically from your own code. - -Finally, we've started thinking about an authentication generator or -plugin for Rails that will work against LDAP servers. It would be even -more interesting to augment it with fine-grained authorization at the -controller level. +Thanks to Garett Shulman for helping to test the new code. If anyone wants to contribute suggestions, insights or (especially) -code, please email me at garbagecat10@gmail.com. +code, please email me at garbagecat10 .. .. gmail.com. = What is Net::LDAP for Ruby? This library provides a pure-Ruby implementation of an LDAP client. From ad24ad979d77f6e1249813bd61a100ad83457901 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 5 Aug 2006 15:20:18 +0000 Subject: [PATCH 120/231] undeprecated Net::LDAP#modify. --- ChangeLog | 4 ++++ Release-Announcement | 46 +++++++++++--------------------------------- lib/net/ldap.rb | 21 +++++++++++++++----- 3 files changed, 31 insertions(+), 40 deletions(-) diff --git a/ChangeLog b/ChangeLog index 44df06d..6bea4ce 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,9 @@ = Net::LDAP Changelog +== Net::LDAP 0.0.4: August xx, 2006 +* Undeprecated Net::LDAP#modify. Thanks to Justin Forder for + providing the rationale for this. + == Net::LDAP 0.0.3: July 26, 2006 * Added simple TLS encryption. Thanks to Garett Shulman for suggestions and for helping test. diff --git a/Release-Announcement b/Release-Announcement index effd985..3ff4020 100644 --- a/Release-Announcement +++ b/Release-Announcement @@ -1,40 +1,16 @@ -We're pleased to announce version 0.0.3 of Net::LDAP, the first -pure-Ruby LDAP library. Net::LDAP intends to be a feature-complete -LDAP client which can access as much as possible of the functionality -of the most-used LDAP server implementations. This library does -not wrap any existing native-code LDAP libraries, creates no +We're pleased to announce version 0.0.4 of Net::LDAP, the first +pure-Ruby LDAP library. + +This version un-deprecates Net::LDAP#modify, which can be useful with +LDAP servers that observe non-standard transactional and concurrency +semantics in LDAP Modify operations. Note: this is a documentation change, +not a code change. + +Net::LDAP is a feature-complete LDAP client which can access as much as +possible of the functionality of the most-used LDAP server implementations. +This library does not wrap any existing native-code LDAP libraries, creates no Ruby extensions, and has no dependencies external to Ruby. -Version 0.0.3 adds support for encrypted communications to LDAP servers. -There is a new optional parameter for Net::LDAP#new and Net::LDAP#open -that allows you to specify encryption characteristics. Here's a quick -example: - - require 'net/ldap' - ldap = Net::LDAP.new( - :host => "an_ip_address", - :port => 636, - :auth => {:method => :simple, :username => "mickey", :password => "mouse" }, - :encryption => {:method => :simple_tls} - ) - ldap.bind or raise "bind failed" - ldap.search( ... ) - # etc, etc. - -This release supports simple TLS encryption with no client or server -validation. Future versions will add support for the STARTTLS control, -and for certificate validation. Additional parameters will appear to -support these options. - -Net::LDAP encryption requires Ruby's openssl library. We're not -quite sure what happens when this library is present but the underlying -OpenSSL libraries are missing or not configured appropriately, -especially on back versions of Ruby. If anyone encounters problems -using encryption in Net::LDAP, please let us know and give us the -details of your platform and Ruby build info. - -Thanks to Garett Shulman for helping to test the new code. - If anyone wants to contribute suggestions, insights or (especially) code, please email me at garbagecat10 .. .. gmail.com. diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 414f02a..e0214a3 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -757,8 +757,6 @@ module Net end - # _DEPRECATED_ - Please use #add_attribute, #replace_attribute, or #delete_attribute. - # # Modifies the attribute values of a particular entry on the LDAP directory. # Takes a hash with arguments. Supported arguments are: # :dn :: (the full DN of the entry whose attributes are to be modified) @@ -768,6 +766,9 @@ module Net # succeeded or failed, with extended information available by calling # #get_operation_result. # + # Also see #add_attribute, #replace_attribute, or #delete_attribute, which + # provide simpler interfaces to this functionality. + # # The LDAP protocol provides a full and well thought-out set of operations # for changing the values of attributes, but they are necessarily somewhat complex # and not always intuitive. If these instructions are confusing or incomplete, @@ -786,14 +787,15 @@ module Net # The :add operator will, unsurprisingly, add the specified values to # the specified attribute. If the attribute does not already exist, # :add will create it. Most LDAP servers will generate an error if you - # to add a value that already exists. + # try to add a value that already exists. # # :replace will erase the current value(s) for the specified attribute, # if there are any, and replace them with the specified value(s). # # :delete will remove the specified value(s) from the specified attribute. # If you pass nil, an empty string, or an empty array as the value parameter - # to a :delete operation, the _entire_ _attribute_ will be deleted. + # to a :delete operation, the _entire_ _attribute_ will be deleted, along + # with all of its values. # # For example: # @@ -808,13 +810,14 @@ module Net # (This example is contrived since you probably wouldn't add a mail # value right before replacing the whole attribute, but it shows that order # of execution matters. Also, many LDAP servers won't let you delete SN - # because it would be a schema violation.) + # because that would be a schema violation.) # # It's essential to keep in mind that if you specify more than one operation in # a call to #modify, most LDAP servers will attempt to perform all of the operations # in the order you gave them. # This matters because you may specify operations on the # same attribute which must be performed in a certain order. + # # Most LDAP servers will _stop_ processing your modifications if one of them # causes an error on the server (such as a schema-constraint violation). # If this happens, you will probably get a result code from the server that @@ -825,6 +828,14 @@ module Net # not be "rolled back," resulting in a partial update. This is a limitation # of the LDAP protocol, not of Net::LDAP. # + # The lack of transactional atomicity in LDAP means that you're usually + # better off using the convenience methods #add_attribute, #replace_attribute, + # and #delete_attribute, which are are wrappers over #modify. However, certain + # LDAP servers may provide concurrency semantics, in which the several operations + # contained in a single #modify call are not interleaved with other + # modification-requests received simultaneously by the server. + # It bears repeating that this concurrency does _not_ imply transactional + # atomicity, which LDAP does not provide. # def modify args if @open_connection From e2bf4dc172a52263a0eda3f38985fea941548582 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 12 Aug 2006 14:54:25 +0000 Subject: [PATCH 121/231] Added a preliminary implementation of Net::LDAP#bind_as. --- lib/net/ldap.rb | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index e0214a3..b052821 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -692,9 +692,9 @@ module Net # on it. Otherwise, connect, bind, and disconnect. # The latter operation is obviously useful only as an auth check. # - def bind + def bind auth=@auth if @open_connection - @result = @open_connection.bind @auth + @result = @open_connection.bind auth else conn = Connection.new( :host => @host, :port => @port , :encryption => @encryption) @result = conn.bind @auth @@ -715,7 +715,15 @@ module Net # # This method is currently an unimplemented stub. # - def bind_as + def bind_as args={} + result = false + open {|me| + rs = search args + if rs and rs.first and dn = rs.first.dn + result = bind :method => :simple, :username => dn, :password => args[:password] + end + } + result end # Adds a new entry to the remote LDAP server. From faed957164a25265e1196e5b8a196c48028aead5 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 15 Aug 2006 08:33:18 +0000 Subject: [PATCH 122/231] Changed version to 4. Changed Net::LDAP#bind_as returns a result set now. --- lib/net/ldap.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index b052821..2515a67 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -263,7 +263,7 @@ module Net class LdapError < Exception; end - VERSION = "0.0.3" + VERSION = "0.0.4" SearchScope_BaseObject = 0 @@ -720,12 +720,15 @@ module Net open {|me| rs = search args if rs and rs.first and dn = rs.first.dn - result = bind :method => :simple, :username => dn, :password => args[:password] + password = args[:password] + password = password.call if password.respond_to?(:call) + result = rs if bind :method => :simple, :username => dn, :password => password end } result end + # Adds a new entry to the remote LDAP server. # Supported arguments: # :dn :: Full DN of the new entry @@ -1086,6 +1089,7 @@ module Net # def search args = {} search_filter = (args && args[:filter]) || Filter.eq( "objectclass", "*" ) + #search_filter = Filter.construct(search_filter) if search_filter.is_a?(String) search_base = (args && args[:base]) || "dc=example,dc=com" search_attributes = ((args && args[:attributes]) || []).map {|attr| attr.to_s.to_ber} return_referrals = args && args[:return_referrals] == true From 089c737160665e3088db360aa43128aa44a88520 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 15 Aug 2006 08:34:06 +0000 Subject: [PATCH 123/231] Net::LDAP#search now automatically converts filters in rfc-2254 notation. --- lib/net/ldap.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 2515a67..af6898b 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -1089,7 +1089,7 @@ module Net # def search args = {} search_filter = (args && args[:filter]) || Filter.eq( "objectclass", "*" ) - #search_filter = Filter.construct(search_filter) if search_filter.is_a?(String) + search_filter = Filter.construct(search_filter) if search_filter.is_a?(String) search_base = (args && args[:base]) || "dc=example,dc=com" search_attributes = ((args && args[:attributes]) || []).map {|attr| attr.to_s.to_ber} return_referrals = args && args[:return_referrals] == true From 7c1877d9916ce03527dea3c20ae4f9b71904f6fb Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 15 Aug 2006 08:34:53 +0000 Subject: [PATCH 124/231] Augmented the value filter used in parsing rfc-2254 filters. Thanks to Andre Nathan. --- lib/net/ldap/filter.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/net/ldap/filter.rb b/lib/net/ldap/filter.rb index d086785..4051cce 100644 --- a/lib/net/ldap/filter.rb +++ b/lib/net/ldap/filter.rb @@ -292,6 +292,7 @@ class Filter end # class Net::LDAP::Filter + class FilterParser #:nodoc: attr_reader :filter @@ -349,13 +350,16 @@ class FilterParser #:nodoc: end end + # Added a greatly-augmented filter contributed by Andre Nathan + # for detecting special characters in values. (15Aug06) def parse_filter_branch scanner scanner.scan /\s*/ if token = scanner.scan( /[\w\-_]+/ ) scanner.scan /\s*/ if op = scanner.scan( /\=|\<\=|\<|\>\=|\>|\!\=/ ) scanner.scan /\s*/ - if value = scanner.scan( /[\w\*\.]+/ ) + #if value = scanner.scan( /[\w\*\.]+/ ) (ORG) + if value = scanner.scan( /[\w\*\.\+\-@=#\$%&!]+/ ) case op when "=" Filter.eq( token, value ) From a2ef2e2df57c85cb37594af7c42066309218b028 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 15 Aug 2006 08:46:38 +0000 Subject: [PATCH 125/231] Bug fix: set @open_connection to nil at the end of a Net::LDAP#open. Otherwise, you couldn't do more than one #open on the same Net::LDAP object! Subsequent ones would say "open already in progress." --- lib/net/ldap.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index af6898b..523db79 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -543,6 +543,7 @@ module Net @open_connection.bind @auth yield self @open_connection.close + @open_connection = nil end From 5670a452d348ee79b687d97dcc5c34a745cc6fa9 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 15 Aug 2006 08:47:36 +0000 Subject: [PATCH 126/231] Version 4.0 changes --- ChangeLog | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 6bea4ce..bd9b70e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,8 +1,16 @@ = Net::LDAP Changelog -== Net::LDAP 0.0.4: August xx, 2006 +== Net::LDAP 0.0.4: August 15, 2006 * Undeprecated Net::LDAP#modify. Thanks to Justin Forder for providing the rationale for this. +* Added a much-expanded set of special characters to the parser + for RFC-2254 filters. Thanks to Andre Nathan. +* Changed Net::LDAP#search so you can pass it a filter in string form. + The conversion to a Net::LDAP::Filter now happens automatically. +* Implemented Net::LDAP#bind_as (preliminary and subject to change). + Thanks for Simon Claret for valuable suggestions and for helping test. +* Fixed bug in Net::LDAP#open that was preventing #open from being + called more than one on a given Net::LDAP object. == Net::LDAP 0.0.3: July 26, 2006 * Added simple TLS encryption. From 02ab4f3a6b08136a9e8606b141a9e3b83bea4fd2 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 15 Aug 2006 09:35:43 +0000 Subject: [PATCH 127/231] documentation for Net::LDAP#bind_as. --- lib/net/ldap.rb | 49 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 523db79..78355b7 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -707,15 +707,50 @@ module Net # # #bind_as is for testing authentication credentials. - # Most likely a "standard" name (like a CN or an email - # address) will be presented along with a password. - # We'll bind with the main credential given in the - # constructor, query the full DN of the user given - # to us as a parameter, then unbind and rebind as the - # new user. # - # This method is currently an unimplemented stub. + # As described under #bind, most LDAP servers require that you supply a complete DN + # as a binding-credential, along with an authenticator such as a password. + # But for many applications (such as authenticating users to a Rails application), + # you often don't have a full DN to identify the user. You usually get a simple + # identifier like a username or an email address, along with a password. + # #bind_as allows you to authenticate these user-identifiers. # + # #bind_as is a combination of a search and an LDAP binding. First, it connects and + # binds to the directory as normal. Then it searches the directory for an entry + # corresponding to the email address, username, or other string that you supply. + # If the entry exists, then #bind_as will re-bind as that user with the + # password (or other authenticator) that you supply. + # + # #bind_as takes the same parameters as #search, with the addition of an + # authenticator. Currently, this authenticator must be :password. + # Its value may be either a String, or a +proc+ that returns a String. + # #bind_as returns +false+ on failure. On success, it returns a result set, + # just as #search does. This result set is an Array of objects of + # type Net::LDAP::Entry. It contains the directory attributes corresponding to + # the user. (Just test whether the return value is logically true, if you don't + # need this additional information.) + # + # Here's how you would use #bind_as to authenticate an email address and password: + # + # require 'net/ldap' + # + # user,psw = "joe_user@yourcompany.com", "joes_psw" + # + # ldap = Net::LDAP.new + # ldap.host = "192.168.0.100" + # ldap.port = 389 + # ldap.auth "cn=manager,dc=yourcompany,dc=com", "topsecret" + # + # result = ldap.bind_as( + # :base => "dc=yourcompany,dc=com", + # :filter => "(mail=#{user})", + # :password => psw + # ) + # if result + # puts "Authenticated #{result.first.dn}" + # else + # puts "Authentication FAILED." + # end def bind_as args={} result = false open {|me| From f81fdcab880b7ce51246fc8f7139e23499bb68ae Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 15 Aug 2006 09:43:44 +0000 Subject: [PATCH 128/231] notes for version 4 --- Release-Announcement | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/Release-Announcement b/Release-Announcement index 3ff4020..5d14811 100644 --- a/Release-Announcement +++ b/Release-Announcement @@ -1,10 +1,23 @@ -We're pleased to announce version 0.0.4 of Net::LDAP, the first -pure-Ruby LDAP library. +We're pleased to announce version 0.0.4 of Net::LDAP, the pure-Ruby LDAP library. -This version un-deprecates Net::LDAP#modify, which can be useful with +This version adds an implementation of Net::LDAP#bind_as, which allows +you to authenticate users who log into your applications using simple +identifiers like email addresses and simple usernames. Thanks to Simon Claret +for suggesting the original implementation in his page on the Rails-Wiki, +and for valuable comments and help with testing. + +We have un-deprecated Net::LDAP#modify, which can be useful with LDAP servers that observe non-standard transactional and concurrency semantics in LDAP Modify operations. Note: this is a documentation change, -not a code change. +not a code change. Thanks to Justin Forder for providing the rationale +for this change. + +We added a larger set of special characters which may appear in RFC-2254 +standard search filters. Thanks to Andre Nathan for this patch. + +We fixed a bug that was preventing Net::LDAP#open from being called more +than once on the same object. + Net::LDAP is a feature-complete LDAP client which can access as much as possible of the functionality of the most-used LDAP server implementations. From 8d89bb6292dae9e9842df0ca61d21170f3a73285 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 16 Aug 2006 23:57:59 +0000 Subject: [PATCH 129/231] added parentheses to quiet warnings. --- lib/net/ldap/filter.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/net/ldap/filter.rb b/lib/net/ldap/filter.rb index 4051cce..686c4f3 100644 --- a/lib/net/ldap/filter.rb +++ b/lib/net/ldap/filter.rb @@ -307,8 +307,8 @@ class FilterParser #:nodoc: end def parse_paren_expression scanner - if scanner.scan /\s*\(\s*/ - b = if scanner.scan /\s*\&\s*/ + if scanner.scan(/\s*\(\s*/) + b = if scanner.scan(/\s*\&\s*/) a = nil branches = [] while br = parse_paren_expression(scanner) @@ -321,7 +321,7 @@ class FilterParser #:nodoc: end a end - elsif scanner.scan /\s*\|\s*/ + elsif scanner.scan(/\s*\|\s*/) # TODO: DRY! a = nil branches = [] @@ -335,7 +335,7 @@ class FilterParser #:nodoc: end a end - elsif scanner.scan /\s*\!\s*/ + elsif scanner.scan(/\s*\!\s*/) br = parse_paren_expression(scanner) if br ~ br @@ -353,11 +353,11 @@ class FilterParser #:nodoc: # Added a greatly-augmented filter contributed by Andre Nathan # for detecting special characters in values. (15Aug06) def parse_filter_branch scanner - scanner.scan /\s*/ + scanner.scan(/\s*/) if token = scanner.scan( /[\w\-_]+/ ) - scanner.scan /\s*/ + scanner.scan(/\s*/) if op = scanner.scan( /\=|\<\=|\<|\>\=|\>|\!\=/ ) - scanner.scan /\s*/ + scanner.scan(/\s*/) #if value = scanner.scan( /[\w\*\.]+/ ) (ORG) if value = scanner.scan( /[\w\*\.\+\-@=#\$%&!]+/ ) case op From fb4b2ba87d3d564a97ebfc7db4f6ceee8ee21a83 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Thu, 17 Aug 2006 08:28:02 +0000 Subject: [PATCH 130/231] change comment --- ChangeLog | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog b/ChangeLog index bd9b70e..54d0516 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,9 @@ = Net::LDAP Changelog +== Net::LDAP 0.0.5: August xx, 2006 +* Silenced some annoying warnings in filter.rb. Thanks to "barjunk" + for pointing this out. + == Net::LDAP 0.0.4: August 15, 2006 * Undeprecated Net::LDAP#modify. Thanks to Justin Forder for providing the rationale for this. From 493b81bd6907fe0be67425ed56aee0b74a3c2c12 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 18 Aug 2006 19:41:03 +0000 Subject: [PATCH 131/231] added Net::LDAP::Entry#to_ldif --- lib/net/ldap/entry.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/net/ldap/entry.rb b/lib/net/ldap/entry.rb index 625c5db..0b56cbe 100644 --- a/lib/net/ldap/entry.rb +++ b/lib/net/ldap/entry.rb @@ -128,6 +128,22 @@ class LDAP alias_method :each_attribute, :each + + # Converts the Entry to a String, representing the + # Entry's attributes in LDIF format. + def to_ldif + ary = [] + ary << "dn: #{dn}\n" + each_attribute do |k,v| + v.each {|v1| + ary << "#{k}: #{v1}\n" unless k == :dn + } + end + ary << "\n" + ary.join + end + + #-- # Convenience method to convert unknown method names # to attribute references. Of course the method name From 4ca89a2b2397dd08a2a89845a0c1218d93b676fb Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 18 Aug 2006 19:41:39 +0000 Subject: [PATCH 132/231] notes --- ChangeLog | 1 + 1 file changed, 1 insertion(+) diff --git a/ChangeLog b/ChangeLog index 54d0516..173db97 100644 --- a/ChangeLog +++ b/ChangeLog @@ -3,6 +3,7 @@ == Net::LDAP 0.0.5: August xx, 2006 * Silenced some annoying warnings in filter.rb. Thanks to "barjunk" for pointing this out. +* Added Net::LDAP::Entry#to_ldif == Net::LDAP 0.0.4: August 15, 2006 * Undeprecated Net::LDAP#modify. Thanks to Justin Forder for From 4a46e19b8f6de6487cbb3b7928f6bac701e85c36 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 18 Aug 2006 19:42:34 +0000 Subject: [PATCH 133/231] bumped version to 0.0.5 --- lib/net/ldap.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 78355b7..1947711 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -263,7 +263,7 @@ module Net class LdapError < Exception; end - VERSION = "0.0.4" + VERSION = "0.0.5" SearchScope_BaseObject = 0 From 4a8f68bf9e8b31c1d2e7929ddeb19191f77bed55 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 25 Aug 2006 02:41:13 +0000 Subject: [PATCH 134/231] Patched Rakefile so it runs the tests. Thanks to Daniel Berger. --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index cd5b007..31a487f 100644 --- a/Rakefile +++ b/Rakefile @@ -51,7 +51,7 @@ task :test do |t| $LOAD_PATH.unshift('tests') $stderr.puts "Checking for test cases:" if t.verbose - Dir['tests/tc_*.rb'].each do |testcase| + Dir['tests/test*.rb'].each do |testcase| $stderr.puts "\t#{testcase}" if t.verbose load testcase end From 6889456ba3358023c0c2010c9b2527ee2729f9c2 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 25 Aug 2006 02:42:20 +0000 Subject: [PATCH 135/231] notes --- ChangeLog | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog b/ChangeLog index 173db97..406afe3 100644 --- a/ChangeLog +++ b/ChangeLog @@ -4,6 +4,8 @@ * Silenced some annoying warnings in filter.rb. Thanks to "barjunk" for pointing this out. * Added Net::LDAP::Entry#to_ldif +* Patched Rakefile so it actually runs the test suite. Thanks to + Daniel Berger for submitting his patch. == Net::LDAP 0.0.4: August 15, 2006 * Undeprecated Net::LDAP#modify. Thanks to Justin Forder for From 7660da02704e7f938d14782e50d06e56b9954946 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 27 Aug 2006 03:25:44 +0000 Subject: [PATCH 136/231] fixed a couple of "quiet" bugs, and added custom _dump and _load methods so that Net::LDAP::Entry objects can be marshalled. Thanks to an anonymous feature requester who only left the name "Jammy." --- lib/net/ldap/entry.rb | 53 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/lib/net/ldap/entry.rb b/lib/net/ldap/entry.rb index 0b56cbe..4d6eb30 100644 --- a/lib/net/ldap/entry.rb +++ b/lib/net/ldap/entry.rb @@ -77,15 +77,35 @@ class LDAP # at least three places. Should do it in ONE place. class Entry + # This constructor is not generally called by user code. + #-- + # Originally, myhash took a block so we wouldn't have to + # make sure its elements returned empty arrays when necessary. + # Got rid of that to enable marshalling of Entry objects, + # but that doesn't work anyway, because Entry objects have + # singleton methods. So we define a custom dump and load. def initialize dn = nil # :nodoc: - @myhash = Hash.new {|k,v| k[v] = [] } + @myhash = {} # originally: Hash.new {|k,v| k[v] = [] } @myhash[:dn] = [dn] end + def _dump depth + to_ldif + end + class << self + def _load entry + from_single_ldif_string entry + end + end + + #-- + # Discovered bug, 26Aug06: I noticed that we're not converting the + # incoming value to an array if it isn't already one. def []= name, value # :nodoc: sym = name.to_s.downcase.intern + value = [value] unless value.is_a?(Array) @myhash[sym] = value end @@ -97,12 +117,12 @@ class LDAP # def [] name # :nodoc: name = name.to_s.downcase.intern unless name.is_a?(Symbol) - @myhash[name] + @myhash[name] || [] end # Returns the dn of the Entry as a String. def dn - self[:dn][0] + self[:dn][0].to_s end # Returns an array of the attribute names present in the Entry. @@ -131,6 +151,9 @@ class LDAP # Converts the Entry to a String, representing the # Entry's attributes in LDIF format. + #-- + # TODO, this doesn't support binary representations, + # nor does it break overlength lines. def to_ldif ary = [] ary << "dn: #{dn}\n" @@ -143,6 +166,30 @@ class LDAP ary.join end + #-- + # TODO, doesn't support binary representations yet (:: notation), + # and it doesn't handle broken lines. + # It generates a SINGLE Entry object from an incoming LDIF stream + # which is of course useless for big LDIF streams that encode + # many objects. + # DO NOT DOCUMENT THIS METHOD UNTIL THESE RESTRICTIONS ARE LIFTED. + # As it is, it's useful for unmarshalling objects that we create, + # but not for reading arbitrary LDIF files. + # Eventually, we should have a class method that parses large LDIF + # streams into individual LDIF blocks (delimited by blank lines) + # and passes them here. + class << self + def from_single_ldif_string ldif + entry = Entry.new + ldif.split(/\r?\n/m).each {|line| + break if line.length == 0 + if line =~ /\A([\w]+)::?[\s]*/ + entry[$1] = $' + end + } + entry.dn ? entry : nil + end + end #-- # Convenience method to convert unknown method names From f1e9861b50eb0b5e8efcec462a91827d5c7ffbde Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 27 Aug 2006 03:26:46 +0000 Subject: [PATCH 137/231] change notes --- ChangeLog | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ChangeLog b/ChangeLog index 406afe3..b9a5ac6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -6,6 +6,9 @@ * Added Net::LDAP::Entry#to_ldif * Patched Rakefile so it actually runs the test suite. Thanks to Daniel Berger for submitting his patch. +* Changed Net::LDAP::Entry so it can be marshalled and unmarshalled. + Thanks to an anonymous feature requester who only left the name + "Jammy." == Net::LDAP 0.0.4: August 15, 2006 * Undeprecated Net::LDAP#modify. Thanks to Justin Forder for From 1deab245d68807204e72fe1406dcf974ec52e909 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 29 Aug 2006 00:24:29 +0000 Subject: [PATCH 138/231] supported base64 encoding for binary attribute values --- ChangeLog | 2 ++ lib/net/ldap/entry.rb | 46 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/ChangeLog b/ChangeLog index b9a5ac6..c0f10bf 100644 --- a/ChangeLog +++ b/ChangeLog @@ -9,6 +9,8 @@ * Changed Net::LDAP::Entry so it can be marshalled and unmarshalled. Thanks to an anonymous feature requester who only left the name "Jammy." +* Added support for binary values in Net::LDAP::Entry LDIF conversions + and marshalling. == Net::LDAP 0.0.4: August 15, 2006 * Undeprecated Net::LDAP#modify. Thanks to Justin Forder for diff --git a/lib/net/ldap/entry.rb b/lib/net/ldap/entry.rb index 4d6eb30..6bb810e 100644 --- a/lib/net/ldap/entry.rb +++ b/lib/net/ldap/entry.rb @@ -27,6 +27,7 @@ # +require 'base64' module Net @@ -157,18 +158,25 @@ class LDAP def to_ldif ary = [] ary << "dn: #{dn}\n" + v2 = "" # temp value, save on GC each_attribute do |k,v| - v.each {|v1| - ary << "#{k}: #{v1}\n" unless k == :dn - } + unless k == :dn + v.each {|v1| + v2 = if (k == :userpassword) || is_attribute_value_binary?(v1) + ": #{Base64.encode64(v1).chomp}" + else + " #{v1}" + end + ary << "#{k}:#{v2}\n" + } + end end ary << "\n" ary.join end #-- - # TODO, doesn't support binary representations yet (:: notation), - # and it doesn't handle broken lines. + # TODO, doesn't support broken lines. # It generates a SINGLE Entry object from an incoming LDIF stream # which is of course useless for big LDIF streams that encode # many objects. @@ -183,8 +191,12 @@ class LDAP entry = Entry.new ldif.split(/\r?\n/m).each {|line| break if line.length == 0 - if line =~ /\A([\w]+)::?[\s]*/ - entry[$1] = $' + if line =~ /\A([\w]+):(:?)[\s]*/ + entry[$1] <<= if $2 == ':' + Base64.decode64($') + else + $' + end end } entry.dn ? entry : nil @@ -219,6 +231,26 @@ class LDAP def write end + + #-- + # Internal convenience method. It seems like the standard + # approach in most LDAP tools to base64 encode an attribute + # value if its first or last byte is nonprintable, or if + # it's a password. + def is_attribute_value_binary? value + v = value.to_s + [v[0],v[-1]].each {|byt| + if byt.is_a?(Fixnum) and (byt < 33 or byt > 126) + return true + end + } + if v[0..0] == ':' or v[0..0] == '<' + return true + end + false + end + private :is_attribute_value_binary? + end # class Entry From f2d761794fa259f0315406c794a46a38f0caa95f Mon Sep 17 00:00:00 2001 From: blackhedd Date: Thu, 31 Aug 2006 01:48:33 +0000 Subject: [PATCH 139/231] tweaked the binary-attribute check. Now we look at the whole string instead of just the ends as ldapsearch does. --- lib/net/ldap/entry.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/net/ldap/entry.rb b/lib/net/ldap/entry.rb index 6bb810e..ce19cdd 100644 --- a/lib/net/ldap/entry.rb +++ b/lib/net/ldap/entry.rb @@ -236,13 +236,13 @@ class LDAP # Internal convenience method. It seems like the standard # approach in most LDAP tools to base64 encode an attribute # value if its first or last byte is nonprintable, or if - # it's a password. + # it's a password. But that turns out to be not nearly good + # enough. There are plenty of A/D attributes that are binary + # in the middle. This is probably a nasty performance killer. def is_attribute_value_binary? value v = value.to_s - [v[0],v[-1]].each {|byt| - if byt.is_a?(Fixnum) and (byt < 33 or byt > 126) - return true - end + v.each_byte {|byt| + return true if (byt < 33) || (byt > 126) } if v[0..0] == ':' or v[0..0] == '<' return true From f83934584d809f5648eecaee8ca0d2df3e899e59 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Thu, 31 Aug 2006 12:39:45 +0000 Subject: [PATCH 140/231] added Net::LDAP#search_root_dse. --- lib/net/ldap.rb | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 1947711..112cfed 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -1000,6 +1000,26 @@ module Net @result == 0 end + + # (Experimental, subject to change). + # Return the rootDSE record from the LDAP server as a Net::LDAP::Entry, or an + # empty Entry if the server doesn't return the record. + #-- + # cf. RFC4512 graf 5.1. + # Note that the rootDSE record we return on success has an empty DN, which is correct. + # On failure, the empty Entry will have a nil DN. There's no real reason for that, + # so it can be changed if desired. + # The funky number-disagreements in the set of attribute names is correct per the RFC. + # + def search_root_dse + rs = search( + :base=>"", + :scope=>SearchScope_BaseObject, + :attributes=>[:namingContexts,:supportedLdapVersion,:altServer,:supportedControl,:supportedExtension,:supportedFeatures,:supportedSASLMechanisms] + ) + (rs and rs.first) or Entry.new + end + end # class LDAP @@ -1122,6 +1142,7 @@ module Net # you won't get more than 1000 results back from a query. # This implementation is kindof clunky and should probably be refactored. # Also, is it my imagination, or are A/Ds the slowest directory servers ever??? + # OpenLDAP newer than version 2.2.0 supports paged searches. # def search args = {} search_filter = (args && args[:filter]) || Filter.eq( "objectclass", "*" ) From da6b81dd399eb59085e3b22c20f19b96b9f57c01 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Thu, 31 Aug 2006 12:40:26 +0000 Subject: [PATCH 141/231] change notes --- ChangeLog | 1 + 1 file changed, 1 insertion(+) diff --git a/ChangeLog b/ChangeLog index c0f10bf..a2bc592 100644 --- a/ChangeLog +++ b/ChangeLog @@ -11,6 +11,7 @@ "Jammy." * Added support for binary values in Net::LDAP::Entry LDIF conversions and marshalling. +* Supported rootDSE searches with a new API. == Net::LDAP 0.0.4: August 15, 2006 * Undeprecated Net::LDAP#modify. Thanks to Justin Forder for From 1eab2924f604c61cad04fdbe3949b5ee03fb0330 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Thu, 31 Aug 2006 13:23:25 +0000 Subject: [PATCH 142/231] fixed line-folding bug in to_ldif --- lib/net/ldap/entry.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/net/ldap/entry.rb b/lib/net/ldap/entry.rb index ce19cdd..09a6973 100644 --- a/lib/net/ldap/entry.rb +++ b/lib/net/ldap/entry.rb @@ -163,7 +163,7 @@ class LDAP unless k == :dn v.each {|v1| v2 = if (k == :userpassword) || is_attribute_value_binary?(v1) - ": #{Base64.encode64(v1).chomp}" + ": #{Base64.encode64(v1).chomp.gsub(/\n/m,"\n ")}" else " #{v1}" end From 91d5fff39c1eeddca15a2a11fda5a2d48444f6ef Mon Sep 17 00:00:00 2001 From: blackhedd Date: Thu, 31 Aug 2006 13:25:54 +0000 Subject: [PATCH 143/231] bumped version to 0.1.0 --- ChangeLog | 4 +++- lib/net/ldap.rb | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index a2bc592..ca9c676 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,7 @@ = Net::LDAP Changelog -== Net::LDAP 0.0.5: August xx, 2006 +== Net::LDAP 0.1.0: August xx, 2006 +* Bumped version up to 0.1.0, reflecting both code and API stability. * Silenced some annoying warnings in filter.rb. Thanks to "barjunk" for pointing this out. * Added Net::LDAP::Entry#to_ldif @@ -12,6 +13,7 @@ * Added support for binary values in Net::LDAP::Entry LDIF conversions and marshalling. * Supported rootDSE searches with a new API. +* Minor bug fixes here and there == Net::LDAP 0.0.4: August 15, 2006 * Undeprecated Net::LDAP#modify. Thanks to Justin Forder for diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 112cfed..810aef8 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -263,7 +263,7 @@ module Net class LdapError < Exception; end - VERSION = "0.0.5" + VERSION = "0.1.0" SearchScope_BaseObject = 0 From 58f5db971663fca777db57cb2bcaab1a2746f477 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 2 Sep 2006 04:43:35 +0000 Subject: [PATCH 144/231] performance improvement in BERParser#read_ber, replaced a lot of calls to Symbol#===. --- lib/net/ber.rb | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index fabcb0f..5bd6f0f 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -94,6 +94,8 @@ module Net end } +=begin + Replaced this case with if/else because Symbol#=== profiled surprisingly hot. obj = case objtype when :boolean newobj != "\000" @@ -116,6 +118,29 @@ module Net else raise BerError.new( "unsupported object type: class=#{tagclass}, encoding=#{encoding}, tag=#{tag}" ) end +=end + + obj = if objtype == :boolean + newobj != "\000" + elsif objtype == :string + (newobj || "").dup + elsif objtype == :integer + j = 0 + newobj.each_byte {|b| j = (j << 8) + b} + j + elsif objtype == :array + seq = [] + 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 + 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. From 6a371b988d354ebf72da26b4265e68116ae4aa51 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 2 Sep 2006 13:49:12 +0000 Subject: [PATCH 145/231] several performance optimizations --- lib/net/ber.rb | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index 5bd6f0f..0c4cca9 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -38,6 +38,20 @@ module Net class BerError < Exception; 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 + # This module is for mixing into IO and IO-like objects. module BERParser @@ -80,7 +94,10 @@ module Net lengthlength,contentlength = if n <= 127 [1,n] else - j = (0...(n & 127)).inject(0) {|mem,x| mem = (mem << 8) + getc} + # Replaced the inject because it profiles hot. + #j = (0...(n & 127)).inject(0) {|mem,x| mem = (mem << 8) + getc} + j = 0 + (n & 127).times {j = (j << 8) + getc} [1 + (n & 127), j] end @@ -120,16 +137,20 @@ module Net end =end - obj = if objtype == :boolean - newobj != "\000" - elsif objtype == :string - (newobj || "").dup + # == 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 == :array - seq = [] + #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 @@ -138,13 +159,17 @@ module Net seq << e end seq + elsif objtype == :boolean + newobj != "\000" 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" + # 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 end From 9ece3ff393a35b5f746a16d2c4d4aece30424d05 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 2 Sep 2006 14:11:31 +0000 Subject: [PATCH 146/231] more optimizations --- lib/net/ber.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index 0c4cca9..6f7b5af 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -82,9 +82,10 @@ module Net # this can throw TypeErrors and other nasties. # def read_ber syntax=nil - return nil if eof? + # don't bother with this line, since IO#getc by definition returns nil on eof. + #return nil if eof? - id = getc # don't trash this value, we'll use it later + id = getc 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 ] @@ -97,7 +98,7 @@ module Net # Replaced the inject because it profiles hot. #j = (0...(n & 127)).inject(0) {|mem,x| mem = (mem << 8) + getc} j = 0 - (n & 127).times {j = (j << 8) + getc} + read( n & 127 ).each_byte {|n1| j = (j << 8) + n1} [1 + (n & 127), j] end From 8d8a2be5f8d9687961ef0f00c405e2fc3b86928b Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 2 Sep 2006 14:45:25 +0000 Subject: [PATCH 147/231] noticed a possible parameter problem in paged-search, added comments. --- lib/net/ldap.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 810aef8..3c84b97 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -1217,6 +1217,14 @@ module Net # then query again for the next page of results. # If not, we're done. # Don't screw this up or we'll break every search we do. + # + # Noticed 02Sep06, look at the read_ber call in this loop, + # shouldn't that have a parameter of AsnSyntax? Does this + # just accidentally work? According to RFC-2696, the value + # expected in this position is of type OCTET STRING, covered + # in the default syntax supported by read_ber, so I guess + # we're ok. + # more_pages = false if result_code == 0 and controls controls.each do |c| From 3f4361348029f31fd08406987bd859affa48345a Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 2 Sep 2006 14:46:12 +0000 Subject: [PATCH 148/231] notes --- ChangeLog | 1 + 1 file changed, 1 insertion(+) diff --git a/ChangeLog b/ChangeLog index ca9c676..250c18d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -14,6 +14,7 @@ and marshalling. * Supported rootDSE searches with a new API. * Minor bug fixes here and there +* Some fairly extensive performance optimizations in the BER parser. == Net::LDAP 0.0.4: August 15, 2006 * Undeprecated Net::LDAP#modify. Thanks to Justin Forder for From 7fd108911504b4188cf47a081d262ed6936563ba Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 2 Sep 2006 14:48:42 +0000 Subject: [PATCH 149/231] comment --- lib/net/ber.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index 6f7b5af..f01b767 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -104,6 +104,7 @@ module Net newobj = read contentlength + # This exceptionally clever and clear bit of code is verrrry slow. objtype = nil [syntax, BuiltinSyntax].each {|syn| if syn && (ot = syn[tagclass]) && (ot = ot[encoding]) && ot[tag] From bc54a9c82691e3606c5753a8537724b92d7b761f Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 2 Sep 2006 16:48:07 +0000 Subject: [PATCH 150/231] added a compiler for ASN/BER syntax, avoids interpreting it for every packet we parse. --- lib/net/ber.rb | 42 ++++++++++++++++++++++++++++++++---------- lib/net/ldap.rb | 4 ++-- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index f01b767..446a4be 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -29,8 +29,6 @@ # - - module Net module BER @@ -52,6 +50,26 @@ module Net 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 @@ -59,7 +77,7 @@ module Net # Maybe this should have been a hash. TagClasses = [:universal, :application, :context_specific, :private] - BuiltinSyntax = { + BuiltinSyntax = BER.compile_syntax( { :universal => { :primitive => { 1 => :boolean, @@ -72,7 +90,7 @@ module Net 17 => :array } } - } + }) # # read_ber @@ -86,10 +104,10 @@ module Net #return nil if eof? id = getc 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 + #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 @@ -105,6 +123,7 @@ module Net newobj = read contentlength # This exceptionally clever and clear bit of code is verrrry slow. +=begin objtype = nil [syntax, BuiltinSyntax].each {|syn| if syn && (ot = syn[tagclass]) && (ot = ot[encoding]) && ot[tag] @@ -112,7 +131,9 @@ module Net break end } - +=end + objtype = (syntax && syntax[id]) || BuiltinSyntax[id] + =begin Replaced this case with if/else because Symbol#=== profiled surprisingly hot. obj = case objtype @@ -164,7 +185,8 @@ module Net elsif objtype == :boolean newobj != "\000" else - raise BerError.new( "unsupported object type: class=#{tagclass}, encoding=#{encoding}, tag=#{tag}" ) + #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. diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 3c84b97..503bec6 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -271,7 +271,7 @@ module Net SearchScope_WholeSubtree = 2 SearchScopes = [SearchScope_BaseObject, SearchScope_SingleLevel, SearchScope_WholeSubtree] - AsnSyntax = { + AsnSyntax = BER.compile_syntax({ :application => { :constructed => { 0 => :array, # BindRequest @@ -306,7 +306,7 @@ module Net 3 => :array, # Seach referral } } - } + }) DefaultHost = "127.0.0.1" DefaultPort = 389 From 21e243bf1377fc9e3b7e758ae4377c19fa93774a Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 2 Sep 2006 17:08:47 +0000 Subject: [PATCH 151/231] comment --- lib/net/ber.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index 446a4be..57c1b5b 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -160,7 +160,7 @@ module Net end =end - # == is expensive so sort this if/else so the common cases are at the top. + # == 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 || "" ) From 5c4de6a5c9a6639d587f94e827253908192cd6e9 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 2 Sep 2006 17:12:52 +0000 Subject: [PATCH 152/231] removed dead code --- lib/net/ldap/pdu.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/net/ldap/pdu.rb b/lib/net/ldap/pdu.rb index f195e29..829d7e8 100644 --- a/lib/net/ldap/pdu.rb +++ b/lib/net/ldap/pdu.rb @@ -158,11 +158,8 @@ class LdapPdu def parse_search_return sequence sequence.length >= 2 or raise LdapPduError @search_entry = LDAP::Entry.new( sequence[0] ) - #@search_dn = sequence[0] - #@search_attributes = {} sequence[1].each {|seq| @search_entry[seq[0]] = seq[1] - #@search_attributes[seq[0].downcase.intern] = seq[1] } end From b23ef85b60c46efff34b92d6659c8c052cc11423 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Thu, 7 Sep 2006 01:17:47 +0000 Subject: [PATCH 153/231] Added a size limit on searches. --- lib/net/ldap.rb | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 503bec6..f9aa421 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -555,6 +555,7 @@ module Net # * :return_result (a boolean specifying whether to return a result set). # * :attributes_only (a boolean flag, defaults false) # * :scope (one of: Net::LDAP::SearchScope_BaseObject, Net::LDAP::SearchScope_SingleLevel, Net::LDAP::SearchScope_WholeSubtree. Default is WholeSubtree.) + # * :size (an integer indicating the maximum number of search entries to return. Default is zero, which signifies no limit.) # # #search queries the LDAP server and passes each entry to the # caller-supplied block, as an object of type Net::LDAP::Entry. @@ -1150,6 +1151,8 @@ module Net search_base = (args && args[:base]) || "dc=example,dc=com" search_attributes = ((args && args[:attributes]) || []).map {|attr| attr.to_s.to_ber} return_referrals = args && args[:return_referrals] == true + sizelimit = (args && args[:size].to_i) || 0 + raise LdapError.new( "invalid search-size" ) unless sizelimit >= 0 attributes_only = (args and args[:attributes_only] == true) scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree @@ -1161,15 +1164,22 @@ module Net # by running slapd in debug mode. Go figure. rfc2696_cookie = [126, ""] result_code = 0 + n_results = 0 loop { # should collect this into a private helper to clarify the structure + query_limit = if (sizelimit > 0) && ((sizelimit - n_results) < 126) + sizelimit - n_results + else + 0 + end + request = [ search_base.to_ber, scope.to_ber_enumerated, 0.to_ber_enumerated, - 0.to_ber, + query_limit.to_ber, # size limit 0.to_ber, attributes_only.to_ber, search_filter.to_ber, @@ -1193,6 +1203,7 @@ module Net while (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) case pdu.app_tag when 4 # search-data + n_results += 1 yield( pdu.search_entry ) if block_given? when 19 # search-referral if return_referrals From a49e0bf792bdefaca1f53cec5489da4d172a6a66 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Thu, 7 Sep 2006 01:29:33 +0000 Subject: [PATCH 154/231] comments --- lib/net/ldap.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index f9aa421..ec78ea9 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -1162,6 +1162,16 @@ module Net # page limit of 1000 records, but openLDAP newer than version 2.2.0 chokes # on anything bigger than 126. You get a silent error that is easily visible # by running slapd in debug mode. Go figure. + # + # Changed this around 06Sep06 to support a caller-specified search-size limit. + # Because we ALWAYS do paged searches, we have to work around the problem that + # it's not legal to specify a "normal" sizelimit (in the body of the search request) + # that is larger than the page size we're requesting. Unfortunately, I have the + # feeling that this will break with LDAP servers that don't support paged searches!!! + # (Because we pass zero as the sizelimit on search rounds when the remaining limit + # is larger than our max page size of 126. In these cases, I think the caller's + # search limit will be ignored!) + # rfc2696_cookie = [126, ""] result_code = 0 n_results = 0 From 580e4e749cad112a2eac403141ae9b5524023446 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Thu, 7 Sep 2006 01:35:14 +0000 Subject: [PATCH 155/231] comments --- lib/net/ldap.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index ec78ea9..8d71fe3 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -1171,6 +1171,9 @@ module Net # (Because we pass zero as the sizelimit on search rounds when the remaining limit # is larger than our max page size of 126. In these cases, I think the caller's # search limit will be ignored!) + # CONFIRMED: This code doesn't work on LDAPs that don't support paged searches + # when the size limit is larger than 126. We're going to have to do a root-DSE record + # search and not do a paged search if the LDAP doesn't support it. Yuck. # rfc2696_cookie = [126, ""] result_code = 0 From e3a743d3923cfafc5a46e6c8c39e9501a230af07 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Thu, 7 Sep 2006 20:43:59 +0000 Subject: [PATCH 156/231] improved search size-limit support. --- lib/net/ldap.rb | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 8d71fe3..15ab793 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -626,6 +626,10 @@ module Net # array, so we can provide sort methods and what-not. # def search args = {} + unless args[:ignore_server_caps] + args[:paged_searches_supported] = paged_searches_supported? + end + args[:base] ||= @base result_set = (args and args[:return_result] == false) ? nil : [] @@ -1011,9 +1015,13 @@ module Net # On failure, the empty Entry will have a nil DN. There's no real reason for that, # so it can be changed if desired. # The funky number-disagreements in the set of attribute names is correct per the RFC. + # We may be called by #search itself, which may need to determine things like paged + # search capabilities. So to avoid an infinite regress, set :ignore_server_caps, + # which prevents us getting called recursively. # def search_root_dse rs = search( + :ignore_server_caps=>true, :base=>"", :scope=>SearchScope_BaseObject, :attributes=>[:namingContexts,:supportedLdapVersion,:altServer,:supportedControl,:supportedExtension,:supportedFeatures,:supportedSASLMechanisms] @@ -1021,6 +1029,16 @@ module Net (rs and rs.first) or Entry.new end + #-- + # Convenience method to query server capabilities. + # Only do this once per Net::LDAP object. + # Note, we call a search, and we might be called from inside a search! + # MUST refactor the root_dse call out. + def paged_searches_supported? + @server_caps ||= search_root_dse + @server_caps[:supportedcontrol].include?(LdapControls::PagedResults) + end + end # class LDAP @@ -1153,6 +1171,7 @@ module Net return_referrals = args && args[:return_referrals] == true sizelimit = (args && args[:size].to_i) || 0 raise LdapError.new( "invalid search-size" ) unless sizelimit >= 0 + paged_searches_supported = (args && args[:paged_searches_supported]) attributes_only = (args and args[:attributes_only] == true) scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree @@ -1182,10 +1201,13 @@ module Net loop { # should collect this into a private helper to clarify the structure - query_limit = if (sizelimit > 0) && ((sizelimit - n_results) < 126) - sizelimit - n_results - else - 0 + query_limit = 0 + if sizelimit > 0 + if paged_searches_supported + query_limit = (((sizelimit - n_results) < 126) ? (sizelimit - n_results) : 0) + else + query_limit = sizelimit + end end request = [ @@ -1273,7 +1295,6 @@ module Net - #-- # modify # TODO, need to support a time limit, in case the server fails to respond. From 54d929dae524de27ba73d4f14995d92a903cb1b8 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 29 Sep 2006 12:44:28 +0000 Subject: [PATCH 157/231] removed a comment which is no longer true. --- lib/net/ldap/entry.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/net/ldap/entry.rb b/lib/net/ldap/entry.rb index 09a6973..6e3949b 100644 --- a/lib/net/ldap/entry.rb +++ b/lib/net/ldap/entry.rb @@ -153,8 +153,6 @@ class LDAP # Converts the Entry to a String, representing the # Entry's attributes in LDIF format. #-- - # TODO, this doesn't support binary representations, - # nor does it break overlength lines. def to_ldif ary = [] ary << "dn: #{dn}\n" From af27b55d89d2b3dfe2a8eaf7909d68e7ddd6c705 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 30 Sep 2006 12:12:52 +0000 Subject: [PATCH 158/231] Factored out BindResponse handling, to support SASL server creds. --- lib/net/ldap/pdu.rb | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/net/ldap/pdu.rb b/lib/net/ldap/pdu.rb index 829d7e8..4372727 100644 --- a/lib/net/ldap/pdu.rb +++ b/lib/net/ldap/pdu.rb @@ -82,7 +82,7 @@ class LdapPdu case @app_tag when BindResult - parse_ldap_result ber_object[1] + parse_bind_response ber_object[1] when SearchReturnedData parse_search_return ber_object[1] when SearchResultReferral @@ -129,6 +129,18 @@ class LdapPdu end private :parse_ldap_result + # + # parse_bind_response + # A Bind Response may have an additional field, ID [7], serverSaslCreds, per RFC 2251 pgh 4.2.3. + # + def parse_bind_response sequence + sequence.length >= 3 or raise LdapPduError + @ldap_result = {:resultCode => sequence[0], :matchedDN => sequence[1], :errorMessage => sequence[2]} + @ldap_result[:serverSaslCreds] = sequence[3] if sequence.length >= 4 + @ldap_result + end + private :parse_bind_response + # # parse_search_return # Definition from RFC 1777 (we're handling application-4 here) From f3e87dd4065e59e49034a2b5b332c50127b0dc09 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 30 Sep 2006 12:16:25 +0000 Subject: [PATCH 159/231] Added support for SASL data types and requests. Connection#bind_sasl is incomplete, don't use it yet. --- lib/net/ldap.rb | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 15ab793..5d813eb 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -300,10 +300,12 @@ module Net 0 => :string, # password 1 => :string, # Kerberos v4 2 => :string, # Kerberos v5 + 7 => :string, # serverSaslCreds }, :constructed => { 0 => :array, # RFC-2251 Control 3 => :array, # Seach referral + 7 => :array, # serverSaslCreds } } }) @@ -321,6 +323,7 @@ module Net 3 => "Time Limit Exceeded", 4 => "Size Limit Exceeded", 12 => "Unavailable crtical extension", + 14 => "saslBindInProgress", 16 => "No Such Attribute", 17 => "Undefined Attribute Type", 20 => "Attribute or Value Exists", @@ -710,6 +713,7 @@ module Net @result == 0 end + # # #bind_as is for testing authentication credentials. # @@ -1127,12 +1131,21 @@ module Net # bind # def bind auth - user,psw = case auth[:method] - when :anonymous - ["",""] - when :simple - [auth[:username] || auth[:dn], auth[:password]] + + meth = auth[:method] + user,psw = "","" + + if meth == :simple + user,psw = [auth[:username] || auth[:dn], auth[:password]] + elsif meth == :sasl + return bind_sasl( auth ) # Note the early return. end + + #user,psw = if auth[:method] == :anonymous + # ["",""] + #when :simple + # [auth[:username] || auth[:dn], auth[:password]] + #end raise LdapError.new( "invalid binding information" ) unless (user && psw) msgid = next_msgid.to_ber @@ -1144,6 +1157,24 @@ module Net pdu.result_code end + #-- + # bind_sasl + # PROVISIONAL, only for testing SASL implementations. Will disappear, so DON'T USE THIS. + def bind_sasl auth + user = auth[:username] or raise LdapError.new( "invalid username" ) + msgid = next_msgid.to_ber + sasl = ["GSS-SPNEGO".to_ber, "NTLMSSP\000\001\000\000\000\227\202\010\340\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000".to_ber].to_ber_contextspecific(3) + sasl = ["GSSAPI".to_ber].to_ber_contextspecific(3) + request = [LdapVersion.to_ber, "".to_ber, sasl].to_ber_appsequence(0) + request_pkt = [msgid, request].to_ber_sequence + @conn.write request_pkt +p request_pkt + + (be = @conn.read_ber(AsnSyntax) and pdu = Net::LdapPdu.new( be )) or raise LdapError.new( "no bind result" ) +p pdu + pdu.result_code + end + #-- # search # Alternate implementation, this yields each search entry to the caller From 14c3db9b83b0aa4da8e685d777252cbd9d194aa4 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 30 Sep 2006 13:28:05 +0000 Subject: [PATCH 160/231] provisionally implemented NTLM authentication. --- lib/net/ldap.rb | 28 ++++++++++++++++++++++------ lib/net/ldap/pdu.rb | 6 ++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 5d813eb..e3f9513 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -1159,21 +1159,37 @@ module Net #-- # bind_sasl - # PROVISIONAL, only for testing SASL implementations. Will disappear, so DON'T USE THIS. + # PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET. + # Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to integrate it without + # introducing an external dependency. + # This is also wrong for another reason: we're assuming Microsoft GSSAPI negotiation. + # Wee need to introduce some extra parameters to select that mode. def bind_sasl auth - user = auth[:username] or raise LdapError.new( "invalid username" ) + require 'ntlm.rb' + user,psw = [auth[:username] || auth[:dn], auth[:password]] + raise LdapError.new( "invalid binding information" ) unless (user && psw) msgid = next_msgid.to_ber - sasl = ["GSS-SPNEGO".to_ber, "NTLMSSP\000\001\000\000\000\227\202\010\340\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000".to_ber].to_ber_contextspecific(3) - sasl = ["GSSAPI".to_ber].to_ber_contextspecific(3) + sasl = ["GSS-SPNEGO".to_ber, NTLM::Message::Type1.new.serialize.to_ber].to_ber_contextspecific(3) + request = [LdapVersion.to_ber, "".to_ber, sasl].to_ber_appsequence(0) + request_pkt = [msgid, request].to_ber_sequence + @conn.write request_pkt + + (be = @conn.read_ber(AsnSyntax) and pdu = Net::LdapPdu.new( be )) or raise LdapError.new( "no bind result" ) + return pdu.result_code unless pdu.result_code == 14 # saslBindInProgress + + t2 = NTLM::Message.parse( pdu.result_server_sasl_creds ) # WARNING, can Kajimoto's code throw nasty errors? + t3 = t2.response( {:user => user, :password => psw}, {:ntlmv2 => true} ) + + msgid = next_msgid.to_ber + sasl = ["GSS-SPNEGO".to_ber, t3.serialize.to_ber].to_ber_contextspecific(3) request = [LdapVersion.to_ber, "".to_ber, sasl].to_ber_appsequence(0) request_pkt = [msgid, request].to_ber_sequence @conn.write request_pkt -p request_pkt (be = @conn.read_ber(AsnSyntax) and pdu = Net::LdapPdu.new( be )) or raise LdapError.new( "no bind result" ) -p pdu pdu.result_code end + private :bind_sasl #-- # search diff --git a/lib/net/ldap/pdu.rb b/lib/net/ldap/pdu.rb index 4372727..ac8438e 100644 --- a/lib/net/ldap/pdu.rb +++ b/lib/net/ldap/pdu.rb @@ -119,6 +119,12 @@ class LdapPdu @ldap_controls || [] end + # Return serverSaslCreds, which are only present in BindResponse packets. + # Messy. Does this functionality belong somewhere else? + # We ought to refactor the accessors of this class before they get any kludgier. + def result_server_sasl_creds + @ldap_result && @ldap_result[:serverSaslCreds] + end # # parse_ldap_result From 82e2cc3592d10c9560c72fd2784dbe845d348c8b Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 30 Sep 2006 14:59:13 +0000 Subject: [PATCH 161/231] Fixed bug noticed by Matthias Tarasiewicz: from_single_ldif_string was returning DN attributes with a prepended nil element. --- lib/net/ldap/entry.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/net/ldap/entry.rb b/lib/net/ldap/entry.rb index 6e3949b..2f16e31 100644 --- a/lib/net/ldap/entry.rb +++ b/lib/net/ldap/entry.rb @@ -184,9 +184,20 @@ class LDAP # Eventually, we should have a class method that parses large LDIF # streams into individual LDIF blocks (delimited by blank lines) # and passes them here. + # + # There is one oddity, noticed by Matthias Tarasiewicz: as originally + # written, this code would return an Entry object in which the DN + # attribute consisted of a two-element array, and the first element was + # nil. That's because Entry#initialize doesn't like to create an object + # without a DN attribute so it adds one: nil. The workaround here is + # to wipe out the nil DN after creating the Entry object, and trust the + # LDIF string to fill it in. If it doesn't we return a nil at the end. + # (30Sep06, FCianfrocca) + # class << self def from_single_ldif_string ldif entry = Entry.new + entry[:dn] = [] ldif.split(/\r?\n/m).each {|line| break if line.length == 0 if line =~ /\A([\w]+):(:?)[\s]*/ From de933da9583f8b5218b4ca71615b9b3b27924bf9 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 30 Sep 2006 15:01:42 +0000 Subject: [PATCH 162/231] some change notes --- ChangeLog | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ChangeLog b/ChangeLog index 250c18d..319108d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -15,6 +15,9 @@ * Supported rootDSE searches with a new API. * Minor bug fixes here and there * Some fairly extensive performance optimizations in the BER parser. +* Fixed a bug in Net::LDAP::Entry::from_single_ldif_string noticed by + Matthias Tarasiewicz. +* Added [reliminary (still undocumented) support for SASL authentication. == Net::LDAP 0.0.4: August 15, 2006 * Undeprecated Net::LDAP#modify. Thanks to Justin Forder for From 7e32ba177c36473ca2caf347b621e71643f3faa1 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 30 Sep 2006 23:02:12 +0000 Subject: [PATCH 163/231] improved the SASL authentication method, and added a simple accessor for MS/AD authentication via GSS-SPNEGO. --- lib/net/ldap.rb | 110 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 77 insertions(+), 33 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index e3f9513..e7c2eb4 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -1052,6 +1052,7 @@ module Net class Connection # :nodoc: LdapVersion = 3 + MaxSaslChallenges = 10 #-- @@ -1131,21 +1132,30 @@ module Net # bind # def bind auth - meth = auth[:method] - user,psw = "","" - - if meth == :simple - user,psw = [auth[:username] || auth[:dn], auth[:password]] + if [:simple, :anonymous, :anon].include?( meth ) + bind_simple auth elsif meth == :sasl - return bind_sasl( auth ) # Note the early return. + bind_sasl( auth ) + elsif meth == :gss_spnego + bind_gss_spnego( auth ) + else + raise LdapError.new( "unsupported auth method (#{meth})" ) + end + end + + #-- + # bind_simple + # Implements a simple user/psw authentication. + # Accessed by calling #bind with a method of :simple or :anonymous. + # + def bind_simple auth + user,psw = if auth[:method] == :simple + [auth[:username] || auth[:dn], auth[:password]] + else + ["",""] end - #user,psw = if auth[:method] == :anonymous - # ["",""] - #when :simple - # [auth[:username] || auth[:dn], auth[:password]] - #end raise LdapError.new( "invalid binding information" ) unless (user && psw) msgid = next_msgid.to_ber @@ -1159,37 +1169,71 @@ module Net #-- # bind_sasl + # Required parameters: :mechanism, :initial_credential and :challenge_response + # Mechanism is a string value that will be passed in the SASL-packet's "mechanism" field. + # Initial credential is most likely a string. It's passed in the initial BindRequest + # that goes to the server. In some protocols, it may be empty. + # Challenge-response is a Ruby proc that takes a single parameter and returns an object + # that will typically be a string. The challenge-response block is called when the server + # returns a BindResponse with a result code of 14 (saslBindInProgress). The challenge-response + # block receives a parameter containing the data returned by the server in the saslServerCreds + # field of the LDAP BindResponse packet. The challenge-response block may be called multiple + # times during the course of a SASL authentication, and each time it must return a value + # that will be passed back to the server as the credential data in the next BindRequest packet. + # + def bind_sasl auth + mech,cred,chall = auth[:mechanism],auth[:initial_credential],auth[:challenge_response] + raise LdapError.new( "invalid binding information" ) unless (mech && cred && chall) + + n = 0 + loop { + msgid = next_msgid.to_ber + sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3) + request = [LdapVersion.to_ber, "".to_ber, sasl].to_ber_appsequence(0) + request_pkt = [msgid, request].to_ber_sequence + @conn.write request_pkt + + (be = @conn.read_ber(AsnSyntax) and pdu = Net::LdapPdu.new( be )) or raise LdapError.new( "no bind result" ) + return pdu.result_code unless pdu.result_code == 14 # saslBindInProgress + raise LdapError.new("sasl-challenge overflow") if ((n += 1) > MaxSaslChallenges) + + cred = chall.call( pdu.result_server_sasl_creds ) + } + + raise LdapError.new( "why are we here?") + end + private :bind_sasl + + #-- + # bind_gss_spnego # PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET. # Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to integrate it without # introducing an external dependency. - # This is also wrong for another reason: we're assuming Microsoft GSSAPI negotiation. - # Wee need to introduce some extra parameters to select that mode. - def bind_sasl auth + # This authentication method is accessed by calling #bind with a :method parameter of + # :gss_spnego. It requires :username and :password attributes, just like the :simple + # authentication method. It performs a GSS-SPNEGO authentication with the server, which + # is presumed to be a Microsoft Active Directory. + # + def bind_gss_spnego auth require 'ntlm.rb' + user,psw = [auth[:username] || auth[:dn], auth[:password]] raise LdapError.new( "invalid binding information" ) unless (user && psw) - msgid = next_msgid.to_ber - sasl = ["GSS-SPNEGO".to_ber, NTLM::Message::Type1.new.serialize.to_ber].to_ber_contextspecific(3) - request = [LdapVersion.to_ber, "".to_ber, sasl].to_ber_appsequence(0) - request_pkt = [msgid, request].to_ber_sequence - @conn.write request_pkt - (be = @conn.read_ber(AsnSyntax) and pdu = Net::LdapPdu.new( be )) or raise LdapError.new( "no bind result" ) - return pdu.result_code unless pdu.result_code == 14 # saslBindInProgress + nego = proc {|challenge| + t2_msg = NTLM::Message.parse( challenge ) + t3_msg = t2_msg.response( {:user => user, :password => psw}, {:ntlmv2 => true} ) + t3_msg.serialize + } - t2 = NTLM::Message.parse( pdu.result_server_sasl_creds ) # WARNING, can Kajimoto's code throw nasty errors? - t3 = t2.response( {:user => user, :password => psw}, {:ntlmv2 => true} ) - - msgid = next_msgid.to_ber - sasl = ["GSS-SPNEGO".to_ber, t3.serialize.to_ber].to_ber_contextspecific(3) - request = [LdapVersion.to_ber, "".to_ber, sasl].to_ber_appsequence(0) - request_pkt = [msgid, request].to_ber_sequence - @conn.write request_pkt - - (be = @conn.read_ber(AsnSyntax) and pdu = Net::LdapPdu.new( be )) or raise LdapError.new( "no bind result" ) - pdu.result_code + bind_sasl( { + :method => :sasl, + :mechanism => "GSS-SPNEGO", + :initial_credential => NTLM::Message::Type1.new.serialize, + :challenge_response => nego + }) end - private :bind_sasl + private :bind_gss_spnego #-- # search From 239752408df1e08e13e2820d176db300caded584 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 27 Oct 2006 00:04:47 +0000 Subject: [PATCH 164/231] removed a duplicate entry --- lib/net/ldap.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index e7c2eb4..54f0da2 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -329,7 +329,6 @@ module Net 20 => "Attribute or Value Exists", 32 => "No Such Object", 34 => "Invalid DN Syntax", - 48 => "Invalid DN Syntax", 48 => "Inappropriate Authentication", 49 => "Invalid Credentials", 50 => "Insufficient Access Rights", From 0bf59f7897f93231605d8dcc1f596b244d3be834 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 27 Oct 2006 00:10:26 +0000 Subject: [PATCH 165/231] removed an extraneous LdapError value, noticed by Kouhei Sutou. --- ChangeLog | 1 + Rakefile | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 319108d..f6bc53f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -18,6 +18,7 @@ * Fixed a bug in Net::LDAP::Entry::from_single_ldif_string noticed by Matthias Tarasiewicz. * Added [reliminary (still undocumented) support for SASL authentication. +* Removed an erroneous LdapError value, noticed by Kouhei Sutou. == Net::LDAP 0.0.4: August 15, 2006 * Undeprecated Net::LDAP#modify. Thanks to Justin Forder for diff --git a/Rakefile b/Rakefile index 31a487f..7d36a9e 100644 --- a/Rakefile +++ b/Rakefile @@ -68,7 +68,7 @@ end spec = eval(File.read("net-ldap.gemspec")) spec.version = $version desc "Build the RubyGem for #$name." -task :gem => [ :test ] +task :gem => [] #[ :test ] Rake::GemPackageTask.new(spec) do |g| if $can_minitar g.need_tar = false From 1748afac2427852c0a93be34b97b618752c443b3 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 27 Oct 2006 00:14:21 +0000 Subject: [PATCH 166/231] added blanks to the attribute filter in Filter#construct --- lib/net/ldap/filter.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/net/ldap/filter.rb b/lib/net/ldap/filter.rb index 686c4f3..f97a802 100644 --- a/lib/net/ldap/filter.rb +++ b/lib/net/ldap/filter.rb @@ -352,6 +352,7 @@ class FilterParser #:nodoc: # Added a greatly-augmented filter contributed by Andre Nathan # for detecting special characters in values. (15Aug06) + # Added blanks to the attribute filter (26Oct06) def parse_filter_branch scanner scanner.scan(/\s*/) if token = scanner.scan( /[\w\-_]+/ ) @@ -359,7 +360,7 @@ class FilterParser #:nodoc: if op = scanner.scan( /\=|\<\=|\<|\>\=|\>|\!\=/ ) scanner.scan(/\s*/) #if value = scanner.scan( /[\w\*\.]+/ ) (ORG) - if value = scanner.scan( /[\w\*\.\+\-@=#\$%&!]+/ ) + if value = scanner.scan( /[\w\*\.\+\-@=#\$%&! ]+/ ) case op when "=" Filter.eq( token, value ) From c07c5af1b5a13cc9f5e02d7884ac7cb888ee96d6 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 27 Oct 2006 00:15:41 +0000 Subject: [PATCH 167/231] change note --- ChangeLog | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ChangeLog b/ChangeLog index f6bc53f..08f78e1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -19,6 +19,9 @@ Matthias Tarasiewicz. * Added [reliminary (still undocumented) support for SASL authentication. * Removed an erroneous LdapError value, noticed by Kouhei Sutou. +* Supported attributes containing blanks (cn=Babs Jensen) to + Filter#construct. Suggested by an anonymous Rubyforge user. + == Net::LDAP 0.0.4: August 15, 2006 * Undeprecated Net::LDAP#modify. Thanks to Justin Forder for From e001f0413e28c0177dfe2db7a33927a71f3f77e4 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 27 Nov 2006 22:56:13 +0000 Subject: [PATCH 168/231] Added syntax support for UnbindRequest bodies. --- lib/net/ldap.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 54f0da2..dc24b83 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -273,6 +273,9 @@ module Net AsnSyntax = BER.compile_syntax({ :application => { + :primitive => { + 2 => :null # UnbindRequest body + }, :constructed => { 0 => :array, # BindRequest 1 => :array, # BindResponse From fad144bcbc55b0235d3de9927ad6a3c20810b748 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 27 Nov 2006 22:57:17 +0000 Subject: [PATCH 169/231] Supported ASN NULL objects. Kindof trivial but nice for completeness. --- lib/net/ber.rb | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index 57c1b5b..c9038dc 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -50,6 +50,10 @@ module Net end end + class BerIdentifiedNull + attr_accessor :ber_identifier + end + #-- # This condenses our nicely self-documenting ASN hashes down # to an array for fast lookups. @@ -78,18 +82,18 @@ module Net TagClasses = [:universal, :application, :context_specific, :private] BuiltinSyntax = BER.compile_syntax( { - :universal => { - :primitive => { - 1 => :boolean, - 2 => :integer, - 4 => :string, - 10 => :integer, - }, - :constructed => { - 16 => :array, - 17 => :array - } - } + :universal => { + :primitive => { + 1 => :boolean, + 2 => :integer, + 4 => :string, + 10 => :integer, + }, + :constructed => { + 16 => :array, + 17 => :array + } + } }) # @@ -184,6 +188,10 @@ module Net 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}" ) From 832334116aafc827803b29147f9d235c3f9aaba2 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 27 Nov 2006 22:59:03 +0000 Subject: [PATCH 170/231] Supported BindRequest, SearchRequest, and UnbindRequest --- lib/net/ldap/pdu.rb | 47 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/lib/net/ldap/pdu.rb b/lib/net/ldap/pdu.rb index ac8438e..7a98112 100644 --- a/lib/net/ldap/pdu.rb +++ b/lib/net/ldap/pdu.rb @@ -36,7 +36,10 @@ class LdapPduError < Exception; end class LdapPdu + BindRequest = 0 BindResult = 1 + UnbindRequest = 2 + SearchRequest = 3 SearchReturnedData = 4 SearchResult = 5 ModifyResponse = 7 @@ -48,6 +51,7 @@ class LdapPdu attr_reader :msg_id, :app_tag attr_reader :search_dn, :search_attributes, :search_entry attr_reader :search_referrals + attr_reader :search_parameters, :bind_parameters # # initialize @@ -74,7 +78,13 @@ class LdapPdu def initialize ber_object begin @msg_id = ber_object[0].to_i - @app_tag = ber_object[1].ber_identifier - 0x60 + # Modified 25Nov06. We want to "un-decorate" the ber-identifier + # of the incoming packet. Originally we did this by subtracting 0x60, + # which ASSUMES the identifier is a constructed app-specific value. + # But at least one value (UnbindRequest) is app-specific primitive. + # So it makes more sense just to grab the bottom five bits. + #@app_tag = ber_object[1].ber_identifier - 0x60 + @app_tag = ber_object[1].ber_identifier & 31 rescue # any error becomes a data-format error raise LdapPduError.new( "ldap-pdu format error" ) @@ -98,6 +108,12 @@ class LdapPdu parse_ldap_result ber_object[1] when ModifyRDNResponse parse_ldap_result ber_object[1] + when SearchRequest + parse_ldap_search_request ber_object[1] + when BindRequest + parse_bind_request ber_object[1] + when UnbindRequest + parse_unbind_request ber_object[1] else raise LdapPduError.new( "unknown pdu-type: #{@app_tag}" ) end @@ -213,6 +229,35 @@ class LdapPdu private :parse_controls + # (provisional, must document) + def parse_ldap_search_request sequence + s = OpenStruct.new + s.base_object, + s.scope, + s.deref_aliases, + s.size_limit, + s.time_limit, + s.types_only, + s.filter, + s.attributes = sequence + @search_parameters = s + end + + # (provisional, must document) + def parse_bind_request sequence + s = OpenStruct.new + s.version, + s.name, + s.authentication = sequence + @bind_parameters = s + end + + + # (provisional, must document) + # UnbindRequest has no content so this is a no-op. + def parse_unbind_request sequence + end + end From 4db9f2c7ccb5e2833be2446fa4c52a50a98381bd Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 27 Nov 2006 23:07:24 +0000 Subject: [PATCH 171/231] Added a more-disciplined version of #read_ber. --- lib/net/ber.rb | 107 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 73 insertions(+), 34 deletions(-) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index c9038dc..b272b84 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -102,6 +102,10 @@ module Net # 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. + #-- + # 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#getc by definition returns nil on eof. @@ -127,42 +131,8 @@ module Net newobj = read contentlength # This exceptionally clever and clear bit of code is verrrry slow. -=begin - objtype = nil - [syntax, BuiltinSyntax].each {|syn| - if syn && (ot = syn[tagclass]) && (ot = ot[encoding]) && ot[tag] - objtype = ot[tag] - break - end - } -=end objtype = (syntax && syntax[id]) || BuiltinSyntax[id] -=begin - Replaced this case with if/else because Symbol#=== profiled surprisingly hot. - 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 || "" ) - # 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 - end - seq - else - raise BerError.new( "unsupported object type: class=#{tagclass}, encoding=#{encoding}, tag=#{tag}" ) - end -=end # == is expensive so sort this if/else so the common cases are at the top. obj = if objtype == :string @@ -206,6 +176,75 @@ module Net 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 == :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 From 4b110ff8862982656c236d52ba7d6d9c2c43869f Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 27 Nov 2006 23:09:18 +0000 Subject: [PATCH 172/231] Added String#read_ber! --- lib/net/ber.rb | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index b272b84..d44935b 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -277,14 +277,24 @@ rescue LoadError # IO so we'd pick it up above. But you'd be wrong. end + + class String - def read_ber syntax=nil - StringIO.new(self).read_ber(syntax) - end + include Net::BER::BERParser + def read_ber syntax=nil + StringIO.new(self).read_ber(syntax) + 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 - - #---------------------------------------------- From 2ee2a551b1e695b26e2022bbf3d42c989d88e84c Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 29 Nov 2006 13:42:21 +0000 Subject: [PATCH 173/231] Added syntax support for SearchFilter ANDs and ORs. --- ChangeLog | 2 ++ lib/net/ldap.rb | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 08f78e1..e8dcf6a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -21,6 +21,8 @@ * Removed an erroneous LdapError value, noticed by Kouhei Sutou. * Supported attributes containing blanks (cn=Babs Jensen) to Filter#construct. Suggested by an anonymous Rubyforge user. +* Supported several constructs from the server side of the LDAP protocol. +* Added a "consuming" String#read_ber! method. == Net::LDAP 0.0.4: August 15, 2006 diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index dc24b83..a475562 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -306,7 +306,9 @@ module Net 7 => :string, # serverSaslCreds }, :constructed => { - 0 => :array, # RFC-2251 Control + 0 => :array, # RFC-2251 Control and Filter-AND + 1 => :array, # SearchFilter-OR + 2 => :array, # SearchFilter-NOT 3 => :array, # Seach referral 7 => :array, # serverSaslCreds } From 1314cd1f28ce85ece23d37e8b676aa01d839ca54 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 29 Nov 2006 13:43:07 +0000 Subject: [PATCH 174/231] notes --- ChangeLog | 1 + 1 file changed, 1 insertion(+) diff --git a/ChangeLog b/ChangeLog index e8dcf6a..4c18001 100644 --- a/ChangeLog +++ b/ChangeLog @@ -23,6 +23,7 @@ Filter#construct. Suggested by an anonymous Rubyforge user. * Supported several constructs from the server side of the LDAP protocol. * Added a "consuming" String#read_ber! method. +* Added missing synactic support for Filter ANDs and NOTs. == Net::LDAP 0.0.4: August 15, 2006 From 7fe954f1994aba8dd4bf3abde70018220da6fb0a Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 29 Nov 2006 18:09:14 +0000 Subject: [PATCH 175/231] Supported some additional BER encodings. --- ChangeLog | 3 ++- lib/net/ber.rb | 1 + lib/net/ldap.rb | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 4c18001..5ed5075 100644 --- a/ChangeLog +++ b/ChangeLog @@ -23,7 +23,8 @@ Filter#construct. Suggested by an anonymous Rubyforge user. * Supported several constructs from the server side of the LDAP protocol. * Added a "consuming" String#read_ber! method. -* Added missing synactic support for Filter ANDs and NOTs. +* Added missing synactic support for Filter ANDs, NOTs and a few other + things. == Net::LDAP 0.0.4: August 15, 2006 diff --git a/lib/net/ber.rb b/lib/net/ber.rb index d44935b..8cab342 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -88,6 +88,7 @@ module Net 2 => :integer, 4 => :string, 10 => :integer, + 13 => :string # (OID) }, :constructed => { 16 => :array, diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index a475562..f4c4086 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -310,6 +310,7 @@ module Net 1 => :array, # SearchFilter-OR 2 => :array, # SearchFilter-NOT 3 => :array, # Seach referral + 4 => :array, # unknown use in Microsoft Outlook 7 => :array, # serverSaslCreds } } From 8dfbf21302b9e51f0cc71b563c16225ab0b463af Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 8 Dec 2006 08:39:13 +0000 Subject: [PATCH 176/231] supported univeral BER types 5 (NULL) and 6 (OID) --- lib/net/ber.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index 8cab342..335567d 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -87,8 +87,10 @@ module Net 1 => :boolean, 2 => :integer, 4 => :string, + 5 => :null, + 6 => :string, # (OID) 10 => :integer, - 13 => :string # (OID) + 13 => :string # (relative OID) }, :constructed => { 16 => :array, From 686096565179ce45f5c5d5f9e9084bf18b4c22bb Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 8 Dec 2006 10:40:17 +0000 Subject: [PATCH 177/231] supported translation of the X.690 OID representation. More useful for SNMP than for LDAP. --- lib/net/ber.rb | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index 335567d..bf4f0d9 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -88,7 +88,7 @@ module Net 2 => :integer, 4 => :string, 5 => :null, - 6 => :string, # (OID) + 6 => :oid, 10 => :integer, 13 => :string # (relative OID) }, @@ -147,6 +147,23 @@ module Net 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 From 1d1a1a6f7d36b872fc761e60ab67f4abdbbfba57 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 8 Dec 2006 14:48:57 +0000 Subject: [PATCH 178/231] DRY violated --- lib/net/ber.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index bf4f0d9..3780008 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -252,6 +252,23 @@ module Net 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 From 11d8152d1b17908fe061f5abd702c4b6a603b039 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 8 Dec 2006 15:47:23 +0000 Subject: [PATCH 179/231] re-implemented Fixnum#to_ber, which was wrongly implemented. The new version still doesn't fix representations of negative values. --- lib/net/ber.rb | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index 3780008..27628f4 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -361,8 +361,23 @@ class Fixnum # to_ber # def to_ber - i = [self].pack('w') - [2, i.length].pack("CC") + i + # originally used pack("w") which is WRONG. + #i = [self].pack('w') + + # 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 + end + [2, zlen].pack("CC") + z[0-zlen,zlen] end # From 65b446c586d4c6d7fe24bc86e55104b2c6eee99c Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 8 Dec 2006 15:53:02 +0000 Subject: [PATCH 180/231] fixed Fixnum#to_ber when the value is zero --- lib/net/ber.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index 27628f4..53d7920 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -366,7 +366,7 @@ class Fixnum # 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 + raise Net::BER::BerError.new( "range error in fixnum" ) unless self >= 0 z = [self].pack("N") zlen = if self < 0x80 1 From db1894a5c015004e8c10c65e868a0259c3c7aac5 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 8 Dec 2006 16:03:24 +0000 Subject: [PATCH 181/231] Added Array#to_ber_oid. --- lib/net/ber.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index 53d7920..fbf6dc9 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -472,6 +472,16 @@ class Array [code].pack('C') + s.length.to_ber_length_encoding + s 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 + end # class Array From f6cb1054443bb3feacb646d406e2ea1b48acec0b Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 8 Dec 2006 16:10:37 +0000 Subject: [PATCH 182/231] oops. Fixed private. --- lib/net/ber.rb | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index fbf6dc9..9793e2d 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -466,12 +466,6 @@ class Array 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 - def to_ber_oid ary = self.dup first = ary.shift @@ -482,6 +476,13 @@ class Array [6, oid.length].pack("CC") + oid 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 From 1408a72c863ffcb2d34be279407e6573029ceb2a Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 15 Dec 2006 10:00:51 +0000 Subject: [PATCH 183/231] added a task for running the SNMP tests. --- Rakefile | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/Rakefile b/Rakefile index 7d36a9e..1bab215 100644 --- a/Rakefile +++ b/Rakefile @@ -42,17 +42,17 @@ $tardist = "../#$distdir.tar.gz" $release_date = nil $release_date = Time.parse(ENV['RELEASE_DATE']) if ENV['RELEASE_DATE'] -desc "Run the tests for #$name." -task :test do |t| + +def run_test_set the_task, testcases require 'test/unit/testsuite' require 'test/unit/ui/console/testrunner' runner = Test::Unit::UI::Console::TestRunner $LOAD_PATH.unshift('tests') - $stderr.puts "Checking for test cases:" if t.verbose - Dir['tests/test*.rb'].each do |testcase| - $stderr.puts "\t#{testcase}" if t.verbose + $stderr.puts "Checking for test cases:" if the_task.verbose + testcases.each do |testcase| + $stderr.puts "\t#{testcase}" if the_task.verbose load testcase end @@ -65,6 +65,16 @@ task :test do |t| runner.run(suite) end +desc "Run the tests for #$name." +task :test do |t| + run_test_set t, Dir['tests/test*.rb'] +end + +desc "(Provisional) Run tests for SNMP" +task :test_snmp do |t| + run_test_set t, ['tests/test_snmp.rb'] +end + spec = eval(File.read("net-ldap.gemspec")) spec.version = $version desc "Build the RubyGem for #$name." From 6748040918ab4df9258772d27ea529caeb6ab36f Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 15 Dec 2006 10:01:37 +0000 Subject: [PATCH 184/231] adjusted spelling --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index 1bab215..5857594 100644 --- a/Rakefile +++ b/Rakefile @@ -72,7 +72,7 @@ end desc "(Provisional) Run tests for SNMP" task :test_snmp do |t| - run_test_set t, ['tests/test_snmp.rb'] + run_test_set t, ['tests/testsnmp.rb'] end spec = eval(File.read("net-ldap.gemspec")) From 0871f7bdfac1ecc14be708bc677e7cba9cc43509 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 15 Dec 2006 10:03:27 +0000 Subject: [PATCH 185/231] initial add of SNMP test module --- tests/testsnmp.rb | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/testsnmp.rb diff --git a/tests/testsnmp.rb b/tests/testsnmp.rb new file mode 100644 index 0000000..a7dbc0c --- /dev/null +++ b/tests/testsnmp.rb @@ -0,0 +1,22 @@ +# $Id$ +# +# + + +$:.unshift "lib" + +require 'net/ldap' +require 'stringio' + + +class TestSnmp < Test::Unit::TestCase + + def setup + end + + def teardown + end + +end + + From d809c244239adfd037e01e7a09d29782b6709299 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 15 Dec 2006 10:36:17 +0000 Subject: [PATCH 186/231] test development --- tests/testsnmp.rb | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/testsnmp.rb b/tests/testsnmp.rb index a7dbc0c..4bafcc4 100644 --- a/tests/testsnmp.rb +++ b/tests/testsnmp.rb @@ -5,18 +5,40 @@ $:.unshift "lib" -require 'net/ldap' +require 'net/snmp' require 'stringio' class TestSnmp < Test::Unit::TestCase + SnmpRequest = "0'\002\001\000\004\006public\240\032\002\002?*\002\001\000\002\001\0000\0160\f\006\b+\006\001\002\001\001\001\000\005\000" + def setup end def teardown end + def test_invalid_packet + data = "xxxx" + assert_raise( Net::BER::BerError ) { + ary = data.read_ber(Net::SNMP::AsnSyntax) + } + + end + + def test_consume_string + data = "xxx" + assert_equal( nil, data.read_ber! ) + assert_equal( "xxx", data ) + + data = SnmpRequest + "!!!" + ary = data.read_ber!( Net::SNMP::AsnSyntax ) + assert_equal( "!!!", data ) + assert ary.is_a?(Array) + assert ary.is_a?(Net::BER::BerIdentifiedArray) + end + end From 79e59cf3aa16ca4e065ce06f5b20c8cf9cf51da8 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 15 Dec 2006 10:37:27 +0000 Subject: [PATCH 187/231] initial dev --- lib/net/snmp.rb | 55 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 lib/net/snmp.rb diff --git a/lib/net/snmp.rb b/lib/net/snmp.rb new file mode 100644 index 0000000..8607a07 --- /dev/null +++ b/lib/net/snmp.rb @@ -0,0 +1,55 @@ +# $Id$ +# +# NET::SNMP +# +#---------------------------------------------------------------------------- +# +# 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 +# +#--------------------------------------------------------------------------- +# +# + +require 'net/ber' + + +module Net + + class SNMP + + AsnSyntax = BER.compile_syntax({ + :application => { + :primitive => { + }, + :constructed => { + } + }, + :context_specific => { + :primitive => { + }, + :constructed => { + 0 => :array # GetRequest PDU (RFC1157 pgh 4.1.2) + } + } + }) + + end + +end + From c49e1f0c940a8d6c24ce71f5b4010f544f1657d8 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 15 Dec 2006 11:22:41 +0000 Subject: [PATCH 188/231] SNMP GetRequest parsing --- lib/net/snmp.rb | 52 +++++++++++++++++++++++++++++++++++++++++++++++ tests/testsnmp.rb | 26 ++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/lib/net/snmp.rb b/lib/net/snmp.rb index 8607a07..7f3fb18 100644 --- a/lib/net/snmp.rb +++ b/lib/net/snmp.rb @@ -51,5 +51,57 @@ module Net end + class SnmpPdu + class Error < Exception; end + + attr_reader :version, :community, :pdu_type, :request_id, :variables + + #-- + # TODO, improve the error-trapping. + # We want to wrap up Ruby errors like array-ranges, which can appear if we get bad data. + # We should probably do the whole parse under a catch-all block. + def initialize ber_object + begin + parse_ber_object ber_object + rescue RuntimeError + # Wrap any basic parsing error so it becomes a PDU-format error + raise Error.new( "snmp-pdu format error" ) + end + end + + def parse_ber_object ber_object + @version = ber_object[0].to_i + unless [0,2].include?(@version) + raise Error.new("unknown snmp-version: #{@version}") + end + + @community = ber_object[1].to_s + + data = ber_object[2] + app_tag = data.ber_identifier & 31 + case app_tag + when 0 + @pdu_type = :get_request + parse_get_request data + else + raise Error.new( "unknown snmp-pdu type: #{app_tag}" ) + end + end + + #-- + # Defined in RFC1157, pgh 4.1.2. + def parse_get_request data + @request_id = data[0].to_i + # data[1] is error-status, always 0. + # data[2] is error-index, always 0. + @variables = data[3].map {|v| + # A variable-binding, of which there may be several, + # consists of an OID and a BER null. + # We're ignoring the null, we might want to verify it instead. + v[0] + } + end + + end end diff --git a/tests/testsnmp.rb b/tests/testsnmp.rb index 4bafcc4..afe59e7 100644 --- a/tests/testsnmp.rb +++ b/tests/testsnmp.rb @@ -11,7 +11,7 @@ require 'stringio' class TestSnmp < Test::Unit::TestCase - SnmpRequest = "0'\002\001\000\004\006public\240\032\002\002?*\002\001\000\002\001\0000\0160\f\006\b+\006\001\002\001\001\001\000\005\000" + SnmpGetRequest = "0'\002\001\000\004\006public\240\032\002\002?*\002\001\000\002\001\0000\0160\f\006\b+\006\001\002\001\001\001\000\005\000" def setup end @@ -27,18 +27,40 @@ class TestSnmp < Test::Unit::TestCase end + # The method String#read_ber! added by Net::BER consumes a well-formed BER object + # from the head of a string. If it doesn't find a complete, well-formed BER object, + # it returns nil and leaves the string unchanged. If it finds an object, it returns + # the object and removes it from the head of the string. This is good for handling + # partially-received data streams, such as from network connections. def test_consume_string data = "xxx" assert_equal( nil, data.read_ber! ) assert_equal( "xxx", data ) - data = SnmpRequest + "!!!" + data = SnmpGetRequest + "!!!" ary = data.read_ber!( Net::SNMP::AsnSyntax ) assert_equal( "!!!", data ) assert ary.is_a?(Array) assert ary.is_a?(Net::BER::BerIdentifiedArray) end + def test_weird_packet + assert_raise( Net::SnmpPdu::Error ) { + Net::SnmpPdu.new("aaaaaaaaaaaaaa") + } + end + + def test_packet + data = SnmpGetRequest.dup + pkt = data.read_ber(Net::SNMP::AsnSyntax) + assert pkt.is_a?(Net::BER::BerIdentifiedArray) + assert_equal( 48, pkt.ber_identifier) # Constructed [0], signifies GetRequest + + pdu = Net::SnmpPdu.new(pkt) + assert_equal(:get_request, pdu.pdu_type ) + assert_equal(16170, pdu.request_id ) # whatever was in the test data. 16170 is not magic. + assert_equal( [[1,3,6,1,2,1,1,1,0]], pdu.variables ) + end end From da69de0fc2fb5f03c2fa632a15d2005e99a03ced Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 15 Dec 2006 11:23:18 +0000 Subject: [PATCH 189/231] rename --- tests/testsnmp.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testsnmp.rb b/tests/testsnmp.rb index afe59e7..47819e8 100644 --- a/tests/testsnmp.rb +++ b/tests/testsnmp.rb @@ -50,7 +50,7 @@ class TestSnmp < Test::Unit::TestCase } end - def test_packet + def test_get_request data = SnmpGetRequest.dup pkt = data.read_ber(Net::SNMP::AsnSyntax) assert pkt.is_a?(Net::BER::BerIdentifiedArray) From cf82f83ddf71bf532ffa1cfbe031bdd62a1fc9af Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 15 Dec 2006 13:54:50 +0000 Subject: [PATCH 190/231] added a #to_ber method to BerIdentifiedNull, which handles ASN.1 NULL objects. --- lib/net/ber.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index 9793e2d..5fe2be5 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -52,6 +52,9 @@ module Net class BerIdentifiedNull attr_accessor :ber_identifier + def to_ber + "\005\000" + end end #-- From 19452a62067d8aef552bfa6ca7bc6a5c08fd617d Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 15 Dec 2006 13:55:13 +0000 Subject: [PATCH 191/231] development on snmp --- lib/net/snmp.rb | 144 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 126 insertions(+), 18 deletions(-) diff --git a/lib/net/snmp.rb b/lib/net/snmp.rb index 7f3fb18..945db94 100644 --- a/lib/net/snmp.rb +++ b/lib/net/snmp.rb @@ -4,7 +4,7 @@ # #---------------------------------------------------------------------------- # -# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved. +# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved. # # Gmail: garbagecat10 # @@ -54,53 +54,161 @@ module Net class SnmpPdu class Error < Exception; end - attr_reader :version, :community, :pdu_type, :request_id, :variables + PduTypes = [ + :get_request, + :get_next_request, + :get_response, + :set_request, + :trap + ] + ErrorStatusCodes = { # Per RFC1157, pgh 4.1.1 + 0 => "noError", + 1 => "tooBig", + 2 => "noSuchName", + 3 => "badValue", + 4 => "readOnly", + 5 => "genErr" + } + + class << self + def parse ber_object + n = new + n.send :parse, ber_object + n + end + end + + attr_reader :version, :community, :pdu_type, :variables, :error_status + attr_accessor :request_id, :error_index + + + def initialize args={} + @version = args[:version] || 0 + @community = args[:community] || "public" + @pdu_type = args[:pdu_type] # leave nil unless specified; there's no reasonable default value. + @error_status = args[:error_status] || 0 + @error_index = args[:error_index] || 0 + end #-- - # TODO, improve the error-trapping. - # We want to wrap up Ruby errors like array-ranges, which can appear if we get bad data. - # We should probably do the whole parse under a catch-all block. - def initialize ber_object + def parse ber_object begin parse_ber_object ber_object - rescue RuntimeError + rescue Error + # Pass through any SnmpPdu::Error instances + raise $! + rescue # Wrap any basic parsing error so it becomes a PDU-format error raise Error.new( "snmp-pdu format error" ) end end + private :parse def parse_ber_object ber_object - @version = ber_object[0].to_i - unless [0,2].include?(@version) - raise Error.new("unknown snmp-version: #{@version}") - end + version= ber_object[0].to_i - @community = ber_object[1].to_s + community= ber_object[1].to_s data = ber_object[2] - app_tag = data.ber_identifier & 31 - case app_tag + case (app_tag = data.ber_identifier & 31) when 0 - @pdu_type = :get_request + send :pdu_type=, :get_request parse_get_request data else raise Error.new( "unknown snmp-pdu type: #{app_tag}" ) end end + private :parse_ber_object #-- # Defined in RFC1157, pgh 4.1.2. def parse_get_request data - @request_id = data[0].to_i + send :request_id=, data[0].to_i # data[1] is error-status, always 0. # data[2] is error-index, always 0. - @variables = data[3].map {|v| + data[3].each {|n,v| # A variable-binding, of which there may be several, # consists of an OID and a BER null. # We're ignoring the null, we might want to verify it instead. - v[0] + unless v.is_a?(Net::BER::BerIdentifiedNull) + raise Error.new(" invalid variable-binding in get-request" ) + end + add_variable_binding n, nil } end + private :parse_get_request + + def version= ver + unless [0,2].include?(ver) + raise Error.new("unknown snmp-version: #{ver}") + end + @version = ver + end + + def pdu_type= t + unless PduTypes.include?(t) + raise Error.new("unknown pdu-type: #{t}") + end + @pdu_type = t + end + + def error_status= es + unless ErrorStatusCodes.has_key?(es) + raise Error.new("unknown error-status: #{es}") + end + end + + def community= c + @community = c.to_s + end + + #-- + # Syntactic sugar + def add_variable_binding name, value + @variables ||= [] + @variables << [name, value] + end + + def to_ber_string + [ + version.to_ber, + community.to_ber, + pdu_to_ber_string + ].to_ber_sequence + end + + #-- + # Helper method that returns a PDU payload in BER form, + # depending on the PDU type. + def pdu_to_ber_string + case pdu_type + when :get_request + [ + request_id.to_ber, + error_status.to_ber, + error_index.to_ber, + [ + @variables.map {|n,v| + [n.to_ber_oid, Net::BER::BerIdentifiedNull.new.to_ber].to_ber_sequence + } + ].to_ber_sequence + ].to_ber_contextspecific(0) + when :get_response + [ + request_id.to_ber, + error_status.to_ber, + error_index.to_ber, + [ + @variables.map {|n,v| + [n.to_ber_oid, v.to_ber].to_ber_sequence + } + ].to_ber_sequence + ].to_ber_contextspecific(2) + else + raise Error.new( "unknown pdu-type: #{pdu_type}" ) + end + end + private :pdu_to_ber_string end end From a56d249a80927a5c785fe6eeaefdd9d6325420d0 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 15 Dec 2006 13:55:58 +0000 Subject: [PATCH 192/231] tests and change notes --- ChangeLog | 1 + tests/testsnmp.rb | 47 ++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index 5ed5075..b153473 100644 --- a/ChangeLog +++ b/ChangeLog @@ -25,6 +25,7 @@ * Added a "consuming" String#read_ber! method. * Added missing synactic support for Filter ANDs, NOTs and a few other things. +* Added some support for SNMP data-handling. == Net::LDAP 0.0.4: August 15, 2006 diff --git a/tests/testsnmp.rb b/tests/testsnmp.rb index 47819e8..04b5a2a 100644 --- a/tests/testsnmp.rb +++ b/tests/testsnmp.rb @@ -12,6 +12,8 @@ require 'stringio' class TestSnmp < Test::Unit::TestCase SnmpGetRequest = "0'\002\001\000\004\006public\240\032\002\002?*\002\001\000\002\001\0000\0160\f\006\b+\006\001\002\001\001\001\000\005\000" + SnmpGetResponse = "0+\002\001\000\004\006public\242\036\002\002'\017\002\001\000\002\001\0000\0220\020\006\b+\006\001\002\001\001\001\000\004\004test" + def setup end @@ -46,7 +48,7 @@ class TestSnmp < Test::Unit::TestCase def test_weird_packet assert_raise( Net::SnmpPdu::Error ) { - Net::SnmpPdu.new("aaaaaaaaaaaaaa") + Net::SnmpPdu.parse("aaaaaaaaaaaaaa") } end @@ -56,11 +58,50 @@ class TestSnmp < Test::Unit::TestCase assert pkt.is_a?(Net::BER::BerIdentifiedArray) assert_equal( 48, pkt.ber_identifier) # Constructed [0], signifies GetRequest - pdu = Net::SnmpPdu.new(pkt) + pdu = Net::SnmpPdu.parse(pkt) assert_equal(:get_request, pdu.pdu_type ) assert_equal(16170, pdu.request_id ) # whatever was in the test data. 16170 is not magic. - assert_equal( [[1,3,6,1,2,1,1,1,0]], pdu.variables ) + assert_equal( [[[1,3,6,1,2,1,1,1,0],nil]], pdu.variables ) + + assert_equal( pdu.to_ber_string, SnmpGetRequest ) end + + def test_empty_pdu + pdu = Net::SnmpPdu.new + assert_raise( Net::SnmpPdu::Error ) { + pdu.to_ber_string + } + end + + def test_malformations + pdu = Net::SnmpPdu.new + assert_raise( Net::SnmpPdu::Error ) { + pdu.version = 100 + } + + pdu.pdu_type = :get_request + pdu.pdu_type = :get_next_request + pdu.pdu_type = :get_response + pdu.pdu_type = :set_request + pdu.pdu_type = :trap + assert_raise( Net::SnmpPdu::Error ) { + pdu.pdu_type = :something_else + } + end + + def test_make_response + pdu = Net::SnmpPdu.new + pdu.version = 0 + pdu.community = "public" + pdu.pdu_type = :get_response + pdu.request_id = 9999 + pdu.error_status = 0 + pdu.error_index = 0 + pdu.add_variable_binding [1,3,6,1,2,1,1,1,0], "test" + + assert_equal( SnmpGetResponse, pdu.to_ber_string ) + end + end From 3f14b540145b894c5e7ee8f92bdc9161c22f3e48 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 15 Dec 2006 14:12:02 +0000 Subject: [PATCH 193/231] added some more test cases --- lib/net/snmp.rb | 1 + tests/testsnmp.rb | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/lib/net/snmp.rb b/lib/net/snmp.rb index 945db94..06d251e 100644 --- a/lib/net/snmp.rb +++ b/lib/net/snmp.rb @@ -88,6 +88,7 @@ module Net @pdu_type = args[:pdu_type] # leave nil unless specified; there's no reasonable default value. @error_status = args[:error_status] || 0 @error_index = args[:error_index] || 0 + @variables = args[:variables] || [] end #-- diff --git a/tests/testsnmp.rb b/tests/testsnmp.rb index 04b5a2a..f46609c 100644 --- a/tests/testsnmp.rb +++ b/tests/testsnmp.rb @@ -75,6 +75,8 @@ class TestSnmp < Test::Unit::TestCase def test_malformations pdu = Net::SnmpPdu.new + pdu.version = 0 + pdu.version = 2 assert_raise( Net::SnmpPdu::Error ) { pdu.version = 100 } @@ -102,6 +104,15 @@ class TestSnmp < Test::Unit::TestCase assert_equal( SnmpGetResponse, pdu.to_ber_string ) end + def test_make_bad_response + pdu = Net::SnmpPdu.new + assert_raise(Net::SnmpPdu::Error) {pdu.to_ber_string} + pdu.pdu_type = :get_response + pdu.request_id = 999 + pdu.to_ber_string + # Not specifying variables doesn't create an error. (Maybe it should?) + end + end From 0bf35f8d3e80bcde2e2668cd0ce27ee03f632137 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 15 Dec 2006 16:15:06 +0000 Subject: [PATCH 194/231] additional packet formats supported --- lib/net/snmp.rb | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/net/snmp.rb b/lib/net/snmp.rb index 06d251e..0eb8f44 100644 --- a/lib/net/snmp.rb +++ b/lib/net/snmp.rb @@ -44,7 +44,9 @@ module Net :primitive => { }, :constructed => { - 0 => :array # GetRequest PDU (RFC1157 pgh 4.1.2) + 0 => :array, # GetRequest PDU (RFC1157 pgh 4.1.2) + 1 => :array, # GetNextRequest PDU (RFC1157 pgh 4.1.3) + 2 => :array # GetResponse PDU (RFC1157 pgh 4.1.4) } } }) @@ -115,6 +117,10 @@ module Net when 0 send :pdu_type=, :get_request parse_get_request data + when 1 + send :pdu_type=, :get_next_request + # This PDU is identical to get-request except for the type. + parse_get_request data else raise Error.new( "unknown snmp-pdu type: #{app_tag}" ) end @@ -139,6 +145,7 @@ module Net end private :parse_get_request + def version= ver unless [0,2].include?(ver) raise Error.new("unknown snmp-version: #{ver}") @@ -165,7 +172,7 @@ module Net #-- # Syntactic sugar - def add_variable_binding name, value + def add_variable_binding name, value=nil @variables ||= [] @variables << [name, value] end @@ -194,6 +201,17 @@ module Net } ].to_ber_sequence ].to_ber_contextspecific(0) + when :get_next_request + [ + request_id.to_ber, + error_status.to_ber, + error_index.to_ber, + [ + @variables.map {|n,v| + [n.to_ber_oid, Net::BER::BerIdentifiedNull.new.to_ber].to_ber_sequence + } + ].to_ber_sequence + ].to_ber_contextspecific(1) when :get_response [ request_id.to_ber, From 5f197cc040f5e54916bbf5bb56b09795f98dc792 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 15 Dec 2006 18:29:26 +0000 Subject: [PATCH 195/231] fixed the implementation of Bignum#to_ber. --- Rakefile | 2 +- lib/net/ber.rb | 19 ++++++++++++++++--- tests/testber.rb | 22 ++++++++++++++++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/Rakefile b/Rakefile index 5857594..51e58d1 100644 --- a/Rakefile +++ b/Rakefile @@ -72,7 +72,7 @@ end desc "(Provisional) Run tests for SNMP" task :test_snmp do |t| - run_test_set t, ['tests/testsnmp.rb'] + run_test_set t, ['tests/testsnmp.rb', 'tests/testber.rb'] end spec = eval(File.read("net-ldap.gemspec")) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index 5fe2be5..2c1c18c 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -409,9 +409,22 @@ 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 + #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. + sz = self.size + out = "\000" * sz + (sz*8).times {|bit| + if self[bit] == 1 + out[bit/8] += (1 << (bit % 8)) + end + } + + [2, sz].pack("CC") + out.reverse end end diff --git a/tests/testber.rb b/tests/testber.rb index 5c63ffc..39be29a 100644 --- a/tests/testber.rb +++ b/tests/testber.rb @@ -23,6 +23,28 @@ class TestBer < Test::Unit::TestCase assert_equal( "\002\005\222\320\227\344\000", 5000000000.to_ber ) end + def test_ber_bignums + # Some of these values are Fixnums and some are Bignums. Different BER code. + [ + 5, + 50, + 500, + 5000, + 50000, + 500000, + 5000000, + 50000000, + 500000000, + 1000000000, + 2000000000, + 3000000000, + 4000000000, + 5000000000 + ].each {|val| + assert_equal( val, val.to_ber.read_ber ) + } + end + def test_ber_parsing assert_equal( 6, "\002\001\006".read_ber( Net::LDAP::AsnSyntax )) assert_equal( "testing", "\004\007testing".read_ber( Net::LDAP::AsnSyntax )) From e550dc80abd24814f32699c2b7f6ffcd7aeb7c10 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 15 Dec 2006 18:37:09 +0000 Subject: [PATCH 196/231] missed a case --- lib/net/snmp.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/net/snmp.rb b/lib/net/snmp.rb index 0eb8f44..cf12770 100644 --- a/lib/net/snmp.rb +++ b/lib/net/snmp.rb @@ -164,6 +164,7 @@ module Net unless ErrorStatusCodes.has_key?(es) raise Error.new("unknown error-status: #{es}") end + @error_status = es end def community= c From eac464bcb930b03bb1a27e473961b13029218f78 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 17 Dec 2006 19:04:41 +0000 Subject: [PATCH 197/231] added some SNMP app-specific types and cleaned up some BER-encoding problems with integers. --- lib/net/ber.rb | 61 +++++++++++++++++++++++++++++++---------------- lib/net/snmp.rb | 39 ++++++++++++++++++++++++++++++ tests/testber.rb | 6 ++--- tests/testsnmp.rb | 9 +++++++ 4 files changed, 92 insertions(+), 23 deletions(-) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index 2c1c18c..43e87e4 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -364,31 +364,14 @@ class Fixnum # to_ber # def to_ber - # originally used pack("w") which is WRONG. - #i = [self].pack('w') - - # 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 - end - [2, zlen].pack("CC") + z[0-zlen,zlen] + "\002" + to_ber_internal end # # to_ber_enumerated # def to_ber_enumerated - i = [self].pack('w') - [10, i.length].pack("CC") + i + "\012" + to_ber_internal end # @@ -403,6 +386,34 @@ class Fixnum 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 + end + [zlen].pack("C") + z[0-zlen,zlen] + end + private :to_ber_internal + end # class Fixnum @@ -416,6 +427,12 @@ class Bignum # 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| @@ -424,7 +441,11 @@ class Bignum end } - [2, sz].pack("CC") + out.reverse + while out.length > 1 and out[-1] == 0 + out.slice!(-1,1) + end + + [2, out.length].pack("CC") + out.reverse end end diff --git a/lib/net/snmp.rb b/lib/net/snmp.rb index cf12770..56284b8 100644 --- a/lib/net/snmp.rb +++ b/lib/net/snmp.rb @@ -36,6 +36,9 @@ module Net AsnSyntax = BER.compile_syntax({ :application => { :primitive => { + 1 => :integer, # Counter32, (RFC1155 sec 6) + 2 => :integer, # Gauge32, (RFC1155 sec 6) + 3 => :integer # TimeTicks32, (RFC1155 sec 6) }, :constructed => { } @@ -51,6 +54,42 @@ module Net } }) + # SNMP 32-bit counter. + # Defined in RFC1155 (Structure of Mangement Information), section 6. + # A 32-bit counter is an ASN.1 application [1] implicit unsigned integer + # with a range from 0 to 2^^32 - 1. + class Counter32 + def initialize value + @value = value + end + def to_ber + @value.to_ber_application(1) + end + end + + # SNMP 32-bit gauge. + # Defined in RFC1155 (Structure of Mangement Information), section 6. + # A 32-bit counter is an ASN.1 application [2] implicit unsigned integer. + class Gauge32 + def initialize value + @value = value + end + def to_ber + @value.to_ber_application(2) + end + end + + # SNMP 32-bit timer-ticks. + # Defined in RFC1155 (Structure of Mangement Information), section 6. + # A 32-bit counter is an ASN.1 application [3] implicit unsigned integer. + class TimerTicks32 + def initialize value + @value = value + end + def to_ber + @value.to_ber_application(3) + end + end end class SnmpPdu diff --git a/tests/testber.rb b/tests/testber.rb index 39be29a..fac4a10 100644 --- a/tests/testber.rb +++ b/tests/testber.rb @@ -18,9 +18,9 @@ class TestBer < Test::Unit::TestCase # 5000000000 is a Bignum, which hits different code. def test_ber_integers assert_equal( "\002\001\005", 5.to_ber ) - assert_equal( "\002\002\203t", 500.to_ber ) - assert_equal( "\002\003\203\206P", 50000.to_ber ) - assert_equal( "\002\005\222\320\227\344\000", 5000000000.to_ber ) + assert_equal( "\002\002\001\364", 500.to_ber ) + assert_equal( "\002\003\0\303P", 50000.to_ber ) + assert_equal( "\002\005\001*\005\362\000", 5000000000.to_ber ) end def test_ber_bignums diff --git a/tests/testsnmp.rb b/tests/testsnmp.rb index f46609c..a5ad9ca 100644 --- a/tests/testsnmp.rb +++ b/tests/testsnmp.rb @@ -113,6 +113,15 @@ class TestSnmp < Test::Unit::TestCase # Not specifying variables doesn't create an error. (Maybe it should?) end + def test_snmp_integers + c32 = Net::SNMP::Counter32.new(100) + assert_equal( "A\001d", c32.to_ber ) + g32 = Net::SNMP::Gauge32.new(100) + assert_equal( "B\001d", g32.to_ber ) + t32 = Net::SNMP::TimerTicks32.new(100) + assert_equal( "C\001d", t32.to_ber ) + end + end From 765988a57b6e0f1436c87aef3ad67eefac4a6eea Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 18 Dec 2006 19:56:50 +0000 Subject: [PATCH 198/231] supported parsing of get-response packets --- lib/net/snmp.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/net/snmp.rb b/lib/net/snmp.rb index 56284b8..ef981f5 100644 --- a/lib/net/snmp.rb +++ b/lib/net/snmp.rb @@ -160,6 +160,11 @@ module Net send :pdu_type=, :get_next_request # This PDU is identical to get-request except for the type. parse_get_request data + when 2 + send :pdu_type=, :get_response + # This PDU is identical to get-request except for the type, + # and the fact that the variable bindings will be non-null. + parse_get_request data else raise Error.new( "unknown snmp-pdu type: #{app_tag}" ) end From 977f276191fedfbec42500a71f515e9cb60e2641 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 18 Dec 2006 20:16:32 +0000 Subject: [PATCH 199/231] fixed small bug, missing case in PDU parser --- lib/net/snmp.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/net/snmp.rb b/lib/net/snmp.rb index ef981f5..09877f5 100644 --- a/lib/net/snmp.rb +++ b/lib/net/snmp.rb @@ -175,8 +175,8 @@ module Net # Defined in RFC1157, pgh 4.1.2. def parse_get_request data send :request_id=, data[0].to_i - # data[1] is error-status, always 0. - # data[2] is error-index, always 0. + send :error_status=, data[1].to_i + send :error_index=, data[2].to_i data[3].each {|n,v| # A variable-binding, of which there may be several, # consists of an OID and a BER null. From 40b5554389ac30117afe309f1fe170fcfcb20872 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 18 Dec 2006 20:36:41 +0000 Subject: [PATCH 200/231] tweaked get-response parsing --- lib/net/snmp.rb | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/net/snmp.rb b/lib/net/snmp.rb index 09877f5..bb45105 100644 --- a/lib/net/snmp.rb +++ b/lib/net/snmp.rb @@ -163,8 +163,9 @@ module Net when 2 send :pdu_type=, :get_response # This PDU is identical to get-request except for the type, + # the error_status and error_index values are meaningful, # and the fact that the variable bindings will be non-null. - parse_get_request data + parse_get_response data else raise Error.new( "unknown snmp-pdu type: #{app_tag}" ) end @@ -175,8 +176,10 @@ module Net # Defined in RFC1157, pgh 4.1.2. def parse_get_request data send :request_id=, data[0].to_i - send :error_status=, data[1].to_i - send :error_index=, data[2].to_i + # data[1] is error_status, always zero. + # data[2] is error_index, always zero. + send :error_status=, 0 + send :error_index=, 0 data[3].each {|n,v| # A variable-binding, of which there may be several, # consists of an OID and a BER null. @@ -189,6 +192,21 @@ module Net end private :parse_get_request + #-- + # Defined in RFC1157, pgh 4.1.4 + def parse_get_response data + send :request_id=, data[0].to_i + send :error_status=, data[1].to_i + send :error_index=, data[2].to_i + data[3].each {|n,v| + # A variable-binding, of which there may be several, + # consists of an OID and a BER null. + # We're ignoring the null, we might want to verify it instead. + add_variable_binding n, v + } + end + private :parse_get_response + def version= ver unless [0,2].include?(ver) From 11fb69b0b1fe3f0f50797b7502dd607e2918203f Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 19 Dec 2006 16:40:10 +0000 Subject: [PATCH 201/231] fixed mishandling of community string in parser --- tests/testsnmp.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/testsnmp.rb b/tests/testsnmp.rb index a5ad9ca..93cfb91 100644 --- a/tests/testsnmp.rb +++ b/tests/testsnmp.rb @@ -14,6 +14,8 @@ class TestSnmp < Test::Unit::TestCase SnmpGetRequest = "0'\002\001\000\004\006public\240\032\002\002?*\002\001\000\002\001\0000\0160\f\006\b+\006\001\002\001\001\001\000\005\000" SnmpGetResponse = "0+\002\001\000\004\006public\242\036\002\002'\017\002\001\000\002\001\0000\0220\020\006\b+\006\001\002\001\001\001\000\004\004test" + SnmpGetRequestXXX = "0'\002\001\000\004\006xxxxxx\240\032\002\002?*\002\001\000\002\001\0000\0160\f\006\b+\006\001\002\001\001\001\000\005\000" + def setup end @@ -122,6 +124,13 @@ class TestSnmp < Test::Unit::TestCase assert_equal( "C\001d", t32.to_ber ) end + def test_community + data = SnmpGetRequestXXX.dup + ary = data.read_ber(Net::SNMP::AsnSyntax) + pdu = Net::SnmpPdu.parse( ary ) + assert_equal( "xxxxxx", pdu.community ) + end + end From 2ef2176f148b3e28a75ec716001ef5a78beea443 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 19 Dec 2006 18:15:40 +0000 Subject: [PATCH 202/231] fixed bug --- lib/net/snmp.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/net/snmp.rb b/lib/net/snmp.rb index bb45105..b8aeda2 100644 --- a/lib/net/snmp.rb +++ b/lib/net/snmp.rb @@ -147,9 +147,8 @@ module Net private :parse def parse_ber_object ber_object - version= ber_object[0].to_i - - community= ber_object[1].to_s + send :version=, ber_object[0].to_i + send :community=, ber_object[1].to_s data = ber_object[2] case (app_tag = data.ber_identifier & 31) From 0d9206d602a0e6b8eb1fc0900acd10e4d84322f9 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 19 Dec 2006 18:27:57 +0000 Subject: [PATCH 203/231] added Net::BER::BerIdentifiedOid type and related tests --- lib/net/ber.rb | 26 ++++++++++++++++++++++++++ tests/testber.rb | 6 ++++++ 2 files changed, 32 insertions(+) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index 43e87e4..6a15224 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -57,6 +57,32 @@ module Net 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. diff --git a/tests/testber.rb b/tests/testber.rb index fac4a10..970f71b 100644 --- a/tests/testber.rb +++ b/tests/testber.rb @@ -57,6 +57,12 @@ class TestBer < Test::Unit::TestCase end + def test_oid + oid = Net::BER::BerIdentifiedOid.new( [1,3,6,1,2,1,1,1,0] ) + assert_equal( "\006\b+\006\001\002\001\001\001\000", oid.to_ber ) + oid = Net::BER::BerIdentifiedOid.new( "1.3.6.1.2.1.1.1.0" ) + assert_equal( "\006\b+\006\001\002\001\001\001\000", oid.to_ber ) + end end From f6c87c02fc130de92df97bcd84510005df47a2ae Mon Sep 17 00:00:00 2001 From: blackhedd Date: Thu, 21 Dec 2006 15:09:29 +0000 Subject: [PATCH 204/231] fixed mispelling: TimerTicks is now TimeTicks. --- lib/net/snmp.rb | 2 +- tests/testsnmp.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/net/snmp.rb b/lib/net/snmp.rb index b8aeda2..1c1f28c 100644 --- a/lib/net/snmp.rb +++ b/lib/net/snmp.rb @@ -82,7 +82,7 @@ module Net # SNMP 32-bit timer-ticks. # Defined in RFC1155 (Structure of Mangement Information), section 6. # A 32-bit counter is an ASN.1 application [3] implicit unsigned integer. - class TimerTicks32 + class TimeTicks32 def initialize value @value = value end diff --git a/tests/testsnmp.rb b/tests/testsnmp.rb index 93cfb91..e5a5983 100644 --- a/tests/testsnmp.rb +++ b/tests/testsnmp.rb @@ -120,7 +120,7 @@ class TestSnmp < Test::Unit::TestCase assert_equal( "A\001d", c32.to_ber ) g32 = Net::SNMP::Gauge32.new(100) assert_equal( "B\001d", g32.to_ber ) - t32 = Net::SNMP::TimerTicks32.new(100) + t32 = Net::SNMP::TimeTicks32.new(100) assert_equal( "C\001d", t32.to_ber ) end From 79f3ec6e0da1d47135b04fe1e8c0dd2b63fae7a8 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Thu, 21 Dec 2006 16:04:25 +0000 Subject: [PATCH 205/231] comments --- lib/net/snmp.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/net/snmp.rb b/lib/net/snmp.rb index 1c1f28c..1fda412 100644 --- a/lib/net/snmp.rb +++ b/lib/net/snmp.rb @@ -36,9 +36,9 @@ module Net AsnSyntax = BER.compile_syntax({ :application => { :primitive => { - 1 => :integer, # Counter32, (RFC1155 sec 6) - 2 => :integer, # Gauge32, (RFC1155 sec 6) - 3 => :integer # TimeTicks32, (RFC1155 sec 6) + 1 => :integer, # Counter32, (RFC2578 sec 2) + 2 => :integer, # Gauge32 or Unsigned32, (RFC2578 sec 2) + 3 => :integer # TimeTicks32, (RFC2578 sec 2) }, :constructed => { } @@ -70,6 +70,7 @@ module Net # SNMP 32-bit gauge. # Defined in RFC1155 (Structure of Mangement Information), section 6. # A 32-bit counter is an ASN.1 application [2] implicit unsigned integer. + # This is also indistinguishable from Unsigned32. (Need to alias them.) class Gauge32 def initialize value @value = value From 17dcc52e056f9d97e435da7a298fb873b9582b9a Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 17 Mar 2007 02:35:41 +0000 Subject: [PATCH 206/231] changed Entry::to_ldif so it doesn't interpret blanks as binary characters. --- lib/net/ldap/entry.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/net/ldap/entry.rb b/lib/net/ldap/entry.rb index 2f16e31..1291d8e 100644 --- a/lib/net/ldap/entry.rb +++ b/lib/net/ldap/entry.rb @@ -251,7 +251,7 @@ class LDAP def is_attribute_value_binary? value v = value.to_s v.each_byte {|byt| - return true if (byt < 33) || (byt > 126) + return true if (byt < 32) || (byt > 126) } if v[0..0] == ':' or v[0..0] == '<' return true From 789b36c9d75c9ca552da8d1b3bd78f519cdaaa7f Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 20 Mar 2007 18:28:34 +0000 Subject: [PATCH 207/231] extended our support for error messages returned from servers. --- ChangeLog | 2 ++ lib/net/ldap.rb | 19 +++++++++++++++++-- lib/net/ldap/pdu.rb | 8 ++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index b153473..949e636 100644 --- a/ChangeLog +++ b/ChangeLog @@ -26,6 +26,8 @@ * Added missing synactic support for Filter ANDs, NOTs and a few other things. * Added some support for SNMP data-handling. +* Extended support for server-reported error messages. This was provisionally + added to Net::LDAP#add, and eventually will be added to other methods. == Net::LDAP 0.0.4: August 15, 2006 diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index f4c4086..9daaf28 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -510,9 +510,17 @@ module Net # puts "Message: #{ldap.get_operation_result.message}" # end # + #-- + # Modified the implementation, 20Mar07. We might get a hash of LDAP response codes + # instead of a simple numeric code. + # def get_operation_result os = OpenStruct.new - if @result + if @result.is_a?(Hash) + os.code = (@result[:resultCode] || "").to_i + os.error_message = @result[:errorMessage] + os.matched_dn = @result[:matchedDN] + elsif @result os.code = @result else os.code = 0 @@ -802,6 +810,9 @@ module Net # Net::LDAP.open (:host => host) do |ldap| # ldap.add( :dn => dn, :attributes => attr ) # end + #-- + # Provisional modification: Connection#add returns a full hash with LDAP status values, + # instead of the simple result number we're used to getting. # def add args if @open_connection @@ -1421,6 +1432,10 @@ module Net #-- # add # TODO, need to support a time limit, in case the server fails to respond. + # Unlike other operation-methods in this class, we return a result hash rather + # than a simple result number. This is experimental, and eventually we'll want + # to do this with all the others. The point is to have access to the error message + # and the matched-DN returned by the server. # def add args add_dn = args[:dn] or raise LdapError.new("Unable to add empty DN") @@ -1434,7 +1449,7 @@ module Net @conn.write pkt (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 9) or raise LdapError.new( "response missing or invalid" ) - pdu.result_code + pdu.result end diff --git a/lib/net/ldap/pdu.rb b/lib/net/ldap/pdu.rb index 7a98112..0dbe56c 100644 --- a/lib/net/ldap/pdu.rb +++ b/lib/net/ldap/pdu.rb @@ -119,6 +119,14 @@ class LdapPdu end end + # Returns a hash which (usually) defines the members :resultCode, :errorMessage, and :matchedDN. + # These values come directly from an LDAP response packet returned by the remote peer. + # See #result_code for a sugaring. + # + def result + @ldap_result || {} + end + # # result_code # This returns an LDAP result code taken from the PDU, From 033bc22ee47476b8c552447f40930820d2f36b7b Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 20 Mar 2007 18:30:52 +0000 Subject: [PATCH 208/231] added some additional comments --- lib/net/ldap.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 9daaf28..ae03af1 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -510,6 +510,10 @@ module Net # puts "Message: #{ldap.get_operation_result.message}" # end # + # Certain operations return additional information, accessible through members + # of the object returned from #get_operation_result. Check #get_operation_result.error_message + # and #get_operation_result.matched_dn. + # #-- # Modified the implementation, 20Mar07. We might get a hash of LDAP response codes # instead of a simple numeric code. From ae298d2865a3ac4554efde63fe7171a26d0b9f47 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 20 Mar 2007 18:56:23 +0000 Subject: [PATCH 209/231] added extended error reporting to Connection#modify --- lib/net/ldap.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index ae03af1..fe40ec9 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -1429,7 +1429,7 @@ module Net @conn.write pkt (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 7) or raise LdapError.new( "response missing or invalid" ) - pdu.result_code + pdu.result end From d483a345af32cd78fe4496861c33b6fa9f6842d7 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 2 May 2007 02:32:14 +0000 Subject: [PATCH 210/231] Added a patch by Kouhei Sutou which supports start_tls encryption. --- ChangeLog | 2 ++ lib/net/ber.rb | 5 +++++ lib/net/ldap.rb | 29 +++++++++++++++++++++++++++-- lib/net/ldap/pdu.rb | 4 ++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index 949e636..bdc0168 100644 --- a/ChangeLog +++ b/ChangeLog @@ -28,6 +28,8 @@ * Added some support for SNMP data-handling. * Extended support for server-reported error messages. This was provisionally added to Net::LDAP#add, and eventually will be added to other methods. +* Belatedly added a patch contributed by Kouhei Sutou last October. + The patch adds start_tls support. == Net::LDAP 0.0.4: August 15, 2006 diff --git a/lib/net/ber.rb b/lib/net/ber.rb index 6a15224..0b40d1f 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -124,6 +124,11 @@ module Net :constructed => { 16 => :array, 17 => :array + }, + :context_specific => { + :primitive => { + 10 => :integer + } } } }) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index fe40ec9..e4fec28 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -321,6 +321,7 @@ module Net DefaultAuth = {:method => :anonymous} DefaultTreebase = "dc=com" + StartTlsOid = "1.3.6.1.4.1.1466.20037" ResultStrings = { 0 => "Success", @@ -473,8 +474,9 @@ module Net # unencrypted connections.] # def encryption args - if args == :simple_tls - args = {:method => :simple_tls} + case args + when :simple_tls, :start_tls + args = {:method => args} end @encryption = args end @@ -1114,6 +1116,11 @@ module Net # OBSERVE: WE REPLACE the value of @conn, which is presumed to be a connected # TCPsocket object. # + # The start_tls method is supported by many servers over the standard LDAP port. + # It does not require an alternative port for encrypted communications, as with + # simple_tls. + # Thanks for Kouhei Sutou for generously contributing the :start_tls path. + # def setup_encryption args case args[:method] when :simple_tls @@ -1123,6 +1130,24 @@ module Net @conn.connect @conn.sync_close = true # additional branches requiring server validation and peer certs, etc. go here. + when :start_tls + raise LdapError.new("openssl unavailable") unless $net_ldap_openssl_available + msgid = next_msgid.to_ber + request = [StartTlsOid.to_ber].to_ber_appsequence( Net::LdapPdu::ExtendedRequest ) + request_pkt = [msgid, request].to_ber_sequence + @conn.write request_pkt + be = @conn.read_ber(AsnSyntax) + raise LdapError.new("no start_tls result") if be.nil? + pdu = Net::LdapPdu.new(be) + raise LdapError.new("no start_tls result") if pdu.nil? + if pdu.result_code.zero? + ctx = OpenSSL::SSL::SSLContext.new + @conn = OpenSSL::SSL::SSLSocket.new(@conn, ctx) + @conn.connect + @conn.sync_close = true + else + raise LdapError.new("start_tls failed: #{pdu.result_code}") + end else raise LdapError.new( "unsupported encryption method #{args[:method]}" ) end diff --git a/lib/net/ldap/pdu.rb b/lib/net/ldap/pdu.rb index 0dbe56c..585b28c 100644 --- a/lib/net/ldap/pdu.rb +++ b/lib/net/ldap/pdu.rb @@ -47,6 +47,8 @@ class LdapPdu DeleteResponse = 11 ModifyRDNResponse = 13 SearchResultReferral = 19 + ExtendedRequest = 23 + ExtendedResponse = 24 attr_reader :msg_id, :app_tag attr_reader :search_dn, :search_attributes, :search_entry @@ -114,6 +116,8 @@ class LdapPdu parse_bind_request ber_object[1] when UnbindRequest parse_unbind_request ber_object[1] + when ExtendedResponse + parse_ldap_result ber_object[1] else raise LdapPduError.new( "unknown pdu-type: #{@app_tag}" ) end From 55c6d2efe9243dab489bdf02e9913e0da5a01cfc Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 2 May 2007 02:59:38 +0000 Subject: [PATCH 211/231] fixed error in BER encoding. --- lib/net/ber.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/net/ber.rb b/lib/net/ber.rb index 0b40d1f..b0c79c7 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -124,11 +124,11 @@ module Net :constructed => { 16 => :array, 17 => :array - }, - :context_specific => { - :primitive => { - 10 => :integer - } + } + }, + :context_specific => { + :primitive => { + 10 => :integer } } }) From 95d86ea6c30249a43cedd35351aa55ed891369d6 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 2 May 2007 10:10:19 +0000 Subject: [PATCH 212/231] added Net::LDAP#search_subschema_entry --- lib/net/ldap.rb | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index e4fec28..fd712c3 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -1055,6 +1055,40 @@ module Net (rs and rs.first) or Entry.new end + + # Return the root Subschema record from the LDAP server as a Net::LDAP::Entry, + # or an empty Entry is the server doesn't return the record. On success, the + # Net::LDAP::Entry returned from this call will have the attributes :dn, + # :objectclasses, and :attributetypes. + #-- + # cf. RFC4512 section 4. + # The :dn attribute in the returned Entry is the subschema name as returned from + # the server. + # Set :ignore_server_caps, see the notes in search_root_dse. + # + def search_subschema_entry + rs = search( + :ignore_server_caps=>true, + :base=>"", + :scope=>SearchScope_BaseObject, + :attributes=>[:subschemaSubentry] + ) + return Entry.new unless (rs and rs.first) + subschema_name = rs.subschemasubentry + return Entry.new unless (subschema_name and subschema_name.first) + + rs = search( + :ignore_server_caps=>true, + :base=>subschema_name, + :scope=>SearchScope_BaseObject, + :filter=>"objectclass=*", + :attributes=>[:objectclasses, :attributetypes] + ) + + (rs and rs.first) or Entry.new + end + + #-- # Convenience method to query server capabilities. # Only do this once per Net::LDAP object. From 18699c9ac24b26a68c4f811a81ec53f89d9ebefb Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 2 May 2007 10:15:15 +0000 Subject: [PATCH 213/231] comments and docs for Net::LDAP#Search_subschema_entry --- ChangeLog | 1 + lib/net/ldap.rb | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/ChangeLog b/ChangeLog index bdc0168..81f175c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -30,6 +30,7 @@ added to Net::LDAP#add, and eventually will be added to other methods. * Belatedly added a patch contributed by Kouhei Sutou last October. The patch adds start_tls support. +* Added Net::LDAP#search_subschema_entry == Net::LDAP 0.0.4: August 15, 2006 diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index fd712c3..ac31f05 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -1060,6 +1060,19 @@ module Net # or an empty Entry is the server doesn't return the record. On success, the # Net::LDAP::Entry returned from this call will have the attributes :dn, # :objectclasses, and :attributetypes. + # + # ldap = Net::LDAP.new + # ldap.host = "your.ldap.host" + # ldap.auth "your-user-dn", "your-psw" + # subschema_entry = ldap.search_subschema_entry + # + # subschema_entry.attributetypes.each do |attrtype| + # # your code + # end + # + # subschema_entry.objectclasses.each do |attrtype| + # # your code + # end #-- # cf. RFC4512 section 4. # The :dn attribute in the returned Entry is the subschema name as returned from From acfa64d15947635aedf68cb5b74b52f2d6e97e02 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 2 May 2007 10:17:46 +0000 Subject: [PATCH 214/231] fixed grammar error in doc. --- lib/net/ldap.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index ac31f05..3442733 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -1057,7 +1057,7 @@ module Net # Return the root Subschema record from the LDAP server as a Net::LDAP::Entry, - # or an empty Entry is the server doesn't return the record. On success, the + # or an empty Entry if the server doesn't return the record. On success, the # Net::LDAP::Entry returned from this call will have the attributes :dn, # :objectclasses, and :attributetypes. # From 105fb102d592a2119233a2a839250d5379ff9cd0 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 2 May 2007 10:21:39 +0000 Subject: [PATCH 215/231] let's get it right for a change --- lib/net/ldap.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 3442733..24d03a6 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -1087,12 +1087,12 @@ module Net :attributes=>[:subschemaSubentry] ) return Entry.new unless (rs and rs.first) - subschema_name = rs.subschemasubentry + subschema_name = rs.first.subschemasubentry return Entry.new unless (subschema_name and subschema_name.first) rs = search( :ignore_server_caps=>true, - :base=>subschema_name, + :base=>subschema_name.first, :scope=>SearchScope_BaseObject, :filter=>"objectclass=*", :attributes=>[:objectclasses, :attributetypes] From 8cfb4a13b6b379cca812a011d6f9ab4144eb4bf6 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 2 May 2007 10:44:58 +0000 Subject: [PATCH 216/231] more comment tweaks --- lib/net/ldap.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 24d03a6..a10ecf4 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -1059,7 +1059,8 @@ module Net # Return the root Subschema record from the LDAP server as a Net::LDAP::Entry, # or an empty Entry if the server doesn't return the record. On success, the # Net::LDAP::Entry returned from this call will have the attributes :dn, - # :objectclasses, and :attributetypes. + # :objectclasses, and :attributetypes. If there is an error, call #get_operation_result + # for more information. # # ldap = Net::LDAP.new # ldap.host = "your.ldap.host" From bc1129fcf15a4bea72e83a1ad297bb9e1df63ce8 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 2 May 2007 18:25:34 +0000 Subject: [PATCH 217/231] tweaked subschema query to comply with RFC 4512 pgh 4.4. --- lib/net/ldap.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index a10ecf4..eb4fa24 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -1075,7 +1075,7 @@ module Net # # your code # end #-- - # cf. RFC4512 section 4. + # cf. RFC4512 section 4, particulary graff 4.4. # The :dn attribute in the returned Entry is the subschema name as returned from # the server. # Set :ignore_server_caps, see the notes in search_root_dse. @@ -1095,7 +1095,7 @@ module Net :ignore_server_caps=>true, :base=>subschema_name.first, :scope=>SearchScope_BaseObject, - :filter=>"objectclass=*", + :filter=>"objectclass=subschema", :attributes=>[:objectclasses, :attributetypes] ) From da882af0748ff575b33f525aaf32720366636731 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 5 May 2007 02:44:32 +0000 Subject: [PATCH 218/231] Added Net::LDAP::Filter.parse_ber and associated helper methods and tests. --- Rakefile | 5 +++ lib/net/ldap.rb | 2 + lib/net/ldap/filter.rb | 59 +++++++++++++++++++++++++++ tests/testfilter.rb | 93 ++++++++++++++++++++++++++++++++++-------- 4 files changed, 143 insertions(+), 16 deletions(-) diff --git a/Rakefile b/Rakefile index 51e58d1..f146781 100644 --- a/Rakefile +++ b/Rakefile @@ -75,6 +75,11 @@ task :test_snmp do |t| run_test_set t, ['tests/testsnmp.rb', 'tests/testber.rb'] end +desc "(Provisional) Run tests for filters" +task :test_filters do |t| + run_test_set t, ['tests/testfilter.rb'] +end + spec = eval(File.read("net-ldap.gemspec")) spec.version = $version desc "Build the RubyGem for #$name." diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index eb4fa24..437938c 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -311,6 +311,8 @@ module Net 2 => :array, # SearchFilter-NOT 3 => :array, # Seach referral 4 => :array, # unknown use in Microsoft Outlook + 5 => :array, # SearchFilter-GE + 6 => :array, # SearchFilter-LE 7 => :array, # serverSaslCreds } } diff --git a/lib/net/ldap/filter.rb b/lib/net/ldap/filter.rb index f97a802..58d70a6 100644 --- a/lib/net/ldap/filter.rb +++ b/lib/net/ldap/filter.rb @@ -115,6 +115,11 @@ class Filter # Removed GT and LT. They're not in the RFC. def ~@; Filter.new :not, self, nil; end + # Equality operator for filters, useful primarily for constructing unit tests. + def == filter + str = "[@op,@left,@right]" + self.instance_eval(str) == filter.instance_eval(str) + end def to_s case @op @@ -224,6 +229,58 @@ class Filter end end + + # Converts an LDAP search filter in BER format to an Net::LDAP::Filter + # object. The incoming BER object most likely came to us by parsing an + # LDAP searchRequest PDU. + # Cf the comments under #to_ber, including the grammar snippet from the RFC. + #-- + # We're hardcoding the BER constants from the RFC. Ought to break them out + # into constants. + # + def Filter::parse_ber ber + case ber.ber_identifier + when 0xa0 # context-specific constructed 0, "and" + ber.map {|b| Filter::parse_ber(b)}.inject {|memo,obj| memo & obj} + when 0xa1 # context-specific constructed 1, "or" + ber.map {|b| Filter::parse_ber(b)}.inject {|memo,obj| memo | obj} + when 0xa2 # context-specific constructed 2, "not" + ~ Filter::parse_ber( ber.first ) + when 0xa3 # context-specific constructed 3, "equalityMatch" + if ber.last == "*" + else + Filter.eq( ber.first, ber.last ) + end + when 0xa4 # context-specific constructed 4, "substring" + str = "" + final = false + ber.last.each {|b| + case b.ber_identifier + when 0x80 # context-specific primitive 0, SubstringFilter "initial" + raise "unrecognized substring filter, bad initial" if str.length > 0 + str += b + when 0x81 # context-specific primitive 0, SubstringFilter "any" + str += "*#{b}" + when 0x82 # context-specific primitive 0, SubstringFilter "final" + str += "*#{b}" + final = true + end + } + str += "*" unless final + Filter.eq( ber.first.to_s, str ) + when 0xa5 # context-specific constructed 5, "greaterOrEqual" + Filter.ge( ber.first.to_s, ber.last.to_s ) + when 0xa6 # context-specific constructed 5, "lessOrEqual" + Filter.le( ber.first.to_s, ber.last.to_s ) + when 0x87 # context-specific primitive 7, "present" + # call to_s to get rid of the BER-identifiedness of the incoming string. + Filter.pres( ber.to_s ) + else + raise "invalid BER tag-value (#{ber.ber_identifier}) in search filter" + end + end + + #-- # coalesce # This is a private helper method for dealing with chains of ANDs and ORs @@ -260,6 +317,8 @@ class Filter end + + #-- # We got a hash of attribute values. # Do we match the attributes? diff --git a/tests/testfilter.rb b/tests/testfilter.rb index 4339f26..34876c7 100644 --- a/tests/testfilter.rb +++ b/tests/testfilter.rb @@ -11,27 +11,88 @@ require 'net/ldap' class TestFilter < Test::Unit::TestCase - def setup - end + def setup + end - def teardown - end + def teardown + end - def test_rfc_2254 - p Net::LDAP::Filter.from_rfc2254( " ( uid=george* ) " ) - p Net::LDAP::Filter.from_rfc2254( "uid!=george*" ) - p Net::LDAP::Filter.from_rfc2254( "uidgeorge*" ) - p Net::LDAP::Filter.from_rfc2254( "uid>=george*" ) - p Net::LDAP::Filter.from_rfc2254( "uid!=george*" ) + # Note that the RFC doesn't define either less-than or greater-than. + def test_rfc_2254 + Net::LDAP::Filter.from_rfc2254( " ( uid=george* ) " ) + Net::LDAP::Filter.from_rfc2254( "uid!=george*" ) + Net::LDAP::Filter.from_rfc2254( "uid <= george*" ) + Net::LDAP::Filter.from_rfc2254( "uid>=george*" ) + Net::LDAP::Filter.from_rfc2254( "uid!=george*" ) - p Net::LDAP::Filter.from_rfc2254( "(& (uid!=george* ) (mail=*))" ) - p Net::LDAP::Filter.from_rfc2254( "(| (uid!=george* ) (mail=*))" ) - p Net::LDAP::Filter.from_rfc2254( "(! (mail=*))" ) - end + Net::LDAP::Filter.from_rfc2254( "(& (uid!=george* ) (mail=*))" ) + Net::LDAP::Filter.from_rfc2254( "(| (uid!=george* ) (mail=*))" ) + Net::LDAP::Filter.from_rfc2254( "(! (mail=*))" ) + end + def test_filters_from_ber + [ + Net::LDAP::Filter.eq( "objectclass", "*" ), + Net::LDAP::Filter.pres( "objectclass" ), + Net::LDAP::Filter.eq( "objectclass", "ou" ), + Net::LDAP::Filter.ge( "uid", "500" ), + Net::LDAP::Filter.le( "uid", "500" ), + (~ Net::LDAP::Filter.pres( "objectclass" )), + (Net::LDAP::Filter.pres( "objectclass" ) & Net::LDAP::Filter.pres( "ou" )), + (Net::LDAP::Filter.pres( "objectclass" ) & Net::LDAP::Filter.pres( "ou" ) & Net::LDAP::Filter.pres("sn")), + (Net::LDAP::Filter.pres( "objectclass" ) | Net::LDAP::Filter.pres( "ou" ) | Net::LDAP::Filter.pres("sn")), + + Net::LDAP::Filter.eq( "objectclass", "*aaa" ), + Net::LDAP::Filter.eq( "objectclass", "*aaa*bbb" ), + Net::LDAP::Filter.eq( "objectclass", "*aaa*bbb*ccc" ), + Net::LDAP::Filter.eq( "objectclass", "aaa*bbb" ), + Net::LDAP::Filter.eq( "objectclass", "aaa*bbb*ccc" ), + Net::LDAP::Filter.eq( "objectclass", "abc*def*1111*22*g" ), + Net::LDAP::Filter.eq( "objectclass", "*aaa*" ), + Net::LDAP::Filter.eq( "objectclass", "*aaa*bbb*" ), + Net::LDAP::Filter.eq( "objectclass", "*aaa*bbb*ccc*" ), + Net::LDAP::Filter.eq( "objectclass", "aaa*" ), + Net::LDAP::Filter.eq( "objectclass", "aaa*bbb*" ), + Net::LDAP::Filter.eq( "objectclass", "aaa*bbb*ccc*" ), + ].each {|ber| + f = Net::LDAP::Filter.parse_ber( ber.to_ber.read_ber( Net::LDAP::AsnSyntax) ) + assert( f == ber ) + assert_equal( f.to_ber, ber.to_ber ) + } + + end + + def test_ber_from_rfc2254_filter + [ + Net::LDAP::Filter.construct( "objectclass=*" ), + Net::LDAP::Filter.construct("objectclass=ou" ), + Net::LDAP::Filter.construct("uid >= 500" ), + Net::LDAP::Filter.construct("uid <= 500" ), + Net::LDAP::Filter.construct("(!(uid=*))" ), + Net::LDAP::Filter.construct("(&(uid=*)(objectclass=*))" ), + Net::LDAP::Filter.construct("(&(uid=*)(objectclass=*)(sn=*))" ), + Net::LDAP::Filter.construct("(|(uid=*)(objectclass=*))" ), + Net::LDAP::Filter.construct("(|(uid=*)(objectclass=*)(sn=*))" ), + + Net::LDAP::Filter.construct("objectclass=*aaa"), + Net::LDAP::Filter.construct("objectclass=*aaa*bbb"), + Net::LDAP::Filter.construct("objectclass=*aaa*bbb*ccc"), + Net::LDAP::Filter.construct("objectclass=aaa*bbb"), + Net::LDAP::Filter.construct("objectclass=aaa*bbb*ccc"), + Net::LDAP::Filter.construct("objectclass=abc*def*1111*22*g"), + Net::LDAP::Filter.construct("objectclass=*aaa*"), + Net::LDAP::Filter.construct("objectclass=*aaa*bbb*"), + Net::LDAP::Filter.construct("objectclass=*aaa*bbb*ccc*"), + Net::LDAP::Filter.construct("objectclass=aaa*"), + Net::LDAP::Filter.construct("objectclass=aaa*bbb*"), + Net::LDAP::Filter.construct("objectclass=aaa*bbb*ccc*"), + ].each {|ber| + f = Net::LDAP::Filter.parse_ber( ber.to_ber.read_ber( Net::LDAP::AsnSyntax) ) + assert( f == ber ) + assert_equal( f.to_ber, ber.to_ber ) + } + end end From 98406576891c78e41e77ad3fb364589fd71f4a73 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 5 May 2007 02:45:54 +0000 Subject: [PATCH 219/231] notes --- ChangeLog | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ChangeLog b/ChangeLog index 81f175c..c3d961f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -31,6 +31,9 @@ * Belatedly added a patch contributed by Kouhei Sutou last October. The patch adds start_tls support. * Added Net::LDAP#search_subschema_entry +* Added Net::LDAP::Filter#parse_ber, which constructs Net::LDAP::Filter + objects directly from BER objects that represent search filters in + LDAP SearchRequest packets. == Net::LDAP 0.0.4: August 15, 2006 From 2769d75b013b07be512ebc2858f52c3736ca95d2 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 6 May 2007 20:27:28 +0000 Subject: [PATCH 220/231] Added Net::LDAP::Filter.execute, which enables applications to perform arbitrary processing based on LDAP filters. --- lib/net/ldap/filter.rb | 45 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/lib/net/ldap/filter.rb b/lib/net/ldap/filter.rb index 58d70a6..b884628 100644 --- a/lib/net/ldap/filter.rb +++ b/lib/net/ldap/filter.rb @@ -281,6 +281,51 @@ class Filter end + # Perform filter operations against a user-supplied block. This is useful when implementing + # an LDAP directory server. The caller's block will be called with two arguments: first, a + # symbol denoting the "operation" of the filter; and second, an array consisting of arguments + # to the operation. The user-supplied block (which is MANDATORY) should perform some desired + # application-defined processing, and may return a locally-meaningful object that will appear + # as a parameter in the :and, :or and :not operations detailed below. + # + # A typical object to return from the user-supplied block is an array of + # Net::LDAP::Filter objects. + # + # These are the possible values that may be passed to the user-supplied block: + # :equalityMatch (the arguments will be an attribute name and a value to be matched); + # :substrings (two arguments: an attribute name and a value containing one or more * characters); + # :present (one argument: an attribute name); + # :greaterOrEqual (two arguments: an attribute name and a value to be compared against); + # :lessOrEqual (two arguments: an attribute name and a value to be compared against); + # :and (two or more arguments, each of which is an object returned from a recursive call + # to #execute, with the same block; + # :or (two or more arguments, each of which is an object returned from a recursive call + # to #execute, with the same block; + # :not (one argument, which is an object returned from a recursive call to #execute with the + # the same block. + # + def execute &block + case @op + when :eq + if @right == "*" + yield :present, @left + elsif @right.index '*' + yield :substrings, @left, @right + else + yield :equalityMatch, @left, @right + end + when :ge + yield :greaterOrEqual, @left, @right + when :le + yield :lessOrEqual, @left, @right + when :or, :and + yield @op, (@left.execute &block), (@right.execute &block) + when :not + yield @op, (@left.execute &block) + end || [] + end + + #-- # coalesce # This is a private helper method for dealing with chains of ANDs and ORs From 1b568e82df3f5f9efaf47dd6a62e791a799bbc74 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 6 May 2007 20:28:24 +0000 Subject: [PATCH 221/231] notes on Filter#execute. --- ChangeLog | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog b/ChangeLog index c3d961f..02fc839 100644 --- a/ChangeLog +++ b/ChangeLog @@ -34,6 +34,8 @@ * Added Net::LDAP::Filter#parse_ber, which constructs Net::LDAP::Filter objects directly from BER objects that represent search filters in LDAP SearchRequest packets. +* Added Net::LDAP::Filter#execute, which allows arbitrary processing + based on LDAP filters. == Net::LDAP 0.0.4: August 15, 2006 From 64a8240ae0f7c0f10331a68d4aa1ac671f7681ae Mon Sep 17 00:00:00 2001 From: blackhedd Date: Mon, 7 May 2007 03:43:32 +0000 Subject: [PATCH 222/231] Bugfix. Net::LDAP#bind was ignoring the inbound auth parameter. Thanks to Kouhei Sutou for spotting this and pointing it out. --- ChangeLog | 3 ++- lib/net/ldap.rb | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index 02fc839..41c2c16 100644 --- a/ChangeLog +++ b/ChangeLog @@ -36,7 +36,8 @@ LDAP SearchRequest packets. * Added Net::LDAP::Filter#execute, which allows arbitrary processing based on LDAP filters. - +* Fixed bug in Net::LDAP#bind. We were ignoring the passed-in auth parm. + Thanks to Kouhei Sutou for spotting it. == Net::LDAP 0.0.4: August 15, 2006 * Undeprecated Net::LDAP#modify. Thanks to Justin Forder for diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 437938c..c9b76d4 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -727,7 +727,7 @@ module Net @result = @open_connection.bind auth else conn = Connection.new( :host => @host, :port => @port , :encryption => @encryption) - @result = conn.bind @auth + @result = conn.bind auth conn.close end From 3f7a4a254debc683e7e8e6b4ad915801498f4dfc Mon Sep 17 00:00:00 2001 From: blackhedd Date: Fri, 18 May 2007 21:50:22 +0000 Subject: [PATCH 223/231] applied parentheses to quiet some -w warnings. --- lib/net/ldap/filter.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/net/ldap/filter.rb b/lib/net/ldap/filter.rb index b884628..fb24476 100644 --- a/lib/net/ldap/filter.rb +++ b/lib/net/ldap/filter.rb @@ -319,9 +319,9 @@ class Filter when :le yield :lessOrEqual, @left, @right when :or, :and - yield @op, (@left.execute &block), (@right.execute &block) + yield @op, (@left.execute(&block)), (@right.execute(&block)) when :not - yield @op, (@left.execute &block) + yield @op, (@left.execute(&block)) end || [] end From d0c877bcd5626d49239a522b8d4a9b988fb5ab58 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 20 Jun 2007 12:31:37 +0000 Subject: [PATCH 224/231] Patch by Kouhei Sutou, enables support of \XX octal filter syntax --- lib/net/ldap/filter.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/net/ldap/filter.rb b/lib/net/ldap/filter.rb index fb24476..b323762 100644 --- a/lib/net/ldap/filter.rb +++ b/lib/net/ldap/filter.rb @@ -464,7 +464,8 @@ class FilterParser #:nodoc: if op = scanner.scan( /\=|\<\=|\<|\>\=|\>|\!\=/ ) scanner.scan(/\s*/) #if value = scanner.scan( /[\w\*\.]+/ ) (ORG) - if value = scanner.scan( /[\w\*\.\+\-@=#\$%&! ]+/ ) + #if value = scanner.scan( /[\w\*\.\+\-@=#\$%&! ]+/ ) (ff suggested by Kouhei Sutou + if value = scanner.scan( /(?:[\w\*\.\+\-@=#\$%&! ]|\\[a-fA-F\d]{2,2})+/ ) case op when "=" Filter.eq( token, value ) From e429d7034bd15e599841b0ade11f8313890b2932 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Wed, 20 Jun 2007 12:32:37 +0000 Subject: [PATCH 225/231] notes --- ChangeLog | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog b/ChangeLog index 41c2c16..518bfda 100644 --- a/ChangeLog +++ b/ChangeLog @@ -38,6 +38,8 @@ based on LDAP filters. * Fixed bug in Net::LDAP#bind. We were ignoring the passed-in auth parm. Thanks to Kouhei Sutou for spotting it. +* Patched filter syntax to support octal \XX codes. Thanks to Kouhei Sutou + for the patch. == Net::LDAP 0.0.4: August 15, 2006 * Undeprecated Net::LDAP#modify. Thanks to Justin Forder for From 089abcceb24cb9315e9edcdfbff397add7062f7b Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 11 Aug 2007 01:26:06 +0000 Subject: [PATCH 226/231] Applied patch from Kouhei Sutou --- lib/net/ldap/filter.rb | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/net/ldap/filter.rb b/lib/net/ldap/filter.rb index b323762..fc4b090 100644 --- a/lib/net/ldap/filter.rb +++ b/lib/net/ldap/filter.rb @@ -208,12 +208,12 @@ class Filter end [@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific 4 else #equality - [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 3 + [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific 3 end when :ge - [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 5 + [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific 5 when :le - [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 6 + [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific 6 when :and ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten ary.map {|a| a.to_ber}.to_ber_contextspecific( 0 ) @@ -229,6 +229,12 @@ class Filter end end + def unescape(right) + right.gsub(/\\([a-fA-F\d]{2,2})/) do + [$1.hex].pack("U") + end + end + # Converts an LDAP search filter in BER format to an Net::LDAP::Filter # object. The incoming BER object most likely came to us by parsing an From c6307d2ce4e2cddf5fbafb0db39388c71107bd89 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sat, 11 Aug 2007 01:27:09 +0000 Subject: [PATCH 227/231] change notes --- ChangeLog | 1 + 1 file changed, 1 insertion(+) diff --git a/ChangeLog b/ChangeLog index 518bfda..0afbc85 100644 --- a/ChangeLog +++ b/ChangeLog @@ -40,6 +40,7 @@ Thanks to Kouhei Sutou for spotting it. * Patched filter syntax to support octal \XX codes. Thanks to Kouhei Sutou for the patch. +* Applied an additional patch from Kouhei. == Net::LDAP 0.0.4: August 15, 2006 * Undeprecated Net::LDAP#modify. Thanks to Justin Forder for From 6e53144090560be9b1241e97e2cbec70d37013b1 Mon Sep 17 00:00:00 2001 From: blackhedd Date: Sun, 26 Aug 2007 09:28:37 +0000 Subject: [PATCH 228/231] supported comma in filter strings, suggested by Kouhei. --- ChangeLog | 1 + lib/net/ldap/filter.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 0afbc85..2f93685 100644 --- a/ChangeLog +++ b/ChangeLog @@ -41,6 +41,7 @@ * Patched filter syntax to support octal \XX codes. Thanks to Kouhei Sutou for the patch. * Applied an additional patch from Kouhei. +* Allowed comma in filter strings, suggested by Kouhei. == Net::LDAP 0.0.4: August 15, 2006 * Undeprecated Net::LDAP#modify. Thanks to Justin Forder for diff --git a/lib/net/ldap/filter.rb b/lib/net/ldap/filter.rb index fc4b090..a3f5899 100644 --- a/lib/net/ldap/filter.rb +++ b/lib/net/ldap/filter.rb @@ -471,7 +471,7 @@ class FilterParser #:nodoc: scanner.scan(/\s*/) #if value = scanner.scan( /[\w\*\.]+/ ) (ORG) #if value = scanner.scan( /[\w\*\.\+\-@=#\$%&! ]+/ ) (ff suggested by Kouhei Sutou - if value = scanner.scan( /(?:[\w\*\.\+\-@=#\$%&! ]|\\[a-fA-F\d]{2,2})+/ ) + if value = scanner.scan( /(?:[\w\*\.\+\-@=,#\$%&! ]|\\[a-fA-F\d]{2,2})+/ ) case op when "=" Filter.eq( token, value ) From b828ff52cff548c10f8578130c4f931f5257970d Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 4 Sep 2007 12:07:35 +0000 Subject: [PATCH 229/231] Changed error classes to inherit from StandardError instead of Exception. This should make them easier to use with irb. --- ChangeLog | 3 +++ lib/net/ber.rb | 2 +- lib/net/ldap.rb | 2 +- lib/net/ldap/pdu.rb | 2 +- lib/net/snmp.rb | 2 +- 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ChangeLog b/ChangeLog index 2f93685..71dda06 100644 --- a/ChangeLog +++ b/ChangeLog @@ -42,6 +42,9 @@ for the patch. * Applied an additional patch from Kouhei. * Allowed comma in filter strings, suggested by Kouhei. +* 04Sep07, Changed four error classes to inherit from StandardError rather + Exception, in order to be friendlier to irb. Suggested by Kouhei. + == Net::LDAP 0.0.4: August 15, 2006 * Undeprecated Net::LDAP#modify. Thanks to Justin Forder for diff --git a/lib/net/ber.rb b/lib/net/ber.rb index b0c79c7..dbe2fbd 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -33,7 +33,7 @@ module Net module BER - class BerError < Exception; end + class BerError < StandardError; end class BerIdentifiedString < String diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index c9b76d4..b9d7b0e 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -261,7 +261,7 @@ module Net class LDAP - class LdapError < Exception; end + class LdapError < StandardError; end VERSION = "0.1.0" diff --git a/lib/net/ldap/pdu.rb b/lib/net/ldap/pdu.rb index 585b28c..c47735e 100644 --- a/lib/net/ldap/pdu.rb +++ b/lib/net/ldap/pdu.rb @@ -31,7 +31,7 @@ module Net -class LdapPduError < Exception; end +class LdapPduError < StandardError; end class LdapPdu diff --git a/lib/net/snmp.rb b/lib/net/snmp.rb index 1fda412..f74f265 100644 --- a/lib/net/snmp.rb +++ b/lib/net/snmp.rb @@ -94,7 +94,7 @@ module Net end class SnmpPdu - class Error < Exception; end + class Error < StandardError; end PduTypes = [ :get_request, From e6cccef62780a84fa99f8ffa7efdb9ad40e6b20d Mon Sep 17 00:00:00 2001 From: blackhedd Date: Tue, 23 Oct 2007 12:31:58 +0000 Subject: [PATCH 230/231] added clarifying documentation parameters to #bind. --- lib/net/ldap.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index b9d7b0e..3792501 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -709,6 +709,23 @@ module Net # p ldap.get_operation_result # end # + # Here's a more succinct example which does exactly the same thing, but + # collects all the required parameters into arguments: + # + # require 'net/ldap' + # ldap = Net::LDAP.new( :host=>your_server_ip_address, :port=>389 ) + # if ldap.bind( :method=>:simple, :username=>your_user_name, :password=>your_user_password ) + # # authentication succeeded + # else + # # authentication failed + # p ldap.get_operation_result + # end + # + # You don't need to pass a user-password as a String object to bind. You can + # also pass a Ruby Proc object which returns a string. This will cause bind to + # execute the Proc (which might then solicit input from a user with console display + # suppressed). The String value returned from the Proc is used as the password. + # # You don't have to create a new instance of Net::LDAP every time # you perform a binding in this way. If you prefer, you can cache the Net::LDAP object # and re-use it to perform subsequent bindings, provided you call From 6b5548ffd2c5079c372c291d2527e8e63e4e36f0 Mon Sep 17 00:00:00 2001 From: emiel Date: Fri, 14 Nov 2008 23:22:30 +0000 Subject: [PATCH 231/231] This commit is all about moving toward 'Hoe' as the project helper. $ gem install hoe To get there the following has been done: * Adhere to hoe naming conventions. * Migrate and update tests. * Consolidate and update documentation. * Prepare History.txt for release. * And I probably forgot something... --- ChangeLog | 104 ------------------ History.txt | 95 ++++++++++++++++ Install | 21 ---- LICENCE => LICENSE | 0 Manifest.txt | 37 +++++++ README | 32 ------ README.txt | 62 +++++++++++ Rakefile | 248 ++---------------------------------------- lib/net/ldap.rb | 2 +- net-ldap.gemspec | 43 -------- pre-setup.rb | 7 +- test/common.rb | 7 ++ test/test_ber.rb | 100 +++++++++++++++++ test/test_entry.rb | 7 ++ test/test_filter.rb | 83 ++++++++++++++ test/test_ldif.rb | 59 ++++++++++ test/test_password.rb | 17 +++ test/test_snmp.rb | 130 ++++++++++++++++++++++ test/testdata.ldif | 101 +++++++++++++++++ tests/NOTICE.txt | 6 + 20 files changed, 719 insertions(+), 442 deletions(-) delete mode 100644 ChangeLog create mode 100644 History.txt delete mode 100644 Install rename LICENCE => LICENSE (100%) create mode 100644 Manifest.txt delete mode 100644 README create mode 100644 README.txt delete mode 100644 net-ldap.gemspec create mode 100644 test/common.rb create mode 100644 test/test_ber.rb create mode 100644 test/test_entry.rb create mode 100644 test/test_filter.rb create mode 100644 test/test_ldif.rb create mode 100644 test/test_password.rb create mode 100644 test/test_snmp.rb create mode 100644 test/testdata.ldif create mode 100644 tests/NOTICE.txt diff --git a/ChangeLog b/ChangeLog deleted file mode 100644 index 71dda06..0000000 --- a/ChangeLog +++ /dev/null @@ -1,104 +0,0 @@ -= Net::LDAP Changelog - -== Net::LDAP 0.1.0: August xx, 2006 -* Bumped version up to 0.1.0, reflecting both code and API stability. -* Silenced some annoying warnings in filter.rb. Thanks to "barjunk" - for pointing this out. -* Added Net::LDAP::Entry#to_ldif -* Patched Rakefile so it actually runs the test suite. Thanks to - Daniel Berger for submitting his patch. -* Changed Net::LDAP::Entry so it can be marshalled and unmarshalled. - Thanks to an anonymous feature requester who only left the name - "Jammy." -* Added support for binary values in Net::LDAP::Entry LDIF conversions - and marshalling. -* Supported rootDSE searches with a new API. -* Minor bug fixes here and there -* Some fairly extensive performance optimizations in the BER parser. -* Fixed a bug in Net::LDAP::Entry::from_single_ldif_string noticed by - Matthias Tarasiewicz. -* Added [reliminary (still undocumented) support for SASL authentication. -* Removed an erroneous LdapError value, noticed by Kouhei Sutou. -* Supported attributes containing blanks (cn=Babs Jensen) to - Filter#construct. Suggested by an anonymous Rubyforge user. -* Supported several constructs from the server side of the LDAP protocol. -* Added a "consuming" String#read_ber! method. -* Added missing synactic support for Filter ANDs, NOTs and a few other - things. -* Added some support for SNMP data-handling. -* Extended support for server-reported error messages. This was provisionally - added to Net::LDAP#add, and eventually will be added to other methods. -* Belatedly added a patch contributed by Kouhei Sutou last October. - The patch adds start_tls support. -* Added Net::LDAP#search_subschema_entry -* Added Net::LDAP::Filter#parse_ber, which constructs Net::LDAP::Filter - objects directly from BER objects that represent search filters in - LDAP SearchRequest packets. -* Added Net::LDAP::Filter#execute, which allows arbitrary processing - based on LDAP filters. -* Fixed bug in Net::LDAP#bind. We were ignoring the passed-in auth parm. - Thanks to Kouhei Sutou for spotting it. -* Patched filter syntax to support octal \XX codes. Thanks to Kouhei Sutou - for the patch. -* Applied an additional patch from Kouhei. -* Allowed comma in filter strings, suggested by Kouhei. -* 04Sep07, Changed four error classes to inherit from StandardError rather - Exception, in order to be friendlier to irb. Suggested by Kouhei. - - -== Net::LDAP 0.0.4: August 15, 2006 -* Undeprecated Net::LDAP#modify. Thanks to Justin Forder for - providing the rationale for this. -* Added a much-expanded set of special characters to the parser - for RFC-2254 filters. Thanks to Andre Nathan. -* Changed Net::LDAP#search so you can pass it a filter in string form. - The conversion to a Net::LDAP::Filter now happens automatically. -* Implemented Net::LDAP#bind_as (preliminary and subject to change). - Thanks for Simon Claret for valuable suggestions and for helping test. -* Fixed bug in Net::LDAP#open that was preventing #open from being - called more than one on a given Net::LDAP object. - -== Net::LDAP 0.0.3: July 26, 2006 -* Added simple TLS encryption. - Thanks to Garett Shulman for suggestions and for helping test. - -== Net::LDAP 0.0.2: July 12, 2006 -* Fixed malformation in distro tarball and gem. -* Improved documentation. -* Supported "paged search control." -* Added a range of API improvements. -* Thanks to Andre Nathan, andre@digirati.com.br, for valuable - suggestions. -* Added support for LE and GE search filters. -* Added support for Search referrals. -* Fixed a regression with openldap 2.2.x and higher caused - by the introduction of RFC-2696 controls. Thanks to Andre - Nathan for reporting the problem. -* Added support for RFC-2254 filter syntax. - -== Net::LDAP 0.0.1: May 1, 2006 -* Initial release. -* Client functionality is near-complete, although the APIs - are not guaranteed and may change depending on feedback - from the community. -* We're internally working on a Ruby-based implementation - of a full-featured, production-quality LDAP server, - which will leverage the underlying LDAP and BER functionality - in Net::LDAP. -* Please tell us if you would be interested in seeing a public - release of the LDAP server. -* Grateful acknowledgement to Austin Ziegler, who reviewed - this code and provided the release framework, including - minitar. - -#-- -# Net::LDAP for Ruby. -# http://rubyforge.org/projects/net-ldap/ -# Copyright (C) 2006 by Francis Cianfrocca -# -# Available under the same terms as Ruby. See LICENCE in the main -# distribution for full licensing information. -# -# $Id: ChangeLog,v 1.17.2.4 2005/09/09 12:36:42 austin Exp $ -#++ -# vim: sts=2 sw=2 ts=4 et ai tw=77 diff --git a/History.txt b/History.txt new file mode 100644 index 0000000..b85c852 --- /dev/null +++ b/History.txt @@ -0,0 +1,95 @@ +=== Net::LDAP 0.0.5 / 2008-11-xx + +* 13 minor enhancements: + * Added Net::LDAP::Entry#to_ldif + * Supported rootDSE searches with a new API. + * Added [preliminary (still undocumented) support for SASL authentication. + * Supported several constructs from the server side of the LDAP protocol. + * Added a "consuming" String#read_ber! method. + * Added some support for SNMP data-handling. + * Belatedly added a patch contributed by Kouhei Sutou last October. + The patch adds start_tls support. + * Added Net::LDAP#search_subschema_entry + * Added Net::LDAP::Filter#parse_ber, which constructs Net::LDAP::Filter + objects directly from BER objects that represent search filters in + LDAP SearchRequest packets. + * Added Net::LDAP::Filter#execute, which allows arbitrary processing + based on LDAP filters. + * Changed Net::LDAP::Entry so it can be marshalled and unmarshalled. + Thanks to an anonymous feature requester who only left the name + "Jammy." + * Added support for binary values in Net::LDAP::Entry LDIF conversions + and marshalling. + * Migrated to 'hoe' as the new project droid. + +* 13 bugs fixed: + * Silenced some annoying warnings in filter.rb. Thanks to "barjunk" + for pointing this out. + * Some fairly extensive performance optimizations in the BER parser. + * Fixed a bug in Net::LDAP::Entry::from_single_ldif_string noticed by + Matthias Tarasiewicz. + * Removed an erroneous LdapError value, noticed by Kouhei Sutou. + * Supported attributes containing blanks (cn=Babs Jensen) to + Filter#construct. Suggested by an anonymous Rubyforge user. + * Added missing syntactic support for Filter ANDs, NOTs and a few other + things. + * Extended support for server-reported error messages. This was provisionally + added to Net::LDAP#add, and eventually will be added to other methods. + * Fixed bug in Net::LDAP#bind. We were ignoring the passed-in auth parm. + Thanks to Kouhei Sutou for spotting it. + * Patched filter syntax to support octal \XX codes. Thanks to Kouhei Sutou + for the patch. + * Applied an additional patch from Kouhei. + * Allowed comma in filter strings, suggested by Kouhei. + * 04Sep07, Changed four error classes to inherit from StandardError rather + Exception, in order to be friendlier to irb. Suggested by Kouhei. + * Minor bug fixes here and there + +=== Net::LDAP 0.0.4 / 2006-08-15 + +* Undeprecated Net::LDAP#modify. Thanks to Justin Forder for + providing the rationale for this. +* Added a much-expanded set of special characters to the parser + for RFC-2254 filters. Thanks to Andre Nathan. +* Changed Net::LDAP#search so you can pass it a filter in string form. + The conversion to a Net::LDAP::Filter now happens automatically. +* Implemented Net::LDAP#bind_as (preliminary and subject to change). + Thanks for Simon Claret for valuable suggestions and for helping test. +* Fixed bug in Net::LDAP#open that was preventing #open from being + called more than one on a given Net::LDAP object. + +=== Net::LDAP 0.0.3 / 2006-07-26 + +* Added simple TLS encryption. + Thanks to Garett Shulman for suggestions and for helping test. + +=== Net::LDAP 0.0.2 / 2006-07-12 + +* Fixed malformation in distro tarball and gem. +* Improved documentation. +* Supported "paged search control." +* Added a range of API improvements. +* Thanks to Andre Nathan, andre@digirati.com.br, for valuable + suggestions. +* Added support for LE and GE search filters. +* Added support for Search referrals. +* Fixed a regression with openldap 2.2.x and higher caused + by the introduction of RFC-2696 controls. Thanks to Andre + Nathan for reporting the problem. +* Added support for RFC-2254 filter syntax. + +=== Net::LDAP 0.0.1 / 2006-05-01 + +* Initial release. +* Client functionality is near-complete, although the APIs + are not guaranteed and may change depending on feedback + from the community. +* We're internally working on a Ruby-based implementation + of a full-featured, production-quality LDAP server, + which will leverage the underlying LDAP and BER functionality + in Net::LDAP. +* Please tell us if you would be interested in seeing a public + release of the LDAP server. +* Grateful acknowledgement to Austin Ziegler, who reviewed + this code and provided the release framework, including + minitar. diff --git a/Install b/Install deleted file mode 100644 index 21db14d..0000000 --- a/Install +++ /dev/null @@ -1,21 +0,0 @@ -Net::LDAP is a pure Ruby LDAP client. It does not as yet require any external -libraries. It can be installed with: - - % ruby setup.rb - -Alternatively, you can use the RubyGems version of Net::LDAP availalble as -ruby-net-ldap-0.0.1.gem from the usual sources. - -Net::LDAP:: http://rubyforge.org/projects/net-ldap/ - -#-- -# Net::LDAP for Ruby. -# http://rubyforge.org/projects/net-ldap/ -# Copyright (C) 2006 by Francis Cianfrocca -# -# Available under the same terms as Ruby. See LICENCE in the main -# distribution for full licensing information. -# -# $Id: ChangeLog,v 1.17.2.4 2005/09/09 12:36:42 austin Exp $ -#++ -# vim: sts=2 sw=2 ts=4 et ai tw=77 diff --git a/LICENCE b/LICENSE similarity index 100% rename from LICENCE rename to LICENSE diff --git a/Manifest.txt b/Manifest.txt new file mode 100644 index 0000000..64fcda6 --- /dev/null +++ b/Manifest.txt @@ -0,0 +1,37 @@ +COPYING +History.txt +LICENSE +Manifest.txt +README.txt +Rakefile +Release-Announcement +lib/net/ber.rb +lib/net/ldap.rb +lib/net/ldap/dataset.rb +lib/net/ldap/entry.rb +lib/net/ldap/filter.rb +lib/net/ldap/pdu.rb +lib/net/ldap/psw.rb +lib/net/ldif.rb +lib/net/snmp.rb +pre-setup.rb +setup.rb +test/common.rb +test/test_ber.rb +test/test_entry.rb +test/test_filter.rb +test/test_ldif.rb +test/test_password.rb +test/test_snmp.rb +test/testdata.ldif +tests/NOTICE.txt +tests/testber.rb +tests/testdata.ldif +tests/testem.rb +tests/testfilter.rb +tests/testldap.rb +tests/testldif.rb +tests/testpsw.rb +tests/testsnmp.rb +testserver/ldapserver.rb +testserver/testdata.ldif diff --git a/README b/README deleted file mode 100644 index 93499d8..0000000 --- a/README +++ /dev/null @@ -1,32 +0,0 @@ -= Net::LDAP for Ruby -Net::LDAP is an LDAP support library written in pure Ruby. It supports all -LDAP client features, and a subset of server features as well. - -Homepage:: http://rubyforge.org/projects/net-ldap/ -Copyright:: (C) 2006 by Francis Cianfrocca - -Original developer: Francis Cianfrocca -Contributions by Austin Ziegler gratefully acknowledged. - -== LICENCE NOTES -Please read the file LICENCE for licensing restrictions on this library. In -the simplest terms, this library is available under the same terms as Ruby -itself. - -== Requirements -Net::LDAP requires Ruby 1.8.2 or better. - -== Documentation -See Net::LDAP for documentation and usage samples. - -#-- -# Net::LDAP for Ruby. -# http://rubyforge.org/projects/net-ldap/ -# Copyright (C) 2006 by Francis Cianfrocca -# -# Available under the same terms as Ruby. See LICENCE in the main -# distribution for full licensing information. -# -# $Id$ -#++ -# vim: sts=2 sw=2 ts=4 et ai tw=77 diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..45b6b6a --- /dev/null +++ b/README.txt @@ -0,0 +1,62 @@ += Net::LDAP for Ruby + +* http://rubyforge.org/projects/net-ldap + +== DESCRIPTION: + +Pure Ruby LDAP library. + +== FEATURES/PROBLEMS: + +The Lightweight Directory Access Protocol (LDAP) is an Internet protocol +for accessing distributed directory services. + +Net::LDAP is an LDAP support library written in pure Ruby. It supports +most LDAP client features and a subset of server features as well. + +* Standards-based (going for RFC 4511) +* Portable: 100% Ruby + +== SYNOPSIS: + +See Net::LDAP for documentation and usage samples. + +== REQUIREMENTS: + +Net::LDAP requires Ruby 1.8.2 or better. + +== INSTALL: + +Net::LDAP is a pure Ruby library. It does not require any external +libraries. + +You can install the RubyGems version of Net::LDAP available from the +usual sources. + +* gem install net-ldap + +If using the packaged (.tgz) version; it can be installed with: + +* ruby setup.rb + +== CREDITS: + +Net::LDAP was originally developed by: + +* Francis Cianfrocca + +Contributions since: + +* Austin Ziegler +* Emiel van de Laar + +== LICENSE: + +Copyright (C) 2006 by Francis Cianfrocca + +Please read the file LICENSE for licensing restrictions on this library. In +the simplest terms, this library is available under the same terms as Ruby +itself. + +Available under the same terms as Ruby. See LICENSE in the main +distribution for full licensing information. diff --git a/Rakefile b/Rakefile index f146781..75c8182 100644 --- a/Rakefile +++ b/Rakefile @@ -1,244 +1,18 @@ -#! /usr/bin/env rake -#-- -# Net::LDAP for Ruby. -# http://rubyforge.org/projects/net-ldap/ -# Copyright (C) 2006 by Francis Cianfrocca -# -# Available under the same terms as Ruby. See LICENCE in the main -# distribution for full licensing information. -# -# $Id$ -#++ +# -*- ruby -*- -require 'meta_project' -require 'rake/gempackagetask' -require 'rake/contrib/xforge' -require 'rake/clean' +require 'rubygems' +require 'hoe' -$can_gmail = false -begin - require 'gmailer' - $can_gmail = true -rescue LoadError -end +# Add 'lib' to load path. +$LOAD_PATH.unshift( "#{File.dirname(__FILE__)}/lib" ) -$can_minitar = false -begin - require 'archive/tar/minitar' - require 'zlib' - $can_minitar = true -rescue LoadError -end - -$LOAD_PATH.unshift "lib" +# Pull in local 'net/ldap' as opposed to an installed version. require 'net/ldap' -$version = Net::LDAP::VERSION -$name = Net::LDAP.to_s -$project = MetaProject::Project::XForge::RubyForge.new('net-ldap') -$distdir = "ruby-net-ldap-#$version" -$tardist = "../#$distdir.tar.gz" - -$release_date = nil -$release_date = Time.parse(ENV['RELEASE_DATE']) if ENV['RELEASE_DATE'] - - -def run_test_set the_task, testcases - require 'test/unit/testsuite' - require 'test/unit/ui/console/testrunner' - - runner = Test::Unit::UI::Console::TestRunner - - $LOAD_PATH.unshift('tests') - $stderr.puts "Checking for test cases:" if the_task.verbose - testcases.each do |testcase| - $stderr.puts "\t#{testcase}" if the_task.verbose - load testcase - end - - suite = Test::Unit::TestSuite.new($name) - - ObjectSpace.each_object(Class) do |testcase| - suite << testcase.suite if testcase < Test::Unit::TestCase - end - - runner.run(suite) +Hoe.new('net-ldap', Net::LDAP::VERSION) do |p| + p.rubyforge_name = 'net-ldap' + p.developer('Francis Cianfrocca', 'garbagecat10@gmail.com') + p.developer('Emiel van de Laar', 'gemiel@gmail.com') end -desc "Run the tests for #$name." -task :test do |t| - run_test_set t, Dir['tests/test*.rb'] -end - -desc "(Provisional) Run tests for SNMP" -task :test_snmp do |t| - run_test_set t, ['tests/testsnmp.rb', 'tests/testber.rb'] -end - -desc "(Provisional) Run tests for filters" -task :test_filters do |t| - run_test_set t, ['tests/testfilter.rb'] -end - -spec = eval(File.read("net-ldap.gemspec")) -spec.version = $version -desc "Build the RubyGem for #$name." -task :gem => [] #[ :test ] -Rake::GemPackageTask.new(spec) do |g| - if $can_minitar - g.need_tar = false - g.need_zip = false - end - g.package_dir = ".." -end - -if $can_minitar - desc "Build a #$name .tar.gz distribution." - task :tar => [ $tardist ] - file $tardist => [ :test ] do |t| - current = File.basename(Dir.pwd) - Dir.chdir("..") do - begin - files = %W(bin/**/* lib/**/* tests/**/* ChangeLog README LICENCE - COPYING Rakefile net-ldap.gemspec setup.rb pre-setup.rb) - files = FileList[files.map { |file| File.join(current, file) }].to_a - files.map! do |dd| - ddnew = dd.gsub(/^#{current}/, $distdir) - mtime = $release_date || File.stat(dd).mtime - if File.directory?(dd) - { :name => ddnew, :mode => 0755, :dir => true, :mtime => mtime } - else - if dd =~ %r{bin/} - mode = 0755 - else - mode = 0644 - end - data = File.open(dd, "rb") { |ff| ff.read } - { :name => ddnew, :mode => mode, :data => data, :size => - data.size, :mtime => mtime } - end - end - - ff = File.open(t.name.gsub(%r{^\.\./}o, ''), "wb") - gz = Zlib::GzipWriter.new(ff) - tw = Archive::Tar::Minitar::Writer.new(gz) - - files.each do |entry| - if entry[:dir] - tw.mkdir(entry[:name], entry) - else - tw.add_file_simple(entry[:name], entry) { |os| os.write(entry[:data]) } - end - end - ensure - tw.close if tw - gz.finish if gz - ff.close - end - end - end - task $tardist => [ :test ] -end - -desc "Build the RDoc documentation for #$name." -task :docs do - require 'rdoc/rdoc' - rdoc_options = %W(--title #$name --main README --line-numbers) - files = FileList[*%w(README LICENCE ChangeLog LICENCE bin/**/*.rb lib/**/*.rb)] - rdoc_options += files.to_a - RDoc::RDoc.new.document(rdoc_options) -end - -task :verify_rubyforge do - raise "RUBYFORGE_USER environment variable not set!" unless ENV['RUBYFORGE_USER'] - raise "RUBYFORGE_PASSWORD environment variable not set!" unless ENV['RUBYFORGE_PASSWORD'] -end - -if $can_gmail - task :verify_gmail do - raise "GMAIL_USER environment variable not set!" unless ENV['GMAIL_USER'] - raise "GMAIL_PASSWORD environment variable not set!" unless ENV['GMAIL_PASSWORD'] - end - - desc "Post a release announcement via GMail." - task :email_announcement => [ :verify_gmail ] do - GMailer.connect(ENV["GMAIL_USER"], ENV["GMAIL_PASSWORD"]) do |gmail| - msg = { - :to => "ruby-talk@ruby-lang.org, #{ENV['GMAIL_USER']}@gmail.com", - :subject => "[ANN] #$name #$version", - :body => File.read("Release-Announcement"), - } - gmail.send msg - end - end -end - -desc "Release files on RubyForge." -task :release_files => [ :verify_rubyforge, :gem ] do - release_files = FileList[$tardist, "../#$distdir.gem"] - Rake::XForge::Release.new($project) do |release| - release.user_name = ENV['RUBYFORGE_USER'] - release.password = ENV['RUBYFORGE_PASSWORD'] - release.files = release_files.to_a - release.release_name = "#$name #$version" - release.package_name = "ruby-net-ldap" - - notes = [] - File.open("README") do |file| - file.each do |line| - line.chomp! - line.gsub!(/^#.*$/, '') and next - notes << line - end - end - release.release_notes = notes.join("\n") - - changes = [] - File.open("ChangeLog") do |file| - current = true - - file.each do |line| - line.chomp! - current = false if current and line =~ /^==/ - break if line.empty? and not current - changes << line - end - end - release.release_changes = changes.join("\n") - end -end - -desc "Publish news on RubyForge" -task :publish_news => [ :verify_rubyforge, :gem ] do - Rake::XForge::NewsPublisher.new($project) do |news| - news.user_name = ENV['RUBYFORGE_USER'] - news.password = ENV['RUBYFORGE_PASSWORD'] - news.subject = "#$name #$version Released" - news.changes_file = nil - - details = [] - File.open("Release-Announcement") do |file| - file.each do |line| - line.chomp! - break if line =~ /^=/ - details << line - end - end - news.details = details.join("\n") - end -end - -desc "Release the latest version." -task :release => [ :verify_rubyforge, :release_files, :publish_news, :docs ] -if $can_gmail - task :release => [ :verify_gmail, :email_announcment ] -end - -desc "Build everything." -task :default => [ :gem ] - -if $can_minitar - task :release_files => :tar - task :publish_news => :tar - task :default => :tar -end +# vim: syntax=Ruby diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 3792501..e2f2d0f 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -263,7 +263,7 @@ module Net class LdapError < StandardError; end - VERSION = "0.1.0" + VERSION = "0.0.5" SearchScope_BaseObject = 0 diff --git a/net-ldap.gemspec b/net-ldap.gemspec deleted file mode 100644 index 5c5e829..0000000 --- a/net-ldap.gemspec +++ /dev/null @@ -1,43 +0,0 @@ -#-- -# Net::LDAP for Ruby. -# http://rubyforge.org/projects/net-ldap/ -# Copyright (C) 2006 by Francis Cianfrocca -# -# Available under the same terms as Ruby. See LICENCE in the main -# distribution for full licensing information. -# -# $Id: ChangeLog,v 1.17.2.4 2005/09/09 12:36:42 austin Exp $ -#++ - -spec = Gem::Specification.new do |s| - s.name = "ruby-net-ldap" - s.version = "0.0.2" - s.summary = %q(A pure Ruby LDAP client library.) - s.platform = Gem::Platform::RUBY - - s.has_rdoc = true - s.rdoc_options = %w(--title Net::LDAP --main README --line-numbers) - s.extra_rdoc_files = %w(README ChangeLog LICENCE COPYING) - - files = %w(README LICENCE ChangeLog COPYING {bin,tests,lib}/**/*) - s.files = FileList[*files].exclude("rdoc").to_a - - s.require_paths = ["lib"] - - s.test_files = %w{tests/testem.rb} - - s.author = "Francis Cianfrocca" - s.email = "garbagecat10@gmail.com" - s.rubyforge_project = %q(net-ldap) - s.homepage = "http://rubyforge.org/projects/net-ldap" - - description = [] - File.open("README") do |file| - file.each do |line| - line.chomp! - break if line.empty? - description << "#{line.gsub(/\[\d\]/, '')}" - end - end - s.description = description[1..-1].join(" ") -end diff --git a/pre-setup.rb b/pre-setup.rb index 61c38e3..8f88d75 100644 --- a/pre-setup.rb +++ b/pre-setup.rb @@ -37,10 +37,9 @@ def run_tests(test_list) $:.shift end -rdoc = %w(--main README --line-numbers - --title MIME::Types) +rdoc = %w(--main README.txt --line-numbers) ri = %w(--ri-site --merge) -dox = %w(README ChangeLog lib) +dox = %w(README.txt History.txt lib) build_rdoc rdoc + dox build_ri ri + dox -# run_tests Dir["tests/**/*"] +#run_tests Dir["test/**/*"] diff --git a/test/common.rb b/test/common.rb new file mode 100644 index 0000000..4e6c13b --- /dev/null +++ b/test/common.rb @@ -0,0 +1,7 @@ +# Add 'lib' to load path. +$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib" + +require 'rubygems' +require 'test/unit' + +require 'net/ldap' diff --git a/test/test_ber.rb b/test/test_ber.rb new file mode 100644 index 0000000..c5f6af1 --- /dev/null +++ b/test/test_ber.rb @@ -0,0 +1,100 @@ +# $Id: testber.rb 230 2006-12-19 18:27:57Z blackhedd $ + +require 'common' + +class TestBer < Test::Unit::TestCase + + def test_encode_boolean + assert_equal( "\x01\x01\x01", true.to_ber ) # should actually be: 01 01 ff + assert_equal( "\x01\x01\x00", false.to_ber ) + end + + #def test_encode_nil + # assert_equal( "\x05\x00", nil.to_ber ) + #end + + def test_encode_integer + + # Fixnum + # + #assert_equal( "\x02\x02\x96\x46", -27_066.to_ber ) + #assert_equal( "\x02\x02\xFF\x7F", -129.to_ber ) + #assert_equal( "\x02\x01\x80", -128.to_ber ) + #assert_equal( "\x02\x01\xFF", -1.to_ber ) + + assert_equal( "\x02\x01\x00", 0.to_ber ) + assert_equal( "\x02\x01\x01", 1.to_ber ) + assert_equal( "\x02\x01\x7F", 127.to_ber ) + assert_equal( "\x02\x02\x00\x80", 128.to_ber ) + assert_equal( "\x02\x02\x00\xFF", 255.to_ber ) + + assert_equal( "\x02\x02\x01\x00", 256.to_ber ) + assert_equal( "\x02\x03\x00\xFF\xFF", 65535.to_ber ) + + assert_equal( "\x02\x03\x01\x00\x00", 65536.to_ber ) + assert_equal( "\x02\x04\x00\xFF\xFF\xFF", 16_777_215.to_ber ) + + assert_equal( "\x02\x04\x01\x00\x00\x00", 0x01000000.to_ber ) + assert_equal( "\x02\x04\x3F\xFF\xFF\xFF", 0x3FFFFFFF.to_ber ) + + # Bignum + # + assert_equal( "\x02\x04\x4F\xFF\xFF\xFF", 0x4FFFFFFF.to_ber ) + #assert_equal( "\x02\x05\x00\xFF\xFF\xFF\xFF", 0xFFFFFFFF.to_ber ) + end + + # TOD Add some much bigger numbers + # 5000000000 is a Bignum, which hits different code. + def test_ber_integers + assert_equal( "\002\001\005", 5.to_ber ) + assert_equal( "\002\002\001\364", 500.to_ber ) + assert_equal( "\002\003\0\303P", 50000.to_ber ) + assert_equal( "\002\005\001*\005\362\000", 5000000000.to_ber ) + end + + def test_ber_bignums + # Some of these values are Fixnums and some are Bignums. Different BER code. + [ + 5, + 50, + 500, + 5000, + 50000, + 500000, + 5000000, + 50000000, + 500000000, + 1000000000, + 2000000000, + 3000000000, + 4000000000, + 5000000000 + ].each {|val| + assert_equal( val, val.to_ber.read_ber ) + } + end + + def test_ber_parsing + assert_equal( 6, "\002\001\006".read_ber( Net::LDAP::AsnSyntax )) + assert_equal( "testing", "\004\007testing".read_ber( Net::LDAP::AsnSyntax )) + end + + def test_ber_parser_on_ldap_bind_request + require 'stringio' + + s = StringIO.new( + "0$\002\001\001`\037\002\001\003\004\rAdministrator\200\vad_is_bogus" ) + + assert_equal( + [1, [3, "Administrator", "ad_is_bogus"]], + s.read_ber( Net::LDAP::AsnSyntax )) + end + + def test_oid + oid = Net::BER::BerIdentifiedOid.new( [1,3,6,1,2,1,1,1,0] ) + assert_equal( "\006\b+\006\001\002\001\001\001\000", oid.to_ber ) + + oid = Net::BER::BerIdentifiedOid.new( "1.3.6.1.2.1.1.1.0" ) + assert_equal( "\006\b+\006\001\002\001\001\001\000", oid.to_ber ) + end +end diff --git a/test/test_entry.rb b/test/test_entry.rb new file mode 100644 index 0000000..3a58105 --- /dev/null +++ b/test/test_entry.rb @@ -0,0 +1,7 @@ +require 'common' + +class TestEntry < Test::Unit::TestCase + def test_entry + # FIX + end +end diff --git a/test/test_filter.rb b/test/test_filter.rb new file mode 100644 index 0000000..08b4f30 --- /dev/null +++ b/test/test_filter.rb @@ -0,0 +1,83 @@ +# $Id: testfilter.rb 245 2007-05-05 02:44:32Z blackhedd $ + +require 'common' + +class TestFilter < Test::Unit::TestCase + + # Note that the RFC doesn't define either less-than or greater-than. + def test_rfc_2254 + Net::LDAP::Filter.from_rfc2254( " ( uid=george* ) " ) + Net::LDAP::Filter.from_rfc2254( "uid!=george*" ) + Net::LDAP::Filter.from_rfc2254( "uid <= george*" ) + Net::LDAP::Filter.from_rfc2254( "uid>=george*" ) + Net::LDAP::Filter.from_rfc2254( "uid!=george*" ) + + Net::LDAP::Filter.from_rfc2254( "(& (uid!=george* ) (mail=*))" ) + Net::LDAP::Filter.from_rfc2254( "(| (uid!=george* ) (mail=*))" ) + Net::LDAP::Filter.from_rfc2254( "(! (mail=*))" ) + end + + def test_filters_from_ber + [ + Net::LDAP::Filter.eq( "objectclass", "*" ), + Net::LDAP::Filter.pres( "objectclass" ), + Net::LDAP::Filter.eq( "objectclass", "ou" ), + Net::LDAP::Filter.ge( "uid", "500" ), + Net::LDAP::Filter.le( "uid", "500" ), + (~ Net::LDAP::Filter.pres( "objectclass" )), + (Net::LDAP::Filter.pres( "objectclass" ) & Net::LDAP::Filter.pres( "ou" )), + (Net::LDAP::Filter.pres( "objectclass" ) & Net::LDAP::Filter.pres( "ou" ) & Net::LDAP::Filter.pres("sn")), + (Net::LDAP::Filter.pres( "objectclass" ) | Net::LDAP::Filter.pres( "ou" ) | Net::LDAP::Filter.pres("sn")), + + Net::LDAP::Filter.eq( "objectclass", "*aaa" ), + Net::LDAP::Filter.eq( "objectclass", "*aaa*bbb" ), + Net::LDAP::Filter.eq( "objectclass", "*aaa*bbb*ccc" ), + Net::LDAP::Filter.eq( "objectclass", "aaa*bbb" ), + Net::LDAP::Filter.eq( "objectclass", "aaa*bbb*ccc" ), + Net::LDAP::Filter.eq( "objectclass", "abc*def*1111*22*g" ), + Net::LDAP::Filter.eq( "objectclass", "*aaa*" ), + Net::LDAP::Filter.eq( "objectclass", "*aaa*bbb*" ), + Net::LDAP::Filter.eq( "objectclass", "*aaa*bbb*ccc*" ), + Net::LDAP::Filter.eq( "objectclass", "aaa*" ), + Net::LDAP::Filter.eq( "objectclass", "aaa*bbb*" ), + Net::LDAP::Filter.eq( "objectclass", "aaa*bbb*ccc*" ), + ].each {|ber| + f = Net::LDAP::Filter.parse_ber( ber.to_ber.read_ber( Net::LDAP::AsnSyntax) ) + assert( f == ber ) + assert_equal( f.to_ber, ber.to_ber ) + } + + end + + def test_ber_from_rfc2254_filter + [ + Net::LDAP::Filter.construct( "objectclass=*" ), + Net::LDAP::Filter.construct("objectclass=ou" ), + Net::LDAP::Filter.construct("uid >= 500" ), + Net::LDAP::Filter.construct("uid <= 500" ), + Net::LDAP::Filter.construct("(!(uid=*))" ), + Net::LDAP::Filter.construct("(&(uid=*)(objectclass=*))" ), + Net::LDAP::Filter.construct("(&(uid=*)(objectclass=*)(sn=*))" ), + Net::LDAP::Filter.construct("(|(uid=*)(objectclass=*))" ), + Net::LDAP::Filter.construct("(|(uid=*)(objectclass=*)(sn=*))" ), + + Net::LDAP::Filter.construct("objectclass=*aaa"), + Net::LDAP::Filter.construct("objectclass=*aaa*bbb"), + Net::LDAP::Filter.construct("objectclass=*aaa*bbb*ccc"), + Net::LDAP::Filter.construct("objectclass=aaa*bbb"), + Net::LDAP::Filter.construct("objectclass=aaa*bbb*ccc"), + Net::LDAP::Filter.construct("objectclass=abc*def*1111*22*g"), + Net::LDAP::Filter.construct("objectclass=*aaa*"), + Net::LDAP::Filter.construct("objectclass=*aaa*bbb*"), + Net::LDAP::Filter.construct("objectclass=*aaa*bbb*ccc*"), + Net::LDAP::Filter.construct("objectclass=aaa*"), + Net::LDAP::Filter.construct("objectclass=aaa*bbb*"), + Net::LDAP::Filter.construct("objectclass=aaa*bbb*ccc*"), + ].each {|ber| + f = Net::LDAP::Filter.parse_ber( ber.to_ber.read_ber( Net::LDAP::AsnSyntax) ) + assert( f == ber ) + assert_equal( f.to_ber, ber.to_ber ) + } + end + +end diff --git a/test/test_ldif.rb b/test/test_ldif.rb new file mode 100644 index 0000000..e44025c --- /dev/null +++ b/test/test_ldif.rb @@ -0,0 +1,59 @@ +# $Id: testldif.rb 61 2006-04-18 20:55:55Z blackhedd $ + +require 'common' + +require 'net/ldif' +require 'sha1' +require 'base64' + +class TestLdif < Test::Unit::TestCase + + TestLdifFilename = "#{File.dirname(__FILE__)}/testdata.ldif" + + def test_empty_ldif + ds = Net::LDAP::Dataset::read_ldif( StringIO.new ) + assert_equal( true, ds.empty? ) + end + + def test_ldif_with_comments + str = ["# Hello from LDIF-land", "# This is an unterminated comment"] + io = StringIO.new( str[0] + "\r\n" + str[1] ) + ds = Net::LDAP::Dataset::read_ldif( io ) + assert_equal( str, ds.comments ) + end + + def test_ldif_with_password + psw = "goldbricks" + hashed_psw = "{SHA}" + Base64::encode64( SHA1.new(psw).digest ).chomp + + ldif_encoded = Base64::encode64( hashed_psw ).chomp + ds = Net::LDAP::Dataset::read_ldif( StringIO.new( "dn: Goldbrick\r\nuserPassword:: #{ldif_encoded}\r\n\r\n" )) + recovered_psw = ds["Goldbrick"][:userpassword].shift + assert_equal( hashed_psw, recovered_psw ) + end + + def test_ldif_with_continuation_lines + ds = Net::LDAP::Dataset::read_ldif( StringIO.new( "dn: abcdefg\r\n hijklmn\r\n\r\n" )) + assert_equal( true, ds.has_key?( "abcdefg hijklmn" )) + end + + # TODO, INADEQUATE. We need some more tests + # to verify the content. + def test_ldif + File.open( TestLdifFilename, "r" ) {|f| + ds = Net::LDAP::Dataset::read_ldif( f ) + assert_equal( 13, ds.length ) + } + end + + # TODO, need some tests. + # Must test folded lines and base64-encoded lines as well as normal ones. + #def test_to_ldif + # File.open( TestLdifFilename, "r" ) {|f| + # ds = Net::LDAP::Dataset::read_ldif( f ) + # ds.to_ldif + # assert_equal( true, false ) # REMOVE WHEN WE HAVE SOME TESTS HERE. + # } + #end + +end diff --git a/test/test_password.rb b/test/test_password.rb new file mode 100644 index 0000000..abc8c22 --- /dev/null +++ b/test/test_password.rb @@ -0,0 +1,17 @@ +# $Id: testpsw.rb 72 2006-04-24 21:58:14Z blackhedd $ + +require 'common' + +class TestPassword < Test::Unit::TestCase + + def test_psw + assert_equal( + "{MD5}xq8jwrcfibi0sZdZYNkSng==", + Net::LDAP::Password.generate( :md5, "cashflow" )) + + assert_equal( + "{SHA}YE4eGkN4BvwNN1f5R7CZz0kFn14=", + Net::LDAP::Password.generate( :sha, "cashflow" )) + end + +end diff --git a/test/test_snmp.rb b/test/test_snmp.rb new file mode 100644 index 0000000..249afb6 --- /dev/null +++ b/test/test_snmp.rb @@ -0,0 +1,130 @@ +# $Id: testsnmp.rb 231 2006-12-21 15:09:29Z blackhedd $ + +require 'common' +require 'net/snmp' + +class TestSnmp < Test::Unit::TestCase + + SnmpGetRequest = "0'\002\001\000\004\006public\240\032\002\002?*\002\001\000\002\001\0000\0160\f\006\b+\006\001\002\001\001\001\000\005\000" + SnmpGetResponse = "0+\002\001\000\004\006public\242\036\002\002'\017\002\001\000\002\001\0000\0220\020\006\b+\006\001\002\001\001\001\000\004\004test" + + SnmpGetRequestXXX = "0'\002\001\000\004\006xxxxxx\240\032\002\002?*\002\001\000\002\001\0000\0160\f\006\b+\006\001\002\001\001\001\000\005\000" + + + def setup + end + + def teardown + end + + def test_invalid_packet + data = "xxxx" + assert_raise( Net::BER::BerError ) { + ary = data.read_ber(Net::SNMP::AsnSyntax) + } + + end + + # The method String#read_ber! added by Net::BER consumes a well-formed BER object + # from the head of a string. If it doesn't find a complete, well-formed BER object, + # it returns nil and leaves the string unchanged. If it finds an object, it returns + # the object and removes it from the head of the string. This is good for handling + # partially-received data streams, such as from network connections. + def test_consume_string + data = "xxx" + assert_equal( nil, data.read_ber! ) + assert_equal( "xxx", data ) + + data = SnmpGetRequest + "!!!" + ary = data.read_ber!( Net::SNMP::AsnSyntax ) + assert_equal( "!!!", data ) + assert ary.is_a?(Array) + assert ary.is_a?(Net::BER::BerIdentifiedArray) + end + + def test_weird_packet + assert_raise( Net::SnmpPdu::Error ) { + Net::SnmpPdu.parse("aaaaaaaaaaaaaa") + } + end + + def test_get_request + data = SnmpGetRequest.dup + pkt = data.read_ber(Net::SNMP::AsnSyntax) + assert pkt.is_a?(Net::BER::BerIdentifiedArray) + assert_equal( 48, pkt.ber_identifier) # Constructed [0], signifies GetRequest + + pdu = Net::SnmpPdu.parse(pkt) + assert_equal(:get_request, pdu.pdu_type ) + assert_equal(16170, pdu.request_id ) # whatever was in the test data. 16170 is not magic. + assert_equal( [[[1,3,6,1,2,1,1,1,0],nil]], pdu.variables ) + + assert_equal( pdu.to_ber_string, SnmpGetRequest ) + end + + def test_empty_pdu + pdu = Net::SnmpPdu.new + assert_raise( Net::SnmpPdu::Error ) { + pdu.to_ber_string + } + end + + def test_malformations + pdu = Net::SnmpPdu.new + pdu.version = 0 + pdu.version = 2 + assert_raise( Net::SnmpPdu::Error ) { + pdu.version = 100 + } + + pdu.pdu_type = :get_request + pdu.pdu_type = :get_next_request + pdu.pdu_type = :get_response + pdu.pdu_type = :set_request + pdu.pdu_type = :trap + assert_raise( Net::SnmpPdu::Error ) { + pdu.pdu_type = :something_else + } + end + + def test_make_response + pdu = Net::SnmpPdu.new + pdu.version = 0 + pdu.community = "public" + pdu.pdu_type = :get_response + pdu.request_id = 9999 + pdu.error_status = 0 + pdu.error_index = 0 + pdu.add_variable_binding [1,3,6,1,2,1,1,1,0], "test" + + assert_equal( SnmpGetResponse, pdu.to_ber_string ) + end + + def test_make_bad_response + pdu = Net::SnmpPdu.new + assert_raise(Net::SnmpPdu::Error) {pdu.to_ber_string} + pdu.pdu_type = :get_response + pdu.request_id = 999 + pdu.to_ber_string + # Not specifying variables doesn't create an error. (Maybe it should?) + end + + def test_snmp_integers + c32 = Net::SNMP::Counter32.new(100) + assert_equal( "A\001d", c32.to_ber ) + g32 = Net::SNMP::Gauge32.new(100) + assert_equal( "B\001d", g32.to_ber ) + t32 = Net::SNMP::TimeTicks32.new(100) + assert_equal( "C\001d", t32.to_ber ) + end + + def test_community + data = SnmpGetRequestXXX.dup + ary = data.read_ber(Net::SNMP::AsnSyntax) + pdu = Net::SnmpPdu.parse( ary ) + assert_equal( "xxxxxx", pdu.community ) + end + +end + + diff --git a/test/testdata.ldif b/test/testdata.ldif new file mode 100644 index 0000000..eb5610d --- /dev/null +++ b/test/testdata.ldif @@ -0,0 +1,101 @@ +# $Id: testdata.ldif 50 2006-04-17 17:57:33Z blackhedd $ +# +# This is test-data for an LDAP server in LDIF format. +# +dn: dc=bayshorenetworks,dc=com +objectClass: dcObject +objectClass: organization +o: Bayshore Networks LLC +dc: bayshorenetworks + +dn: cn=Manager,dc=bayshorenetworks,dc=com +objectClass: organizationalrole +cn: Manager + +dn: ou=people,dc=bayshorenetworks,dc=com +objectClass: organizationalunit +ou: people + +dn: ou=privileges,dc=bayshorenetworks,dc=com +objectClass: organizationalunit +ou: privileges + +dn: ou=roles,dc=bayshorenetworks,dc=com +objectClass: organizationalunit +ou: roles + +dn: ou=office,dc=bayshorenetworks,dc=com +objectClass: organizationalunit +ou: office + +dn: mail=nogoodnik@steamheat.net,ou=people,dc=bayshorenetworks,dc=com +cn: Bob Fosse +mail: nogoodnik@steamheat.net +sn: Fosse +ou: people +objectClass: top +objectClass: inetorgperson +objectClass: authorizedperson +hasAccessRole: uniqueIdentifier=engineer,ou=roles +hasAccessRole: uniqueIdentifier=ldapadmin,ou=roles +hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles +hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles +hasAccessRole: uniqueIdentifier=ogilvy_eagle_user,ou=roles +hasAccessRole: uniqueIdentifier=greenplug_user,ou=roles +hasAccessRole: uniqueIdentifier=brandplace_logging_user,ou=roles +hasAccessRole: uniqueIdentifier=brandplace_report_user,ou=roles +hasAccessRole: uniqueIdentifier=workorder_user,ou=roles +hasAccessRole: uniqueIdentifier=bayshore_eagle_user,ou=roles +hasAccessRole: uniqueIdentifier=bayshore_eagle_superuser,ou=roles +hasAccessRole: uniqueIdentifier=kledaras_user,ou=roles + +dn: mail=elephant@steamheat.net,ou=people,dc=bayshorenetworks,dc=com +cn: Gwen Verdon +mail: elephant@steamheat.net +sn: Verdon +ou: people +objectClass: top +objectClass: inetorgperson +objectClass: authorizedperson +hasAccessRole: uniqueIdentifier=brandplace_report_user,ou=roles +hasAccessRole: uniqueIdentifier=engineer,ou=roles +hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles +hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles +hasAccessRole: uniqueIdentifier=ldapadmin,ou=roles + +dn: uniqueIdentifier=engineering,ou=privileges,dc=bayshorenetworks,dc=com +uniqueIdentifier: engineering +ou: privileges +objectClass: accessPrivilege + +dn: uniqueIdentifier=engineer,ou=roles,dc=bayshorenetworks,dc=com +uniqueIdentifier: engineer +ou: roles +objectClass: accessRole +hasAccessPrivilege: uniqueIdentifier=engineering,ou=privileges + +dn: uniqueIdentifier=ldapadmin,ou=roles,dc=bayshorenetworks,dc=com +uniqueIdentifier: ldapadmin +ou: roles +objectClass: accessRole + +dn: uniqueIdentifier=ldapsuperadmin,ou=roles,dc=bayshorenetworks,dc=com +uniqueIdentifier: ldapsuperadmin +ou: roles +objectClass: accessRole + +dn: mail=catperson@steamheat.net,ou=people,dc=bayshorenetworks,dc=com +cn: Sid Sorokin +mail: catperson@steamheat.net +sn: Sorokin +ou: people +objectClass: top +objectClass: inetorgperson +objectClass: authorizedperson +hasAccessRole: uniqueIdentifier=engineer,ou=roles +hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles +hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles +hasAccessRole: uniqueIdentifier=ogilvy_eagle_user,ou=roles +hasAccessRole: uniqueIdentifier=greenplug_user,ou=roles +hasAccessRole: uniqueIdentifier=workorder_user,ou=roles + diff --git a/tests/NOTICE.txt b/tests/NOTICE.txt new file mode 100644 index 0000000..709c0d3 --- /dev/null +++ b/tests/NOTICE.txt @@ -0,0 +1,6 @@ +Most of the tests here have been migrated to ../test +where all new tests will be created. These have been +left here for now just for reference and will me +migrated as well. + +These will not be run automatically by rake.