class Excon::Socket

Constants

CONNECT_RETRY_EXCEPTION_CLASSES

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

OPERATION_TO_TIMEOUT

Maps a socket operation to a timeout property.

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 51
def initialize(data = {})
  @data = data
  @nonblock = data[:nonblock]
  @port ||= @data[:port] || 80
  @read_buffer = String.new
  @read_offset = 0
  @eof = false
  @backend_eof = false

  connect
end

Public Instance Methods

local_address() click to toggle source
# File lib/excon/socket.rb, line 112
def local_address
  unpacked_sockaddr[1]
end
local_port() click to toggle source
# File lib/excon/socket.rb, line 116
def local_port
  unpacked_sockaddr[0]
end
params() click to toggle source
# File lib/excon/socket.rb, line 36
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 41
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 63
def read(max_length = nil)
  if @eof
    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 73
def readline
  if @nonblock
    result = String.new
    block = consume_read_buffer

    loop do
      idx = block.index("\n")

      if idx.nil?
        result << block
      else
        result << block[0..idx]
        rewind_read_buffer(block, idx)
        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 104
def write(data)
  if @nonblock
    write_nonblock(data)
  else
    write_block(data)
  end
end

Private Instance Methods

connect() click to toggle source
# File lib/excon/socket.rb, line 122
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

  resolver = @data[:resolv_resolver] || Resolv::DefaultResolver

  # Deprecated
  if @data[:dns_timeouts]
    Excon.display_warning('dns_timeouts is deprecated, use resolv_resolver instead.')
    dns_resolver = Resolv::DNS.new
    dns_resolver.timeouts = @data[:dns_timeouts]
    resolver = Resolv.new([Resolv::Hosts.new, dns_resolver])
  end

  resolver.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
consume_read_buffer() click to toggle source

Consume any bytes remaining in the read buffer before making a system call.

# File lib/excon/socket.rb, line 212
def consume_read_buffer
  block = @read_buffer[@read_offset..-1]

  @read_offset = @read_buffer.length

  block
end
read_block(max_length) click to toggle source
# File lib/excon/socket.rb, line 300
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 227
def read_nonblock(max_length)
  begin
    if @read_offset != 0 && @read_offset >= @read_buffer.length
      # Clear the buffer so we can test for emptiness below
      @read_buffer.clear
      # Reset the offset so it matches the length of the buffer when empty.
      @read_offset = 0
    end

    if max_length
      until @backend_eof || readable_bytes >= max_length
        if @read_buffer.empty?
          # Avoid allocating a new buffer string when the read buffer is empty
          @read_buffer = @socket.read_nonblock(max_length, @read_buffer)
        else
          @read_buffer << @socket.read_nonblock(max_length - readable_bytes)
        end
      end
    else
      until @backend_eof
        if @read_buffer.empty?
          # Avoid allocating a new buffer string when the read buffer is empty
          @read_buffer = @socket.read_nonblock(@data[:chunk_size], @read_buffer)
        else
          @read_buffer << @socket.read_nonblock(@data[:chunk_size])
        end
      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

  if max_length
    if @read_buffer.empty?
      # EOF met at beginning
      @eof = @backend_eof
      nil
    else
      start = @read_offset

      # Ensure that we can seek backwards when reading until a terminator string.
      # The read offset must never point past the end of the read buffer.
      @read_offset += max_length > readable_bytes ? readable_bytes : max_length
      @read_buffer[start...@read_offset]
    end
  else
    # read until EOFError, so return everything
    start = @read_offset

    @read_offset = @read_buffer.length
    @eof = @backend_eof

    @read_buffer[start..-1]
  end
end
readable_bytes() click to toggle source
# File lib/excon/socket.rb, line 296
def readable_bytes
  @read_buffer.length - @read_offset
end
request_time_remaining() click to toggle source

Returns the remaining time in seconds until we reach the deadline for the request timeout. Raises an exception if we have exceeded the request timeout’s deadline.

# File lib/excon/socket.rb, line 399
def request_time_remaining
  now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  deadline = @data[:deadline]

  raise(Excon::Errors::Timeout.new('request timeout reached')) if now >= deadline

  deadline - now
end
rewind_read_buffer(chunk, idx) click to toggle source

Rewind the read buffer to just after the given index. The offset is moved back to the start of the current chunk and then forward until just after the index.

# File lib/excon/socket.rb, line 222
def rewind_read_buffer(chunk, idx)
  @read_offset = @read_offset - chunk.length + (idx + 1)
  @eof = false
end
select_with_timeout(socket, type) click to toggle source
# File lib/excon/socket.rb, line 359
def select_with_timeout(socket, type)
  timeout_kind = type
  timeout = @data[OPERATION_TO_TIMEOUT[type]]

  # Check whether the request has a timeout configured.
  if @data.include?(:deadline)
    request_timeout = request_time_remaining

    # If the time remaining until the request times out is less than the timeout for the type of select,
    # use the time remaining as the timeout instead.
    if request_timeout < timeout
      timeout_kind = :request
      timeout = request_timeout
    end
  end

  select = case type
  when :connect_read
    IO.select([socket], nil, nil, timeout)
  when :connect_write
    IO.select(nil, [socket], nil, timeout)
  when :read
    IO.select([socket], nil, nil, timeout)
  when :write
    IO.select(nil, [socket], nil, timeout)
  end

  select || raise(Excon::Errors::Timeout.new("#{timeout_kind} timeout reached"))
end
unpacked_sockaddr() click to toggle source
# File lib/excon/socket.rb, line 389
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 349
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 314
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