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/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/LICENSE b/LICENSE new file mode 100644 index 0000000..953ea0b --- /dev/null +++ b/LICENSE @@ -0,0 +1,55 @@ +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 + 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/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.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 new file mode 100644 index 0000000..75c8182 --- /dev/null +++ b/Rakefile @@ -0,0 +1,18 @@ +# -*- ruby -*- + +require 'rubygems' +require 'hoe' + +# Add 'lib' to load path. +$LOAD_PATH.unshift( "#{File.dirname(__FILE__)}/lib" ) + +# Pull in local 'net/ldap' as opposed to an installed version. +require 'net/ldap' + +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 + +# vim: syntax=Ruby diff --git a/Release-Announcement b/Release-Announcement new file mode 100644 index 0000000..5d14811 --- /dev/null +++ b/Release-Announcement @@ -0,0 +1,95 @@ +We're pleased to announce version 0.0.4 of Net::LDAP, the pure-Ruby LDAP library. + +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. 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. +This library does not wrap any existing native-code LDAP libraries, creates no +Ruby extensions, and has no dependencies external to Ruby. + +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. + +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-2251, which specifies the Lightweight Directory +Access Protocol, as amended and extended by subsequent RFCs and by the more +widely-used directory implementations. + +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 +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. + +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.2.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.2: May 3, 2006 +* Fixed malformation in distro tarball and gem. +* Improved documentation. +* Supported "paged search control." + diff --git a/lib/net/ber.rb b/lib/net/ber.rb new file mode 100644 index 0000000..dbe2fbd --- /dev/null +++ b/lib/net/ber.rb @@ -0,0 +1,556 @@ +# $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 < StandardError; end + + + class BerIdentifiedString < String + attr_accessor :ber_identifier + def initialize args + super args + end + end + + class BerIdentifiedArray < Array + attr_accessor :ber_identifier + def initialize + super + end + end + + class BerIdentifiedNull + attr_accessor :ber_identifier + def to_ber + "\005\000" + end + end + + class BerIdentifiedOid + attr_accessor :ber_identifier + def initialize oid + if oid.is_a?(String) + oid = oid.split(/\./).map {|s| s.to_i } + end + @value = oid + end + def to_ber + # Provisional implementation. + # We ASSUME that our incoming value is an array, and we + # use the Array#to_ber_oid method defined below. + # We probably should obsolete that method, actually, in + # and move the code here. + # WE ARE NOT CURRENTLY ENCODING THE BER-IDENTIFIER. + # This implementation currently hardcodes 6, the universal OID tag. + ary = @value.dup + first = ary.shift + raise Net::BER::BerError.new(" invalid OID" ) unless [0,1,2].include?(first) + first = first * 40 + ary.shift + ary.unshift first + oid = ary.pack("w*") + [6, oid.length].pack("CC") + oid + end + end + + #-- + # This condenses our nicely self-documenting ASN hashes down + # to an array for fast lookups. + # Scoped to be called as a module method, but not intended for + # user code to call. + # + def self.compile_syntax syn + out = [nil] * 256 + syn.each {|tclass,tclasses| + tagclass = {:universal=>0, :application=>64, :context_specific=>128, :private=>192} [tclass] + tclasses.each {|codingtype,codings| + encoding = {:primitive=>0, :constructed=>32} [codingtype] + codings.each {|tag,objtype| + out[tagclass + encoding + tag] = objtype + } + } + } + out + end + + # This module is for mixing into IO and IO-like objects. + module BERParser + + # The order of these follows the class-codes in BER. + # Maybe this should have been a hash. + TagClasses = [:universal, :application, :context_specific, :private] + + BuiltinSyntax = BER.compile_syntax( { + :universal => { + :primitive => { + 1 => :boolean, + 2 => :integer, + 4 => :string, + 5 => :null, + 6 => :oid, + 10 => :integer, + 13 => :string # (relative OID) + }, + :constructed => { + 16 => :array, + 17 => :array + } + }, + :context_specific => { + :primitive => { + 10 => :integer + } + } + }) + + # + # read_ber + # TODO: clean this up so it works properly with partial + # packets coming from streams that don't block when + # we ask for more data (like StringIOs). At it is, + # this can throw TypeErrors and other nasties. + #-- + # 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. + #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 + + n = getc + lengthlength,contentlength = if n <= 127 + [1,n] + else + # Replaced the inject because it profiles hot. + #j = (0...(n & 127)).inject(0) {|mem,x| mem = (mem << 8) + getc} + j = 0 + read( n & 127 ).each_byte {|n1| j = (j << 8) + n1} + [1 + (n & 127), j] + end + + newobj = read contentlength + + # This exceptionally clever and clear bit of code is verrrry slow. + objtype = (syntax && syntax[id]) || BuiltinSyntax[id] + + + # == is expensive so sort this if/else so the common cases are at the top. + obj = if objtype == :string + #(newobj || "").dup + s = BerIdentifiedString.new( newobj || "" ) + s.ber_identifier = id + s + elsif objtype == :integer + j = 0 + newobj.each_byte {|b| j = (j << 8) + b} + j + elsif objtype == :oid + # cf X.690 pgh 8.19 for an explanation of this algorithm. + # Potentially not good enough. We may need a BerIdentifiedOid + # as a subclass of BerIdentifiedArray, to get the ber identifier + # and also a to_s method that produces the familiar dotted notation. + oid = newobj.unpack("w*") + f = oid.shift + g = if f < 40 + [0, f] + elsif f < 80 + [1, f-40] + else + [2, f-80] # f-80 can easily be > 80. What a weird optimization. + end + oid.unshift g.last + oid.unshift g.first + oid + elsif objtype == :array + #seq = [] + seq = BerIdentifiedArray.new + seq.ber_identifier = id + sio = StringIO.new( newobj || "" ) + # Interpret the subobject, but note how the loop + # is built: nil ends the loop, but false (a valid + # BER value) does not! + while (e = sio.read_ber(syntax)) != nil + seq << e + end + seq + elsif objtype == :boolean + newobj != "\000" + elsif objtype == :null + n = BerIdentifiedNull.new + n.ber_identifier = id + n + else + #raise BerError.new( "unsupported object type: class=#{tagclass}, encoding=#{encoding}, tag=#{tag}" ) + raise BerError.new( "unsupported object type: id=#{id}" ) + end + + # Add the identifier bits into the object if it's a String or an Array. + # We can't add extra stuff to Fixnums and booleans, not that it makes much sense anyway. + # Replaced this mechanism with subclasses because the instance_eval profiled too hot. + #obj and ([String,Array].include? obj.class) and obj.instance_eval "def ber_identifier; #{id}; end" + #obj.ber_identifier = id if obj.respond_to?(:ber_identifier) + obj + + end + + #-- + # Violates DRY! This replicates the functionality of #read_ber. + # Eventually this method may replace that one. + # This version of #read_ber behaves properly in the face of incomplete + # data packets. If a full BER object is detected, we return an array containing + # the detected object and the number of bytes consumed from the string. + # If we don't detect a complete packet, return nil. + # + # Observe that weirdly we recursively call the original #read_ber in here. + # That needs to be fixed if we ever obsolete the original method in favor of this one. + def read_ber_from_string str, syntax=nil + id = str[0] or return nil + n = str[1] or return nil + n_consumed = 2 + lengthlength,contentlength = if n <= 127 + [1,n] + else + n1 = n & 127 + return nil unless str.length >= (n_consumed + n1) + j = 0 + n1.times { + j = (j << 8) + str[n_consumed] + n_consumed += 1 + } + [1 + (n1), j] + end + + return nil unless str.length >= (n_consumed + contentlength) + newobj = str[n_consumed...(n_consumed + contentlength)] + n_consumed += contentlength + + objtype = (syntax && syntax[id]) || BuiltinSyntax[id] + + # == is expensive so sort this if/else so the common cases are at the top. + obj = if objtype == :array + seq = BerIdentifiedArray.new + seq.ber_identifier = id + sio = StringIO.new( newobj || "" ) + # Interpret the subobject, but note how the loop + # is built: nil ends the loop, but false (a valid + # BER value) does not! + # Also, we can use the standard read_ber method because + # we know for sure we have enough data. (Although this + # might be faster than the standard method.) + while (e = sio.read_ber(syntax)) != nil + seq << e + end + seq + elsif objtype == :string + s = BerIdentifiedString.new( newobj || "" ) + s.ber_identifier = id + s + elsif objtype == :integer + j = 0 + newobj.each_byte {|b| j = (j << 8) + b} + j + elsif objtype == :oid + # cf X.690 pgh 8.19 for an explanation of this algorithm. + # Potentially not good enough. We may need a BerIdentifiedOid + # as a subclass of BerIdentifiedArray, to get the ber identifier + # and also a to_s method that produces the familiar dotted notation. + oid = newobj.unpack("w*") + f = oid.shift + g = if f < 40 + [0,f] + elsif f < 80 + [1, f-40] + else + [2, f-80] # f-80 can easily be > 80. What a weird optimization. + end + oid.unshift g.last + oid.unshift g.first + oid + elsif objtype == :boolean + newobj != "\000" + elsif objtype == :null + n = BerIdentifiedNull.new + n.ber_identifier = id + n + else + raise BerError.new( "unsupported object type: id=#{id}" ) + end + + [obj, n_consumed] + end + + end # module BERParser + end # module BER + +end # module Net + + +class IO + include Net::BER::BERParser +end + +require "stringio" +class StringIO + include Net::BER::BERParser +end + +begin + require 'openssl' + class OpenSSL::SSL::SSLSocket + include Net::BER::BERParser + end +rescue LoadError +# Ignore LoadError. +# DON'T ignore NameError, which means the SSLSocket class +# is somehow unavailable on this implementation of Ruby's openssl. +# This may be WRONG, however, because we don't yet know how Ruby's +# openssl behaves on machines with no OpenSSL library. I suppose +# it's possible they do not fail to require 'openssl' but do not +# create the classes. So this code is provisional. +# Also, you might think that OpenSSL::SSL::SSLSocket inherits from +# IO so we'd pick it up above. But you'd be wrong. +end + + + +class String + include Net::BER::BERParser + def read_ber syntax=nil + StringIO.new(self).read_ber(syntax) + end + def read_ber! syntax=nil + obj,n_consumed = read_ber_from_string(self, syntax) + if n_consumed + self.slice!(0...n_consumed) + obj + else + nil + end + end +end + +#---------------------------------------------- + + +class FalseClass + # + # to_ber + # + def to_ber + "\001\001\000" + end +end + + +class TrueClass + # + # to_ber + # + def to_ber + "\001\001\001" + end +end + + + +class Fixnum + # + # to_ber + # + def to_ber + "\002" + to_ber_internal + end + + # + # to_ber_enumerated + # + def to_ber_enumerated + "\012" + to_ber_internal + end + + # + # to_ber_length_encoding + # + def to_ber_length_encoding + if self <= 127 + [self].pack('C') + else + i = [self].pack('N').sub(/^[\0]+/,"") + [0x80 + i.length].pack('C') + i + 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 + + +class Bignum + + def to_ber + #i = [self].pack('w') + #i.length > 126 and raise Net::BER::BerError.new( "range error in bignum" ) + #[2, i.length].pack("CC") + i + + # Ruby represents Bignums as two's-complement numbers so we may actually be + # good as far as representing negatives goes. + # I'm sure this implementation can be improved performance-wise if necessary. + # Ruby's Bignum#size returns the number of bytes in the internal representation + # of the number, but it can and will include leading zero bytes on at least + # some implementations. Evidently Ruby stores these as sets of quadbytes. + # It's not illegal in BER to encode all of the leading zeroes but let's strip + # them out anyway. + # + sz = self.size + out = "\000" * sz + (sz*8).times {|bit| + if self[bit] == 1 + out[bit/8] += (1 << (bit % 8)) + end + } + + while out.length > 1 and out[-1] == 0 + out.slice!(-1,1) + end + + [2, out.length].pack("CC") + out.reverse + end + +end + + + +class String + # + # to_ber + # A universal octet-string is tag number 4, + # but others are possible depending on the context, so we + # let the caller give us one. + # The preferred way to do this in user code is via to_ber_application_sring + # and to_ber_contextspecific. + # + def to_ber code = 4 + [code].pack('C') + length.to_ber_length_encoding + self + end + + # + # to_ber_application_string + # + def to_ber_application_string code + to_ber( 0x40 + code ) + end + + # + # to_ber_contextspecific + # + def to_ber_contextspecific code + to_ber( 0x80 + code ) + end + +end # class String + + + +class Array + # + # to_ber_appsequence + # An application-specific sequence usually gets assigned + # a tag that is meaningful to the particular protocol being used. + # This is different from the universal sequence, which usually + # gets a tag value of 16. + # Now here's an interesting thing: We're adding the X.690 + # "application constructed" code at the top of the tag byte (0x60), + # but some clients, notably ldapsearch, send "context-specific + # constructed" (0xA0). The latter would appear to violate RFC-1777, + # but what do I know? We may need to change this. + # + + def to_ber id = 0; to_ber_seq_internal( 0x30 + id ); end + def to_ber_set id = 0; to_ber_seq_internal( 0x31 + id ); end + def to_ber_sequence id = 0; to_ber_seq_internal( 0x30 + id ); end + def to_ber_appsequence id = 0; to_ber_seq_internal( 0x60 + id ); end + def to_ber_contextspecific id = 0; to_ber_seq_internal( 0xA0 + id ); end + + def to_ber_oid + ary = self.dup + first = ary.shift + raise Net::BER::BerError.new( "invalid OID" ) unless [0,1,2].include?(first) + first = first * 40 + ary.shift + ary.unshift first + oid = ary.pack("w*") + [6, oid.length].pack("CC") + oid + end + + private + def to_ber_seq_internal code + s = self.to_s + [code].pack('C') + s.length.to_ber_length_encoding + s + end + + +end # class Array + + diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb new file mode 100644 index 0000000..e2f2d0f --- /dev/null +++ b/lib/net/ldap.rb @@ -0,0 +1,1592 @@ +# $Id$ +# +# Net::LDAP for Ruby +# +# +# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved. +# +# Written and maintained by Francis Cianfrocca, gmail: garbagecat10. +# +# 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. +# +# +# See Net::LDAP for documentation and usage samples. +# + + +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' +require 'net/ldap/dataset' +require 'net/ldap/psw' +require 'net/ldap/entry' + + +module Net + + + # == Net::LDAP + # + # This library provides a pure-Ruby implementation of the + # LDAP client protocol, per RFC-2251. + # 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-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' + # + # 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 + # + # + # == A Brief 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 relate to 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 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 sn 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, #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 + # 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 + # 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 + # set of attribute values for each entity, depending on what attributes the search requested. + # + # ==== Add + # #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 + # #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. + # Net::LDAP provides three easier methods to modify an entry's attribute values: + # #add_attribute, #replace_attribute, and #delete_attribute. + # + # ==== Delete + # #delete 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 + # + # 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' + # + # 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, 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 + # 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 < StandardError; end + + VERSION = "0.0.5" + + + SearchScope_BaseObject = 0 + SearchScope_SingleLevel = 1 + SearchScope_WholeSubtree = 2 + SearchScopes = [SearchScope_BaseObject, SearchScope_SingleLevel, SearchScope_WholeSubtree] + + AsnSyntax = BER.compile_syntax({ + :application => { + :primitive => { + 2 => :null # UnbindRequest body + }, + :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 + 19 => :array, # SearchResultReferral + 24 => :array, # Unsolicited Notification + } + }, + :context_specific => { + :primitive => { + 0 => :string, # password + 1 => :string, # Kerberos v4 + 2 => :string, # Kerberos v5 + 7 => :string, # serverSaslCreds + }, + :constructed => { + 0 => :array, # RFC-2251 Control and Filter-AND + 1 => :array, # SearchFilter-OR + 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 + } + } + }) + + DefaultHost = "127.0.0.1" + DefaultPort = 389 + DefaultAuth = {:method => :anonymous} + DefaultTreebase = "dc=com" + + StartTlsOid = "1.3.6.1.4.1.1466.20037" + + ResultStrings = { + 0 => "Success", + 1 => "Operations Error", + 2 => "Protocol Error", + 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", + 32 => "No Such Object", + 34 => "Invalid DN Syntax", + 48 => "Inappropriate Authentication", + 49 => "Invalid Credentials", + 50 => "Insufficient Access Rights", + 51 => "Busy", + 52 => "Unavailable", + 53 => "Unwilling to perform", + 65 => "Object Class Violation", + 68 => "Entry Already Exists" + } + + + module LdapControls + PagedResults = "1.2.840.113556.1.4.319" # Microsoft evil from RFC 2696 + end + + + # + # LDAP::result2string + # + def LDAP::result2string code # :nodoc: + 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, 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) + # * :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. + # * :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 + # object. + # + 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 + @base = args[:base] || DefaultTreebase + encryption args[:encryption] # may be nil + + 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. + @open_connection = nil + end + + # 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, + # 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} + end + + 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 + case args + when :simple_tls, :start_tls + args = {:method => args} + 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 + # 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. + # + # # (PSEUDOCODE) + # 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 + ldap1 = LDAP.new args + ldap1.open {|ldap| yield ldap } + end + + # 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), + # and a human-readable string. + # unless ldap.bind + # puts "Result: #{ldap.get_operation_result.code}" + # 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. + # + def get_operation_result + os = OpenStruct.new + 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 + end + os.message = LDAP.result2string( os.code ) + os + end + + + # 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. + # + # # (PSEUDOCODE) + # 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. + # 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 + raise LdapError.new( "open already in progress" ) if @open_connection + @open_connection = Connection.new( :host => @host, :port => @port, :encryption => @encryption ) + @open_connection.bind @auth + yield self + @open_connection.close + @open_connection = nil + 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). + # * :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. + # 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. + # + #-- + # 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 + # 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. + #++ + # #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 + # 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). + # 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 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 + # 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. + # + # 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 = {} + 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 : [] + + if @open_connection + @result = @open_connection.search( args ) {|entry| + result_set << entry if result_set + yield( entry ) if block_given? + } + else + @result = 0 + 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 + yield( entry ) if block_given? + } + end + conn.close + end + + @result == 0 and result_set + end + + # #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 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 + # + # 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 + # #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. + # The latter operation is obviously useful only as an auth check. + # + def bind auth=@auth + if @open_connection + @result = @open_connection.bind auth + else + conn = Connection.new( :host => @host, :port => @port , :encryption => @encryption) + @result = conn.bind auth + conn.close + end + + @result == 0 + end + + + # + # #bind_as is for testing authentication credentials. + # + # 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| + rs = search args + if rs and rs.first and dn = rs.first.dn + 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 + # :attributes :: Attributes of the new entry. + # + # 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 + #-- + # 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 + @result = @open_connection.add( args ) + else + @result = 0 + conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption) + if (@result = conn.bind( args[:auth] || @auth )) == 0 + @result = conn.add( args ) + end + conn.close + end + @result == 0 + end + + + # 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) + # + # This method returns True or False to indicate whether the operation + # 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, + # 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 + # 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, along + # with all of its values. + # + # 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 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 + # 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. + # + # 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 + @result = @open_connection.modify( args ) + else + @result = 0 + conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption ) + if (@result = conn.bind( args[:auth] || @auth )) == 0 + @result = conn.modify( args ) + end + conn.close + end + @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_ + # + def rename args + if @open_connection + @result = @open_connection.rename( args ) + else + @result = 0 + conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption ) + if (@result = conn.bind( args[:auth] || @auth )) == 0 + @result = conn.rename( args ) + end + conn.close + end + @result == 0 + end + + # modify_rdn is an alias for #rename. + def modify_rdn args + 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, :encryption => @encryption ) + if (@result = conn.bind( args[:auth] || @auth )) == 0 + @result = conn.delete( args ) + end + conn.close + end + @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. + # 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] + ) + (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 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. If there is an error, call #get_operation_result + # for more information. + # + # 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, 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. + # + 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.first.subschemasubentry + return Entry.new unless (subschema_name and subschema_name.first) + + rs = search( + :ignore_server_caps=>true, + :base=>subschema_name.first, + :scope=>SearchScope_BaseObject, + :filter=>"objectclass=subschema", + :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. + # 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 + + + + class LDAP + # This is a private class used internally by the library. It should not be called by user code. + class Connection # :nodoc: + + LdapVersion = 3 + MaxSaslChallenges = 10 + + + #-- + # initialize + # + def initialize server + begin + @conn = TCPsocket.new( server[:host], server[:port] ) + rescue + 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. + # + # 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 + 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. + 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 + 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 + # + def next_msgid + @msgid ||= 0 + @msgid += 1 + end + + + #-- + # bind + # + def bind auth + meth = auth[:method] + if [:simple, :anonymous, :anon].include?( meth ) + bind_simple auth + elsif meth == :sasl + 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 + + 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 + + #-- + # 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 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) + + nego = proc {|challenge| + t2_msg = NTLM::Message.parse( challenge ) + t3_msg = t2_msg.response( {:user => user, :password => psw}, {:ntlmv2 => true} ) + t3_msg.serialize + } + + bind_sasl( { + :method => :sasl, + :mechanism => "GSS-SPNEGO", + :initial_credential => NTLM::Message::Type1.new.serialize, + :challenge_response => nego + }) + end + private :bind_gss_spnego + + #-- + # 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. + # + # 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??? + # OpenLDAP newer than version 2.2.0 supports paged searches. + # + 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 + 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 + raise LdapError.new( "invalid search scope" ) unless SearchScopes.include?(scope) + + # 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. + # + # 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!) + # 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 + n_results = 0 + + loop { + # should collect this into a private helper to clarify the structure + + 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 = [ + search_base.to_ber, + scope.to_ber_enumerated, + 0.to_ber_enumerated, + query_limit.to_ber, # size limit + 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 + n_results += 1 + 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 + break + else + raise LdapError.new( "invalid response-type in search: #{pdu.app_tag}" ) + 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. + # + # 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| + 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 + + + + #-- + # 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 + end + + + #-- + # 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") + 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 + 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 + + + #-- + # 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 + + +end # module Net + + diff --git a/lib/net/ldap/dataset.rb b/lib/net/ldap/dataset.rb new file mode 100644 index 0000000..3fc60d0 --- /dev/null +++ b/lib/net/ldap/dataset.rb @@ -0,0 +1,108 @@ +# $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 < 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 + # Avoid the Base64 class because not all Ruby versions have it. + attrvalue = ($2 == ":") ? $'.unpack('m').shift : $' + ds[dn][$1.downcase.intern] << attrvalue + end + + line = nextline + end + end + + ds + end + + + def initialize + @comments = [] + 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 +end # Net + + diff --git a/lib/net/ldap/entry.rb b/lib/net/ldap/entry.rb new file mode 100644 index 0000000..1291d8e --- /dev/null +++ b/lib/net/ldap/entry.rb @@ -0,0 +1,269 @@ +# $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 +# +#--------------------------------------------------------------------------- +# + + +require 'base64' + + +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 Symbols, + # 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. + # 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 + # 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. + #-- + # 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 = {} # 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 + + + #-- + # 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. + # + 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].to_s + 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| + attr_name,values = a,self[a] + yield attr_name, values + } + end + end + + 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" + v2 = "" # temp value, save on GC + each_attribute do |k,v| + unless k == :dn + v.each {|v1| + v2 = if (k == :userpassword) || is_attribute_value_binary?(v1) + ": #{Base64.encode64(v1).chomp.gsub(/\n/m,"\n ")}" + else + " #{v1}" + end + ary << "#{k}:#{v2}\n" + } + end + end + ary << "\n" + ary.join + end + + #-- + # 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. + # 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. + # + # 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]*/ + entry[$1] <<= if $2 == ':' + Base64.decode64($') + else + $' + end + end + } + entry.dn ? entry : nil + end + end + + #-- + # 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 + 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 + + + #-- + # 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. 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.each_byte {|byt| + return true if (byt < 32) || (byt > 126) + } + if v[0..0] == ':' or v[0..0] == '<' + return true + end + false + end + private :is_attribute_value_binary? + + end # class Entry + + +end # class LDAP +end # module Net + + diff --git a/lib/net/ldap/filter.rb b/lib/net/ldap/filter.rb new file mode 100644 index 0000000..a3f5899 --- /dev/null +++ b/lib/net/ldap/filter.rb @@ -0,0 +1,499 @@ +# $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 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 + @op = op + @left = a + @right = 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*" ) + #-- + # 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::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. + # 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 + 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_s.to_ber_contextspecific 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_s.to_ber, seq.to_ber].to_ber_contextspecific 4 + else #equality + [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific 3 + end + when :ge + [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific 5 + when :le + [@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 ) + 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 + + 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 + # 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 + + + # 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 + # 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 + + + + #-- + # 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 + + # Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254) + # to a Net::LDAP::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 + + + +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 + + # 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\-_]+/ ) + scanner.scan(/\s*/) + if op = scanner.scan( /\=|\<\=|\<|\>\=|\>|\!\=/ ) + 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})+/ ) + 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/lib/net/ldap/pdu.rb b/lib/net/ldap/pdu.rb new file mode 100644 index 0000000..c47735e --- /dev/null +++ b/lib/net/ldap/pdu.rb @@ -0,0 +1,277 @@ +# $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 < StandardError; end + + +class LdapPdu + + BindRequest = 0 + BindResult = 1 + UnbindRequest = 2 + SearchRequest = 3 + SearchReturnedData = 4 + SearchResult = 5 + ModifyResponse = 7 + AddResponse = 9 + DeleteResponse = 11 + ModifyRDNResponse = 13 + SearchResultReferral = 19 + ExtendedRequest = 23 + ExtendedResponse = 24 + + 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 + # An LDAP PDU always looks like a BerSequence with + # 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. + # 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. + # + # 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 + # 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" ) + end + + case @app_tag + when BindResult + parse_bind_response 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] + when ModifyResponse + 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] + 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] + when ExtendedResponse + parse_ldap_result ber_object[1] + else + raise LdapPduError.new( "unknown pdu-type: #{@app_tag}" ) + 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, + # 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 + + # Return RFC-2251 Controls if any. + # Messy. Does this functionality belong somewhere else? + def result_controls + @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 + # + def parse_ldap_result sequence + sequence.length >= 3 or raise LdapPduError + @ldap_result = {:resultCode => sequence[0], :matchedDN => sequence[1], :errorMessage => sequence[2]} + 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) + # + # 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. + # 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. + # + # 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] ) + sequence[1].each {|seq| + @search_entry[seq[0]] = seq[1] + } + end + + # + # 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 + # 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 + + + # (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 + + +end # module Net + diff --git a/lib/net/ldap/psw.rb b/lib/net/ldap/psw.rb new file mode 100644 index 0000000..b4f0442 --- /dev/null +++ b/lib/net/ldap/psw.rb @@ -0,0 +1,64 @@ +# $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. + # 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 + # 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/lib/net/ldif.rb b/lib/net/ldif.rb new file mode 100644 index 0000000..be95bcd --- /dev/null +++ b/lib/net/ldif.rb @@ -0,0 +1,39 @@ +# $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 + + diff --git a/lib/net/snmp.rb b/lib/net/snmp.rb new file mode 100644 index 0000000..f74f265 --- /dev/null +++ b/lib/net/snmp.rb @@ -0,0 +1,297 @@ +# $Id$ +# +# NET::SNMP +# +#---------------------------------------------------------------------------- +# +# Copyright (C) 2006-07 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 => { + 1 => :integer, # Counter32, (RFC2578 sec 2) + 2 => :integer, # Gauge32 or Unsigned32, (RFC2578 sec 2) + 3 => :integer # TimeTicks32, (RFC2578 sec 2) + }, + :constructed => { + } + }, + :context_specific => { + :primitive => { + }, + :constructed => { + 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) + } + } + }) + + # 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. + # This is also indistinguishable from Unsigned32. (Need to alias them.) + 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 TimeTicks32 + def initialize value + @value = value + end + def to_ber + @value.to_ber_application(3) + end + end + end + + class SnmpPdu + class Error < StandardError; end + + 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 + @variables = args[:variables] || [] + end + + #-- + def parse ber_object + begin + parse_ber_object ber_object + 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 + 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) + 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 + 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_response 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 + send :request_id=, data[0].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. + # We're ignoring the null, we might want to verify it instead. + 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 + + #-- + # 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) + 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 + @error_status = es + end + + def community= c + @community = c.to_s + end + + #-- + # Syntactic sugar + def add_variable_binding name, value=nil + @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_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, + 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 + diff --git a/pre-setup.rb b/pre-setup.rb new file mode 100644 index 0000000..8f88d75 --- /dev/null +++ b/pre-setup.rb @@ -0,0 +1,45 @@ +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.document(["--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.txt --line-numbers) +ri = %w(--ri-site --merge) +dox = %w(README.txt History.txt lib) +build_rdoc rdoc + dox +build_ri ri + dox +#run_tests Dir["test/**/*"] 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 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. diff --git a/tests/testber.rb b/tests/testber.rb new file mode 100644 index 0000000..970f71b --- /dev/null +++ b/tests/testber.rb @@ -0,0 +1,70 @@ +# $Id$ +# +# + + +$:.unshift "lib" + +require 'net/ldap' +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\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 + 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/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/testem.rb b/tests/testem.rb new file mode 100644 index 0000000..4b0f73e --- /dev/null +++ b/tests/testem.rb @@ -0,0 +1,12 @@ +# $Id$ +# +# + +require 'test/unit' +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..34876c7 --- /dev/null +++ b/tests/testfilter.rb @@ -0,0 +1,98 @@ +# $Id$ +# +# + +require 'test/unit' + +$:.unshift "lib" + +require 'net/ldap' + + +class TestFilter < Test::Unit::TestCase + + def setup + end + + + def teardown + end + + # 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/tests/testldap.rb b/tests/testldap.rb new file mode 100644 index 0000000..a401476 --- /dev/null +++ b/tests/testldap.rb @@ -0,0 +1,190 @@ +# $Id$ +# +# + + +$:.unshift "lib" + +require 'test/unit' + +require 'net/ldap' +require 'stringio' + + +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" + @port = 3890 + @auth = { + :method => :simple, + :username => "cn=bigshot,dc=bayshorenetworks,dc=com", + :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. + # 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( 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( 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( false, ldap.bind ) + assert_equal( 49, ldap.get_operation_result.code ) + assert_equal( "Invalid Credentials", ldap.get_operation_result.message ) + end + + + + def test_search + ldap = Net::LDAP.new :host => @host, :port => @port, :auth => @auth + + search = {:base => "dc=smalldomain,dc=com"} + assert_equal( false, ldap.search( search )) + assert_equal( 32, ldap.get_operation_result.code ) + + search = {:base => "dc=bayshorenetworks,dc=com"} + assert_equal( true, ldap.search( search )) + assert_equal( 0, ldap.get_operation_result.code ) + + ldap.search( search ) {|res| + assert_equal( res, @ldif ) + } + end + + + + + # 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( ldap.bind ) + + search = { + :base => "dc=bayshorenetworks,dc=com", + :attributes => attrs_to_search + } + + ldif = @ldif + ldif.each {|dn,entry| + entry.delete_if {|attr,value| + ! attrs_to_search.include?(attr) + } + } + + assert_equal( true, ldap.search( search )) + ldap.search( search ) {|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", "Fosse" ) + } + + ldap.search( search ) {|res| + p res + } + end + + + + def test_open + 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( true, 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( true, rc ) + } + } + end + + + + + +end + + diff --git a/tests/testldif.rb b/tests/testldif.rb new file mode 100644 index 0000000..7bd76fb --- /dev/null +++ b/tests/testldif.rb @@ -0,0 +1,69 @@ +# $Id$ +# +# + + +$:.unshift "lib" + +require 'test/unit' + +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 + + # 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/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 + + diff --git a/tests/testsnmp.rb b/tests/testsnmp.rb new file mode 100644 index 0000000..e5a5983 --- /dev/null +++ b/tests/testsnmp.rb @@ -0,0 +1,136 @@ +# $Id$ +# +# + + +$:.unshift "lib" + +require 'net/snmp' +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" + + 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/testserver/ldapserver.rb b/testserver/ldapserver.rb new file mode 100644 index 0000000..60b8572 --- /dev/null +++ b/testserver/ldapserver.rb @@ -0,0 +1,229 @@ +# $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) + 7 => :string # present filter + }, + :constructed => { + 3 => :array # equality filter + }, + } + } + + 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 1, pdu[0].to_i, 2, "", "We only support version 3" + elsif pdu[1][1] != "cn=bigshot,dc=bayshorenetworks,dc=com" + send_ldap_response 1, pdu[0].to_i, 48, "", "Who are you?" + elsif pdu[1][2].ber_identifier != 0x80 + send_ldap_response 1, pdu[0].to_i, 7, "", "Keep it simple, man" + elsif pdu[1][2] != "opensesame" + send_ldap_response 1, pdu[0].to_i, 49, "", "Make my day" + else + @authenticated = true + send_ldap_response 1, pdu[0].to_i, 0, pdu[1][1], "I'll take it" + 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 + # NOTE, early exit. + send_ldap_response 5, pdu[0].to_i, 50, "", "Who did you say you were?" + return + end + + treebase = pdu[1][0] + 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 + + # 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 + + filters = pdu[1][6] + 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 + } + + appseq = [dn.to_ber, attrs.to_ber_sequence].to_ber_appsequence(4) + pkt = [msgid.to_ber, appseq].to_ber_sequence + send_data pkt + 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 + +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 + + 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" + + $ldif = load_test_data + + require 'net/ldap' + + 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 + 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 +