-
Notifications
You must be signed in to change notification settings - Fork 252
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Lazy initialize Net::LDAP::Connection's internal socket #235
Changes from all commits
d5e6afd
4a415bc
b856806
76dde7b
53cc6b5
e9a1bf1
9a2e26e
259f18a
f6ad189
e7cc5ae
d35d3bf
1aab8c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,19 +9,28 @@ class Net::LDAP::Connection #:nodoc: | |
LdapVersion = 3 | ||
MaxSaslChallenges = 10 | ||
|
||
def initialize(server) | ||
# Initialize a connection to an LDAP server | ||
# | ||
# :server | ||
# :hosts Array of tuples specifying host, port | ||
# :host host | ||
# :port port | ||
# :socket prepared socket | ||
# | ||
def initialize(server = {}) | ||
@server = server | ||
@instrumentation_service = server[:instrumentation_service] | ||
|
||
if server[:socket] | ||
prepare_socket(server) | ||
else | ||
server[:hosts] = [[server[:host], server[:port]]] if server[:hosts].nil? | ||
open_connection(server) | ||
end | ||
# Allows tests to parameterize what socket class to use | ||
@socket_class = server.fetch(:socket_class, DefaultSocket) | ||
|
||
yield self if block_given? | ||
end | ||
|
||
def socket_class=(socket_class) | ||
@socket_class = socket_class | ||
end | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These methods could be removed if you allow the socket class to be set in the initializer. |
||
def prepare_socket(server) | ||
socket = server[:socket] | ||
encryption = server[:encryption] | ||
|
@@ -41,7 +50,7 @@ def open_connection(server) | |
errors = [] | ||
hosts.each do |host, port| | ||
begin | ||
prepare_socket(server.merge(socket: Socket.tcp(host, port, socket_opts))) | ||
prepare_socket(server.merge(socket: @socket_class.new(host, port, socket_opts))) | ||
return | ||
rescue Net::LDAP::Error, SocketError, SystemCallError, | ||
OpenSSL::SSL::SSLError => e | ||
|
@@ -202,7 +211,7 @@ def message_queue | |
def read(syntax = Net::LDAP::AsnSyntax) | ||
ber_object = | ||
instrument "read.net_ldap_connection", :syntax => syntax do |payload| | ||
@conn.read_ber(syntax) do |id, content_length| | ||
socket.read_ber(syntax) do |id, content_length| | ||
payload[:object_type_id] = id | ||
payload[:content_length] = content_length | ||
end | ||
|
@@ -232,7 +241,7 @@ def read(syntax = Net::LDAP::AsnSyntax) | |
def write(request, controls = nil, message_id = next_msgid) | ||
instrument "write.net_ldap_connection" do |payload| | ||
packet = [message_id.to_ber, request, controls].compact.to_ber_sequence | ||
payload[:content_length] = @conn.write(packet) | ||
payload[:content_length] = socket.write(packet) | ||
end | ||
end | ||
private :write | ||
|
@@ -652,4 +661,33 @@ def delete(args) | |
|
||
pdu | ||
end | ||
|
||
# Internal: Returns a Socket like object used internally to communicate with | ||
# LDAP server. | ||
# | ||
# Typically a TCPSocket, but can be a OpenSSL::SSL::SSLSocket | ||
def socket | ||
return @conn if defined? @conn | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Out of curiosity, I have a question. @foo = 1 # <= defined instance variable
puts 'ok' if @foo # => 'ok'
puts 'ok' if defined? @foo # => 'ok' so the both are same.
@bar = nil # defined but its value is nil
puts 'ok' if @bar # => nothing
puts 'ok' if defined @bar # => 'ok' so different behavior I believe that this difference doesn't affect on the behavior of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @satoryu This is likely done to avoid warnings from the interpreter for using uninitialized instance variables. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
|
||
# First refactoring uses the existing methods open_connection and | ||
# prepare_socket to set @conn. Next cleanup would centralize connection | ||
# handling here. | ||
if @server[:socket] | ||
prepare_socket(@server) | ||
else | ||
@server[:hosts] = [[@server[:host], @server[:port]]] if @server[:hosts].nil? | ||
open_connection(@server) | ||
end | ||
|
||
@conn | ||
end | ||
|
||
private | ||
|
||
# Wrap around Socket.tcp to normalize with other Socket initializers | ||
class DefaultSocket | ||
def self.new(host, port, socket_opts = {}) | ||
Socket.tcp(host, port, socket_opts) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking at a somewhat old definition of http://ruby-doc.org/stdlib-1.9.3/libdoc/socket/rdoc/Socket.html#method-c-tcp The newer version of that method has an ability to take the socket options, but you need to pass something along to the http://ruby-doc.org/stdlib-2.3.0/libdoc/socket/rdoc/Socket.html#method-c-tcp There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @javanthropus this is possible because we dropped support for 1.9.x in 0.13.0 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jch Check the documentation I linked for version 2.3.0. Unless I misunderstood something there, you'll need to set the |
||
end | ||
end | ||
end # class Connection |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,44 +9,55 @@ def capture_stderr | |
$stderr = stderr | ||
end | ||
|
||
# Fake socket for testing | ||
# | ||
# FakeTCPSocket.new("success", 636) | ||
# FakeTCPSocket.new("fail.SocketError", 636) # raises SocketError | ||
class FakeTCPSocket | ||
def initialize(host, port, socket_opts = {}) | ||
status, error = host.split(".") | ||
if status == "fail" | ||
raise Object.const_get(error) | ||
end | ||
end | ||
end | ||
|
||
def test_list_of_hosts_with_first_host_successful | ||
hosts = [ | ||
['test.mocked.com', 636], | ||
['test2.mocked.com', 636], | ||
['test3.mocked.com', 636], | ||
["success.host", 636], | ||
["fail.SocketError", 636], | ||
["fail.SocketError", 636], | ||
] | ||
flexmock(Socket).should_receive(:tcp).ordered.with(*hosts[0], { connect_timeout: 5 }).once.and_return(nil) | ||
flexmock(Socket).should_receive(:tcp).ordered.never | ||
Net::LDAP::Connection.new(:hosts => hosts) | ||
|
||
connection = Net::LDAP::Connection.new(:hosts => hosts, :socket_class => FakeTCPSocket) | ||
connection.socket | ||
end | ||
|
||
def test_list_of_hosts_with_first_host_failure | ||
hosts = [ | ||
['test.mocked.com', 636], | ||
['test2.mocked.com', 636], | ||
['test3.mocked.com', 636], | ||
["fail.SocketError", 636], | ||
["success.host", 636], | ||
["fail.SocketError", 636], | ||
] | ||
flexmock(Socket).should_receive(:tcp).ordered.with(*hosts[0], { connect_timeout: 5 }).once.and_raise(SocketError) | ||
flexmock(Socket).should_receive(:tcp).ordered.with(*hosts[1], { connect_timeout: 5 }).once.and_return(nil) | ||
flexmock(Socket).should_receive(:tcp).ordered.never | ||
Net::LDAP::Connection.new(:hosts => hosts) | ||
|
||
connection = Net::LDAP::Connection.new(:hosts => hosts, :socket_class => FakeTCPSocket) | ||
connection.socket | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jch Uses such as these 3 lines are what I'm proposing to simplify a bit. I think it would look better and be easier to use like this: connection = Net::LDAP::Connection.new(:hosts => hosts, :socket_class => FakeTCPSocket) The idea is for this single call to create the connection and arrange for the opening of the socket using the specified socket class when needed all in a single go rather than requiring the This is a fairly common way to handle dependency injection as you're trying to enable here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BTW, with this change in place, you wouldn't need to add the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @javanthropus ++ your idea sounds good 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @javanthropus ah, I totally misunderstood! Parameterizing the socket class rather than the socket instance is a good idea. I'll roll with that. |
||
end | ||
|
||
def test_list_of_hosts_with_all_hosts_failure | ||
hosts = [ | ||
['test.mocked.com', 636], | ||
['test2.mocked.com', 636], | ||
['test3.mocked.com', 636], | ||
["fail.SocketError", 636], | ||
["fail.SocketError", 636], | ||
["fail.SocketError", 636], | ||
] | ||
flexmock(Socket).should_receive(:tcp).ordered.with(*hosts[0], { connect_timeout: 5 }).once.and_raise(SocketError) | ||
flexmock(Socket).should_receive(:tcp).ordered.with(*hosts[1], { connect_timeout: 5 }).once.and_raise(SocketError) | ||
flexmock(Socket).should_receive(:tcp).ordered.with(*hosts[2], { connect_timeout: 5 }).once.and_raise(SocketError) | ||
flexmock(Socket).should_receive(:tcp).ordered.never | ||
|
||
connection = Net::LDAP::Connection.new(:hosts => hosts, :socket_class => FakeTCPSocket) | ||
assert_raise Net::LDAP::ConnectionError do | ||
Net::LDAP::Connection.new(:hosts => hosts) | ||
connection.socket | ||
end | ||
end | ||
|
||
# This belongs in test_ldap, not test_ldap_connection | ||
def test_result_for_connection_failed_is_set | ||
flexmock(Socket).should_receive(:tcp).and_raise(Errno::ECONNREFUSED) | ||
|
||
|
@@ -61,42 +72,42 @@ def test_result_for_connection_failed_is_set | |
end | ||
|
||
def test_unresponsive_host | ||
connection = Net::LDAP::Connection.new(:host => "fail.Errno::ETIMEDOUT", :port => 636, :socket_class => FakeTCPSocket) | ||
assert_raise Net::LDAP::Error do | ||
Net::LDAP::Connection.new(:host => 'test.mocked.com', :port => 636) | ||
connection.socket | ||
end | ||
end | ||
|
||
def test_blocked_port | ||
flexmock(Socket).should_receive(:tcp).and_raise(SocketError) | ||
connection = Net::LDAP::Connection.new(:host => "fail.SocketError", :port => 636, :socket_class => FakeTCPSocket) | ||
assert_raise Net::LDAP::Error do | ||
Net::LDAP::Connection.new(:host => 'test.mocked.com', :port => 636) | ||
connection.socket | ||
end | ||
end | ||
|
||
def test_connection_refused | ||
flexmock(Socket).should_receive(:tcp).and_raise(Errno::ECONNREFUSED) | ||
connection = Net::LDAP::Connection.new(:host => "fail.Errno::ECONNREFUSED", :port => 636, :socket_class => FakeTCPSocket) | ||
stderr = capture_stderr do | ||
assert_raise Net::LDAP::ConnectionRefusedError do | ||
Net::LDAP::Connection.new(:host => 'test.mocked.com', :port => 636) | ||
connection.socket | ||
end | ||
end | ||
assert_equal("Deprecation warning: Net::LDAP::ConnectionRefused will be deprecated. Use Errno::ECONNREFUSED instead.\n", stderr) | ||
end | ||
|
||
def test_connection_timedout | ||
flexmock(Socket).should_receive(:tcp).and_raise(Errno::ETIMEDOUT) | ||
def test_connection_timeout | ||
connection = Net::LDAP::Connection.new(:host => "fail.Errno::ETIMEDOUT", :port => 636, :socket_class => FakeTCPSocket) | ||
stderr = capture_stderr do | ||
assert_raise Net::LDAP::Error do | ||
Net::LDAP::Connection.new(:host => 'test.mocked.com', :port => 636) | ||
connection.socket | ||
end | ||
end | ||
end | ||
|
||
def test_raises_unknown_exceptions | ||
error = Class.new(StandardError) | ||
flexmock(Socket).should_receive(:tcp).and_raise(error) | ||
assert_raise error do | ||
Net::LDAP::Connection.new(:host => 'test.mocked.com', :port => 636) | ||
connection = Net::LDAP::Connection.new(:host => "fail.StandardError", :port => 636, :socket_class => FakeTCPSocket) | ||
assert_raise StandardError do | ||
connection.socket | ||
end | ||
end | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you add the following here, you can simplify overriding the socket class:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 1aab8c9