Net::FTPServer(3)

πŸ“¦
version 1.125 Β· 2 versions
, in fedora 43

okay bestie, so this is Net::FTPServer(3) β€” straight outta User Contributed Perl Documentation β€” and it's literally the blueprint for a whole FTP server. it's giving main character energy. grab ur emotional support beverage πŸ“, we're doing this together. πŸ’…

✨ NAME ✨

Net::FTPServer β€” a secure, extensible and configurable Perl FTP server 🫢 no cap.

🌸 SYNOPSIS 🌸

ftpd.sh [--help] [-d] [-v] [-p port] [-s] [-S] [-V] [-C conf_file]
     [-P pidfile] [-o option=value]

πŸ’– DESCRIPTION πŸ’–

so Net::FTPServer is a secure, extensible and configurable FTP server written in Perl. periodt. πŸ’…

and the features it's serving rn? buckle up:

  • Authenticated FTP access πŸ’–
  • Anonymous FTP access 🌸
  • Complete implementation of current RFCs πŸ‘‘
  • ASCII or binary type file transfers
  • Active or passive mode file transfers
  • Run standalone or from inetd(8)
  • Security features: chroot, resource limits, tainting, protection against buffer overflows πŸ›‘οΈ
  • IP-based and/or IP-less virtual hosts
  • Complete access control system
  • Anonymous read-only FTP personality
  • Virtual filesystem allows files to be served from a database
  • Directory aliases and CDPATH support
  • Extensible command set
  • Generate archives on the fly ✨

πŸ¦‹ INSTALLING AND RUNNING THE SERVER πŸ¦‹

so a standard ftpd.conf file ships with the distribution. the full tea on every single option you can put in that file lives right here in this manual page β€” go look at the CONFIGURATION section below. iykyk.

once you do make install, that standard ftpd.conf file should have landed in /etc/ftpd.conf. bestie, you will probably need to edit it to match ur local setup. no shortcuts.

also once make install has run, a bunch of start-up scripts get installed in /usr/sbin/*ftpd.pl. (on Debian they go in /usr/bin or /usr/local/bin.) each start-up script boots the server in a different configuration β€” either as a full FTP server, or as an anonymous-only read-only FTP server, etc. pick ur fighter.

the commonly used scripts are:

  • /usr/sbin/ftpd.pl
  • /usr/sbin/ro-ftpd.pl

the first script is for the full FTP server.

these scripts assume the perl interpreter can be found on the current $PATH. in the rare situation when that's not the case, you may need to edit these scripts. πŸ™ˆ

🧚 STANDALONE SERVER 🧚

if you have a high load site, you're gonna want to run Net::FTPServer as a standalone server. to start Net::FTPServer as a standalone server, do:

/usr/sbin/ftpd.pl -S

you may want to add this to your local start-up files so the server starts automatically when you boot the machine. future you says thank you.

to stop the server, do:

killall ftpd.pl
(Note: Azazel points out that the above is a Linux-ism. Solaris administrators may get a nasty shock if they type killall as root! Just kill the parent ftpd.pl process by hand instead).

🌈 RUNNING FROM INETD 🌈

add the following line to /etc/inetd.conf:

ftp stream tcp nowait root /usr/sbin/tcpd ftpd.pl
(This assumes that you have the tcp-wrappers package installed to provide basic access control through /etc/hosts.allow and /etc/hosts.deny. This access control is in addition to any access control which you may configure through /etc/ftpd.conf.)

after editing this file you will need to inform inetd:

killall -HUP inetd

⭐ RUNNING FROM XINETD ⭐

xinetd is a modern alternative to inetd which is supposedly simpler to configure. in practice, however, it has proven to be quite difficult to configure services under xinetd (mainly because xinetd gives no diagnostic information when things go wrong 😭). the following configuration has worked for me:

create the file /etc/xinetd.d/net-ftpserver containing:

# default: on
# description: Net::FTPServer, a secure, \
#              extensible, configurable FTP server.
#
service ftp
{
       socket_type             = stream
       wait                    = no
       user                    = root
       server                  = /usr/sbin/ftpd.pl
       log_on_success          += DURATION USERID
       log_on_failure          += USERID
       disable                 = no
}

check any other possible FTP server configurations to ensure they are all disabled (ie. disable = yes in all other files).

restart xinetd using:

/etc/init.d/xinetd restart

πŸ’« COMMAND LINE FLAGS πŸ’«

  • --help β€” Display help and exit
  • -d, -v β€” Enable debugging
  • -p PORT β€” Listen on port PORT instead of the default port
  • -s β€” Run in daemon mode (default: run from inetd)
  • -S β€” Run in background and in daemon mode
  • -V β€” Show version information and exit
  • -C CONF β€” Use CONF as configuration file (default: /etc/ftpd.conf)
  • -P PIDFILE β€” Save pid into PIDFILE (daemon mode only)
  • -o option=value β€” Override config file option with value
  • --test β€” Test mode (used only in automatic testing scripts)

πŸŽ€ CONFIGURING AND EXTENDING THE SERVER πŸŽ€

so Net::FTPServer can be configured and extended in a number of different ways. let's count 'em:

firstly, almost all common server configuration can be carried out by editing the configuration file /etc/ftpd.conf. easy.

secondly, commands can be loaded into the server at run-time to provide custom extensions to the common FTP command set. these custom commands are written in Perl. slay.

thirdly, one of several different supplied personalities can be chosen. personalities can be used to make deep changes to the FTP server: for example, there is a supplied personality which allows the FTP server to serve files from a relational database. by subclassing Net::FTPServer, Net::FTPServer::DirHandle and Net::FTPServer::FileHandle you may also write your own personalities. iconic.

the next sections talk about each of these possibilities in turn.

πŸ’œ CONFIGURATION πŸ’œ

a standard /etc/ftpd.conf file is supplied with Net::FTPServer in the distribution. the possible configuration options are listed in full below.

simple configuration options can also be given on the command line using the -o option. command line configuration options override those from the configuration file. yes, command line wins. periodt.

  • <Include filename> β€” Use the <Include filename> directive to include the contents of filename directly at the current point within the configuration file.
You cannot use <Include> within a <Host> section, or at least you can but it won't work the way you expect. 😬
  • <IncludeWildcard wildcard> β€” Include all files matching wildcard at this point in the file. The files are included in alphabetical order.
You cannot use <IncludeWildcard> within a <Host> section, or at least you can but it won't work the way you expect. 😬
  • debug β€” Run with debugging. Equivalent to the command line -d option.
    • Default: 0
    • Example: debug: 1
  • port β€” The TCP port number on which the FTP server listens when running in daemon mode (see daemon mode option below).
    • Default: The standard ftp/tcp service port from /etc/services
    • Example: port: 8021
  • daemon mode β€” Run as a daemon. If set, the FTP server will open a listening socket on its default port number, accept new connections and fork off a new process to handle each connection. If not set (the default), the FTP server will handle a single connection on stdin/stdout, which is suitable for use from inetd.
    • The equivalent command line options are -s and -S.
    • Default: 0
    • Example: daemon mode: 1
  • run in background β€” Run in the background. If set, the FTP server will fork into the background before running.
    • The equivalent command line option is -S.
    • Default: 0
    • Example: run in background: 1
  • error log β€” If set, then all warning and error messages are appended to this file. If not set, warning and error messages get sent to STDERR and to syslog.
    • Having an error log is highly recommended. πŸ‘‘
    • Default: (not set, warnings and errors go to syslog)
    • Example: error log: /var/log/ftpd.errors
  • rotate log files β€” If set, and if the log file names contain a % directive, then the server will check if a new log file is needed whenever the system accepts a new connection. This implements a log rotation feature for long-running servers.
    • If not set, then any % directive will be evaluated only when the log files gets created.
    • Default: (not set, log file name evaluated only once)
    • Example: rotate log files: 1
  • maintainer email β€” Maintainer's email address.
    • Default: root@hostname
    • Example: maintainer email: bob@example.com
  • class β€” Assign users into classes. One or more class directives can be added to the configuration file to aggregate individual users into larger groups of users called classes.

By default all anonymous users are in class anonymous and every other user is in class users.

The configuration file can contain zero or more class directives. The format of the class directive is either:

class: CLASSNAME USERNAME[,USERNAME[,...]]

or:

class: CLASSNAME { perl code ... }

Examples of the first form are:

class: staff rich
class: students ann,mary,pete

User rich will be placed into class staff, and users ann, mary and pete will be placed into class students.

Examples of the second form are:

class: family { /jones$/ }
class: friends { $_ ne "jeff" }

Any username ending in jones (eg. rjones, timjones) will be in class family. Any other user except jeff will be placed in class friends. Note that the Perl code must be surrounded by {...} and must return a boolean true or false value. The username is available as $_. The Perl code is arbitrary: it might, for example, use an external file or database lookup in order to work out if a user belongs to a class.

class directives are evaluated in the order in which they appear in the configuration file until one matches the username.

  • Default: Anonymous users are assigned to class anonymous and everyone else is assigned to class users.
  • timeout β€” Timeout on control connection. If a command has not been received after this many seconds, the server drops the connection. You may set this to zero to disable timeouts completely (although this is not recommended). ⚠️
    • Default: 900 (seconds)
    • Example: timeout: 600
  • limit memory, limit nr processes, limit nr files β€” Resource limits. These limits are applied to each child process and are important in avoiding denial of service (DoS) attacks against the FTP server. πŸ›‘οΈ

the resource limit vibe check πŸŽ€

okay so here's the tea on the three resource limits, in their natural habitat:

  • limit memory β€” default 65536, unit KBytes, aka "Amount of memory per child"
  • limit nr processes β€” default 10, unit (none), aka "Number of processes"
  • limit nr files β€” default 20, unit (none), aka "Number of open files"

want the server to just... not limit something? set that limit to -1 and let it breathe. ✨

Example:

limit memory:      131072
limit nr processes:    20
limit nr files:        40

limit nr processes:    -1

max clients πŸ‘‘

a hard cap on how many clients can be connected at the same exact time. if this limit ever gets hit, new clients get closed immediately β€” like, the server won't even ask them to login. rude but efficient πŸ’…. heads up though: this feature works in daemon mode only.

Default: 255

Example: "max clients: 600"

max clients message πŸ’¬

the message we show when max clients has been reached.

you can drop these % escape sequences in there and they'll get swapped for internal variables:

  • %x β€” the max clients setting that has been reached
  • %E β€” maintainer email address (from the maintainer email setting above)
  • %G β€” time in GMT
  • %R β€” remote hostname or IP address if resolve addresses is not set
  • %L β€” local hostname
  • %T β€” local time
  • %% β€” just an ordinary %

Default: Maximum connections reached

Example: "max clients message: Only %x simultaneous connections allowed. Please try again later."

resolve addresses πŸ”

resolve addresses, bestie. if set, we attempt a reverse lookup on client addresses for logging purposes. but fr fr, if you turn this on then some clients may experience long delays when they try to connect. not recommended on high load servers.

Default: 0

Example: "resolve addresses: 1"

require resolved addresses πŸ™…β€β™€οΈ

require resolved addresses. if set, client addresses must validly resolve β€” otherwise clients just won't be able to connect. and ngl, if you set this then some clients won't be able to connect even though it is probably the fault of their ISP. 😭

Default: 0

Example: "require resolved addresses: 1"

change process name 🏷️

change process name. if set (which is the default, btw) then the FTP server will change its process name to reflect the IP address or hostname of the client. if not set, the FTP server will not try to change its process name.

Default: 1

Example: "change process name: 0"

greeting type πŸ‘‹

greeting type. the greeting gets printed before the user has logged in. possible greeting types are:

  • full β€” Full greeting, including hostname and version number.
  • brief β€” Hostname only.
  • terse β€” Nothing
  • text β€” Display greeting from greeting text option.

just so you know, the SITE VERSION command can also reveal the version number. you may need to turn this off by setting "allow site version command: 0" below.

Default: full

Example: "greeting type: text"

greeting text πŸ“

greeting text. if the greeting type is set to text then this contains the text to display.

Default: none

Example: "greeting text: Hello. I'll be your server today."

welcome type 🫢

welcome type. the welcome gets printed after a user has logged in. possible welcome types are:

  • normal β€” Normal welcome message: Welcome <<username>>.
  • text β€” Take the welcome message from welcome text option.
  • file β€” Take the welcome message from welcome file file.

Default: normal

Example: "welcome type: text"

welcome text πŸ’Œ

if welcome type is set to text, then this contains the text to be printed after a user has logged in.

you can use these % escape sequences inside the welcome text and they'll substitute for internal variables:

  • %E β€” maintainer's email address (from the maintainer email setting above)
  • %G β€” time in GMT
  • %R β€” remote hostname or IP address if resolve addresses is not set
  • %L β€” local hostname
  • %m β€” user's home directory (see home directory below)
  • %T β€” local time
  • %U β€” username given when logging in
  • %u β€” currently a synonym for %U, but in future will be determined from RFC931 authentication, like wu-ftpd
  • %% β€” just an ordinary %

Default: none

Example: "welcome text: Welcome to this FTP server."

welcome file πŸ“‚

if welcome type is set to file, then this contains the file to be printed after a user has logged in.

you may use any of the % escape sequences defined in welcome text above.

Default: none

Example: "welcome file: /etc/motd"

home directory 🏠

home directory. this is the home directory where we put the user once they have logged in. this only applies to non-anonymous logins, okay? anonymous logins are always placed in "/", which is at the root of their chrooted environment.

you may use an absolute path here, or else one of these special forms:

  • %m β€” Use home directory from password file or from NSS.
  • %U β€” Username.
  • %% β€” A single % character.

for example, to force a user to start in "~/anon-ftp" when they log in, set this to "%m/anon-ftp".

note that setting the home directory does not perform a chroot. use the root directory setting below to jail users into a particular directory.

home directories are relative to the current root directory.

in the anonymous read-only (ro-ftpd) personality, set home directory to "/" or else you will get a warning whenever a user logs in. πŸ™ˆ

Default: %m

Examples:

home directory: %m/anon-ftp
home directory: /

root directory πŸ”’

root directory. immediately after logging in, perform a chroot into the named directory. this only applies to non-anonymous logins, and furthermore it only applies if you have a non-database VFS installed. database VFSes typically cannot perform chroot (or, to be more accurate, they have a different concept of chroot β€” typically assigning each user their own completely separate namespace).

you may use %m and %U as above.

for example, to jail a user under "~/anon-ftp" after login, do:

home directory: /
root directory: %m/anon-ftp

notice that the home directory is relative to the current root directory. πŸ‘‘

Default: (none)

Example: "root directory: %m/anon-ftp"

time zone πŸ•°οΈ

time zone to be used for MDTM and LIST stat information.

Default: GMT

Examples:

time zone: Etc/GMT+3
time zone: Europe/London
time zone: US/Mountain

local address 🌐

local addresses. if you wish the FTP server (in daemon mode) to only bind to a particular local interface, then give its address here.

Default: none

Example: "local address: 127.0.0.1"

allow anonymous πŸ•΅οΈβ€β™€οΈ

allow anonymous access. if set, then allow anonymous access through the "ftp" and "anonymous" accounts.

Default: 0

Example: "allow anonymous: 1"

anonymous password check / anonymous password enforce βœ…

validate email addresses. normally when logging in anonymously, you are asked to enter your email address as a password. these options can be used to check and enforce email addresses in this field (to some extent, at least β€” you obviously can't force someone to enter a true email address, bestie).

the anonymous password check option may be set to "rfc822", "no browser", "trivial" or "none". if set to "rfc822" then the user must enter a valid RFC 822 email address as password. if set to "no browser" then a valid RFC 822 email address must be entered, and various common browser email addresses like "mozilla@" and "IEverUser@" are refused. if set to "trivial" then we just check that the address contains an @ char. if set to "none", then we do no checking. the default is "none".

if the anonymous password enforce option is set and the password fails the check above, then the user will not be allowed to log in. the default is 0 (unset).

these options only have effect when "allow anonymous" is set.

Example:

anonymous password check: rfc822
anonymous password enforce: 1

allow proxy ftp 🎭

allow proxy FTP. if this is set, then the FTP server can be told to actively connect to addresses and ports on any machine in the world. this is not such a great idea, but required if you follow the RFC very closely. if not set (the default), the FTP server will only connect back to the client machine.

Default: 0

Example: "allow proxy ftp: 1"

allow connect low port ⚠️

allow the FTP server to connect back to ports < 1024. this is rarely useful and could pose a serious security hole in some circumstances.

Default: 0

Example: "allow connect low port: 1"

passive port range 🎚️

what range of local ports will the FTP server listen on in passive mode? choose a range here like "1024-5999,49152-65535". the special value 0 means that the FTP server will use a kernel-assigned ephemeral port.

Default: 49152-65535

Example: "passive port range: 0"

ftp data port πŸ”₯

which source port to use for active (non-passive) mode when connecting to the client for PORT mode transfers. the special value 0 means that the FTP server will use a kernel-assigned ephemeral port. to strictly follow RFC, this should be set to "ftp-data(20)". this may be required for certain brain-damaged firewall configurations. however, for security reasons, the default setting is intentionally set to 0 to utilize a kernel-assigned ephemeral port. use this directive at your own risk!

SECURITY PRECAUTIONS:

  1. unfortunately, to use a port < 1024 requires super-user privileges. thus, low ports will not work unless the FTP server is invoked as super-user. this also implies that all processes handling the client connections must also remain super-user throughout the entire session. it is highly discouraged to use a low port.

http://cr.yp.to/ftp/security.html (See "Connection laundering" section)

  1. there sometimes exists a danger of needing to connect to the same remote host:port. using the same IP/port on both sides will cause connect() to fail if the old socket is still being broken down. this condition will not occur if using an ephemeral port.

  1. Lots of hackers use source port 20 to sneak past naive firewalls, lowkey. Using an ephemeral port (which is the default) can help discourage that dangerous naivety. πŸ’€
man nmap

(peep the -g option)

Default: 0

Example: "ftp data port: ftp-data"

  • max login attempts β€” how many login tries we let you flop before we hang up the connection and drop a warning in the logs. Wu-ftpd defaults this to 5, but we're built different.

Default: 3

Example: "max login attempts: 5"

  • pam authentication β€” use PAM for auth. Required on systems like Red Hat Linux and Solaris that use PAM instead of the normal /etc/passwd mechanisms. Heads up bestie: you'll need the Authen::PAM Perl module installed for this to work, no cap.

Default: 0

Example: "pam authentication: 1"

  • pam application name β€” if PAM auth is enabled, this is the PAM application name. The default is ftp, which is the same name wu-ftpd chooses. FreeBSD girlies will want to use ftpd here.

Default: ftp

Example: "pam application name: ftpd"

  • password file β€” only in the Full personality ✨. This lets you specify a password file used for authentication. If you enable this option, normal PAM or /etc/passwd gets bypassed and this password file is used instead.

Each line in the password file has this format:

username:crypted_password:unix_user[:root_directory]

Comments and blank lines are ignored. 🫢

For example, a line with:

guest:ab01FAX.bQRSU:rich:/home/rich/guest-uploads

would let someone log in as guest with password 123456. After logging in, the FTP server assumes the identity of the real Unix user rich, and chroots itself into the /home/rich/guest-uploads directory.

(Note that because ordinary PAM/passwd is bypassed, it would no longer be possible for a user to log in directly with the username rich.)

Crypted passwords can be generated with this command:

perl -e 'print crypt ("123456", "ab"), "\n"'

Replace 123456 with the actual password, and replace ab with two random letters from the set [a-zA-Z0-9./]. (Those two random letters are the so-called salt and they make dictionary attacks against the password file more difficult β€” see crypt(3).)

The user's home directory comes from the real Unix password file (or nsswitch-configured source) for the real Unix user. You cannot use password files to override this, so if you're using the optional root_directory parameter, it would make sense to add home directory: / into your configuration file.

Anonymous logins are not affected by the password file option. Use the allow anonymous flag to control whether anonymous logins are permitted in the Full back-end.

Password files are not the height of security, but they're included because they can sometimes be useful. In particular, if the password file can be read by untrusted users, then it's likely those same users can run the crack program and eventually find out your passwords. 😭 Some small additional security is offered by having the password file readable only by root (mode 0600). In future we may offer MD5 or salted SHA-1 hashed passwords to make this harder.

A curious artifact of the implementation allows you to list the same user with multiple different passwords. Any of the passwords is then valid for logins (and you could even have the user map to different real Unix users in different chrooted directories!) πŸ‘‘

Default: (none)

Example: "password file: /etc/ftpd.passwd"

  • pidfile β€” location of the file to store the process ID (PID). Applies only to the deamonized process, not the child processes.

Default: (no pidfile created)

Example: "pidfile: /var/run/ftpd.pid"

  • client logging β€” location to store all client commands sent to the server. The format is the date, the pid, and the command. Following the pid is a - if not authenticated, and the username if the connection is authenticated. Example of before and after authentication:
[Wed Feb 21 18:41:32 2001][23818:-]USER rob
[Wed Feb 21 18:41:33 2001][23818:-]PASS 123456
[Wed Feb 21 18:41:33 2001][23818:*]SYST

Default: (no logging)

Examples:

client logging: /var/log/ftpd.log
client logging: /tmp/ftpd_log.$hostname
  • xfer logging β€” location of transfer log. The format was taken from wu-ftpd and ProFTPD xferlog. (See also man xferlog)

Default: (no logging)

Examples:

xfer logging: /var/log/xferlog
xfer logging: /tmp/xferlog.$hostname
  • hide passwords in client log β€” if set to 1, then password (PASS) commands will not be logged in the client log. This option has no effect unless client logging is enabled.

Default: 0 (PASS lines will be shown)

Example: "hide passwords in client log: 1"

  • enable syslog β€” enable syslogging. If set, Net::FTPServer sends much information to syslog. On many systems this info shows up in /var/log/messages or /var/adm/messages. If clear, syslogging is disabled.

Default: 1

Example: "enable syslog: 0"

  • ident timeout β€” timeout for ident authentication lookups. A timeout (in seconds) must be specified in order to enable ident lookups. There is no way to specify an infinite timeout. Use 0 to disable this feature.

Default: 0

Example: "ident timeout: 10"

  • access control rule
  • user access control rule
  • retrieve rule
  • store rule
  • delete rule
  • list rule
  • mkdir rule
  • rename rule
  • chdir rule β€” access control rules, the whole squad πŸ’….

Access control rules are all specified as short snippets of Perl script. This gives you maximum configurability β€” you can express just about any rule you want β€” but at the price of learning a little Perl.

You can use these variables from the Perl:

$hostname      Resolved hostname of the client [1]
$ip            IP address of the client
$user          User name [2]
$class         Class of user [2]
$user_is_anonymous  True if the user is an anonymous user [2]
$pathname      Full pathname of the file being affected [2]
$filename      Filename of the file being affected [2,3]
$dirname       Directory name containing file being affected [2]
$type          'A' for ASCII, 'B' for binary, 'L8' for local 8-bit
$form          Always 'N'
$mode          Always 'S'
$stru          Always 'F'

Notes:

  1. May be undefined, particularly if resolve addresses is not set.
  2. Not available in access control rule since the user has not logged in at this point.
  3. Not available for list directory rule.

Access control rule. The FTP server will not accept any connections from a site unless this rule succeeds. Note that only $hostname and $ip are available to this rule, and unless resolve addresses and require resolved addresses are both set, $hostname may be undefined.

Default: 1

Examples:

(a) Deny connections from *.badguys.com:

access control rule: defined ($hostname) && \
                     $hostname !~ /\.badguys\.com$/

(b) Only allow connections from local network 10.0.0.0/24:

access control rule: $ip =~ /^10\./

User access control rule. After the user logs in successfully, this rule is then called to determine if the user may be permitted access.

Default: 1

Examples:

(a) Only allow ``rich'' to log in from 10.x.x.x network:

user access control rule: $user ne "rich" || \
                          $ip =~ /^10\./

(b) Only allow anonymous users to log in if they come from hosts with resolving hostnames (``resolve addresses'' must also be set):

user access control rule: !$user_is_anonymous || \
                          defined ($hostname)

(c) Do not allow user ``jeff'' to log in at all:

user access control rule: $user ne "jeff"

Retrieve rule. This rule controls who may retrieve (download) files.

Default: 1

Examples:

(a) Do not allow anyone to retrieve /etc/*'' or any file anywhere called .htaccess'':

retrieve rule: $dirname !~ m(^/etc/) && $filename ne ".htaccess"

(b) Only allow anonymous users to retrieve files from under the ``/pub'' directory.

retrieve rule: !$user_is_anonymous || $dirname =~ m(^/pub/)

Store rule. This rule controls who may store (upload) files.

In the anonymous read-only (ro-ftpd) personality, it is not possible to upload files anyway, so setting this rule has no effect.

Default: 1

Examples:

(a) Only allow users to upload files to the ``/incoming'' directory.

store rule: $dirname =~ m(^/incoming/)

(b) Anonymous users can only upload files to ``/incoming'' directory.

store rule: !$user_is_anonymous || $dirname =~ m(^/incoming/)

(c) Disable file upload.

store rule: 0

Delete rule. This rule controls who may delete files or rmdir directories.

In the anonymous read-only (ro-ftpd) personality, it is not possible to delete files anyway, so setting this rule has no effect.

Default: 1

Example: "delete rule: 0"

List rule. This rule controls who may list out the contents of a directory.

Default: 1

Example: "list rule: $dirname =~ m(^/pub/)"

Mkdir rule. This rule controls who may create a subdirectory.

In the anonymous read-only (ro-ftpd) personality, it is not possible to create directories anyway, so setting this rule has no effect.

Default: 1

Example: "mkdir rule: 0"

Rename rule. This rule controls which files or directories can be renamed.

Default: 1

Example: "rename rule: $pathname !~ m(/.htaccess$)"

Chdir rule. This rule controls which directories are acceptable to a CWD or CDUP.

Example: "chdir rule: $pathname !~ m/private/"

πŸŽ€ chdir message file

The change-directory message file, bestie. If you set this, then the first time per session that a user wanders into a directory that has a file with this name inside it, that file gets shown to them. It's giving ✨welcome wagon✨.

That file can hold any of these % escape sequences:

  • %C β€” current working directory
  • %E β€” maintainer's email address (pulled from the ``maintainer email'' setting above)
  • %G β€” time in GMT
  • %R β€” remote hostname, or IP address if ``resolve addresses'' is not set
  • %L β€” local hostname
  • %m β€” user's home directory (see ``home directory'' below)
  • %T β€” local time
  • %U β€” username given when logging in
  • %u β€” currently a synonym for %U, but in future will be determined from RFC931 authentication, like wu-ftpd
  • %% β€” just an ordinary %

Default: (none)

Example: "chdir message file: .message"


πŸ”„ allow rename to overwrite

Should the rename (RNFR/RNTO) command be allowed to overwrite files? Say it with me: permission slip. If it's unset, then we try to test whether the rename command would overwrite a file and disallow it. But heads up β€” there are some race conditions with this test. 😬

Default: 1

Example: "allow rename to overwrite: 0"


πŸ“₯ allow store to overwrite

Same energy, different command. Should the store commands (STOR/STOU/APPE) be allowed to overwrite files? If it's unset, then we try to test whether the store command would overwrite a file and disallow it. And again β€” there are some race conditions with this test. No cap.

Default: 1

Example: "allow store to overwrite: 0"


πŸͺž alias

Define an alias "name" for directory "dir". Like, the command "alias: mirror /pub/mirror" would let the user reach the /pub/mirror directory directly just by typing "cd mirror". So convenient, honestly.

Aliases only apply to the cd (CWD) command. The "cd foo" command checks for directories in this order:

  1. foo in the current directory
  2. an alias called foo
  3. foo in each directory in the cdpath (see ``cdpath'' command below)

You may list as many aliases as you want. πŸ’…

Alias names cannot contain slashes (/).

Even though alias dirs may start without a slash (/), this is unwise, and it's better that they always start with a slash (/) char.

General format: "alias: name dir"


🧭 cdpath

Define a search path that gets used when changing directories. So the command "cdpath: /pub/mirror /pub/sites" would let the user reach the /pub/mirror/ftp.cpan.org directory directly just by typing "cd ftp.cpan.org". Iconic behaviour, truly.

The "cd foo" command checks for directories in this order:

  1. foo in the current directory
  2. an alias called foo (see ``alias'' command above)
  3. foo in each directory in the cdpath

General format: "cdpath: dir1 [dir2 [dir3 ...]]"


🏷️ allow site version command

The SITE VERSION command. If it's set, then SITE VERSION reveals the current Net::FTPServer version string. If it's unset, then the command is disabled. Simple as.

Default: 1

Example: "allow site version command: 0"


πŸ’€ allow site exec command

The SITE EXEC command. If it's set, then SITE EXEC allows arbitrary commands to be executed on the server as the current user. If it's unset, then this command is disabled.

The default is disabled for obvious security reasons. Like, OBVIOUS obvious. 🚨

If you do allow SITE EXEC, you may need to increase the per process memory, processes and files limits above.

Default: 0

Example: "allow site exec command: 1"


πŸ—œοΈ enable archive mode

Archive mode! If it's set (which is the default), then archive mode is enabled, letting users request, say, "file.gz" and get a version of "file" that's gzip-compressed on the fly. If it's zero, then this feature is disabled. See the section ARCHIVE MODE elsewhere in this manual for the details.

Since archive mode is implemented using external commands, you need to make sure that programs like "gzip", "compress", "bzip2", "uuencode", etc. are available on the $PATH (even inside the chrooted environment), and you also need to substantially increase the normal per-process memory, processes and files limits. It's a whole thing. 🫢

Default: 1

Example: "enable archive mode: 0"


πŸ“¦ archive zip temporaries

Temporary directory for generating ZIP files in archive mode. So here's the tea: in archive mode, when generating ZIP files, the FTP server can either create a temporary file on local disk containing the ZIP contents, or it can generate the file completely in memory. The first method saves memory. The second method (only practical on small ZIP files) lets the server work more securely and in certain read-only chrooted environments.

(Unfortunately the ZIP file format itself prevents ZIP files from being easily created on the fly. Rude of it, honestly. 😭)

If it's not specified in the configuration file, this option defaults to using "/tmp". If there are local users on the FTP server box, then this can lead to various "tmp" races, so for maximum security you will probably want to change this.

If specified, and set to a string, then the string is the name of a directory used for storing temporary zip files. This directory must be writable, and must exist inside the chrooted environment (if chroot is being used).

If specified, but set to "0" or an empty string, then the server will always generate the ZIP file in memory.

In any case, if the directory is found at runtime to be unwritable, then the server falls back to creating ZIP files in memory. Resilient queen. πŸ‘‘

Default: "/tmp"

Example: "archive zip temporaries: "

Example: "archive zip temporaries: /var/ziptmp"


πŸ› οΈ site command

Custom SITE commands! Use this command to define them. Please read the section LOADING CUSTOMIZED SITE COMMANDS in this manual page for more detailed information.

The "site command" command has the form:

"site command: cmdname file"

cmdname is the name of the command (eg. for SITE README you would set cmdname == "readme"). file is a file containing the code of the site command in the form of an anonymous Perl subroutine. The file should have the form:

sub {
  my $self = shift;            # The FTPServer object.
  my $cmd = shift;             # Contains the command itself.
  my $rest = shift;            # Contains any parameters passed by the user.

     :     :
     :     :

  $self->reply (RESPONSE_CODE, RESPONSE_TEXT);
}

You may define as many site commands as you want. You may also override site commands from the current personality here. Flexible much? πŸ’–

Example:

site command: quota /usr/local/lib/ftp/quota.pl

and the file "/usr/local/lib/ftp/quota.pl" contains:

sub {
  my $self = shift;            # The FTPServer object.
  my $cmd = shift;             # Contains "QUOTA".
  my $rest = shift;            # Contains parameters passed by user.

  # ... Some code to compute the user's quota ...

  $self->reply (200, "Your quota is $quota MB.");
}

The client types "SITE QUOTA" and the server responds with:

"200 Your quota is 12.5 MB.".

🏠 <Host hostname> ... </Host>

<Host hostname> ... </Host> encloses commands that are applicable only to a particular host. "hostname" may be either a fully-qualified domain name (for IP-less virtual hosts) or an IP address (for IP-based virtual hosts). You should read the section VIRTUAL HOSTS in this manual page for more info on the different types of virtual hosts and how to set it up in more detail.

Also note: unless you have set "enable virtual hosts: 1", all <Host> sections will be ignored. Just straight up ghosted. πŸ™ˆ


🌐 enable virtual hosts

Unless this option is uncommented, virtual hosting is disabled and the <Host> sections in the configuration file have no effect whatsoever.

Default: 0

Example: "enable virtual hosts: 1"


πŸ¦‹ virtual host multiplex

IP-less virtual hosts. If you want to enable IP-less virtual hosts, then you must set up your DNS so that all hosts map to a single IP address, and place that IP address here. This is roughly equivalent to the Apache "NameVirtualHost" option.

IP-less virtual hosting is an experimental feature which requires changes to clients. πŸ§ͺ

Default: (none)

Example: "virtual host multiplex: 1.2.3.4"

Example <Host> section. Allow the dangerous SITE EXEC command on local connections. (Note that this is still dangerous).

<Host localhost.localdomain>
  ip: 127.0.0.1
  allow site exec command: 1
</Host>

Example <Host> section. This shows you how to do IP-based virtual hosts. I assume that you have set up your DNS so that "ftp.bob.example.com" maps to IP 1.2.3.4 and "ftp.jane.example.com" maps to IP 1.2.3.5, and you have set up suitable IP aliasing in the kernel.

You do not need the "ip:" command if you have configured reverse DNS correctly AND you trust your local DNS servers.

<Host ftp.bob.example.com>
  ip: 1.2.3.4
  root directory: /home/bob
  home directory: /
  user access control rule: $user eq "bob"
  maintainer email: bob@bob.example.com
</Host>

<Host ftp.jane.example.com>
  ip: 1.2.3.5
  root directory: /home/jane
  home directory: /
  allow anonymous: 1
  user access control rule: $user_is_anonymous
  maintainer email: jane@jane.example.com
</Host>

These rules set up two virtual hosts called "ftp.bob.example.com" and "ftp.jane.example.com". The former is located under bob's home directory and only he is allowed to log in. The latter is located under jane's home directory and only allows anonymous access. Two little ecosystems, living in harmony. 🌸

Example <Host> section. This shows you how to do IP-less virtual hosts. Note that IP-less virtual hosts are a highly experimental feature, and require the client to support the HOST command.

You need to set up your DNS so that both "ftp.bob.example.com" and "ftp.jane.example.com" point to your own IP address.

virtual host multiplex: 1.2.3.4

<Host ftp.bob.example.com>
  root directory: /home/bob
  home directory: /
  user access control rule: $user eq "bob"
</Host>

<Host ftp.jane.example.com>
  root directory: /home/jane
  home directory: /
  allow anonymous: 1
  user access control rule: $user_is_anonymous
</Host>
  • log socket type β€” this is the socket type the server uses to slide into syslog's DMs πŸ’¬. Literally just the argument you'd hand to the Sys::Syslog::setlogsock function.

Default: unix

Example: log socket type: inet

  • listen queue β€” how long the listen queue gets to be when you're running in daemon mode 🎧.

Default: 10

Example: listen queue: 20

  • tcp window β€” sets the TCP window, bestie. See RFC 2415 Simulation Studies of Increased Initial TCP Window Size. Heads up: this one only touches the data socket. And ngl, you're probably never gonna need to change this from the system-specific default, and honestly you probably shouldn't πŸ™ˆ.

Default: (system-specific TCP window size)

Example: tcp window: 4380

  • tcp keepalive β€” sets TCP keepalive.

Default: (system-specific keepalive setting)

Example: tcp keepalive: 1

  • command filter β€” the command bouncer πŸšͺ✨. If you set this, every single command gets checked against this regular expression before it runs. Command doesn't match the filter? The command connection gets dropped immediately, no explanation, no nothing. This is basically the AllowFilter command in ProFTPD. Do not forget to wrap your filter in ^...$ β€” that's on you, not on us πŸ’….

Default: (no filter)

Example: command filter: ^[A-Za-z0-9 /]+$

  • restrict command β€” advanced command filtering, the VIP section πŸŽ€. The restrict command directive takes this form:
restrict command: "COMMAND" perl code ...

So when the user tries to run COMMAND, that perl code gets evaluated first. If it comes back true, the command is allowed to go ahead. If not, the server throws an error back at the user and just… doesn't run the command. Simple as.

Important detail: COMMAND here is the FTP protocol command, which is not necessarily the same thing as whatever the user typed into their FTP client. Go read RFC 959 for the more common FTP protocol commands, fr fr.

The Perl code gets the exact same variables as access control rules do (eg. $user, $class, $ip, etc.). And do not let your code touch the global $_ variable (which holds the complete command) β€” that's a hard no 🚫.

Default: all commands are allowed by default

Examples:

Only let users in the class nukers delete files and directories:

restrict command: "DELE" $class eq "nukers"
restrict command: "RMD" $class eq "nukers"

Only let staff use the SITE WHO command:

restrict command: "SITE WHO" $class eq "staff"

Only let rich run the SITE EXEC command:

allow site exec command: 1
restrict command: "SITE EXEC" $user eq "rich"
  • command wait β€” go slow, bestie 🐌. If you set this, the server sleeps for this many seconds before it even starts processing each command. Ngl this would be so much more useful if you could apply it to only particular classes of connection, but you can't. That's just how it is.

Default: (no wait)

Example: command wait: 5

  • no authentication commands β€” okay so the list of commands a client is allowed to issue before they've authenticated is genuinely tiny. Obviously USER and PASS are on it (otherwise a user could never log in, bestie 😭), plus QUIT, LANG, HOST and FEAT. HELP is also permitted (although that's dubious, lowkey). Any other command that's not on this list gets a 530 Not logged in. error, periodt.

This list really ought to contain at least USER, PASS and QUIT β€” otherwise the server just won't be very functional.

And some commands cannot be added here. Eg. adding CWD or RETR to this list is likely to make the FTP server crash, or else let users read files that are only supposed to be available to root πŸ‘‘πŸ’€. So use this with great care.

Default: USER PASS QUIT LANG HOST FEAT HELP

Example: no authentication commands: USER PASS QUIT

  • <Perl> ... </Perl> β€” use the <Perl> directive to write Perl code directly into your configuration file, no cap ✨. Here's a simple example:
<Perl>
use Sys::Hostname;
$config{'maintainer email'} = "root\@" . hostname ();
$config{port} = 8000 + 21;
$config{debug} = $ENV{FTP_DEBUG} ? 1 : 0;
</Perl>

As you can see in the example, to set a configuration option called foo, you simply assign to the variable $config{foo}. That's it, that's the whole thing 🫢.

All normal Perl functionality is available to you, including using require if you need to run an external Perl script.

The <Perl> and </Perl> directives must each appear on a single line on their own β€” one per line, no roomies.

Wanna assign multiple configuration options with the same name? Use an array ref:

<Perl>
my @aliases = ( "foo /pub/foo",
                "bar /pub/bar",
                "baz /pub/baz" );
$config{alias} = \@aliases;
</Perl>

You cannot use a <Perl> section inside a <Host> section. Instead you have to simulate it by assigning to the %host_config variable like this:

<Perl>
$host_config{'localhost.localdomain'}{ip} = "127.0.0.1";
$host_config{'localhost.localdomain'}{'allow site exec command'}= 1;
</Perl>

The above is equivalent to this ordinary <Host> section:

<Host localhost.localdomain>
  ip: 127.0.0.1
  allow site exec command: 1
</Host>

You may also assign to the $self variable to set variables directly in the Net::FTPServer object itself. This is pretty hairy, and hence not recommended β€” but you dig your own hole if you want, bestie πŸ•³οΈ. Here's a contrived example:

<Perl>
$self->{version_string} = "my FTP server/1.0";
</Perl>

A cleaner, but more complex way to do this would be to use a personality.

The <Perl> directive is potentially quite powerful πŸ”₯. Here's a good idea that Rob Brown had:

<Perl>
my %H;
dbmopen (%H, "/etc/ftpd.db", 0644);
%config = %H;
dbmclose (%H);
</Perl>

Notice how this lets you crunch a possibly very large configuration file down into a hash, for very rapid loading at run time. Iconic.

Another useful way to use <Perl> is to set environment variables (particularly $PATH).

<Perl>
$ENV{PATH} = "/usr/local/bin:$ENV{PATH}"
</Perl>

Here's yet another wonderful way to use <Perl>. Look in /usr/local/lib/ftp/ for a list of site commands and load each one:

<Perl>

my @files = glob "/usr/local/lib/ftp/*.pl";
my @site_commands;

foreach (@files)
 {
   push @site_commands, "$1 $_" if /([a-z]+)\.pl/;
 }

$config{'site command'} = \@site_commands;

</Perl>

To force a particular version of Net::FTPServer to be used, include the following code in your configuration file:

<Perl>
die "requires Net::FTPServer version >= 1.025"
  unless $Net::FTPServer::VERSION !~ /\..*\./ &&
         $Net::FTPServer::VERSION >= 1.025;
</Perl>

🌸 LOADING CUSTOMIZED SITE COMMANDS 🌸

Writing custom SITE commands is so simple, bestie πŸ’–. These commands become available to users when they type SITE XYZ in a command line FTP client, or when they define a custom SITE command in their graphical FTP client.

SITE commands are unregulated by RFCs. You may define any commands, give them any names, and give them any function you wish. However, over time various standard SITE commands have been recognized and implemented in many FTP servers, and Net::FTPServer implements those too. They are:

  • SITE VERSION β€” Display the server software version.
  • SITE EXEC β€” Execute a shell command on the server (in Net::FTPServer this is disabled by default! 😱)
  • SITE ALIAS β€” Display chdir aliases.
  • SITE CDPATH β€” Display chdir paths.
  • SITE CHECKMETHOD β€” Implement checksums.
  • SITE CHECKSUM
  • SITE IDLE β€” Get or set the idle timeout.
  • SITE SYNC β€” Synchronize hard disks.

The following commands are found in wu-ftpd, but are not currently implemented by Net::FTPServer: SITE CHMOD, SITE GPASS, SITE GROUP, SITE GROUPS, SITE INDEX, SITE MINFO, SITE NEWER, SITE UMASK.

So when you're picking a name for a SITE command, it's probably best not to pick one of the names above β€” unless you're specifically implementing or overriding that command, iykyk πŸ’….

Custom SITE commands have to be written in Perl. But honestly there is very little you need to understand to write these commands β€” you only need a basic knowledge of Perl scripting. You got this 🫢.

As our first example, we will implement a SITE README command. This command just prints out some standard information.

Firstly, create a file called /usr/local/lib/site_readme.pl (you may choose a different path if you want). The file should contain:

sub {
  my $self = shift;
  my $cmd = shift;
  my $rest = shift;

  $self->reply (200,
                "This is the README file for mysite.example.com.",
                "Mirrors are contained in /pub/mirrors directory.",
                "       :       :       :       :       :",
                "End of the README file.");
}

Edit /etc/ftpd.conf and add the following command:

site command: readme /usr/local/lib/site_readme.pl

and restart the FTP server (check your system log [/var/log/messages] for any syntax errors or other problems). Here is an example of a user running the SITE README command:

ftp> quote help site
214-The following commands are recognized:
214-    ALIAS   CHECKMETHOD     EXEC    README
214-    CDPATH  CHECKSUM        IDLE    VERSION
214 You can also use HELP to list general commands.
ftp> site readme
200-This is the README file for mysite.example.com.
200-Mirrors are contained in /pub/mirrors directory.
200-       :       :       :       :       :
200 End of the README file.

Our second example demonstrates how to use parameters (the $rest argument). This is the SITE ECHO command.

sub {
  my $self = shift;
  my $cmd = shift;
  my $rest = shift;

  # Split the parameters up.
  my @params = split /\s+/, $rest;

  # Quote each parameter.
  my $reply = join ", ", map { "'$_'" } @params;

  $self->reply (200, "You said: $reply");
}

Here is the SITE ECHO command in use:

ftp> quote help site
214-The following commands are recognized:
214-    ALIAS   CHECKMETHOD     ECHO    IDLE
214-    CDPATH  CHECKSUM        EXEC    VERSION
214 You can also use HELP to list general commands.
ftp> site echo hello how are you?
200 You said: 'hello', 'how', 'are', 'you?'

Okay bestie, example number three is where it gets real 🌢️ β€” we're playing with the virtual filesystem (the VFS), and that's the whole personality of this server, no cap. Our new command is SITE SHOW, and what it does is print a text file straight into the FTP conversation instead of making the user download it and open it locally. So like, SITE SHOW readme.txt should dump the contents of the readme.txt file that lives in the current directory β€” if it exists, obviously πŸ™ˆ.

The golden rule here, and I mean golden: every single file access has to go through the VFS, never by poking at the disk directly. Follow that and your commands stay secure and they'll just work across all the different back-end personalities β€” especially the ones where the "files" are actually BLOBs sitting in a relational database. It's giving portability πŸ’….
sub {
  my $self = shift;
  my $cmd = shift;
  my $rest = shift;

  # Get the file handle.
  my ($dirh, $fileh, $filename) = $self->_get ($rest);

  # File doesn't exist or not accessible. Return an error.
  unless ($fileh)
    {
      $self->reply (550, "File or directory not found.");
      return;
    }

  # Check it's a simple file.
  my ($mode) = $fileh->status;

  unless ($mode eq "f")
    {
      $self->reply (550,
                    "SITE SHOW command is only supported on plain files.");
      return;
    }

  # Try to open the file.
  my $file = $fileh->open ("r");

  unless ($file)
    {
      $self->reply (550, "File or directory not found.");
      return;
    }

  # Copy data into memory.
  my @lines = ();

  while (defined ($_ = $file->getline))
    {
      # Remove any native line endings.
      s/[\n\r]+$//;

      push @lines, $_;
    }

  # Close the file handle.
  unless ($file->close)
    {
      $self->reply (550, "Close failed: ".$self->system_error_hook());
      return;
    }

  # Send the file back to the user.
  $self->reply (200, "File $filename:", @lines, "End of file.");
}

Now bestie, real talk β€” this code is not quite complete πŸ‘€. A better implementation would also check the retrieve rule, so that nobody could use SITE SHOW to sneak around the access control limits the server admin put in place (the audacity fr fr πŸ’€). It would also look at the file more closely to confirm it's actually a text file, and it would refuse to list very large files.

Here's an (abbreviated) example of a user hitting the SITE SHOW command:

ftp> site show README
200-File README:
200-README
200-======
200-
200-Biblio@Tech Net::FTPServer - A full-featured, secure, extensible
[...]
200-Copyright (C) 2000-2003 Richard Jones <rich@annexia.org> and other contributors.
200 End of file.

🌈 STANDARD PERSONALITIES 🌈

Right now Net::FTPServer ships with three standard personalities, and they are:

  • Full β€” the complete read/write anonymous/authenticated FTP server that serves files out of a standard Unix filesystem. The whole package ✨
  • RO β€” a small read-only, anonymous-only FTP server, functionally similar to Dan Bernstein's publicfile program.
  • DBeg1 β€” an example FTP server that serves files to a PostgreSQL database. It supports files and hierarchical directories, multiple users (but not file permissions) and file upload.

The standard Full personality is not gonna be explained here, periodt.

The RO personality is literally the Full personality with all the code related to writing files, creating directories, deleting, etc. removed 🚫. It also only permits anonymous logins and contains no code at all for ordinary authentication. Which means it's safe to use RO when all you care about is serving files to anonymous users and you don't wanna lie awake worrying that a cracker will figure out a way to trick the FTP server into writing over a file. Peace of mind, honestly 🫢.

The DBeg1 personality is a complete read/write FTP server that stores files as BLOBs (Binary Large OBjects) in a PostgreSQL relational database. It supports file download and upload, and it contains code to authenticate users against a users table in the database (so database "users" are completely unrelated to real Unix users, iykyk). Heads up though: DBeg1 is intended only as an example. It does not support advanced features like file permissions and quotas. As part of the schoolmaster.net project, Bibliotech Ltd. developed an even more advanced database personality supporting users, groups, access control lists, quotas, recursive moves and copies and many other features β€” but that database personality is not available as source 😭.

To use the DBeg1 personality you first need a PostgreSQL server running (version 6.4 or above), and you need to be sure you can reach it from your local user account. Use the initdb, createdb and createuser commands to create the appropriate user account and database. (Please go read the PostgreSQL administrators manual for more info on that β€” I do not answer questions about basic PostgreSQL knowledge, sorry bestie, that's a you-and-the-manual situation πŸ’….)

Here's my correctly set-up PostgreSQL server, accessed from my local user account rich:

cruiser:~$ psql
Welcome to the POSTGRESQL interactive sql monitor:
  Please read the file COPYRIGHT for copyright terms of POSTGRESQL

   type \? for help on slash commands
   type \q to quit
   type \g or terminate with semicolon to execute query
 You are currently connected to the database: rich

rich=> \d
Couldn't find any tables, sequences or indices!

You'll also need these Perl modules installed: DBI, DBD::Pg 🐘.

Now you need to create a database called ftp and populate it with data. Here's how:

createdb ftp
psql ftp < doc/eg1.sql

Check that PostgreSQL reports no ERRORs. Vibe check your output, basically ✨.

At this point you should be able to start the FTP server by running this command β€” and not as root, okay? πŸ‘‘

./dbeg1-ftpd -S -p 2000 -C ftpd.conf

If the FTP server doesn't start correctly, go check the system log file [/var/log/messages].

Connect to the FTP server like this:

ftp localhost 2000

Log in as either rich/123456 or dan/123456, then go wild β€” move around, upload and download files, create and delete directories, etc. Main character energy fr πŸ¦‹.


πŸ’œ SUBCLASSING THE Net::FTPServer CLASSES πŸ’œ

By subclassing Net::FTPServer, Net::FTPServer::DirHandle and/or Net::FTPServer::FileHandle, you can create custom personalities for the FTP server. Slay.

Typically, by overriding the hooks in the Net::FTPServer class you can change the basic behaviour of the FTP server β€” turning it into an anonymous read-only server, for example.

By overriding the hooks in Net::FTPServer::DirHandle and Net::FTPServer::FileHandle you can create virtual filesystems: serving files into and out of a database, for example.

This current manual page contains information about the hooks in Net::FTPServer which may be overridden.

See Net::FTPServer::DirHandle(3) for info about the methods in Net::FTPServer::DirHandle which may be overridden.

See Net::FTPServer::FileHandle(3) for info about the methods in Net::FTPServer::FileHandle which may be overridden.

The most reasonable way to build your own personality is to extend one of the existing ones β€” pick whichever one most closely matches the personality you're trying to create. Say, for example, you want to make another database personality. A good place to start would be copying lib/Net/FTPServer/DBeg1/*.pm into a new directory lib/Net/FTPServer/MyDB/ (for example). Now edit those files and substitute MyDB for DBeg1. Then go through each subroutine in those files and modify them, consulting the appropriate manual page whenever you need to. Iconic little workflow, ngl 🌸.


🧚 VIRTUAL HOSTS 🧚

Net:FTPServer is capable of hosting multiple FTP sites on a single machine. Because of the nature of the FTP protocol, virtual hosting is almost always done by allocating a single separate IP address per FTP site. However, Net::FTPServer also supports an experimental IP-less virtual hosting system β€” although that one requires modifications to the client.

Normal (IP-based) virtual hosting is carried out like this:

  • For each FTP site, allocate a separate IP address.
  • Configure IP aliasing on your normal interface so that the single physical interface responds to multiple virtual IP addresses.
  • Add entries (A records) in DNS mapping each site's name to a separate IP address.
  • Add reverse entries (PTR records) in DNS mapping each IP address back to the site hostname. It is important that both forward and reverse DNS is set up correctly, else virtual hosting may not work.
  • In /etc/ftpd.conf you will need to add a virtual host section for each site like this:
<Host sitename>

  ip: 1.2.3.4
  ... any specific configuration options for this site ...

</Host>

You don't in fact need the ip: part assuming that your forward and reverse DNS are set up correctly.

  • If you want to specify a lot of external sites, or generate the configuration file automatically from a database or a script, you may find the <Include filename> syntax useful.

There are examples in /etc/ftpd.conf. Here's how IP-based virtual hosting works:

  • The server starts by listening on all interfaces.
  • A connection arrives at one of the IP addresses and a process is forked off.
  • The child process finds out which interface the client connected to and reverses the name.
  • If:
    • the IP address matches one of the ip: declarations in any of the Host sections,

or:

  • there is a reversal for the name, and the name matches one of the Host sections in the configuration file,

then:

  • configuration options are read from that section of the file and override any global configuration options specified elsewhere in the file.
  • Otherwise, the global configuration options only are used.

IP-less virtual hosting is an experimental feature πŸ™ˆ. It requires the client to send a HOST command very early on in the command stream β€” before USER and PASS. The HOST command explicitly gives the hostname that the FTP client is attempting to connect to, which is what lets many FTP sites be multiplexed onto a single IP address. At the present time, I am not aware of any FTP clients which implement the HOST command, although they will undoubtedly become more common in future.

Here's how to set up IP-less virtual hosting:

  • Add entries (A or CNAME records) in DNS mapping the name of each site to a single IP address.
  • In /etc/ftpd.conf you will need to list the same single IP address to which all your sites map:
  • virtual host multiplex: 1.2.3.4
  • In /etc/ftpd.conf you're gonna add a virtual host section for each site, like this:
<Host sitename>

  ... any specific configuration options for this site ...

</Host>

✨ So here's how IP-less virtual hosting works ✨

  • The server starts by listening on one interface.
  • A connection arrives at the IP address and a process gets forked off. 🍴
  • The IP address matches virtual host multiplex, so like… no IP-based virtual host processing happens at all.
  • One of the very first commands the client sends is HOST followed by the hostname of the site.
  • If there's a matching Host section in the config file, then the config options get read from that section and they override any global config options specified elsewhere in the file. πŸ‘‘
  • If there's no matching Host section, then the global config options alone are used.

The client is not permitted to issue the HOST command more than once, and is not permitted to issue it after login. Periodt. 🚫


πŸ”₯ VIRTUAL HOSTING AND SECURITY πŸ”₯

Only certain config options are available inside the <Host> sections of the config file. Generally speaking, the only ones you can put there are the ones that take effect after the site name has been determined β€” so like, allow anonymous is fine (it's an option that's parsed after determining the site name and during log in), but port is a hard no (it gets parsed long before any clients ever connect). πŸ’…

Make sure your default global configuration is secure. If you're using IP-less virtual hosting this is particularly important, because if the client never sends a HOST command, the client just… gets the global configuration. And even with IP-based virtual hosting it may be possible for clients to sometimes get the global configuration β€” for example if your local name server fails. 😭

IP-based virtual hosting always takes precedence above IP-less virtual hosting. No cap.

With IP-less virtual hosting, access control cannot be performed on a per-site basis. Why? Because the client has to issue commands (ie. the HOST command at least) before the site name is even known to the server. So there's just no way. However, you may still have a global access control rule. 🫢


🌈 ARCHIVE MODE 🌈

Beginning with version 1.100, Net::FTPServer is able to generate certain types of compressed and archived files on the fly. In practice what this means is: if a user requests, say, file.gz and this file doesn't actually exist (but file does exist), then the server will dynamically generate a gzip-compressed version of file for the user. ✨

This also works on directories, so a user might request dir.tar.gz which doesn't exist (but directory dir does exist), and the server tars up and compresses the entire contents of dir and presents that right back to the user. It's giving magic, honestly. πŸͺ„

Archive mode is enabled by default. However, it will not work unless you substantially increase the per-process memory, processes and files limits. The reason for this is that archive mode works by forking external programs such as gzip to perform the compression. For the same reason you may also need to ensure that at least gzip, compress, bzip2 and uuencode programs are available on the current $PATH β€” particularly if you're using a chrooted environment. πŸ™ˆ

To disable archive mode, put enable archive mode: 0 into the config file.

The following file extensions are supported:

  • .gz β€” GZip compressed. Requires gzip program on PATH.
  • .Z β€” Unix compressed. Requires compress program on PATH.
  • .bz2 β€” BZip2 compressed. Requires bzip2 program on PATH.
  • .uue β€” UU-encoded. Requires uuencode program on PATH.
  • .tar β€” Tar archive. Requires Perl Archive::Tar module.
  • .zip β€” DOS ZIP archive. Requires Perl Archive::Zip module.
  • .list β€” Return a list of all the files in this directory.

File extensions may be combined. Hence .tar.gz, .tar.bz2 and even .tar.gz.uue will all work as you expect. Obsessed. πŸ’–

Archive mode is, of course, extensible. It is particularly simple to add another compression / filter format. In your personality (or in a <Perl> section in the config file) you need to add another key to the archive_filters hash. Like so:

$ftps->{archive_filters}{".foo"} = &_foo_filter;

The value of this key should be a function as defined below:

\%filter = _foo_filter ($ftps, $sock);

The filter should return a hash reference (undef if it fails). The hash should contain the following keys:

  • sock β€” Newly opened socket.
  • pid β€” PID of filter program.

The _foo_filter function takes the existing socket and filters it, providing a new socket which the FTP server will write to (for the data connection back to the client). If your filter is a Unix program, then the simplest thing is just to define _foo_filter as:

sub _foo_filter
{
  return $_[0]->archive_filter_external ($_[1], "foo" [, args ...]);
}

The archive_filter_external function takes care of the tricky bits for you. 🫑

Adding new generators (akin to the existing tar and ZIP) is more tricky. I suggest you look closely at the code and consult the author for more information. πŸ‘‘

✨ METHODS ✨

Okay bestie, this is the meat of it β€” every method and every hook your personality can lean on. No skipping, we're doing all of them πŸ’…

Net::FTPServer->run ([\@ARGV]);

This is the main entry point into the FTP server. It starts the FTP server running. This function never normally returns. Like, it's the main character and it does not do exits. πŸ‘‘

If no arguments are given, then command line arguments are taken from the global @ARGV array.

$regex = $ftps->wildcard_to_regex ($wildcard)

This is a general library function shared between many of the back-end database personalities. It converts a general wildcard (eg. *.c) into a regular expression (eg. ^.*\.c$ ).

Thanks to: Terrence Monroe Brannon <terrence.brannon@oracle.com>.

$regex = $ftps->wildcard_to_sql_like ($wildcard)

Another general library function shared between many of the back-end database personalities. It converts a general wildcard (eg. *.c) into the strange wildcardish format used by the SQL LIKE operator (eg. %.c).

$ftps->reply ($code, $line, [$line, ...])

This function sends a standard single line or multi-line FTP server reply to the client. The $code should be one of the standard reply codes listed in RFC 959. The one or more $line arguments are the (free text) of the reply. Do not include carriage returns at the end of each $line. This function adds the correct line ending format as specified in the RFC. ✨

$ftps->log ($level, $message, ...);

This function is identical to the normal syslog function to be found in Sys::Syslog. However, it only uses syslog if the enable syslog configuration option is set to true.

Use this function instead of calling syslog directly. Fr fr. 🫢

$ftps->config ($name);

Read configuration option $name from the configuration file.

$ftps->ip_host_config ($ip_addr);

Look for a <Host> section which contains ip: $ip_addr. If one is found, return the site name of the Host section. Otherwise return undef.

$filter = $ftps->archive_filter_external ($sock, $cmd [, $args]);

Apply $cmd as a filter to socket $sock. Returns a hash reference which contains the following keys:

sock      Newly opened socket.
pid       PID of filter program.

If it fails, returns undef.

See section ARCHIVE MODE elsewhere in this manual for more information.

$ftps->visit ($dirh, \%functions);

The visit function recursively "visits" every file and directory contained in $dirh (which must be a directory handle).

\%functions is a reference to a hash of file types to functions. For example:

'f' => \&visit_file,
'd' => \&visit_directory,
'l' => \&visit_symlink,
&c.

When a file of the known type is encountered, the appropriate function is called with $_ set to the file handle. (All functions are optional: if visit encounters a file with a type not listed in the %functions hash, then that file is just ignored).

The return value from functions is ignored, except for the return value from the directory ('d') function. The directory function should return 1 to indicate that visit should recurse into that directory. If the directory function returns 0, then visit will skip that directory.

visit will call the directory function once for $dirh.

$sock = $self->open_data_connection;

Open a data connection. Returns the socket (an instance of IO::Socket) or undef if it fails for some reason.


πŸŽ€ HOOKS πŸŽ€

The hook list. This is where your personality gets to be a lil extra, and honestly? We love that for you. πŸ’–

$self->pre_configuration_hook ();

Hook: Called before command line arguments and configuration file are read.

Status: optional.

Notes: You may append your own information to $self->{version_string} from this hook.

$self->options_hook (\@args);

Hook: Called before command line arguments are parsed.

Status: optional.

Notes: You can use this hook to supply your own command line arguments. If you parse any arguments, you should remove them from the @args array.

$self->post_configuration_hook ();

Hook: Called after all command line arguments and configuration file have been read and parsed.

Status: optional.

$self->post_bind_hook ();

Hook: Called only in daemon mode after the control port is bound but before starting the accept infinite loop block.

Status: optional.

$self->pre_accept_hook ();

Hook: Called in daemon mode only just before accept(2) is called in the parent FTP server process.

Status: optional.

$self->post_accept_hook ();

Hook: Called both in daemon mode and in inetd mode just after the connection has been accepted. This is called in the child process.

Status: optional.

$rv = $self->access_control_hook;

Hook: Called after accept(2)-ing the connection to perform access control. Detailed request information is contained in the $self object. If the function returns -1 then the socket is immediately closed and no FTP processing happens on it. If the function returns 0, then normal access control is performed on the socket before FTP processing starts. If the function returns 1, then normal access control is not performed on the socket and FTP processing begins immediately.

Status: optional.

$rv = $self->process_limits_hook;

Hook: Called after accept(2)-ing the connection to perform per-process limits (eg. by using the setrlimit(2) system call). Access control has already been performed and detailed request information is contained in the $self object.

If the function returns -1 then the socket is immediately closed and no FTP processing happens on it. If the function returns 0, then normal per-process limits are applied before any FTP processing starts. If the function returns 1, then normal per-process limits are not performed and FTP processing begins immediately.

Status: optional.

$rv = $self->authentication_hook ($user, $pass, $user_is_anon)

Hook: Called to perform authentication. If the authentication succeeds, this should return 0 (or any positive integer >= 0). If the authentication fails, this should return -1.

Status: required. Not optional bestie, you actually need this one. 😭

$self->user_login_hook ($user, $user_is_anon)

Hook: Called just after user $user has successfully logged in. A good place to change uid and chroot if necessary.

Status: optional.

$dirh = $self->root_directory_hook;

Hook: Return an instance of a subclass of Net::FTPServer::DirHandle corresponding to the root directory.

Status: required. πŸ‘‘

$self->pre_command_hook;

Hook: This hook is called just before the server begins to wait for the client to issue the next command over the control connection.

Status: optional.

$rv = $self->command_filter_hook ($cmdline);

Hook: This hook is called immediately after the client issues command $cmdline, but before any checking or processing is performed on the command. If this function returns -1, then the server immediately goes back to waiting for the next command. If this function returns 0, then normal command filtering is carried out and the command is processed. If this function returns 1 then normal command filtering is not performed and the command processing begins immediately.

Important Note: This hook must be careful not to overwrite the global $_ variable. πŸ’€ Do not do it.

Do not use this function to add your own commands. Instead use the $self->{command_table} and $self->{site_command_table} hashes.

Status: optional.

$error = $self->transfer_hook ($mode, $file, $sock, \$buffer);

$mode     -  Open mode on the File object (Either reading or writing)
$file     -  File object as returned from DirHandle::open
$sock     -  Data IO::Socket object used for transfering
\$buffer  -  Reference to current buffer about to be written

The \$buffer is passed by reference to minimize the stack overhead for efficiency purposes only. It is not meant to be modified by the transfer_hook subroutine. (It can cause corruption if the length of $buffer is modified.)

Hook: This hook is called after reading $buffer and before writing $buffer to its destination. If arg1 is "r", $buffer was read from the File object and written to the Data socket. If arg1 is "w", $buffer will be written to the File object because it was read from the Data Socket. The return value is the error for not being able to perform the write. Return undef to avoid aborting the transfer process.

Status: optional.

$self->post_command_hook ($cmd, $rest)

Hook: This hook is called after all command processing has been carried out on this command. $cmd is the command, and $rest is the remainder of the command line.

Status: optional.

$self->system_error_hook

Hook: This hook is used instead of $! when what looks like a system error occurs during a virtual filesystem handle method. It can be used by the virtual filesystem to provide explanatory text for a virtual filesystem failure which did not actually set the real $!.

Status: optional.

$self->quit_hook

Hook: This hook is called after the user has QUIT or if the FTP client cleanly drops the connection. Please note, however, that this hook is not called whenever the FTP server exits, particularly in cases such as:

  • The FTP server, the Perl interpreter or the personality crashes unexpectedly.
  • The user fails to log in.
  • The FTP server detects a fatal error, sends a 421 error code, and abruptly exits.
  • Idle timeouts.
  • Access control violations.
  • Manual server shutdowns.

Unfortunately it is not in general easily possible to catch these cases and cleanly call a hook. If your personality needs to do cleanup in all cases, then it is probably better to use an END block inside your Server object (see perlmod(3)). Even using an END block cannot catch cases where the Perl interpreter crashes. 😭

Status: optional.

πŸ› BUGS (aka the tea on what's still lowkey broken) πŸ’€

okay bestie, gather round, because this is the part where we're honest about what's still a little bit of a mess. no gatekeeping here. 🫢

  • The SIZE, REST and RETR commands probably do not work correctly in ASCII mode. πŸ’…
  • REST does not work before STOR/STOU/APPE (is it supposed to?) πŸ‘€
  • User upload/download limits. (not a thing yet, sorry)
  • Limit number of clients by host or IP address. (also not a thing yet)
  • The following commands are recognized by "wu-ftpd", but are not yet implemented by "Net::FTPServer":
    • SITE CHMOD β€” There is a problem supporting this with our VFS.
    • SITE GPASS β€” Group functions are not really relevant for us.
    • SITE GROUP β€” -"- ditto -"-
    • SITE GROUPS β€” -"- ditto -"-
    • SITE INDEX β€” This is a synonym for SITE EXEC.
    • SITE MINFO β€” This command is no longer supported by wu-ftpd.
    • SITE NEWER β€” This command is no longer supported by wu-ftpd.
    • SITE UMASK β€” This command is difficult to support with VFS.
  • Symbolic links are not handled elegantly (or indeed at all) yet. 😭
  • Equivalent of ProFTPD's ``DisplayReadme'' function.
  • The ability to hide dot files (probably best to build this into the VFS layer). This should apply across all commands. See ProFTPD's ``IgnoreHidden'' function.
  • Access to LDAP authentication database (can currently be done using a PAM module). In general, we should support pluggable authentication. πŸ”
  • Log formatting similar to ProFTPD command LogFormat.
  • More timeouts to avoid various denial of service attacks. For example, the server should always timeout when waiting too long for an active data connection. ⏰
  • Support for IPv6 (see RFC 2428), EPRT, EPSV commands.
  • See also "XXX" comments in the code for other problems, missing features and bugs.
fr fr, if you go grepping for XXX in the source you're gonna find a whole mood board of stuff still waiting on its glow-up. πŸ’–

πŸ“¦ DEPENDENCY

IO::Dir, IO::stringy

like, that's the whole vibe. those two. that's it. πŸŽ€


πŸ‘‘ AUTHORS

Richard Jones (rich@annexia.org), Rob Brown (bbb@cpan.org), Keith Turner (keitht at silvaco.com), Azazel (azazel at azazel.net), and many others.

shoutout to the "many others" too, y'all ate. πŸ”₯


🫑 MAINTAINER

Ryo Okamoto "<ryo at aquahill dot net>"


🌈 REPOSITORY

https://github.com/ryochin/p5-net-ftpserver

Copyright (C) 2000 Biblio@Tech Ltd., Unit 2-3, 50 Carnwath Road, London, SW6 3EG, UK.

Copyright (C) 2000-2003 Richard Jones (rich@annexia.org) and other contributors.

This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

⚠️ bestie, note the energy here: no warranty, like, at all. "WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE" β€” that's the legal way of saying if it breaks, that's on you. πŸ’€

πŸ‘€ SEE ALSO

Net::FTPServer::Handle(3), Net::FTPServer::FileHandle(3), Net::FTPServer::DirHandle(3), Net::FTP(3), perl(1), RFC 765, RFC 959, RFC 1579, RFC 2389, RFC 2428, RFC 2577, RFC 2640, Extensions to FTP Internet Draft draft-ietf-ftpext-mlst-NN.txt. Net::FTPServer::XferLog Test::FTP::Server


and that's a wrap on the whole page, bestie!! πŸ’–βœ¨ this lil manual is brought to you by perl v5.42.0, dated 2025-07-25 β€” go forth, run your FTP server, and slay. πŸ¦‹πŸ’…πŸŽ€

πŸ“¦ where this page comes from