From c16ae119ce8f42c43f0a653139e5d0b591197224 Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Sun, 30 Nov 2014 20:33:32 -0800 Subject: [PATCH 01/11] Extract read_ber_id --- lib/net/ber/ber_parser.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/net/ber/ber_parser.rb b/lib/net/ber/ber_parser.rb index 39d3737e..56bc7893 100644 --- a/lib/net/ber/ber_parser.rb +++ b/lib/net/ber/ber_parser.rb @@ -166,7 +166,7 @@ def read_ber(syntax = nil) # from streams that don't block when we ask for more data (like # StringIOs). At it is, this can throw TypeErrors and other nasties. - id = getbyte or return nil # don't trash this value, we'll use it later + id = read_ber_id or return nil # don't trash this value, we'll use it later content_length = read_ber_length yield id, content_length if block_given? @@ -179,4 +179,9 @@ def read_ber(syntax = nil) parse_ber_object(syntax, id, data) end + + # Internal: Returns the BER message ID or nil. + def read_ber_id + getbyte + end end From aec21d93c943650633f451c858c6d4764528c300 Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Sun, 30 Nov 2014 20:33:51 -0800 Subject: [PATCH 02/11] Implement read_ber_id with nonblocking read --- lib/net/ber/ber_parser.rb | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/net/ber/ber_parser.rb b/lib/net/ber/ber_parser.rb index 56bc7893..77d0a09e 100644 --- a/lib/net/ber/ber_parser.rb +++ b/lib/net/ber/ber_parser.rb @@ -25,6 +25,9 @@ module Net::BER::BERParser BuiltinSyntax = Net::BER.compile_syntax(:universal => universal, :context_specific => context) + # Public: specify the BER socket read timeouts, nil by default (no timeout). + attr_accessor :read_ber_timeout + ## # This is an extract of our BER object parsing to simplify our # understanding of how we parse basic BER object types. @@ -182,6 +185,17 @@ def read_ber(syntax = nil) # Internal: Returns the BER message ID or nil. def read_ber_id - getbyte + begin + read_nonblock(1).ord + rescue IO::WaitReadable + if IO.select([self], nil, nil, read_ber_timeout) + read_nonblock(1).ord + else + raise Net::LDAP::LdapError, "Timed out reading from the socket" + end + end + rescue EOFError + # nothing to read on the socket (StringIO) + nil end end From df3b4b55fa9c5201db74b497b98ef9f67865ca02 Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Sun, 30 Nov 2014 20:39:03 -0800 Subject: [PATCH 03/11] Extract getbyte_nonblock, use with read_ber_length --- lib/net/ber/ber_parser.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/net/ber/ber_parser.rb b/lib/net/ber/ber_parser.rb index 77d0a09e..7aa8602f 100644 --- a/lib/net/ber/ber_parser.rb +++ b/lib/net/ber/ber_parser.rb @@ -136,7 +136,7 @@ def parse_ber_object(syntax, id, data) # invalid BER length case. Because the "lengthlength" value was not used # inside of #read_ber, we no longer return it. def read_ber_length - n = getbyte + n = getbyte_nonblock if n <= 0x7f n @@ -185,6 +185,11 @@ def read_ber(syntax = nil) # Internal: Returns the BER message ID or nil. def read_ber_id + getbyte_nonblock + end + + # Internal: Replaces `getbyte` with nonblocking implementation. + def getbyte_nonblock begin read_nonblock(1).ord rescue IO::WaitReadable From 5073c6b553c624ff49f3268842827fda95cc342b Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Sun, 30 Nov 2014 20:45:18 -0800 Subject: [PATCH 04/11] Read length nonblocking --- lib/net/ber/ber_parser.rb | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/net/ber/ber_parser.rb b/lib/net/ber/ber_parser.rb index 7aa8602f..dfb60b6c 100644 --- a/lib/net/ber/ber_parser.rb +++ b/lib/net/ber/ber_parser.rb @@ -146,7 +146,20 @@ def read_ber_length raise Net::BER::BerError, "Invalid BER length 0xFF detected." else v = 0 - read(n & 0x7f).each_byte do |b| + len = n & 0x7f + + buffer = + begin + read_nonblock(len) + rescue IO::WaitReadable + if IO.select([self], nil, nil, read_ber_timeout) + read_nonblock(len) + else + raise Net::LDAP::LdapError, "Timed out reading from the socket" + end + end + + buffer.each_byte do |b| v = (v << 8) + b end From 22197b4ddd6fac39f90ddc18a0493ce81096d41d Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Sun, 30 Nov 2014 20:45:27 -0800 Subject: [PATCH 05/11] Make internal methods private --- lib/net/ber/ber_parser.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/net/ber/ber_parser.rb b/lib/net/ber/ber_parser.rb index dfb60b6c..cb7442be 100644 --- a/lib/net/ber/ber_parser.rb +++ b/lib/net/ber/ber_parser.rb @@ -200,6 +200,7 @@ def read_ber(syntax = nil) def read_ber_id getbyte_nonblock end + private :read_ber_id # Internal: Replaces `getbyte` with nonblocking implementation. def getbyte_nonblock @@ -216,4 +217,5 @@ def getbyte_nonblock # nothing to read on the socket (StringIO) nil end + private :getbyte_nonblock end From cc2ba712fcd98514d8fc8fbdc80ec8bb3c841881 Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Sun, 30 Nov 2014 21:53:38 -0800 Subject: [PATCH 06/11] Extract read_ber_nonblock, use for all reads --- lib/net/ber/ber_parser.rb | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/net/ber/ber_parser.rb b/lib/net/ber/ber_parser.rb index cb7442be..ba23503f 100644 --- a/lib/net/ber/ber_parser.rb +++ b/lib/net/ber/ber_parser.rb @@ -146,23 +146,9 @@ def read_ber_length raise Net::BER::BerError, "Invalid BER length 0xFF detected." else v = 0 - len = n & 0x7f - - buffer = - begin - read_nonblock(len) - rescue IO::WaitReadable - if IO.select([self], nil, nil, read_ber_timeout) - read_nonblock(len) - else - raise Net::LDAP::LdapError, "Timed out reading from the socket" - end - end - - buffer.each_byte do |b| + read_ber_nonblock(n & 0x7f).each_byte do |b| v = (v << 8) + b end - v end end @@ -191,7 +177,7 @@ def read_ber(syntax = nil) raise Net::BER::BerError, "Indeterminite BER content length not implemented." end - data = read(content_length) + data = read_ber_nonblock(content_length) parse_ber_object(syntax, id, data) end @@ -218,4 +204,18 @@ def getbyte_nonblock nil end private :getbyte_nonblock + + # Internal: Read `len` bytes, respecting timeout. + def read_ber_nonblock(len) + begin + read_nonblock(len) + rescue IO::WaitReadable + if IO.select([self], nil, nil, read_ber_timeout) + read_nonblock(len) + else + raise Net::LDAP::LdapError, "Timed out reading from the socket" + end + end + end + private :read_ber_nonblock end From 92410273b3e037ac39bb11725688b0091793336b Mon Sep 17 00:00:00 2001 From: Matt Todd Date: Sun, 30 Nov 2014 22:16:58 -0800 Subject: [PATCH 07/11] Configure socket read timeouts --- lib/net/ldap/connection.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/net/ldap/connection.rb b/lib/net/ldap/connection.rb index b01984f4..9206e81b 100644 --- a/lib/net/ldap/connection.rb +++ b/lib/net/ldap/connection.rb @@ -67,6 +67,9 @@ def open_connection(server) close errors << [e, host, port] end + if server[:timeout] + @conn.read_ber_timeout = server[:timeout] + end end raise Net::LDAP::ConnectionError.new(errors) From c740a20a568c71a0d9ee6ea313319038062dbc48 Mon Sep 17 00:00:00 2001 From: John Wedoff Date: Thu, 11 Apr 2019 11:16:59 -0400 Subject: [PATCH 08/11] Add support for timeouts on read and write operations --- Contributors.rdoc | 1 + lib/net/ber/ber_parser.rb | 125 +++++++++--- lib/net/ldap.rb | 17 +- lib/net/ldap/connection.rb | 234 ++++++++++++----------- test/integration/test_password_modify.rb | 2 +- test/test_auth_adapter.rb | 1 + test/test_ldap.rb | 4 + test/test_ldap_connection.rb | 32 ++-- test/test_search.rb | 4 + 9 files changed, 271 insertions(+), 149 deletions(-) diff --git a/Contributors.rdoc b/Contributors.rdoc index 137394f8..820ba8e1 100644 --- a/Contributors.rdoc +++ b/Contributors.rdoc @@ -23,3 +23,4 @@ Contributions since: * Cody Cutrer (ccutrer) * WoodsBagotAndreMarquesLee * Rufus Post (mynameisrufus) +* Akamai Technologies, Inc. (jwedoff) diff --git a/lib/net/ber/ber_parser.rb b/lib/net/ber/ber_parser.rb index ba23503f..08787374 100644 --- a/lib/net/ber/ber_parser.rb +++ b/lib/net/ber/ber_parser.rb @@ -136,7 +136,7 @@ def parse_ber_object(syntax, id, data) # invalid BER length case. Because the "lengthlength" value was not used # inside of #read_ber, we no longer return it. def read_ber_length - n = getbyte_nonblock + n = ber_timeout_getbyte if n <= 0x7f n @@ -146,7 +146,7 @@ def read_ber_length raise Net::BER::BerError, "Invalid BER length 0xFF detected." else v = 0 - read_ber_nonblock(n & 0x7f).each_byte do |b| + ber_timeout_read(n & 0x7f).each_byte do |b| v = (v << 8) + b end v @@ -177,45 +177,126 @@ def read_ber(syntax = nil) raise Net::BER::BerError, "Indeterminite BER content length not implemented." end - data = read_ber_nonblock(content_length) + data = ber_timeout_read(content_length) parse_ber_object(syntax, id, data) end # Internal: Returns the BER message ID or nil. def read_ber_id - getbyte_nonblock + ber_timeout_getbyte end private :read_ber_id + # Internal: specify the BER socket read timeouts, nil by default (no timeout). + attr_accessor :ber_io_deadline + private :ber_io_deadline + + ## + # sets a timeout of timeout seconds for read_ber and ber_timeout_write operations in the provided block the proin the future for if there is not already a earlier deadline set + def with_timeout(timeout) + timeout = timeout.to_f + # don't change deadline if run without timeout + return yield if timeout <= 0 + # clear deadline if it is not in the future + self.ber_io_deadline = nil unless ber_io_timeout&.send(:>, 0) + new_deadline = Time.now + timeout + # don't add deadline if current deadline is shorter + return yield if ber_io_deadline && ber_io_deadline < new_deadline + old_deadline = ber_io_deadline + begin + self.ber_io_deadline = new_deadline + yield + ensure + self.ber_io_deadline = old_deadline + end + end + + # seconds until ber_io_deadline + def ber_io_timeout + ber_io_deadline ? ber_io_deadline - Time.now : nil + end + private :ber_io_timeout + + def read_select! + return if IO.select([self], nil, nil, ber_io_timeout) + raise Net::LDAP::LdapError, "Timed out reading from the socket" + end + private :read_select! + + def write_select! + return if IO.select(nil, [self], nil, ber_io_timeout) + raise Net::LDAP::LdapError, "Timed out reading from the socket" + end + private :write_select! + # Internal: Replaces `getbyte` with nonblocking implementation. - def getbyte_nonblock + def ber_timeout_getbyte begin read_nonblock(1).ord rescue IO::WaitReadable - if IO.select([self], nil, nil, read_ber_timeout) - read_nonblock(1).ord - else - raise Net::LDAP::LdapError, "Timed out reading from the socket" - end + read_select! + retry + rescue IO::WaitWritable + write_select! + retry + rescue EOFError + # nothing to read on the socket (StringIO) + nil end - rescue EOFError - # nothing to read on the socket (StringIO) - nil end - private :getbyte_nonblock + private :ber_timeout_getbyte # Internal: Read `len` bytes, respecting timeout. - def read_ber_nonblock(len) + def ber_timeout_read(len) + buffer ||= ''.force_encoding(Encoding::ASCII_8BIT) begin - read_nonblock(len) - rescue IO::WaitReadable - if IO.select([self], nil, nil, read_ber_timeout) - read_nonblock(len) - else - raise Net::LDAP::LdapError, "Timed out reading from the socket" + read_nonblock(len, buffer) + return buffer if buffer.bytesize >= len + rescue IO::WaitReadable, IO::WaitWritable + buffer.clear + rescue EOFError + # nothing to read on the socket (StringIO) + nil + end + block ||= ''.force_encoding(Encoding::ASCII_8BIT) + len -= buffer.bytesize + loop do + begin + read_nonblock(len, block) + rescue IO::WaitReadable + read_select! + retry + rescue IO::WaitWritable + write_select! + retry + rescue EOFError + return buffer.empty? ? nil : buffer + end + buffer << block + len -= block.bytesize + return buffer if len <= 0 + end + end + private :ber_timeout_read + + ## + # Writes val as a plain write would, but respecting the dealine set by with_timeout + def ber_timeout_write(val) + total_written = 0 + while 0 < val.bytesize + begin + written = write_nonblock(val) + rescue IO::WaitReadable + read_select! + retry + rescue IO::WaitWritable + write_select! + retry end + total_written += written + val = val.byteslice(written..-1) end + total_written end - private :read_ber_nonblock end diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index f7a98ef5..d929cd0e 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -553,6 +553,7 @@ def initialize(args = {}) @force_no_page = args[:force_no_page] || DefaultForceNoPage @encryption = normalize_encryption(args[:encryption]) # may be nil @connect_timeout = args[:connect_timeout] + @io_timeout = args[:io_timeout] if pr = @auth[:password] and pr.respond_to?(:call) @auth[:password] = pr.call @@ -1293,14 +1294,19 @@ def connection=(connection) # result from that, and :use_connection: will not yield at all. If not # the return value is whatever is returned from the block. def use_connection(args) + timeout_args = args.slice(:io_timeout).values if @open_connection - yield @open_connection + @open_connection.with_timeout(*timeout_args) do + yield(@open_connection) + end else begin conn = new_connection - result = conn.bind(args[:auth] || @auth) - return result unless result.result_code == Net::LDAP::ResultCodeSuccess - yield conn + conn.with_timeout(*timeout_args) do + result = conn.bind(args[:auth] || @auth) + return result unless result.result_code == Net::LDAP::ResultCodeSuccess + yield(conn) + end ensure conn.close if conn end @@ -1315,7 +1321,8 @@ def new_connection :hosts => @hosts, :encryption => @encryption, :instrumentation_service => @instrumentation_service, - :connect_timeout => @connect_timeout + :connect_timeout => @connect_timeout, + :io_timeout => @io_timeout # Force connect to see if there's a connection error connection.socket diff --git a/lib/net/ldap/connection.rb b/lib/net/ldap/connection.rb index 9206e81b..552a7e7d 100644 --- a/lib/net/ldap/connection.rb +++ b/lib/net/ldap/connection.rb @@ -4,7 +4,7 @@ class Net::LDAP::Connection #:nodoc: include Net::LDAP::Instrumentation # Seconds before failing for socket connect timeout - DefaultConnectTimeout = 5 + DefaultTimeout = 5 LdapVersion = 3 @@ -38,11 +38,17 @@ def prepare_socket(server, timeout=nil) setup_encryption(encryption, timeout) if encryption end + # Internal: simple private method that can be replaced, if necessary, to allow this warning to be modified + def ssl_verify_warning(host, port) + warn "not verifying SSL hostname of LDAPS server '#{host}:#{port}'" + end + private :ssl_verify_warning + def open_connection(server) hosts = server[:hosts] encryption = server[:encryption] - timeout = server[:connect_timeout] || DefaultConnectTimeout + timeout = server[:connect_timeout] || DefaultTimeout socket_opts = { connect_timeout: timeout, } @@ -55,7 +61,7 @@ def open_connection(server) if encryption[:tls_options] && encryption[:tls_options][:verify_mode] && encryption[:tls_options][:verify_mode] == OpenSSL::SSL::VERIFY_NONE - warn "not verifying SSL hostname of LDAPS server '#{host}:#{port}'" + ssl_verify_warning_handler(host, port) else @conn.post_connection_check(host) end @@ -67,9 +73,6 @@ def open_connection(server) close errors << [e, host, port] end - if server[:timeout] - @conn.read_ber_timeout = server[:timeout] - end end raise Net::LDAP::ConnectionError.new(errors) @@ -264,7 +267,7 @@ def read(syntax = Net::LDAP::AsnSyntax) def write(request, controls = nil, message_id = next_msgid) instrument "write.net_ldap_connection" do |payload| packet = [message_id.to_ber, request, controls].compact.to_ber_sequence - payload[:content_length] = socket.write(packet) + payload[:content_length] = socket.ber_timeout_write(packet) end end private :write @@ -274,11 +277,19 @@ def next_msgid @msgid += 1 end + ## + # calls with_timeout on socket, with timeout defaulting to the :io_timeout parameter + def with_timeout(timeout=@server[:io_timeout], &block) + socket.with_timeout(timeout, &block) + end + def bind(auth) - instrument "bind.net_ldap_connection" do |payload| - payload[:method] = meth = auth[:method] - adapter = Net::LDAP::AuthAdapter[meth] - adapter.new(self).bind(auth) + with_timeout do + instrument "bind.net_ldap_connection" do |payload| + payload[:method] = meth = auth[:method] + adapter = Net::LDAP::AuthAdapter[meth] + adapter.new(self).bind(auth) + end end end @@ -388,6 +399,12 @@ def search(args = nil) message_id = next_msgid + timeout = if time > 0 + time + 0.5 #give remote server half a second to respond before killing connection + else + @server[:io_timeout] + end + instrument "search.net_ldap_connection", message_id: message_id, filter: filter, @@ -399,115 +416,116 @@ def search(args = nil) referrals: refs, deref: deref, attributes: attrs do |payload| - loop do - # should collect this into a private helper to clarify the structure - query_limit = 0 - if size > 0 - query_limit = if paged - (((size - n_results) < 126) ? (size - n_results) : 0) - else - size - end - end + with_timeout(timeout) do + loop do + # should collect this into a private helper to clarify the structure + query_limit = 0 + if size > 0 + query_limit = if paged + (((size - n_results) < 126) ? (size - n_results) : 0) + else + size + end + end - request = [ - base.to_ber, - scope.to_ber_enumerated, - deref.to_ber_enumerated, - query_limit.to_ber, # size limit - time.to_ber, - attrs_only.to_ber, - filter.to_ber, - ber_attrs.to_ber_sequence, - ].to_ber_appsequence(Net::LDAP::PDU::SearchRequest) - - # rfc2696_cookie sometimes contains binary data from Microsoft Active Directory - # this breaks when calling to_ber. (Can't force binary data to UTF-8) - # we have to disable paging (even though server supports it) to get around this... - - controls = [] - controls << - [ - Net::LDAP::LDAPControls::PAGED_RESULTS.to_ber, - # Criticality MUST be false to interoperate with normal LDAPs. - false.to_ber, - rfc2696_cookie.map(&:to_ber).to_ber_sequence.to_s.to_ber, - ].to_ber_sequence if paged - controls << ber_sort if ber_sort - controls = controls.empty? ? nil : controls.to_ber_contextspecific(0) - - write(request, controls, message_id) - - result_pdu = nil - controls = [] - - while pdu = queued_read(message_id) - case pdu.app_tag - when Net::LDAP::PDU::SearchReturnedData - n_results += 1 - yield pdu.search_entry if block_given? - when Net::LDAP::PDU::SearchResultReferral - if refs - if block_given? - se = Net::LDAP::Entry.new - se[:search_referrals] = (pdu.search_referrals || []) - yield se + request = [ + base.to_ber, + scope.to_ber_enumerated, + deref.to_ber_enumerated, + query_limit.to_ber, # size limit + time.to_ber, + attrs_only.to_ber, + filter.to_ber, + ber_attrs.to_ber_sequence, + ].to_ber_appsequence(Net::LDAP::PDU::SearchRequest) + + # rfc2696_cookie sometimes contains binary data from Microsoft Active Directory + # this breaks when calling to_ber. (Can't force binary data to UTF-8) + # we have to disable paging (even though server supports it) to get around this... + + controls = [] + controls << + [ + Net::LDAP::LDAPControls::PAGED_RESULTS.to_ber, + # Criticality MUST be false to interoperate with normal LDAPs. + false.to_ber, + rfc2696_cookie.map(&:to_ber).to_ber_sequence.to_s.to_ber, + ].to_ber_sequence if paged + controls << ber_sort if ber_sort + controls = controls.empty? ? nil : controls.to_ber_contextspecific(0) + + write(request, controls, message_id) + + result_pdu = nil + controls = [] + + while pdu = queued_read(message_id) + case pdu.app_tag + when Net::LDAP::PDU::SearchReturnedData + n_results += 1 + yield pdu.search_entry if block_given? + when Net::LDAP::PDU::SearchResultReferral + if refs + if block_given? + se = Net::LDAP::Entry.new + se[:search_referrals] = (pdu.search_referrals || []) + yield se + end end - end - when Net::LDAP::PDU::SearchResult - result_pdu = pdu - controls = pdu.result_controls - if refs && pdu.result_code == Net::LDAP::ResultCodeReferral - if block_given? - se = Net::LDAP::Entry.new - se[:search_referrals] = (pdu.search_referrals || []) - yield se + when Net::LDAP::PDU::SearchResult + result_pdu = pdu + controls = pdu.result_controls + if refs && pdu.result_code == Net::LDAP::ResultCodeReferral + if block_given? + se = Net::LDAP::Entry.new + se[:search_referrals] = (pdu.search_referrals || []) + yield se + end end + break + else + raise Net::LDAP::ResponseTypeInvalidError, "invalid response-type in search: #{pdu.app_tag}" end - break - else - raise Net::LDAP::ResponseTypeInvalidError, "invalid response-type in search: #{pdu.app_tag}" end - end - if result_pdu.nil? - raise Net::LDAP::ResponseMissingOrInvalidError, "response missing" - end + if result_pdu.nil? + raise Net::LDAP::ResponseMissingOrInvalidError, "response missing" + end - # count number of pages of results - payload[:page_count] ||= 0 - payload[:page_count] += 1 - - # When we get here, we have seen a type-5 response. If there is no - # error AND there is an RFC-2696 cookie, then query again for the next - # page of results. If not, we're done. Don't screw this up or we'll - # break every search we do. - # - # Noticed 02Sep06, look at the read_ber call in this loop, shouldn't - # that have a parameter of AsnSyntax? Does this just accidentally - # work? According to RFC-2696, the value expected in this position is - # of type OCTET STRING, covered in the default syntax supported by - # read_ber, so I guess we're ok. - more_pages = false - if result_pdu.result_code == Net::LDAP::ResultCodeSuccess and controls - controls.each do |c| - if c.oid == Net::LDAP::LDAPControls::PAGED_RESULTS - # just in case some bogus server sends us more than 1 of these. - more_pages = false - if c.value and c.value.length > 0 - cookie = c.value.read_ber[1] - if cookie and cookie.length > 0 - rfc2696_cookie[1] = cookie - more_pages = true + # count number of pages of results + payload[:page_count] ||= 0 + payload[:page_count] += 1 + + # When we get here, we have seen a type-5 response. If there is no + # error AND there is an RFC-2696 cookie, then query again for the next + # page of results. If not, we're done. Don't screw this up or we'll + # break every search we do. + # + # Noticed 02Sep06, look at the read_ber call in this loop, shouldn't + # that have a parameter of AsnSyntax? Does this just accidentally + # work? According to RFC-2696, the value expected in this position is + # of type OCTET STRING, covered in the default syntax supported by + # read_ber, so I guess we're ok. + more_pages = false + if result_pdu.result_code == Net::LDAP::ResultCodeSuccess and controls + controls.each do |c| + if c.oid == Net::LDAP::LDAPControls::PAGED_RESULTS + # just in case some bogus server sends us more than 1 of these. + more_pages = false + if c.value and c.value.length > 0 + cookie = c.value.read_ber[1] + if cookie and cookie.length > 0 + rfc2696_cookie[1] = cookie + more_pages = true + end end end end end - end - - break unless more_pages - end # loop + break unless more_pages + end # loop + end # with_timeout # track total result count payload[:result_count] = n_results diff --git a/test/integration/test_password_modify.rb b/test/integration/test_password_modify.rb index ed8d4f5b..db1a00a7 100644 --- a/test/integration/test_password_modify.rb +++ b/test/integration/test_password_modify.rb @@ -3,7 +3,7 @@ class TestPasswordModifyIntegration < LDAPIntegrationTestCase def setup super - @admin_account = {dn: 'cn=admin,dc=rubyldap,dc=com', password: 'passworD1', method: :simple} + @admin_account = { dn: 'cn=admin,dc=rubyldap,dc=com', password: 'passworD1', method: :simple } @ldap.authenticate @admin_account[:dn], @admin_account[:password] @dn = 'uid=modify-password-user1,ou=People,dc=rubyldap,dc=com' diff --git a/test/test_auth_adapter.rb b/test/test_auth_adapter.rb index 9e4c6002..fadf2a76 100644 --- a/test/test_auth_adapter.rb +++ b/test/test_auth_adapter.rb @@ -2,6 +2,7 @@ class TestAuthAdapter < Test::Unit::TestCase class FakeSocket + include Net::BER::BERParser def initialize(*args) end end diff --git a/test/test_ldap.rb b/test/test_ldap.rb index 8d6a9a72..9510ee19 100644 --- a/test/test_ldap.rb +++ b/test/test_ldap.rb @@ -21,6 +21,10 @@ def search(*args) yield @search_success if block_given? @search_success end + + def with_timeout(timeout = nil, &block) + yield + end end def setup diff --git a/test/test_ldap_connection.rb b/test/test_ldap_connection.rb index 8489c377..5cb63bb3 100644 --- a/test/test_ldap_connection.rb +++ b/test/test_ldap_connection.rb @@ -132,22 +132,25 @@ def test_modify_ops_replace def test_write mock = flexmock("socket") - mock.should_receive(:write).with([1.to_ber, "request"].to_ber_sequence).and_return(true) + mock.extend(Net::BER::BERParser) + mock.should_receive(:ber_timeout_write).with([1.to_ber, "request"].to_ber_sequence).and_return(true) conn = Net::LDAP::Connection.new(:socket => mock) conn.send(:write, "request") end def test_write_with_controls mock = flexmock("socket") - mock.should_receive(:write).with([1.to_ber, "request", "controls"].to_ber_sequence).and_return(true) + mock.extend(Net::BER::BERParser) + mock.should_receive(:ber_timeout_write).with([1.to_ber, "request", "controls"].to_ber_sequence).and_return(true) conn = Net::LDAP::Connection.new(:socket => mock) conn.send(:write, "request", "controls") end def test_write_increments_msgid mock = flexmock("socket") - mock.should_receive(:write).with([1.to_ber, "request1"].to_ber_sequence).and_return(true) - mock.should_receive(:write).with([2.to_ber, "request2"].to_ber_sequence).and_return(true) + mock.extend(Net::BER::BERParser) + mock.should_receive(:ber_timeout_write).with([1.to_ber, "request1"].to_ber_sequence).and_return(true) + mock.should_receive(:ber_timeout_write).with([2.to_ber, "request2"].to_ber_sequence).and_return(true) conn = Net::LDAP::Connection.new(:socket => mock) conn.send(:write, "request1") conn.send(:write, "request2") @@ -209,7 +212,7 @@ def test_queued_read_modify mock.should_receive(:read_ber). and_return(result1). and_return(result2) - mock.should_receive(:write) + mock.should_receive(:ber_timeout_write) conn = Net::LDAP::Connection.new(:socket => mock) conn.next_msgid # simulates ongoing query @@ -230,7 +233,7 @@ def test_queued_read_add mock.should_receive(:read_ber). and_return(result1). and_return(result2) - mock.should_receive(:write) + mock.should_receive(:ber_timeout_write) conn = Net::LDAP::Connection.new(:socket => mock) conn.next_msgid # simulates ongoing query @@ -248,7 +251,7 @@ def test_queued_read_rename mock.should_receive(:read_ber). and_return(result1). and_return(result2) - mock.should_receive(:write) + mock.should_receive(:ber_timeout_write) conn = Net::LDAP::Connection.new(:socket => mock) conn.next_msgid # simulates ongoing query @@ -269,7 +272,7 @@ def test_queued_read_delete mock.should_receive(:read_ber). and_return(result1). and_return(result2) - mock.should_receive(:write) + mock.should_receive(:ber_timeout_write) conn = Net::LDAP::Connection.new(:socket => mock) conn.next_msgid # simulates ongoing query @@ -287,7 +290,7 @@ def test_queued_read_setup_encryption_with_start_tls mock.should_receive(:read_ber). and_return(result1). and_return(result2) - mock.should_receive(:write) + mock.should_receive(:ber_timeout_write) conn = Net::LDAP::Connection.new(:socket => mock) flexmock(Net::LDAP::Connection).should_receive(:wrap_with_ssl).with(mock, {}, nil). and_return(mock) @@ -303,10 +306,11 @@ def test_queued_read_bind_simple result2 = make_message(2, app_tag: Net::LDAP::PDU::BindResult) mock = flexmock("socket") + mock.extend(Net::BER::BERParser) mock.should_receive(:read_ber). and_return(result1). and_return(result2) - mock.should_receive(:write) + mock.should_receive(:ber_timeout_write) conn = Net::LDAP::Connection.new(:socket => mock) conn.next_msgid # simulates ongoing query @@ -324,10 +328,11 @@ def test_queued_read_bind_sasl result2 = make_message(2, app_tag: Net::LDAP::PDU::BindResult) mock = flexmock("socket") + mock.extend(Net::BER::BERParser) mock.should_receive(:read_ber). and_return(result1). and_return(result2) - mock.should_receive(:write) + mock.should_receive(:ber_timeout_write) conn = Net::LDAP::Connection.new(:socket => mock) conn.next_msgid # simulates ongoing query @@ -345,7 +350,7 @@ def test_queued_read_bind_sasl class TestLDAPConnectionErrors < Test::Unit::TestCase def setup @tcp_socket = flexmock(:connection) - @tcp_socket.should_receive(:write) + @tcp_socket.should_receive(:ber_timeout_write) flexmock(Socket).should_receive(:tcp).and_return(@tcp_socket) @connection = Net::LDAP::Connection.new(:host => 'test.mocked.com', :port => 636) end @@ -374,7 +379,8 @@ def test_no_error_on_success class TestLDAPConnectionInstrumentation < Test::Unit::TestCase def setup @tcp_socket = flexmock(:connection) - @tcp_socket.should_receive(:write) + @tcp_socket.extend(Net::BER::BERParser) + @tcp_socket.should_receive(:ber_timeout_write) flexmock(Socket).should_receive(:tcp).and_return(@tcp_socket) @service = MockInstrumentationService.new diff --git a/test/test_search.rb b/test/test_search.rb index c577a6a2..ee0fcee7 100644 --- a/test/test_search.rb +++ b/test/test_search.rb @@ -6,6 +6,10 @@ class FakeConnection def search(args) OpenStruct.new(:result_code => Net::LDAP::ResultCodeOperationsError, :message => "error", :success? => false) end + + def with_timeout(timeout = nil, &block) + yield + end end def setup From 56513344672cb0e7f28dfdfd85cf95b6aec44271 Mon Sep 17 00:00:00 2001 From: jwedoff Date: Tue, 16 Apr 2019 10:54:18 -0400 Subject: [PATCH 09/11] fix ssl_verify_warning --- lib/net/ldap/connection.rb | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/net/ldap/connection.rb b/lib/net/ldap/connection.rb index 552a7e7d..b7810218 100644 --- a/lib/net/ldap/connection.rb +++ b/lib/net/ldap/connection.rb @@ -38,12 +38,6 @@ def prepare_socket(server, timeout=nil) setup_encryption(encryption, timeout) if encryption end - # Internal: simple private method that can be replaced, if necessary, to allow this warning to be modified - def ssl_verify_warning(host, port) - warn "not verifying SSL hostname of LDAPS server '#{host}:#{port}'" - end - private :ssl_verify_warning - def open_connection(server) hosts = server[:hosts] encryption = server[:encryption] @@ -61,7 +55,7 @@ def open_connection(server) if encryption[:tls_options] && encryption[:tls_options][:verify_mode] && encryption[:tls_options][:verify_mode] == OpenSSL::SSL::VERIFY_NONE - ssl_verify_warning_handler(host, port) + warn "not verifying SSL hostname of LDAPS server '#{host}:#{port}'" else @conn.post_connection_check(host) end From 9c28773e7559090d28abb5bf61d5abaed968400f Mon Sep 17 00:00:00 2001 From: jwedoff Date: Tue, 16 Apr 2019 11:02:30 -0400 Subject: [PATCH 10/11] don't use slice on hash --- lib/net/ldap.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index d929cd0e..7b86ffe0 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -1294,7 +1294,7 @@ def connection=(connection) # result from that, and :use_connection: will not yield at all. If not # the return value is whatever is returned from the block. def use_connection(args) - timeout_args = args.slice(:io_timeout).values + timeout_args = args.has_key?(:io_timeout) ? [args[:io_timeout]] : [] if @open_connection @open_connection.with_timeout(*timeout_args) do yield(@open_connection) From c6cef5c80cad9acf534b4c449b6de3b4a6307726 Mon Sep 17 00:00:00 2001 From: jwedoff Date: Tue, 16 Apr 2019 11:08:45 -0400 Subject: [PATCH 11/11] backwards compatible expressions --- lib/net/ber/ber_parser.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/net/ber/ber_parser.rb b/lib/net/ber/ber_parser.rb index 08787374..aa87b76c 100644 --- a/lib/net/ber/ber_parser.rb +++ b/lib/net/ber/ber_parser.rb @@ -199,7 +199,7 @@ def with_timeout(timeout) # don't change deadline if run without timeout return yield if timeout <= 0 # clear deadline if it is not in the future - self.ber_io_deadline = nil unless ber_io_timeout&.send(:>, 0) + self.ber_io_deadline = nil unless ber_io_timeout.to_f > 0 new_deadline = Time.now + timeout # don't add deadline if current deadline is shorter return yield if ber_io_deadline && ber_io_deadline < new_deadline