=begin = net/ftp.rb written by Shugo Maeda This library is distributed under the terms of the Ruby license. You can freely distribute/modify this library. =end require "socket" require "monitor" module Net class FTPError < StandardError; end class FTPReplyError < FTPError; end class FTPTempError < FTPError; end class FTPPermError < FTPError; end class FTPProtoError < FTPError; end class FTP include MonitorMixin FTP_PORT = 21 CRLF = "\r\n" DEFAULT_BLOCKSIZE = 4096 attr_accessor :binary, :passive, :return_code, :debug_mode, :resume attr_reader :welcome, :lastresp def FTP.open(host, user = nil, passwd = nil, acct = nil) new(host, user, passwd, acct) end def initialize(host = nil, user = nil, passwd = nil, acct = nil) super() @binary = true @passive = false @return_code = "\n" @debug_mode = false @resume = false if host connect(host) if user login(user, passwd, acct) end end end def open_socket(host, port) if defined? SOCKSsocket and ENV["SOCKS_SERVER"] @passive = true return SOCKSsocket.open(host, port) else return TCPSocket.open(host, port) end end private :open_socket def connect(host, port = FTP_PORT) if @debug_mode print "connect: ", host, ", ", port, "\n" end synchronize do @sock = open_socket(host, port) voidresp end end def sanitize(s) if s =~ /^PASS /i return s[0, 5] + "*" * (s.length - 5) else return s end end private :sanitize def putline(line) if @debug_mode print "put: ", sanitize(line), "\n" end line = line + CRLF @sock.write(line) end private :putline def getline line = @sock.readline # if get EOF, raise EOFError if line[-2, 2] == CRLF line = line[0 .. -3] elsif line[-1] == ?\r or line[-1] == ?\n line = line[0 .. -2] end if @debug_mode print "get: ", sanitize(line), "\n" end return line end private :getline def getmultiline line = getline buff = line if line[3] == ?- code = line[0, 3] begin line = getline buff << "\n" << line end until line[0, 3] == code and line[3] != ?- end return buff << "\n" end private :getmultiline def getresp resp = getmultiline @lastresp = resp[0, 3] c = resp[0] case c when ?1, ?2, ?3 return resp when ?4 raise FTPTempError, resp when ?5 raise FTPPermError, resp else raise FTPProtoError, resp end end private :getresp def voidresp resp = getresp if resp[0] != ?2 raise FTPReplyError, resp end end private :voidresp def sendcmd(cmd) synchronize do putline(cmd) return getresp end end def voidcmd(cmd) synchronize do putline(cmd) voidresp end end def sendport(host, port) af = (@sock.peeraddr)[0] if af == "AF_INET" hbytes = host.split(".") pbytes = [port / 256, port % 256] bytes = hbytes + pbytes cmd = "PORT " + bytes.join(",") elsif af == "AF_INET6" cmd = "EPRT |2|" + host + "|" + sprintf("%d", port) + "|" else raise FTPProtoError, host end voidcmd(cmd) end private :sendport def makeport sock = TCPServer.open(@sock.addr[3], 0) port = sock.addr[1] host = sock.addr[3] resp = sendport(host, port) return sock end private :makeport def makepasv if @sock.peeraddr[0] == "AF_INET" host, port = parse227(sendcmd("PASV")) else host, port = parse229(sendcmd("EPSV")) # host, port = parse228(sendcmd("LPSV")) end return host, port end private :makepasv def transfercmd(cmd, rest_offset = nil) if @passive host, port = makepasv conn = open_socket(host, port) if @resume and rest_offset resp = sendcmd("REST " + rest_offset.to_s) if resp[0] != ?3 raise FTPReplyError, resp end end resp = sendcmd(cmd) if resp[0] != ?1 raise FTPReplyError, resp end else sock = makeport if @resume and rest_offset resp = sendcmd("REST " + rest_offset.to_s) if resp[0] != ?3 raise FTPReplyError, resp end end resp = sendcmd(cmd) if resp[0] != ?1 raise FTPReplyError, resp end conn = sock.accept sock.close end return conn end private :transfercmd def getaddress thishost = Socket.gethostname if not thishost.index(".") thishost = Socket.gethostbyname(thishost)[0] end if ENV.has_key?("LOGNAME") realuser = ENV["LOGNAME"] elsif ENV.has_key?("USER") realuser = ENV["USER"] else realuser = "anonymous" end return realuser + "@" + thishost end private :getaddress def login(user = "anonymous", passwd = nil, acct = nil) if user == "anonymous" and passwd == nil passwd = getaddress end resp = "" synchronize do resp = sendcmd('USER ' + user) if resp[0] == ?3 resp = sendcmd('PASS ' + passwd) end if resp[0] == ?3 resp = sendcmd('ACCT ' + acct) end end if resp[0] != ?2 raise FTPReplyError, resp end @welcome = resp end def retrbinary(cmd, blocksize, rest_offset = nil) synchronize do voidcmd("TYPE I") conn = transfercmd(cmd, rest_offset) loop do data = conn.read(blocksize) break if data == nil yield(data) end conn.close voidresp end end def retrlines(cmd) synchronize do voidcmd("TYPE A") conn = transfercmd(cmd) loop do line = conn.gets break if line == nil if line[-2, 2] == CRLF line = line[0 .. -3] elsif line[-1] == ?\n line = line[0 .. -2] end yield(line) end conn.close voidresp end end def storbinary(cmd, file, blocksize, rest_offset = nil, &block) synchronize do voidcmd("TYPE I") conn = transfercmd(cmd, rest_offset) loop do buf = file.read(blocksize) break if buf == nil conn.write(buf) yield(buf) if block end conn.close voidresp end end def storlines(cmd, file, &block) synchronize do voidcmd("TYPE A") conn = transfercmd(cmd) loop do buf = file.gets break if buf == nil if buf[-2, 2] != CRLF buf = buf.chomp + CRLF end conn.write(buf) yield(buf) if block end conn.close voidresp end end def getbinaryfile(remotefile, localfile = File.basename(remotefile), blocksize = DEFAULT_BLOCKSIZE, &block) if @resume rest_offset = File.size?(localfile) f = open(localfile, "a") else rest_offset = nil f = open(localfile, "w") end begin f.binmode retrbinary("RETR " + remotefile, blocksize, rest_offset) do |data| f.write(data) yield(data) if block end ensure f.close end end def gettextfile(remotefile, localfile = File.basename(remotefile), &block) f = open(localfile, "w") begin retrlines("RETR " + remotefile) do |line| line = line + @return_code f.write(line) yield(line) if block end ensure f.close end end def get(localfile, remotefile = File.basename(localfile), blocksize = DEFAULT_BLOCKSIZE, &block) unless @binary gettextfile(localfile, remotefile, &block) else getbinaryfile(localfile, remotefile, blocksize, &block) end end def putbinaryfile(localfile, remotefile = File.basename(localfile), blocksize = DEFAULT_BLOCKSIZE, &block) if @resume rest_offset = size(remotefile) else rest_offset = nil end f = open(localfile) begin f.binmode storbinary("STOR " + remotefile, f, blocksize, rest_offset, &block) ensure f.close end end def puttextfile(localfile, remotefile = File.basename(localfile), &block) f = open(localfile) begin storlines("STOR " + remotefile, f, &block) ensure f.close end end def put(localfile, remotefile = File.basename(localfile), blocksize = DEFAULT_BLOCKSIZE, &block) unless @binary puttextfile(localfile, remotefile, &block) else putbinaryfile(localfile, remotefile, blocksize, &block) end end def acct(account) cmd = "ACCT " + account voidcmd(cmd) end def nlst(dir = nil) cmd = "NLST" if dir cmd = cmd + " " + dir end files = [] retrlines(cmd) do |line| files.push(line) end return files end def list(*args, &block) cmd = "LIST" args.each do |arg| cmd = cmd + " " + arg end if block retrlines(cmd, &block) else lines = [] retrlines(cmd) do |line| lines << line end return lines end end alias ls list alias dir list def rename(fromname, toname) resp = sendcmd("RNFR " + fromname) if resp[0] != ?3 raise FTPReplyError, resp end voidcmd("RNTO " + toname) end def delete(filename) resp = sendcmd("DELE " + filename) if resp[0, 3] == "250" return elsif resp[0] == ?5 raise FTPPermError, resp else raise FTPReplyError, resp end end def chdir(dirname) if dirname == ".." begin voidcmd("CDUP") return rescue FTPPermError if $![0, 3] != "500" raise FTPPermError, $! end end end cmd = "CWD " + dirname voidcmd(cmd) end def size(filename) voidcmd("TYPE I") resp = sendcmd("SIZE " + filename) if resp[0, 3] != "213" raise FTPReplyError, resp end return resp[3..-1].strip.to_i end MDTM_REGEXP = /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/ def mtime(filename, local = false) str = mdtm(filename) ary = str.scan(MDTM_REGEXP)[0].collect {|i| i.to_i} return local ? Time.local(*ary) : Time.gm(*ary) end def mkdir(dirname) resp = sendcmd("MKD " + dirname) return parse257(resp) end def rmdir(dirname) voidcmd("RMD " + dirname) end def pwd resp = sendcmd("PWD") return parse257(resp) end alias getdir pwd def system resp = sendcmd("SYST") if resp[0, 3] != "215" raise FTPReplyError, resp end return resp[4 .. -1] end def abort line = "ABOR" + CRLF print "put: ABOR\n" if @debug_mode @sock.send(line, Socket::MSG_OOB) resp = getmultiline unless ["426", "226", "225"].include?(resp[0, 3]) raise FTPProtoError, resp end return resp end def status line = "STAT" + CRLF print "put: STAT\n" if @debug_mode @sock.send(line, Socket::MSG_OOB) return getresp end def mdtm(filename) resp = sendcmd("MDTM " + filename) if resp[0, 3] == "213" return resp[3 .. -1].strip end end def help(arg = nil) cmd = "HELP" if arg cmd = cmd + " " + arg end sendcmd(cmd) end def quit voidcmd("QUIT") end def noop voidcmd("NOOP") end def site(arg) cmd = "SITE " + arg voidcmd(cmd) end def close @sock.close if @sock and not @sock.closed? end def closed? @sock == nil or @sock.closed? end def parse227(resp) if resp[0, 3] != "227" raise FTPReplyError, resp end left = resp.index("(") right = resp.index(")") if left == nil or right == nil raise FTPProtoError, resp end numbers = resp[left + 1 .. right - 1].split(",") if numbers.length != 6 raise FTPProtoError, resp end host = numbers[0, 4].join(".") port = (numbers[4].to_i << 8) + numbers[5].to_i return host, port end private :parse227 def parse228(resp) if resp[0, 3] != "228" raise FTPReplyError, resp end left = resp.index("(") right = resp.index(")") if left == nil or right == nil raise FTPProtoError, resp end numbers = resp[left + 1 .. right - 1].split(",") if numbers[0] == "4" if numbers.length != 9 || numbers[1] != "4" || numbers[2 + 4] != "2" raise FTPProtoError, resp end host = numbers[2, 4].join(".") port = (numbers[7].to_i << 8) + numbers[8].to_i elsif numbers[0] == "6" if numbers.length != 21 || numbers[1] != "16" || numbers[2 + 16] != "2" raise FTPProtoError, resp end v6 = ["", "", "", "", "", "", "", ""] for i in 0 .. 7 v6[i] = sprintf("%02x%02x", numbers[(i * 2) + 2].to_i, numbers[(i * 2) + 3].to_i) end host = v6[0, 8].join(":") port = (numbers[19].to_i << 8) + numbers[20].to_i end return host, port end private :parse228 def parse229(resp) if resp[0, 3] != "229" raise FTPReplyError, resp end left = resp.index("(") right = resp.index(")") if left == nil or right == nil raise FTPProtoError, resp end numbers = resp[left + 1 .. right - 1].split(resp[left + 1, 1]) if numbers.length != 4 raise FTPProtoError, resp end port = numbers[3].to_i host = (@sock.peeraddr())[3] return host, port end private :parse229 def parse257(resp) if resp[0, 3] != "257" raise FTPReplyError, resp end if resp[3, 2] != ' "' return "" end dirname = "" i = 5 n = resp.length while i < n c = resp[i, 1] i = i + 1 if c == '"' if i > n or resp[i, 1] != '"' break end i = i + 1 end dirname = dirname + c end return dirname end private :parse257 end end