net-ldap 0.0.5 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of net-ldap might be problematic. Click here for more details.

@@ -0,0 +1,112 @@
1
+ require 'stringio'
2
+
3
+ module Net
4
+ module BER
5
+ module BERParser
6
+ VERSION = '0.1.0'
7
+
8
+ # The order of these follows the class-codes in BER.
9
+ # Maybe this should have been a hash.
10
+ TagClasses = [:universal, :application, :context_specific, :private]
11
+
12
+ BuiltinSyntax = Net::BER.compile_syntax( {
13
+ :universal => {
14
+ :primitive => {
15
+ 1 => :boolean,
16
+ 2 => :integer,
17
+ 4 => :string,
18
+ 5 => :null,
19
+ 6 => :oid,
20
+ 10 => :integer,
21
+ 13 => :string # (relative OID)
22
+ },
23
+ :constructed => {
24
+ 16 => :array,
25
+ 17 => :array
26
+ }
27
+ },
28
+ :context_specific => {
29
+ :primitive => {
30
+ 10 => :integer
31
+ }
32
+ }
33
+ })
34
+
35
+ def read_ber syntax=nil
36
+ # TODO: clean this up so it works properly with partial
37
+ # packets coming from streams that don't block when
38
+ # we ask for more data (like StringIOs). At it is,
39
+ # this can throw TypeErrors and other nasties.
40
+
41
+ id = getbyte or return nil # don't trash this value, we'll use it later
42
+
43
+ n = getbyte
44
+ lengthlength,contentlength = if n <= 127
45
+ [1,n]
46
+ else
47
+ # Replaced the inject because it profiles hot.
48
+ # j = (0...(n & 127)).inject(0) {|mem,x| mem = (mem << 8) + getc}
49
+ j = 0
50
+ read( n & 127 ).each_byte {|n1| j = (j << 8) + n1}
51
+ [1 + (n & 127), j]
52
+ end
53
+
54
+ newobj = read contentlength
55
+
56
+ # This exceptionally clever and clear bit of code is verrrry slow.
57
+ objtype = (syntax && syntax[id]) || BuiltinSyntax[id]
58
+
59
+ # == is expensive so sort this if/else so the common cases are at the top.
60
+ obj = if objtype == :string
61
+ #(newobj || "").dup
62
+ s = BerIdentifiedString.new( newobj || "" )
63
+ s.ber_identifier = id
64
+ s
65
+ elsif objtype == :integer
66
+ j = 0
67
+ newobj.each_byte {|b| j = (j << 8) + b}
68
+ j
69
+ elsif objtype == :oid
70
+ # cf X.690 pgh 8.19 for an explanation of this algorithm.
71
+ # Potentially not good enough. We may need a BerIdentifiedOid
72
+ # as a subclass of BerIdentifiedArray, to get the ber identifier
73
+ # and also a to_s method that produces the familiar dotted notation.
74
+ oid = newobj.unpack("w*")
75
+ f = oid.shift
76
+ g = if f < 40
77
+ [0, f]
78
+ elsif f < 80
79
+ [1, f-40]
80
+ else
81
+ [2, f-80] # f-80 can easily be > 80. What a weird optimization.
82
+ end
83
+ oid.unshift g.last
84
+ oid.unshift g.first
85
+ oid
86
+ elsif objtype == :array
87
+ #seq = []
88
+ seq = BerIdentifiedArray.new
89
+ seq.ber_identifier = id
90
+ sio = StringIO.new( newobj || "" )
91
+ # Interpret the subobject, but note how the loop
92
+ # is built: nil ends the loop, but false (a valid
93
+ # BER value) does not!
94
+ while (e = sio.read_ber(syntax)) != nil
95
+ seq << e
96
+ end
97
+ seq
98
+ elsif objtype == :boolean
99
+ newobj != "\000"
100
+ elsif objtype == :null
101
+ n = BerIdentifiedNull.new
102
+ n.ber_identifier = id
103
+ n
104
+ else
105
+ raise BerError.new( "unsupported object type: id=#{id}" )
106
+ end
107
+
108
+ obj
109
+ end
110
+ end
111
+ end
112
+ end
data/lib/net/ldap.rb CHANGED
@@ -1,41 +1,15 @@
1
- # $Id$
2
- #
3
- # Net::LDAP for Ruby
4
- #
5
- #
6
- # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
7
- #
8
- # Written and maintained by Francis Cianfrocca, gmail: garbagecat10.
9
- #
10
- # This program is free software.
11
- # You may re-distribute and/or modify this program under the same terms
12
- # as Ruby itself: Ruby Distribution License or GNU General Public License.
13
- #
14
- #
15
- # See Net::LDAP for documentation and usage samples.
16
- #
17
-
18
-
19
- require 'socket'
1
+ require 'openssl'
20
2
  require 'ostruct'
21
3
 
22
- begin
23
- require 'openssl'
24
- $net_ldap_openssl_available = true
25
- rescue LoadError
26
- end
27
-
28
4
  require 'net/ber'
29
5
  require 'net/ldap/pdu'
30
6
  require 'net/ldap/filter'
31
7
  require 'net/ldap/dataset'
32
8
  require 'net/ldap/psw'
33
9
  require 'net/ldap/entry'
34
-
10
+ require 'net/ldap/core_ext/all'
35
11
 
36
12
  module Net
37
-
38
-
39
13
  # == Net::LDAP
40
14
  #
41
15
  # This library provides a pure-Ruby implementation of the
@@ -257,46 +231,42 @@ module Net
257
231
  # and then disconnect from the server. The exception is Net::LDAP#open, which makes a connection
258
232
  # to the server and then keeps it open while it executes a user-supplied block. Net::LDAP#open
259
233
  # closes the connection on completion of the block.
260
- #
261
-
262
234
  class LDAP
263
-
264
235
  class LdapError < StandardError; end
265
236
 
266
- VERSION = "0.0.5"
267
-
237
+ VERSION = "0.1.0"
268
238
 
269
239
  SearchScope_BaseObject = 0
270
240
  SearchScope_SingleLevel = 1
271
241
  SearchScope_WholeSubtree = 2
272
242
  SearchScopes = [SearchScope_BaseObject, SearchScope_SingleLevel, SearchScope_WholeSubtree]
273
243
 
274
- AsnSyntax = BER.compile_syntax({
244
+ AsnSyntax = Net::BER.compile_syntax({
275
245
  :application => {
276
- :primitive => {
277
- 2 => :null # UnbindRequest body
278
- },
279
- :constructed => {
280
- 0 => :array, # BindRequest
281
- 1 => :array, # BindResponse
282
- 2 => :array, # UnbindRequest
283
- 3 => :array, # SearchRequest
284
- 4 => :array, # SearchData
285
- 5 => :array, # SearchResult
286
- 6 => :array, # ModifyRequest
287
- 7 => :array, # ModifyResponse
288
- 8 => :array, # AddRequest
289
- 9 => :array, # AddResponse
290
- 10 => :array, # DelRequest
291
- 11 => :array, # DelResponse
292
- 12 => :array, # ModifyRdnRequest
293
- 13 => :array, # ModifyRdnResponse
294
- 14 => :array, # CompareRequest
295
- 15 => :array, # CompareResponse
296
- 16 => :array, # AbandonRequest
297
- 19 => :array, # SearchResultReferral
298
- 24 => :array, # Unsolicited Notification
299
- }
246
+ :primitive => {
247
+ 2 => :null # UnbindRequest body
248
+ },
249
+ :constructed => {
250
+ 0 => :array, # BindRequest
251
+ 1 => :array, # BindResponse
252
+ 2 => :array, # UnbindRequest
253
+ 3 => :array, # SearchRequest
254
+ 4 => :array, # SearchData
255
+ 5 => :array, # SearchResult
256
+ 6 => :array, # ModifyRequest
257
+ 7 => :array, # ModifyResponse
258
+ 8 => :array, # AddRequest
259
+ 9 => :array, # AddResponse
260
+ 10 => :array, # DelRequest
261
+ 11 => :array, # DelResponse
262
+ 12 => :array, # ModifyRdnRequest
263
+ 13 => :array, # ModifyRdnResponse
264
+ 14 => :array, # CompareRequest
265
+ 15 => :array, # CompareResponse
266
+ 16 => :array, # AbandonRequest
267
+ 19 => :array, # SearchResultReferral
268
+ 24 => :array, # Unsolicited Notification
269
+ }
300
270
  },
301
271
  :context_specific => {
302
272
  :primitive => {
@@ -348,23 +318,17 @@ module Net
348
318
  68 => "Entry Already Exists"
349
319
  }
350
320
 
351
-
352
321
  module LdapControls
353
322
  PagedResults = "1.2.840.113556.1.4.319" # Microsoft evil from RFC 2696
354
323
  end
355
324
 
356
-
357
- #
358
325
  # LDAP::result2string
359
- #
360
326
  def LDAP::result2string code # :nodoc:
361
327
  ResultStrings[code] || "unknown result (#{code})"
362
328
  end
363
329
 
364
-
365
330
  attr_accessor :host, :port, :base
366
331
 
367
-
368
332
  # Instantiate an object of type Net::LDAP to perform directory operations.
369
333
  # This constructor takes a Hash containing arguments, all of which are either optional or may be specified later with other methods as described below. The following arguments
370
334
  # are supported:
@@ -478,12 +442,11 @@ module Net
478
442
  def encryption args
479
443
  case args
480
444
  when :simple_tls, :start_tls
481
- args = {:method => args}
445
+ args = {:method => args}
482
446
  end
483
447
  @encryption = args
484
448
  end
485
449
 
486
-
487
450
  # #open takes the same parameters as #new. #open makes a network connection to the
488
451
  # LDAP server and then passes a newly-created Net::LDAP object to the caller-supplied block.
489
452
  # Within the block, you can call any of the instance methods of Net::LDAP to
@@ -504,30 +467,31 @@ module Net
504
467
  ldap1.open {|ldap| yield ldap }
505
468
  end
506
469
 
507
- # Returns a meaningful result any time after
508
- # a protocol operation (#bind, #search, #add, #modify, #rename, #delete)
509
- # has completed.
510
- # It returns an #OpenStruct containing an LDAP result code (0 means success),
511
- # and a human-readable string.
470
+ # Returns a meaningful result any time after a protocol operation
471
+ # (#bind, #search, #add, #modify, #rename, #delete) has completed.
472
+ # It returns an #OpenStruct containing an LDAP result code (0 means
473
+ # success), and a human-readable string.
474
+ #
512
475
  # unless ldap.bind
513
476
  # puts "Result: #{ldap.get_operation_result.code}"
514
477
  # puts "Message: #{ldap.get_operation_result.message}"
515
478
  # end
516
479
  #
517
- # Certain operations return additional information, accessible through members
518
- # of the object returned from #get_operation_result. Check #get_operation_result.error_message
519
- # and #get_operation_result.matched_dn.
480
+ # Certain operations return additional information, accessible through
481
+ # members of the object returned from #get_operation_result. Check
482
+ # #get_operation_result.error_message and
483
+ # #get_operation_result.matched_dn.
520
484
  #
521
485
  #--
522
- # Modified the implementation, 20Mar07. We might get a hash of LDAP response codes
523
- # instead of a simple numeric code.
524
- #
486
+ # Modified the implementation, 20Mar07. We might get a hash of LDAP
487
+ # response codes instead of a simple numeric code.
488
+ #++
525
489
  def get_operation_result
526
490
  os = OpenStruct.new
527
491
  if @result.is_a?(Hash)
528
- os.code = (@result[:resultCode] || "").to_i
529
- os.error_message = @result[:errorMessage]
530
- os.matched_dn = @result[:matchedDN]
492
+ os.code = (@result[:resultCode] || "").to_i
493
+ os.error_message = @result[:errorMessage]
494
+ os.matched_dn = @result[:matchedDN]
531
495
  elsif @result
532
496
  os.code = @result
533
497
  else
@@ -537,7 +501,6 @@ module Net
537
501
  os
538
502
  end
539
503
 
540
-
541
504
  # Opens a network connection to the server and then
542
505
  # passes <tt>self</tt> to the caller-supplied block. The connection is
543
506
  # closed when the block completes. Used for executing multiple
@@ -561,6 +524,7 @@ module Net
561
524
  # We then pass self to the caller's block, where he will execute
562
525
  # his LDAP operations. Of course they will all generate auth failures
563
526
  # if the bind was unsuccessful.
527
+ #++
564
528
  def open
565
529
  raise LdapError.new( "open already in progress" ) if @open_connection
566
530
  begin
@@ -573,7 +537,6 @@ module Net
573
537
  end
574
538
  end
575
539
 
576
-
577
540
  # Searches the LDAP directory for directory entries.
578
541
  # Takes a hash argument with parameters. Supported parameters include:
579
542
  # * :base (a string specifying the tree-base for the search);
@@ -651,7 +614,7 @@ module Net
651
614
  # handle DNs. Change it to a plain array. Eventually we may
652
615
  # want to return a Dataset object that delegates to an internal
653
616
  # array, so we can provide sort methods and what-not.
654
- #
617
+ #++
655
618
  def search args = {}
656
619
  unless args[:ignore_server_caps]
657
620
  args[:paged_searches_supported] = paged_searches_supported?
@@ -744,8 +707,8 @@ module Net
744
707
  # If there is an @open_connection, then perform the bind
745
708
  # on it. Otherwise, connect, bind, and disconnect.
746
709
  # The latter operation is obviously useful only as an auth check.
747
- #
748
- def bind auth=@auth
710
+ #++
711
+ def bind(auth=@auth)
749
712
  if @open_connection
750
713
  @result = @open_connection.bind auth
751
714
  else
@@ -760,7 +723,6 @@ module Net
760
723
  @result == 0
761
724
  end
762
725
 
763
-
764
726
  #
765
727
  # #bind_as is for testing authentication credentials.
766
728
  #
@@ -820,7 +782,6 @@ module Net
820
782
  result
821
783
  end
822
784
 
823
-
824
785
  # Adds a new entry to the remote LDAP server.
825
786
  # Supported arguments:
826
787
  # :dn :: Full DN of the new entry
@@ -847,7 +808,7 @@ module Net
847
808
  #--
848
809
  # Provisional modification: Connection#add returns a full hash with LDAP status values,
849
810
  # instead of the simple result number we're used to getting.
850
- #
811
+ #++
851
812
  def add args
852
813
  if @open_connection
853
814
  @result = @open_connection.add( args )
@@ -865,7 +826,6 @@ module Net
865
826
  @result == 0
866
827
  end
867
828
 
868
-
869
829
  # Modifies the attribute values of a particular entry on the LDAP directory.
870
830
  # Takes a hash with arguments. Supported arguments are:
871
831
  # :dn :: (the full DN of the entry whose attributes are to be modified)
@@ -963,7 +923,6 @@ module Net
963
923
  @result == 0
964
924
  end
965
925
 
966
-
967
926
  # Add a value to an attribute.
968
927
  # Takes the full DN of the entry to modify,
969
928
  # the name (Symbol or String) of the attribute, and the value (String or
@@ -1017,7 +976,6 @@ module Net
1017
976
  modify :dn => dn, :operations => [[:delete, attribute, nil]]
1018
977
  end
1019
978
 
1020
-
1021
979
  # Rename an entry on the remote DIS by changing the last RDN of its DN.
1022
980
  # _Documentation_ _stub_
1023
981
  #
@@ -1071,7 +1029,6 @@ module Net
1071
1029
  @result == 0
1072
1030
  end
1073
1031
 
1074
-
1075
1032
  # (Experimental, subject to change).
1076
1033
  # Return the rootDSE record from the LDAP server as a Net::LDAP::Entry, or an
1077
1034
  # empty Entry if the server doesn't return the record.
@@ -1084,7 +1041,7 @@ module Net
1084
1041
  # We may be called by #search itself, which may need to determine things like paged
1085
1042
  # search capabilities. So to avoid an infinite regress, set :ignore_server_caps,
1086
1043
  # which prevents us getting called recursively.
1087
- #
1044
+ #++
1088
1045
  def search_root_dse
1089
1046
  rs = search(
1090
1047
  :ignore_server_caps=>true,
@@ -1095,519 +1052,508 @@ module Net
1095
1052
  (rs and rs.first) or Entry.new
1096
1053
  end
1097
1054
 
1055
+ # Return the root Subschema record from the LDAP server as a Net::LDAP::Entry,
1056
+ # or an empty Entry if the server doesn't return the record. On success, the
1057
+ # Net::LDAP::Entry returned from this call will have the attributes :dn,
1058
+ # :objectclasses, and :attributetypes. If there is an error, call #get_operation_result
1059
+ # for more information.
1060
+ #
1061
+ # ldap = Net::LDAP.new
1062
+ # ldap.host = "your.ldap.host"
1063
+ # ldap.auth "your-user-dn", "your-psw"
1064
+ # subschema_entry = ldap.search_subschema_entry
1065
+ #
1066
+ # subschema_entry.attributetypes.each do |attrtype|
1067
+ # # your code
1068
+ # end
1069
+ #
1070
+ # subschema_entry.objectclasses.each do |attrtype|
1071
+ # # your code
1072
+ # end
1073
+ #--
1074
+ # cf. RFC4512 section 4, particulary graff 4.4.
1075
+ # The :dn attribute in the returned Entry is the subschema name as returned from
1076
+ # the server.
1077
+ # Set :ignore_server_caps, see the notes in search_root_dse.
1078
+ #++
1079
+ def search_subschema_entry
1080
+ rs = search(
1081
+ :ignore_server_caps=>true,
1082
+ :base=>"",
1083
+ :scope=>SearchScope_BaseObject,
1084
+ :attributes=>[:subschemaSubentry]
1085
+ )
1086
+ return Entry.new unless (rs and rs.first)
1087
+ subschema_name = rs.first.subschemasubentry
1088
+ return Entry.new unless (subschema_name and subschema_name.first)
1098
1089
 
1099
- # Return the root Subschema record from the LDAP server as a Net::LDAP::Entry,
1100
- # or an empty Entry if the server doesn't return the record. On success, the
1101
- # Net::LDAP::Entry returned from this call will have the attributes :dn,
1102
- # :objectclasses, and :attributetypes. If there is an error, call #get_operation_result
1103
- # for more information.
1104
- #
1105
- # ldap = Net::LDAP.new
1106
- # ldap.host = "your.ldap.host"
1107
- # ldap.auth "your-user-dn", "your-psw"
1108
- # subschema_entry = ldap.search_subschema_entry
1109
- #
1110
- # subschema_entry.attributetypes.each do |attrtype|
1111
- # # your code
1112
- # end
1113
- #
1114
- # subschema_entry.objectclasses.each do |attrtype|
1115
- # # your code
1116
- # end
1117
- #--
1118
- # cf. RFC4512 section 4, particulary graff 4.4.
1119
- # The :dn attribute in the returned Entry is the subschema name as returned from
1120
- # the server.
1121
- # Set :ignore_server_caps, see the notes in search_root_dse.
1122
- #
1123
- def search_subschema_entry
1124
- rs = search(
1125
- :ignore_server_caps=>true,
1126
- :base=>"",
1127
- :scope=>SearchScope_BaseObject,
1128
- :attributes=>[:subschemaSubentry]
1129
- )
1130
- return Entry.new unless (rs and rs.first)
1131
- subschema_name = rs.first.subschemasubentry
1132
- return Entry.new unless (subschema_name and subschema_name.first)
1133
-
1134
- rs = search(
1135
- :ignore_server_caps=>true,
1136
- :base=>subschema_name.first,
1137
- :scope=>SearchScope_BaseObject,
1138
- :filter=>"objectclass=subschema",
1139
- :attributes=>[:objectclasses, :attributetypes]
1140
- )
1141
-
1142
- (rs and rs.first) or Entry.new
1143
- end
1090
+ rs = search(
1091
+ :ignore_server_caps=>true,
1092
+ :base=>subschema_name.first,
1093
+ :scope=>SearchScope_BaseObject,
1094
+ :filter=>"objectclass=subschema",
1095
+ :attributes=>[:objectclasses, :attributetypes]
1096
+ )
1144
1097
 
1098
+ (rs and rs.first) or Entry.new
1099
+ end
1145
1100
 
1146
1101
  #--
1147
1102
  # Convenience method to query server capabilities.
1148
1103
  # Only do this once per Net::LDAP object.
1149
1104
  # Note, we call a search, and we might be called from inside a search!
1150
1105
  # MUST refactor the root_dse call out.
1106
+ #++
1151
1107
  def paged_searches_supported?
1152
1108
  @server_caps ||= search_root_dse
1153
1109
  @server_caps[:supportedcontrol].include?(LdapControls::PagedResults)
1154
1110
  end
1155
-
1156
1111
  end # class LDAP
1157
1112
 
1158
-
1159
-
1160
1113
  class LDAP
1161
- # This is a private class used internally by the library. It should not be called by user code.
1162
- class Connection # :nodoc:
1114
+ # This is a private class used internally by the library. It should not
1115
+ # be called by user code.
1116
+ class Connection # :nodoc:
1117
+ LdapVersion = 3
1118
+ MaxSaslChallenges = 10
1163
1119
 
1164
- LdapVersion = 3
1165
- MaxSaslChallenges = 10
1120
+ def initialize server
1121
+ begin
1122
+ @conn = TCPSocket.new( server[:host], server[:port] )
1123
+ rescue
1124
+ raise LdapError.new( "no connection to server" )
1125
+ end
1166
1126
 
1127
+ if server[:encryption]
1128
+ setup_encryption server[:encryption]
1129
+ end
1167
1130
 
1168
- #--
1169
- # initialize
1170
- #
1171
- def initialize server
1172
- begin
1173
- @conn = TCPSocket.new( server[:host], server[:port] )
1174
- rescue
1175
- raise LdapError.new( "no connection to server" )
1131
+ yield self if block_given?
1176
1132
  end
1177
1133
 
1178
- if server[:encryption]
1179
- setup_encryption server[:encryption]
1134
+ module GetbyteForSSLSocket
1135
+ def getbyte
1136
+ getc.ord
1137
+ end
1180
1138
  end
1181
1139
 
1182
- yield self if block_given?
1183
- end
1184
-
1185
-
1186
- #--
1187
- # Helper method called only from new, and only after we have a successfully-opened
1188
- # @conn instance variable, which is a TCP connection.
1189
- # Depending on the received arguments, we establish SSL, potentially replacing
1190
- # the value of @conn accordingly.
1191
- # Don't generate any errors here if no encryption is requested.
1192
- # DO raise LdapError objects if encryption is requested and we have trouble setting
1193
- # it up. That includes if OpenSSL is not set up on the machine. (Question:
1194
- # how does the Ruby OpenSSL wrapper react in that case?)
1195
- # DO NOT filter exceptions raised by the OpenSSL library. Let them pass back
1196
- # to the user. That should make it easier for us to debug the problem reports.
1197
- # Presumably (hopefully?) that will also produce recognizable errors if someone
1198
- # tries to use this on a machine without OpenSSL.
1199
- #
1200
- # The simple_tls method is intended as the simplest, stupidest, easiest solution
1201
- # for people who want nothing more than encrypted comms with the LDAP server.
1202
- # It doesn't do any server-cert validation and requires nothing in the way
1203
- # of key files and root-cert files, etc etc.
1204
- # OBSERVE: WE REPLACE the value of @conn, which is presumed to be a connected
1205
- # TCPSocket object.
1206
- #
1207
- # The start_tls method is supported by many servers over the standard LDAP port.
1208
- # It does not require an alternative port for encrypted communications, as with
1209
- # simple_tls.
1210
- # Thanks for Kouhei Sutou for generously contributing the :start_tls path.
1211
- #
1212
- def setup_encryption args
1213
- case args[:method]
1214
- when :simple_tls
1215
- raise LdapError.new("openssl unavailable") unless $net_ldap_openssl_available
1140
+ def self.wrap_with_ssl(io)
1216
1141
  ctx = OpenSSL::SSL::SSLContext.new
1217
- @conn = OpenSSL::SSL::SSLSocket.new(@conn, ctx)
1218
- @conn.connect
1219
- @conn.sync_close = true
1220
- # additional branches requiring server validation and peer certs, etc. go here.
1221
- when :start_tls
1222
- raise LdapError.new("openssl unavailable") unless $net_ldap_openssl_available
1223
- msgid = next_msgid.to_ber
1224
- request = [StartTlsOid.to_ber].to_ber_appsequence( Net::LdapPdu::ExtendedRequest )
1225
- request_pkt = [msgid, request].to_ber_sequence
1226
- @conn.write request_pkt
1227
- be = @conn.read_ber(AsnSyntax)
1228
- raise LdapError.new("no start_tls result") if be.nil?
1229
- pdu = Net::LdapPdu.new(be)
1230
- raise LdapError.new("no start_tls result") if pdu.nil?
1231
- if pdu.result_code.zero?
1232
- ctx = OpenSSL::SSL::SSLContext.new
1233
- @conn = OpenSSL::SSL::SSLSocket.new(@conn, ctx)
1234
- @conn.connect
1235
- @conn.sync_close = true
1236
- else
1237
- raise LdapError.new("start_tls failed: #{pdu.result_code}")
1238
- end
1239
- else
1240
- raise LdapError.new( "unsupported encryption method #{args[:method]}" )
1142
+ conn = OpenSSL::SSL::SSLSocket.new(io, ctx)
1143
+ conn.connect
1144
+ conn.sync_close = true
1145
+
1146
+ conn.extend(GetbyteForSSLSocket) unless conn.respond_to?(:getbyte)
1147
+
1148
+ conn
1241
1149
  end
1242
- end
1243
-
1244
- #--
1245
- # close
1246
- # This is provided as a convenience method to make
1247
- # sure a connection object gets closed without waiting
1248
- # for a GC to happen. Clients shouldn't have to call it,
1249
- # but perhaps it will come in handy someday.
1250
- def close
1251
- @conn.close
1252
- @conn = nil
1253
- end
1254
-
1255
- #--
1256
- # next_msgid
1257
- #
1258
- def next_msgid
1259
- @msgid ||= 0
1260
- @msgid += 1
1261
- end
1262
1150
 
1151
+ #--
1152
+ # Helper method called only from new, and only after we have a successfully-opened
1153
+ # @conn instance variable, which is a TCP connection.
1154
+ # Depending on the received arguments, we establish SSL, potentially replacing
1155
+ # the value of @conn accordingly.
1156
+ # Don't generate any errors here if no encryption is requested.
1157
+ # DO raise LdapError objects if encryption is requested and we have trouble setting
1158
+ # it up. That includes if OpenSSL is not set up on the machine. (Question:
1159
+ # how does the Ruby OpenSSL wrapper react in that case?)
1160
+ # DO NOT filter exceptions raised by the OpenSSL library. Let them pass back
1161
+ # to the user. That should make it easier for us to debug the problem reports.
1162
+ # Presumably (hopefully?) that will also produce recognizable errors if someone
1163
+ # tries to use this on a machine without OpenSSL.
1164
+ #
1165
+ # The simple_tls method is intended as the simplest, stupidest, easiest solution
1166
+ # for people who want nothing more than encrypted comms with the LDAP server.
1167
+ # It doesn't do any server-cert validation and requires nothing in the way
1168
+ # of key files and root-cert files, etc etc.
1169
+ # OBSERVE: WE REPLACE the value of @conn, which is presumed to be a connected
1170
+ # TCPSocket object.
1171
+ #
1172
+ # The start_tls method is supported by many servers over the standard LDAP port.
1173
+ # It does not require an alternative port for encrypted communications, as with
1174
+ # simple_tls.
1175
+ # Thanks for Kouhei Sutou for generously contributing the :start_tls path.
1176
+ #++
1177
+ def setup_encryption args
1178
+ case args[:method]
1179
+ when :simple_tls
1180
+ @conn = self.class.wrap_with_ssl(@conn)
1181
+ # additional branches requiring server validation and peer certs, etc. go here.
1182
+ when :start_tls
1183
+ msgid = next_msgid.to_ber
1184
+ request = [StartTlsOid.to_ber].to_ber_appsequence( Net::LdapPdu::ExtendedRequest )
1185
+ request_pkt = [msgid, request].to_ber_sequence
1186
+ @conn.write request_pkt
1187
+ be = @conn.read_ber(AsnSyntax)
1188
+ raise LdapError.new("no start_tls result") if be.nil?
1189
+ pdu = Net::LdapPdu.new(be)
1190
+ raise LdapError.new("no start_tls result") if pdu.nil?
1191
+ if pdu.result_code.zero?
1192
+ @conn = self.class.wrap_with_ssl(@conn)
1193
+ else
1194
+ raise LdapError.new("start_tls failed: #{pdu.result_code}")
1195
+ end
1196
+ else
1197
+ raise LdapError.new( "unsupported encryption method #{args[:method]}" )
1198
+ end
1199
+ end
1263
1200
 
1264
- #--
1265
- # bind
1266
- #
1267
- def bind auth
1268
- meth = auth[:method]
1269
- if [:simple, :anonymous, :anon].include?( meth )
1270
- bind_simple auth
1271
- elsif meth == :sasl
1272
- bind_sasl( auth )
1273
- elsif meth == :gss_spnego
1274
- bind_gss_spnego( auth )
1275
- else
1276
- raise LdapError.new( "unsupported auth method (#{meth})" )
1201
+ #--
1202
+ # close
1203
+ # This is provided as a convenience method to make
1204
+ # sure a connection object gets closed without waiting
1205
+ # for a GC to happen. Clients shouldn't have to call it,
1206
+ # but perhaps it will come in handy someday.
1207
+ #++
1208
+ def close
1209
+ @conn.close
1210
+ @conn = nil
1277
1211
  end
1278
- end
1279
1212
 
1280
- #--
1281
- # bind_simple
1282
- # Implements a simple user/psw authentication.
1283
- # Accessed by calling #bind with a method of :simple or :anonymous.
1284
- #
1285
- def bind_simple auth
1286
- user,psw = if auth[:method] == :simple
1287
- [auth[:username] || auth[:dn], auth[:password]]
1288
- else
1289
- ["",""]
1213
+ #--
1214
+ # next_msgid
1215
+ #++
1216
+ def next_msgid
1217
+ @msgid ||= 0
1218
+ @msgid += 1
1290
1219
  end
1291
1220
 
1292
- raise LdapError.new( "invalid binding information" ) unless (user && psw)
1221
+ #--
1222
+ # bind
1223
+ #++
1224
+ def bind auth
1225
+ meth = auth[:method]
1226
+ if [:simple, :anonymous, :anon].include?( meth )
1227
+ bind_simple auth
1228
+ elsif meth == :sasl
1229
+ bind_sasl( auth )
1230
+ elsif meth == :gss_spnego
1231
+ bind_gss_spnego( auth )
1232
+ else
1233
+ raise LdapError.new( "unsupported auth method (#{meth})" )
1234
+ end
1235
+ end
1293
1236
 
1294
- msgid = next_msgid.to_ber
1295
- request = [LdapVersion.to_ber, user.to_ber, psw.to_ber_contextspecific(0)].to_ber_appsequence(0)
1296
- request_pkt = [msgid, request].to_ber_sequence
1297
- @conn.write request_pkt
1237
+ #--
1238
+ # bind_simple
1239
+ # Implements a simple user/psw authentication.
1240
+ # Accessed by calling #bind with a method of :simple or :anonymous.
1241
+ #++
1242
+ def bind_simple auth
1243
+ user,psw = if auth[:method] == :simple
1244
+ [auth[:username] || auth[:dn], auth[:password]]
1245
+ else
1246
+ ["",""]
1247
+ end
1298
1248
 
1299
- (be = @conn.read_ber(AsnSyntax) and pdu = Net::LdapPdu.new( be )) or raise LdapError.new( "no bind result" )
1300
- pdu.result_code
1301
- end
1249
+ raise LdapError.new( "invalid binding information" ) unless (user && psw)
1302
1250
 
1303
- #--
1304
- # bind_sasl
1305
- # Required parameters: :mechanism, :initial_credential and :challenge_response
1306
- # Mechanism is a string value that will be passed in the SASL-packet's "mechanism" field.
1307
- # Initial credential is most likely a string. It's passed in the initial BindRequest
1308
- # that goes to the server. In some protocols, it may be empty.
1309
- # Challenge-response is a Ruby proc that takes a single parameter and returns an object
1310
- # that will typically be a string. The challenge-response block is called when the server
1311
- # returns a BindResponse with a result code of 14 (saslBindInProgress). The challenge-response
1312
- # block receives a parameter containing the data returned by the server in the saslServerCreds
1313
- # field of the LDAP BindResponse packet. The challenge-response block may be called multiple
1314
- # times during the course of a SASL authentication, and each time it must return a value
1315
- # that will be passed back to the server as the credential data in the next BindRequest packet.
1316
- #
1317
- def bind_sasl auth
1318
- mech,cred,chall = auth[:mechanism],auth[:initial_credential],auth[:challenge_response]
1319
- raise LdapError.new( "invalid binding information" ) unless (mech && cred && chall)
1320
-
1321
- n = 0
1322
- loop {
1323
1251
  msgid = next_msgid.to_ber
1324
- sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3)
1325
- request = [LdapVersion.to_ber, "".to_ber, sasl].to_ber_appsequence(0)
1252
+ request = [LdapVersion.to_ber, user.to_ber, psw.to_ber_contextspecific(0)].to_ber_appsequence(0)
1326
1253
  request_pkt = [msgid, request].to_ber_sequence
1327
1254
  @conn.write request_pkt
1328
1255
 
1329
1256
  (be = @conn.read_ber(AsnSyntax) and pdu = Net::LdapPdu.new( be )) or raise LdapError.new( "no bind result" )
1330
- return pdu.result_code unless pdu.result_code == 14 # saslBindInProgress
1331
- raise LdapError.new("sasl-challenge overflow") if ((n += 1) > MaxSaslChallenges)
1257
+ pdu.result_code
1258
+ end
1332
1259
 
1333
- cred = chall.call( pdu.result_server_sasl_creds )
1334
- }
1260
+ #--
1261
+ # bind_sasl
1262
+ # Required parameters: :mechanism, :initial_credential and :challenge_response
1263
+ # Mechanism is a string value that will be passed in the SASL-packet's "mechanism" field.
1264
+ # Initial credential is most likely a string. It's passed in the initial BindRequest
1265
+ # that goes to the server. In some protocols, it may be empty.
1266
+ # Challenge-response is a Ruby proc that takes a single parameter and returns an object
1267
+ # that will typically be a string. The challenge-response block is called when the server
1268
+ # returns a BindResponse with a result code of 14 (saslBindInProgress). The challenge-response
1269
+ # block receives a parameter containing the data returned by the server in the saslServerCreds
1270
+ # field of the LDAP BindResponse packet. The challenge-response block may be called multiple
1271
+ # times during the course of a SASL authentication, and each time it must return a value
1272
+ # that will be passed back to the server as the credential data in the next BindRequest packet.
1273
+ #++
1274
+ def bind_sasl auth
1275
+ mech,cred,chall = auth[:mechanism],auth[:initial_credential],auth[:challenge_response]
1276
+ raise LdapError.new( "invalid binding information" ) unless (mech && cred && chall)
1277
+
1278
+ n = 0
1279
+ loop {
1280
+ msgid = next_msgid.to_ber
1281
+ sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3)
1282
+ request = [LdapVersion.to_ber, "".to_ber, sasl].to_ber_appsequence(0)
1283
+ request_pkt = [msgid, request].to_ber_sequence
1284
+ @conn.write request_pkt
1285
+
1286
+ (be = @conn.read_ber(AsnSyntax) and pdu = Net::LdapPdu.new( be )) or raise LdapError.new( "no bind result" )
1287
+ return pdu.result_code unless pdu.result_code == 14 # saslBindInProgress
1288
+ raise LdapError.new("sasl-challenge overflow") if ((n += 1) > MaxSaslChallenges)
1289
+
1290
+ cred = chall.call( pdu.result_server_sasl_creds )
1291
+ }
1335
1292
 
1336
- raise LdapError.new( "why are we here?")
1337
- end
1338
- private :bind_sasl
1293
+ raise LdapError.new( "why are we here?")
1294
+ end
1295
+ private :bind_sasl
1296
+
1297
+ #--
1298
+ # bind_gss_spnego
1299
+ # PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET.
1300
+ # Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to integrate it without
1301
+ # introducing an external dependency.
1302
+ # This authentication method is accessed by calling #bind with a :method parameter of
1303
+ # :gss_spnego. It requires :username and :password attributes, just like the :simple
1304
+ # authentication method. It performs a GSS-SPNEGO authentication with the server, which
1305
+ # is presumed to be a Microsoft Active Directory.
1306
+ #++
1307
+ def bind_gss_spnego auth
1308
+ require 'ntlm.rb'
1309
+
1310
+ user,psw = [auth[:username] || auth[:dn], auth[:password]]
1311
+ raise LdapError.new( "invalid binding information" ) unless (user && psw)
1312
+
1313
+ nego = proc {|challenge|
1314
+ t2_msg = NTLM::Message.parse( challenge )
1315
+ t3_msg = t2_msg.response( {:user => user, :password => psw}, {:ntlmv2 => true} )
1316
+ t3_msg.serialize
1317
+ }
1339
1318
 
1340
- #--
1341
- # bind_gss_spnego
1342
- # PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET.
1343
- # Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to integrate it without
1344
- # introducing an external dependency.
1345
- # This authentication method is accessed by calling #bind with a :method parameter of
1346
- # :gss_spnego. It requires :username and :password attributes, just like the :simple
1347
- # authentication method. It performs a GSS-SPNEGO authentication with the server, which
1348
- # is presumed to be a Microsoft Active Directory.
1349
- #
1350
- def bind_gss_spnego auth
1351
- require 'ntlm.rb'
1352
-
1353
- user,psw = [auth[:username] || auth[:dn], auth[:password]]
1354
- raise LdapError.new( "invalid binding information" ) unless (user && psw)
1355
-
1356
- nego = proc {|challenge|
1357
- t2_msg = NTLM::Message.parse( challenge )
1358
- t3_msg = t2_msg.response( {:user => user, :password => psw}, {:ntlmv2 => true} )
1359
- t3_msg.serialize
1360
- }
1319
+ bind_sasl( {
1320
+ :method => :sasl,
1321
+ :mechanism => "GSS-SPNEGO",
1322
+ :initial_credential => NTLM::Message::Type1.new.serialize,
1323
+ :challenge_response => nego
1324
+ })
1325
+ end
1326
+ private :bind_gss_spnego
1327
+
1328
+ #--
1329
+ # search
1330
+ # Alternate implementation, this yields each search entry to the caller
1331
+ # as it are received.
1332
+ # TODO, certain search parameters are hardcoded.
1333
+ # TODO, if we mis-parse the server results or the results are wrong, we can block
1334
+ # forever. That's because we keep reading results until we get a type-5 packet,
1335
+ # which might never come. We need to support the time-limit in the protocol.
1336
+ #--
1337
+ # WARNING: this code substantially recapitulates the searchx method.
1338
+ #
1339
+ # 02May06: Well, I added support for RFC-2696-style paged searches.
1340
+ # This is used on all queries because the extension is marked non-critical.
1341
+ # As far as I know, only A/D uses this, but it's required for A/D. Otherwise
1342
+ # you won't get more than 1000 results back from a query.
1343
+ # This implementation is kindof clunky and should probably be refactored.
1344
+ # Also, is it my imagination, or are A/Ds the slowest directory servers ever???
1345
+ # OpenLDAP newer than version 2.2.0 supports paged searches.
1346
+ #++
1347
+ def search args = {}
1348
+ search_filter = (args && args[:filter]) || Filter.eq( "objectclass", "*" )
1349
+ search_filter = Filter.construct(search_filter) if search_filter.is_a?(String)
1350
+ search_base = (args && args[:base]) || "dc=example,dc=com"
1351
+ search_attributes = ((args && args[:attributes]) || []).map {|attr| attr.to_s.to_ber}
1352
+ return_referrals = args && args[:return_referrals] == true
1353
+ sizelimit = (args && args[:size].to_i) || 0
1354
+ raise LdapError.new( "invalid search-size" ) unless sizelimit >= 0
1355
+ paged_searches_supported = (args && args[:paged_searches_supported])
1356
+
1357
+ attributes_only = (args and args[:attributes_only] == true)
1358
+ scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree
1359
+ raise LdapError.new( "invalid search scope" ) unless SearchScopes.include?(scope)
1360
+
1361
+ # An interesting value for the size limit would be close to A/D's built-in
1362
+ # page limit of 1000 records, but openLDAP newer than version 2.2.0 chokes
1363
+ # on anything bigger than 126. You get a silent error that is easily visible
1364
+ # by running slapd in debug mode. Go figure.
1365
+ #
1366
+ # Changed this around 06Sep06 to support a caller-specified search-size limit.
1367
+ # Because we ALWAYS do paged searches, we have to work around the problem that
1368
+ # it's not legal to specify a "normal" sizelimit (in the body of the search request)
1369
+ # that is larger than the page size we're requesting. Unfortunately, I have the
1370
+ # feeling that this will break with LDAP servers that don't support paged searches!!!
1371
+ # (Because we pass zero as the sizelimit on search rounds when the remaining limit
1372
+ # is larger than our max page size of 126. In these cases, I think the caller's
1373
+ # search limit will be ignored!)
1374
+ # CONFIRMED: This code doesn't work on LDAPs that don't support paged searches
1375
+ # when the size limit is larger than 126. We're going to have to do a root-DSE record
1376
+ # search and not do a paged search if the LDAP doesn't support it. Yuck.
1377
+ #
1378
+ rfc2696_cookie = [126, ""]
1379
+ result_code = 0
1380
+ n_results = 0
1361
1381
 
1362
- bind_sasl( {
1363
- :method => :sasl,
1364
- :mechanism => "GSS-SPNEGO",
1365
- :initial_credential => NTLM::Message::Type1.new.serialize,
1366
- :challenge_response => nego
1367
- })
1368
- end
1369
- private :bind_gss_spnego
1382
+ loop {
1383
+ # should collect this into a private helper to clarify the structure
1370
1384
 
1371
- #--
1372
- # search
1373
- # Alternate implementation, this yields each search entry to the caller
1374
- # as it are received.
1375
- # TODO, certain search parameters are hardcoded.
1376
- # TODO, if we mis-parse the server results or the results are wrong, we can block
1377
- # forever. That's because we keep reading results until we get a type-5 packet,
1378
- # which might never come. We need to support the time-limit in the protocol.
1379
- #--
1380
- # WARNING: this code substantially recapitulates the searchx method.
1381
- #
1382
- # 02May06: Well, I added support for RFC-2696-style paged searches.
1383
- # This is used on all queries because the extension is marked non-critical.
1384
- # As far as I know, only A/D uses this, but it's required for A/D. Otherwise
1385
- # you won't get more than 1000 results back from a query.
1386
- # This implementation is kindof clunky and should probably be refactored.
1387
- # Also, is it my imagination, or are A/Ds the slowest directory servers ever???
1388
- # OpenLDAP newer than version 2.2.0 supports paged searches.
1389
- #
1390
- def search args = {}
1391
- search_filter = (args && args[:filter]) || Filter.eq( "objectclass", "*" )
1392
- search_filter = Filter.construct(search_filter) if search_filter.is_a?(String)
1393
- search_base = (args && args[:base]) || "dc=example,dc=com"
1394
- search_attributes = ((args && args[:attributes]) || []).map {|attr| attr.to_s.to_ber}
1395
- return_referrals = args && args[:return_referrals] == true
1396
- sizelimit = (args && args[:size].to_i) || 0
1397
- raise LdapError.new( "invalid search-size" ) unless sizelimit >= 0
1398
- paged_searches_supported = (args && args[:paged_searches_supported])
1399
-
1400
- attributes_only = (args and args[:attributes_only] == true)
1401
- scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree
1402
- raise LdapError.new( "invalid search scope" ) unless SearchScopes.include?(scope)
1403
-
1404
- # An interesting value for the size limit would be close to A/D's built-in
1405
- # page limit of 1000 records, but openLDAP newer than version 2.2.0 chokes
1406
- # on anything bigger than 126. You get a silent error that is easily visible
1407
- # by running slapd in debug mode. Go figure.
1408
- #
1409
- # Changed this around 06Sep06 to support a caller-specified search-size limit.
1410
- # Because we ALWAYS do paged searches, we have to work around the problem that
1411
- # it's not legal to specify a "normal" sizelimit (in the body of the search request)
1412
- # that is larger than the page size we're requesting. Unfortunately, I have the
1413
- # feeling that this will break with LDAP servers that don't support paged searches!!!
1414
- # (Because we pass zero as the sizelimit on search rounds when the remaining limit
1415
- # is larger than our max page size of 126. In these cases, I think the caller's
1416
- # search limit will be ignored!)
1417
- # CONFIRMED: This code doesn't work on LDAPs that don't support paged searches
1418
- # when the size limit is larger than 126. We're going to have to do a root-DSE record
1419
- # search and not do a paged search if the LDAP doesn't support it. Yuck.
1420
- #
1421
- rfc2696_cookie = [126, ""]
1422
- result_code = 0
1423
- n_results = 0
1424
-
1425
- loop {
1426
- # should collect this into a private helper to clarify the structure
1427
-
1428
- query_limit = 0
1429
- if sizelimit > 0
1430
- if paged_searches_supported
1431
- query_limit = (((sizelimit - n_results) < 126) ? (sizelimit - n_results) : 0)
1432
- else
1433
- query_limit = sizelimit
1385
+ query_limit = 0
1386
+ if sizelimit > 0
1387
+ if paged_searches_supported
1388
+ query_limit = (((sizelimit - n_results) < 126) ? (sizelimit - n_results) : 0)
1389
+ else
1390
+ query_limit = sizelimit
1391
+ end
1434
1392
  end
1435
- end
1436
1393
 
1437
- request = [
1438
- search_base.to_ber,
1439
- scope.to_ber_enumerated,
1440
- 0.to_ber_enumerated,
1441
- query_limit.to_ber, # size limit
1442
- 0.to_ber,
1443
- attributes_only.to_ber,
1444
- search_filter.to_ber,
1445
- search_attributes.to_ber_sequence
1446
- ].to_ber_appsequence(3)
1394
+ request = [
1395
+ search_base.to_ber,
1396
+ scope.to_ber_enumerated,
1397
+ 0.to_ber_enumerated,
1398
+ query_limit.to_ber, # size limit
1399
+ 0.to_ber,
1400
+ attributes_only.to_ber,
1401
+ search_filter.to_ber,
1402
+ search_attributes.to_ber_sequence
1403
+ ].to_ber_appsequence(3)
1447
1404
 
1448
- controls = [
1449
- [
1450
- LdapControls::PagedResults.to_ber,
1451
- false.to_ber, # criticality MUST be false to interoperate with normal LDAPs.
1452
- rfc2696_cookie.map{|v| v.to_ber}.to_ber_sequence.to_s.to_ber
1453
- ].to_ber_sequence
1454
- ].to_ber_contextspecific(0)
1455
-
1456
- pkt = [next_msgid.to_ber, request, controls].to_ber_sequence
1457
- @conn.write pkt
1458
-
1459
- result_code = 0
1460
- controls = []
1461
-
1462
- while (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be ))
1463
- case pdu.app_tag
1464
- when 4 # search-data
1465
- n_results += 1
1466
- yield( pdu.search_entry ) if block_given?
1467
- when 19 # search-referral
1468
- if return_referrals
1469
- if block_given?
1470
- se = Net::LDAP::Entry.new
1471
- se[:search_referrals] = (pdu.search_referrals || [])
1472
- yield se
1405
+ controls = [
1406
+ [
1407
+ LdapControls::PagedResults.to_ber,
1408
+ false.to_ber, # criticality MUST be false to interoperate with normal LDAPs.
1409
+ rfc2696_cookie.map{|v| v.to_ber}.to_ber_sequence.to_s.to_ber
1410
+ ].to_ber_sequence
1411
+ ].to_ber_contextspecific(0)
1412
+
1413
+ pkt = [next_msgid.to_ber, request, controls].to_ber_sequence
1414
+ @conn.write pkt
1415
+
1416
+ result_code = 0
1417
+ controls = []
1418
+
1419
+ while (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be ))
1420
+ case pdu.app_tag
1421
+ when 4 # search-data
1422
+ n_results += 1
1423
+ yield( pdu.search_entry ) if block_given?
1424
+ when 19 # search-referral
1425
+ if return_referrals
1426
+ if block_given?
1427
+ se = Net::LDAP::Entry.new
1428
+ se[:search_referrals] = (pdu.search_referrals || [])
1429
+ yield se
1430
+ end
1473
1431
  end
1432
+ #p pdu.referrals
1433
+ when 5 # search-result
1434
+ result_code = pdu.result_code
1435
+ controls = pdu.result_controls
1436
+ break
1437
+ else
1438
+ raise LdapError.new( "invalid response-type in search: #{pdu.app_tag}" )
1474
1439
  end
1475
- #p pdu.referrals
1476
- when 5 # search-result
1477
- result_code = pdu.result_code
1478
- controls = pdu.result_controls
1479
- break
1480
- else
1481
- raise LdapError.new( "invalid response-type in search: #{pdu.app_tag}" )
1482
1440
  end
1483
- end
1484
1441
 
1485
- # When we get here, we have seen a type-5 response.
1486
- # If there is no error AND there is an RFC-2696 cookie,
1487
- # then query again for the next page of results.
1488
- # If not, we're done.
1489
- # Don't screw this up or we'll break every search we do.
1490
- #
1491
- # Noticed 02Sep06, look at the read_ber call in this loop,
1492
- # shouldn't that have a parameter of AsnSyntax? Does this
1493
- # just accidentally work? According to RFC-2696, the value
1494
- # expected in this position is of type OCTET STRING, covered
1495
- # in the default syntax supported by read_ber, so I guess
1496
- # we're ok.
1497
- #
1498
- more_pages = false
1499
- if result_code == 0 and controls
1500
- controls.each do |c|
1501
- if c.oid == LdapControls::PagedResults
1502
- more_pages = false # just in case some bogus server sends us >1 of these.
1503
- if c.value and c.value.length > 0
1504
- cookie = c.value.read_ber[1]
1505
- if cookie and cookie.length > 0
1506
- rfc2696_cookie[1] = cookie
1507
- more_pages = true
1442
+ # When we get here, we have seen a type-5 response.
1443
+ # If there is no error AND there is an RFC-2696 cookie,
1444
+ # then query again for the next page of results.
1445
+ # If not, we're done.
1446
+ # Don't screw this up or we'll break every search we do.
1447
+ #
1448
+ # Noticed 02Sep06, look at the read_ber call in this loop,
1449
+ # shouldn't that have a parameter of AsnSyntax? Does this
1450
+ # just accidentally work? According to RFC-2696, the value
1451
+ # expected in this position is of type OCTET STRING, covered
1452
+ # in the default syntax supported by read_ber, so I guess
1453
+ # we're ok.
1454
+ #
1455
+ more_pages = false
1456
+ if result_code == 0 and controls
1457
+ controls.each do |c|
1458
+ if c.oid == LdapControls::PagedResults
1459
+ more_pages = false # just in case some bogus server sends us >1 of these.
1460
+ if c.value and c.value.length > 0
1461
+ cookie = c.value.read_ber[1]
1462
+ if cookie and cookie.length > 0
1463
+ rfc2696_cookie[1] = cookie
1464
+ more_pages = true
1465
+ end
1508
1466
  end
1509
1467
  end
1510
1468
  end
1511
1469
  end
1512
- end
1513
1470
 
1514
- break unless more_pages
1515
- } # loop
1471
+ break unless more_pages
1472
+ } # loop
1516
1473
 
1517
- result_code
1518
- end
1519
-
1520
-
1521
-
1522
- #--
1523
- # modify
1524
- # TODO, need to support a time limit, in case the server fails to respond.
1525
- # TODO!!! We're throwing an exception here on empty DN.
1526
- # Should return a proper error instead, probaby from farther up the chain.
1527
- # TODO!!! If the user specifies a bogus opcode, we'll throw a
1528
- # confusing error here ("to_ber_enumerated is not defined on nil").
1529
- #
1530
- def modify args
1531
- modify_dn = args[:dn] or raise "Unable to modify empty DN"
1532
- modify_ops = []
1533
- a = args[:operations] and a.each {|op, attr, values|
1534
- # TODO, fix the following line, which gives a bogus error
1535
- # if the opcode is invalid.
1536
- op_1 = {:add => 0, :delete => 1, :replace => 2} [op.to_sym].to_ber_enumerated
1537
- 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
1538
- }
1539
-
1540
- request = [modify_dn.to_ber, modify_ops.to_ber_sequence].to_ber_appsequence(6)
1541
- pkt = [next_msgid.to_ber, request].to_ber_sequence
1542
- @conn.write pkt
1543
-
1544
- (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 7) or raise LdapError.new( "response missing or invalid" )
1545
- pdu.result
1546
- end
1547
-
1548
-
1549
- #--
1550
- # add
1551
- # TODO, need to support a time limit, in case the server fails to respond.
1552
- # Unlike other operation-methods in this class, we return a result hash rather
1553
- # than a simple result number. This is experimental, and eventually we'll want
1554
- # to do this with all the others. The point is to have access to the error message
1555
- # and the matched-DN returned by the server.
1556
- #
1557
- def add args
1558
- add_dn = args[:dn] or raise LdapError.new("Unable to add empty DN")
1559
- add_attrs = []
1560
- a = args[:attributes] and a.each {|k,v|
1561
- add_attrs << [ k.to_s.to_ber, v.to_a.map {|m| m.to_ber}.to_ber_set ].to_ber_sequence
1562
- }
1563
-
1564
- request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(8)
1565
- pkt = [next_msgid.to_ber, request].to_ber_sequence
1566
- @conn.write pkt
1474
+ result_code
1475
+ end
1567
1476
 
1568
- (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 9) or raise LdapError.new( "response missing or invalid" )
1569
- pdu.result
1570
- end
1477
+ #--
1478
+ # modify
1479
+ # TODO, need to support a time limit, in case the server fails to respond.
1480
+ # TODO!!! We're throwing an exception here on empty DN.
1481
+ # Should return a proper error instead, probaby from farther up the chain.
1482
+ # TODO!!! If the user specifies a bogus opcode, we'll throw a
1483
+ # confusing error here ("to_ber_enumerated is not defined on nil").
1484
+ #++
1485
+ def modify args
1486
+ modify_dn = args[:dn] or raise "Unable to modify empty DN"
1487
+ modify_ops = []
1488
+ a = args[:operations] and a.each {|op, attr, values|
1489
+ # TODO, fix the following line, which gives a bogus error
1490
+ # if the opcode is invalid.
1491
+ op_1 = {:add => 0, :delete => 1, :replace => 2} [op.to_sym].to_ber_enumerated
1492
+ 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
1493
+ }
1571
1494
 
1495
+ request = [modify_dn.to_ber, modify_ops.to_ber_sequence].to_ber_appsequence(6)
1496
+ pkt = [next_msgid.to_ber, request].to_ber_sequence
1497
+ @conn.write pkt
1572
1498
 
1573
- #--
1574
- # rename
1575
- # TODO, need to support a time limit, in case the server fails to respond.
1576
- #
1577
- def rename args
1578
- old_dn = args[:olddn] or raise "Unable to rename empty DN"
1579
- new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN"
1580
- delete_attrs = args[:delete_attributes] ? true : false
1499
+ (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 7) or raise LdapError.new( "response missing or invalid" )
1500
+ pdu.result
1501
+ end
1581
1502
 
1582
- request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber].to_ber_appsequence(12)
1583
- pkt = [next_msgid.to_ber, request].to_ber_sequence
1584
- @conn.write pkt
1503
+ #--
1504
+ # add
1505
+ # TODO, need to support a time limit, in case the server fails to respond.
1506
+ # Unlike other operation-methods in this class, we return a result hash rather
1507
+ # than a simple result number. This is experimental, and eventually we'll want
1508
+ # to do this with all the others. The point is to have access to the error message
1509
+ # and the matched-DN returned by the server.
1510
+ #++
1511
+ def add args
1512
+ add_dn = args[:dn] or raise LdapError.new("Unable to add empty DN")
1513
+ add_attrs = []
1514
+ a = args[:attributes] and a.each {|k,v|
1515
+ add_attrs << [ k.to_s.to_ber, v.to_a.map {|m| m.to_ber}.to_ber_set ].to_ber_sequence
1516
+ }
1585
1517
 
1586
- (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 13) or raise LdapError.new( "response missing or invalid" )
1587
- pdu.result_code
1588
- end
1518
+ request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(8)
1519
+ pkt = [next_msgid.to_ber, request].to_ber_sequence
1520
+ @conn.write pkt
1589
1521
 
1522
+ (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 9) or raise LdapError.new( "response missing or invalid" )
1523
+ pdu.result
1524
+ end
1590
1525
 
1591
- #--
1592
- # delete
1593
- # TODO, need to support a time limit, in case the server fails to respond.
1594
- #
1595
- def delete args
1596
- dn = args[:dn] or raise "Unable to delete empty DN"
1526
+ #--
1527
+ # rename
1528
+ # TODO, need to support a time limit, in case the server fails to respond.
1529
+ #++
1530
+ def rename args
1531
+ old_dn = args[:olddn] or raise "Unable to rename empty DN"
1532
+ new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN"
1533
+ delete_attrs = args[:delete_attributes] ? true : false
1534
+
1535
+ request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber].to_ber_appsequence(12)
1536
+ pkt = [next_msgid.to_ber, request].to_ber_sequence
1537
+ @conn.write pkt
1597
1538
 
1598
- request = dn.to_s.to_ber_application_string(10)
1599
- pkt = [next_msgid.to_ber, request].to_ber_sequence
1600
- @conn.write pkt
1539
+ (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 13) or raise LdapError.new( "response missing or invalid" )
1540
+ pdu.result_code
1541
+ end
1601
1542
 
1602
- (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 11) or raise LdapError.new( "response missing or invalid" )
1603
- pdu.result_code
1604
- end
1543
+ #--
1544
+ # delete
1545
+ # TODO, need to support a time limit, in case the server fails to respond.
1546
+ #++
1547
+ def delete args
1548
+ dn = args[:dn] or raise "Unable to delete empty DN"
1605
1549
 
1550
+ request = dn.to_s.to_ber_application_string(10)
1551
+ pkt = [next_msgid.to_ber, request].to_ber_sequence
1552
+ @conn.write pkt
1606
1553
 
1607
- end # class Connection
1554
+ (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 11) or raise LdapError.new( "response missing or invalid" )
1555
+ pdu.result_code
1556
+ end
1557
+ end # class Connection
1608
1558
  end # class LDAP
1609
-
1610
-
1611
1559
  end # module Net
1612
-
1613
-