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