My favorites | Sign in
Logo
                
Search
for
Updated Sep 13, 2009 by billiej...@gmail.com
Labels: SortOrder-1, Featured
Tutorial  
Tutorial containing API reference and example usages

Table of contents

1.0 - Introduction

pyftpdlib implements the server side of the FTP protocol as defined in RFC-959. pyftpdlib consist of a single file, ftpserver.py, which contains a hierarchy of classes, functions and variables which implement the backend functionality for the ftpd.
This document is intended to serve as a simple API reference of most important classes and functions. Also included is an introduction to customization through the use of some example scripts. Some of them are included in demo directory of pyftpdlib source distribution.

If you have written a customized configuration you think could be useful to the community feel free to share it by adding a comment at the end of this document.

2.0 - API reference

function ftpserver.log(msg)

Log messages intended for the end user.

function ftpserver.logline(msg)
Log commands and responses passing through the command channel.

function ftpserver.logerror(msg)
Log traceback outputs occurring in case of errors.

class ftpserver.AuthorizerError()
Base class for authorizers exceptions.

class ftpserver.DummyAuthorizer()

Basic "dummy" authorizer class, suitable for subclassing to create your own custom authorizers. An "authorizer" is a class handling authentications and permissions of the FTP server. It is used inside FTPHandler class for verifying user's password, getting users home directory, checking user permissions when a filesystem read/write event occurs and changing user before accessing the filesystem. DummyAuthorizer is the base authorizer, providing a platform independent interface for managing "virtual" FTP users. Typically the first thing you have to do is create an instance of this class and start adding ftp users:
>>> from pyftpdlib import ftpserver
>>> authorizer = ftpserver.DummyAuthorizer()
>>> authorizer.add_user('user', 'password', '/home/user', perm='elradfmw')
>>> authorizer.add_anonymous('/home/nobody')
  • add_user(username, password, homedir[, perm="elr"[, msg_login="Login successful."[, msg_quit="Goodbye."]]])
    Add a user to the virtual users table. AuthorizerError exceptions raised on error conditions such as insufficient permissions or duplicate usernames. Optional perm argument is a set of letters referencing the user's permissions. Every letter is used to indicate that the access rights the current FTP user has over the following specific actions are granted. The available permissions are the following listed below:
Read permissions:
  • "e" = change directory (CWD command)
  • "l" = list files (LIST, NLST, STAT, MLSD, MLST commands)
  • "r" = retrieve file from the server (RETR command)
Write permissions
  • "a" = append data to an existing file (APPE command)
  • "d" = delete file or directory (DELE, RMD commands)
  • "f" = rename file or directory (RNFR, RNTO commands)
  • "m" = create directory (MKD command)
  • "w" = store a file to the server (STOR, STOU commands)
Optional msg_login and msg_quit arguments can be specified to provide customized response strings when user log-in and quit. The perm argument of the add_user() method refers to user's permissions. Every letter is used to indicate that the access rights the current FTP user has over the following specific actions are granted.
  • add_anonymous(homedir[, **kwargs])
    Add an anonymous user to the virtual users table. AuthorizerError exception raised on error conditions such as insufficient permissions, missing home directory, or duplicate anonymous users. The keyword arguments in kwargs are the same expected by add_user() method: perm, msg_login and msg_quit. The optional perm keyword argument is a string defaulting to "elr" referencing "read-only" anonymous user's permission. Using a "write" value results in a RuntimeWarning.
  • override_perm(directory, perm[, recursive=False])
    Override permissions for a given directory. New in 0.5.0
  • validate_authentication(username, password)
    Return True if the supplied username and password match the stored credentials.
  • impersonate_user(username, password)
    Impersonate another user (noop). It is always called before accessing the filesystem. By default it does nothing. The subclass overriding this method is expected to provide a mechanism to change the current user. New in 0.4.0
  • terminate_impersonation(username, password)
    Terminate impersonation (noop). It is always called after having accessed the filesystem. By default it does nothing. The subclass overriding this method is expected to provide a mechanism to switch back to the original user. New in 0.4.0
  • remove_user(username)
    Remove a user from the virtual user table.

class ftpserver.FTPHandler(conn, server)

This class implements the FTP server Protocol Interpreter (see RFC-959), handling commands received from the client on the control channel by calling the command's corresponding method (e.g. for received command "MKD pathname", ftp_MKD() method is called with pathname as the argument). All relevant session information are stored in instance variables. conn is the underlying socket object instance of the newly established connection, server is the FTPServer class instance. Basic usage simply requires creating an instance of FTPHandler class and specify which authorizer instance it will going to use:
>>> ftp_handler = ftpserver.FTPHandler
>>> ftp_handler.authorizer = authorizer
All relevant session information is stored in class attributes reproduced below and can be modified before instantiating this class:
  • timeout
    The timeout which is the maximum time a remote client may spend between FTP commands. If the timeout triggers, the remote client will be kicked off (defaults to 300 seconds). New in 5.0
  • banner
    String sent when client connects (default "pyftpdlib %s ready." %__ver__).
  • max_login_attempts
    Maximum number of wrong authentications before disconnecting (default 3).
  • permit_foreign_addresses
    Whether enable FXP feature (default False).
  • permit_privileged_ports
    Set to True if you want to permit active connections (PORT) over privileged ports (not recommended, default False).
  • masquerade_address
    The "masqueraded" IP address to provide along PASV reply when pyftpdlib is running behind a NAT or other types of gateways. When configured pyftpdlib will hide its local address and instead use the public address of your NAT (default None).
  • passive_ports
    What ports ftpd will use for its passive data transfers. Value expected is a list of integers (e.g. range(60000, 65535)). When configured pyftpdlib will no longer use kernel-assigned random ports (default None).
  • on_file_sent(file)
    Called every time a file has been successfully sent (read the FAQ on how to run time consuming tasks). New in 0.5.1
  • on_file_received(file)
    Called every time a file has been successfully received (read the FAQ on how to run time consuming tasks). New in 0.5.1

class ftpserver.DTPHandler(sock_obj, cmd_channel)

This class handles the server-data-transfer-process (server-DTP, see RFC-959) managing all transfer operations regarding the data channel. sock_obj is the underlying socket object instance of the newly established connection, cmd_channel is the FTPHandler class instance.
  • timeout
    The timeout which roughly is the maximum time we permit data transfers to stall for with no progress. If the timeout triggers, the remote client will be kicked off. New in 5.0
  • ac_in_buffer_size
  • ac_out_buffer_size
    The buffer sizes to use when receiving and sending data (both defaulting to 65536 bytes). For LANs you may want this to be fairly large. Depending on available memory and number of connected clients setting them to a lower value can result in better performances.

class ftpserver.ThrottledDTPHandler(sock_obj, cmd_channel)

A DTPHandler subclass which wraps sending and receiving in a data counter and temporarily "sleeps" the channel so that you burst to no more than x Kb/sec average. Use it instead of DTPHandler to set transfer rates limits for both downloads and/or uploads (see the demo script showing the example usage). New in 0.5.2
  • read_limit
    The maximum number of bytes to read (receive) in one second (defaults to 0 == no limit)
  • write_limit
    The maximum number of bytes to write (send) in one second (defaults to 0 == no limit).

class ftpserver.FTPServer(address, handler)

This class is an asyncore.dispatcher subclass. It creates a FTP socket listening on address (a tuple containing the ip:port pair), dispatching the requests to a "handler" (typically FTPHandler class object). It is typically used for starting asyncore polling loop:
>>> address = ('127.0.0.1', 21)
>>> ftpd = ftpserver.FTPServer(address, ftp_handler)
>>> ftpd.serve_forever()
  • max_cons
    Number of maximum simultaneous connections accepted (default 0 == no limit).
  • max_cons_per_ip
    Number of maximum connections accepted for the same IP address (default 0 == no limit).
  • serve_forever([timeout=1[, use_poll=False[, map=None[, count=None]]])
    A wrap around asyncore.loop(); starts the asyncore polling loop including running the scheduler. The arguments are the same expected by original asyncore.loop() function.
  • close()
    Stop serving without disconnecting currently connected clients.
  • close_all([map=None[, ignore_all=False]])
    Stop serving disconnecting also the currently connected clients. The map parameter is a dictionary whose items are the channels to close. If map is omitted, the default asyncore.socket_map is used. Having ignore_all parameter set to False results in raising exception in case of unexpected errors.

class ftpserver.AbstractedFS()

A class used to interact with the file system, providing a cross-platform interface compatible with both Windows and UNIX style filesystems where all paths use "/" separator.
AbstractedFS distinguishes between "real" filesystem paths and "virtual" ftp paths emulating a UNIX chroot jail where the user can not escape its home directory (example: real "/home/user" path will be seen as "/" by the client).
It also provides some utility methods and wraps around all os.* calls involving operations against the filesystem like creating files or removing directories.
  • root
    User's home directory ("real").
  • cwd
    User's current working directory ("virtual").
  • ftpnorm(ftppath)
    Normalize a "virtual" ftp pathname depending on the current working directory (e.g. having "/foo" as current working directory "x" becomes "/foo/x"). New in 3.0
  • ftp2fs(ftppath)
    Translate a "virtual" ftp pathname into equivalent absolute "real" filesystem pathname (e.g. having "/home/user" as root directory "x" becomes "/home/user/x"). New in 3.0
  • fs2ftp(fspath)
    Translate a "real" filesystem pathname into equivalent absolute "virtual" ftp pathname depending on the user's root directory (e.g. having "/home/user" as root directory "/home/user/x" becomes "/x". New in 3.0
  • validpath(path)
    Check whether the path belongs to user's home directory. Expected argument is a "real" filesystem path. If path is a symbolic link it is resolved to check its real destination. Pathnames escaping from user's root directory are considered not valid (return False).

class ftpserver.CallLater(seconds, target [, *args [, **kwargs]])

Calls a function at a later time. It can be used to asynchronously schedule a call within the polling loop without blocking it. The instance returned is an object that can be used to cancel or reschedule the call. New in 0.5.0
  • cancelled
    Whether the call has been cancelled.
  • reset()
    Reschedule the call resetting the current countdown.
  • delay(seconds)
    Reschedule the call for a later time.
  • cancel()
    Unschedule the call.

3.0 - Customizing your FTP server

Below is a set of example scripts showing some of the possible customizations that can be done with pyftpdlib. Some of them are included in demo directory of pyftpdlib source distribution.

3.1 - Building a Base FTP server

The script below is a basic configuration, and it's probably the best starting point for understanding how things work. It uses the base DummyAuthorizer for adding a bunch of "virtual" users.

It also sets a limit for connections by overriding FTPServer.max_cons and FTPServer.max_cons_per_ip attributes which are intended to set limits for maximum connections to handle simultaneously and maximum connections from the same IP address. Overriding these variables is always a good idea (they default to 0, or "no limit") since they are a good workaround for avoiding DoS attacks.

download script

#!/usr/bin/env python
# basic_ftpd.py
   
"""A basic FTP server which uses a DummyAuthorizer for managing 'virtual
users', setting a limit for incoming connections.
"""
   
import os
   
from pyftpdlib import ftpserver
   
   
if __name__ == "__main__":
   
    # Instantiate a dummy authorizer for managing 'virtual' users
    authorizer = ftpserver.DummyAuthorizer()
   
    # Define a new user having full r/w permissions and a read-only
    # anonymous user
    authorizer.add_user('user', '12345', os.getcwd(), perm='elradfmw')
    authorizer.add_anonymous(os.getcwd())
   
    # Instantiate FTP handler class
    ftp_handler = ftpserver.FTPHandler
    ftp_handler.authorizer = authorizer
   
    # Define a customized banner (string returned when client connects)
    ftp_handler.banner = "pyftpdlib %s based ftpd ready." %ftpserver.__ver__
   
    # Specify a masquerade address and the range of ports to use for
    # passive connections.  Decomment in case you're behind a NAT.
    #ftp_handler.masquerade_address = '151.25.42.11'
    #ftp_handler.passive_ports = range(60000, 65535)
   
    # Instantiate FTP server class and listen to 0.0.0.0:21
    address = ('', 21)
    ftpd = ftpserver.FTPServer(address, ftp_handler)
   
    # set a limit for connections
    ftpd.max_cons = 256
    ftpd.max_cons_per_ip = 5
   
    # start ftp server
    ftpd.serve_forever()

3.2 - Logging management

As mentioned, ftpserver.py comes with 3 different functions intended for a separate logging system: log(), logline() and logerror(). Let's suppose you don't want to print FTPd messages on screen but you want to write them into different files: "/var/log/ftpd.log" will be main log file, "/var/log/ftpd.lines.log" the one where you'll want to store commands and responses passing through the control connection.

Here's one method this could be implemented:

#!/usr/bin/env python
# logging_management.py
   
import os
import time
   
from pyftpdlib import ftpserver
   
now = lambda: time.strftime("[%Y-%b-%d %H:%M:%S]")
   
def standard_logger(msg):
    f1.write("%s %s\n" %(now(), msg))
   
def line_logger(msg):
    f2.write("%s %s\n" %(now(), msg))
   
if __name__ == "__main__":
    f1 = open('ftpd.log', 'a')
    f2 = open('ftpd.lines.log', 'a')
    ftpserver.log = standard_logger
    ftpserver.logline = line_logger

    authorizer = ftpserver.DummyAuthorizer()
    authorizer.add_anonymous(os.getcwd())
    ftp_handler = ftpserver.FTPHandler
    ftp_handler.authorizer = authorizer
    address = ('', 21)
    ftpd = ftpserver.FTPServer(address, ftp_handler)
    ftpd.serve_forever()

3.3 - Storing passwords as hash digests

Using FTP server library with the default DummyAuthorizer means that password will be stored in clear-text. An end-user ftpd using the default dummy authorizer would typically require a configuration file for authenticating users and their passwords but storing clear-text passwords is of course undesirable.

The most common way to do things in such case would be first creating new users and then storing their usernames + passwords as hash digests into a file or wherever you find it convenient.

The example below shows how to easily create an encrypted account storage system by storing passwords as one-way hashes by using md5 algorithm. This could be easily done by using the hashlib module included with Python stdlib and by sub-classing the original DummyAuthorizer class overriding its validate_authentication() method.

download script

#!/usr/bin/env python
# md5_ftpd.py
   
"""A basic ftpd storing passwords as hash digests (platform independent).
"""
   
import os
try:
    from hashlib import md5
except ImportError:
    # backward compatibility with Python < 2.5
    from md5 import new as md5
   
from pyftpdlib import ftpserver
   
   
class DummyMD5Authorizer(ftpserver.DummyAuthorizer):
   
    def validate_authentication(self, username, password):
        hash = md5(password).hexdigest()
        return self.user_table[username]['pwd'] == hash
   
if __name__ == "__main__":
    # get a hash digest from a clear-text password
    hash = md5('12345').hexdigest()
    authorizer = DummyMD5Authorizer()
    authorizer.add_user('user', hash, os.getcwd(), perm='elradfmw')
    authorizer.add_anonymous(os.getcwd())
    ftp_handler = ftpserver.FTPHandler
    ftp_handler.authorizer = authorizer
    address = ('', 21)
    ftpd = ftpserver.FTPServer(address, ftp_handler)
    ftpd.serve_forever()

3.4 - Unix FTP Server

If you're running a Unix system you may want to configure your ftpd to include support for "real" users existing on the system.

The example below shows how to use pwd and spwd modules available since Python 2.5 to interact with UNIX user account and shadow passwords database and also to automatically get the user's home directory.

impersonate_user() and terminate_impersonation() methods of the dummy authorizer are overridden to provide the proper mechanism to reflect the current logged-in user every time he's going to access the filesystem.

Note that the users you're going to add through the add_user method must already exist on the system.

download script

#!/usr/bin/env python
# unix_ftpd.py
   
"""A ftpd using local unix account database to authenticate users
(users must already exist).
   
It also provides a mechanism to (temporarily) impersonate the system
users every time they are going to perform filesystem operations.
"""
   
import os
import pwd, spwd, crypt
   
from pyftpdlib import ftpserver
   
   
class UnixAuthorizer(ftpserver.DummyAuthorizer):
   
    # the uid/gid the daemon runs under
    PROCESS_UID = os.getuid()
    PROCESS_GID = os.getgid()
   
    def add_user(self, username, homedir=None, **kwargs):
        """Add a "real" system user to the virtual users table.
   
        If no home argument is specified the user's home directory will
        be used.
   
        The keyword arguments in kwargs are the same expected by the
        original add_user method: "perm", "msg_login" and "msg_quit".
        """
        # get the list of all available users on the system and check
        # if provided username exists
        users = [entry.pw_name for entry in pwd.getpwall()]
        if not username in users:
            raise ftpserver.AuthorizerError('No such user "%s".' %username)
        if not homedir:
            homedir = pwd.getpwnam(username).pw_dir
        ftpserver.DummyAuthorizer.add_user(self, username, '', homedir,**kwargs)
   
    def add_anonymous(self, homedir=None, realuser="nobody", **kwargs):
        """Add an anonymous user to the virtual users table.

        If no homedir argument is specified the realuser's home
        directory will possibly be determined and used.

        realuser argument specifies the system user to use for managing
        anonymous sessions.  On many UNIX systems "nobody" is tipically
        used but it may change (e.g. "ftp").
        """
        users = [entry.pw_name for entry in pwd.getpwall()]
        if not realuser in users:
            raise ftpserver.AuthorizerError('No such user "%s".' %realuser)
        if not homedir:
            homedir = pwd.getpwnam(realuser).pw_dir
        ftpserver.DummyAuthorizer.add_anonymous(self, homedir, **kwargs)
        self.anon_user = realuser
   
    def validate_authentication(self, username, password):
        if (username == "anonymous") and self.has_user('anonymous'):
            username = self.anon_user
   
        pw1 = spwd.getspnam(username).sp_pwd
        pw2 = crypt.crypt(password, pw1)
        return pw1 == pw2
   
    def impersonate_user(self, username, password):
        if (username == "anonymous") and self.has_user('anonymous'):
            username = self.anon_user
        uid = pwd.getpwnam(username).pw_uid
        gid = pwd.getpwnam(username).pw_gid
        os.setegid(gid)
        os.seteuid(uid)
   
    def terminate_impersonation(self):
        os.setegid(self.PROCESS_GID)
        os.seteuid(self.PROCESS_UID)
   
   
if __name__ == "__main__":
    authorizer = UnixAuthorizer()
    # add a user (note: user must already exists)
    authorizer.add_user('user', perm='elradfmw')
    authorizer.add_anonymous(os.getcwd())
    ftp_handler = ftpserver.FTPHandler
    ftp_handler.authorizer = authorizer
    address = ('', 21)
    ftpd = ftpserver.FTPServer(address, ftp_handler)
    ftpd.serve_forever()

3.5 - Windows NT FTP Server

The following code shows how to implement a basic authorizer for a Windows NT workstation to authenticate against existing Windows user accounts. This code uses Mark Hammond's pywin32 extension which is required to be installed previously.

Note that, as for UNIX authorizer, the users you're going to add through the add_user method must already exist on the system.

download script

#!/usr/bin/env python
# winnt_ftpd.py
   
"""A ftpd using local Windows NT account database to authenticate users
(users must already exist).
   
It also provides a mechanism to (temporarily) impersonate the system
users every time they are going to perform filesystem operations.
"""
   
import os
import win32security, win32net, pywintypes, win32con
   
from pyftpdlib import ftpserver
   
   
def get_profile_dir(username):
    """Return the user's profile directory."""
    import _winreg, win32api
    sid = win32security.ConvertSidToStringSid(
            win32security.LookupAccountName(None, username)[0])
    try:
        key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE,
          r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList"+"\\"+sid)
    except WindowsError:
        raise ftpserver.AuthorizerError("No profile directory defined for %s "
                                        "user" %username)
    value = _winreg.QueryValueEx(key, "ProfileImagePath")[0]
    return win32api.ExpandEnvironmentStrings(value)
   
   
class WinNtAuthorizer(ftpserver.DummyAuthorizer):
   
    def add_user(self, username, homedir=None, **kwargs):
        """Add a "real" system user to the virtual users table.
   
        If no homedir argument is specified the user's profile
        directory will possibly be determined and used.
   
        The keyword arguments in kwargs are the same expected by the
        original add_user method: "perm", "msg_login" and "msg_quit".
        """
        # get the list of all available users on the system and check
        # if provided username exists
        users = [entry['name'] for entry in win32net.NetUserEnum(None, 0)[0]]
        if not username in users:
            raise ftpserver.AuthorizerError('No such user "%s".' %username)
        if not homedir:
            homedir = get_profile_dir(username)
        ftpserver.DummyAuthorizer.add_user(self, username, '', homedir,
                                           **kwargs)
   
    def add_anonymous(self, homedir=None, realuser="Guest",
                      password="", **kwargs):
        """Add an anonymous user to the virtual users table.
   
        If no homedir argument is specified the realuser's profile
        directory will possibly be determined and used.
   
        realuser and password arguments are the credentials to use for
        managing anonymous sessions.
        The same behaviour is followed in IIS where the Guest account
        is used to do so (note: it must be enabled first).
        """
        users = [entry['name'] for entry in win32net.NetUserEnum(None, 0)[0]]
        if not realuser in users:
            raise ftpserver.AuthorizerError('No such user "%s".' %realuser)
        if not homedir:
            homedir = get_profile_dir(realuser)
        # make sure provided credentials are valid, otherwise an exception
        # will be thrown; to do so we actually try to impersonate the user
        self.impersonate_user(realuser, password)
        self.terminate_impersonation()
        ftpserver.DummyAuthorizer.add_anonymous(self, homedir, **kwargs)
        self.anon_user = realuser
        self.anon_pwd = password
   
    def validate_authentication(self, username, password):
        if (username == "anonymous") and self.has_user('anonymous'):
            username = self.anon_user
            password = self.anon_pwd
        try:
            win32security.LogonUser(username, None, password,
                win32con.LOGON32_LOGON_INTERACTIVE,
                win32con.LOGON32_PROVIDER_DEFAULT)
            return True
        except pywintypes.error:
            return False
   
    def impersonate_user(self, username, password):
        if (username == "anonymous") and self.has_user('anonymous'):
            username = self.anon_user
            password = self.anon_pwd
        handler = win32security.LogonUser(username, None, password,
                      win32con.LOGON32_LOGON_INTERACTIVE,
                      win32con.LOGON32_PROVIDER_DEFAULT)
        win32security.ImpersonateLoggedOnUser(handler)
        handler.Close()
   
    def terminate_impersonation(self):
        win32security.RevertToSelf()


if __name__ == "__main__":
    authorizer = WinNtAuthorizer()
    # add a user (note: user must already exists)
    authorizer.add_user('user', perm='elradfmw')
    # add an anonymous user using Guest account to handle the anonymous
    # sessions (note: Guest must be enabled first)
    authorizer.add_anonymous(os.getcwd())
    ftp_handler = ftpserver.FTPHandler
    ftp_handler.authorizer = authorizer
    address = ('', 21)
    ftpd = ftpserver.FTPServer(address, ftp_handler)
    ftpd.serve_forever()

3.6 - Throttle bandwidth

An important feature for an ftpd is limiting the speed for downloads and uploads affecting the data channel.

Starting from version 0.5.2 it is possible to use the new ThrottledDTPHandler class to set such limits. The basic idea behind ThrottledDTPHandler is to wrap sending and receiving in a data counter and temporary "sleep" the data channel so that you burst to no more than x Kb/sec average. When it realizes that more than x Kb in a second are being transmitted it temporary blocks the transfer for a certain number of seconds.

download script

4.0 - Advanced usages

4.1 - FTPS (FTP over TLS/SSL) server

Although the standard code base does not offer any "official" FTPS support yet starting from version 0.5.1 the demo directory contains a script which implements a simple FTPS daemon supporting both TLS and SSL protocols and AUTH, PBSZ and PROT commands as defined in RFC-4217.

This is possible thanks to the new ssl module introduced in Python 2.6.

download script


Comment by tassoman, Jan 19, 2008

I cannot edit page... but if you're behind a nat and you registered a Dynamic DNS free service, you could try to get your ip address by importing socket module, then by gethostbyname.

import socket
ftp_handler.masquerade_address = socket.gethostbyname('username.dyndns.org')
Comment by makambug, Nov 07, 2008

I just want to thank you for that lib. It's just amazing simple and functional.

Viva Python


Sign in to add a comment
Hosted by Google Code