From cede61d2391b2e3746e18c0b0c0d963c5d01e02d Mon Sep 17 00:00:00 2001 From: Jeremy Bopp Date: Sun, 27 Sep 2015 22:00:50 -0500 Subject: [PATCH 1/2] Add the ability to provide a list of hosts to use when opening a connection --- lib/net/ldap.rb | 5 +++ lib/net/ldap/connection.rb | 68 ++++++++++++++++++++++++++---------- test/test_ldap_connection.rb | 38 ++++++++++++++++++++ 3 files changed, 93 insertions(+), 18 deletions(-) diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 635aa97d..ffb48719 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -432,6 +432,7 @@ def self.result2string(code) #:nodoc: attr_accessor :host attr_accessor :port + attr_accessor :hosts attr_accessor :base # Instantiate an object of type Net::LDAP to perform directory operations. @@ -440,6 +441,8 @@ def self.result2string(code) #:nodoc: # described below. The following arguments are supported: # * :host => the LDAP server's IP-address (default 127.0.0.1) # * :port => the LDAP server's TCP port (default 389) + # * :hosts => an enumerable of pairs of hosts and corresponding ports with + # which to attempt opening connections (default [[host, port]]) # * :auth => a Hash containing authorization parameters. Currently # supported values include: {:method => :anonymous} and {:method => # :simple, :username => your_user_name, :password => your_password } @@ -468,6 +471,7 @@ def self.result2string(code) #:nodoc: def initialize(args = {}) @host = args[:host] || DefaultHost @port = args[:port] || DefaultPort + @hosts = args[:hosts] @verbose = false # Make this configurable with a switch on the class. @auth = args[:auth] || DefaultAuth @base = args[:base] || DefaultTreebase @@ -1230,6 +1234,7 @@ def new_connection Net::LDAP::Connection.new \ :host => @host, :port => @port, + :hosts => @hosts, :encryption => @encryption, :instrumentation_service => @instrumentation_service end diff --git a/lib/net/ldap/connection.rb b/lib/net/ldap/connection.rb index 8e0e8c18..05aedfef 100644 --- a/lib/net/ldap/connection.rb +++ b/lib/net/ldap/connection.rb @@ -8,24 +8,56 @@ class Net::LDAP::Connection #:nodoc: def initialize(server) @instrumentation_service = server[:instrumentation_service] + server[:hosts] = [[server[:host], server[:port]]] if server[:hosts].nil? + if server[:socket] + prepare_socket(server) + else + open_connection(server) + end + + yield self if block_given? + end + + def prepare_socket(server) + @conn = server[:socket] + + if server[:encryption] + setup_encryption server[:encryption] + end + end + + def open_connection(server) + errors = [] + server[:hosts].each do |host, port| + begin + return connect_to_host(host, port, server) + rescue Net::LDAP::Error + errors << $! + end + end + + raise errors.first if errors.size == 1 + raise Net::LDAP::Error, + "Unable to connect to any given server: \n #{errors.join("\n ")}" + end + + def connect_to_host(host, port, server) begin - @conn = server[:socket] || TCPSocket.new(server[:host], server[:port]) + @conn = TCPSocket.new(host, port) rescue SocketError raise Net::LDAP::Error, "No such address or other socket error." rescue Errno::ECONNREFUSED - raise Net::LDAP::ConnectionRefusedError, "Server #{server[:host]} refused connection on port #{server[:port]}." + raise Net::LDAP::ConnectionRefusedError, "Server #{host} refused connection on port #{port}." rescue Errno::EHOSTUNREACH => error - raise Net::LDAP::Error, "Host #{server[:host]} was unreachable (#{error.message})" + raise Net::LDAP::Error, "Host #{host} was unreachable (#{error.message})" rescue Errno::ETIMEDOUT - raise Net::LDAP::Error, "Connection to #{server[:host]} timed out." + raise Net::LDAP::Error, "Connection to #{host} timed out." end if server[:encryption] setup_encryption server[:encryption] end - - yield self if block_given? end module GetbyteForSSLSocket @@ -63,18 +95,18 @@ def self.wrap_with_ssl(io, tls_options = {}) end #-- - # Helper method called only from new, and only after we have a - # successfully-opened @conn instance variable, which is a TCP connection. - # Depending on the received arguments, we establish SSL, potentially - # replacing the value of @conn accordingly. Don't generate any errors here - # if no encryption is requested. DO raise Net::LDAP::Error objects if encryption - # is requested and we have trouble setting it up. That includes if OpenSSL - # is not set up on the machine. (Question: how does the Ruby OpenSSL - # wrapper react in that case?) DO NOT filter exceptions raised by the - # OpenSSL library. Let them pass back to the user. That should make it - # easier for us to debug the problem reports. Presumably (hopefully?) that - # will also produce recognizable errors if someone tries to use this on a - # machine without OpenSSL. + # Helper method called only from prepare_socket or open_connection, and only + # after we have a successfully-opened @conn instance variable, which is a TCP + # connection. Depending on the received arguments, we establish SSL, + # potentially replacing the value of @conn accordingly. Don't generate any + # errors here if no encryption is requested. DO raise Net::LDAP::Error objects + # if encryption is requested and we have trouble setting it up. That includes + # if OpenSSL is not set up on the machine. (Question: how does the Ruby + # OpenSSL wrapper react in that case?) DO NOT filter exceptions raised by the + # OpenSSL library. Let them pass back to the user. That should make it easier + # for us to debug the problem reports. Presumably (hopefully?) that will also + # produce recognizable errors if someone tries to use this on a machine + # without OpenSSL. # # The simple_tls method is intended as the simplest, stupidest, easiest # solution for people who want nothing more than encrypted comms with the diff --git a/test/test_ldap_connection.rb b/test/test_ldap_connection.rb index 4fa7b22d..6fdf0b0a 100644 --- a/test/test_ldap_connection.rb +++ b/test/test_ldap_connection.rb @@ -9,6 +9,44 @@ def capture_stderr $stderr = stderr end + def test_list_of_hosts_with_first_host_successful + hosts = [ + ['test.mocked.com', 636], + ['test2.mocked.com', 636], + ['test3.mocked.com', 636], + ] + flexmock(TCPSocket).should_receive(:new).ordered.with(*hosts[0]).once.and_return(nil) + flexmock(TCPSocket).should_receive(:new).ordered.never + Net::LDAP::Connection.new(:hosts => hosts.to_enum(:each)) + end + + def test_list_of_hosts_with_first_host_failure + hosts = [ + ['test.mocked.com', 636], + ['test2.mocked.com', 636], + ['test3.mocked.com', 636], + ] + flexmock(TCPSocket).should_receive(:new).ordered.with(*hosts[0]).once.and_raise(SocketError) + flexmock(TCPSocket).should_receive(:new).ordered.with(*hosts[1]).once.and_return(nil) + flexmock(TCPSocket).should_receive(:new).ordered.never + Net::LDAP::Connection.new(:hosts => hosts.to_enum(:each)) + end + + def test_list_of_hosts_with_all_hosts_failure + hosts = [ + ['test.mocked.com', 636], + ['test2.mocked.com', 636], + ['test3.mocked.com', 636], + ] + flexmock(TCPSocket).should_receive(:new).ordered.with(*hosts[0]).once.and_raise(SocketError) + flexmock(TCPSocket).should_receive(:new).ordered.with(*hosts[1]).once.and_raise(SocketError) + flexmock(TCPSocket).should_receive(:new).ordered.with(*hosts[2]).once.and_raise(SocketError) + flexmock(TCPSocket).should_receive(:new).ordered.never + assert_raise Net::LDAP::Error do + Net::LDAP::Connection.new(:hosts => hosts.to_enum(:each)) + end + end + def test_unresponsive_host assert_raise Net::LDAP::Error do Net::LDAP::Connection.new(:host => 'test.mocked.com', :port => 636) From c0db1d17d4b29da18cc77540177f41c265ad18d0 Mon Sep 17 00:00:00 2001 From: Jeremy Bopp Date: Mon, 28 Sep 2015 18:13:44 -0500 Subject: [PATCH 2/2] Remove enumerable enforcement attempt in tests --- test/test_ldap_connection.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_ldap_connection.rb b/test/test_ldap_connection.rb index 6fdf0b0a..e5104838 100644 --- a/test/test_ldap_connection.rb +++ b/test/test_ldap_connection.rb @@ -17,7 +17,7 @@ def test_list_of_hosts_with_first_host_successful ] flexmock(TCPSocket).should_receive(:new).ordered.with(*hosts[0]).once.and_return(nil) flexmock(TCPSocket).should_receive(:new).ordered.never - Net::LDAP::Connection.new(:hosts => hosts.to_enum(:each)) + Net::LDAP::Connection.new(:hosts => hosts) end def test_list_of_hosts_with_first_host_failure @@ -29,7 +29,7 @@ def test_list_of_hosts_with_first_host_failure flexmock(TCPSocket).should_receive(:new).ordered.with(*hosts[0]).once.and_raise(SocketError) flexmock(TCPSocket).should_receive(:new).ordered.with(*hosts[1]).once.and_return(nil) flexmock(TCPSocket).should_receive(:new).ordered.never - Net::LDAP::Connection.new(:hosts => hosts.to_enum(:each)) + Net::LDAP::Connection.new(:hosts => hosts) end def test_list_of_hosts_with_all_hosts_failure @@ -43,7 +43,7 @@ def test_list_of_hosts_with_all_hosts_failure flexmock(TCPSocket).should_receive(:new).ordered.with(*hosts[2]).once.and_raise(SocketError) flexmock(TCPSocket).should_receive(:new).ordered.never assert_raise Net::LDAP::Error do - Net::LDAP::Connection.new(:hosts => hosts.to_enum(:each)) + Net::LDAP::Connection.new(:hosts => hosts) end end