Skip to content

Commit f6611e2

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 b05d766 commit f6611e2

File tree

6 files changed

+62
-24
lines changed

6 files changed

+62
-24
lines changed

lib/net/ldap.rb

Lines changed: 17 additions & 7 deletions
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
#
@@ -487,22 +495,22 @@ def self.result2string(code) #:nodoc:
487495
# The :start_tls like the :simple_tls encryption method also encrypts all
488496
# communcations with the LDAP server. With the exception that it operates
489497
# over the standard TCP port.
490-
#
498+
#
491499
# In order to verify certificates and enable other TLS options, the
492500
# :tls_options hash can be passed alongside :simple_tls or :start_tls.
493501
# This hash contains any options that can be passed to
494502
# OpenSSL::SSL::SSLContext#set_params(). The most common options passed
495503
# should be OpenSSL::SSL::SSLContext::DEFAULT_PARAMS, or the :ca_file option,
496504
# which contains a path to a Certificate Authority file (PEM-encoded).
497-
#
505+
#
498506
# Example for a default setup without custom settings:
499507
# {
500508
# :method => :simple_tls,
501509
# :tls_options => OpenSSL::SSL::SSLContext::DEFAULT_PARAMS
502510
# }
503-
#
511+
#
504512
# Example for specifying a CA-File and only allowing TLSv1.1 connections:
505-
#
513+
#
506514
# {
507515
# :method => :start_tls,
508516
# :tls_options => { :ca_file => "/etc/cafile.pem", :ssl_version => "TLSv1_1" }
@@ -524,6 +532,7 @@ def initialize(args = {})
524532
@base = args[:base] || DefaultTreebase
525533
@force_no_page = args[:force_no_page] || DefaultForceNoPage
526534
@encryption = args[:encryption] # may be nil
535+
@connect_timeout = args[:connect_timeout]
527536

528537
if pr = @auth[:password] and pr.respond_to?(:call)
529538
@auth[:password] = pr.call
@@ -587,7 +596,7 @@ def authenticate(username, password)
587596
# additional capabilities are added, more configuration values will be
588597
# added here.
589598
#
590-
# This method is deprecated.
599+
# This method is deprecated.
591600
#
592601
def encryption(args)
593602
warn "Deprecation warning: please give :encryption option as a Hash to Net::LDAP.new"
@@ -1247,8 +1256,9 @@ def new_connection
12471256
:port => @port,
12481257
:hosts => @hosts,
12491258
:encryption => @encryption,
1250-
:instrumentation_service => @instrumentation_service
1251-
rescue Errno::ECONNREFUSED, Net::LDAP::ConnectionRefusedError => e
1259+
:instrumentation_service => @instrumentation_service,
1260+
:connect_timeout => @connect_timeout
1261+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, Net::LDAP::ConnectionRefusedError => e
12521262
@result = {
12531263
:resultCode => 52,
12541264
:errorMessage => ResultStrings[ResultCodeUnavailable]

lib/net/ldap/connection.rb

Lines changed: 8 additions & 1 deletion
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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 8 additions & 0 deletions
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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 24 additions & 15 deletions
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)