module NetFTPSpecs class DummyFTP attr_accessor :connect_message attr_reader :login_user, :login_pass, :login_acct # hostname or IP address attr_reader :hostname # port number attr_reader :server_port def initialize @hostname = "localhost" @server = TCPServer.new(@hostname, 0) @server_port = @server.addr[1] @handlers = {} @commands = [] @connect_message = nil end def serve_once @thread = Thread.new do @socket = @server.accept @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_OOBINLINE, 1) begin handle_request ensure @socket.close end end end def handle_request # Send out the welcome message. response @connect_message || "220 Dummy FTP Server ready!" begin while command = @socket.gets command, argument = command.chomp.split(" ", 2) if command == "QUIT" self.response("221 OK, bye") break elsif proc_handler = @handlers[command.downcase.to_sym] if argument.nil? proc_handler.call(self) else proc_handler.call(self, argument) end else if argument.nil? self.send(command.downcase.to_sym) else self.send(command.downcase.to_sym, argument) end end end rescue => e self.error_response("Exception: #{e} #{e.backtrace.inspect}") end end def error_response(text) self.response("451 #{text}") end def response(text) @socket.puts(text) unless @socket.closed? end def stop @datasocket.close unless @datasocket.nil? || @datasocket.closed? @server.close @thread.join end ## def handle(sym, &block) @handlers[sym] = block end def should_receive(method) @handler_for = method self end def and_respond(text) @handlers[@handler_for] = -> s, *args { s.response(text) } end ## # FTP methods ## def abor self.response("226 Closing data connection. (ABOR)") end def acct(account) @login_acct = account self.response("230 User '#{account}' logged in, proceed. (ACCT)") end def cdup self.response("200 Command okay. (CDUP)") end def cwd(dir) self.response("200 Command okay. (CWD #{dir})") end def dele(file) self.response("250 Requested file action okay, completed. (DELE #{file})") end def eprt(arg) _, _, host, port = arg.split("|") @datasocket = TCPSocket.new(host, port) self.response("200 port opened") end def help(param = :default) if param == :default self.response("211 System status, or system help reply. (HELP)") else self.response("211 System status, or system help reply. (HELP #{param})") end end def list(folder) self.response("150 opening ASCII connection for file list") @datasocket.puts("-rw-r--r-- 1 spec staff 507 17 Jul 18:41 last_response_code.rb") @datasocket.puts("-rw-r--r-- 1 spec staff 50 17 Jul 18:41 list.rb") @datasocket.puts("-rw-r--r-- 1 spec staff 48 17 Jul 18:41 pwd.rb") @datasocket.close() self.response("226 transfer complete (LIST #{folder})") end def mdtm(filename) self.response("213 19980705132316") end def mkd(foldername) self.response(%Q{257 "#{foldername.gsub('"', '""')}" created.}) end def nlst(folder = nil) self.response("150 opening ASCII connection for file list") @datasocket.puts("last_response_code.rb") @datasocket.puts("list.rb") @datasocket.puts("pwd.rb") @datasocket.close() self.response("226 transfer complete (NLST#{folder ? " #{folder}" : ""})") end def noop self.response("200 Command okay. (NOOP)") end def pass(password) @login_pass = password self.response("230 User logged in, proceed. (PASS #{password})") end def port(arg) nums = arg.split(",") if nums[0] == "::1" # IPv6 port = nums[1].to_i * 256 + nums[2].to_i host = nums[0] else # IPv4 port = nums[4].to_i * 256 + nums[5].to_i host = nums[0..3].join(".") end @datasocket = TCPSocket.new(host, port) self.response("200 port opened") end def pwd self.response('257 "/some/dir/" - current directory') end def retr(file) self.response("125 Data transfer starting") if @restart_at && @restart_at == 20 @datasocket.puts("of the file named '#{file}'.") @restart_at = nil else @datasocket.puts("This is the content") @datasocket.puts("of the file named '#{file}'.") end @datasocket.close() self.response("226 Closing data connection. (RETR #{file})") end def rest(at_bytes) @restart_at = at_bytes.to_i self.response("350 Requested file action pending further information. (REST)") end def rmd(folder) self.response("250 Requested file action okay, completed. (RMD #{folder})") end def rnfr(from) @rename_from = from self.response("350 Requested file action pending further information.") end def rnto(to) self.response("250 Requested file action okay, completed. (Renamed #{@rename_from} to #{to})") @rename_from = nil end def site(param) self.response("200 Command okay. (SITE #{param})") end def size(filename) if filename == "binary" self.response("213 24") else self.response("213 1024") end end def stat(param = :default) if param == :default self.response("211 System status, or system help reply. (STAT)") else self.response("211 System status, or system help reply. (STAT #{param})") end end def stor(file) tmp_file = tmp("#{file}file", false) self.response("125 Data transfer starting.") mode = @restart_at ? "a" : "w" File.open(tmp_file, mode + "b") do |f| loop do data = @datasocket.recv(1024) break if !data || data.empty? f << data end end @datasocket.close() self.response("200 OK, Data received. (STOR #{file})") end def appe(file) @restart_at = true stor(file) end def syst self.response("215 FTP Dummy Server (SYST)") end def type(type) self.response("200 TYPE switched to #{type}") end def user(name) @login_user = name self.response("230 User logged in, proceed. (USER #{name})") end end end