"Fossies" - the Fresh Open Source Software Archive

Member "ruby-2.7.4/lib/net/ftp.rb" (7 Jul 2021, 43851 Bytes) of package /linux/misc/ruby-2.7.4.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Ruby source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "ftp.rb": 2.7.3_vs_2.7.4.

    1 # frozen_string_literal: true
    2 #
    3 # = net/ftp.rb - FTP Client Library
    4 #
    5 # Written by Shugo Maeda <shugo@ruby-lang.org>.
    6 #
    7 # Documentation by Gavin Sinclair, sourced from "Programming Ruby" (Hunt/Thomas)
    8 # and "Ruby In a Nutshell" (Matsumoto), used with permission.
    9 #
   10 # This library is distributed under the terms of the Ruby license.
   11 # You can freely distribute/modify this library.
   12 #
   13 # It is included in the Ruby standard library.
   14 #
   15 # See the Net::FTP class for an overview.
   16 #
   17 
   18 require "socket"
   19 require "monitor"
   20 require_relative "protocol"
   21 require "time"
   22 begin
   23   require "openssl"
   24 rescue LoadError
   25 end
   26 
   27 module Net
   28 
   29   # :stopdoc:
   30   class FTPError < StandardError; end
   31   class FTPReplyError < FTPError; end
   32   class FTPTempError < FTPError; end
   33   class FTPPermError < FTPError; end
   34   class FTPProtoError < FTPError; end
   35   class FTPConnectionError < FTPError; end
   36   # :startdoc:
   37 
   38   #
   39   # This class implements the File Transfer Protocol.  If you have used a
   40   # command-line FTP program, and are familiar with the commands, you will be
   41   # able to use this class easily.  Some extra features are included to take
   42   # advantage of Ruby's style and strengths.
   43   #
   44   # == Example
   45   #
   46   #   require 'net/ftp'
   47   #
   48   # === Example 1
   49   #
   50   #   ftp = Net::FTP.new('example.com')
   51   #   ftp.login
   52   #   files = ftp.chdir('pub/lang/ruby/contrib')
   53   #   files = ftp.list('n*')
   54   #   ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024)
   55   #   ftp.close
   56   #
   57   # === Example 2
   58   #
   59   #   Net::FTP.open('example.com') do |ftp|
   60   #     ftp.login
   61   #     files = ftp.chdir('pub/lang/ruby/contrib')
   62   #     files = ftp.list('n*')
   63   #     ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024)
   64   #   end
   65   #
   66   # == Major Methods
   67   #
   68   # The following are the methods most likely to be useful to users:
   69   # - FTP.open
   70   # - #getbinaryfile
   71   # - #gettextfile
   72   # - #putbinaryfile
   73   # - #puttextfile
   74   # - #chdir
   75   # - #nlst
   76   # - #size
   77   # - #rename
   78   # - #delete
   79   #
   80   class FTP < Protocol
   81     include MonitorMixin
   82     if defined?(OpenSSL::SSL)
   83       include OpenSSL
   84       include SSL
   85     end
   86 
   87     # :stopdoc:
   88     FTP_PORT = 21
   89     CRLF = "\r\n"
   90     DEFAULT_BLOCKSIZE = BufferedIO::BUFSIZE
   91     @@default_passive = true
   92     # :startdoc:
   93 
   94     # When +true+, transfers are performed in binary mode.  Default: +true+.
   95     attr_reader :binary
   96 
   97     # When +true+, the connection is in passive mode.  Default: +true+.
   98     attr_accessor :passive
   99 
  100     # When +true+, use the IP address in PASV responses.  Otherwise, it uses
  101     # the same IP address for the control connection.  Default: +false+.
  102     attr_accessor :use_pasv_ip
  103 
  104     # When +true+, all traffic to and from the server is written
  105     # to +$stdout+.  Default: +false+.
  106     attr_accessor :debug_mode
  107 
  108     # Sets or retrieves the +resume+ status, which decides whether incomplete
  109     # transfers are resumed or restarted.  Default: +false+.
  110     attr_accessor :resume
  111 
  112     # Number of seconds to wait for the connection to open. Any number
  113     # may be used, including Floats for fractional seconds. If the FTP
  114     # object cannot open a connection in this many seconds, it raises a
  115     # Net::OpenTimeout exception. The default value is +nil+.
  116     attr_accessor :open_timeout
  117 
  118     # Number of seconds to wait for the TLS handshake. Any number
  119     # may be used, including Floats for fractional seconds. If the FTP
  120     # object cannot complete the TLS handshake in this many seconds, it
  121     # raises a Net::OpenTimeout exception. The default value is +nil+.
  122     # If +ssl_handshake_timeout+ is +nil+, +open_timeout+ is used instead.
  123     attr_accessor :ssl_handshake_timeout
  124 
  125     # Number of seconds to wait for one block to be read (via one read(2)
  126     # call). Any number may be used, including Floats for fractional
  127     # seconds. If the FTP object cannot read data in this many seconds,
  128     # it raises a Timeout::Error exception. The default value is 60 seconds.
  129     attr_reader :read_timeout
  130 
  131     # Setter for the read_timeout attribute.
  132     def read_timeout=(sec)
  133       @sock.read_timeout = sec
  134       @read_timeout = sec
  135     end
  136 
  137     # The server's welcome message.
  138     attr_reader :welcome
  139 
  140     # The server's last response code.
  141     attr_reader :last_response_code
  142     alias lastresp last_response_code
  143 
  144     # The server's last response.
  145     attr_reader :last_response
  146 
  147     # When +true+, connections are in passive mode per default.
  148     # Default: +true+.
  149     def self.default_passive=(value)
  150       @@default_passive = value
  151     end
  152 
  153     # When +true+, connections are in passive mode per default.
  154     # Default: +true+.
  155     def self.default_passive
  156       @@default_passive
  157     end
  158 
  159     #
  160     # A synonym for <tt>FTP.new</tt>, but with a mandatory host parameter.
  161     #
  162     # If a block is given, it is passed the +FTP+ object, which will be closed
  163     # when the block finishes, or when an exception is raised.
  164     #
  165     def FTP.open(host, *args)
  166       if block_given?
  167         ftp = new(host, *args)
  168         begin
  169           yield ftp
  170         ensure
  171           ftp.close
  172         end
  173       else
  174         new(host, *args)
  175       end
  176     end
  177 
  178     # :call-seq:
  179     #    Net::FTP.new(host = nil, options = {})
  180     #
  181     # Creates and returns a new +FTP+ object. If a +host+ is given, a connection
  182     # is made.
  183     #
  184     # +options+ is an option hash, each key of which is a symbol.
  185     #
  186     # The available options are:
  187     #
  188     # port::      Port number (default value is 21)
  189     # ssl::       If options[:ssl] is true, then an attempt will be made
  190     #             to use SSL (now TLS) to connect to the server.  For this
  191     #             to work OpenSSL [OSSL] and the Ruby OpenSSL [RSSL]
  192     #             extensions need to be installed.  If options[:ssl] is a
  193     #             hash, it's passed to OpenSSL::SSL::SSLContext#set_params
  194     #             as parameters.
  195     # private_data_connection::  If true, TLS is used for data connections.
  196     #                            Default: +true+ when options[:ssl] is true.
  197     # username::  Username for login.  If options[:username] is the string
  198     #             "anonymous" and the options[:password] is +nil+,
  199     #             "anonymous@" is used as a password.
  200     # password::  Password for login.
  201     # account::   Account information for ACCT.
  202     # passive::   When +true+, the connection is in passive mode. Default:
  203     #             +true+.
  204     # open_timeout::  Number of seconds to wait for the connection to open.
  205     #                 See Net::FTP#open_timeout for details.  Default: +nil+.
  206     # read_timeout::  Number of seconds to wait for one block to be read.
  207     #                 See Net::FTP#read_timeout for details.  Default: +60+.
  208     # ssl_handshake_timeout::  Number of seconds to wait for the TLS
  209     #                          handshake.
  210     #                          See Net::FTP#ssl_handshake_timeout for
  211     #                          details.  Default: +nil+.
  212     # use_pasv_ip::  When +true+, use the IP address in PASV responses.
  213     #                Otherwise, it uses the same IP address for the control
  214     #                connection.  Default: +false+.
  215     # debug_mode::  When +true+, all traffic to and from the server is
  216     #               written to +$stdout+.  Default: +false+.
  217     #
  218     def initialize(host = nil, user_or_options = {}, passwd = nil, acct = nil)
  219       super()
  220       begin
  221         options = user_or_options.to_hash
  222       rescue NoMethodError
  223         # for backward compatibility
  224         options = {}
  225         options[:username] = user_or_options
  226         options[:password] = passwd
  227         options[:account] = acct
  228       end
  229       @host = nil
  230       if options[:ssl]
  231         unless defined?(OpenSSL::SSL)
  232           raise "SSL extension not installed"
  233         end
  234         ssl_params = options[:ssl] == true ? {} : options[:ssl]
  235         @ssl_context = SSLContext.new
  236         @ssl_context.set_params(ssl_params)
  237         if defined?(VerifyCallbackProc)
  238           @ssl_context.verify_callback = VerifyCallbackProc
  239         end
  240         @ssl_context.session_cache_mode =
  241           OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT |
  242           OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
  243         @ssl_context.session_new_cb = proc {|sock, sess| @ssl_session = sess }
  244         @ssl_session = nil
  245         if options[:private_data_connection].nil?
  246           @private_data_connection = true
  247         else
  248           @private_data_connection = options[:private_data_connection]
  249         end
  250       else
  251         @ssl_context = nil
  252         if options[:private_data_connection]
  253           raise ArgumentError,
  254             "private_data_connection can be set to true only when ssl is enabled"
  255         end
  256         @private_data_connection = false
  257       end
  258       @binary = true
  259       if options[:passive].nil?
  260         @passive = @@default_passive
  261       else
  262         @passive = options[:passive]
  263       end
  264       if options[:debug_mode].nil?
  265         @debug_mode = false
  266       else
  267         @debug_mode = options[:debug_mode]
  268       end
  269       @resume = false
  270       @bare_sock = @sock = NullSocket.new
  271       @logged_in = false
  272       @open_timeout = options[:open_timeout]
  273       @ssl_handshake_timeout = options[:ssl_handshake_timeout]
  274       @read_timeout = options[:read_timeout] || 60
  275       @use_pasv_ip = options[:use_pasv_ip] || false
  276       if host
  277         connect(host, options[:port] || FTP_PORT)
  278         if options[:username]
  279           login(options[:username], options[:password], options[:account])
  280         end
  281       end
  282     end
  283 
  284     # A setter to toggle transfers in binary mode.
  285     # +newmode+ is either +true+ or +false+
  286     def binary=(newmode)
  287       if newmode != @binary
  288         @binary = newmode
  289         send_type_command if @logged_in
  290       end
  291     end
  292 
  293     # Sends a command to destination host, with the current binary sendmode
  294     # type.
  295     #
  296     # If binary mode is +true+, then "TYPE I" (image) is sent, otherwise "TYPE
  297     # A" (ascii) is sent.
  298     def send_type_command # :nodoc:
  299       if @binary
  300         voidcmd("TYPE I")
  301       else
  302         voidcmd("TYPE A")
  303       end
  304     end
  305     private :send_type_command
  306 
  307     # Toggles transfers in binary mode and yields to a block.
  308     # This preserves your current binary send mode, but allows a temporary
  309     # transaction with binary sendmode of +newmode+.
  310     #
  311     # +newmode+ is either +true+ or +false+
  312     def with_binary(newmode) # :nodoc:
  313       oldmode = binary
  314       self.binary = newmode
  315       begin
  316         yield
  317       ensure
  318         self.binary = oldmode
  319       end
  320     end
  321     private :with_binary
  322 
  323     # Obsolete
  324     def return_code # :nodoc:
  325       warn("Net::FTP#return_code is obsolete and do nothing", uplevel: 1)
  326       return "\n"
  327     end
  328 
  329     # Obsolete
  330     def return_code=(s) # :nodoc:
  331       warn("Net::FTP#return_code= is obsolete and do nothing", uplevel: 1)
  332     end
  333 
  334     # Constructs a socket with +host+ and +port+.
  335     #
  336     # If SOCKSSocket is defined and the environment (ENV) defines
  337     # SOCKS_SERVER, then a SOCKSSocket is returned, else a Socket is
  338     # returned.
  339     def open_socket(host, port) # :nodoc:
  340       return Timeout.timeout(@open_timeout, OpenTimeout) {
  341         if defined? SOCKSSocket and ENV["SOCKS_SERVER"]
  342           @passive = true
  343           SOCKSSocket.open(host, port)
  344         else
  345           Socket.tcp(host, port)
  346         end
  347       }
  348     end
  349     private :open_socket
  350 
  351     def start_tls_session(sock)
  352       ssl_sock = SSLSocket.new(sock, @ssl_context)
  353       ssl_sock.sync_close = true
  354       ssl_sock.hostname = @host if ssl_sock.respond_to? :hostname=
  355       if @ssl_session &&
  356           Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout
  357         # ProFTPD returns 425 for data connections if session is not reused.
  358         ssl_sock.session = @ssl_session
  359       end
  360       ssl_socket_connect(ssl_sock, @ssl_handshake_timeout || @open_timeout)
  361       if @ssl_context.verify_mode != VERIFY_NONE
  362         ssl_sock.post_connection_check(@host)
  363       end
  364       return ssl_sock
  365     end
  366     private :start_tls_session
  367 
  368     #
  369     # Establishes an FTP connection to host, optionally overriding the default
  370     # port. If the environment variable +SOCKS_SERVER+ is set, sets up the
  371     # connection through a SOCKS proxy. Raises an exception (typically
  372     # <tt>Errno::ECONNREFUSED</tt>) if the connection cannot be established.
  373     #
  374     def connect(host, port = FTP_PORT)
  375       if @debug_mode
  376         print "connect: ", host, ", ", port, "\n"
  377       end
  378       synchronize do
  379         @host = host
  380         @bare_sock = open_socket(host, port)
  381         @sock = BufferedSocket.new(@bare_sock, read_timeout: @read_timeout)
  382         voidresp
  383         if @ssl_context
  384           begin
  385             voidcmd("AUTH TLS")
  386             ssl_sock = start_tls_session(@bare_sock)
  387             @sock = BufferedSSLSocket.new(ssl_sock, read_timeout: @read_timeout)
  388             if @private_data_connection
  389               voidcmd("PBSZ 0")
  390               voidcmd("PROT P")
  391             end
  392           rescue OpenSSL::SSL::SSLError, OpenTimeout
  393             @sock.close
  394             raise
  395           end
  396         end
  397       end
  398     end
  399 
  400     #
  401     # Set the socket used to connect to the FTP server.
  402     #
  403     # May raise FTPReplyError if +get_greeting+ is false.
  404     def set_socket(sock, get_greeting = true)
  405       synchronize do
  406         @sock = sock
  407         if get_greeting
  408           voidresp
  409         end
  410       end
  411     end
  412 
  413     # If string +s+ includes the PASS command (password), then the contents of
  414     # the password are cleaned from the string using "*"
  415     def sanitize(s) # :nodoc:
  416       if s =~ /^PASS /i
  417         return s[0, 5] + "*" * (s.length - 5)
  418       else
  419         return s
  420       end
  421     end
  422     private :sanitize
  423 
  424     # Ensures that +line+ has a control return / line feed (CRLF) and writes
  425     # it to the socket.
  426     def putline(line) # :nodoc:
  427       if @debug_mode
  428         print "put: ", sanitize(line), "\n"
  429       end
  430       if /[\r\n]/ =~ line
  431         raise ArgumentError, "A line must not contain CR or LF"
  432       end
  433       line = line + CRLF
  434       @sock.write(line)
  435     end
  436     private :putline
  437 
  438     # Reads a line from the sock.  If EOF, then it will raise EOFError
  439     def getline # :nodoc:
  440       line = @sock.readline # if get EOF, raise EOFError
  441       line.sub!(/(\r\n|\n|\r)\z/n, "")
  442       if @debug_mode
  443         print "get: ", sanitize(line), "\n"
  444       end
  445       return line
  446     end
  447     private :getline
  448 
  449     # Receive a section of lines until the response code's match.
  450     def getmultiline # :nodoc:
  451       lines = []
  452       lines << getline
  453       code = lines.last.slice(/\A([0-9a-zA-Z]{3})-/, 1)
  454       if code
  455         delimiter = code + " "
  456         begin
  457           lines << getline
  458         end until lines.last.start_with?(delimiter)
  459       end
  460       return lines.join("\n") + "\n"
  461     end
  462     private :getmultiline
  463 
  464     # Receives a response from the destination host.
  465     #
  466     # Returns the response code or raises FTPTempError, FTPPermError, or
  467     # FTPProtoError
  468     def getresp # :nodoc:
  469       @last_response = getmultiline
  470       @last_response_code = @last_response[0, 3]
  471       case @last_response_code
  472       when /\A[123]/
  473         return @last_response
  474       when /\A4/
  475         raise FTPTempError, @last_response
  476       when /\A5/
  477         raise FTPPermError, @last_response
  478       else
  479         raise FTPProtoError, @last_response
  480       end
  481     end
  482     private :getresp
  483 
  484     # Receives a response.
  485     #
  486     # Raises FTPReplyError if the first position of the response code is not
  487     # equal 2.
  488     def voidresp # :nodoc:
  489       resp = getresp
  490       if !resp.start_with?("2")
  491         raise FTPReplyError, resp
  492       end
  493     end
  494     private :voidresp
  495 
  496     #
  497     # Sends a command and returns the response.
  498     #
  499     def sendcmd(cmd)
  500       synchronize do
  501         putline(cmd)
  502         return getresp
  503       end
  504     end
  505 
  506     #
  507     # Sends a command and expect a response beginning with '2'.
  508     #
  509     def voidcmd(cmd)
  510       synchronize do
  511         putline(cmd)
  512         voidresp
  513       end
  514     end
  515 
  516     # Constructs and send the appropriate PORT (or EPRT) command
  517     def sendport(host, port) # :nodoc:
  518       remote_address = @bare_sock.remote_address
  519       if remote_address.ipv4?
  520         cmd = "PORT " + (host.split(".") + port.divmod(256)).join(",")
  521       elsif remote_address.ipv6?
  522         cmd = sprintf("EPRT |2|%s|%d|", host, port)
  523       else
  524         raise FTPProtoError, host
  525       end
  526       voidcmd(cmd)
  527     end
  528     private :sendport
  529 
  530     # Constructs a TCPServer socket
  531     def makeport # :nodoc:
  532       Addrinfo.tcp(@bare_sock.local_address.ip_address, 0).listen
  533     end
  534     private :makeport
  535 
  536     # sends the appropriate command to enable a passive connection
  537     def makepasv # :nodoc:
  538       if @bare_sock.remote_address.ipv4?
  539         host, port = parse227(sendcmd("PASV"))
  540       else
  541         host, port = parse229(sendcmd("EPSV"))
  542         #     host, port = parse228(sendcmd("LPSV"))
  543       end
  544       return host, port
  545     end
  546     private :makepasv
  547 
  548     # Constructs a connection for transferring data
  549     def transfercmd(cmd, rest_offset = nil) # :nodoc:
  550       if @passive
  551         host, port = makepasv
  552         conn = open_socket(host, port)
  553         if @resume and rest_offset
  554           resp = sendcmd("REST " + rest_offset.to_s)
  555           if !resp.start_with?("3")
  556             raise FTPReplyError, resp
  557           end
  558         end
  559         resp = sendcmd(cmd)
  560         # skip 2XX for some ftp servers
  561         resp = getresp if resp.start_with?("2")
  562         if !resp.start_with?("1")
  563           raise FTPReplyError, resp
  564         end
  565       else
  566         sock = makeport
  567         begin
  568           addr = sock.local_address
  569           sendport(addr.ip_address, addr.ip_port)
  570           if @resume and rest_offset
  571             resp = sendcmd("REST " + rest_offset.to_s)
  572             if !resp.start_with?("3")
  573               raise FTPReplyError, resp
  574             end
  575           end
  576           resp = sendcmd(cmd)
  577           # skip 2XX for some ftp servers
  578           resp = getresp if resp.start_with?("2")
  579           if !resp.start_with?("1")
  580             raise FTPReplyError, resp
  581           end
  582           conn, = sock.accept
  583           sock.shutdown(Socket::SHUT_WR) rescue nil
  584           sock.read rescue nil
  585         ensure
  586           sock.close
  587         end
  588       end
  589       if @private_data_connection
  590         return BufferedSSLSocket.new(start_tls_session(conn),
  591                                      read_timeout: @read_timeout)
  592       else
  593         return BufferedSocket.new(conn, read_timeout: @read_timeout)
  594       end
  595     end
  596     private :transfercmd
  597 
  598     #
  599     # Logs in to the remote host.  The session must have been
  600     # previously connected.  If +user+ is the string "anonymous" and
  601     # the +password+ is +nil+, "anonymous@" is used as a password.  If
  602     # the +acct+ parameter is not +nil+, an FTP ACCT command is sent
  603     # following the successful login.  Raises an exception on error
  604     # (typically <tt>Net::FTPPermError</tt>).
  605     #
  606     def login(user = "anonymous", passwd = nil, acct = nil)
  607       if user == "anonymous" and passwd == nil
  608         passwd = "anonymous@"
  609       end
  610 
  611       resp = ""
  612       synchronize do
  613         resp = sendcmd('USER ' + user)
  614         if resp.start_with?("3")
  615           raise FTPReplyError, resp if passwd.nil?
  616           resp = sendcmd('PASS ' + passwd)
  617         end
  618         if resp.start_with?("3")
  619           raise FTPReplyError, resp if acct.nil?
  620           resp = sendcmd('ACCT ' + acct)
  621         end
  622       end
  623       if !resp.start_with?("2")
  624         raise FTPReplyError, resp
  625       end
  626       @welcome = resp
  627       send_type_command
  628       @logged_in = true
  629     end
  630 
  631     #
  632     # Puts the connection into binary (image) mode, issues the given command,
  633     # and fetches the data returned, passing it to the associated block in
  634     # chunks of +blocksize+ characters. Note that +cmd+ is a server command
  635     # (such as "RETR myfile").
  636     #
  637     def retrbinary(cmd, blocksize, rest_offset = nil) # :yield: data
  638       synchronize do
  639         with_binary(true) do
  640           begin
  641             conn = transfercmd(cmd, rest_offset)
  642             while data = conn.read(blocksize)
  643               yield(data)
  644             end
  645             conn.shutdown(Socket::SHUT_WR) rescue nil
  646             conn.read_timeout = 1
  647             conn.read rescue nil
  648           ensure
  649             conn.close if conn
  650           end
  651           voidresp
  652         end
  653       end
  654     end
  655 
  656     #
  657     # Puts the connection into ASCII (text) mode, issues the given command, and
  658     # passes the resulting data, one line at a time, to the associated block. If
  659     # no block is given, prints the lines. Note that +cmd+ is a server command
  660     # (such as "RETR myfile").
  661     #
  662     def retrlines(cmd) # :yield: line
  663       synchronize do
  664         with_binary(false) do
  665           begin
  666             conn = transfercmd(cmd)
  667             while line = conn.gets
  668               yield(line.sub(/\r?\n\z/, ""), !line.match(/\n\z/).nil?)
  669             end
  670             conn.shutdown(Socket::SHUT_WR) rescue nil
  671             conn.read_timeout = 1
  672             conn.read rescue nil
  673           ensure
  674             conn.close if conn
  675           end
  676           voidresp
  677         end
  678       end
  679     end
  680 
  681     #
  682     # Puts the connection into binary (image) mode, issues the given server-side
  683     # command (such as "STOR myfile"), and sends the contents of the file named
  684     # +file+ to the server. If the optional block is given, it also passes it
  685     # the data, in chunks of +blocksize+ characters.
  686     #
  687     def storbinary(cmd, file, blocksize, rest_offset = nil) # :yield: data
  688       if rest_offset
  689         file.seek(rest_offset, IO::SEEK_SET)
  690       end
  691       synchronize do
  692         with_binary(true) do
  693           begin
  694             conn = transfercmd(cmd)
  695             while buf = file.read(blocksize)
  696               conn.write(buf)
  697               yield(buf) if block_given?
  698             end
  699             conn.shutdown(Socket::SHUT_WR) rescue nil
  700             conn.read_timeout = 1
  701             conn.read rescue nil
  702           ensure
  703             conn.close if conn
  704           end
  705           voidresp
  706         end
  707       end
  708     rescue Errno::EPIPE
  709       # EPIPE, in this case, means that the data connection was unexpectedly
  710       # terminated.  Rather than just raising EPIPE to the caller, check the
  711       # response on the control connection.  If getresp doesn't raise a more
  712       # appropriate exception, re-raise the original exception.
  713       getresp
  714       raise
  715     end
  716 
  717     #
  718     # Puts the connection into ASCII (text) mode, issues the given server-side
  719     # command (such as "STOR myfile"), and sends the contents of the file
  720     # named +file+ to the server, one line at a time. If the optional block is
  721     # given, it also passes it the lines.
  722     #
  723     def storlines(cmd, file) # :yield: line
  724       synchronize do
  725         with_binary(false) do
  726           begin
  727             conn = transfercmd(cmd)
  728             while buf = file.gets
  729               if buf[-2, 2] != CRLF
  730                 buf = buf.chomp + CRLF
  731               end
  732               conn.write(buf)
  733               yield(buf) if block_given?
  734             end
  735             conn.shutdown(Socket::SHUT_WR) rescue nil
  736             conn.read_timeout = 1
  737             conn.read rescue nil
  738           ensure
  739             conn.close if conn
  740           end
  741           voidresp
  742         end
  743       end
  744     rescue Errno::EPIPE
  745       # EPIPE, in this case, means that the data connection was unexpectedly
  746       # terminated.  Rather than just raising EPIPE to the caller, check the
  747       # response on the control connection.  If getresp doesn't raise a more
  748       # appropriate exception, re-raise the original exception.
  749       getresp
  750       raise
  751     end
  752 
  753     #
  754     # Retrieves +remotefile+ in binary mode, storing the result in +localfile+.
  755     # If +localfile+ is nil, returns retrieved data.
  756     # If a block is supplied, it is passed the retrieved data in +blocksize+
  757     # chunks.
  758     #
  759     def getbinaryfile(remotefile, localfile = File.basename(remotefile),
  760                       blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
  761       f = nil
  762       result = nil
  763       if localfile
  764         if @resume
  765           rest_offset = File.size?(localfile)
  766           f = File.open(localfile, "a")
  767         else
  768           rest_offset = nil
  769           f = File.open(localfile, "w")
  770         end
  771       elsif !block_given?
  772         result = String.new
  773       end
  774       begin
  775         f&.binmode
  776         retrbinary("RETR #{remotefile}", blocksize, rest_offset) do |data|
  777           f&.write(data)
  778           block&.(data)
  779           result&.concat(data)
  780         end
  781         return result
  782       ensure
  783         f&.close
  784       end
  785     end
  786 
  787     #
  788     # Retrieves +remotefile+ in ASCII (text) mode, storing the result in
  789     # +localfile+.
  790     # If +localfile+ is nil, returns retrieved data.
  791     # If a block is supplied, it is passed the retrieved data one
  792     # line at a time.
  793     #
  794     def gettextfile(remotefile, localfile = File.basename(remotefile),
  795                     &block) # :yield: line
  796       f = nil
  797       result = nil
  798       if localfile
  799         f = File.open(localfile, "w")
  800       elsif !block_given?
  801         result = String.new
  802       end
  803       begin
  804         retrlines("RETR #{remotefile}") do |line, newline|
  805           l = newline ? line + "\n" : line
  806           f&.print(l)
  807           block&.(line, newline)
  808           result&.concat(l)
  809         end
  810         return result
  811       ensure
  812         f&.close
  813       end
  814     end
  815 
  816     #
  817     # Retrieves +remotefile+ in whatever mode the session is set (text or
  818     # binary).  See #gettextfile and #getbinaryfile.
  819     #
  820     def get(remotefile, localfile = File.basename(remotefile),
  821             blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
  822       if @binary
  823         getbinaryfile(remotefile, localfile, blocksize, &block)
  824       else
  825         gettextfile(remotefile, localfile, &block)
  826       end
  827     end
  828 
  829     #
  830     # Transfers +localfile+ to the server in binary mode, storing the result in
  831     # +remotefile+. If a block is supplied, calls it, passing in the transmitted
  832     # data in +blocksize+ chunks.
  833     #
  834     def putbinaryfile(localfile, remotefile = File.basename(localfile),
  835                       blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
  836       if @resume
  837         begin
  838           rest_offset = size(remotefile)
  839         rescue Net::FTPPermError
  840           rest_offset = nil
  841         end
  842       else
  843         rest_offset = nil
  844       end
  845       f = File.open(localfile)
  846       begin
  847         f.binmode
  848         if rest_offset
  849           storbinary("APPE #{remotefile}", f, blocksize, rest_offset, &block)
  850         else
  851           storbinary("STOR #{remotefile}", f, blocksize, rest_offset, &block)
  852         end
  853       ensure
  854         f.close
  855       end
  856     end
  857 
  858     #
  859     # Transfers +localfile+ to the server in ASCII (text) mode, storing the result
  860     # in +remotefile+. If callback or an associated block is supplied, calls it,
  861     # passing in the transmitted data one line at a time.
  862     #
  863     def puttextfile(localfile, remotefile = File.basename(localfile), &block) # :yield: line
  864       f = File.open(localfile)
  865       begin
  866         storlines("STOR #{remotefile}", f, &block)
  867       ensure
  868         f.close
  869       end
  870     end
  871 
  872     #
  873     # Transfers +localfile+ to the server in whatever mode the session is set
  874     # (text or binary).  See #puttextfile and #putbinaryfile.
  875     #
  876     def put(localfile, remotefile = File.basename(localfile),
  877             blocksize = DEFAULT_BLOCKSIZE, &block)
  878       if @binary
  879         putbinaryfile(localfile, remotefile, blocksize, &block)
  880       else
  881         puttextfile(localfile, remotefile, &block)
  882       end
  883     end
  884 
  885     #
  886     # Sends the ACCT command.
  887     #
  888     # This is a less common FTP command, to send account
  889     # information if the destination host requires it.
  890     #
  891     def acct(account)
  892       cmd = "ACCT " + account
  893       voidcmd(cmd)
  894     end
  895 
  896     #
  897     # Returns an array of filenames in the remote directory.
  898     #
  899     def nlst(dir = nil)
  900       cmd = "NLST"
  901       if dir
  902         cmd = "#{cmd} #{dir}"
  903       end
  904       files = []
  905       retrlines(cmd) do |line|
  906         files.push(line)
  907       end
  908       return files
  909     end
  910 
  911     #
  912     # Returns an array of file information in the directory (the output is like
  913     # `ls -l`).  If a block is given, it iterates through the listing.
  914     #
  915     def list(*args, &block) # :yield: line
  916       cmd = "LIST"
  917       args.each do |arg|
  918         cmd = "#{cmd} #{arg}"
  919       end
  920       lines = []
  921       retrlines(cmd) do |line|
  922         lines << line
  923       end
  924       if block
  925         lines.each(&block)
  926       end
  927       return lines
  928     end
  929     alias ls list
  930     alias dir list
  931 
  932     #
  933     # MLSxEntry represents an entry in responses of MLST/MLSD.
  934     # Each entry has the facts (e.g., size, last modification time, etc.)
  935     # and the pathname.
  936     #
  937     class MLSxEntry
  938       attr_reader :facts, :pathname
  939 
  940       def initialize(facts, pathname)
  941         @facts = facts
  942         @pathname = pathname
  943       end
  944 
  945       standard_facts = %w(size modify create type unique perm
  946                           lang media-type charset)
  947       standard_facts.each do |factname|
  948         define_method factname.gsub(/-/, "_") do
  949           facts[factname]
  950         end
  951       end
  952 
  953       #
  954       # Returns +true+ if the entry is a file (i.e., the value of the type
  955       # fact is file).
  956       #
  957       def file?
  958         return facts["type"] == "file"
  959       end
  960 
  961       #
  962       # Returns +true+ if the entry is a directory (i.e., the value of the
  963       # type fact is dir, cdir, or pdir).
  964       #
  965       def directory?
  966         if /\A[cp]?dir\z/.match(facts["type"])
  967           return true
  968         else
  969           return false
  970         end
  971       end
  972 
  973       #
  974       # Returns +true+ if the APPE command may be applied to the file.
  975       #
  976       def appendable?
  977         return facts["perm"].include?(?a)
  978       end
  979 
  980       #
  981       # Returns +true+ if files may be created in the directory by STOU,
  982       # STOR, APPE, and RNTO.
  983       #
  984       def creatable?
  985         return facts["perm"].include?(?c)
  986       end
  987 
  988       #
  989       # Returns +true+ if the file or directory may be deleted by DELE/RMD.
  990       #
  991       def deletable?
  992         return facts["perm"].include?(?d)
  993       end
  994 
  995       #
  996       # Returns +true+ if the directory may be entered by CWD/CDUP.
  997       #
  998       def enterable?
  999         return facts["perm"].include?(?e)
 1000       end
 1001 
 1002       #
 1003       # Returns +true+ if the file or directory may be renamed by RNFR.
 1004       #
 1005       def renamable?
 1006         return facts["perm"].include?(?f)
 1007       end
 1008 
 1009       #
 1010       # Returns +true+ if the listing commands, LIST, NLST, and MLSD are
 1011       # applied to the directory.
 1012       #
 1013       def listable?
 1014         return facts["perm"].include?(?l)
 1015       end
 1016 
 1017       #
 1018       # Returns +true+ if the MKD command may be used to create a new
 1019       # directory within the directory.
 1020       #
 1021       def directory_makable?
 1022         return facts["perm"].include?(?m)
 1023       end
 1024 
 1025       #
 1026       # Returns +true+ if the objects in the directory may be deleted, or
 1027       # the directory may be purged.
 1028       #
 1029       def purgeable?
 1030         return facts["perm"].include?(?p)
 1031       end
 1032 
 1033       #
 1034       # Returns +true+ if the RETR command may be applied to the file.
 1035       #
 1036       def readable?
 1037         return facts["perm"].include?(?r)
 1038       end
 1039 
 1040       #
 1041       # Returns +true+ if the STOR command may be applied to the file.
 1042       #
 1043       def writable?
 1044         return facts["perm"].include?(?w)
 1045       end
 1046     end
 1047 
 1048     CASE_DEPENDENT_PARSER = ->(value) { value }
 1049     CASE_INDEPENDENT_PARSER = ->(value) { value.downcase }
 1050     DECIMAL_PARSER = ->(value) { value.to_i }
 1051     OCTAL_PARSER = ->(value) { value.to_i(8) }
 1052     TIME_PARSER = ->(value, local = false) {
 1053       unless /\A(?<year>\d{4})(?<month>\d{2})(?<day>\d{2})
 1054             (?<hour>\d{2})(?<min>\d{2})(?<sec>\d{2})
 1055             (\.(?<fractions>\d+))?/x =~ value
 1056         raise FTPProtoError, "invalid time-val: #{value}"
 1057       end
 1058       usec = fractions.to_i * 10 ** (6 - fractions.to_s.size)
 1059       Time.send(local ? :local : :utc, year, month, day, hour, min, sec, usec)
 1060     }
 1061     FACT_PARSERS = Hash.new(CASE_DEPENDENT_PARSER)
 1062     FACT_PARSERS["size"] = DECIMAL_PARSER
 1063     FACT_PARSERS["modify"] = TIME_PARSER
 1064     FACT_PARSERS["create"] = TIME_PARSER
 1065     FACT_PARSERS["type"] = CASE_INDEPENDENT_PARSER
 1066     FACT_PARSERS["unique"] = CASE_DEPENDENT_PARSER
 1067     FACT_PARSERS["perm"] = CASE_INDEPENDENT_PARSER
 1068     FACT_PARSERS["lang"] = CASE_INDEPENDENT_PARSER
 1069     FACT_PARSERS["media-type"] = CASE_INDEPENDENT_PARSER
 1070     FACT_PARSERS["charset"] = CASE_INDEPENDENT_PARSER
 1071     FACT_PARSERS["unix.mode"] = OCTAL_PARSER
 1072     FACT_PARSERS["unix.owner"] = DECIMAL_PARSER
 1073     FACT_PARSERS["unix.group"] = DECIMAL_PARSER
 1074     FACT_PARSERS["unix.ctime"] = TIME_PARSER
 1075     FACT_PARSERS["unix.atime"] = TIME_PARSER
 1076 
 1077     def parse_mlsx_entry(entry)
 1078       facts, pathname = entry.chomp.split(/ /, 2)
 1079       unless pathname
 1080         raise FTPProtoError, entry
 1081       end
 1082       return MLSxEntry.new(
 1083         facts.scan(/(.*?)=(.*?);/).each_with_object({}) {
 1084           |(factname, value), h|
 1085           name = factname.downcase
 1086           h[name] = FACT_PARSERS[name].(value)
 1087         },
 1088         pathname)
 1089     end
 1090     private :parse_mlsx_entry
 1091 
 1092     #
 1093     # Returns data (e.g., size, last modification time, entry type, etc.)
 1094     # about the file or directory specified by +pathname+.
 1095     # If +pathname+ is omitted, the current directory is assumed.
 1096     #
 1097     def mlst(pathname = nil)
 1098       cmd = pathname ? "MLST #{pathname}" : "MLST"
 1099       resp = sendcmd(cmd)
 1100       if !resp.start_with?("250")
 1101         raise FTPReplyError, resp
 1102       end
 1103       line = resp.lines[1]
 1104       unless line
 1105         raise FTPProtoError, resp
 1106       end
 1107       entry = line.sub(/\A(250-| *)/, "")
 1108       return parse_mlsx_entry(entry)
 1109     end
 1110 
 1111     #
 1112     # Returns an array of the entries of the directory specified by
 1113     # +pathname+.
 1114     # Each entry has the facts (e.g., size, last modification time, etc.)
 1115     # and the pathname.
 1116     # If a block is given, it iterates through the listing.
 1117     # If +pathname+ is omitted, the current directory is assumed.
 1118     #
 1119     def mlsd(pathname = nil, &block) # :yield: entry
 1120       cmd = pathname ? "MLSD #{pathname}" : "MLSD"
 1121       entries = []
 1122       retrlines(cmd) do |line|
 1123         entries << parse_mlsx_entry(line)
 1124       end
 1125       if block
 1126         entries.each(&block)
 1127       end
 1128       return entries
 1129     end
 1130 
 1131     #
 1132     # Renames a file on the server.
 1133     #
 1134     def rename(fromname, toname)
 1135       resp = sendcmd("RNFR #{fromname}")
 1136       if !resp.start_with?("3")
 1137         raise FTPReplyError, resp
 1138       end
 1139       voidcmd("RNTO #{toname}")
 1140     end
 1141 
 1142     #
 1143     # Deletes a file on the server.
 1144     #
 1145     def delete(filename)
 1146       resp = sendcmd("DELE #{filename}")
 1147       if resp.start_with?("250")
 1148         return
 1149       elsif resp.start_with?("5")
 1150         raise FTPPermError, resp
 1151       else
 1152         raise FTPReplyError, resp
 1153       end
 1154     end
 1155 
 1156     #
 1157     # Changes the (remote) directory.
 1158     #
 1159     def chdir(dirname)
 1160       if dirname == ".."
 1161         begin
 1162           voidcmd("CDUP")
 1163           return
 1164         rescue FTPPermError => e
 1165           if e.message[0, 3] != "500"
 1166             raise e
 1167           end
 1168         end
 1169       end
 1170       cmd = "CWD #{dirname}"
 1171       voidcmd(cmd)
 1172     end
 1173 
 1174     def get_body(resp) # :nodoc:
 1175       resp.slice(/\A[0-9a-zA-Z]{3} (.*)$/, 1)
 1176     end
 1177     private :get_body
 1178 
 1179     #
 1180     # Returns the size of the given (remote) filename.
 1181     #
 1182     def size(filename)
 1183       with_binary(true) do
 1184         resp = sendcmd("SIZE #{filename}")
 1185         if !resp.start_with?("213")
 1186           raise FTPReplyError, resp
 1187         end
 1188         return get_body(resp).to_i
 1189       end
 1190     end
 1191 
 1192     #
 1193     # Returns the last modification time of the (remote) file.  If +local+ is
 1194     # +true+, it is returned as a local time, otherwise it's a UTC time.
 1195     #
 1196     def mtime(filename, local = false)
 1197       return TIME_PARSER.(mdtm(filename), local)
 1198     end
 1199 
 1200     #
 1201     # Creates a remote directory.
 1202     #
 1203     def mkdir(dirname)
 1204       resp = sendcmd("MKD #{dirname}")
 1205       return parse257(resp)
 1206     end
 1207 
 1208     #
 1209     # Removes a remote directory.
 1210     #
 1211     def rmdir(dirname)
 1212       voidcmd("RMD #{dirname}")
 1213     end
 1214 
 1215     #
 1216     # Returns the current remote directory.
 1217     #
 1218     def pwd
 1219       resp = sendcmd("PWD")
 1220       return parse257(resp)
 1221     end
 1222     alias getdir pwd
 1223 
 1224     #
 1225     # Returns system information.
 1226     #
 1227     def system
 1228       resp = sendcmd("SYST")
 1229       if !resp.start_with?("215")
 1230         raise FTPReplyError, resp
 1231       end
 1232       return get_body(resp)
 1233     end
 1234 
 1235     #
 1236     # Aborts the previous command (ABOR command).
 1237     #
 1238     def abort
 1239       line = "ABOR" + CRLF
 1240       print "put: ABOR\n" if @debug_mode
 1241       @sock.send(line, Socket::MSG_OOB)
 1242       resp = getmultiline
 1243       unless ["426", "226", "225"].include?(resp[0, 3])
 1244         raise FTPProtoError, resp
 1245       end
 1246       return resp
 1247     end
 1248 
 1249     #
 1250     # Returns the status (STAT command).
 1251     #
 1252     # pathname:: when stat is invoked with pathname as a parameter it acts like
 1253     #            list but a lot faster and over the same tcp session.
 1254     #
 1255     def status(pathname = nil)
 1256       line = pathname ? "STAT #{pathname}" : "STAT"
 1257       if /[\r\n]/ =~ line
 1258         raise ArgumentError, "A line must not contain CR or LF"
 1259       end
 1260       print "put: #{line}\n" if @debug_mode
 1261       @sock.send(line + CRLF, Socket::MSG_OOB)
 1262       return getresp
 1263     end
 1264 
 1265     #
 1266     # Returns the raw last modification time of the (remote) file in the format
 1267     # "YYYYMMDDhhmmss" (MDTM command).
 1268     #
 1269     # Use +mtime+ if you want a parsed Time instance.
 1270     #
 1271     def mdtm(filename)
 1272       resp = sendcmd("MDTM #{filename}")
 1273       if resp.start_with?("213")
 1274         return get_body(resp)
 1275       end
 1276     end
 1277 
 1278     #
 1279     # Issues the HELP command.
 1280     #
 1281     def help(arg = nil)
 1282       cmd = "HELP"
 1283       if arg
 1284         cmd = cmd + " " + arg
 1285       end
 1286       sendcmd(cmd)
 1287     end
 1288 
 1289     #
 1290     # Exits the FTP session.
 1291     #
 1292     def quit
 1293       voidcmd("QUIT")
 1294     end
 1295 
 1296     #
 1297     # Issues a NOOP command.
 1298     #
 1299     # Does nothing except return a response.
 1300     #
 1301     def noop
 1302       voidcmd("NOOP")
 1303     end
 1304 
 1305     #
 1306     # Issues a SITE command.
 1307     #
 1308     def site(arg)
 1309       cmd = "SITE " + arg
 1310       voidcmd(cmd)
 1311     end
 1312 
 1313     #
 1314     # Issues a FEAT command
 1315     #
 1316     # Returns an array of supported optional features
 1317     #
 1318     def features
 1319       resp = sendcmd("FEAT")
 1320       if !resp.start_with?("211")
 1321         raise FTPReplyError, resp
 1322       end
 1323 
 1324       feats = []
 1325       resp.split("\n").each do |line|
 1326         next if !line.start_with?(' ') # skip status lines
 1327 
 1328         feats << line.strip
 1329       end
 1330 
 1331       return feats
 1332     end
 1333 
 1334     #
 1335     # Issues an OPTS command
 1336     # - name Should be the name of the option to set
 1337     # - params is any optional parameters to supply with the option
 1338     #
 1339     # example: option('UTF8', 'ON') => 'OPTS UTF8 ON'
 1340     #
 1341     def option(name, params = nil)
 1342       cmd = "OPTS #{name}"
 1343       cmd += " #{params}" if params
 1344 
 1345       voidcmd(cmd)
 1346     end
 1347 
 1348     #
 1349     # Closes the connection.  Further operations are impossible until you open
 1350     # a new connection with #connect.
 1351     #
 1352     def close
 1353       if @sock and not @sock.closed?
 1354         begin
 1355           @sock.shutdown(Socket::SHUT_WR) rescue nil
 1356           orig, self.read_timeout = self.read_timeout, 3
 1357           @sock.read rescue nil
 1358         ensure
 1359           @sock.close
 1360           self.read_timeout = orig
 1361         end
 1362       end
 1363     end
 1364 
 1365     #
 1366     # Returns +true+ iff the connection is closed.
 1367     #
 1368     def closed?
 1369       @sock == nil or @sock.closed?
 1370     end
 1371 
 1372     # handler for response code 227
 1373     # (Entering Passive Mode (h1,h2,h3,h4,p1,p2))
 1374     #
 1375     # Returns host and port.
 1376     def parse227(resp) # :nodoc:
 1377       if !resp.start_with?("227")
 1378         raise FTPReplyError, resp
 1379       end
 1380       if m = /\((?<host>\d+(,\d+){3}),(?<port>\d+,\d+)\)/.match(resp)
 1381         if @use_pasv_ip
 1382           host = parse_pasv_ipv4_host(m["host"])
 1383         else
 1384           host = @bare_sock.remote_address.ip_address
 1385         end
 1386         return host, parse_pasv_port(m["port"])
 1387       else
 1388         raise FTPProtoError, resp
 1389       end
 1390     end
 1391     private :parse227
 1392 
 1393     # handler for response code 228
 1394     # (Entering Long Passive Mode)
 1395     #
 1396     # Returns host and port.
 1397     def parse228(resp) # :nodoc:
 1398       if !resp.start_with?("228")
 1399         raise FTPReplyError, resp
 1400       end
 1401       if m = /\(4,4,(?<host>\d+(,\d+){3}),2,(?<port>\d+,\d+)\)/.match(resp)
 1402         return parse_pasv_ipv4_host(m["host"]), parse_pasv_port(m["port"])
 1403       elsif m = /\(6,16,(?<host>\d+(,(\d+)){15}),2,(?<port>\d+,\d+)\)/.match(resp)
 1404         return parse_pasv_ipv6_host(m["host"]), parse_pasv_port(m["port"])
 1405       else
 1406         raise FTPProtoError, resp
 1407       end
 1408     end
 1409     private :parse228
 1410 
 1411     def parse_pasv_ipv4_host(s)
 1412       return s.tr(",", ".")
 1413     end
 1414     private :parse_pasv_ipv4_host
 1415 
 1416     def parse_pasv_ipv6_host(s)
 1417       return s.split(/,/).map { |i|
 1418         "%02x" % i.to_i
 1419       }.each_slice(2).map(&:join).join(":")
 1420     end
 1421     private :parse_pasv_ipv6_host
 1422 
 1423     def parse_pasv_port(s)
 1424       return s.split(/,/).map(&:to_i).inject { |x, y|
 1425         (x << 8) + y
 1426       }
 1427     end
 1428     private :parse_pasv_port
 1429 
 1430     # handler for response code 229
 1431     # (Extended Passive Mode Entered)
 1432     #
 1433     # Returns host and port.
 1434     def parse229(resp) # :nodoc:
 1435       if !resp.start_with?("229")
 1436         raise FTPReplyError, resp
 1437       end
 1438       if m = /\((?<d>[!-~])\k<d>\k<d>(?<port>\d+)\k<d>\)/.match(resp)
 1439         return @bare_sock.remote_address.ip_address, m["port"].to_i
 1440       else
 1441         raise FTPProtoError, resp
 1442       end
 1443     end
 1444     private :parse229
 1445 
 1446     # handler for response code 257
 1447     # ("PATHNAME" created)
 1448     #
 1449     # Returns host and port.
 1450     def parse257(resp) # :nodoc:
 1451       if !resp.start_with?("257")
 1452         raise FTPReplyError, resp
 1453       end
 1454       return resp.slice(/"(([^"]|"")*)"/, 1).to_s.gsub(/""/, '"')
 1455     end
 1456     private :parse257
 1457 
 1458     # :stopdoc:
 1459     class NullSocket
 1460       def read_timeout=(sec)
 1461       end
 1462 
 1463       def closed?
 1464         true
 1465       end
 1466 
 1467       def close
 1468       end
 1469 
 1470       def method_missing(mid, *args)
 1471         raise FTPConnectionError, "not connected"
 1472       end
 1473     end
 1474 
 1475     class BufferedSocket < BufferedIO
 1476       [:local_address, :remote_address, :addr, :peeraddr, :send, :shutdown].each do |method|
 1477         define_method(method) { |*args|
 1478           @io.__send__(method, *args)
 1479         }
 1480       end
 1481 
 1482       def read(len = nil)
 1483         if len
 1484           s = super(len, String.new, true)
 1485           return s.empty? ? nil : s
 1486         else
 1487           result = String.new
 1488           while s = super(DEFAULT_BLOCKSIZE, String.new, true)
 1489             break if s.empty?
 1490             result << s
 1491           end
 1492           return result
 1493         end
 1494       end
 1495 
 1496       def gets
 1497         line = readuntil("\n", true)
 1498         return line.empty? ? nil : line
 1499       end
 1500 
 1501       def readline
 1502         line = gets
 1503         if line.nil?
 1504           raise EOFError, "end of file reached"
 1505         end
 1506         return line
 1507       end
 1508     end
 1509 
 1510     if defined?(OpenSSL::SSL::SSLSocket)
 1511       class BufferedSSLSocket <  BufferedSocket
 1512         def initialize(*args, **options)
 1513           super
 1514           @is_shutdown = false
 1515         end
 1516 
 1517         def shutdown(*args)
 1518           # SSL_shutdown() will be called from SSLSocket#close, and
 1519           # SSL_shutdown() will send the "close notify" alert to the peer,
 1520           # so shutdown(2) should not be called.
 1521           @is_shutdown = true
 1522         end
 1523 
 1524         def send(mesg, flags, dest = nil)
 1525           # Ignore flags and dest.
 1526           @io.write(mesg)
 1527         end
 1528 
 1529         private
 1530 
 1531         def rbuf_fill
 1532           if @is_shutdown
 1533             raise EOFError, "shutdown has been called"
 1534           else
 1535             super
 1536           end
 1537         end
 1538       end
 1539     end
 1540     # :startdoc:
 1541   end
 1542 end
 1543 
 1544 
 1545 # Documentation comments:
 1546 #  - sourced from pickaxe and nutshell, with improvements (hopefully)