summaryrefslogtreecommitdiff
path: root/lib/net/protocol.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/net/protocol.rb')
-rw-r--r--lib/net/protocol.rb255
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