1#
2# = net/ftp.rb - FTP Client Library
3#
4# Written by Shugo Maeda <shugo@ruby-lang.org>.
5#
6# Documentation by Gavin Sinclair, sourced from "Programming Ruby" (Hunt/Thomas)
7# and "Ruby In a Nutshell" (Matsumoto), used with permission.
8#
9# This library is distributed under the terms of the Ruby license.
10# You can freely distribute/modify this library.
11#
12# It is included in the Ruby standard library.
13#
14# See the Net::FTP class for an overview.
15#
16
17require "socket"
18require "monitor"
19require "net/protocol"
20
21module Net
22
23  # :stopdoc:
24  class FTPError < StandardError; end
25  class FTPReplyError < FTPError; end
26  class FTPTempError < FTPError; end
27  class FTPPermError < FTPError; end
28  class FTPProtoError < FTPError; end
29  class FTPConnectionError < FTPError; end
30  # :startdoc:
31
32  #
33  # This class implements the File Transfer Protocol.  If you have used a
34  # command-line FTP program, and are familiar with the commands, you will be
35  # able to use this class easily.  Some extra features are included to take
36  # advantage of Ruby's style and strengths.
37  #
38  # == Example
39  #
40  #   require 'net/ftp'
41  #
42  # === Example 1
43  #
44  #   ftp = Net::FTP.new('example.com')
45  #   ftp.login
46  #   files = ftp.chdir('pub/lang/ruby/contrib')
47  #   files = ftp.list('n*')
48  #   ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024)
49  #   ftp.close
50  #
51  # === Example 2
52  #
53  #   Net::FTP.open('example.com') do |ftp|
54  #     ftp.login
55  #     files = ftp.chdir('pub/lang/ruby/contrib')
56  #     files = ftp.list('n*')
57  #     ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024)
58  #   end
59  #
60  # == Major Methods
61  #
62  # The following are the methods most likely to be useful to users:
63  # - FTP.open
64  # - #getbinaryfile
65  # - #gettextfile
66  # - #putbinaryfile
67  # - #puttextfile
68  # - #chdir
69  # - #nlst
70  # - #size
71  # - #rename
72  # - #delete
73  #
74  class FTP
75    include MonitorMixin
76
77    # :stopdoc:
78    FTP_PORT = 21
79    CRLF = "\r\n"
80    DEFAULT_BLOCKSIZE = BufferedIO::BUFSIZE
81    # :startdoc:
82
83    # When +true+, transfers are performed in binary mode.  Default: +true+.
84    attr_reader :binary
85
86    # When +true+, the connection is in passive mode.  Default: +false+.
87    attr_accessor :passive
88
89    # When +true+, all traffic to and from the server is written
90    # to +$stdout+.  Default: +false+.
91    attr_accessor :debug_mode
92
93    # Sets or retrieves the +resume+ status, which decides whether incomplete
94    # transfers are resumed or restarted.  Default: +false+.
95    attr_accessor :resume
96
97    # Number of seconds to wait for the connection to open. Any number
98    # may be used, including Floats for fractional seconds. If the FTP
99    # object cannot open a connection in this many seconds, it raises a
100    # Net::OpenTimeout exception. The default value is +nil+.
101    attr_accessor :open_timeout
102
103    # Number of seconds to wait for one block to be read (via one read(2)
104    # call). Any number may be used, including Floats for fractional
105    # seconds. If the FTP object cannot read data in this many seconds,
106    # it raises a TimeoutError exception. The default value is 60 seconds.
107    attr_reader :read_timeout
108
109    # Setter for the read_timeout attribute.
110    def read_timeout=(sec)
111      @sock.read_timeout = sec
112      @read_timeout = sec
113    end
114
115    # The server's welcome message.
116    attr_reader :welcome
117
118    # The server's last response code.
119    attr_reader :last_response_code
120    alias lastresp last_response_code
121
122    # The server's last response.
123    attr_reader :last_response
124
125    #
126    # A synonym for <tt>FTP.new</tt>, but with a mandatory host parameter.
127    #
128    # If a block is given, it is passed the +FTP+ object, which will be closed
129    # when the block finishes, or when an exception is raised.
130    #
131    def FTP.open(host, user = nil, passwd = nil, acct = nil)
132      if block_given?
133        ftp = new(host, user, passwd, acct)
134        begin
135          yield ftp
136        ensure
137          ftp.close
138        end
139      else
140        new(host, user, passwd, acct)
141      end
142    end
143
144    #
145    # Creates and returns a new +FTP+ object. If a +host+ is given, a connection
146    # is made. Additionally, if the +user+ is given, the given user name,
147    # password, and (optionally) account are used to log in.  See #login.
148    #
149    def initialize(host = nil, user = nil, passwd = nil, acct = nil)
150      super()
151      @binary = true
152      @passive = false
153      @debug_mode = false
154      @resume = false
155      @sock = NullSocket.new
156      @logged_in = false
157      @open_timeout = nil
158      @read_timeout = 60
159      if host
160        connect(host)
161        if user
162          login(user, passwd, acct)
163        end
164      end
165    end
166
167    # A setter to toggle transfers in binary mode.
168    # +newmode+ is either +true+ or +false+
169    def binary=(newmode)
170      if newmode != @binary
171        @binary = newmode
172        send_type_command if @logged_in
173      end
174    end
175
176    # Sends a command to destination host, with the current binary sendmode
177    # type.
178    #
179    # If binary mode is +true+, then "TYPE I" (image) is sent, otherwise "TYPE
180    # A" (ascii) is sent.
181    def send_type_command # :nodoc:
182      if @binary
183        voidcmd("TYPE I")
184      else
185        voidcmd("TYPE A")
186      end
187    end
188    private :send_type_command
189
190    # Toggles transfers in binary mode and yields to a block.
191    # This preserves your current binary send mode, but allows a temporary
192    # transaction with binary sendmode of +newmode+.
193    #
194    # +newmode+ is either +true+ or +false+
195    def with_binary(newmode) # :nodoc:
196      oldmode = binary
197      self.binary = newmode
198      begin
199        yield
200      ensure
201        self.binary = oldmode
202      end
203    end
204    private :with_binary
205
206    # Obsolete
207    def return_code # :nodoc:
208      $stderr.puts("warning: Net::FTP#return_code is obsolete and do nothing")
209      return "\n"
210    end
211
212    # Obsolete
213    def return_code=(s) # :nodoc:
214      $stderr.puts("warning: Net::FTP#return_code= is obsolete and do nothing")
215    end
216
217    # Contructs a socket with +host+ and +port+.
218    #
219    # If SOCKSSocket is defined and the environment (ENV) defines
220    # SOCKS_SERVER, then a SOCKSSocket is returned, else a TCPSocket is
221    # returned.
222    def open_socket(host, port) # :nodoc:
223      return Timeout.timeout(@open_timeout, Net::OpenTimeout) {
224        if defined? SOCKSSocket and ENV["SOCKS_SERVER"]
225          @passive = true
226          sock = SOCKSSocket.open(host, port)
227        else
228          sock = TCPSocket.open(host, port)
229        end
230        io = BufferedSocket.new(sock)
231        io.read_timeout = @read_timeout
232        io
233      }
234    end
235    private :open_socket
236
237    #
238    # Establishes an FTP connection to host, optionally overriding the default
239    # port. If the environment variable +SOCKS_SERVER+ is set, sets up the
240    # connection through a SOCKS proxy. Raises an exception (typically
241    # <tt>Errno::ECONNREFUSED</tt>) if the connection cannot be established.
242    #
243    def connect(host, port = FTP_PORT)
244      if @debug_mode
245        print "connect: ", host, ", ", port, "\n"
246      end
247      synchronize do
248        @sock = open_socket(host, port)
249        voidresp
250      end
251    end
252
253    #
254    # WRITEME or make private
255    #
256    def set_socket(sock, get_greeting = true)
257      synchronize do
258        @sock = sock
259        if get_greeting
260          voidresp
261        end
262      end
263    end
264
265    # If string +s+ includes the PASS command (password), then the contents of
266    # the password are cleaned from the string using "*"
267    def sanitize(s) # :nodoc:
268      if s =~ /^PASS /i
269        return s[0, 5] + "*" * (s.length - 5)
270      else
271        return s
272      end
273    end
274    private :sanitize
275
276    # Ensures that +line+ has a control return / line feed (CRLF) and writes
277    # it to the socket.
278    def putline(line) # :nodoc:
279      if @debug_mode
280        print "put: ", sanitize(line), "\n"
281      end
282      line = line + CRLF
283      @sock.write(line)
284    end
285    private :putline
286
287    # Reads a line from the sock.  If EOF, then it will raise EOFError
288    def getline # :nodoc:
289      line = @sock.readline # if get EOF, raise EOFError
290      line.sub!(/(\r\n|\n|\r)\z/n, "")
291      if @debug_mode
292        print "get: ", sanitize(line), "\n"
293      end
294      return line
295    end
296    private :getline
297
298    # Receive a section of lines until the response code's match.
299    def getmultiline # :nodoc:
300      line = getline
301      buff = line
302      if line[3] == ?-
303          code = line[0, 3]
304        begin
305          line = getline
306          buff << "\n" << line
307        end until line[0, 3] == code and line[3] != ?-
308      end
309      return buff << "\n"
310    end
311    private :getmultiline
312
313    # Recieves a response from the destination host.
314    #
315    # Returns the response code or raises FTPTempError, FTPPermError, or
316    # FTPProtoError
317    def getresp # :nodoc:
318      @last_response = getmultiline
319      @last_response_code = @last_response[0, 3]
320      case @last_response_code
321      when /\A[123]/
322        return @last_response
323      when /\A4/
324        raise FTPTempError, @last_response
325      when /\A5/
326        raise FTPPermError, @last_response
327      else
328        raise FTPProtoError, @last_response
329      end
330    end
331    private :getresp
332
333    # Recieves a response.
334    #
335    # Raises FTPReplyError if the first position of the response code is not
336    # equal 2.
337    def voidresp # :nodoc:
338      resp = getresp
339      if resp[0] != ?2
340        raise FTPReplyError, resp
341      end
342    end
343    private :voidresp
344
345    #
346    # Sends a command and returns the response.
347    #
348    def sendcmd(cmd)
349      synchronize do
350        putline(cmd)
351        return getresp
352      end
353    end
354
355    #
356    # Sends a command and expect a response beginning with '2'.
357    #
358    def voidcmd(cmd)
359      synchronize do
360        putline(cmd)
361        voidresp
362      end
363    end
364
365    # Constructs and send the appropriate PORT (or EPRT) command
366    def sendport(host, port) # :nodoc:
367      af = (@sock.peeraddr)[0]
368      if af == "AF_INET"
369        cmd = "PORT " + (host.split(".") + port.divmod(256)).join(",")
370      elsif af == "AF_INET6"
371        cmd = sprintf("EPRT |2|%s|%d|", host, port)
372      else
373        raise FTPProtoError, host
374      end
375      voidcmd(cmd)
376    end
377    private :sendport
378
379    # Constructs a TCPServer socket, and sends it the PORT command
380    #
381    # Returns the constructed TCPServer socket
382    def makeport # :nodoc:
383      sock = TCPServer.open(@sock.addr[3], 0)
384      port = sock.addr[1]
385      host = sock.addr[3]
386      sendport(host, port)
387      return sock
388    end
389    private :makeport
390
391    # sends the appropriate command to enable a passive connection
392    def makepasv # :nodoc:
393      if @sock.peeraddr[0] == "AF_INET"
394        host, port = parse227(sendcmd("PASV"))
395      else
396        host, port = parse229(sendcmd("EPSV"))
397        #     host, port = parse228(sendcmd("LPSV"))
398      end
399      return host, port
400    end
401    private :makepasv
402
403    # Constructs a connection for transferring data
404    def transfercmd(cmd, rest_offset = nil) # :nodoc:
405      if @passive
406        host, port = makepasv
407        conn = open_socket(host, port)
408        if @resume and rest_offset
409          resp = sendcmd("REST " + rest_offset.to_s)
410          if resp[0] != ?3
411            raise FTPReplyError, resp
412          end
413        end
414        resp = sendcmd(cmd)
415        # skip 2XX for some ftp servers
416        resp = getresp if resp[0] == ?2
417        if resp[0] != ?1
418          raise FTPReplyError, resp
419        end
420      else
421        sock = makeport
422        if @resume and rest_offset
423          resp = sendcmd("REST " + rest_offset.to_s)
424          if resp[0] != ?3
425            raise FTPReplyError, resp
426          end
427        end
428        resp = sendcmd(cmd)
429        # skip 2XX for some ftp servers
430        resp = getresp if resp[0] == ?2
431        if resp[0] != ?1
432          raise FTPReplyError, resp
433        end
434        conn = BufferedSocket.new(sock.accept)
435        conn.read_timeout = @read_timeout
436        sock.shutdown(Socket::SHUT_WR) rescue nil
437        sock.read rescue nil
438        sock.close
439      end
440      return conn
441    end
442    private :transfercmd
443
444    #
445    # Logs in to the remote host. The session must have been previously
446    # connected.  If +user+ is the string "anonymous" and the +password+ is
447    # +nil+, a password of <tt>user@host</tt> is synthesized. If the +acct+
448    # parameter is not +nil+, an FTP ACCT command is sent following the
449    # successful login.  Raises an exception on error (typically
450    # <tt>Net::FTPPermError</tt>).
451    #
452    def login(user = "anonymous", passwd = nil, acct = nil)
453      if user == "anonymous" and passwd == nil
454        passwd = "anonymous@"
455      end
456
457      resp = ""
458      synchronize do
459        resp = sendcmd('USER ' + user)
460        if resp[0] == ?3
461          raise FTPReplyError, resp if passwd.nil?
462          resp = sendcmd('PASS ' + passwd)
463        end
464        if resp[0] == ?3
465          raise FTPReplyError, resp if acct.nil?
466          resp = sendcmd('ACCT ' + acct)
467        end
468      end
469      if resp[0] != ?2
470        raise FTPReplyError, resp
471      end
472      @welcome = resp
473      send_type_command
474      @logged_in = true
475    end
476
477    #
478    # Puts the connection into binary (image) mode, issues the given command,
479    # and fetches the data returned, passing it to the associated block in
480    # chunks of +blocksize+ characters. Note that +cmd+ is a server command
481    # (such as "RETR myfile").
482    #
483    def retrbinary(cmd, blocksize, rest_offset = nil) # :yield: data
484      synchronize do
485        with_binary(true) do
486          begin
487            conn = transfercmd(cmd, rest_offset)
488            loop do
489              data = conn.read(blocksize)
490              break if data == nil
491              yield(data)
492            end
493            conn.shutdown(Socket::SHUT_WR)
494            conn.read_timeout = 1
495            conn.read
496          ensure
497            conn.close if conn
498          end
499          voidresp
500        end
501      end
502    end
503
504    #
505    # Puts the connection into ASCII (text) mode, issues the given command, and
506    # passes the resulting data, one line at a time, to the associated block. If
507    # no block is given, prints the lines. Note that +cmd+ is a server command
508    # (such as "RETR myfile").
509    #
510    def retrlines(cmd) # :yield: line
511      synchronize do
512        with_binary(false) do
513          begin
514            conn = transfercmd(cmd)
515            loop do
516              line = conn.gets
517              break if line == nil
518              yield(line.sub(/\r?\n\z/, ""), !line.match(/\n\z/).nil?)
519            end
520            conn.shutdown(Socket::SHUT_WR)
521            conn.read_timeout = 1
522            conn.read
523          ensure
524            conn.close if conn
525          end
526          voidresp
527        end
528      end
529    end
530
531    #
532    # Puts the connection into binary (image) mode, issues the given server-side
533    # command (such as "STOR myfile"), and sends the contents of the file named
534    # +file+ to the server. If the optional block is given, it also passes it
535    # the data, in chunks of +blocksize+ characters.
536    #
537    def storbinary(cmd, file, blocksize, rest_offset = nil) # :yield: data
538      if rest_offset
539        file.seek(rest_offset, IO::SEEK_SET)
540      end
541      synchronize do
542        with_binary(true) do
543          conn = transfercmd(cmd)
544          loop do
545            buf = file.read(blocksize)
546            break if buf == nil
547            conn.write(buf)
548            yield(buf) if block_given?
549          end
550          conn.close
551          voidresp
552        end
553      end
554    rescue Errno::EPIPE
555      # EPIPE, in this case, means that the data connection was unexpectedly
556      # terminated.  Rather than just raising EPIPE to the caller, check the
557      # response on the control connection.  If getresp doesn't raise a more
558      # appropriate exception, re-raise the original exception.
559      getresp
560      raise
561    end
562
563    #
564    # Puts the connection into ASCII (text) mode, issues the given server-side
565    # command (such as "STOR myfile"), and sends the contents of the file
566    # named +file+ to the server, one line at a time. If the optional block is
567    # given, it also passes it the lines.
568    #
569    def storlines(cmd, file) # :yield: line
570      synchronize do
571        with_binary(false) do
572          conn = transfercmd(cmd)
573          loop do
574            buf = file.gets
575            break if buf == nil
576            if buf[-2, 2] != CRLF
577              buf = buf.chomp + CRLF
578            end
579            conn.write(buf)
580            yield(buf) if block_given?
581          end
582          conn.close
583          voidresp
584        end
585      end
586    rescue Errno::EPIPE
587      # EPIPE, in this case, means that the data connection was unexpectedly
588      # terminated.  Rather than just raising EPIPE to the caller, check the
589      # response on the control connection.  If getresp doesn't raise a more
590      # appropriate exception, re-raise the original exception.
591      getresp
592      raise
593    end
594
595    #
596    # Retrieves +remotefile+ in binary mode, storing the result in +localfile+.
597    # If +localfile+ is nil, returns retrieved data.
598    # If a block is supplied, it is passed the retrieved data in +blocksize+
599    # chunks.
600    #
601    def getbinaryfile(remotefile, localfile = File.basename(remotefile),
602                      blocksize = DEFAULT_BLOCKSIZE) # :yield: data
603      result = nil
604      if localfile
605        if @resume
606          rest_offset = File.size?(localfile)
607          f = open(localfile, "a")
608        else
609          rest_offset = nil
610          f = open(localfile, "w")
611        end
612      elsif !block_given?
613        result = ""
614      end
615      begin
616        f.binmode if localfile
617        retrbinary("RETR " + remotefile.to_s, blocksize, rest_offset) do |data|
618          f.write(data) if localfile
619          yield(data) if block_given?
620          result.concat(data) if result
621        end
622        return result
623      ensure
624        f.close if localfile
625      end
626    end
627
628    #
629    # Retrieves +remotefile+ in ASCII (text) mode, storing the result in
630    # +localfile+.
631    # If +localfile+ is nil, returns retrieved data.
632    # If a block is supplied, it is passed the retrieved data one
633    # line at a time.
634    #
635    def gettextfile(remotefile, localfile = File.basename(remotefile)) # :yield: line
636      result = nil
637      if localfile
638        f = open(localfile, "w")
639      elsif !block_given?
640        result = ""
641      end
642      begin
643        retrlines("RETR " + remotefile) do |line, newline|
644          l = newline ? line + "\n" : line
645          f.print(l) if localfile
646          yield(line, newline) if block_given?
647          result.concat(l) if result
648        end
649        return result
650      ensure
651        f.close if localfile
652      end
653    end
654
655    #
656    # Retrieves +remotefile+ in whatever mode the session is set (text or
657    # binary).  See #gettextfile and #getbinaryfile.
658    #
659    def get(remotefile, localfile = File.basename(remotefile),
660            blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
661      if @binary
662        getbinaryfile(remotefile, localfile, blocksize, &block)
663      else
664        gettextfile(remotefile, localfile, &block)
665      end
666    end
667
668    #
669    # Transfers +localfile+ to the server in binary mode, storing the result in
670    # +remotefile+. If a block is supplied, calls it, passing in the transmitted
671    # data in +blocksize+ chunks.
672    #
673    def putbinaryfile(localfile, remotefile = File.basename(localfile),
674                      blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
675      if @resume
676        begin
677          rest_offset = size(remotefile)
678        rescue Net::FTPPermError
679          rest_offset = nil
680        end
681      else
682        rest_offset = nil
683      end
684      f = open(localfile)
685      begin
686        f.binmode
687        if rest_offset
688          storbinary("APPE " + remotefile, f, blocksize, rest_offset, &block)
689        else
690          storbinary("STOR " + remotefile, f, blocksize, rest_offset, &block)
691        end
692      ensure
693        f.close
694      end
695    end
696
697    #
698    # Transfers +localfile+ to the server in ASCII (text) mode, storing the result
699    # in +remotefile+. If callback or an associated block is supplied, calls it,
700    # passing in the transmitted data one line at a time.
701    #
702    def puttextfile(localfile, remotefile = File.basename(localfile), &block) # :yield: line
703      f = open(localfile)
704      begin
705        storlines("STOR " + remotefile, f, &block)
706      ensure
707        f.close
708      end
709    end
710
711    #
712    # Transfers +localfile+ to the server in whatever mode the session is set
713    # (text or binary).  See #puttextfile and #putbinaryfile.
714    #
715    def put(localfile, remotefile = File.basename(localfile),
716            blocksize = DEFAULT_BLOCKSIZE, &block)
717      if @binary
718        putbinaryfile(localfile, remotefile, blocksize, &block)
719      else
720        puttextfile(localfile, remotefile, &block)
721      end
722    end
723
724    #
725    # Sends the ACCT command.
726    #
727    # This is a less common FTP command, to send account
728    # information if the destination host requires it.
729    #
730    def acct(account)
731      cmd = "ACCT " + account
732      voidcmd(cmd)
733    end
734
735    #
736    # Returns an array of filenames in the remote directory.
737    #
738    def nlst(dir = nil)
739      cmd = "NLST"
740      if dir
741        cmd = cmd + " " + dir
742      end
743      files = []
744      retrlines(cmd) do |line|
745        files.push(line)
746      end
747      return files
748    end
749
750    #
751    # Returns an array of file information in the directory (the output is like
752    # `ls -l`).  If a block is given, it iterates through the listing.
753    #
754    def list(*args, &block) # :yield: line
755      cmd = "LIST"
756      args.each do |arg|
757        cmd = cmd + " " + arg.to_s
758      end
759      if block
760        retrlines(cmd, &block)
761      else
762        lines = []
763        retrlines(cmd) do |line|
764          lines << line
765        end
766        return lines
767      end
768    end
769    alias ls list
770    alias dir list
771
772    #
773    # Renames a file on the server.
774    #
775    def rename(fromname, toname)
776      resp = sendcmd("RNFR " + fromname)
777      if resp[0] != ?3
778        raise FTPReplyError, resp
779      end
780      voidcmd("RNTO " + toname)
781    end
782
783    #
784    # Deletes a file on the server.
785    #
786    def delete(filename)
787      resp = sendcmd("DELE " + filename)
788      if resp[0, 3] == "250"
789        return
790      elsif resp[0] == ?5
791        raise FTPPermError, resp
792      else
793        raise FTPReplyError, resp
794      end
795    end
796
797    #
798    # Changes the (remote) directory.
799    #
800    def chdir(dirname)
801      if dirname == ".."
802        begin
803          voidcmd("CDUP")
804          return
805        rescue FTPPermError => e
806          if e.message[0, 3] != "500"
807            raise e
808          end
809        end
810      end
811      cmd = "CWD " + dirname
812      voidcmd(cmd)
813    end
814
815    #
816    # Returns the size of the given (remote) filename.
817    #
818    def size(filename)
819      with_binary(true) do
820        resp = sendcmd("SIZE " + filename)
821        if resp[0, 3] != "213"
822          raise FTPReplyError, resp
823        end
824        return resp[3..-1].strip.to_i
825      end
826    end
827
828    MDTM_REGEXP = /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/  # :nodoc:
829
830    #
831    # Returns the last modification time of the (remote) file.  If +local+ is
832    # +true+, it is returned as a local time, otherwise it's a UTC time.
833    #
834    def mtime(filename, local = false)
835      str = mdtm(filename)
836      ary = str.scan(MDTM_REGEXP)[0].collect {|i| i.to_i}
837      return local ? Time.local(*ary) : Time.gm(*ary)
838    end
839
840    #
841    # Creates a remote directory.
842    #
843    def mkdir(dirname)
844      resp = sendcmd("MKD " + dirname)
845      return parse257(resp)
846    end
847
848    #
849    # Removes a remote directory.
850    #
851    def rmdir(dirname)
852      voidcmd("RMD " + dirname)
853    end
854
855    #
856    # Returns the current remote directory.
857    #
858    def pwd
859      resp = sendcmd("PWD")
860      return parse257(resp)
861    end
862    alias getdir pwd
863
864    #
865    # Returns system information.
866    #
867    def system
868      resp = sendcmd("SYST")
869      if resp[0, 3] != "215"
870        raise FTPReplyError, resp
871      end
872      return resp[4 .. -1]
873    end
874
875    #
876    # Aborts the previous command (ABOR command).
877    #
878    def abort
879      line = "ABOR" + CRLF
880      print "put: ABOR\n" if @debug_mode
881      @sock.send(line, Socket::MSG_OOB)
882      resp = getmultiline
883      unless ["426", "226", "225"].include?(resp[0, 3])
884        raise FTPProtoError, resp
885      end
886      return resp
887    end
888
889    #
890    # Returns the status (STAT command).
891    #
892    def status
893      line = "STAT" + CRLF
894      print "put: STAT\n" if @debug_mode
895      @sock.send(line, Socket::MSG_OOB)
896      return getresp
897    end
898
899    #
900    # Issues the MDTM command.  TODO: more info.
901    #
902    def mdtm(filename)
903      resp = sendcmd("MDTM " + filename)
904      if resp[0, 3] == "213"
905        return resp[3 .. -1].strip
906      end
907    end
908
909    #
910    # Issues the HELP command.
911    #
912    def help(arg = nil)
913      cmd = "HELP"
914      if arg
915        cmd = cmd + " " + arg
916      end
917      sendcmd(cmd)
918    end
919
920    #
921    # Exits the FTP session.
922    #
923    def quit
924      voidcmd("QUIT")
925    end
926
927    #
928    # Issues a NOOP command.
929    #
930    # Does nothing except return a response.
931    #
932    def noop
933      voidcmd("NOOP")
934    end
935
936    #
937    # Issues a SITE command.
938    #
939    def site(arg)
940      cmd = "SITE " + arg
941      voidcmd(cmd)
942    end
943
944    #
945    # Closes the connection.  Further operations are impossible until you open
946    # a new connection with #connect.
947    #
948    def close
949      if @sock and not @sock.closed?
950        begin
951          @sock.shutdown(Socket::SHUT_WR) rescue nil
952          orig, self.read_timeout = self.read_timeout, 3
953          @sock.read rescue nil
954        ensure
955          @sock.close
956          self.read_timeout = orig
957        end
958      end
959    end
960
961    #
962    # Returns +true+ iff the connection is closed.
963    #
964    def closed?
965      @sock == nil or @sock.closed?
966    end
967
968    # handler for response code 227
969    # (Entering Passive Mode (h1,h2,h3,h4,p1,p2))
970    #
971    # Returns host and port.
972    def parse227(resp) # :nodoc:
973      if resp[0, 3] != "227"
974        raise FTPReplyError, resp
975      end
976      if m = /\((?<host>\d+(,\d+){3}),(?<port>\d+,\d+)\)/.match(resp)
977        return parse_pasv_ipv4_host(m["host"]), parse_pasv_port(m["port"])
978      else
979        raise FTPProtoError, resp
980      end
981    end
982    private :parse227
983
984    # handler for response code 228
985    # (Entering Long Passive Mode)
986    #
987    # Returns host and port.
988    def parse228(resp) # :nodoc:
989      if resp[0, 3] != "228"
990        raise FTPReplyError, resp
991      end
992      if m = /\(4,4,(?<host>\d+(,\d+){3}),2,(?<port>\d+,\d+)\)/.match(resp)
993        return parse_pasv_ipv4_host(m["host"]), parse_pasv_port(m["port"])
994      elsif m = /\(6,16,(?<host>\d+(,(\d+)){15}),2,(?<port>\d+,\d+)\)/.match(resp)
995        return parse_pasv_ipv6_host(m["host"]), parse_pasv_port(m["port"])
996      else
997        raise FTPProtoError, resp
998      end
999      return host, port
1000    end
1001    private :parse228
1002
1003    def parse_pasv_ipv4_host(s)
1004      return s.tr(",", ".")
1005    end
1006    private :parse_pasv_ipv4_host
1007
1008    def parse_pasv_ipv6_host(s)
1009      return s.split(/,/).map { |i|
1010        "%02x" % i.to_i
1011      }.each_slice(2).map(&:join).join(":")
1012    end
1013    private :parse_pasv_ipv6_host
1014
1015    def parse_pasv_port(s)
1016      return s.split(/,/).map(&:to_i).inject { |x, y|
1017        (x << 8) + y
1018      }
1019    end
1020    private :parse_pasv_port
1021
1022    # handler for response code 229
1023    # (Extended Passive Mode Entered)
1024    #
1025    # Returns host and port.
1026    def parse229(resp) # :nodoc:
1027      if resp[0, 3] != "229"
1028        raise FTPReplyError, resp
1029      end
1030      if m = /\((?<d>[!-~])\k<d>\k<d>(?<port>\d+)\k<d>\)/.match(resp)
1031        return @sock.peeraddr[3], m["port"].to_i
1032      else
1033        raise FTPProtoError, resp
1034      end
1035    end
1036    private :parse229
1037
1038    # handler for response code 257
1039    # ("PATHNAME" created)
1040    #
1041    # Returns host and port.
1042    def parse257(resp) # :nodoc:
1043      if resp[0, 3] != "257"
1044        raise FTPReplyError, resp
1045      end
1046      if resp[3, 2] != ' "'
1047        return ""
1048      end
1049      dirname = ""
1050      i = 5
1051      n = resp.length
1052      while i < n
1053        c = resp[i, 1]
1054        i = i + 1
1055        if c == '"'
1056          if i > n or resp[i, 1] != '"'
1057            break
1058          end
1059          i = i + 1
1060        end
1061        dirname = dirname + c
1062      end
1063      return dirname
1064    end
1065    private :parse257
1066
1067    # :stopdoc:
1068    class NullSocket
1069      def read_timeout=(sec)
1070      end
1071
1072      def close
1073      end
1074
1075      def method_missing(mid, *args)
1076        raise FTPConnectionError, "not connected"
1077      end
1078    end
1079
1080    class BufferedSocket < BufferedIO
1081      [:addr, :peeraddr, :send, :shutdown].each do |method|
1082        define_method(method) { |*args|
1083          @io.__send__(method, *args)
1084        }
1085      end
1086
1087      def read(len = nil)
1088        if len
1089          s = super(len, "", true)
1090          return s.empty? ? nil : s
1091        else
1092          result = ""
1093          while s = super(DEFAULT_BLOCKSIZE, "", true)
1094            break if s.empty?
1095            result << s
1096          end
1097          return result
1098        end
1099      end
1100
1101      def gets
1102        return readuntil("\n")
1103      rescue EOFError
1104        return nil
1105      end
1106
1107      def readline
1108        return readuntil("\n")
1109      end
1110    end
1111    # :startdoc:
1112  end
1113end
1114
1115
1116# Documentation comments:
1117#  - sourced from pickaxe and nutshell, with improvements (hopefully)
1118#  - three methods should be private (search WRITEME)
1119#  - two methods need more information (search TODO)
1120