Skip to content

Commit c03e044

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). A new option is now exposed through Net::LDAP: - connect_timeout: sets a timeout for socket#connect (defaults to 1s) It also provides an integration test to validate the new behaviour (#244)
1 parent b94d968 commit c03e044

File tree

6 files changed

+57
-19
lines changed

6 files changed

+57
-19
lines changed

lib/net/ldap.rb

+12-2
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@ class LDAP
7979
#
8080
# p ldap.get_operation_result
8181
#
82+
# === Setting connect timeout
83+
#
84+
# By default, Net::LDAP uses TCP sockets with a connection timeout of 5 seconds.
85+
#
86+
# This value can be tweaked passing the :connect_timeout parameter.
87+
# i.e.
88+
# ldap = Net::LDAP.new ...,
89+
# :connect_timeout => 3
8290
#
8391
# == A Brief Introduction to LDAP
8492
#
@@ -482,6 +490,7 @@ def initialize(args = {})
482490
@auth = args[:auth] || DefaultAuth
483491
@base = args[:base] || DefaultTreebase
484492
@force_no_page = args[:force_no_page] || DefaultForceNoPage
493+
@connect_timeout = args[:connect_timeout]
485494
encryption args[:encryption] # may be nil
486495

487496
if pr = @auth[:password] and pr.respond_to?(:call)
@@ -1242,8 +1251,9 @@ def new_connection
12421251
:port => @port,
12431252
:hosts => @hosts,
12441253
:encryption => @encryption,
1245-
:instrumentation_service => @instrumentation_service
1246-
rescue Errno::ECONNREFUSED, Net::LDAP::ConnectionRefusedError => e
1254+
:instrumentation_service => @instrumentation_service,
1255+
:connect_timeout => @connect_timeout
1256+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, Net::LDAP::ConnectionRefusedError => e
12471257
@result = {
12481258
:resultCode => 52,
12491259
:errorMessage => ResultStrings[ResultCodeUnavailable]

lib/net/ldap/connection.rb

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

6+
# Seconds before failing for socket connect timeout
7+
DefaultConnectTimeout = 5
8+
69
LdapVersion = 3
710
MaxSaslChallenges = 10
811

@@ -31,10 +34,14 @@ def open_connection(server)
3134
hosts = server[:hosts]
3235
encryption = server[:encryption]
3336

37+
socket_opts = {
38+
connect_timeout: server[:connect_timeout] || DefaultConnectTimeout
39+
}
40+
3441
errors = []
3542
hosts.each do |host, port|
3643
begin
37-
prepare_socket(server.merge(socket: TCPSocket.new(host, port)))
44+
prepare_socket(server.merge(socket: Socket.tcp(host, port, socket_opts)))
3845
return
3946
rescue Net::LDAP::Error, SocketError, SystemCallError,
4047
OpenSSL::SSL::SSLError => e

script/install-openldap

+3
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,7 @@ chgrp ssl-cert /etc/ssl/private/ldap01_slapd_key.pem
109109
chmod g+r /etc/ssl/private/ldap01_slapd_key.pem
110110
chmod o-r /etc/ssl/private/ldap01_slapd_key.pem
111111

112+
# Drop packets on a secondary port used to specific timeout tests
113+
iptables -A OUTPUT -p tcp -j DROP --dport 8389
114+
112115
service slapd restart

test/integration/test_bind.rb

+8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ def test_bind_success
55
assert @ldap.bind(method: :simple, username: "uid=user1,ou=People,dc=rubyldap,dc=com", password: "passworD1"), @ldap.get_operation_result.inspect
66
end
77

8+
def test_bind_timeout
9+
@ldap.port = 8389
10+
error = assert_raise Net::LDAP::Error do
11+
@ldap.bind(method: :simple, username: "uid=user1,ou=People,dc=rubyldap,dc=com", password: "passworD1")
12+
end
13+
assert_equal('Connection timed out - user specified timeout', error.message)
14+
end
15+
816
def test_bind_anonymous_fail
917
refute @ldap.bind(method: :simple, username: "uid=user1,ou=People,dc=rubyldap,dc=com", password: ""), @ldap.get_operation_result.inspect
1018

test/test_auth_adapter.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
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).ordered.with('ldap.example.com', 379, { connect_timeout: 5 }).once.and_return(nil)
6+
67
conn = Net::LDAP::Connection.new(host: 'ldap.example.com', port: 379)
78
assert_raise Net::LDAP::AuthMethodUnsupportedError, "Unsupported auth method (foo)" do
89
conn.bind(method: :foo)

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], { connect_timeout: 5 }).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], { connect_timeout: 5 }).once.and_raise(SocketError)
30+
flexmock(Socket).should_receive(:tcp).ordered.with(*hosts[1], { connect_timeout: 5 }).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], { connect_timeout: 5 }).once.and_raise(SocketError)
42+
flexmock(Socket).should_receive(:tcp).ordered.with(*hosts[1], { connect_timeout: 5 }).once.and_raise(SocketError)
43+
flexmock(Socket).should_receive(:tcp).ordered.with(*hosts[2], { connect_timeout: 5 }).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)