Class: FTW::Connection

Inherits:
Object
  • Object
show all
Includes:
Cabin::Inspectable, Poolable
Defined in:
lib/ftw/connection.rb

Overview

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

Defined Under Namespace

Classes: ConnectRefused, ConnectTimeout, ReadTimeout, SecureHandshakeTimeout, WriteTimeout

Instance Method Summary collapse

Methods included from Poolable

#available?, #mark, #release

Instance Method Details

#client?Boolean

def secured?

Returns:

  • (Boolean)


351
352
353
# File 'lib/ftw/connection.rb', line 351

def client?
  return @mode == :client
end

#connect(timeout = nil) ⇒ nil, StandardError or subclass

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:

  • FTW::Connection::ConnectRefused

  • FTW::Connection::ConnectTimeout

Returns:

  • (nil)

    if the connection was successful

  • (StandardError or subclass)

    if the connection failed



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/ftw/connection.rb', line 121

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.
  @remote_address = FTW::DNS.singleton.resolve_random(host)
  @logger.debug("Connecting", :address => @remote_address,
                :host => host, :port => port)

  # Addresses with colon ':' in them are assumed to be IPv6
  family = @remote_address.include?(":") ? Socket::AF_INET6 : Socket::AF_INET
  @socket = Socket.new(family, Socket::SOCK_STREAM, 0)

  # This api is terrible. pack_sockaddr_in? This isn't C, man...
  sockaddr = Socket.pack_sockaddr_in(port, @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 :(

    if writable?(timeout)
      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
        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.
        return ConnectTimeout.new("#{host}[#{@remote_address}]:#{port}")
      end
    else
      # Connection timeout
      # Fire 'disconnected' event with reason :timeout
      return ConnectTimeout.new("#{host}[#{@remote_address}]:#{port}")
    end
  end

  # We're now connected.
  @connected = true
  return nil
end

#connected?Boolean

Is this Connection connected?

Returns:

  • (Boolean)


178
179
180
# File 'lib/ftw/connection.rb', line 178

def connected?
  return @connected
end

#disconnect(reason) ⇒ Object

End this connection, specifying why.



237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/ftw/connection.rb', line 237

def disconnect(reason)
  begin 
    @socket.close_read
  rescue IOError => e
    # Ignore, perhaps we shouldn't ignore.
  end

  begin 
    @socket.close_write
  rescue IOError => e
    # Ignore, perhaps we shouldn't ignore.
  end
end

#peerObject

The host:port



270
271
272
# File 'lib/ftw/connection.rb', line 270

def peer
  return @remote_address
end

#pushback(data) ⇒ Object

Push back some data onto the connection’s read buffer.



232
233
234
# File 'lib/ftw/connection.rb', line 232

def pushback(data)
  @pushback_buffer << data
end

#read(length = 16384, timeout = nil) ⇒ Object

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



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/ftw/connection.rb', line 202

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
      @socket.sysread(@read_size - data.length, @read_buffer)
      data << @read_buffer
      return data
    rescue EOFError => e
      raise e
    end
  else
    if have_pushback
      return data
    else
      raise ReadTimeout.new
    end
  end
end

#readable?(timeout) ⇒ Boolean

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.

Returns:

  • (Boolean)


264
265
266
267
# File 'lib/ftw/connection.rb', line 264

def readable?(timeout)
  readable, writable, errors = IO.select([@socket], nil, nil, timeout)
  return !readable.nil?
end

#secure(timeout = nil, options = {}) ⇒ Object

Secure this connection with TLS.



280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/ftw/connection.rb', line 280

def secure(timeout=nil, options={})
  # Skip this if we're already secure.
  return if secured?

  @logger.debug("Securing this connection", :peer => peer, :connection => self)
  # Wrap this connection with TLS/SSL
  require "openssl"
  sslcontext = OpenSSL::SSL::SSLContext.new
  sslcontext.ssl_version = :TLSv1
  # 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
  # TODO(sissel): Try to be smart about setting this default.
  sslcontext.ca_path = "/etc/ssl/certs"
  @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)
  else
    do_secure(:accept_nonblock)
  end
end

#secured?Boolean

Has this connection been secured?

Returns:

  • (Boolean)


347
348
349
# File 'lib/ftw/connection.rb', line 347

def secured?
  return @secure
end

#server?Boolean

def client?

Returns:

  • (Boolean)


355
356
357
# File 'lib/ftw/connection.rb', line 355

def server?
  return @mode == :server
end

#to_ioObject

Support ‘to_io’ so you can use IO::select on this object.



275
276
277
# File 'lib/ftw/connection.rb', line 275

def to_io
  return @socket
end

#writable?(timeout) ⇒ Boolean

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.

Returns:

  • (Boolean)


255
256
257
258
# File 'lib/ftw/connection.rb', line 255

def writable?(timeout)
  readable, writable, errors = IO.select(nil, [@socket], nil, timeout)
  return !writable.nil?
end

#write(data, timeout = nil) ⇒ Object

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)



188
189
190
191
192
193
194
195
# File 'lib/ftw/connection.rb', line 188

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