Skip to content

Commit a447c11

Browse files
astrattoStefano Tortarolo
authored and
Stefano Tortarolo
committed
Use Socket.tcp instead of TCPSocket.new to provide socket timeouts
This patch prevents LDAP connections to hang up for an eccessive amount of time and instead returns earlier in case of failures (e.g., packets dropped). Two options are now exposed through Net::LDAP: - connect_timeout: sets a timeout for socket#connect (defaults to 1s) - timeout: sets a timeout for receiving data (defaults to 300s)
1 parent b94d968 commit a447c11

File tree

5 files changed

+68
-19
lines changed

5 files changed

+68
-19
lines changed

lib/net/ldap.rb

+16-2
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,16 @@ class LDAP
7979
#
8080
# p ldap.get_operation_result
8181
#
82+
# === Setting timeouts
83+
#
84+
# By default, Net::LDAP uses TCP sockets with a connection timeout of 1 second
85+
# and a read timeout of 300 seconds.
86+
#
87+
# These values can be tweaked passing the two parameters.
88+
# i.e.
89+
# ldap = Net::LDAP.new ...,
90+
# :connect_timeout => 3,
91+
# :timeout => 60
8292
#
8393
# == A Brief Introduction to LDAP
8494
#
@@ -482,6 +492,8 @@ def initialize(args = {})
482492
@auth = args[:auth] || DefaultAuth
483493
@base = args[:base] || DefaultTreebase
484494
@force_no_page = args[:force_no_page] || DefaultForceNoPage
495+
@connect_timeout = args[:connect_timeout]
496+
@timeout = args[:timeout]
485497
encryption args[:encryption] # may be nil
486498

487499
if pr = @auth[:password] and pr.respond_to?(:call)
@@ -1242,8 +1254,10 @@ def new_connection
12421254
:port => @port,
12431255
:hosts => @hosts,
12441256
:encryption => @encryption,
1245-
:instrumentation_service => @instrumentation_service
1246-
rescue Errno::ECONNREFUSED, Net::LDAP::ConnectionRefusedError => e
1257+
:instrumentation_service => @instrumentation_service,
1258+
:timeout => @timeout,
1259+
:connect_timeout => @connect_timeout
1260+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, Net::LDAP::ConnectionRefusedError => e
12471261
@result = {
12481262
:resultCode => 52,
12491263
:errorMessage => ResultStrings[ResultCodeUnavailable]

lib/net/ldap/connection.rb

+11-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
class Net::LDAP::Connection #:nodoc:
44
include Net::LDAP::Instrumentation
55

6+
# Seconds before failing for socket connect timeout
7+
DefaultConnectTimeout = 1
8+
# Seconds before failing for socket read timeout
9+
DefaultTimeout = 300
10+
611
LdapVersion = 3
712
MaxSaslChallenges = 10
813

@@ -31,10 +36,15 @@ def open_connection(server)
3136
hosts = server[:hosts]
3237
encryption = server[:encryption]
3338

39+
socket_opts = {
40+
connect_timeout: server[:connect_timeout] || DefaultConnectTimeout,
41+
timeout: server[:timeout] || DefaultTimeout
42+
}
43+
3444
errors = []
3545
hosts.each do |host, port|
3646
begin
37-
prepare_socket(server.merge(socket: TCPSocket.new(host, port)))
47+
prepare_socket(server.merge(socket: Socket.tcp(host, port, socket_opts)))
3848
return
3949
rescue Net::LDAP::Error, SocketError, SystemCallError,
4050
OpenSSL::SSL::SSLError => e

test/test_auth_adapter.rb

+5-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33
class TestAuthAdapter < Test::Unit::TestCase
44
def test_undefined_auth_adapter
5-
flexmock(TCPSocket).should_receive(:new).ordered.with('ldap.example.com', 379).once.and_return(nil)
5+
flexmock(Socket).should_receive(:tcp)
6+
.ordered
7+
.with('ldap.example.com', 379, SocketHelpers.default_socket_opts)
8+
.once.and_return(nil)
9+
610
conn = Net::LDAP::Connection.new(host: 'ldap.example.com', port: 379)
711
assert_raise Net::LDAP::AuthMethodUnsupportedError, "Unsupported auth method (foo)" do
812
conn.bind(method: :foo)

test/test_helper.rb

+12
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,15 @@ def setup
6464
instrumentation_service: @service
6565
end
6666
end
67+
68+
# Provide information about socket creation
69+
class SocketHelpers
70+
class << self
71+
def default_socket_opts
72+
{
73+
connect_timeout: Net::LDAP::Connection::DefaultConnectTimeout,
74+
timeout: Net::LDAP::Connection::DefaultTimeout
75+
}
76+
end
77+
end
78+
end

test/test_ldap_connection.rb

+24-15
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ def test_list_of_hosts_with_first_host_successful
1515
['test2.mocked.com', 636],
1616
['test3.mocked.com', 636],
1717
]
18-
flexmock(TCPSocket).should_receive(:new).ordered.with(*hosts[0]).once.and_return(nil)
19-
flexmock(TCPSocket).should_receive(:new).ordered.never
18+
flexmock(Socket).should_receive(:tcp).ordered.with(*hosts[0], SocketHelpers.default_socket_opts).once.and_return(nil)
19+
flexmock(Socket).should_receive(:tcp).ordered.never
2020
Net::LDAP::Connection.new(:hosts => hosts)
2121
end
2222

@@ -26,9 +26,9 @@ def test_list_of_hosts_with_first_host_failure
2626
['test2.mocked.com', 636],
2727
['test3.mocked.com', 636],
2828
]
29-
flexmock(TCPSocket).should_receive(:new).ordered.with(*hosts[0]).once.and_raise(SocketError)
30-
flexmock(TCPSocket).should_receive(:new).ordered.with(*hosts[1]).once.and_return(nil)
31-
flexmock(TCPSocket).should_receive(:new).ordered.never
29+
flexmock(Socket).should_receive(:tcp).ordered.with(*hosts[0], SocketHelpers.default_socket_opts).once.and_raise(SocketError)
30+
flexmock(Socket).should_receive(:tcp).ordered.with(*hosts[1], SocketHelpers.default_socket_opts).once.and_return(nil)
31+
flexmock(Socket).should_receive(:tcp).ordered.never
3232
Net::LDAP::Connection.new(:hosts => hosts)
3333
end
3434

@@ -38,17 +38,17 @@ def test_list_of_hosts_with_all_hosts_failure
3838
['test2.mocked.com', 636],
3939
['test3.mocked.com', 636],
4040
]
41-
flexmock(TCPSocket).should_receive(:new).ordered.with(*hosts[0]).once.and_raise(SocketError)
42-
flexmock(TCPSocket).should_receive(:new).ordered.with(*hosts[1]).once.and_raise(SocketError)
43-
flexmock(TCPSocket).should_receive(:new).ordered.with(*hosts[2]).once.and_raise(SocketError)
44-
flexmock(TCPSocket).should_receive(:new).ordered.never
41+
flexmock(Socket).should_receive(:tcp).ordered.with(*hosts[0], SocketHelpers.default_socket_opts).once.and_raise(SocketError)
42+
flexmock(Socket).should_receive(:tcp).ordered.with(*hosts[1], SocketHelpers.default_socket_opts).once.and_raise(SocketError)
43+
flexmock(Socket).should_receive(:tcp).ordered.with(*hosts[2], SocketHelpers.default_socket_opts).once.and_raise(SocketError)
44+
flexmock(Socket).should_receive(:tcp).ordered.never
4545
assert_raise Net::LDAP::ConnectionError do
4646
Net::LDAP::Connection.new(:hosts => hosts)
4747
end
4848
end
4949

5050
def test_result_for_connection_failed_is_set
51-
flexmock(TCPSocket).should_receive(:new).and_raise(Errno::ECONNREFUSED)
51+
flexmock(Socket).should_receive(:tcp).and_raise(Errno::ECONNREFUSED)
5252

5353
ldap_client = Net::LDAP.new(host: '127.0.0.1', port: 12345)
5454

@@ -67,14 +67,14 @@ def test_unresponsive_host
6767
end
6868

6969
def test_blocked_port
70-
flexmock(TCPSocket).should_receive(:new).and_raise(SocketError)
70+
flexmock(Socket).should_receive(:tcp).and_raise(SocketError)
7171
assert_raise Net::LDAP::Error do
7272
Net::LDAP::Connection.new(:host => 'test.mocked.com', :port => 636)
7373
end
7474
end
7575

7676
def test_connection_refused
77-
flexmock(TCPSocket).should_receive(:new).and_raise(Errno::ECONNREFUSED)
77+
flexmock(Socket).should_receive(:tcp).and_raise(Errno::ECONNREFUSED)
7878
stderr = capture_stderr do
7979
assert_raise Net::LDAP::ConnectionRefusedError do
8080
Net::LDAP::Connection.new(:host => 'test.mocked.com', :port => 636)
@@ -83,9 +83,18 @@ def test_connection_refused
8383
assert_equal("Deprecation warning: Net::LDAP::ConnectionRefused will be deprecated. Use Errno::ECONNREFUSED instead.\n", stderr)
8484
end
8585

86+
def test_connection_timedout
87+
flexmock(Socket).should_receive(:tcp).and_raise(Errno::ETIMEDOUT)
88+
stderr = capture_stderr do
89+
assert_raise Net::LDAP::Error do
90+
Net::LDAP::Connection.new(:host => 'test.mocked.com', :port => 636)
91+
end
92+
end
93+
end
94+
8695
def test_raises_unknown_exceptions
8796
error = Class.new(StandardError)
88-
flexmock(TCPSocket).should_receive(:new).and_raise(error)
97+
flexmock(Socket).should_receive(:tcp).and_raise(error)
8998
assert_raise error do
9099
Net::LDAP::Connection.new(:host => 'test.mocked.com', :port => 636)
91100
end
@@ -328,7 +337,7 @@ class TestLDAPConnectionErrors < Test::Unit::TestCase
328337
def setup
329338
@tcp_socket = flexmock(:connection)
330339
@tcp_socket.should_receive(:write)
331-
flexmock(TCPSocket).should_receive(:new).and_return(@tcp_socket)
340+
flexmock(Socket).should_receive(:tcp).and_return(@tcp_socket)
332341
@connection = Net::LDAP::Connection.new(:host => 'test.mocked.com', :port => 636)
333342
end
334343

@@ -357,7 +366,7 @@ class TestLDAPConnectionInstrumentation < Test::Unit::TestCase
357366
def setup
358367
@tcp_socket = flexmock(:connection)
359368
@tcp_socket.should_receive(:write)
360-
flexmock(TCPSocket).should_receive(:new).and_return(@tcp_socket)
369+
flexmock(Socket).should_receive(:tcp).and_return(@tcp_socket)
361370

362371
@service = MockInstrumentationService.new
363372
@connection = Net::LDAP::Connection.new \

0 commit comments

Comments
 (0)