diff options
Diffstat (limited to 'lib/net/protocol.rb')
| -rw-r--r-- | lib/net/protocol.rb | 1098 |
1 files changed, 391 insertions, 707 deletions
diff --git a/lib/net/protocol.rb b/lib/net/protocol.rb index 343721add3..8c81298c0e 100644 --- a/lib/net/protocol.rb +++ b/lib/net/protocol.rb @@ -1,279 +1,73 @@ -=begin - -= net/protocol.rb version 1.1.34 - -written by Minero Aoki <aamine@dp.u-netsurf.ne.jp> - -This program is free software. -You can distribute/modify this program under -the terms of the Ruby Distribute License. - -Japanese version of this document is in "net" full package. -You can get it from RAA -(Ruby Application Archive: http://www.ruby-lang.org/en/raa.html). - - -== Net::Protocol - -the abstract class for Internet protocol - -=== Super Class - -Object - -=== Class Methods - -: new( address = 'localhost', port = nil ) - This method Creates a new protocol object. - -: start( address = 'localhost', port = nil, *protoargs ) -: start( address = 'localhost', port = nil, *protoargs ) {|proto| .... } - This method creates a new Protocol object and opens a session. - equals to Net::Protocol.new( address, port ).start( *protoargs ) - -=== Methods - -: address - the address of connecting server (FQDN). - -: port - connecting port number - -: start( *args ) -: start( *args ) {|proto| .... } - This method starts protocol. If protocol was already started, - do nothing and returns false. - - '*args' are specified in subclasses. - - When is called with block, gives Protocol object to block and - close session when block finished. - -: finish - This method ends protocol. If you call this method before protocol starts, - it only return false without doing anything. - -: active? - true if session have been started - -=end +# frozen_string_literal: true +# +# = net/protocol.rb +# +#-- +# Copyright (c) 1999-2004 Yukihiro Matsumoto +# Copyright (c) 1999-2004 Minero Aoki +# +# written and maintained by Minero Aoki <aamine@loveruby.net> +# +# This program is free software. You can re-distribute and/or +# modify this program under the same terms as Ruby itself, +# Ruby Distribute License or GNU General Public License. +# +# $Id$ +#++ +# +# WARNING: This file is going to remove. +# Do not rely on the implementation written in this file. +# require 'socket' require 'timeout' +require 'io/wait' +module Net # :nodoc: -module Net + class Protocol #:nodoc: internal use only + VERSION = "0.2.2" - module NetPrivate - end - - def self.net_private( &block ) - ::Net::NetPrivate.module_eval( &block ) - end - - - class Protocol - - Version = '1.1.34' - - - class << self - - def start( address = 'localhost', port = nil, *args ) - instance = new( address, port ) - - if block_given? then - instance.start( *args ) { yield instance } - else - instance.start( *args ) - instance + private + def Protocol.protocol_param(name, val) + module_eval(<<-End, __FILE__, __LINE__ + 1) + def #{name} + #{val} end - end - - private - - def protocol_param( name, val ) - module_eval %- - def self.#{name.id2name} - #{val} + 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 - - - end - - end - - - # - # sub-class requirements - # - # protocol_param command_type - # protocol_param port - # - # private method do_start (optional) - # private method do_finish (optional) - # - - protocol_param :port, 'nil' - protocol_param :command_type, 'nil' - protocol_param :socket_type, '::Net::NetPrivate::Socket' - - - def initialize( addr = nil, port = nil ) - @address = addr || 'localhost' - @port = port || type.port - - @command = nil - @socket = nil - - @active = false - - @open_timeout = nil - @read_timeout = nil - - @dout = nil - end - - attr_reader :address - attr_reader :port - - attr_reader :command - attr_reader :socket - - attr_accessor :open_timeout - attr_accessor :read_timeout - - def active? - @active - end - - def set_debug_output( arg ) # un-documented - @dout = arg - end - - alias set_pipe set_debug_output - - def inspect - "#<#{type} #{address}:#{port} open=#{active?}>" - end - - # - # open session - # - - def start( *args ) - return false if active? - - if block_given? then - begin - _start args - yield self - ensure - finish + timeout -= Process.clock_gettime(Process::CLOCK_MONOTONIC) - start end else - _start args - end - end - - private - - def _start( args ) - connect - do_start( *args ) - @active = true - end - - def connect - conn_socket @address, @port - conn_command @socket - on_connect - end - - def re_connect - @socket.reopen @open_timeout - on_connect - end - - def conn_socket( addr, port ) - @socket = type.socket_type.open( - addr, port, @open_timeout, @read_timeout, @dout ) - end - - def conn_command( sock ) - @command = type.command_type.new( sock ) - end - - def on_connect - end - - def do_start - end - - # - # close session - # - - public - - def finish - return false unless active? - - do_finish if @command and not @command.critical? - disconnect - @active = false - true - end - - private - - def do_finish - @command.quit - end - - def disconnect - @command = nil - if @socket and not @socket.closed? then - @socket.close + s.connect end - @socket = nil - on_disconnect - end - - def on_disconnect - end - - end - - Session = Protocol - - - net_private { - - class Response - - def initialize( ctype, cno, msg ) - @code_type = ctype - @code = cno - @message = msg - super() end - attr_reader :code_type, :code, :message - alias msg message - - def inspect - "#<#{type} #{code}>" - end - - def error!( data = nil ) - raise code_type.error_type.new( code + ' ' + Net.quote(msg), data ) + 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 @@ -283,593 +77,483 @@ module Net class ProtoCommandError < ProtocolError; end class ProtoRetriableError < ProtocolError; end ProtocRetryError = ProtoRetriableError + # :startdoc: - class ProtocolError - - def initialize( msg, data = nil ) - super msg - @data = data - end - - attr :data - - def inspect - "#<#{type}>" - end - - end - + ## + # OpenTimeout, a subclass of Timeout::Error, is raised if a connection cannot + # be created within the open_timeout. - class Code + class OpenTimeout < Timeout::Error; end - def initialize( paren, err ) - @parents = paren - @err = err + ## + # ReadTimeout, a subclass of Timeout::Error, is raised if a chunk of the + # response cannot be read within the read_timeout. - @parents.push self + class ReadTimeout < Timeout::Error + # :stopdoc: + def initialize(io = nil) + @io = io end + attr_reader :io - attr_reader :parents - - def inspect - "#<#{type}>" - end - - def error_type - @err - end - - def ===( response ) - response.code_type.parents.reverse_each {|i| return true if i == self } - false - end - - def mkchild( err = nil ) - type.new( @parents + [self], err || @err ) - end - - end - - ReplyCode = Code.new( [], ProtoUnknownError ) - InformationCode = ReplyCode.mkchild( ProtoUnknownError ) - SuccessCode = ReplyCode.mkchild( ProtoUnknownError ) - ContinueCode = ReplyCode.mkchild( ProtoUnknownError ) - ErrorCode = ReplyCode.mkchild( ProtocolError ) - SyntaxErrorCode = ErrorCode.mkchild( ProtoSyntaxError ) - FatalErrorCode = ErrorCode.mkchild( ProtoFatalError ) - ServerErrorCode = ErrorCode.mkchild( ProtoServerError ) - AuthErrorCode = ErrorCode.mkchild( ProtoAuthError ) - RetriableCode = ReplyCode.mkchild( ProtoRetriableError ) - UnknownCode = ReplyCode.mkchild( ProtoUnknownError ) - - - - net_private { - - class WriteAdapter - - def initialize( sock, mid ) - @sock = sock - @mid = mid - end - - def inspect - "#<#{type}>" - end - - def write( str ) - @sock.__send__ @mid, str - end - - def <<( str ) - @sock.__send__ @mid, str - self - end - - end - - class ReadAdapter - - def initialize( block ) - @block = block - end - - def inspect - "#<#{type}>" - end - - def <<( str ) - callblock( str, &@block ) if @block - end - - private - - def callblock( str ) - begin - user_break = true - yield str - user_break = false - rescue Exception - user_break = false - raise - ensure - if user_break then - @block = nil - return # stop break - end + 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 Command - - def initialize( sock ) - @socket = sock - @last_reply = nil - @critical = false + class WriteTimeout < Timeout::Error + # :stopdoc: + def initialize(io = nil) + @io = io end + attr_reader :io - attr_accessor :socket - attr_reader :last_reply - - def inspect - "#<#{type}>" - end - - # abstract quit - - - private - - # abstract get_reply() - - def check_reply( *oks ) - @last_reply = get_reply - reply_must( @last_reply, *oks ) - end - - def reply_must( rep, *oks ) - oks.each do |i| - if i === rep then - return rep - end + def message + msg = super + if @io + msg = "#{msg} with #{@io.inspect}" end - rep.error! - end - - def getok( line, expect = SuccessCode ) - @socket.writeline line - check_reply expect - end - - - # - # error handle - # - - public - - def critical? - @critical - end - - def error_ok - @critical = false - end - - - private - - def critical - @critical = true - ret = yield - @critical = false - ret + msg end - - def begin_critical - ret = @critical - @critical = true - not ret - end - - def end_critical - @critical = false - end - end - class Socket - - def initialize( addr, port, otime = nil, rtime = nil, dout = nil ) - @addr = addr - @port = port - - @read_timeout = rtime - - @debugout = dout - - @socket = nil - @sending = '' - @buffer = '' - - connect otime - D 'opened' + class BufferedIO #:nodoc: internal use only + def initialize(io, read_timeout: 60, write_timeout: 60, continue_timeout: nil, debug_output: nil) + @io = io + @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 - def connect( otime ) - D "opening connection to #{@addr}..." - timeout( otime ) { - @socket = TCPsocket.new( @addr, @port ) - } - end - private :connect - - attr :pipe, true - - class << self - alias open new - end + attr_reader :io + attr_accessor :read_timeout + attr_accessor :write_timeout + attr_accessor :continue_timeout + attr_accessor :debug_output def inspect - "#<#{type} #{closed? ? 'closed' : 'opened'}>" + "#<#{self.class} io=#{@io}>" end - def reopen( otime = nil ) - D 'reopening...' - close - connect otime - D 'reopened' - end - - attr :socket, true - - def close - if @socket then - @socket.close - D 'closed' - else - D 'close call for already closed socket' - end - @socket = nil - @buffer = '' + def eof? + @io.eof? end def closed? - not @socket - end - - def address - @addr.dup + @io.closed? end - alias addr address - - attr_reader :port - - def ip_address - @socket or return '' - @socket.addr[3] + def close + @io.close end - alias ipaddr ip_address - - attr_reader :sending - - # - # read + # Read # public - CRLF = "\r\n" - - def read( len, dest = '', ignerr = false ) - D_off "reading #{len} bytes..." - - rsize = 0 + def read(len, dest = ''.b, ignore_eof = false) + LOG "reading #{len} bytes..." + read_bytes = 0 begin - while rsize + @buffer.size < len do - rsize += rbuf_moveto( dest, @buffer.size ) + while read_bytes + rbuf_size < len + if s = rbuf_consume_all + read_bytes += s.bytesize + dest << s + end rbuf_fill end - rbuf_moveto dest, len - rsize + s = rbuf_consume(len - read_bytes) + read_bytes += s.bytesize + dest << s rescue EOFError - raise unless igneof + raise unless ignore_eof end - - D_on "read #{len} bytes" + LOG "read #{read_bytes} bytes" dest end - def read_all( dest = '' ) - D_off 'reading all...' - - rsize = 0 + def read_all(dest = ''.b) + LOG 'reading all...' + read_bytes = 0 begin - while true do - rsize += rbuf_moveto( dest, @buffer.size ) + while true + if s = rbuf_consume_all + read_bytes += s.bytesize + dest << s + end rbuf_fill end rescue EOFError ; end - - D_on "read #{rsize} bytes" + LOG "read #{read_bytes} bytes" dest end - def readuntil( target, igneof = false ) - dest = '' + def readuntil(terminator, ignore_eof = false) + offset = @rbuf_offset begin - while true do - idx = @buffer.index( target ) - break if idx + until idx = @rbuf.index(terminator, offset) + offset = @rbuf.bytesize rbuf_fill end - rbuf_moveto dest, idx + target.size + return rbuf_consume(idx + terminator.bytesize - @rbuf_offset) rescue EOFError - raise unless igneof - rbuf_moveto dest, @buffer.size - end - dest - end - - def readline - ret = readuntil( "\n" ) - ret.chop! - ret - end - - def read_pendstr( dest ) - D_off 'reading text...' - - rsize = 0 - while (str = readuntil("\r\n")) != ".\r\n" do - rsize += str.size - str.gsub!( /\A\./, '' ) - dest << str + raise unless ignore_eof + return rbuf_consume end - - D_on "read #{rsize} bytes" - dest end - # private use only (can not handle 'break') - def read_pendlist - D_off 'reading list...' - - str = nil - i = 0 - while (str = readuntil("\r\n")) != ".\r\n" do - i += 1 - str.chop! - yield str - end - - D_on "read #{i} items" + def readline + readuntil("\n").chop end - private - - READ_SIZE = 1024 * 4 + BUFSIZE = 1024 * 16 def rbuf_fill - unless IO.select [@socket], nil, nil, @read_timeout then - on_read_timeout + 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 + @rbuf << rv + rv.clear + end + 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 + (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 - @buffer << @socket.sysread( READ_SIZE ) + nil end - def on_read_timeout - raise TimeoutError, "socket read timeout (#{@read_timeout} sec)" + def rbuf_size + @rbuf.bytesize - @rbuf_offset end - def rbuf_moveto( dest, len ) - bsi = @buffer.size - s = @buffer[ 0, len ] - dest << s - @buffer = @buffer[ len, bsi - len ] - - @debugout << %<read "#{Net.quote s}"\n> if @debugout - len + 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 # - # write interfece + # Write # public - def write( str ) + def write(*strs) writing { - do_write str + write0(*strs) } end - def writeline( str ) + alias << write + + def writeline(str) writing { - do_write str + "\r\n" + write0 str + "\r\n" } end - def write_bin( src, block ) - writing { - if block then - block.call ::Net::NetPrivate::WriteAdapter.new( self, :do_write ) - else - src.each do |bin| - do_write bin + private + + def writing + @written_bytes = 0 + @debug_output << '<- ' if @debug_output + yield + @debug_output << "\n" if @debug_output + bytes = @written_bytes + @written_bytes = nil + bytes + end + + 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 - 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 - def write_pendstr( src, block ) - D_off "writing text from #{src.type}" + # + # Logging + # - wsize = use_each_crlf_line { - if block then - block.call ::Net::NetPrivate::WriteAdapter.new( self, :wpend_in ) - else - wpend_in src - end - } + private - D_on "wrote #{wsize} bytes text" - wsize + def LOG_off + @save_debug_out = @debug_output + @debug_output = nil end + def LOG_on + @debug_output = @save_debug_out + end - private + def LOG(msg) + return unless @debug_output + @debug_output << msg + "\n" + end + end - def wpend_in( src ) - line = nil - pre = @writtensize - each_crlf_line( src ) do |line| - do_write '.' if line[0] == ?. - do_write line + class InternetMessageIO < BufferedIO #:nodoc: internal use only + def initialize(*, **) + super + @wbuf = nil + end + + # + # Read + # + + def each_message_chunk + LOG 'reading message...' + LOG_off() + read_bytes = 0 + while (line = readuntil("\r\n")) != ".\r\n" + read_bytes += line.size + yield line.delete_prefix('.') end + LOG_on() + LOG "read message (#{read_bytes} bytes)" + end - @writtensize - pre + # *library private* (cannot handle 'break') + def each_list_item + while (str = readuntil("\r\n")) != ".\r\n" + yield str.chop + end end - def use_each_crlf_line - writing { - @wbuf = '' + def write_message_0(src) + prev = @written_bytes + each_crlf_line(src) do |line| + write0 dot_stuff(line) + end + @written_bytes - prev + end - yield + # + # Write + # - if not @wbuf.empty? then # un-terminated last line - if @wbuf[-1] == ?\r then - @wbuf.chop! - end - @wbuf.concat "\r\n" - do_write @wbuf - elsif @writtensize == 0 then # empty src - do_write "\r\n" - end - do_write ".\r\n" + def write_message(src) + LOG "writing message from #{src.class}" + LOG_off() + len = writing { + using_each_crlf_line { + write_message_0 src + } + } + LOG_on() + LOG "wrote #{len} bytes" + len + end - @wbuf = nil + def write_message_by_block(&block) + LOG 'writing message from block' + LOG_off() + len = writing { + using_each_crlf_line { + begin + block.call(WriteAdapter.new(self.method(:write_message_0))) + rescue LocalJumpError + # allow `break' from writer block + end + } } + LOG_on() + LOG "wrote #{len} bytes" + len end - def each_crlf_line( src ) - str = m = beg = nil + private - adding( src ) do - beg = 0 - buf = @wbuf - while buf.index( /\n|\r\n|\r/, beg ) do - m = Regexp.last_match - if m.begin(0) == buf.size - 1 and buf[-1] == ?\r then - # "...\r" : can follow "\n..." - break - end - str = buf[ beg ... m.begin(0) ] - str.concat "\r\n" - yield str - beg = m.end(0) - end - @wbuf = buf[ beg ... buf.size ] + def dot_stuff(s) + s.sub(/\A\./, '..') + end + + def using_each_crlf_line + @wbuf = ''.b + yield + if not @wbuf.empty? # unterminated last line + write0 dot_stuff(@wbuf.chomp) + "\r\n" + elsif @written_bytes == 0 # empty src + write0 "\r\n" end + write0 ".\r\n" + @wbuf = nil end - def adding( src ) - i = nil + def each_crlf_line(src) + buffer_filling(@wbuf, src) do + while line = @wbuf.slice!(/\A[^\r\n]*(?:\n|\r(?:\n|(?!\z)))/) + yield line.chomp("\n") + "\r\n" + end + end + end + def buffer_filling(buf, src) case src - when String - 0.step( src.size - 1, 2048 ) do |i| - @wbuf << src[i,2048] + when String # for speeding up. + 0.step(src.size - 1, 1024) do |i| + buf << src[i, 1024] yield end - - when File - while true do - i = src.read( 2048 ) - break unless i - i[0,0] = @wbuf - @wbuf = i + when File # for speeding up. + while s = src.read(1024) + buf << s yield end - - else - src.each do |i| - @wbuf << i - if @wbuf.size > 2048 then - yield - end + else # generic reader + src.each do |str| + buf << str + yield if buf.size > 1024 end - yield unless @wbuf.empty? + yield unless buf.empty? end end + end - def writing - @writtensize = 0 - @sending = '' + # + # The writer adapter class + # + class WriteAdapter + # :stopdoc: + def initialize(writer) + @writer = writer + end - yield + def inspect + "#<#{self.class} writer=#{@writer.inspect}>" + end - if @debugout then - @debugout << 'write "' - @debugout << @sending - @debugout << "\"\n" - end - @socket.flush - @writtensize + def write(str) + @writer.call(str) end - def do_write( arg ) - if @debugout or @sending.size < 128 then - @sending << Net.quote( arg ) - else - @sending << '...' unless @sending[-1] == ?. - end + alias print write - s = @socket.write( arg ) - @writtensize += s - s + def <<(str) + write str + self + end + + def puts(str = '') + write str.chomp("\n") + "\n" end + def printf(*args) + write sprintf(*args) + end + end - def D_off( msg ) - D msg - @savedo, @debugout = @debugout, nil + + class ReadAdapter #:nodoc: internal use only + def initialize(block) + @block = block end - def D_on( msg ) - @debugout = @savedo - D msg + def inspect + "#<#{self.class}>" end - def D( msg ) - @debugout or return - @debugout << msg - @debugout << "\n" + def <<(str) + call_block(str, &@block) if @block end - end + private - } + # This method is needed because @block must be called by yield, + # not Proc#call. You can see difference when using `break' in + # the block. + def call_block(str) + yield str + end + end - def Net.quote( str ) - str = str.gsub( "\n", '\\n' ) - str.gsub!( "\r", '\\r' ) - str.gsub!( "\t", '\\t' ) - str + module NetPrivate #:nodoc: obsolete + Socket = ::Net::InternetMessageIO end end # module Net |
