class Excon::Socket

Constants

CONNECT_RETRY_EXCEPTION_CLASSES

read/write drawn from github.com/ruby-amqp/bunny/commit/75d9dd79551b31a5dd3d1254c537bad471f108cf

READ_RETRY_EXCEPTION_CLASSES
WRITE_RETRY_EXCEPTION_CLASSES

Attributes

data[RW]
remote_ip[R]

Public Class Methods

new(data = {}) click to toggle source
# File lib/excon/socket.rb, line 44
def initialize(data = {})
  @data = data
  @nonblock = data[:nonblock]
  @port ||= @data[:port] || 80
  @read_buffer = String.new
  @eof = false
  @backend_eof = false
  connect
end

Public Instance Methods

local_address() click to toggle source
# File lib/excon/socket.rb, line 101
def local_address
  unpacked_sockaddr[1]
end
local_port() click to toggle source
# File lib/excon/socket.rb, line 105
def local_port
  unpacked_sockaddr[0]
end
params() click to toggle source
# File lib/excon/socket.rb, line 29
def params
  Excon.display_warning('Excon::Socket#params is deprecated use Excon::Socket#data instead.')
  @data
end
params=(new_params) click to toggle source
# File lib/excon/socket.rb, line 34
def params=(new_params)
  Excon.display_warning('Excon::Socket#params= is deprecated use Excon::Socket#data= instead.')
  @data = new_params
end
read(max_length = nil) click to toggle source
# File lib/excon/socket.rb, line 54
def read(max_length = nil)
  if @eof
    return max_length ? nil : ''
  elsif @nonblock
    read_nonblock(max_length)
  else
    read_block(max_length)
  end
end
readline() click to toggle source
# File lib/excon/socket.rb, line 64
def readline
  if @nonblock && RUBY_VERSION.to_f > 1.8_7
    result = String.new
    block = @read_buffer
    @read_buffer = String.new

    loop do
      idx = block.index("\n")
      if idx.nil?
        result << block
      else
        result << block.slice!(0, idx+1)
        add_to_read_buffer(block)
        break
      end
      block = read_nonblock(@data[:chunk_size]) || raise(EOFError)
    end
    result
  else # nonblock/legacy
    begin
      Timeout.timeout(@data[:read_timeout]) do
        @socket.readline
      end
    rescue Timeout::Error
      raise Excon::Errors::Timeout.new('read timeout reached')
    end
  end
end
write(data) click to toggle source
# File lib/excon/socket.rb, line 93
def write(data)
  if @nonblock
    write_nonblock(data)
  else
    write_block(data)
  end
end

Private Instance Methods

add_to_read_buffer(str) click to toggle source
# File lib/excon/socket.rb, line 190
def add_to_read_buffer(str)
  @read_buffer << str
  @eof = false
end
connect() click to toggle source
# File lib/excon/socket.rb, line 111
def connect
  @socket = nil
  exception = nil
  hostname = @data[:hostname]
  port = @port
  family = @data[:family]

  if @data[:proxy]
    hostname = @data[:proxy][:hostname]
    port = @data[:proxy][:port]
    family = @data[:proxy][:family]
  end

  Resolv.each_address(hostname) do |ip|
    # already succeeded on previous addrinfo
    if @socket
      break
    end

    @remote_ip = ip
    @data[:remote_ip] = ip

    # nonblocking connect
    begin
      sockaddr = ::Socket.sockaddr_in(port, ip)
      addrinfo = Addrinfo.getaddrinfo(ip, port, family, :STREAM).first
      socket = ::Socket.new(addrinfo.pfamily, addrinfo.socktype, addrinfo.protocol)

      if @data[:reuseaddr]
        socket.setsockopt(::Socket::Constants::SOL_SOCKET, ::Socket::Constants::SO_REUSEADDR, true)
        if defined?(::Socket::Constants::SO_REUSEPORT)
          socket.setsockopt(::Socket::Constants::SOL_SOCKET, ::Socket::Constants::SO_REUSEPORT, true)
        end
      end

      if @nonblock
        socket.connect_nonblock(sockaddr)
      else
        socket.connect(sockaddr)
      end
      @socket = socket
    rescue *CONNECT_RETRY_EXCEPTION_CLASSES
      select_with_timeout(socket, :connect_write)
      begin
        socket.connect_nonblock(sockaddr)
        @socket = socket
      rescue Errno::EISCONN
        @socket = socket
      rescue SystemCallError => exception
        socket.close rescue nil
      end
    rescue SystemCallError => exception
      socket.close rescue nil if socket
    end
  end

  exception ||= Resolv::ResolvError.new("no address for #{hostname}")

  # this will be our last encountered exception
  fail exception unless @socket

  if @data[:tcp_nodelay]
    @socket.setsockopt(::Socket::IPPROTO_TCP,
                       ::Socket::TCP_NODELAY,
                       true)
  end

  if @data[:keepalive]
    if [:SOL_SOCKET, :SO_KEEPALIVE, :SOL_TCP, :TCP_KEEPIDLE, :TCP_KEEPINTVL, :TCP_KEEPCNT].all?{|c| ::Socket.const_defined? c}
      @socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, true)
      @socket.setsockopt(::Socket::SOL_TCP, ::Socket::TCP_KEEPIDLE, @data[:keepalive][:time])
      @socket.setsockopt(::Socket::SOL_TCP, ::Socket::TCP_KEEPINTVL, @data[:keepalive][:intvl])
      @socket.setsockopt(::Socket::SOL_TCP, ::Socket::TCP_KEEPCNT, @data[:keepalive][:probes])
    else
      Excon.display_warning('Excon::Socket keepalive was set, but is not supported by Ruby version.')
    end
  end
end
read_block(max_length) click to toggle source
# File lib/excon/socket.rb, line 237
def read_block(max_length)
  @socket.read(max_length)
rescue OpenSSL::SSL::SSLError => error
  if error.message == 'read would block'
    select_with_timeout(@socket, :read) && retry
  else
    raise(error)
  end
rescue *READ_RETRY_EXCEPTION_CLASSES
  select_with_timeout(@socket, :read) && retry
rescue EOFError
  @eof = true
end
read_nonblock(max_length) click to toggle source
# File lib/excon/socket.rb, line 195
def read_nonblock(max_length)
  begin
    if max_length
      until @backend_eof || @read_buffer.length >= max_length
        @read_buffer << @socket.read_nonblock(max_length - @read_buffer.length)
      end
    else
      while !@backend_eof
        @read_buffer << @socket.read_nonblock(@data[:chunk_size])
      end
    end
  rescue OpenSSL::SSL::SSLError => error
    if error.message == 'read would block'
      if @read_buffer.empty?
        select_with_timeout(@socket, :read) && retry
      end
    else
      raise(error)
    end
  rescue *READ_RETRY_EXCEPTION_CLASSES
    if @read_buffer.empty?
      # if we didn't read anything, try again...
      select_with_timeout(@socket, :read) && retry
    end
  rescue EOFError
    @backend_eof = true
  end

  ret = if max_length
    if @read_buffer.empty?
      nil # EOF met at beginning
    else
      @read_buffer.slice!(0, max_length)
    end
  else
    # read until EOFError, so return everything
    @read_buffer.slice!(0, @read_buffer.length)
  end
  @eof = @backend_eof && @read_buffer.empty?
  ret
end
select_with_timeout(socket, type) click to toggle source
# File lib/excon/socket.rb, line 296
def select_with_timeout(socket, type)
  select = case type
  when :connect_read
    IO.select([socket], nil, nil, @data[:connect_timeout])
  when :connect_write
    IO.select(nil, [socket], nil, @data[:connect_timeout])
  when :read
    IO.select([socket], nil, nil, @data[:read_timeout])
  when :write
    IO.select(nil, [socket], nil, @data[:write_timeout])
  end
  select || raise(Excon::Errors::Timeout.new("#{type} timeout reached"))
end
unpacked_sockaddr() click to toggle source
# File lib/excon/socket.rb, line 310
def unpacked_sockaddr
  @unpacked_sockaddr ||= ::Socket.unpack_sockaddr_in(@socket.to_io.getsockname)
rescue ArgumentError => e
  unless e.message == 'not an AF_INET/AF_INET6 sockaddr'
    raise
  end
end
write_block(data) click to toggle source
# File lib/excon/socket.rb, line 286
def write_block(data)
  @socket.write(data)
rescue OpenSSL::SSL::SSLError, *WRITE_RETRY_EXCEPTION_CLASSES => error
  if error.is_a?(OpenSSL::SSL::SSLError) && error.message != 'write would block'
    raise error
  else
    select_with_timeout(@socket, :write) && retry
  end
end
write_nonblock(data) click to toggle source
# File lib/excon/socket.rb, line 251
def write_nonblock(data)
  data = binary_encode(data)
  loop do
    written = nil
    begin
      # I wish that this API accepted a start position, then we wouldn't
      # have to slice data when there is a short write.
      written = @socket.write_nonblock(data)
    rescue Errno::EFAULT => error
      if OpenSSL.const_defined?(:OPENSSL_LIBRARY_VERSION) && OpenSSL::OPENSSL_LIBRARY_VERSION.split(' ')[1] == '1.0.2'
        msg = "The version of OpenSSL this ruby is built against (1.0.2) has a vulnerability
               which causes a fault. For more, see https://github.com/excon/excon/issues/467"
        raise SecurityError.new(msg)
      else
        raise error
      end
    rescue OpenSSL::SSL::SSLError, *WRITE_RETRY_EXCEPTION_CLASSES => error
      if error.is_a?(OpenSSL::SSL::SSLError) && error.message != 'write would block'
        raise error
      else
        select_with_timeout(@socket, :write) && retry
      end
    end

    # Fast, common case.
    break if written == data.size

    # This takes advantage of the fact that most ruby implementations
    # have Copy-On-Write strings. Thusly why requesting a subrange
    # of data, we actually don't copy data because the new string
    # simply references a subrange of the original.
    data = data[written, data.size]
  end
end