diff options
Diffstat (limited to 'lib/net/protocol.rb')
| -rw-r--r-- | lib/net/protocol.rb | 255 |
1 files changed, 206 insertions, 49 deletions
diff --git a/lib/net/protocol.rb b/lib/net/protocol.rb index f908f3a1d6..8c81298c0e 100644 --- a/lib/net/protocol.rb +++ b/lib/net/protocol.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # # = net/protocol.rb # @@ -20,10 +21,13 @@ require 'socket' require 'timeout' +require 'io/wait' module Net # :nodoc: class Protocol #:nodoc: internal use only + VERSION = "0.2.2" + private def Protocol.protocol_param(name, val) module_eval(<<-End, __FILE__, __LINE__ + 1) @@ -32,9 +36,38 @@ module Net # :nodoc: end End end + + def ssl_socket_connect(s, timeout) + if timeout + while true + raise Net::OpenTimeout if timeout <= 0 + start = Process.clock_gettime Process::CLOCK_MONOTONIC + # to_io is required because SSLSocket doesn't have wait_readable yet + case s.connect_nonblock(exception: false) + when :wait_readable; s.to_io.wait_readable(timeout) + when :wait_writable; s.to_io.wait_writable(timeout) + else; break + end + timeout -= Process.clock_gettime(Process::CLOCK_MONOTONIC) - start + end + else + s.connect + end + end + + tcp_socket_parameters = TCPSocket.instance_method(:initialize).parameters + TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT = if tcp_socket_parameters != [[:rest]] + tcp_socket_parameters.include?([:key, :open_timeout]) + else + # Use Socket.tcp to find out since there is no parameters information for TCPSocket#initialize + # See discussion in https://github.com/ruby/net-http/pull/224 + Socket.method(:tcp).parameters.include?([:key, :open_timeout]) + end + private_constant :TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT end + # :stopdoc: class ProtocolError < StandardError; end class ProtoSyntaxError < ProtocolError; end class ProtoFatalError < ProtocolError; end @@ -44,18 +77,71 @@ module Net # :nodoc: class ProtoCommandError < ProtocolError; end class ProtoRetriableError < ProtocolError; end ProtocRetryError = ProtoRetriableError + # :startdoc: + + ## + # OpenTimeout, a subclass of Timeout::Error, is raised if a connection cannot + # be created within the open_timeout. + + class OpenTimeout < Timeout::Error; end + + ## + # ReadTimeout, a subclass of Timeout::Error, is raised if a chunk of the + # response cannot be read within the read_timeout. + + class ReadTimeout < Timeout::Error + # :stopdoc: + def initialize(io = nil) + @io = io + end + attr_reader :io + + def message + msg = super + if @io + msg = "#{msg} with #{@io.inspect}" + end + msg + end + end + + ## + # WriteTimeout, a subclass of Timeout::Error, is raised if a chunk of the + # response cannot be written within the write_timeout. Not raised on Windows. + + class WriteTimeout < Timeout::Error + # :stopdoc: + def initialize(io = nil) + @io = io + end + attr_reader :io + + def message + msg = super + if @io + msg = "#{msg} with #{@io.inspect}" + end + msg + end + end class BufferedIO #:nodoc: internal use only - def initialize(io) + def initialize(io, read_timeout: 60, write_timeout: 60, continue_timeout: nil, debug_output: nil) @io = io - @read_timeout = 60 - @debug_output = nil - @rbuf = '' + @read_timeout = read_timeout + @write_timeout = write_timeout + @continue_timeout = continue_timeout + @debug_output = debug_output + @rbuf = ''.b + @rbuf_empty = true + @rbuf_offset = 0 end attr_reader :io attr_accessor :read_timeout + attr_accessor :write_timeout + attr_accessor :continue_timeout attr_accessor :debug_output def inspect @@ -80,17 +166,20 @@ module Net # :nodoc: public - def read(len, dest = '', ignore_eof = false) + def read(len, dest = ''.b, ignore_eof = false) LOG "reading #{len} bytes..." read_bytes = 0 begin - while read_bytes + @rbuf.size < len - dest << (s = rbuf_consume(@rbuf.size)) - read_bytes += s.size + while read_bytes + rbuf_size < len + if s = rbuf_consume_all + read_bytes += s.bytesize + dest << s + end rbuf_fill end - dest << (s = rbuf_consume(len - read_bytes)) - read_bytes += s.size + s = rbuf_consume(len - read_bytes) + read_bytes += s.bytesize + dest << s rescue EOFError raise unless ignore_eof end @@ -98,13 +187,15 @@ module Net # :nodoc: dest end - def read_all(dest = '') + def read_all(dest = ''.b) LOG 'reading all...' read_bytes = 0 begin while true - dest << (s = rbuf_consume(@rbuf.size)) - read_bytes += s.size + if s = rbuf_consume_all + read_bytes += s.bytesize + dest << s + end rbuf_fill end rescue EOFError @@ -115,14 +206,16 @@ module Net # :nodoc: end def readuntil(terminator, ignore_eof = false) + offset = @rbuf_offset begin - until idx = @rbuf.index(terminator) + until idx = @rbuf.index(terminator, offset) + offset = @rbuf.bytesize rbuf_fill end - return rbuf_consume(idx + terminator.size) + return rbuf_consume(idx + terminator.bytesize - @rbuf_offset) rescue EOFError raise unless ignore_eof - return rbuf_consume(@rbuf.size) + return rbuf_consume end end @@ -135,27 +228,64 @@ module Net # :nodoc: BUFSIZE = 1024 * 16 def rbuf_fill - begin - @rbuf << @io.read_nonblock(BUFSIZE) - rescue IO::WaitReadable - if IO.select([@io], nil, nil, @read_timeout) - retry + tmp = @rbuf_empty ? @rbuf : nil + case rv = @io.read_nonblock(BUFSIZE, tmp, exception: false) + when String + @rbuf_empty = false + if rv.equal?(tmp) + @rbuf_offset = 0 else - raise Timeout::Error + @rbuf << rv + rv.clear end - rescue IO::WaitWritable + return + when :wait_readable + (io = @io.to_io).wait_readable(@read_timeout) or raise Net::ReadTimeout.new(io) + # continue looping + when :wait_writable # OpenSSL::Buffering#read_nonblock may fail with IO::WaitWritable. # http://www.openssl.org/support/faq.html#PROG10 - if IO.select(nil, [@io], nil, @read_timeout) - retry - else - raise Timeout::Error - end + (io = @io.to_io).wait_writable(@read_timeout) or raise Net::ReadTimeout.new(io) + # continue looping + when nil + raise EOFError, 'end of file reached' + end while true + end + + def rbuf_flush + if @rbuf_empty + @rbuf.clear + @rbuf_offset = 0 end + nil end - def rbuf_consume(len) - s = @rbuf.slice!(0, len) + def rbuf_size + @rbuf.bytesize - @rbuf_offset + end + + def rbuf_consume_all + rbuf_consume if rbuf_size > 0 + end + + def rbuf_consume(len = nil) + if @rbuf_offset == 0 && (len.nil? || len == @rbuf.bytesize) + s = @rbuf + @rbuf = ''.b + @rbuf_offset = 0 + @rbuf_empty = true + elsif len.nil? + s = @rbuf.byteslice(@rbuf_offset..-1) + @rbuf = ''.b + @rbuf_offset = 0 + @rbuf_empty = true + else + s = @rbuf.byteslice(@rbuf_offset, len) + @rbuf_offset += len + @rbuf_empty = @rbuf_offset == @rbuf.bytesize + rbuf_flush + end + @debug_output << %Q[-> #{s.dump}\n] if @debug_output s end @@ -166,9 +296,9 @@ module Net # :nodoc: public - def write(str) + def write(*strs) writing { - write0 str + write0(*strs) } end @@ -192,11 +322,34 @@ module Net # :nodoc: bytes end - def write0(str) - @debug_output << str.dump if @debug_output - len = @io.write(str) - @written_bytes += len - len + def write0(*strs) + @debug_output << strs.map(&:dump).join if @debug_output + orig_written_bytes = @written_bytes + strs.each_with_index do |str, i| + need_retry = true + case len = @io.write_nonblock(str, exception: false) + when Integer + @written_bytes += len + len -= str.bytesize + if len == 0 + if strs.size == i+1 + return @written_bytes - orig_written_bytes + else + need_retry = false + # next string + end + elsif len < 0 + str = str.byteslice(len, -len) + else # len > 0 + need_retry = false + # next string + end + # continue looping + when :wait_writable + (io = @io.to_io).wait_writable(@write_timeout) or raise Net::WriteTimeout.new(io) + # continue looping + end while need_retry + end end # @@ -222,7 +375,7 @@ module Net # :nodoc: class InternetMessageIO < BufferedIO #:nodoc: internal use only - def initialize(io) + def initialize(*, **) super @wbuf = nil end @@ -237,7 +390,7 @@ module Net # :nodoc: read_bytes = 0 while (line = readuntil("\r\n")) != ".\r\n" read_bytes += line.size - yield line.sub(/\A\./, '') + yield line.delete_prefix('.') end LOG_on() LOG "read message (#{read_bytes} bytes)" @@ -253,7 +406,7 @@ module Net # :nodoc: def write_message_0(src) prev = @written_bytes each_crlf_line(src) do |line| - write0 line.sub(/\A\./, '..') + write0 dot_stuff(line) end @written_bytes - prev end @@ -281,7 +434,7 @@ module Net # :nodoc: len = writing { using_each_crlf_line { begin - block.call(WriteAdapter.new(self, :write_message_0)) + block.call(WriteAdapter.new(self.method(:write_message_0))) rescue LocalJumpError # allow `break' from writer block end @@ -294,11 +447,15 @@ module Net # :nodoc: private + def dot_stuff(s) + s.sub(/\A\./, '..') + end + def using_each_crlf_line - @wbuf = '' + @wbuf = ''.b yield if not @wbuf.empty? # unterminated last line - write0 @wbuf.chomp + "\r\n" + write0 dot_stuff(@wbuf.chomp) + "\r\n" elsif @written_bytes == 0 # empty src write0 "\r\n" end @@ -308,7 +465,7 @@ module Net # :nodoc: def each_crlf_line(src) buffer_filling(@wbuf, src) do - while line = @wbuf.slice!(/\A.*(?:\n|\r\n|\r(?!\z))/n) + while line = @wbuf.slice!(/\A[^\r\n]*(?:\n|\r(?:\n|(?!\z)))/) yield line.chomp("\n") + "\r\n" end end @@ -341,17 +498,17 @@ module Net # :nodoc: # The writer adapter class # class WriteAdapter - def initialize(socket, method) - @socket = socket - @method_id = method + # :stopdoc: + def initialize(writer) + @writer = writer end def inspect - "#<#{self.class} socket=#{@socket.inspect}>" + "#<#{self.class} writer=#{@writer.inspect}>" end def write(str) - @socket.__send__(@method_id, str) + @writer.call(str) end alias print write |
