rubinius-net-ldap 0.11

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