class FTW::Connection
A network connection. This is TCP.
You can use IO::select on this objects of this type. (at least, in MRI you can)
You can activate SSL/TLS on this connection by invoking FTW::Connection#secure
This class also implements buffering itself because some IO-like classes (OpenSSL::SSL::SSLSocket) do not support IO#ungetbyte
Public Class Methods
A new network connection. The 'destination' argument can be an array of strings or a single string. String format is expected to be “host:port”
Example:
conn = FTW::Connection.new(["1.2.3.4:80", "1.2.3.5:80"])
If you specify multiple destinations, they are used in a round-robin decision made during reconnection.
# File lib/ftw/connection.rb, line 59 def initialize(destinations) if destinations.is_a?(String) @destinations = [destinations] else @destinations = destinations end @mode = :client setup end
Private Class Methods
Create a new connection from an existing IO instance (like a socket)
Valid modes are :server and :client.
-
specify :server if this connection is from a server (via Socket#accept)
-
specify :client if this connection is from a client (via Socket#connect)
# File lib/ftw/connection.rb, line 101 def self.from_io(io, mode=:server) valid_modes = [:server, :client] if !valid_modes.include?(mode) raise InvalidArgument.new("Invalid connection mode '#{mode}'. Valid modes: #{valid_modes.inspect}") end connection = self.new(nil) # New connection with no destinations connection.instance_eval do @socket = io @connected = true port, address = Socket.unpack_sockaddr_in(io.getpeername) @remote_address = "#{address}:#{port}" @mode = mode end return connection end
Public Instance Methods
Is this a client connection?
# File lib/ftw/connection.rb, line 453 def client? return @mode == :client end
Connect now.
Timeout value is optional. If no timeout is given, this method blocks until a connection is successful or an error occurs.
You should check the return value of this method to determine if a connection was successful.
Possible return values are on error include:
@return [nil] if the connection was successful @return [StandardError or subclass] if the connection failed
# File lib/ftw/connection.rb, line 133 def connect(timeout=nil) # TODO(sissel): Raise if we're already connected? disconnect("reconnecting") if connected? host, port = @destinations.first.split(":") @destinations = @destinations.rotate # round-robin # Do dns resolution on the host. If there are multiple # addresses resolved, return one at random. addresses = FTW::DNS.singleton.resolve(host) addresses.each do |address| # Try each address until one works. @remote_address = address # Addresses with colon ':' in them are assumed to be IPv6 family = @remote_address.include?(":") ? Socket::AF_INET6 : Socket::AF_INET @logger.debug("Connecting", :address => @remote_address, :host => host, :port => port, :family => family) @socket = Socket.new(family, Socket::SOCK_STREAM, 0) @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) # This api is terrible. pack_sockaddr_in? This isn't C, man... @logger.debug("packing", :data => [port.to_i, @remote_address]) sockaddr = Socket.pack_sockaddr_in(port.to_i, @remote_address) # TODO(sissel): Support local address binding # Connect with timeout begin @socket.connect_nonblock(sockaddr) rescue IO::WaitWritable, Errno::EINPROGRESS # Ruby actually raises Errno::EINPROGRESS, but for some reason # the documentation says to use this IO::WaitWritable thing... # I don't get it, but whatever :( writable = writable?(timeout) # http://jira.codehaus.org/browse/JRUBY-6528; IO.select doesn't behave # correctly on JRuby < 1.7, so work around it. if writable || (RUBY_PLATFORM == "java" and JRUBY_VERSION < "1.7.0") begin @socket.connect_nonblock(sockaddr) # check connection failure rescue Errno::EISCONN # Ignore, we're already connected. rescue Errno::ECONNREFUSED => e # Fire 'disconnected' event with reason :refused @socket.close return ConnectRefused.new("#{host}[#{@remote_address}]:#{port}") rescue Errno::ETIMEDOUT # This occurs when the system's TCP timeout hits, we have no # control over this, as far as I can tell. *maybe* setsockopt(2) # has a flag for this, but I haven't checked.. # TODO(sissel): We should instead do 'retry' unless we've exceeded # the timeout. @socket.close return ConnectTimeout.new("#{host}[#{@remote_address}]:#{port}") rescue Errno::EINPROGRESS # If we get here, it's likely JRuby version < 1.7.0. EINPROGRESS at # this point in the code means that we have timed out. @socket.close return ConnectTimeout.new("#{host}[#{@remote_address}]:#{port}") end else # Connection timeout; return ConnectTimeout.new("#{host}[#{@remote_address}]:#{port}") end # If no error at this point, we're now connected. @connected = true break end # addresses.each end return nil end
Is this Connection
connected?
# File lib/ftw/connection.rb, line 207 def connected? return @connected end
End this connection, specifying why.
# File lib/ftw/connection.rb, line 270 def disconnect(reason) io = @socket if @socket.is_a?(OpenSSL::SSL::SSLSocket) @socket.sysclose() io = @socket.io end begin io.close_read rescue IOError => e # Ignore, perhaps we shouldn't ignore. end begin io.close_write rescue IOError => e # Ignore, perhaps we shouldn't ignore. end end
The host:port
# File lib/ftw/connection.rb, line 308 def peer return @remote_address end
Push back some data onto the connection's read buffer.
# File lib/ftw/connection.rb, line 265 def pushback(data) @pushback_buffer << data end
Read data from this connection This method blocks until the read succeeds unless a timeout is given.
This method is not guaranteed to read exactly 'length' bytes. See IO#sysread
# File lib/ftw/connection.rb, line 231 def read(length=16384, timeout=nil) data = "" data.force_encoding("BINARY") if data.respond_to?(:force_encoding) have_pushback = !@pushback_buffer.empty? if have_pushback data << @pushback_buffer @pushback_buffer = "" # We have data 'now' so don't wait. timeout = 0 end if readable?(timeout) begin # Read at most 'length' data, so read less from the socket # We'll read less than 'length' if the pushback buffer has # data in it already. @socket.sysread(length - data.length, @read_buffer) data << @read_buffer return data rescue EOFError => e @socket.close @connected = false raise e end else if have_pushback return data else raise ReadTimeout.new end end end
Is this connection readable? Returns true if it is readable within the timeout period. False otherwise.
The time out is in seconds. Fractional seconds are OK.
# File lib/ftw/connection.rb, line 302 def readable?(timeout) readable, writable, errors = IO.select([@socket], nil, nil, timeout) return !readable.nil? end
Secure this connection with TLS.
Options:
-
:certificate_store, an OpenSSL::X509::Store
-
:timeout, a timeout threshold in seconds.
-
:ciphers, an OpenSSL ciphers string, see `openssl ciphers` manual for details.
-
:ssl_version, any of: SSLv2, SSLv3, TLSv1, TLSv1.1, TLSv1.2
-
:certificate, an OpenSSL::X509::Certificate
-
:key, an OpenSSL::PKey (like OpenSSL::PKey::RSA)
Both `certificate` and `key` are highly recommended if the connection belongs to a server (not a client connection).
Notes:
-
Version may depend on your platform (openssl compilation settings, JVM version, export restrictions, etc)
-
Available ciphers will depend on your version of Ruby (or JRuby and JVM),
OpenSSL, etc.
-
# File lib/ftw/connection.rb, line 336 def secure(options=nil) # Skip this if we're already secure. return if secured? defaults = { :timeout => nil, :ciphers => FTW::Agent::Configuration::SSL_CIPHER_MAP["MOZILLA_MODERN"], :ssl_version => "TLSv1.1" } settings = defaults.merge(options) unless options.nil? @logger.info("Securing this connection", :peer => peer, :options => settings) # Wrap this connection with TLS/SSL sslcontext = OpenSSL::SSL::SSLContext.new # If you use VERIFY_NONE, you are removing the trust feature of TLS. Don't do that. # Encryption without trust means you don't know who you are talking to. sslcontext.verify_mode = OpenSSL::SSL::VERIFY_PEER # ruby-core is refusing to patch ruby's default openssl settings to be more # secure, so let's fix that here. The next few lines setting options and # ciphers come from jmhodges' proposed patch ssloptions = OpenSSL::SSL::OP_ALL if defined?(OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS) ssloptions &= ~OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS end if defined?(OpenSSL::SSL::OP_NO_COMPRESSION) ssloptions |= OpenSSL::SSL::OP_NO_COMPRESSION end # https://github.com/jruby/jruby/issues/1874 version = OpenSSL::SSL::SSLContext::METHODS.find { |x| x.to_s.gsub("_",".") == settings[:ssl_version] } raise InvalidConfiguration, "Invalid SSL/TLS version '#{settings[:ssl_version]}'" if version.nil? sslcontext.ssl_version = version # We have to set ciphers *after* setting ssl_version because setting # ssl_version will reset the cipher set. sslcontext.options = ssloptions sslcontext.ciphers = settings[:ciphers] sslcontext.verify_callback = proc do |*args| @logger.debug("Verify peer via FTW::Connection#secure", :callback => settings[:verify_callback]) if settings[:verify_callback].respond_to?(:call) settings[:verify_callback].call(*args) end end sslcontext.cert_store = settings[:certificate_store] if settings.include?(:certificate) && settings.include?(:key) sslcontext.cert = settings[:certificate] sslcontext.key = settings[:key] end @socket = OpenSSL::SSL::SSLSocket.new(@socket, sslcontext) # TODO(sissel): Set up local certificat/key stuff. This is required for # server-side ssl operation, I think. if client? do_secure(:connect_nonblock, settings[:timeout]) else do_secure(:accept_nonblock, settings[:timeout]) end end
Has this connection been secured?
# File lib/ftw/connection.rb, line 448 def secured? return @secure end
Is this a server connection?
# File lib/ftw/connection.rb, line 458 def server? return @mode == :server end
Support 'to_io' so you can use IO::select on this object.
# File lib/ftw/connection.rb, line 313 def to_io return @socket end
Is this connection writable? Returns true if it is writable within the timeout period. False otherwise.
The time out is in seconds. Fractional seconds are OK.
# File lib/ftw/connection.rb, line 293 def writable?(timeout) readable, writable, errors = IO.select(nil, [@socket], nil, timeout) return !writable.nil? end
Write data to this connection. This method blocks until the write succeeds unless a timeout is given.
This method is not guaranteed to have written the full data given.
Returns the number of bytes written (See also IO#syswrite)
# File lib/ftw/connection.rb, line 217 def write(data, timeout=nil) #connect if !connected? if writable?(timeout) return @socket.syswrite(data) else raise FTW::Connection::WriteTimeout.new(self.inspect) end end
Private Instance Methods
Secure this connection.
The handshake method for OpenSSL::SSL::SSLSocket is different depending on the mode (client or server).
@param [Symbol] handshake_method The method to call on the socket to
complete the ssl handshake. See OpenSSL::SSL::SSLSocket#connect_nonblock of #accept_nonblock for more details
# File lib/ftw/connection.rb, line 407 def do_secure(handshake_method, timeout=nil) # SSLSocket#connect_nonblock will do the SSL/TLS handshake. # TODO(sissel): refactor this into a method that both secure and connect # methods can call. start = Time.now begin @socket.send(handshake_method) rescue IO::WaitReadable, IO::WaitWritable # The ruby OpenSSL docs for 1.9.3 have example code saying I should use # IO::WaitReadable, but in the real world it raises an SSLError with # a specific string message instead of Errno::EAGAIN or IO::WaitReadable # explicitly... # # This SSLSocket#connect_nonblock raising WaitReadable (Technically, # OpenSSL::SSL::SSLError) is in contrast to what Socket#connect_nonblock # raises, WaitWritable (ok, Errno::EINPROGRESS, technically) # Ruby's SSL exception for 'this call would block' is pretty shitty. # # So we rescue both IO::Wait{Readable,Writable} and keep trying # until timeout occurs. # if !timeout.nil? time_left = timeout - (Time.now - start) raise SecureHandshakeTimeout.new if time_left < 0 r, w, e = IO.select([@socket], [@socket], nil, time_left) else r, w, e = IO.select([@socket], [@socket], nil, timeout) end # keep going if the socket is ready retry if r.size > 0 || w.size > 0 rescue => e @logger.warn(e) raise e end @secure = true end
Set up this connection.
# File lib/ftw/connection.rb, line 71 def setup @logger = Cabin::Channel.get @connect_timeout = 2 # Use a fixed-size string that we set to BINARY encoding. # Not all byte sequences are UTF-8 friendly :0 @read_size = 16384 @read_buffer = " " * @read_size @pushback_buffer = "" # Tell Ruby 1.9 that this string is a binary string, not utf-8 or somesuch. if @read_buffer.respond_to?(:force_encoding) @read_buffer.force_encoding("BINARY") end @inspectables = [:@destinations, :@connected, :@remote_address, :@secure] @connected = false @remote_address = nil @secure = false # TODO(sissel): Validate @destinations # TODO(sissel): Barf if a destination is not of the form "host:port" end