Python FTP server library (pyftpdlib)

Date: 2007-02-22
Name: Python FTP server library (pyftpdlib)
Current version: v0.1.1
Status: experimental
Programming language: Python
License: GNU
Author: billiejoex (ITA)
Mail: Web: http://billiejoex.altervista.org
  1. About
  2. Features
  3. Download
  4. Requirements
  5. Quick start
  6. Advanced usages

About

Python FTP server library provides an high-level portable interface to easily write asynchronous FTP servers with Python.
Based on asyncore / asynchat frameworks pyftpdlib is actually the most complete RFC959 FTP server implementation available for Python programming language.

Features

Download

Google code download page

Requirements

Python 2.3 or higher.

Quick start

Python 2.4.3 (#1, Oct 1 2006, 18:00:19)
[GCC 4.1.1 20060928 (Red Hat 4.1.1-28)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from pyftpdlib import FTPServer
>>> authorizer = FTPServer.dummy_authorizer()
>>> authorizer.add_user('user', '12345', '/home/user', perm=('r', 'w'))
>>> authorizer.add_anonymous('/home/nobody')
>>> ftp_handler = FTPServer.ftp_handler
>>> ftp_handler.authorizer = authorizer
>>> ftp_handler.msg_connect = "Hey there!"

>>> address = ("127.0.0.1", 21)
>>> ftpd = FTPServer.ftp_server(address, ftp_handler)
>>> ftpd.serve_forever()
Serving FTP on 0.0.0.0:21.
[]10.0.0.1:1089 connected.
10.0.0.1:1089 ==> 220 Ready.
10.0.0.1:1089 <== USER anonymous
10.0.0.1:1089 ==> 331 Username ok, send passowrd.
10.0.0.1:1089 <== PASS ******
10.0.0.1:1089 ==> 230 User anonymous logged in.
[anonymous]@10.0.0.1:1089 User anonymous logged in.
10.0.0.1:1089 <== CWD /
[anonymous]@10.0.0.1:1089 OK CWD "/"
10.0.0.1:1089 ==> 250 "/" is the current directory.
10.0.0.1:1089 <== TYPE A
10.0.0.1:1089 ==> 200 Type set to: ASCII.
10.0.0.1:1089 <== PASV
10.0.0.1:1089 ==> 227 Entering passive mode (10,0,0,1,4,0)
10.0.0.1:1089 <== LIST
10.0.0.1:1089 ==> 125 Data connection already open; Transfer starting.
[anonymous]@10.0.0.1:1089 OK LIST. Transfer starting.
10.0.0.1:1089 ==> 226 Transfer complete.
[anonymous]@10.0.0.1:1089 Trasfer complete. 548 bytes transmitted.
10.0.0.1:1089 <== QUIT
10.0.0.1:1089 ==> 221 Goodbye.
[anonymous]@10.0.0.1:1089 Disconnected.


Advanced usages


Logging management

FTP server library provides 3 different logging streams:

This last one is disabled by default, the first and the second one are printed to stdout through log() and linelog() functions of FTPserver library.
Let's suppose you don't want to print FTPd messages to screen but you want to write them into a file (for example /var/log/ftpd.log) and 'line-logs' messages into a different one ('/var/log/ftpd.lines.log'). Here's how you can do that:

#!/usr/bin/env python

from pyftpdlib import FTPServer
import os

def standard_logger(msg):
f = open('/var/log/ftpd.log', 'a')
f.write(msg + '\n')
f.close()

def line_logger(msg):
f = open('/var/log/ftpd.lines.log', 'a')
f.write(msg + '\n')
f.close()

if __name__ == "__main__":
FTPServer.log = standard_logger
FTPServer.logline = line_logger

authorizer = FTPServer.dummy_authorizer()
authorizer.add_anonymous(os.getcwd())
ftp_handler = FTPServer.ftp_handler
ftp_handler.authorizer = authorizer
address = ('', 21)
ftpd = FTPServer.ftp_server(address, ftp_handler)
ftpd.serve_forever()


Storing passwords as hash digests

Using FTP server lib with the default dummy_authorizer means that password will be stored in clear-text. If you want to implement a basic encrypted account storage system you have to write your own authorizer. The example below shows how to easily store passwords as one-way hashes by using md5 algorithm and by sub-classing the original dummy_authorizer class overriding its validate_authentication method:

#!/usr/bin/env python
# md5_ftpd.py

# FTPd storing passwords as hash digest (platform independent).

import md5
import os
from pyftpdlib import FTPServer

class dummy_encrypted_authorizer(FTPServer.dummy_authorizer):

def __init__(self):
FTPServer.dummy_authorizer.__init__(self)

def validate_authentication(self, username, password):
if username == 'anonymous':
if self.has_user('anonymous'):
return 1
else:
return 0
hash = md5.new(password).hexdigest()
return self.user_table[username]['pwd'] == hash

if __name__ == "__main__":
# get an hash digest from a clear-text password
hash = md5.new('12345').hexdigest()
authorizer = dummy_encrypted_authorizer()
authorizer.add_user('user', hash, os.getcwd(), perm=('r', 'w'))
authorizer.add_anonymous(os.getcwd())
ftp_handler = FTPServer.ftp_handler
ftp_handler.authorizer = authorizer
address = ('', 21)
ftpd = FTPServer.ftp_server(address, ftp_handler)
ftpd.serve_forever()


Unix FTP Server

If you're running a Unix system you could want to configure pyftpd to include support for 'real' users existing on the system. The example below shows how to use pwd / spwd modules (available since Python 2.5) to interact with UNIX user account and password database (users must be created previously).
This basic authorizer also gets the user's home directory.

#!/usr/bin/env python
# unix_ftpd.py

# FTPd using local unix account database to authenticate users and get
# their home directories (users must be created previously).
import os
import pwd, spwd, crypt
from pyftpdlib import FTPServer

class unix_authorizer(FTPServer.dummy_authorizer):

def __init__(self):
FTPServer.dummy_authorizer.__init__(self)

def add_user(self, username, home='', perm=('r')):
assert username in [i[0] for i in pwd.getpwall()], 'No such user "%s".' %username
pw = spwd.getspnam(username).sp_pwd
if not home:
home = pwd.getpwnam(username).pw_dir
assert os.path.isdir(home), 'No such directory "%s".' %home
dic = {'pwd' : pw,
'home' : home,
'perm' : perm
}
self.user_table[username] = dic

def validate_authentication(self, username, password):
if username == 'anonymous':
if self.has_user('anonymous'):
return 1
else:
return 0
else:
pw1 = spwd.getspnam(username).sp_pwd
pw2 = crypt.crypt(password, pw1)
return pw1 == pw2

if __name__ == "__main__":
authorizer = unix_authorizer()
authorizer.add_user('user', perm=('r', 'w'))
authorizer.add_anonymous(os.getcwd())
ftp_handler = FTPServer.ftp_handler
ftp_handler.authorizer = authorizer
address = ('', 21)
ftpd = FTPServer.ftp_server(address, ftp_handler)
ftpd.serve_forever()



Windows NT FTP Server


This next code shows how to implement a basic authorizer for a Windows NT workstation (windows NT, 2000, XP, 2003 server and so on...) by using pywin32 extension.

# winFTPserver.py
# Basic authorizer for Windows NT accounts (users must be created previously).

import os
import win32security, win32net, pywintypes
from pyftpdlib import FTPServer

class winNT_authorizer(FTPServer.dummy_authorizer):

def __init__(self):
FTPServer.dummy_authorizer.__init__(self)

def add_user(self, username, home, perm=('r')):
# check if user exists
users = [elem['name'] for elem in win32net.NetUserEnum(None, 0)[0]]
assert username in users, 'No such user "%s".' %username
assert os.path.isdir(home), 'No such directory "%s".' %home
dic = {'pwd' : None,
'home' : home,
'perm' : perm
}
self.user_table[username] = dic

def validate_authentication(self, username, password):
if username == 'anonymous':
if self.has_user('anonymous'):
return 1
else:
return 0
else:
try:
# check credentials
win32security.LogonUser (
username,
None,
password,
win32security.LOGON32_LOGON_NETWORK,
win32security.LOGON32_PROVIDER_DEFAULT
)
return 1
except pywintypes.error, err:
return 0

if __name__ == "__main__":
authorizer = winNT_authorizer()
authorizer.add_user ('user', os.getcwd(),perm=('r', 'w'))
authorizer.add_anonymous (os.getcwd())
ftp_handler = FTPServer.ftp_handler
ftp_handler.authorizer = authorizer
address = ('', 21)
ftpd = FTPServer.ftp_server(address, ftp_handler)
ftpd.serve_forever()

 

EOF
billiejoex 2007-02-20 05:28PM