Migrate the experimental_netldap branch to trunk.

This commit is contained in:
emiel 2008-11-18 14:02:37 +00:00
commit b1fe518161
37 changed files with 7290 additions and 0 deletions

272
COPYING Normal file
View file

@ -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.

95
History.txt Normal file
View file

@ -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.

55
LICENSE Normal file
View file

@ -0,0 +1,55 @@
Net::LDAP is copyrighted free software by Francis Cianfrocca
<garbagecat10@gmail.com>. 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.

37
Manifest.txt Normal file
View file

@ -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

62
README.txt Normal file
View file

@ -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 <garbagecat10@gmail.com>
Contributions since:
* Austin Ziegler <halostatue@gmail.com>
* Emiel van de Laar <gemiel@gmail.com>
== 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.

18
Rakefile Normal file
View file

@ -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

95
Release-Announcement Normal file
View file

@ -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."

556
lib/net/ber.rb Normal file
View file

@ -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

1592
lib/net/ldap.rb Normal file

File diff suppressed because it is too large Load diff

108
lib/net/ldap/dataset.rb Normal file
View file

@ -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

269
lib/net/ldap/entry.rb Normal file
View file

@ -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
# <i>values.</i> 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

499
lib/net/ldap/filter.rb Normal file
View file

@ -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 <tt>:filter</tt> 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 <i>present</i> 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 <tt>mail.</tt>
# 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 <tt>sAMAccountName:</tt>
# 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 <i>not</i> supported
# due to limitations in the underlying LDAP protocol.
# This example selects any entry with a <tt>mail</tt> 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 <tt>objectclass</tt>
# attribute AND have a <tt>mail</tt> 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 <tt>objectclass</tt>
# attribute OR a <tt>mail</tt> 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 <i>do not</i> have an <tt>objectclass</tt>
# 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

277
lib/net/ldap/pdu.rb Normal file
View file

@ -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

64
lib/net/ldap/psw.rb Normal file
View file

@ -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

39
lib/net/ldif.rb Normal file
View file

@ -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

297
lib/net/snmp.rb Normal file
View file

@ -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

45
pre-setup.rb Normal file
View file

@ -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/**/*"]

1366
setup.rb Normal file

File diff suppressed because it is too large Load diff

7
test/common.rb Normal file
View file

@ -0,0 +1,7 @@
# Add 'lib' to load path.
$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"
require 'rubygems'
require 'test/unit'
require 'net/ldap'

100
test/test_ber.rb Normal file
View file

@ -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

7
test/test_entry.rb Normal file
View file

@ -0,0 +1,7 @@
require 'common'
class TestEntry < Test::Unit::TestCase
def test_entry
# FIX
end
end

83
test/test_filter.rb Normal file
View file

@ -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

59
test/test_ldif.rb Normal file
View file

@ -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

17
test/test_password.rb Normal file
View file

@ -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

130
test/test_snmp.rb Normal file
View file

@ -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

101
test/testdata.ldif Normal file
View file

@ -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

6
tests/NOTICE.txt Normal file
View file

@ -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.

70
tests/testber.rb Normal file
View file

@ -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

101
tests/testdata.ldif Normal file
View file

@ -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

12
tests/testem.rb Normal file
View file

@ -0,0 +1,12 @@
# $Id$
#
#
require 'test/unit'
require 'tests/testber'
require 'tests/testldif'
require 'tests/testldap'
require 'tests/testpsw'
require 'tests/testfilter'

98
tests/testfilter.rb Normal file
View file

@ -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

190
tests/testldap.rb Normal file
View file

@ -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

69
tests/testldif.rb Normal file
View file

@ -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

28
tests/testpsw.rb Normal file
View file

@ -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

136
tests/testsnmp.rb Normal file
View file

@ -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

229
testserver/ldapserver.rb Normal file
View file

@ -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

101
testserver/testdata.ldif Normal file
View file

@ -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