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.
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.
Log messages intended for the end user.
Log commands and responses passing through the command channel.
Log traceback outputs occurring in case of errors.
Base class for authorizers exceptions.
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')
Read permissions:Write permissions
- "e" = change directory (CWD command)
- "l" = list files (LIST, NLST, MLSD commands)
- "r" = retrieve file from the server (RETR command)
- "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)
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:
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. Unless you want to add extra functionalities like bandwidth throttling you shouldn't be interested in putting hands on this class.
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()
class ftpserver.AbstractedFS()
A class used to interact with the file system, providing a high level, cross-platform interface compatible with both Windows and UNIX style filesystems. It provides some utility methods to operate on pathnames and the wraps around the common calls to interact with the filesystem (e.g. open(), os.mkdir(), os.listdir(), etc...). These latter ones are not reproduced below (see the source instead).
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
#!/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()
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()
#!/usr/bin/env python
# md5_ftpd.py
"""A basic ftpd storing passwords as hash digests (platform independent).
"""
import md5
import os
from pyftpdlib import ftpserver
class DummyMD5Authorizer(ftpserver.DummyAuthorizer):
def validate_authentication(self, username, password):
hash = md5.new(password).hexdigest()
return self.user_table[username]['pwd'] == hash
if __name__ == "__main__":
# get a hash digest from a clear-text password
hash = md5.new('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()
The example below shows how to use pwd and spwd modules available in Python 2.5 or greater (UNIX systems only) 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.
#!/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()
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.
#!/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()