net-ldap 0.8.0 → 0.9.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.

Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +7 -0
  3. data/.travis.yml +19 -1
  4. data/CONTRIBUTING.md +54 -0
  5. data/Hacking.rdoc +2 -4
  6. data/History.rdoc +37 -0
  7. data/Manifest.txt +0 -4
  8. data/README.rdoc +8 -0
  9. data/Rakefile +1 -3
  10. data/lib/net/ber/core_ext.rb +5 -5
  11. data/lib/net/ber/core_ext/string.rb +7 -7
  12. data/lib/net/ber/core_ext/true_class.rb +2 -3
  13. data/lib/net/ldap.rb +134 -620
  14. data/lib/net/ldap/connection.rb +692 -0
  15. data/lib/net/ldap/dataset.rb +18 -4
  16. data/lib/net/ldap/entry.rb +1 -1
  17. data/lib/net/ldap/filter.rb +7 -7
  18. data/lib/net/ldap/password.rb +11 -11
  19. data/lib/net/ldap/pdu.rb +28 -4
  20. data/lib/net/ldap/version.rb +1 -1
  21. data/lib/net/snmp.rb +235 -241
  22. data/net-ldap.gemspec +7 -33
  23. data/script/install-openldap +47 -0
  24. data/script/package +7 -0
  25. data/script/release +16 -0
  26. data/test/ber/core_ext/test_array.rb +22 -0
  27. data/test/ber/core_ext/test_string.rb +25 -0
  28. data/test/ber/test_ber.rb +126 -0
  29. data/test/fixtures/openldap/memberof.ldif +33 -0
  30. data/test/fixtures/openldap/retcode.ldif +76 -0
  31. data/test/fixtures/openldap/slapd.conf.ldif +67 -0
  32. data/test/fixtures/seed.ldif +374 -0
  33. data/test/integration/test_add.rb +28 -0
  34. data/test/integration/test_ber.rb +30 -0
  35. data/test/integration/test_bind.rb +22 -0
  36. data/test/integration/test_delete.rb +31 -0
  37. data/test/integration/test_open.rb +88 -0
  38. data/test/integration/test_return_codes.rb +38 -0
  39. data/test/integration/test_search.rb +77 -0
  40. data/test/support/vm/openldap/.gitignore +1 -0
  41. data/test/support/vm/openldap/README.md +32 -0
  42. data/test/support/vm/openldap/Vagrantfile +33 -0
  43. data/test/test_dn.rb +44 -0
  44. data/test/test_entry.rb +62 -56
  45. data/test/test_filter.rb +98 -2
  46. data/test/test_filter_parser.rb +16 -0
  47. data/test/test_helper.rb +54 -0
  48. data/test/test_ldap.rb +60 -0
  49. data/test/test_ldap_connection.rb +382 -2
  50. data/test/test_ldif.rb +26 -1
  51. data/test/test_password.rb +3 -10
  52. data/test/test_rename.rb +2 -2
  53. data/test/test_search.rb +39 -0
  54. data/test/test_snmp.rb +1 -1
  55. data/test/test_ssl_ber.rb +40 -0
  56. metadata +70 -75
  57. data/.autotest +0 -11
  58. data/.gemtest +0 -0
  59. data/.rspec +0 -2
  60. data/autotest/discover.rb +0 -1
  61. data/spec/integration/ssl_ber_spec.rb +0 -39
  62. data/spec/spec.opts +0 -2
  63. data/spec/spec_helper.rb +0 -28
  64. data/spec/unit/ber/ber_spec.rb +0 -141
  65. data/spec/unit/ber/core_ext/array_spec.rb +0 -24
  66. data/spec/unit/ber/core_ext/string_spec.rb +0 -51
  67. data/spec/unit/ldap/dn_spec.rb +0 -80
  68. data/spec/unit/ldap/entry_spec.rb +0 -51
  69. data/spec/unit/ldap/filter_parser_spec.rb +0 -26
  70. data/spec/unit/ldap/filter_spec.rb +0 -115
  71. data/spec/unit/ldap/search_spec.rb +0 -49
  72. data/spec/unit/ldap_spec.rb +0 -223
  73. data/test/common.rb +0 -3
@@ -0,0 +1,692 @@
1
+ # This is a private class used internally by the library. It should not
2
+ # be called by user code.
3
+ class Net::LDAP::Connection #:nodoc:
4
+ include Net::LDAP::Instrumentation
5
+
6
+ LdapVersion = 3
7
+ MaxSaslChallenges = 10
8
+
9
+ def initialize(server)
10
+ @instrumentation_service = server[:instrumentation_service]
11
+
12
+ begin
13
+ @conn = server[:socket] || TCPSocket.new(server[:host], server[:port])
14
+ rescue SocketError
15
+ raise Net::LDAP::LdapError, "No such address or other socket error."
16
+ rescue Errno::ECONNREFUSED
17
+ raise Net::LDAP::LdapError, "Server #{server[:host]} refused connection on port #{server[:port]}."
18
+ rescue Errno::EHOSTUNREACH => error
19
+ raise Net::LDAP::LdapError, "Host #{server[:host]} was unreachable (#{error.message})"
20
+ end
21
+
22
+ if server[:encryption]
23
+ setup_encryption server[:encryption]
24
+ end
25
+
26
+ yield self if block_given?
27
+ end
28
+
29
+ module GetbyteForSSLSocket
30
+ def getbyte
31
+ getc.ord
32
+ end
33
+ end
34
+
35
+ module FixSSLSocketSyncClose
36
+ def close
37
+ super
38
+ io.close
39
+ end
40
+ end
41
+
42
+ def self.wrap_with_ssl(io)
43
+ raise Net::LDAP::LdapError, "OpenSSL is unavailable" unless Net::LDAP::HasOpenSSL
44
+ ctx = OpenSSL::SSL::SSLContext.new
45
+ conn = OpenSSL::SSL::SSLSocket.new(io, ctx)
46
+ conn.connect
47
+
48
+ # Doesn't work:
49
+ # conn.sync_close = true
50
+
51
+ conn.extend(GetbyteForSSLSocket) unless conn.respond_to?(:getbyte)
52
+ conn.extend(FixSSLSocketSyncClose)
53
+
54
+ conn
55
+ end
56
+
57
+ #--
58
+ # Helper method called only from new, and only after we have a
59
+ # successfully-opened @conn instance variable, which is a TCP connection.
60
+ # Depending on the received arguments, we establish SSL, potentially
61
+ # replacing the value of @conn accordingly. Don't generate any errors here
62
+ # if no encryption is requested. DO raise Net::LDAP::LdapError objects if encryption
63
+ # is requested and we have trouble setting it up. That includes if OpenSSL
64
+ # is not set up on the machine. (Question: how does the Ruby OpenSSL
65
+ # wrapper react in that case?) DO NOT filter exceptions raised by the
66
+ # OpenSSL library. Let them pass back to the user. That should make it
67
+ # easier for us to debug the problem reports. Presumably (hopefully?) that
68
+ # will also produce recognizable errors if someone tries to use this on a
69
+ # machine without OpenSSL.
70
+ #
71
+ # The simple_tls method is intended as the simplest, stupidest, easiest
72
+ # solution for people who want nothing more than encrypted comms with the
73
+ # LDAP server. It doesn't do any server-cert validation and requires
74
+ # nothing in the way of key files and root-cert files, etc etc. OBSERVE:
75
+ # WE REPLACE the value of @conn, which is presumed to be a connected
76
+ # TCPSocket object.
77
+ #
78
+ # The start_tls method is supported by many servers over the standard LDAP
79
+ # port. It does not require an alternative port for encrypted
80
+ # communications, as with simple_tls. Thanks for Kouhei Sutou for
81
+ # generously contributing the :start_tls path.
82
+ #++
83
+ def setup_encryption(args)
84
+ case args[:method]
85
+ when :simple_tls
86
+ @conn = self.class.wrap_with_ssl(@conn)
87
+ # additional branches requiring server validation and peer certs, etc.
88
+ # go here.
89
+ when :start_tls
90
+ message_id = next_msgid
91
+ request = [
92
+ Net::LDAP::StartTlsOid.to_ber_contextspecific(0)
93
+ ].to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest)
94
+
95
+ write(request, nil, message_id)
96
+ pdu = queued_read(message_id)
97
+
98
+ if pdu.nil? || pdu.app_tag != Net::LDAP::PDU::ExtendedResponse
99
+ raise Net::LDAP::LdapError, "no start_tls result"
100
+ end
101
+
102
+ if pdu.result_code.zero?
103
+ @conn = self.class.wrap_with_ssl(@conn)
104
+ else
105
+ raise Net::LDAP::LdapError, "start_tls failed: #{pdu.result_code}"
106
+ end
107
+ else
108
+ raise Net::LDAP::LdapError, "unsupported encryption method #{args[:method]}"
109
+ end
110
+ end
111
+
112
+ #--
113
+ # This is provided as a convenience method to make sure a connection
114
+ # object gets closed without waiting for a GC to happen. Clients shouldn't
115
+ # have to call it, but perhaps it will come in handy someday.
116
+ #++
117
+ def close
118
+ @conn.close
119
+ @conn = nil
120
+ end
121
+
122
+ # Internal: Reads messages by ID from a queue, falling back to reading from
123
+ # the connected socket until a message matching the ID is read. Any messages
124
+ # with mismatched IDs gets queued for subsequent reads by the origin of that
125
+ # message ID.
126
+ #
127
+ # Returns a Net::LDAP::PDU object or nil.
128
+ def queued_read(message_id)
129
+ if pdu = message_queue[message_id].shift
130
+ return pdu
131
+ end
132
+
133
+ # read messages until we have a match for the given message_id
134
+ while pdu = read
135
+ if pdu.message_id == message_id
136
+ return pdu
137
+ else
138
+ message_queue[pdu.message_id].push pdu
139
+ next
140
+ end
141
+ end
142
+
143
+ pdu
144
+ end
145
+
146
+ # Internal: The internal queue of messages, read from the socket, grouped by
147
+ # message ID.
148
+ #
149
+ # Used by `queued_read` to return messages sent by the server with the given
150
+ # ID. If no messages are queued for that ID, `queued_read` will `read` from
151
+ # the socket and queue messages that don't match the given ID for other
152
+ # readers.
153
+ #
154
+ # Returns the message queue Hash.
155
+ def message_queue
156
+ @message_queue ||= Hash.new do |hash, key|
157
+ hash[key] = []
158
+ end
159
+ end
160
+
161
+ # Internal: Reads and parses data from the configured connection.
162
+ #
163
+ # - syntax: the BER syntax to use to parse the read data with
164
+ #
165
+ # Returns parsed Net::LDAP::PDU object.
166
+ def read(syntax = Net::LDAP::AsnSyntax)
167
+ ber_object =
168
+ instrument "read.net_ldap_connection", :syntax => syntax do |payload|
169
+ @conn.read_ber(syntax) do |id, content_length|
170
+ payload[:object_type_id] = id
171
+ payload[:content_length] = content_length
172
+ end
173
+ end
174
+
175
+ return unless ber_object
176
+
177
+ instrument "parse_pdu.net_ldap_connection" do |payload|
178
+ pdu = payload[:pdu] = Net::LDAP::PDU.new(ber_object)
179
+
180
+ payload[:message_id] = pdu.message_id
181
+ payload[:app_tag] = pdu.app_tag
182
+
183
+ pdu
184
+ end
185
+ end
186
+ private :read
187
+
188
+ # Internal: Write a BER formatted packet with the next message id to the
189
+ # configured connection.
190
+ #
191
+ # - request: required BER formatted request
192
+ # - controls: optional BER formatted controls
193
+ #
194
+ # Returns the return value from writing to the connection, which in some
195
+ # cases is the Integer number of bytes written to the socket.
196
+ def write(request, controls = nil, message_id = next_msgid)
197
+ instrument "write.net_ldap_connection" do |payload|
198
+ packet = [message_id.to_ber, request, controls].compact.to_ber_sequence
199
+ payload[:content_length] = @conn.write(packet)
200
+ end
201
+ end
202
+ private :write
203
+
204
+ def next_msgid
205
+ @msgid ||= 0
206
+ @msgid += 1
207
+ end
208
+
209
+ def bind(auth)
210
+ instrument "bind.net_ldap_connection" do |payload|
211
+ payload[:method] = meth = auth[:method]
212
+ if [:simple, :anonymous, :anon].include?(meth)
213
+ bind_simple auth
214
+ elsif meth == :sasl
215
+ bind_sasl(auth)
216
+ elsif meth == :gss_spnego
217
+ bind_gss_spnego(auth)
218
+ else
219
+ raise Net::LDAP::LdapError, "Unsupported auth method (#{meth})"
220
+ end
221
+ end
222
+ end
223
+
224
+ #--
225
+ # Implements a simple user/psw authentication. Accessed by calling #bind
226
+ # with a method of :simple or :anonymous.
227
+ #++
228
+ def bind_simple(auth)
229
+ user, psw = if auth[:method] == :simple
230
+ [auth[:username] || auth[:dn], auth[:password]]
231
+ else
232
+ ["", ""]
233
+ end
234
+
235
+ raise Net::LDAP::LdapError, "Invalid binding information" unless (user && psw)
236
+
237
+ message_id = next_msgid
238
+ request = [
239
+ LdapVersion.to_ber, user.to_ber,
240
+ psw.to_ber_contextspecific(0)
241
+ ].to_ber_appsequence(Net::LDAP::PDU::BindRequest)
242
+
243
+ write(request, nil, message_id)
244
+ pdu = queued_read(message_id)
245
+
246
+ if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult
247
+ raise Net::LDAP::LdapError, "no bind result"
248
+ end
249
+
250
+ pdu
251
+ end
252
+
253
+ #--
254
+ # Required parameters: :mechanism, :initial_credential and
255
+ # :challenge_response
256
+ #
257
+ # Mechanism is a string value that will be passed in the SASL-packet's
258
+ # "mechanism" field.
259
+ #
260
+ # Initial credential is most likely a string. It's passed in the initial
261
+ # BindRequest that goes to the server. In some protocols, it may be empty.
262
+ #
263
+ # Challenge-response is a Ruby proc that takes a single parameter and
264
+ # returns an object that will typically be a string. The
265
+ # challenge-response block is called when the server returns a
266
+ # BindResponse with a result code of 14 (saslBindInProgress). The
267
+ # challenge-response block receives a parameter containing the data
268
+ # returned by the server in the saslServerCreds field of the LDAP
269
+ # BindResponse packet. The challenge-response block may be called multiple
270
+ # times during the course of a SASL authentication, and each time it must
271
+ # return a value that will be passed back to the server as the credential
272
+ # data in the next BindRequest packet.
273
+ #++
274
+ def bind_sasl(auth)
275
+ mech, cred, chall = auth[:mechanism], auth[:initial_credential],
276
+ auth[:challenge_response]
277
+ raise Net::LDAP::LdapError, "Invalid binding information" unless (mech && cred && chall)
278
+
279
+ message_id = next_msgid
280
+
281
+ n = 0
282
+ loop {
283
+ sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3)
284
+ request = [
285
+ LdapVersion.to_ber, "".to_ber, sasl
286
+ ].to_ber_appsequence(Net::LDAP::PDU::BindRequest)
287
+
288
+ write(request, nil, message_id)
289
+ pdu = queued_read(message_id)
290
+
291
+ if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult
292
+ raise Net::LDAP::LdapError, "no bind result"
293
+ end
294
+
295
+ return pdu unless pdu.result_code == Net::LDAP::ResultCodeSaslBindInProgress
296
+ raise Net::LDAP::LdapError, "sasl-challenge overflow" if ((n += 1) > MaxSaslChallenges)
297
+
298
+ cred = chall.call(pdu.result_server_sasl_creds)
299
+ }
300
+
301
+ raise Net::LDAP::LdapError, "why are we here?"
302
+ end
303
+ private :bind_sasl
304
+
305
+ #--
306
+ # PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET.
307
+ # Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to
308
+ # integrate it without introducing an external dependency.
309
+ #
310
+ # This authentication method is accessed by calling #bind with a :method
311
+ # parameter of :gss_spnego. It requires :username and :password
312
+ # attributes, just like the :simple authentication method. It performs a
313
+ # GSS-SPNEGO authentication with the server, which is presumed to be a
314
+ # Microsoft Active Directory.
315
+ #++
316
+ def bind_gss_spnego(auth)
317
+ require 'ntlm'
318
+
319
+ user, psw = [auth[:username] || auth[:dn], auth[:password]]
320
+ raise Net::LDAP::LdapError, "Invalid binding information" unless (user && psw)
321
+
322
+ nego = proc { |challenge|
323
+ t2_msg = NTLM::Message.parse(challenge)
324
+ t3_msg = t2_msg.response({ :user => user, :password => psw },
325
+ { :ntlmv2 => true })
326
+ t3_msg.serialize
327
+ }
328
+
329
+ bind_sasl(:method => :sasl, :mechanism => "GSS-SPNEGO",
330
+ :initial_credential => NTLM::Message::Type1.new.serialize,
331
+ :challenge_response => nego)
332
+ end
333
+ private :bind_gss_spnego
334
+
335
+
336
+ #--
337
+ # Allow the caller to specify a sort control
338
+ #
339
+ # The format of the sort control needs to be:
340
+ #
341
+ # :sort_control => ["cn"] # just a string
342
+ # or
343
+ # :sort_control => [["cn", "matchingRule", true]] #attribute, matchingRule, direction (true / false)
344
+ # or
345
+ # :sort_control => ["givenname","sn"] #multiple strings or arrays
346
+ #
347
+ def encode_sort_controls(sort_definitions)
348
+ return sort_definitions unless sort_definitions
349
+
350
+ sort_control_values = sort_definitions.map do |control|
351
+ control = Array(control) # if there is only an attribute name as a string then infer the orderinrule and reverseorder
352
+ control[0] = String(control[0]).to_ber,
353
+ control[1] = String(control[1]).to_ber,
354
+ control[2] = (control[2] == true).to_ber
355
+ control.to_ber_sequence
356
+ end
357
+ sort_control = [
358
+ Net::LDAP::LDAPControls::SORT_REQUEST.to_ber,
359
+ false.to_ber,
360
+ sort_control_values.to_ber_sequence.to_s.to_ber
361
+ ].to_ber_sequence
362
+ end
363
+
364
+ #--
365
+ # Alternate implementation, this yields each search entry to the caller as
366
+ # it are received.
367
+ #
368
+ # TODO: certain search parameters are hardcoded.
369
+ # TODO: if we mis-parse the server results or the results are wrong, we
370
+ # can block forever. That's because we keep reading results until we get a
371
+ # type-5 packet, which might never come. We need to support the time-limit
372
+ # in the protocol.
373
+ #++
374
+ def search(args = nil)
375
+ args ||= {}
376
+
377
+ # filtering, scoping, search base
378
+ # filter: https://tools.ietf.org/html/rfc4511#section-4.5.1.7
379
+ # base: https://tools.ietf.org/html/rfc4511#section-4.5.1.1
380
+ # scope: https://tools.ietf.org/html/rfc4511#section-4.5.1.2
381
+ filter = args[:filter] || Net::LDAP::Filter.eq("objectClass", "*")
382
+ base = args[:base]
383
+ scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree
384
+
385
+ # attr handling
386
+ # attrs: https://tools.ietf.org/html/rfc4511#section-4.5.1.8
387
+ # attrs_only: https://tools.ietf.org/html/rfc4511#section-4.5.1.6
388
+ attrs = Array(args[:attributes])
389
+ attrs_only = args[:attributes_only] == true
390
+
391
+ # references
392
+ # refs: https://tools.ietf.org/html/rfc4511#section-4.5.3
393
+ # deref: https://tools.ietf.org/html/rfc4511#section-4.5.1.3
394
+ refs = args[:return_referrals] == true
395
+ deref = args[:deref] || Net::LDAP::DerefAliases_Never
396
+
397
+ # limiting, paging, sorting
398
+ # size: https://tools.ietf.org/html/rfc4511#section-4.5.1.4
399
+ # time: https://tools.ietf.org/html/rfc4511#section-4.5.1.5
400
+ size = args[:size].to_i
401
+ time = args[:time].to_i
402
+ paged = args[:paged_searches_supported]
403
+ sort = args.fetch(:sort_controls, false)
404
+
405
+ # arg validation
406
+ raise Net::LDAP::LdapError, "search base is required" unless base
407
+ raise Net::LDAP::LdapError, "invalid search-size" unless size >= 0
408
+ raise Net::LDAP::LdapError, "invalid search scope" unless Net::LDAP::SearchScopes.include?(scope)
409
+ raise Net::LDAP::LdapError, "invalid alias dereferencing value" unless Net::LDAP::DerefAliasesArray.include?(deref)
410
+
411
+ # arg transforms
412
+ filter = Net::LDAP::Filter.construct(filter) if filter.is_a?(String)
413
+ ber_attrs = attrs.map { |attr| attr.to_s.to_ber }
414
+ ber_sort = encode_sort_controls(sort)
415
+
416
+ # An interesting value for the size limit would be close to A/D's
417
+ # built-in page limit of 1000 records, but openLDAP newer than version
418
+ # 2.2.0 chokes on anything bigger than 126. You get a silent error that
419
+ # is easily visible by running slapd in debug mode. Go figure.
420
+ #
421
+ # Changed this around 06Sep06 to support a caller-specified search-size
422
+ # limit. Because we ALWAYS do paged searches, we have to work around the
423
+ # problem that it's not legal to specify a "normal" sizelimit (in the
424
+ # body of the search request) that is larger than the page size we're
425
+ # requesting. Unfortunately, I have the feeling that this will break
426
+ # with LDAP servers that don't support paged searches!!!
427
+ #
428
+ # (Because we pass zero as the sizelimit on search rounds when the
429
+ # remaining limit is larger than our max page size of 126. In these
430
+ # cases, I think the caller's search limit will be ignored!)
431
+ #
432
+ # CONFIRMED: This code doesn't work on LDAPs that don't support paged
433
+ # searches when the size limit is larger than 126. We're going to have
434
+ # to do a root-DSE record search and not do a paged search if the LDAP
435
+ # doesn't support it. Yuck.
436
+ rfc2696_cookie = [126, ""]
437
+ result_pdu = nil
438
+ n_results = 0
439
+
440
+ message_id = next_msgid
441
+
442
+ instrument "search.net_ldap_connection",
443
+ message_id: message_id,
444
+ filter: filter,
445
+ base: base,
446
+ scope: scope,
447
+ size: size,
448
+ time: time,
449
+ sort: sort,
450
+ referrals: refs,
451
+ deref: deref,
452
+ attributes: attrs do |payload|
453
+ loop do
454
+ # should collect this into a private helper to clarify the structure
455
+ query_limit = 0
456
+ if size > 0
457
+ if paged
458
+ query_limit = (((size - n_results) < 126) ? (size -
459
+ n_results) : 0)
460
+ else
461
+ query_limit = size
462
+ end
463
+ end
464
+
465
+ request = [
466
+ base.to_ber,
467
+ scope.to_ber_enumerated,
468
+ deref.to_ber_enumerated,
469
+ query_limit.to_ber, # size limit
470
+ time.to_ber,
471
+ attrs_only.to_ber,
472
+ filter.to_ber,
473
+ ber_attrs.to_ber_sequence
474
+ ].to_ber_appsequence(Net::LDAP::PDU::SearchRequest)
475
+
476
+ # rfc2696_cookie sometimes contains binary data from Microsoft Active Directory
477
+ # this breaks when calling to_ber. (Can't force binary data to UTF-8)
478
+ # we have to disable paging (even though server supports it) to get around this...
479
+
480
+ controls = []
481
+ controls <<
482
+ [
483
+ Net::LDAP::LDAPControls::PAGED_RESULTS.to_ber,
484
+ # Criticality MUST be false to interoperate with normal LDAPs.
485
+ false.to_ber,
486
+ rfc2696_cookie.map{ |v| v.to_ber}.to_ber_sequence.to_s.to_ber
487
+ ].to_ber_sequence if paged
488
+ controls << ber_sort if ber_sort
489
+ controls = controls.empty? ? nil : controls.to_ber_contextspecific(0)
490
+
491
+ write(request, controls, message_id)
492
+
493
+ result_pdu = nil
494
+ controls = []
495
+
496
+ while pdu = queued_read(message_id)
497
+ case pdu.app_tag
498
+ when Net::LDAP::PDU::SearchReturnedData
499
+ n_results += 1
500
+ yield pdu.search_entry if block_given?
501
+ when Net::LDAP::PDU::SearchResultReferral
502
+ if refs
503
+ if block_given?
504
+ se = Net::LDAP::Entry.new
505
+ se[:search_referrals] = (pdu.search_referrals || [])
506
+ yield se
507
+ end
508
+ end
509
+ when Net::LDAP::PDU::SearchResult
510
+ result_pdu = pdu
511
+ controls = pdu.result_controls
512
+ if refs && pdu.result_code == Net::LDAP::ResultCodeReferral
513
+ if block_given?
514
+ se = Net::LDAP::Entry.new
515
+ se[:search_referrals] = (pdu.search_referrals || [])
516
+ yield se
517
+ end
518
+ end
519
+ break
520
+ else
521
+ raise Net::LDAP::LdapError, "invalid response-type in search: #{pdu.app_tag}"
522
+ end
523
+ end
524
+
525
+ # count number of pages of results
526
+ payload[:page_count] ||= 0
527
+ payload[:page_count] += 1
528
+
529
+ # When we get here, we have seen a type-5 response. If there is no
530
+ # error AND there is an RFC-2696 cookie, then query again for the next
531
+ # page of results. If not, we're done. Don't screw this up or we'll
532
+ # break every search we do.
533
+ #
534
+ # Noticed 02Sep06, look at the read_ber call in this loop, shouldn't
535
+ # that have a parameter of AsnSyntax? Does this just accidentally
536
+ # work? According to RFC-2696, the value expected in this position is
537
+ # of type OCTET STRING, covered in the default syntax supported by
538
+ # read_ber, so I guess we're ok.
539
+ more_pages = false
540
+ if result_pdu.result_code == Net::LDAP::ResultCodeSuccess and controls
541
+ controls.each do |c|
542
+ if c.oid == Net::LDAP::LDAPControls::PAGED_RESULTS
543
+ # just in case some bogus server sends us more than 1 of these.
544
+ more_pages = false
545
+ if c.value and c.value.length > 0
546
+ cookie = c.value.read_ber[1]
547
+ if cookie and cookie.length > 0
548
+ rfc2696_cookie[1] = cookie
549
+ more_pages = true
550
+ end
551
+ end
552
+ end
553
+ end
554
+ end
555
+
556
+ break unless more_pages
557
+ end # loop
558
+
559
+ # track total result count
560
+ payload[:result_count] = n_results
561
+
562
+ result_pdu || OpenStruct.new(:status => :failure, :result_code => Net::LDAP::ResultCodeOperationsError, :message => "Invalid search")
563
+ end # instrument
564
+ ensure
565
+ # clean up message queue for this search
566
+ messages = message_queue.delete(message_id)
567
+
568
+ # in the exceptional case some messages were *not* consumed from the queue,
569
+ # instrument the event but do not fail.
570
+ unless messages.empty?
571
+ instrument "search_messages_unread.net_ldap_connection",
572
+ message_id: message_id, messages: messages
573
+ end
574
+ end
575
+
576
+ MODIFY_OPERATIONS = { #:nodoc:
577
+ :add => 0,
578
+ :delete => 1,
579
+ :replace => 2
580
+ }
581
+
582
+ def self.modify_ops(operations)
583
+ ops = []
584
+ if operations
585
+ operations.each { |op, attrib, values|
586
+ # TODO, fix the following line, which gives a bogus error if the
587
+ # opcode is invalid.
588
+ op_ber = MODIFY_OPERATIONS[op.to_sym].to_ber_enumerated
589
+ values = [ values ].flatten.map { |v| v.to_ber if v }.to_ber_set
590
+ values = [ attrib.to_s.to_ber, values ].to_ber_sequence
591
+ ops << [ op_ber, values ].to_ber
592
+ }
593
+ end
594
+ ops
595
+ end
596
+
597
+ #--
598
+ # TODO: need to support a time limit, in case the server fails to respond.
599
+ # TODO: We're throwing an exception here on empty DN. Should return a
600
+ # proper error instead, probaby from farther up the chain.
601
+ # TODO: If the user specifies a bogus opcode, we'll throw a confusing
602
+ # error here ("to_ber_enumerated is not defined on nil").
603
+ #++
604
+ def modify(args)
605
+ modify_dn = args[:dn] or raise "Unable to modify empty DN"
606
+ ops = self.class.modify_ops args[:operations]
607
+
608
+ message_id = next_msgid
609
+ request = [
610
+ modify_dn.to_ber,
611
+ ops.to_ber_sequence
612
+ ].to_ber_appsequence(Net::LDAP::PDU::ModifyRequest)
613
+
614
+ write(request, nil, message_id)
615
+ pdu = queued_read(message_id)
616
+
617
+ if !pdu || pdu.app_tag != Net::LDAP::PDU::ModifyResponse
618
+ raise Net::LDAP::LdapError, "response missing or invalid"
619
+ end
620
+
621
+ pdu
622
+ end
623
+
624
+ #--
625
+ # TODO: need to support a time limit, in case the server fails to respond.
626
+ # Unlike other operation-methods in this class, we return a result hash
627
+ # rather than a simple result number. This is experimental, and eventually
628
+ # we'll want to do this with all the others. The point is to have access
629
+ # to the error message and the matched-DN returned by the server.
630
+ #++
631
+ def add(args)
632
+ add_dn = args[:dn] or raise Net::LDAP::LdapError, "Unable to add empty DN"
633
+ add_attrs = []
634
+ a = args[:attributes] and a.each { |k, v|
635
+ add_attrs << [ k.to_s.to_ber, Array(v).map { |m| m.to_ber}.to_ber_set ].to_ber_sequence
636
+ }
637
+
638
+ message_id = next_msgid
639
+ request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(Net::LDAP::PDU::AddRequest)
640
+
641
+ write(request, nil, message_id)
642
+ pdu = queued_read(message_id)
643
+
644
+ if !pdu || pdu.app_tag != Net::LDAP::PDU::AddResponse
645
+ raise Net::LDAP::LdapError, "response missing or invalid"
646
+ end
647
+
648
+ pdu
649
+ end
650
+
651
+ #--
652
+ # TODO: need to support a time limit, in case the server fails to respond.
653
+ #++
654
+ def rename(args)
655
+ old_dn = args[:olddn] or raise "Unable to rename empty DN"
656
+ new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN"
657
+ delete_attrs = args[:delete_attributes] ? true : false
658
+ new_superior = args[:new_superior]
659
+
660
+ message_id = next_msgid
661
+ request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber]
662
+ request << new_superior.to_ber_contextspecific(0) unless new_superior == nil
663
+
664
+ write(request.to_ber_appsequence(Net::LDAP::PDU::ModifyRDNRequest), nil, message_id)
665
+ pdu = queued_read(message_id)
666
+
667
+ if !pdu || pdu.app_tag != Net::LDAP::PDU::ModifyRDNResponse
668
+ raise Net::LDAP::LdapError.new "response missing or invalid"
669
+ end
670
+
671
+ pdu
672
+ end
673
+
674
+ #--
675
+ # TODO, need to support a time limit, in case the server fails to respond.
676
+ #++
677
+ def delete(args)
678
+ dn = args[:dn] or raise "Unable to delete empty DN"
679
+ controls = args.include?(:control_codes) ? args[:control_codes].to_ber_control : nil #use nil so we can compact later
680
+ message_id = next_msgid
681
+ request = dn.to_s.to_ber_application_string(Net::LDAP::PDU::DeleteRequest)
682
+
683
+ write(request, controls, message_id)
684
+ pdu = queued_read(message_id)
685
+
686
+ if !pdu || pdu.app_tag != Net::LDAP::PDU::DeleteResponse
687
+ raise Net::LDAP::LdapError, "response missing or invalid"
688
+ end
689
+
690
+ pdu
691
+ end
692
+ end # class Connection