examples/example.rb
#!/usr/bin/env ruby
unless $:.include?(File.dirname(__FILE__) + '/../lib')
$:.unshift(File.dirname(__FILE__) + '/../lib')
end
require 'ftpd'
require 'ipaddr'
require 'optparse'
module Example
# 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 :read_only
attr_reader :session_timeout
attr_reader :tls
attr_reader :user
def initialize(argv)
@interface = '127.0.0.1'
@tls = :explicit
@port = 0
@auth_level = 'password'
# When running on travisci, the LOGNAME environment variable is
# not set, but we require it to be set.
@user = ENV['LOGNAME'] || "test"
@password = ''
@account = ''
@session_timeout = default_session_timeout
@log = nil
@nat_ip = nil
@passive_ports = nil
op = option_parser
op.parse!(argv)
rescue OptionParser::ParseError => e
$stderr.puts e
exit(1)
end
private
def option_parser
OptionParser.new do |op|
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') do |t|
@interface = t
end
op.on('--tls [TYPE]', [:off, :explicit, :implicit],
'Select TLS support (off, explicit, implicit)',
'default = 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)',
'default = password') do |t|
@auth_level = t
end
op.on('-U', '--user NAME', 'User for authentication',
'defaults to current user') do |t|
@user = t
end
op.on('-P', '--password PW', 'Password for authentication',
'defaults to empty string') do |t|
@password = t
end
op.on('-A', '--account PW', 'Account for authentication',
'defaults to empty string') do |t|
@account = t
end
op.on('--timeout SEC', Integer, 'Session idle timeout',
"defaults 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
end
end
def default_session_timeout
Ftpd::FtpServer::DEFAULT_SESSION_TIMEOUT
end
end
end
module Example
# The FTP server requires and instance of a _driver_ which can
# authenticate users and create a file system drivers for a given
# user. You can use this as a template for creating your own
# driver.
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
end.new(@data_dir)
end
end
end
module Example
class Main
include Ftpd::InsecureCertificate
def initialize(argv)
@args = Arguments.new(argv)
@data_dir = Ftpd::TempDir.make
create_files
@driver = Driver.new(user, password, account,
@data_dir, @args.read_only)
@server = Ftpd::FtpServer.new(@driver)
configure_server
@server.start
display_connection_info
create_connection_script
end
def run
wait_until_stopped
end
private
def configure_server
@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',
"This file, and the directory it is in, will go away\n"
"When this example exits.\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 display_connection_info
puts "Interface: #{@server.interface}"
puts "Port: #{@server.bound_port}"
puts "User: #{user.inspect}"
puts "Pass: #{password.inspect}" if auth_level >= Ftpd::AUTH_PASSWORD
puts "Account: #{account.inspect}" if auth_level >= Ftpd::AUTH_ACCOUNT
puts "TLS: #{@args.tls}"
puts "Directory: #{@data_dir}"
puts "URI: #{uri}"
puts "PID: #{$$}"
end
def uri
"ftp://#{connection_host}:#{@server.bound_port}"
end
def create_connection_script
command_path = '/tmp/connect-to-example-ftp-server.sh'
File.open(command_path, 'w') do |file|
file.puts "#!/bin/bash"
file.puts "ftp $FTP_ARGS #{connection_host} #{@server.bound_port}"
end
system("chmod +x #{command_path}")
puts "Connection script written to #{command_path}"
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
def user
@args.user
end
def password
@args.password
end
def account
@args.account
end
def make_log
@args.debug && Logger.new($stdout)
end
def connection_host
addr = IPAddr.new(@server.interface)
if addr.ipv6?
'::1'
else
'127.0.0.1'
end
end
end
end
Example::Main.new(ARGV).run if $0 == __FILE__