Skip to content

Commit 7b049aa

Browse files
author
blackhedd
committed
Added net subdirectory and started refactor to net/ldap.
Thanks for the suggestion, Austin.
1 parent 7770280 commit 7b049aa

File tree

1 file changed

+387
-0
lines changed

1 file changed

+387
-0
lines changed

lib/net/ldap.rb

Lines changed: 387 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,387 @@
1+
# $Id$
2+
#
3+
# Net::LDAP for Ruby
4+
#
5+
#
6+
#
7+
# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
8+
#
9+
# Gmail: garbagecat10
10+
#
11+
# This program is free software; you can redistribute it and/or modify
12+
# it under the terms of the GNU General Public License as published by
13+
# the Free Software Foundation; either version 2 of the License, or
14+
# (at your option) any later version.
15+
#
16+
# This program is distributed in the hope that it will be useful,
17+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
# GNU General Public License for more details.
20+
#
21+
# You should have received a copy of the GNU General Public License
22+
# along with this program; if not, write to the Free Software
23+
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
24+
#
25+
#
26+
# == Miscellaneous
27+
#
28+
# For reasons relating to the source-code layout, this file doesn't
29+
# require all the outboard stuff it actually needs, like netber.
30+
# Until we figure out how to do that without damaging the directory
31+
# structure, we're reliant on user programs to explicitly require
32+
# everything, and in the correct order too!
33+
#
34+
# == BUGS:
35+
#
36+
# Try querying the objectGUID attribute from an A/D. It's a binary value
37+
# which we're reading correctly, but we need to make sure it gets base64-encoded
38+
# if we're going to put it out to an LDIF.
39+
#
40+
41+
42+
#require 'rubygems'
43+
#require_gem "eventmachine", ">= 0.3.1"
44+
45+
require 'socket'
46+
47+
48+
module Net
49+
50+
51+
#
52+
# class LDAP
53+
#
54+
class LDAP
55+
56+
class LdapError < Exception; end
57+
58+
AsnSyntax = {
59+
:application => {
60+
:constructed => {
61+
0 => :array, # BindRequest
62+
1 => :array, # BindResponse
63+
2 => :array, # UnbindRequest
64+
3 => :array, # SearchRequest
65+
4 => :array, # SearchData
66+
5 => :array, # SearchResult
67+
6 => :array, # ModifyRequest
68+
7 => :array, # ModifyResponse
69+
8 => :array, # AddRequest
70+
9 => :array, # AddResponse
71+
10 => :array, # DelRequest
72+
11 => :array, # DelResponse
73+
12 => :array, # ModifyRdnRequest
74+
13 => :array, # ModifyRdnResponse
75+
14 => :array, # CompareRequest
76+
15 => :array, # CompareResponse
77+
16 => :array, # AbandonRequest
78+
}
79+
},
80+
:context_specific => {
81+
:primitive => {
82+
0 => :string, # password
83+
1 => :string, # Kerberos v4
84+
2 => :string, # Kerberos v5
85+
}
86+
}
87+
}
88+
89+
DefaultHost = "127.0.0.1"
90+
DefaultPort = 389
91+
DefaultAuth = {:method => :anonymous}
92+
93+
94+
ResultStrings = {
95+
0 => "Success",
96+
1 => "Operations Error",
97+
16 => "No Such Attribute",
98+
17 => "Undefined Attribute Type",
99+
20 => "Attribute or Value Exists",
100+
32 => "No Such Object",
101+
34 => "Invalid DN Syntax",
102+
48 => "Invalid DN Syntax",
103+
48 => "Inappropriate Authentication",
104+
49 => "Invalid Credentials",
105+
50 => "Insufficient Access Rights",
106+
51 => "Busy",
107+
52 => "Unavailable",
108+
53 => "Unwilling to perform",
109+
68 => "Entry Already Exists"
110+
}
111+
112+
#
113+
# LDAP::result2string
114+
#
115+
def LDAP::result2string code
116+
ResultStrings[code] || "unknown result (#{code})"
117+
end
118+
119+
#
120+
# initialize
121+
#
122+
def initialize args
123+
@host = args[:host] || DefaultHost
124+
@port = args[:port] || DefaultPort
125+
@verbose = false # Make this configurable with a switch on the class.
126+
@auth = args[:auth] || DefaultAuth
127+
128+
# This variable is only set when we are created with LDAP::open.
129+
# All of our internal methods will connect using it, or else
130+
# they will create their own.
131+
@open_connection = nil
132+
end
133+
134+
#
135+
# open
136+
#
137+
def LDAP::open
138+
end
139+
140+
#
141+
# search
142+
#
143+
def search args
144+
conn = Connection.new( :host => @host, :port => @port )
145+
# TODO, hardcoded Ldap result code in next line
146+
(rc = conn.bind @auth) == 0 or return rc
147+
result_code = conn.search( args ) {|values|
148+
block_given? and yield( values )
149+
}
150+
result_code
151+
end
152+
153+
#
154+
# bind
155+
# Bind and unbind.
156+
# Can serve as a connectivity test as well as an auth test.
157+
#
158+
def bind
159+
conn = Connection.new( :host => @host, :port => @port )
160+
conn.bind @auth
161+
end
162+
163+
#
164+
# bind_as
165+
# This is for testing authentication credentials.
166+
# Most likely a "standard" name (like a CN or an email
167+
# address) will be presented along with a password.
168+
# We'll bind with the main credential given in the
169+
# constructor, query the full DN of the user given
170+
# to us as a parameter, then unbind and rebind as the
171+
# new user.
172+
#
173+
def bind_as
174+
end
175+
176+
#
177+
# add
178+
# Add a full RDN to the remote DIS.
179+
#
180+
def add args
181+
conn = Connection.new( :host => @host, :port => @port )
182+
# TODO, hardcoded Ldap result code in next line
183+
(rc = conn.bind @auth) == 0 or return rc
184+
conn.add( args )
185+
end
186+
187+
188+
#
189+
# modify
190+
# Modify the attributes of an entry on the remote DIS.
191+
#
192+
def modify args
193+
conn = Connection.new( :host => @host, :port => @port )
194+
# TODO, hardcoded Ldap result code in next line
195+
(rc = conn.bind @auth) == 0 or return rc
196+
conn.modify( args )
197+
end
198+
199+
#
200+
# rename
201+
# Rename an entry on the remote DIS by changing the last RDN of its DN.
202+
#
203+
def rename args
204+
conn = Connection.new( :host => @host, :port => @port )
205+
# TODO, hardcoded Ldap result code in next line
206+
(rc = conn.bind @auth) == 0 or return rc
207+
conn.rename( args )
208+
end
209+
210+
end # class LDAP
211+
212+
213+
214+
class LDAP
215+
class Connection
216+
217+
LdapVersion = 3
218+
219+
220+
#
221+
# initialize
222+
#
223+
def initialize server
224+
begin
225+
@conn = TCPsocket.new( server[:host], server[:port] )
226+
rescue
227+
raise LdapError.new( "no connection to server" )
228+
end
229+
230+
block_given? and yield self
231+
end
232+
233+
#
234+
# next_msgid
235+
#
236+
def next_msgid
237+
@msgid ||= 0
238+
@msgid += 1
239+
end
240+
241+
242+
#
243+
# bind
244+
#
245+
def bind auth
246+
user,psw = case auth[:method]
247+
when :anonymous
248+
["",""]
249+
when :simple
250+
[auth[:username] || auth[:dn], auth[:password]]
251+
end
252+
raise LdapError.new( "invalid binding information" ) unless (user && psw)
253+
254+
msgid = next_msgid.to_ber
255+
request = [LdapVersion.to_ber, user.to_ber, psw.to_ber_contextspecific(0)].to_ber_appsequence(0)
256+
request_pkt = [msgid, request].to_ber_sequence
257+
@conn.write request_pkt
258+
259+
(be = @conn.read_ber(AsnSyntax) and pdu = Net::LdapPdu.new( be )) or raise LdapError.new( "no bind result" )
260+
pdu.result_code
261+
end
262+
263+
#
264+
# search
265+
# TODO, certain search parameters are hardcoded.
266+
# TODO, if we mis-parse the server results or the results are wrong, we can block
267+
# forever. That's because we keep reading results until we get a type-5 packet,
268+
# which might never come. We need to support the time-limit in the protocol.
269+
#
270+
def search args
271+
search_filter = (args && args[:filter]) || Filter.eq( "objectclass", "*" )
272+
search_base = (args && args[:base]) || "dc=example,dc=com"
273+
search_attributes = ((args && args[:attributes]) || []).map {|attr| attr.to_s.to_ber}
274+
request = [
275+
search_base.to_ber,
276+
2.to_ber_enumerated,
277+
0.to_ber_enumerated,
278+
0.to_ber,
279+
0.to_ber,
280+
false.to_ber,
281+
search_filter.to_ber,
282+
search_attributes.to_ber_sequence
283+
].to_ber_appsequence(3)
284+
pkt = [next_msgid.to_ber, request].to_ber_sequence
285+
@conn.write pkt
286+
287+
search_results = {}
288+
result_code = 0
289+
290+
while (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be ))
291+
case pdu.app_tag
292+
when 4 # search-data
293+
search_results [pdu.search_dn] = pdu.search_attributes
294+
when 5 # search-result
295+
result_code = pdu.result_code
296+
block_given? and yield( search_results )
297+
break
298+
else
299+
raise LdapError.new( "invalid response-type in search: #{pdu.app_tag}" )
300+
end
301+
end
302+
303+
result_code
304+
end
305+
306+
#
307+
# modify
308+
# TODO, need to support a time limit, in case the server fails to respond.
309+
# TODO!!! We're throwing an exception here on empty DN.
310+
# Should return a proper error instead, probaby from farther up the chain.
311+
# TODO!!! If the user specifies a bogus opcode, we'll throw a
312+
# confusing error here ("to_ber_enumerated is not defined on nil").
313+
#
314+
def modify args
315+
modify_dn = args[:dn] or raise "Unable to modify empty DN"
316+
modify_ops = []
317+
a = args[:operations] and a.each {|op, attr, values|
318+
# TODO, fix the following line, which gives a bogus error
319+
# if the opcode is invalid.
320+
op_1 = {:add => 0, :delete => 1, :replace => 2} [op.to_sym].to_ber_enumerated
321+
modify_ops << [op_1, [attr.to_s.to_ber, values.to_a.map {|v| v.to_ber}.to_ber_set].to_ber_sequence].to_ber_sequence
322+
}
323+
324+
request = [modify_dn.to_ber, modify_ops.to_ber_sequence].to_ber_appsequence(6)
325+
pkt = [next_msgid.to_ber, request].to_ber_sequence
326+
@conn.write pkt
327+
328+
(be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 7) or raise LdapError.new( "response missing or invalid" )
329+
pdu.result_code
330+
end
331+
332+
333+
#
334+
# add
335+
# TODO, need to support a time limit, in case the server fails to respond.
336+
#
337+
def add args
338+
add_dn = args[:dn] or raise LdapError.new("Unable to add empty DN")
339+
add_attrs = []
340+
a = args[:attributes] and a.each {|k,v|
341+
add_attrs << [ k.to_s.to_ber, v.to_a.map {|m| m.to_ber}.to_ber_set ].to_ber_sequence
342+
}
343+
344+
request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(8)
345+
pkt = [next_msgid.to_ber, request].to_ber_sequence
346+
@conn.write pkt
347+
348+
(be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 9) or raise LdapError.new( "response missing or invalid" )
349+
pdu.result_code
350+
end
351+
352+
353+
#
354+
# rename
355+
# TODO, need to support a time limit, in case the server fails to respond.
356+
#
357+
def rename args
358+
old_dn = args[:olddn] or raise "Unable to rename empty DN"
359+
new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN"
360+
delete_attrs = args[:delete_attributes] ? true : false
361+
362+
request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber].to_ber_appsequence(12)
363+
pkt = [next_msgid.to_ber, request].to_ber_sequence
364+
@conn.write pkt
365+
366+
(be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 13) or raise LdapError.new( "response missing or invalid" )
367+
pdu.result_code
368+
end
369+
370+
371+
end # class Connection
372+
end # class LDAP
373+
374+
375+
end # module Net
376+
377+
378+
#------------------------------------------------------
379+
380+
if __FILE__ == $0
381+
puts "No default action"
382+
end
383+
384+
385+
386+
387+

0 commit comments

Comments
 (0)