wconrad/ftpd

View on GitHub
bin/ftpdrb

Summary

Maintainability
Test Coverage
#!/usr/bin/env ruby
#
# Ftpd is a pure Ruby FTP server library. CLI version
#

dev_lib_path = File.dirname(__FILE__) + '/../lib'
if File.directory?(dev_lib_path)
  unless $:.include?(dev_lib_path)
    $:.unshift(dev_lib_path)
  end
end

require 'ftpd'
require 'ipaddr'
require 'optparse'

module FTPDrb

  # Command-line option parser
  class Arguments

    attr_reader :account
    attr_reader :auth_level
    attr_reader :debug
    attr_reader :eplf
    attr_reader :interface
    attr_reader :nat_ip
    attr_reader :passive_ports
    attr_reader :password
    attr_reader :port
    attr_reader :path
    attr_reader :read_only
    attr_reader :session_timeout
    attr_reader :servername
    attr_reader :tls
    attr_reader :user

    def initialize(argv)
      @interface       = '127.0.0.1'
      @tls             = :explicit
      @port            = 0
      @auth_level      = 'password'
      @user            = ENV['LOGNAME']
      @password        = ''
      @account         = ''
      @path            = nil
      @session_timeout = default_session_timeout
      @servername      = 'FTPd - Pure Ruby FTP server'
      @log             = nil
      @nat_ip          = nil
      @passive_ports   = nil
      
      opts = option_parser
      opts.parse!(argv)
    rescue OptionParser::ParseError => e
      $stderr.puts e
      exit(1)
    end
    
    private
    
    def option_parser
      opts = OptionParser.new do |op|
        op.banner = "ftpdrb - Pure Ruby FTP Server. (ftpd ruby gem)\n\nUsage: #{__FILE__} [OPTIONS]"
        
        op.separator "Help menu:"
        op.on('-p', '--port N', Integer, 'Bind to a specific port') do |t|
          @port = t
        end
        op.on('-i', '--interface IP', 'Bind to a specific interface. (Use "0.0.0.0" for all interfaces)') do |t|
          @interface = t
        end
        op.on('-s', '--servername SERVERNAME', 'Set servername for FTP server)',
              "\tdefault = FTPd - Pure Ruby FTP server") do |t|
          @servername = t
        end
        op.on('--tls [TYPE]', [:off, :explicit, :implicit],
              'Select TLS support (off, explicit, implicit)',
              "\tdefault = off") do |t|
          @tls = t
        end
        op.on('--eplf', 'LIST uses EPLF format') do |t|
          @eplf = t
        end 
        op.on('--read-only', 'Prohibit put, delete, rmdir, etc.') do |t|
          @read_only = t
        end
        op.on('--auth [LEVEL]', [:user, :password, :account],
              'Set authorization level (user, password, account)',
              "\tdefault = password") do |t|
          @auth_level = t
        end
        op.on('-U', '--user NAME', 'User for authentication',
              "\tdefaults to current user") do |t|
          @user = t
        end
        op.on('-P', '--password PASS', 'Password for authentication',
              "\tdefaults to empty string") do |t|
          @password = t
        end
        op.on('-A', '--account P', 'Account for authentication',
              "\tdefaults to empty string") do |t|
          @account = t
        end
        op.on('--path PATH', 'Directory path for share',
              "\tdefaults to random temp directory") do |t|
          @path = t
        end
        op.on('--timeout SEC', Integer, 'Session idle timeout',
              "\tdefaults to #{default_session_timeout}") do |t|
          @session_timeout = t
        end
        op.on('-d', '--debug', 'Write server debug log to stdout') do |t|
          @debug = t
        end
        op.on('--nat-ip IP', 'Set advertised passive mode IP') do |t|
          @nat_ip = t
        end
        op.on('--ports MIN..MAX', 'Port numbers for passive mode sockets') do |v|
          @passive_ports = Range.new(*v.split(/\.\./).map(&:to_i))
        end
        op.on("-h","--help","Show the full help screen.") do
          puts opts
          puts "\nExample:"
          puts "    #{__FILE__} -i 0.0.0.0 -p 21 -U ftp -P ftp --path /tmp/\n\n"
          exit 0
        end
      end
    end

    def default_session_timeout
      Ftpd::FtpServer::DEFAULT_SESSION_TIMEOUT
    end

  end
end

module FTPDrb

  # The FTP server requires and instance of a _driver_ which can
  # authenticate users and create a file system drivers for a given
  # user.
  
  class Driver

    # Your driver's initialize method can be anything you need.  Ftpd
    # does not create an instance of your driver.

    def initialize(user, password, account, data_dir, read_only)
      @user      = user
      @password  = password
      @account   = account
      @data_dir  = data_dir
      @read_only = read_only
    end

    # Return true if the user should be allowed to log in.
    # @param user [String]
    # @param password [String]
    # @param account [String]
    # @return [Boolean]
    #
    # Depending upon the server's auth_level, some of these parameters
    # may be nil.  A parameter with a nil value is not required for
    # authentication.  Here are the parameters that are non-nil for
    # each auth_level:
    # * :user (user)
    # * :password (user, password)
    # * :account (user, password, account)

    def authenticate(user, password, account)
      user == @user &&
        (password.nil? || password == @password) &&
        (account.nil? || account == @account)
    end

    # Return the file system to use for a user.
    # @param user [String]
    # @return A file system driver that quacks like {Ftpd::DiskFileSystem}

    def file_system(user)
      if @read_only
        Ftpd::ReadOnlyDiskFileSystem
      else
        Ftpd::DiskFileSystem.new(@data_dir)
      end
    end
    
  end
end

module FTPDrb
  class Server

    include Ftpd::InsecureCertificate
    
    def initialize(argv)
      @args = Arguments.new(argv)
      # Create a temp directory if path argument is not assigned 
      @args.path.nil? ? @data_dir = Ftpd::TempDir.make : @data_dir = @args.path
      create_files if @args.path.nil?
      @driver = Driver.new(user, password, account,
                           @data_dir, @args.read_only)
      @server = Ftpd::FtpServer.new(@driver)
      configure_server
    end
    
    def run
      @server.start
      wait_until_stopped
    end
    
    def wait_until_stopped
      puts "FTP server started.  Press ENTER or c-C to stop it"
      $stdout.flush
      begin
        gets
      rescue Interrupt
        puts "Interrupt"
      end
    end

    private
    
    def configure_server
      @server.server_name   = @args.servername 
      @server.interface     = @args.interface
      @server.port          = @args.port
      @server.tls           = @args.tls
      @server.passive_ports = @args.passive_ports
      @server.certfile_path = insecure_certfile_path
      if @args.eplf
        @server.list_formatter = Ftpd::ListFormat::Eplf
      end
      @server.auth_level      = auth_level
      @server.session_timeout = @args.session_timeout
      @server.log             = make_log
      @server.nat_ip          = @args.nat_ip
    end

    def auth_level
      Ftpd.const_get("AUTH_#{@args.auth_level.upcase}")
    end

    def create_files
      create_file 'README',
          "Automatically created by FTPdrb.\n" +
          "This file, and the directory it is in, will be deleted\n" + 
          "When ftpd server exits. Make sure your data is backed-up.\n"
    end
    
    def create_file(path, contents)
      full_path = File.expand_path(path, @data_dir)
      FileUtils.mkdir_p File.dirname(full_path)
      File.open(full_path, 'w') do |file|
        file.write contents
      end
    end
    
    def connection_info
      {
        "Servername"=> @server.server_name,
        "Interface" => @server.interface,
        "Port"      => @server.bound_port,
        "User"      => user,
        "Pass"      => "#{password if auth_level >= Ftpd::AUTH_PASSWORD}",
        "Account"   => "#{account  if auth_level >= Ftpd::AUTH_ACCOUNT}",
        "TLS"       => @args.tls,
        "Directory" => @data_dir,
        "URI"       => "ftp://#{user}:#{password}@#{IPAddr.new(@server.interface)}:#{@server.bound_port}",
        "PID"       => $$
      }
    end
    
    def user
      @args.user
    end
    
    def password
      @args.password
    end
    
    def account
      @args.account
    end
    
    def make_log
      @args.debug && Logger.new($stdout)
    end
    
  end
end

FTPDrb::Server.new(ARGV).run