net-ldap 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

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