rubinius-net-ldap 0.11

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.
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