lib/net/ftp.rb


DEFINITIONS

This source file includes following functions.


   1  =begin
   2  
   3  = net/ftp.rb
   4  
   5  written by Shugo Maeda <shugo@ruby-lang.org>
   6  
   7  This library is distributed under the terms of the Ruby license.
   8  You can freely distribute/modify this library.
   9  
  10  =end
  11  
  12  require "socket"
  13  require "monitor"
  14  
  15  module Net
  16  
  17    class FTPError < StandardError; end
  18    class FTPReplyError < FTPError; end
  19    class FTPTempError < FTPError; end
  20    class FTPPermError < FTPError; end
  21    class FTPProtoError < FTPError; end
  22  
  23    class FTP
  24      include MonitorMixin
  25      
  26      FTP_PORT = 21
  27      CRLF = "\r\n"
  28  
  29      DEFAULT_BLOCKSIZE = 4096
  30      
  31      attr_accessor :binary, :passive, :return_code, :debug_mode, :resume
  32      attr_reader :welcome, :lastresp
  33      
  34      def FTP.open(host, user = nil, passwd = nil, acct = nil)
  35        new(host, user, passwd, acct)
  36      end
  37      
  38      def initialize(host = nil, user = nil, passwd = nil, acct = nil)
  39        super()
  40        @binary = true
  41        @passive = false
  42        @return_code = "\n"
  43        @debug_mode = false
  44        @resume = false
  45        if host
  46          connect(host)
  47          if user
  48            login(user, passwd, acct)
  49          end
  50        end
  51      end
  52  
  53      def open_socket(host, port)
  54        if defined? SOCKSsocket and ENV["SOCKS_SERVER"]
  55          @passive = true
  56          return SOCKSsocket.open(host, port)
  57        else
  58          return TCPSocket.open(host, port)
  59        end
  60      end
  61      private :open_socket
  62      
  63      def connect(host, port = FTP_PORT)
  64        if @debug_mode
  65          print "connect: ", host, ", ", port, "\n"
  66        end
  67        synchronize do
  68          @sock = open_socket(host, port)
  69          voidresp
  70        end
  71      end
  72  
  73      def set_socket(sock, get_greeting = true)
  74        synchronize do
  75          @sock = sock
  76          if get_greeting
  77            voidresp
  78          end
  79        end
  80      end
  81  
  82      def sanitize(s)
  83        if s =~ /^PASS /i
  84          return s[0, 5] + "*" * (s.length - 5)
  85        else
  86          return s
  87        end
  88      end
  89      private :sanitize
  90      
  91      def putline(line)
  92        if @debug_mode
  93          print "put: ", sanitize(line), "\n"
  94        end
  95        line = line + CRLF
  96        @sock.write(line)
  97      end
  98      private :putline
  99      
 100      def getline
 101        line = @sock.readline # if get EOF, raise EOFError
 102        if line[-2, 2] == CRLF
 103          line = line[0 .. -3]
 104        elsif line[-1] == ?\r or
 105            line[-1] == ?\n
 106          line = line[0 .. -2]
 107        end
 108        if @debug_mode
 109          print "get: ", sanitize(line), "\n"
 110        end
 111        return line
 112      end
 113      private :getline
 114      
 115      def getmultiline
 116        line = getline
 117        buff = line
 118        if line[3] == ?-
 119            code = line[0, 3]
 120          begin
 121            line = getline
 122            buff << "\n" << line
 123          end until line[0, 3] == code and line[3] != ?-
 124        end
 125        return buff << "\n"
 126      end
 127      private :getmultiline
 128      
 129      def getresp
 130        resp = getmultiline
 131        @lastresp = resp[0, 3]
 132        c = resp[0]
 133        case c
 134        when ?1, ?2, ?3
 135          return resp
 136        when ?4
 137          raise FTPTempError, resp
 138        when ?5
 139          raise FTPPermError, resp
 140        else
 141          raise FTPProtoError, resp
 142        end
 143      end
 144      private :getresp
 145      
 146      def voidresp
 147        resp = getresp
 148        if resp[0] != ?2
 149          raise FTPReplyError, resp
 150        end
 151      end
 152      private :voidresp
 153      
 154      def sendcmd(cmd)
 155        synchronize do
 156          putline(cmd)
 157          return getresp
 158        end
 159      end
 160      
 161      def voidcmd(cmd)
 162        synchronize do
 163          putline(cmd)
 164          voidresp
 165        end
 166      end
 167      
 168      def sendport(host, port)
 169        af = (@sock.peeraddr)[0]
 170        if af == "AF_INET"
 171          hbytes = host.split(".")
 172          pbytes = [port / 256, port % 256]
 173          bytes = hbytes + pbytes
 174          cmd = "PORT " + bytes.join(",")
 175        elsif af == "AF_INET6"
 176          cmd = "EPRT |2|" + host + "|" + sprintf("%d", port) + "|"
 177        else
 178          raise FTPProtoError, host
 179        end
 180        voidcmd(cmd)
 181      end
 182      private :sendport
 183      
 184      def makeport
 185        sock = TCPServer.open(@sock.addr[3], 0)
 186        port = sock.addr[1]
 187        host = sock.addr[3]
 188        resp = sendport(host, port)
 189        return sock
 190      end
 191      private :makeport
 192      
 193      def makepasv
 194        if @sock.peeraddr[0] == "AF_INET"
 195          host, port = parse227(sendcmd("PASV"))
 196        else
 197          host, port = parse229(sendcmd("EPSV"))
 198          #     host, port = parse228(sendcmd("LPSV"))
 199        end
 200        return host, port
 201      end
 202      private :makepasv
 203      
 204      def transfercmd(cmd, rest_offset = nil)
 205        if @passive
 206          host, port = makepasv
 207          conn = open_socket(host, port)
 208          if @resume and rest_offset
 209            resp = sendcmd("REST " + rest_offset.to_s) 
 210            if resp[0] != ?3
 211              raise FTPReplyError, resp
 212            end
 213          end
 214          resp = sendcmd(cmd)
 215          if resp[0] != ?1
 216            raise FTPReplyError, resp
 217          end
 218        else
 219          sock = makeport
 220          if @resume and rest_offset
 221            resp = sendcmd("REST " + rest_offset.to_s) 
 222            if resp[0] != ?3
 223              raise FTPReplyError, resp
 224            end
 225          end
 226          resp = sendcmd(cmd)
 227          if resp[0] != ?1
 228            raise FTPReplyError, resp
 229          end
 230          conn = sock.accept
 231          sock.close
 232        end
 233        return conn
 234      end
 235      private :transfercmd
 236      
 237      def getaddress
 238        thishost = Socket.gethostname
 239        if not thishost.index(".")
 240          thishost = Socket.gethostbyname(thishost)[0]
 241        end
 242        if ENV.has_key?("LOGNAME")
 243          realuser = ENV["LOGNAME"]
 244        elsif ENV.has_key?("USER")
 245          realuser = ENV["USER"]
 246        else
 247          realuser = "anonymous"
 248        end
 249        return realuser + "@" + thishost
 250      end
 251      private :getaddress
 252      
 253      def login(user = "anonymous", passwd = nil, acct = nil)
 254        if user == "anonymous" and passwd == nil
 255          passwd = getaddress
 256        end
 257        
 258        resp = ""
 259        synchronize do
 260          resp = sendcmd('USER ' + user)
 261          if resp[0] == ?3
 262            resp = sendcmd('PASS ' + passwd)
 263          end
 264          if resp[0] == ?3
 265            resp = sendcmd('ACCT ' + acct)
 266          end
 267        end
 268        if resp[0] != ?2
 269          raise FTPReplyError, resp
 270        end
 271        @welcome = resp
 272      end
 273      
 274      def retrbinary(cmd, blocksize, rest_offset = nil)
 275        synchronize do
 276          voidcmd("TYPE I")
 277          conn = transfercmd(cmd, rest_offset)
 278          loop do
 279            data = conn.read(blocksize)
 280            break if data == nil
 281            yield(data)
 282          end
 283          conn.close
 284          voidresp
 285        end
 286      end
 287      
 288      def retrlines(cmd)
 289        synchronize do
 290          voidcmd("TYPE A")
 291          conn = transfercmd(cmd)
 292          loop do
 293            line = conn.gets
 294            break if line == nil
 295            if line[-2, 2] == CRLF
 296              line = line[0 .. -3]
 297            elsif line[-1] == ?\n
 298              line = line[0 .. -2]
 299            end
 300            yield(line)
 301          end
 302          conn.close
 303          voidresp
 304        end
 305      end
 306      
 307      def storbinary(cmd, file, blocksize, rest_offset = nil, &block)
 308        synchronize do
 309          voidcmd("TYPE I")
 310          conn = transfercmd(cmd, rest_offset)
 311          loop do
 312            buf = file.read(blocksize)
 313            break if buf == nil
 314            conn.write(buf)
 315            yield(buf) if block
 316          end
 317          conn.close
 318          voidresp
 319        end
 320      end
 321      
 322      def storlines(cmd, file, &block)
 323        synchronize do
 324          voidcmd("TYPE A")
 325          conn = transfercmd(cmd)
 326          loop do
 327            buf = file.gets
 328            break if buf == nil
 329            if buf[-2, 2] != CRLF
 330              buf = buf.chomp + CRLF
 331            end
 332            conn.write(buf)
 333            yield(buf) if block
 334          end
 335          conn.close
 336          voidresp
 337        end
 338      end
 339  
 340      def getbinaryfile(remotefile, localfile = File.basename(remotefile),
 341                        blocksize = DEFAULT_BLOCKSIZE, &block)
 342        if @resume
 343          rest_offset = File.size?(localfile)
 344          f = open(localfile, "a")
 345        else
 346          rest_offset = nil
 347          f = open(localfile, "w")
 348        end
 349        begin
 350          f.binmode
 351          retrbinary("RETR " + remotefile, blocksize, rest_offset) do |data|
 352            f.write(data)
 353            yield(data) if block
 354          end
 355        ensure
 356          f.close
 357        end
 358      end
 359      
 360      def gettextfile(remotefile, localfile = File.basename(remotefile), &block)
 361        f = open(localfile, "w")
 362        begin
 363          retrlines("RETR " + remotefile) do |line|
 364            line = line + @return_code
 365            f.write(line)
 366            yield(line) if block
 367          end
 368        ensure
 369          f.close
 370        end
 371      end
 372  
 373      def get(localfile, remotefile = File.basename(localfile),
 374              blocksize = DEFAULT_BLOCKSIZE, &block)
 375        unless @binary
 376          gettextfile(localfile, remotefile, &block)
 377        else
 378          getbinaryfile(localfile, remotefile, blocksize, &block)
 379        end
 380      end
 381      
 382      def putbinaryfile(localfile, remotefile = File.basename(localfile),
 383                        blocksize = DEFAULT_BLOCKSIZE, &block)
 384        if @resume
 385          rest_offset = size(remotefile)
 386        else
 387          rest_offset = nil
 388        end
 389        f = open(localfile)
 390        begin
 391          f.binmode
 392          storbinary("STOR " + remotefile, f, blocksize, rest_offset, &block)
 393        ensure
 394          f.close
 395        end
 396      end
 397      
 398      def puttextfile(localfile, remotefile = File.basename(localfile), &block)
 399        f = open(localfile)
 400        begin
 401          storlines("STOR " + remotefile, f, &block)
 402        ensure
 403          f.close
 404        end
 405      end
 406  
 407      def put(localfile, remotefile = File.basename(localfile),
 408              blocksize = DEFAULT_BLOCKSIZE, &block)
 409        unless @binary
 410          puttextfile(localfile, remotefile, &block)
 411        else
 412          putbinaryfile(localfile, remotefile, blocksize, &block)
 413        end
 414      end
 415  
 416      def acct(account)
 417        cmd = "ACCT " + account
 418        voidcmd(cmd)
 419      end
 420      
 421      def nlst(dir = nil)
 422        cmd = "NLST"
 423        if dir
 424          cmd = cmd + " " + dir
 425        end
 426        files = []
 427        retrlines(cmd) do |line|
 428          files.push(line)
 429        end
 430        return files
 431      end
 432      
 433      def list(*args, &block)
 434        cmd = "LIST"
 435        args.each do |arg|
 436          cmd = cmd + " " + arg
 437        end
 438        if block
 439          retrlines(cmd, &block)
 440        else
 441          lines = []
 442          retrlines(cmd) do |line|
 443            lines << line
 444          end
 445          return lines
 446        end
 447      end
 448      alias ls list
 449      alias dir list
 450      
 451      def rename(fromname, toname)
 452        resp = sendcmd("RNFR " + fromname)
 453        if resp[0] != ?3
 454          raise FTPReplyError, resp
 455        end
 456        voidcmd("RNTO " + toname)
 457      end
 458      
 459      def delete(filename)
 460        resp = sendcmd("DELE " + filename)
 461        if resp[0, 3] == "250"
 462          return
 463        elsif resp[0] == ?5
 464          raise FTPPermError, resp
 465        else
 466          raise FTPReplyError, resp
 467        end
 468      end
 469      
 470      def chdir(dirname)
 471        if dirname == ".."
 472          begin
 473            voidcmd("CDUP")
 474            return
 475          rescue FTPPermError
 476            if $![0, 3] != "500"
 477              raise FTPPermError, $!
 478            end
 479          end
 480        end
 481        cmd = "CWD " + dirname
 482        voidcmd(cmd)
 483      end
 484      
 485      def size(filename)
 486        voidcmd("TYPE I")
 487        resp = sendcmd("SIZE " + filename)
 488        if resp[0, 3] != "213" 
 489          raise FTPReplyError, resp
 490        end
 491        return resp[3..-1].strip.to_i
 492      end
 493      
 494      MDTM_REGEXP = /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/
 495      
 496      def mtime(filename, local = false)
 497        str = mdtm(filename)
 498        ary = str.scan(MDTM_REGEXP)[0].collect {|i| i.to_i}
 499        return local ? Time.local(*ary) : Time.gm(*ary)
 500      end
 501      
 502      def mkdir(dirname)
 503        resp = sendcmd("MKD " + dirname)
 504        return parse257(resp)
 505      end
 506      
 507      def rmdir(dirname)
 508        voidcmd("RMD " + dirname)
 509      end
 510      
 511      def pwd
 512        resp = sendcmd("PWD")
 513        return parse257(resp)
 514      end
 515      alias getdir pwd
 516      
 517      def system
 518        resp = sendcmd("SYST")
 519        if resp[0, 3] != "215"
 520          raise FTPReplyError, resp
 521        end
 522        return resp[4 .. -1]
 523      end
 524      
 525      def abort
 526        line = "ABOR" + CRLF
 527        print "put: ABOR\n" if @debug_mode
 528        @sock.send(line, Socket::MSG_OOB)
 529        resp = getmultiline
 530        unless ["426", "226", "225"].include?(resp[0, 3])
 531          raise FTPProtoError, resp
 532        end
 533        return resp
 534      end
 535      
 536      def status
 537        line = "STAT" + CRLF
 538        print "put: STAT\n" if @debug_mode
 539        @sock.send(line, Socket::MSG_OOB)
 540        return getresp
 541      end
 542      
 543      def mdtm(filename)
 544        resp = sendcmd("MDTM " + filename)
 545        if resp[0, 3] == "213"
 546          return resp[3 .. -1].strip
 547        end
 548      end
 549      
 550      def help(arg = nil)
 551        cmd = "HELP"
 552        if arg
 553          cmd = cmd + " " + arg
 554        end
 555        sendcmd(cmd)
 556      end
 557      
 558      def quit
 559        voidcmd("QUIT")
 560      end
 561  
 562      def noop
 563        voidcmd("NOOP")
 564      end
 565  
 566      def site(arg)
 567        cmd = "SITE " + arg
 568        voidcmd(cmd)
 569      end
 570      
 571      def close
 572        @sock.close if @sock and not @sock.closed?
 573      end
 574      
 575      def closed?
 576        @sock == nil or @sock.closed?
 577      end
 578      
 579      def parse227(resp)
 580        if resp[0, 3] != "227"
 581          raise FTPReplyError, resp
 582        end
 583        left = resp.index("(")
 584        right = resp.index(")")
 585        if left == nil or right == nil
 586          raise FTPProtoError, resp
 587        end
 588        numbers = resp[left + 1 .. right - 1].split(",")
 589        if numbers.length != 6
 590          raise FTPProtoError, resp
 591        end
 592        host = numbers[0, 4].join(".")
 593        port = (numbers[4].to_i << 8) + numbers[5].to_i
 594        return host, port
 595      end
 596      private :parse227
 597      
 598      def parse228(resp)
 599        if resp[0, 3] != "228"
 600          raise FTPReplyError, resp
 601        end
 602        left = resp.index("(")
 603        right = resp.index(")")
 604        if left == nil or right == nil
 605          raise FTPProtoError, resp
 606        end
 607        numbers = resp[left + 1 .. right - 1].split(",")
 608        if numbers[0] == "4"
 609          if numbers.length != 9 || numbers[1] != "4" || numbers[2 + 4] != "2"
 610            raise FTPProtoError, resp
 611          end
 612          host = numbers[2, 4].join(".")
 613          port = (numbers[7].to_i << 8) + numbers[8].to_i
 614        elsif numbers[0] == "6"
 615          if numbers.length != 21 || numbers[1] != "16" || numbers[2 + 16] != "2"
 616            raise FTPProtoError, resp
 617          end
 618          v6 = ["", "", "", "", "", "", "", ""]
 619          for i in 0 .. 7
 620            v6[i] = sprintf("%02x%02x", numbers[(i * 2) + 2].to_i,
 621                            numbers[(i * 2) + 3].to_i)
 622          end
 623          host = v6[0, 8].join(":")
 624          port = (numbers[19].to_i << 8) + numbers[20].to_i
 625        end 
 626        return host, port
 627      end
 628      private :parse228
 629      
 630      def parse229(resp)
 631        if resp[0, 3] != "229"
 632          raise FTPReplyError, resp
 633        end
 634        left = resp.index("(")
 635        right = resp.index(")")
 636        if left == nil or right == nil
 637          raise FTPProtoError, resp
 638        end
 639        numbers = resp[left + 1 .. right - 1].split(resp[left + 1, 1])
 640        if numbers.length != 4
 641          raise FTPProtoError, resp
 642        end
 643        port = numbers[3].to_i
 644        host = (@sock.peeraddr())[3]
 645        return host, port
 646      end
 647      private :parse229
 648      
 649      def parse257(resp)
 650        if resp[0, 3] != "257"
 651          raise FTPReplyError, resp
 652        end
 653        if resp[3, 2] != ' "'
 654          return ""
 655        end
 656        dirname = ""
 657        i = 5
 658        n = resp.length
 659        while i < n
 660          c = resp[i, 1]
 661          i = i + 1
 662          if c == '"'
 663            if i > n or resp[i, 1] != '"'
 664              break
 665            end
 666            i = i + 1
 667          end
 668          dirname = dirname + c
 669        end
 670        return dirname
 671      end
 672      private :parse257
 673    end
 674  
 675  end