class FakeFtp::Server

Constants

CMDS
LNBK

Attributes

mode[R]
passive_port[RW]
path[R]
port[RW]

Public Class Methods

new(control_port = 21, data_port = nil, options = {}) click to toggle source
# File lib/fake_ftp/server.rb, line 34
def initialize(control_port = 21, data_port = nil, options = {})
  self.port = control_port
  self.passive_port = data_port
  raise(Errno::EADDRINUSE, "#{port}") if !control_port.zero? && is_running?
  raise(Errno::EADDRINUSE, "#{passive_port}") if passive_port && !passive_port.zero? && is_running?(passive_port)
  @connection = nil
  @options = options
  @files = []
  @mode = :active
  @path = "/pub"
end

Public Instance Methods

add_file(filename, data, last_modified_time = Time.now) click to toggle source
# File lib/fake_ftp/server.rb, line 58
def add_file(filename, data, last_modified_time = Time.now)
  @files << FakeFtp::File.new(::File.basename(filename.to_s), data, @mode, last_modified_time)
end
file(name) click to toggle source
# File lib/fake_ftp/server.rb, line 50
def file(name)
  @files.detect { |file| file.name == name }
end
files() click to toggle source
# File lib/fake_ftp/server.rb, line 46
def files
  @files.map(&:name)
end
is_running?(tcp_port = nil) click to toggle source
# File lib/fake_ftp/server.rb, line 104
def is_running?(tcp_port = nil)
  tcp_port.nil? ? port_is_open?(port) : port_is_open?(tcp_port)
end
reset() click to toggle source
# File lib/fake_ftp/server.rb, line 54
def reset
  @files.clear
end
start() click to toggle source
# File lib/fake_ftp/server.rb, line 62
def start
  @started = true
  @server = ::TCPServer.new('127.0.0.1', port)
  @port = @server.addr[1]
  @thread = Thread.new do
    while @started
      @client = @server.accept rescue nil
      if @client
        respond_with('220 Can has FTP?')
        @connection = Thread.new(@client) do |socket|
          while @started && !socket.nil? && !socket.closed?
            input = socket.gets rescue nil
            respond_with parse(input) if input
          end
          unless @client.nil?
            @client.close unless @client.closed?
            @client = nil
          end
        end
      end
    end
    unless @server.nil?
      @server.close unless @server.closed?
      @server = nil
    end
  end

  if passive_port
    @data_server = ::TCPServer.new('127.0.0.1', passive_port)
    @passive_port = @data_server.addr[1]
  end
end
stop() click to toggle source
# File lib/fake_ftp/server.rb, line 95
def stop
  @started = false
  @client.close if @client
  @server.close if @server
  @server = nil
  @data_server.close if @data_server
  @data_server = nil
end

Private Instance Methods

_acct(*args) click to toggle source

FTP commands

Methods are prefixed with an underscore to avoid conflicts with internal server methods. Methods map 1:1 to FTP command words.

# File lib/fake_ftp/server.rb, line 134
def _acct(*args)
  '230 WHATEVER!'
end
_cdup(*args) click to toggle source
# File lib/fake_ftp/server.rb, line 144
def _cdup(*args)
  '250 OK!'
end
_cwd(*args) click to toggle source
# File lib/fake_ftp/server.rb, line 138
def _cwd(*args)
  @path = args[0]
  @path = "/#{path}" if path[0].chr != "/"
  '250 OK!'
end
_dele(filename = '') click to toggle source
# File lib/fake_ftp/server.rb, line 289
def _dele(filename = '')
  files_to_delete = @files.select{ |file| file.name == filename }
  return '550 Delete operation failed.' if files_to_delete.count == 0

  @files = @files - files_to_delete

  '250 Delete operation successful.'
end
_list(*args) click to toggle source
# File lib/fake_ftp/server.rb, line 148
def _list(*args)
  wildcards = []
  args.each do |arg|
    next unless arg.include? '*'
    wildcards << arg.gsub('*', '.*')
  end

  respond_with('425 Ain\'t no data port!') && return if active? && @active_connection.nil?

  respond_with('150 Listing status ok, about to open data connection')
  data_client = active? ? @active_connection : @data_server.accept

  files = @files
  if not wildcards.empty?
    files = files.select do |f|
      wildcards.any? { |wildcard| f.name =~ /#{wildcard}/ }
    end
  end
  files = files.map do |f|
    "-rw-r--r--\t1\towner\tgroup\t#{f.bytes}\t#{f.created.strftime('%b %d %H:%M')}\t#{f.name}\n"
  end
  data_client.write(files.join)
  data_client.close
  @active_connection = nil

  '226 List information transferred'
end
_mdtm(filename = '', local = false) click to toggle source
# File lib/fake_ftp/server.rb, line 176
def _mdtm(filename = '', local = false)
  respond_with('501 No filename given') && return if filename.empty?
  server_file = file(filename)
  respond_with('550 File not found') && return if server_file.nil?

  respond_with("213 #{server_file.last_modified_time.strftime("%Y%m%d%H%M%S")}")
end
_mkd(directory) click to toggle source
# File lib/fake_ftp/server.rb, line 313
def _mkd(directory)
  "257 OK!"
end
_nlst(*args) click to toggle source
# File lib/fake_ftp/server.rb, line 184
def _nlst(*args)
  respond_with('425 Ain\'t no data port!') && return if active? && @active_connection.nil?

  respond_with('150 Listing status ok, about to open data connection')
  data_client = active? ? @active_connection : @data_server.accept

  data_client.write(files.join("\n"))
  data_client.close
  @active_connection = nil

  '226 List information transferred'
end
_pass(*args) click to toggle source
# File lib/fake_ftp/server.rb, line 197
def _pass(*args)
  '230 logged in'
end
_pasv(*args) click to toggle source
# File lib/fake_ftp/server.rb, line 201
def _pasv(*args)
  if passive_port
    @mode = :passive
    p1 = (passive_port / 256).to_i
    p2 = passive_port % 256
    "227 Entering Passive Mode (127,0,0,1,#{p1},#{p2})"
  else
    '502 Aww hell no, use Active'
  end
end
_port(remote = '') click to toggle source
# File lib/fake_ftp/server.rb, line 212
def _port(remote = '')
  remote = remote.split(',')
  remote_port = remote[4].to_i * 256 + remote[5].to_i
  unless @active_connection.nil?
    @active_connection.close
    @active_connection = nil
  end
  @mode = :active
  @active_connection = ::TCPSocket.open('127.0.0.1', remote_port)
  '200 Okay'
end
_pwd(*args) click to toggle source
# File lib/fake_ftp/server.rb, line 224
def _pwd(*args)
  "257 \"#{path}\" is current directory"
end
_quit(*args) click to toggle source
# File lib/fake_ftp/server.rb, line 228
def _quit(*args)
  respond_with '221 OMG bye!'
  @client.close if @client
  @client = nil
end
_retr(filename = '') click to toggle source
# File lib/fake_ftp/server.rb, line 234
def _retr(filename = '')
  respond_with('501 No filename given') if filename.empty?

  file = file(::File.basename(filename.to_s))
  return respond_with('550 File not found') if file.nil?

  respond_with('425 Ain\'t no data port!') && return if active? && @active_connection.nil?

  respond_with('150 File status ok, about to open data connection')
  data_client = active? ? @active_connection : @data_server.accept

  data_client.write(file.data)

  data_client.close
  @active_connection = nil
  '226 File transferred'
end
_rnfr(rename_from='') click to toggle source
# File lib/fake_ftp/server.rb, line 252
def _rnfr(rename_from='')
  return '501 Send path name.' if rename_from.nil? || rename_from.size < 1

  @rename_from = rename_from
  '350 Send RNTO to complete rename.'
end
_rnto(rename_to='') click to toggle source
# File lib/fake_ftp/server.rb, line 259
def _rnto(rename_to='')
  return '501 Send path name.' if rename_to.nil? || rename_to.size < 1

  return '503 Send RNFR first.' unless @rename_from

  if file = file(@rename_from)
    file.name = rename_to
    @rename_from = nil
    '250 Path renamed.'
  else
    @rename_from = nil
    '550 File not found.'
  end
end
_stor(filename = '') click to toggle source
# File lib/fake_ftp/server.rb, line 274
def _stor(filename = '')
  respond_with('425 Ain\'t no data port!') && return if active? && @active_connection.nil?

  respond_with('125 Do it!')
  data_client = active? ? @active_connection : @data_server.accept

  data = data_client.read(nil).chomp
  file = FakeFtp::File.new(::File.basename(filename.to_s), data, @mode)
  @files << file

  data_client.close
  @active_connection = nil
  '226 Did it!'
end
_type(type = 'A') click to toggle source
# File lib/fake_ftp/server.rb, line 298
def _type(type = 'A')
  case type.to_s
  when 'A'
    '200 Type set to A.'
  when 'I'
    '200 Type set to I.'
  else
    '504 We don\'t allow those'
  end
end
_user(name = '') click to toggle source
# File lib/fake_ftp/server.rb, line 309
def _user(name = '')
  (name.to_s == 'anonymous') ? '230 logged in' : '331 send your password'
end
active?() click to toggle source
# File lib/fake_ftp/server.rb, line 317
def active?
  @mode == :active
end
parse(request) click to toggle source
# File lib/fake_ftp/server.rb, line 114
def parse(request)
  return if request.nil?
  puts request if @options[:debug]
  command = request[0, 4].downcase.strip
  contents = request.split
  message = contents[1..contents.length]
  case command
  when *CMDS
    __send__ "_#{command}", *message
  else
    '500 Unknown command'
  end
end
port_is_open?(port) click to toggle source
# File lib/fake_ftp/server.rb, line 323
def port_is_open?(port)
  begin
    Timeout::timeout(1) do
      begin
        s = TCPSocket.new("127.0.0.1", port)
        s.close
        return true
      rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
        return false
      end
    end
  rescue Timeout::Error
  end

  return false
end
respond_with(stuff) click to toggle source
# File lib/fake_ftp/server.rb, line 110
def respond_with(stuff)
  @client.print stuff << LNBK unless stuff.nil? or @client.nil? or @client.closed?
end