diff --git a/lib/net/ldap/connection.rb b/lib/net/ldap/connection.rb index cc74a170..91fb7e90 100644 --- a/lib/net/ldap/connection.rb +++ b/lib/net/ldap/connection.rb @@ -87,10 +87,18 @@ def setup_encryption(args) # additional branches requiring server validation and peer certs, etc. # go here. when :start_tls - request = [Net::LDAP::StartTlsOid.to_ber_contextspecific(0)].to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest) - write(request) - pdu = read - raise Net::LDAP::LdapError, "no start_tls result" if pdu.nil? + message_id = next_msgid + request = [ + Net::LDAP::StartTlsOid.to_ber_contextspecific(0) + ].to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest) + + write(request, nil, message_id) + pdu = queued_read(message_id) + + if pdu.nil? || pdu.app_tag != Net::LDAP::PDU::ExtendedResponse + raise Net::LDAP::LdapError, "no start_tls result" + end + if pdu.result_code.zero? @conn = self.class.wrap_with_ssl(@conn) else @@ -226,12 +234,18 @@ def bind_simple(auth) raise Net::LDAP::LdapError, "Invalid binding information" unless (user && psw) - request = [LdapVersion.to_ber, user.to_ber, - psw.to_ber_contextspecific(0)].to_ber_appsequence(Net::LDAP::PDU::BindRequest) - write(request) + message_id = next_msgid + request = [ + LdapVersion.to_ber, user.to_ber, + psw.to_ber_contextspecific(0) + ].to_ber_appsequence(Net::LDAP::PDU::BindRequest) - pdu = read - raise Net::LDAP::LdapError, "no bind result" unless pdu + write(request, nil, message_id) + pdu = queued_read(message_id) + + if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult + raise Net::LDAP::LdapError, "no bind result" + end pdu end @@ -262,14 +276,21 @@ def bind_sasl(auth) auth[:challenge_response] raise Net::LDAP::LdapError, "Invalid binding information" unless (mech && cred && chall) + message_id = next_msgid + n = 0 loop { sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3) - request = [LdapVersion.to_ber, "".to_ber, sasl].to_ber_appsequence(Net::LDAP::PDU::BindRequest) - write(request) + request = [ + LdapVersion.to_ber, "".to_ber, sasl + ].to_ber_appsequence(Net::LDAP::PDU::BindRequest) - pdu = read - raise Net::LDAP::LdapError, "no bind result" unless pdu + write(request, nil, message_id) + pdu = queued_read(message_id) + + if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult + raise Net::LDAP::LdapError, "no bind result" + end return pdu unless pdu.result_code == Net::LDAP::ResultCodeSaslBindInProgress raise Net::LDAP::LdapError, "sasl-challenge overflow" if ((n += 1) > MaxSaslChallenges) @@ -583,11 +604,15 @@ def self.modify_ops(operations) def modify(args) modify_dn = args[:dn] or raise "Unable to modify empty DN" ops = self.class.modify_ops args[:operations] - request = [ modify_dn.to_ber, - ops.to_ber_sequence ].to_ber_appsequence(Net::LDAP::PDU::ModifyRequest) - write(request) - pdu = read + message_id = next_msgid + request = [ + modify_dn.to_ber, + ops.to_ber_sequence + ].to_ber_appsequence(Net::LDAP::PDU::ModifyRequest) + + write(request, nil, message_id) + pdu = queued_read(message_id) if !pdu || pdu.app_tag != Net::LDAP::PDU::ModifyResponse raise Net::LDAP::LdapError, "response missing or invalid" @@ -610,10 +635,11 @@ def add(args) add_attrs << [ k.to_s.to_ber, Array(v).map { |m| m.to_ber}.to_ber_set ].to_ber_sequence } - request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(Net::LDAP::PDU::AddRequest) - write(request) + message_id = next_msgid + request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(Net::LDAP::PDU::AddRequest) - pdu = read + write(request, nil, message_id) + pdu = queued_read(message_id) if !pdu || pdu.app_tag != Net::LDAP::PDU::AddResponse raise Net::LDAP::LdapError, "response missing or invalid" @@ -631,12 +657,12 @@ def rename(args) delete_attrs = args[:delete_attributes] ? true : false new_superior = args[:new_superior] - request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber] - request << new_superior.to_ber_contextspecific(0) unless new_superior == nil - - write(request.to_ber_appsequence(Net::LDAP::PDU::ModifyRDNRequest)) + message_id = next_msgid + request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber] + request << new_superior.to_ber_contextspecific(0) unless new_superior == nil - pdu = read + write(request.to_ber_appsequence(Net::LDAP::PDU::ModifyRDNRequest), nil, message_id) + pdu = queued_read(message_id) if !pdu || pdu.app_tag != Net::LDAP::PDU::ModifyRDNResponse raise Net::LDAP::LdapError.new "response missing or invalid" @@ -650,11 +676,12 @@ def rename(args) #++ def delete(args) dn = args[:dn] or raise "Unable to delete empty DN" - controls = args.include?(:control_codes) ? args[:control_codes].to_ber_control : nil #use nil so we can compact later - request = dn.to_s.to_ber_application_string(Net::LDAP::PDU::DeleteRequest) - write(request, controls) + controls = args.include?(:control_codes) ? args[:control_codes].to_ber_control : nil #use nil so we can compact later + message_id = next_msgid + request = dn.to_s.to_ber_application_string(Net::LDAP::PDU::DeleteRequest) - pdu = read + write(request, controls, message_id) + pdu = queued_read(message_id) if !pdu || pdu.app_tag != Net::LDAP::PDU::DeleteResponse raise Net::LDAP::LdapError, "response missing or invalid" diff --git a/test/integration/test_open.rb b/test/integration/test_open.rb index 6d86f49a..36724f5d 100644 --- a/test/integration/test_open.rb +++ b/test/integration/test_open.rb @@ -21,6 +21,10 @@ def test_binds_with_open assert_equal 1, events.size end + # NOTE: query for two or more entries so that the socket must be read + # multiple times. + # See The Problem: https://github.com/ruby-ldap/ruby-net-ldap/issues/136 + def test_nested_search_without_open entries = [] nested_entry = nil @@ -48,4 +52,37 @@ def test_nested_search_with_open assert_equal "user3", nested_entry.uid.first assert_equal %w(user1 user2), entries end + + def test_nested_add_with_open + entries = [] + nested_entry = nil + + dn = "uid=nested-open-added-user1,ou=People,dc=rubyldap,dc=com" + attrs = { + objectclass: %w(top inetOrgPerson organizationalPerson person), + uid: "nested-open-added-user1", + cn: "nested-open-added-user1", + sn: "nested-open-added-user1", + mail: "nested-open-added-user1@rubyldap.com" + } + + @ldap.authenticate "cn=admin,dc=rubyldap,dc=com", "passworD1" + @ldap.delete dn: dn + + @ldap.open do + @ldap.search(filter: "(|(uid=user1)(uid=user2))", base: "ou=People,dc=rubyldap,dc=com") do |entry| + entries << entry.uid.first + + nested_entry ||= begin + assert @ldap.add(dn: dn, attributes: attrs), @ldap.get_operation_result.inspect + @ldap.search(base: dn, scope: Net::LDAP::SearchScope_BaseObject).first + end + end + end + + assert_equal %w(user1 user2), entries + assert_equal "nested-open-added-user1", nested_entry.uid.first + ensure + @ldap.delete dn: dn + end end diff --git a/test/test_ldap_connection.rb b/test/test_ldap_connection.rb index 46e535ff..56dfe813 100644 --- a/test/test_ldap_connection.rb +++ b/test/test_ldap_connection.rb @@ -67,6 +67,193 @@ def test_write_increments_msgid end end +class TestLDAPConnectionSocketReads < Test::Unit::TestCase + def make_message(message_id, options = {}) + options = { + app_tag: Net::LDAP::PDU::SearchResult, + code: Net::LDAP::ResultCodeSuccess, + matched_dn: "", + error_message: "" + }.merge(options) + result = Net::BER::BerIdentifiedArray.new([options[:code], options[:matched_dn], options[:error_message]]) + result.ber_identifier = options[:app_tag] + [message_id, result] + end + + def test_queued_read_drains_queue_before_read + result1a = make_message(1, error_message: "one") + result1b = make_message(1, error_message: "two") + + mock = flexmock("socket") + mock.should_receive(:read_ber).and_return(result1b) + conn = Net::LDAP::Connection.new(:socket => mock) + + conn.message_queue[1].push Net::LDAP::PDU.new(result1a) + + assert msg1 = conn.queued_read(1) + assert msg2 = conn.queued_read(1) + + assert_equal 1, msg1.message_id + assert_equal "one", msg1.error_message + assert_equal 1, msg2.message_id + assert_equal "two", msg2.error_message + end + + def test_queued_read_reads_until_message_id_match + result1 = make_message(1) + result2 = make_message(2) + + mock = flexmock("socket") + mock.should_receive(:read_ber). + and_return(result1). + and_return(result2) + conn = Net::LDAP::Connection.new(:socket => mock) + + assert result = conn.queued_read(2) + assert_equal 2, result.message_id + assert_equal 1, conn.queued_read(1).message_id + end + + def test_queued_read_modify + result1 = make_message(1, app_tag: Net::LDAP::PDU::SearchResult) + result2 = make_message(2, app_tag: Net::LDAP::PDU::ModifyResponse) + + mock = flexmock("socket") + mock.should_receive(:read_ber). + and_return(result1). + and_return(result2) + mock.should_receive(:write) + conn = Net::LDAP::Connection.new(:socket => mock) + + conn.next_msgid # simulates ongoing query + + conn.instance_variable_get("@msgid") + + assert result = conn.modify(dn: "uid=modified-user1,ou=People,dc=rubyldap,dc=com", + operations: [[:add, :mail, "modified-user1@example.com"]]) + assert result.success? + assert_equal 2, result.message_id + end + + def test_queued_read_add + result1 = make_message(1, app_tag: Net::LDAP::PDU::SearchResult) + result2 = make_message(2, app_tag: Net::LDAP::PDU::AddResponse) + + mock = flexmock("socket") + mock.should_receive(:read_ber). + and_return(result1). + and_return(result2) + mock.should_receive(:write) + conn = Net::LDAP::Connection.new(:socket => mock) + + conn.next_msgid # simulates ongoing query + + assert result = conn.add(dn: "uid=added-user1,ou=People,dc=rubyldap,dc=com") + assert result.success? + assert_equal 2, result.message_id + end + + def test_queued_read_rename + result1 = make_message(1, app_tag: Net::LDAP::PDU::SearchResult) + result2 = make_message(2, app_tag: Net::LDAP::PDU::ModifyRDNResponse) + + mock = flexmock("socket") + mock.should_receive(:read_ber). + and_return(result1). + and_return(result2) + mock.should_receive(:write) + conn = Net::LDAP::Connection.new(:socket => mock) + + conn.next_msgid # simulates ongoing query + + assert result = conn.rename( + olddn: "uid=renamable-user1,ou=People,dc=rubyldap,dc=com", + newrdn: "uid=renamed-user1" + ) + assert result.success? + assert_equal 2, result.message_id + end + + def test_queued_read_delete + result1 = make_message(1, app_tag: Net::LDAP::PDU::SearchResult) + result2 = make_message(2, app_tag: Net::LDAP::PDU::DeleteResponse) + + mock = flexmock("socket") + mock.should_receive(:read_ber). + and_return(result1). + and_return(result2) + mock.should_receive(:write) + conn = Net::LDAP::Connection.new(:socket => mock) + + conn.next_msgid # simulates ongoing query + + assert result = conn.delete(dn: "uid=deletable-user1,ou=People,dc=rubyldap,dc=com") + assert result.success? + assert_equal 2, result.message_id + end + + def test_queued_read_setup_encryption_with_start_tls + result1 = make_message(1, app_tag: Net::LDAP::PDU::SearchResult) + result2 = make_message(2, app_tag: Net::LDAP::PDU::ExtendedResponse) + + mock = flexmock("socket") + mock.should_receive(:read_ber). + and_return(result1). + and_return(result2) + mock.should_receive(:write) + conn = Net::LDAP::Connection.new(:socket => mock) + flexmock(Net::LDAP::Connection).should_receive(:wrap_with_ssl).with(mock). + and_return(mock) + + conn.next_msgid # simulates ongoing query + + assert result = conn.setup_encryption(method: :start_tls) + assert_equal mock, result + end + + def test_queued_read_bind_simple + result1 = make_message(1, app_tag: Net::LDAP::PDU::SearchResult) + result2 = make_message(2, app_tag: Net::LDAP::PDU::BindResult) + + mock = flexmock("socket") + mock.should_receive(:read_ber). + and_return(result1). + and_return(result2) + mock.should_receive(:write) + conn = Net::LDAP::Connection.new(:socket => mock) + + conn.next_msgid # simulates ongoing query + + assert result = conn.bind( + method: :simple, + username: "uid=user1,ou=People,dc=rubyldap,dc=com", + password: "passworD1") + assert result.success? + assert_equal 2, result.message_id + end + + def test_queued_read_bind_sasl + result1 = make_message(1, app_tag: Net::LDAP::PDU::SearchResult) + result2 = make_message(2, app_tag: Net::LDAP::PDU::BindResult) + + mock = flexmock("socket") + mock.should_receive(:read_ber). + and_return(result1). + and_return(result2) + mock.should_receive(:write) + conn = Net::LDAP::Connection.new(:socket => mock) + + conn.next_msgid # simulates ongoing query + + assert result = conn.bind( + method: :sasl, + mechanism: "fake", + initial_credential: "passworD1", + challenge_response: flexmock("challenge proc")) + assert result.success? + assert_equal 2, result.message_id + end +end class TestLDAPConnectionErrors < Test::Unit::TestCase def setup @@ -79,7 +266,7 @@ def setup def test_error_failed_operation ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeUnwillingToPerform, "", "The provided password value was rejected by a password validator: The provided password did not contain enough characters from the character set 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'. The minimum number of characters from that set that must be present in user passwords is 1"]) ber.ber_identifier = Net::LDAP::PDU::ModifyResponse - @tcp_socket.should_receive(:read_ber).and_return([2, ber]) + @tcp_socket.should_receive(:read_ber).and_return([1, ber]) result = @connection.modify(:dn => "1", :operations => [[:replace, "mail", "something@sothsdkf.com"]]) assert result.failure?, "should be failure" @@ -89,7 +276,7 @@ def test_error_failed_operation def test_no_error_on_success ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, "", ""]) ber.ber_identifier = Net::LDAP::PDU::ModifyResponse - @tcp_socket.should_receive(:read_ber).and_return([2, ber]) + @tcp_socket.should_receive(:read_ber).and_return([1, ber]) result = @connection.modify(:dn => "1", :operations => [[:replace, "mail", "something@sothsdkf.com"]]) assert result.success?, "should be success" @@ -113,7 +300,7 @@ def setup def test_write_net_ldap_connection_event ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, "", ""]) ber.ber_identifier = Net::LDAP::PDU::BindResult - read_result = [2, ber] + read_result = [1, ber] @tcp_socket.should_receive(:read_ber).and_return(read_result) events = @service.subscribe "write.net_ldap_connection" @@ -130,7 +317,7 @@ def test_write_net_ldap_connection_event def test_read_net_ldap_connection_event ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, "", ""]) ber.ber_identifier = Net::LDAP::PDU::BindResult - read_result = [2, ber] + read_result = [1, ber] @tcp_socket.should_receive(:read_ber).and_return(read_result) events = @service.subscribe "read.net_ldap_connection" @@ -147,7 +334,7 @@ def test_read_net_ldap_connection_event def test_parse_pdu_net_ldap_connection_event ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, "", ""]) ber.ber_identifier = Net::LDAP::PDU::BindResult - read_result = [2, ber] + read_result = [1, ber] @tcp_socket.should_receive(:read_ber).and_return(read_result) events = @service.subscribe "parse_pdu.net_ldap_connection" @@ -161,7 +348,7 @@ def test_parse_pdu_net_ldap_connection_event assert payload.has_key?(:app_tag) assert payload.has_key?(:message_id) assert_equal Net::LDAP::PDU::BindResult, payload[:app_tag] - assert_equal 2, payload[:message_id] + assert_equal 1, payload[:message_id] pdu = payload[:pdu] assert_equal Net::LDAP::ResultCodeSuccess, pdu.result_code end @@ -169,7 +356,7 @@ def test_parse_pdu_net_ldap_connection_event def test_bind_net_ldap_connection_event ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, "", ""]) ber.ber_identifier = Net::LDAP::PDU::BindResult - bind_result = [2, ber] + bind_result = [1, ber] @tcp_socket.should_receive(:read_ber).and_return(bind_result) events = @service.subscribe "bind.net_ldap_connection"
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: