#!/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