@@ -111,6 +111,45 @@ def close
111
111
@conn = nil
112
112
end
113
113
114
+ # Internal: Reads messages by ID from a queue, falling back to reading from
115
+ # the connected socket until a message matching the ID is read. Any messages
116
+ # with mismatched IDs gets queued for subsequent reads by the origin of that
117
+ # message ID.
118
+ #
119
+ # Returns a Net::LDAP::PDU object or nil.
120
+ def queued_read ( message_id )
121
+ if pdu = message_queue [ message_id ] . shift
122
+ return pdu
123
+ end
124
+
125
+ # read messages until we have a match for the given message_id
126
+ while pdu = read
127
+ if pdu . message_id == message_id
128
+ return pdu
129
+ else
130
+ message_queue [ pdu . message_id ] . push pdu
131
+ next
132
+ end
133
+ end
134
+
135
+ pdu
136
+ end
137
+
138
+ # Internal: The internal queue of messages, read from the socket, grouped by
139
+ # message ID.
140
+ #
141
+ # Used by `queued_read` to return messages sent by the server with the given
142
+ # ID. If no messages are queued for that ID, `queued_read` will `read` from
143
+ # the socket and queue messages that don't match the given ID for other
144
+ # readers.
145
+ #
146
+ # Returns the message queue Hash.
147
+ def message_queue
148
+ @message_queue ||= Hash . new do |hash , key |
149
+ hash [ key ] = [ ]
150
+ end
151
+ end
152
+
114
153
# Internal: Reads and parses data from the configured connection.
115
154
#
116
155
# - syntax: the BER syntax to use to parse the read data with
@@ -146,9 +185,9 @@ def read(syntax = Net::LDAP::AsnSyntax)
146
185
#
147
186
# Returns the return value from writing to the connection, which in some
148
187
# cases is the Integer number of bytes written to the socket.
149
- def write ( request , controls = nil )
188
+ def write ( request , controls = nil , message_id = next_msgid )
150
189
instrument "write.net_ldap_connection" do |payload |
151
- packet = [ next_msgid . to_ber , request , controls ] . compact . to_ber_sequence
190
+ packet = [ message_id . to_ber , request , controls ] . compact . to_ber_sequence
152
191
payload [ :content_length ] = @conn . write ( packet )
153
192
end
154
193
end
@@ -311,26 +350,47 @@ def encode_sort_controls(sort_definitions)
311
350
# type-5 packet, which might never come. We need to support the time-limit
312
351
# in the protocol.
313
352
#++
314
- def search ( args = { } )
315
- search_filter = ( args && args [ :filter ] ) ||
316
- Net ::LDAP ::Filter . eq ( "objectclass" , "*" )
317
- search_filter = Net ::LDAP ::Filter . construct ( search_filter ) if search_filter . is_a? ( String )
318
- search_base = ( args && args [ :base ] ) || "dc=example, dc=com"
319
- search_attributes = ( ( args && args [ :attributes ] ) || [ ] ) . map { |attr | attr . to_s . to_ber }
320
- return_referrals = args && args [ :return_referrals ] == true
321
- sizelimit = ( args && args [ :size ] . to_i ) || 0
322
- raise Net ::LDAP ::LdapError , "invalid search-size" unless sizelimit >= 0
323
- paged_searches_supported = ( args && args [ :paged_searches_supported ] )
324
-
325
- attributes_only = ( args and args [ :attributes_only ] == true )
326
- scope = args [ :scope ] || Net ::LDAP ::SearchScope_WholeSubtree
353
+ def search ( args = nil )
354
+ args ||= { }
355
+
356
+ # filtering, scoping, search base
357
+ # filter: https://tools.ietf.org/html/rfc4511#section-4.5.1.7
358
+ # base: https://tools.ietf.org/html/rfc4511#section-4.5.1.1
359
+ # scope: https://tools.ietf.org/html/rfc4511#section-4.5.1.2
360
+ filter = args [ :filter ] || Net ::LDAP ::Filter . eq ( "objectClass" , "*" )
361
+ base = args [ :base ]
362
+ scope = args [ :scope ] || Net ::LDAP ::SearchScope_WholeSubtree
363
+
364
+ # attr handling
365
+ # attrs: https://tools.ietf.org/html/rfc4511#section-4.5.1.8
366
+ # attrs_only: https://tools.ietf.org/html/rfc4511#section-4.5.1.6
367
+ attrs = Array ( args [ :attributes ] )
368
+ attrs_only = args [ :attributes_only ] == true
369
+
370
+ # references
371
+ # refs: https://tools.ietf.org/html/rfc4511#section-4.5.3
372
+ # deref: https://tools.ietf.org/html/rfc4511#section-4.5.1.3
373
+ refs = args [ :return_referrals ] == true
374
+ deref = args [ :deref ] || Net ::LDAP ::DerefAliases_Never
375
+
376
+ # limiting, paging, sorting
377
+ # size: https://tools.ietf.org/html/rfc4511#section-4.5.1.4
378
+ # time: https://tools.ietf.org/html/rfc4511#section-4.5.1.5
379
+ size = args [ :size ] . to_i
380
+ time = args [ :time ] . to_i
381
+ paged = args [ :paged_searches_supported ]
382
+ sort = args . fetch ( :sort_controls , false )
383
+
384
+ # arg validation
385
+ raise Net ::LDAP ::LdapError , "search base is required" unless base
386
+ raise Net ::LDAP ::LdapError , "invalid search-size" unless size >= 0
327
387
raise Net ::LDAP ::LdapError , "invalid search scope" unless Net ::LDAP ::SearchScopes . include? ( scope )
388
+ raise Net ::LDAP ::LdapError , "invalid alias dereferencing value" unless Net ::LDAP ::DerefAliasesArray . include? ( deref )
328
389
329
- sort_control = encode_sort_controls ( args . fetch ( :sort_controls ) { false } )
330
-
331
- deref = args [ :deref ] || Net ::LDAP ::DerefAliases_Never
332
- raise Net ::LDAP ::LdapError . new ( "invalid alias dereferencing value" ) unless Net ::LDAP ::DerefAliasesArray . include? ( deref )
333
-
390
+ # arg transforms
391
+ filter = Net ::LDAP ::Filter . construct ( filter ) if filter . is_a? ( String )
392
+ ber_attrs = attrs . map { |attr | attr . to_s . to_ber }
393
+ ber_sort = encode_sort_controls ( sort )
334
394
335
395
# An interesting value for the size limit would be close to A/D's
336
396
# built-in page limit of 1000 records, but openLDAP newer than version
@@ -356,36 +416,40 @@ def search(args = {})
356
416
result_pdu = nil
357
417
n_results = 0
358
418
419
+ message_id = next_msgid
420
+
359
421
instrument "search.net_ldap_connection" ,
360
- :filter => search_filter ,
361
- :base => search_base ,
362
- :scope => scope ,
363
- :limit => sizelimit ,
364
- :sort => sort_control ,
365
- :referrals => return_referrals ,
366
- :deref => deref ,
367
- :attributes => search_attributes do |payload |
422
+ message_id : message_id ,
423
+ filter : filter ,
424
+ base : base ,
425
+ scope : scope ,
426
+ size : size ,
427
+ time : time ,
428
+ sort : sort ,
429
+ referrals : refs ,
430
+ deref : deref ,
431
+ attributes : attrs do |payload |
368
432
loop do
369
433
# should collect this into a private helper to clarify the structure
370
434
query_limit = 0
371
- if sizelimit > 0
372
- if paged_searches_supported
373
- query_limit = ( ( ( sizelimit - n_results ) < 126 ) ? ( sizelimit -
435
+ if size > 0
436
+ if paged
437
+ query_limit = ( ( ( size - n_results ) < 126 ) ? ( size -
374
438
n_results ) : 0 )
375
439
else
376
- query_limit = sizelimit
440
+ query_limit = size
377
441
end
378
442
end
379
443
380
444
request = [
381
- search_base . to_ber ,
445
+ base . to_ber ,
382
446
scope . to_ber_enumerated ,
383
447
deref . to_ber_enumerated ,
384
448
query_limit . to_ber , # size limit
385
- 0 . to_ber ,
386
- attributes_only . to_ber ,
387
- search_filter . to_ber ,
388
- search_attributes . to_ber_sequence
449
+ time . to_ber ,
450
+ attrs_only . to_ber ,
451
+ filter . to_ber ,
452
+ ber_attrs . to_ber_sequence
389
453
] . to_ber_appsequence ( 3 )
390
454
391
455
# rfc2696_cookie sometimes contains binary data from Microsoft Active Directory
@@ -399,22 +463,22 @@ def search(args = {})
399
463
# Criticality MUST be false to interoperate with normal LDAPs.
400
464
false . to_ber ,
401
465
rfc2696_cookie . map { |v | v . to_ber } . to_ber_sequence . to_s . to_ber
402
- ] . to_ber_sequence if paged_searches_supported
403
- controls << sort_control if sort_control
466
+ ] . to_ber_sequence if paged
467
+ controls << ber_sort if ber_sort
404
468
controls = controls . empty? ? nil : controls . to_ber_contextspecific ( 0 )
405
469
406
- write ( request , controls )
470
+ write ( request , controls , message_id )
407
471
408
472
result_pdu = nil
409
473
controls = [ ]
410
474
411
- while pdu = read
475
+ while pdu = queued_read ( message_id )
412
476
case pdu . app_tag
413
477
when Net ::LDAP ::PDU ::SearchReturnedData
414
478
n_results += 1
415
479
yield pdu . search_entry if block_given?
416
480
when Net ::LDAP ::PDU ::SearchResultReferral
417
- if return_referrals
481
+ if refs
418
482
if block_given?
419
483
se = Net ::LDAP ::Entry . new
420
484
se [ :search_referrals ] = ( pdu . search_referrals || [ ] )
@@ -424,7 +488,7 @@ def search(args = {})
424
488
when Net ::LDAP ::PDU ::SearchResult
425
489
result_pdu = pdu
426
490
controls = pdu . result_controls
427
- if return_referrals && pdu . result_code == 10
491
+ if refs && pdu . result_code == 10
428
492
if block_given?
429
493
se = Net ::LDAP ::Entry . new
430
494
se [ :search_referrals ] = ( pdu . search_referrals || [ ] )
@@ -476,6 +540,16 @@ def search(args = {})
476
540
477
541
result_pdu || OpenStruct . new ( :status => :failure , :result_code => 1 , :message => "Invalid search" )
478
542
end # instrument
543
+ ensure
544
+ # clean up message queue for this search
545
+ messages = message_queue . delete ( message_id )
546
+
547
+ # in the exceptional case some messages were *not* consumed from the queue,
548
+ # instrument the event but do not fail.
549
+ unless messages . empty?
550
+ instrument "search_messages_unread.net_ldap_connection" ,
551
+ message_id : message_id , messages : messages
552
+ end
479
553
end
480
554
481
555
MODIFY_OPERATIONS = { #:nodoc:
0 commit comments