PageRenderTime 43ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/excon/socket.rb

https://github.com/rinrinne/excon
Ruby | 185 lines | 148 code | 17 blank | 20 comment | 18 complexity | e9cc41029707535095b92031e542ca88 MD5 | raw file
  1. module Excon
  2. class Socket
  3. extend Forwardable
  4. attr_accessor :params
  5. def_delegators(:@socket, :close, :close)
  6. def_delegators(:@socket, :readline, :readline)
  7. def initialize(params = {}, proxy = nil)
  8. @params, @proxy = params, proxy
  9. @read_buffer = ''
  10. @eof = false
  11. connect
  12. end
  13. def connect
  14. @socket = nil
  15. exception = nil
  16. addrinfo = if @proxy
  17. ::Socket.getaddrinfo(@proxy[:host], @proxy[:port].to_i, ::Socket::Constants::AF_UNSPEC, ::Socket::Constants::SOCK_STREAM)
  18. else
  19. ::Socket.getaddrinfo(@params[:host], @params[:port].to_i, ::Socket::Constants::AF_UNSPEC, ::Socket::Constants::SOCK_STREAM)
  20. end
  21. addrinfo.each do |_, port, _, ip, a_family, s_type|
  22. # nonblocking connect
  23. begin
  24. sockaddr = ::Socket.sockaddr_in(port, ip)
  25. socket = ::Socket.new(a_family, s_type, 0)
  26. if @params[:nonblock]
  27. socket.connect_nonblock(sockaddr)
  28. else
  29. begin
  30. Timeout.timeout(@params[:connect_timeout]) do
  31. socket.connect(sockaddr)
  32. end
  33. rescue Timeout::Error
  34. raise Excon::Errors::Timeout.new('connect timeout reached')
  35. end
  36. end
  37. @socket = socket
  38. break
  39. rescue Errno::EINPROGRESS
  40. unless IO.select(nil, [socket], nil, @params[:connect_timeout])
  41. raise(Excon::Errors::Timeout.new("connect timeout reached"))
  42. end
  43. begin
  44. socket.connect_nonblock(sockaddr)
  45. @socket = socket
  46. break
  47. rescue Errno::EISCONN
  48. @socket = socket
  49. break
  50. rescue SystemCallError => exception
  51. socket.close
  52. next
  53. end
  54. rescue SystemCallError => exception
  55. socket.close
  56. next
  57. end
  58. end
  59. unless @socket
  60. # this will be our last encountered exception
  61. raise exception
  62. end
  63. end
  64. def read(max_length=nil)
  65. return nil if @eof
  66. if @eof
  67. ''
  68. elsif @params[:nonblock]
  69. begin
  70. if max_length
  71. until @read_buffer.length >= max_length
  72. @read_buffer << @socket.read_nonblock(max_length - @read_buffer.length)
  73. end
  74. else
  75. while true
  76. @read_buffer << @socket.read_nonblock(@params[:chunk_size])
  77. end
  78. end
  79. rescue OpenSSL::SSL::SSLError => error
  80. if error.message == 'read would block'
  81. if IO.select([@socket], nil, nil, @params[:read_timeout])
  82. retry
  83. else
  84. raise(Excon::Errors::Timeout.new("read timeout reached"))
  85. end
  86. else
  87. raise(error)
  88. end
  89. rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable
  90. if IO.select([@socket], nil, nil, @params[:read_timeout])
  91. retry
  92. else
  93. raise(Excon::Errors::Timeout.new("read timeout reached"))
  94. end
  95. rescue EOFError
  96. @eof = true
  97. end
  98. if max_length
  99. @read_buffer.slice!(0, max_length)
  100. else
  101. # read until EOFError, so return everything
  102. @read_buffer.slice!(0, @read_buffer.length)
  103. end
  104. else
  105. begin
  106. Timeout.timeout(@params[:read_timeout]) do
  107. @socket.read(max_length)
  108. end
  109. rescue Timeout::Error
  110. raise Excon::Errors::Timeout.new('read timeout reached')
  111. end
  112. end
  113. end
  114. def write(data)
  115. if @params[:nonblock]
  116. # We normally return from the return in the else block below, but
  117. # we guard that data is still something in case we get weird
  118. # values and String#[] returns nil. (This behavior has been observed
  119. # in the wild, so this is a simple defensive mechanism)
  120. while data
  121. begin
  122. # I wish that this API accepted a start position, then we wouldn't
  123. # have to slice data when there is a short write.
  124. written = @socket.write_nonblock(data)
  125. rescue OpenSSL::SSL::SSLError => error
  126. if error.message == 'write would block'
  127. if IO.select(nil, [@socket], nil, @params[:write_timeout])
  128. retry
  129. else
  130. raise(Excon::Errors::Timeout.new("write timeout reached"))
  131. end
  132. else
  133. raise(error)
  134. end
  135. rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitWritable
  136. if IO.select(nil, [@socket], nil, @params[:write_timeout])
  137. retry
  138. else
  139. raise(Excon::Errors::Timeout.new("write timeout reached"))
  140. end
  141. else
  142. # Fast, common case.
  143. # The >= seems weird, why would it have written MORE than we
  144. # requested. But we're getting some weird behavior when @socket
  145. # is an OpenSSL socket, where it seems like it's saying it wrote
  146. # more (perhaps due to SSL packet overhead?).
  147. #
  148. # Pretty weird, but this is a simple defensive mechanism.
  149. return if written >= data.size
  150. # This takes advantage of the fact that most ruby implementations
  151. # have Copy-On-Write strings. Thusly why requesting a subrange
  152. # of data, we actually don't copy data because the new string
  153. # simply references a subrange of the original.
  154. data = data[written, data.size]
  155. end
  156. end
  157. else
  158. begin
  159. Timeout.timeout(@params[:write_timeout]) do
  160. @socket.write(data)
  161. end
  162. rescue Timeout::Error
  163. Excon::Errors::Timeout.new('write timeout reached')
  164. end
  165. end
  166. end
  167. end
  168. end