High severityNVD Advisory· Published Oct 19, 2010· Updated Apr 29, 2026
CVE-2007-6738
CVE-2007-6738
Description
pyftpdlib before 0.1.1 does not choose a random value for the port associated with the PASV command, which makes it easier for remote attackers to obtain potentially sensitive information about the number of in-progress data connections by reading the response to this command.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
pyftpdlibPyPI | < 0.1.1 | 0.1.1 |
Affected products
1Patches
1d171bdc4ef7aTagging the 0.1.1 release.
16 files changed · +2854 −0
demo/basic_ftpd.py+15 −0 added@@ -0,0 +1,15 @@ +#!/usr/bin/env python +# basic_ftpd.py + +import os +from pyftpdlib import FTPServer + +if __name__ == "__main__": + authorizer = FTPServer.dummy_authorizer() + authorizer.add_user ('user', '12345', os.getcwd(), perm=('r', 'w')) + authorizer.add_anonymous (os.getcwd()) + ftp_handler = FTPServer.ftp_handler + ftp_handler.authorizer = authorizer + address = ('127.0.0.1', 21) + ftpd = FTPServer.ftp_server (address, ftp_handler) + ftpd.serve_forever()
demo/md5_ftpd.py+34 −0 added@@ -0,0 +1,34 @@ +#!/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()
demo/unix_ftpd.py+47 −0 added@@ -0,0 +1,47 @@ +#!/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()
demo/winNT_ftpd.py+55 −0 added@@ -0,0 +1,55 @@ +# 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() + +
doc/pyftpdlib.css+95 −0 added@@ -0,0 +1,95 @@ +BODY { + BORDER-RIGHT: 0px; PADDING-RIGHT: 0px; BORDER-TOP: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN-LEFT: 2em; BORDER-LEFT: 0px; MARGIN-RIGHT: 2em; PADDING-TOP: 0px; BORDER-BOTTOM: 0px; FONT-FAMILY: Verdana, Arial, Helvetica, sans-serif; FONT-SIZE: 12pt +} +.done { + COLOR: #005500; BACKGROUND-COLOR: #99ff99 +} +.notdone { + COLOR: #550000; BACKGROUND-COLOR: #ff9999 +} +PRE { + BORDER-RIGHT: black thin solid; PADDING-RIGHT: 1em; BORDER-TOP: black thin solid; PADDING-LEFT: 1em; FONT-SIZE: 12pt; PADDING-BOTTOM: 1em; BORDER-LEFT: black thin solid; PADDING-TOP: 1em; BORDER-BOTTOM: black thin solid; FONT-FAMILY: Neep Alt, Courier New, Courier +} +.boxed { + BORDER-RIGHT: black thin solid; PADDING-RIGHT: 1em; BORDER-TOP: black thin solid; PADDING-LEFT: 1em; PADDING-BOTTOM: 1em; BORDER-LEFT: black thin solid; PADDING-TOP: 1em; BORDER-BOTTOM: black thin solid +} +.shell { + BACKGROUND-COLOR: #ffffdd +} +.python { + BACKGROUND-COLOR: #FFFFFF; font-family: Lucida,Courier New; FONT-SIZE: 10pt +} + +} +.doit { + BORDER-RIGHT: blue thin dashed; BORDER-TOP: blue thin dashed; BORDER-LEFT: blue thin dashed; BORDER-BOTTOM: blue thin dashed; BACKGROUND-COLOR: #0ef +} +.py-src-comment { + COLOR: #1111cc +} +.py-src-keyword { + FONT-WEIGHT: bold; COLOR: #3333cc +} +.py-src-parameter { + FONT-WEIGHT: bold; COLOR: #000066 +} +.py-src-identifier { + COLOR: #cc0000 +} +.py-src-string { + COLOR: #115511 +} +.py-src-endmarker { + DISPLAY: block +} +.py-listing { + BORDER-RIGHT: black thin solid; BORDER-TOP: black thin solid; MARGIN: 1ex; BORDER-LEFT: black thin solid; BORDER-BOTTOM: black thin solid; BACKGROUND-COLOR: #eee +} +.html-listing { + BORDER-RIGHT: black thin solid; BORDER-TOP: black thin solid; MARGIN: 1ex; BORDER-LEFT: black thin solid; BORDER-BOTTOM: black thin solid; BACKGROUND-COLOR: #eee +} +.listing { + BORDER-RIGHT: black thin solid; BORDER-TOP: black thin solid; MARGIN: 1ex; BORDER-LEFT: black thin solid; BORDER-BOTTOM: black thin solid; BACKGROUND-COLOR: #eee +} +.py-listing PRE { + BORDER-RIGHT: medium none; BORDER-TOP: medium none; MARGIN: 0px; BORDER-LEFT: medium none; BORDER-BOTTOM: black thin solid +} +.html-listing PRE { + BORDER-RIGHT: medium none; BORDER-TOP: medium none; MARGIN: 0px; BORDER-LEFT: medium none; BORDER-BOTTOM: black thin solid +} +.listing PRE { + BORDER-RIGHT: medium none; BORDER-TOP: medium none; MARGIN: 0px; BORDER-LEFT: medium none; BORDER-BOTTOM: black thin solid +} +.py-listing .python { + BORDER-RIGHT: medium none; BORDER-TOP: medium none; MARGIN-TOP: 0px; MARGIN-BOTTOM: 0px; BORDER-LEFT: medium none; BORDER-BOTTOM: black thin solid +} +.html-listing .htmlsource { + BORDER-RIGHT: medium none; BORDER-TOP: medium none; MARGIN-TOP: 0px; MARGIN-BOTTOM: 0px; BORDER-LEFT: medium none; BORDER-BOTTOM: black thin solid +} +.caption { + PADDING-BOTTOM: 0.5em; PADDING-TOP: 0.5em; TEXT-ALIGN: center +} +.filename { + FONT-STYLE: italic +} +.manhole-output { + COLOR: blue +} +HR { + DISPLAY: inline +} +UL { + PADDING-RIGHT: 0px; PADDING-LEFT: 1em; PADDING-BOTTOM: 0px; MARGIN: 0px 0px 0px 1em; BORDER-LEFT: 1em; PADDING-TOP: 0px +} +LI { + PADDING-RIGHT: 2px; PADDING-LEFT: 2px; PADDING-BOTTOM: 2px; PADDING-TOP: 2px +} +DT { + FONT-WEIGHT: bold; MARGIN-LEFT: 1ex +} +DD { + MARGIN-BOTTOM: 1em +} +DIV.note { + BORDER-RIGHT: black thin solid; PADDING-RIGHT: 5%; BORDER-TOP: black thin solid; MARGIN-TOP: 1ex; PADDING-LEFT: 5%; MARGIN-LEFT: 5%; BORDER-LEFT: black thin solid; MARGIN-RIGHT: 5%; PADDING-TOP: 1ex; BORDER-BOTTOM: black thin solid; BACKGROUND-COLOR: #ffffcc +}
doc/pyftpdlib.html+276 −0 added@@ -0,0 +1,276 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> +<html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head> + + + <title>Python FTP server library (pyftpdlib)</title><meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> + + + <link href="pyftpdlib.css" type="text/css" rel="stylesheet"> + + <meta content="MSHTML 6.00.2900.2963" name="GENERATOR"> + + <style type="text/css"> +<!-- +.style20 { + font-family: Verdana, Arial, Helvetica, sans-serif; + font-size: 12px; +} +.style22 {font-size: 12px} +.style27 {font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 12px; font-weight: bold; } +.style28 { + font-size: 16px; + font-weight: bold; +} +.style29 { + font-size: 9px; + font-family: Arial, Helvetica, sans-serif; + font-style: italic; +} +.style30 {color: #FF6600} +.style32 {color: #009900} +--> + </style></head> + +<body bgcolor="white"> + +<h1 class="title" style20="" style23=""> +Python FTP server library (pyftpdlib) </h1> + +<table border="0" width="100%"> + + <tbody> + <tr> + + <td> + <table align="center" border="0" height="100" width="98%"> + + <tbody> + <tr> + + <td><span class="style27">Date: 2007-02-22<br> + +Name: Python FTP server library (pyftpdlib)<br> + +Current version: v0.1.1<br> + +Status: experimental<br> + +Programming language: Python<br> + +License: GNU<br> + +Author: billiejoex (ITA)<br> + +Mail: +<!-- +# ascii_converter.py +s = '<p><a href="mlto:ml_AT_ address" class="style3">ml_AT_ address</a></p>' +em = '' +for i in s: + em += "%s%.2x" %('%', ord(i)) +--> + <script language="javascript" type="text/javascript"> +em= "%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%62%69%6c%6c%69%65%6a%6f%65%78%40%67%6d%61%69%6c%2e%63%6f%6d%22%3e%62%69%6c%6c%69%65%6a%6f%65%78%40%67%6d%61%69%6c%2e%63%6f%6d%3c%2f%61%3e"; +document.write (unescape(em + "<br>")); + </script> +Web: <a href="http://billiejoex.altervista.org">http://billiejoex.altervista.org</a></span></td> + + </tr> + + + </tbody> + </table> + </td> + + </tr> + + </tbody> +</table> + +<div class="style27"></div> + +<div class="toc" style20=""> +<ol class="style20"> + + <li><a href="#1">About</a></li> + + <li><a href="#2">Features</a></li> + + <li><a href="#3">Download</a></li> + + <li><a href="#4">Requirements</a></li> + + <li><a href="#5">Quick start</a></li> + + <li><a href="#6">Advanced usages </a> + + <ul> + + <li><a href="#7">Logging management</a></li> + + <li><a href="#8">Storing passwords as hash digests </a></li> + + <li><a href="#9">UNIX FTP Server</a></li> + + <li><a href="#10">Windows NT FTP Server </a></li> + + + </ul> + + </li> + + +</ol> + +</div> + +<div class="content"> + +<p class="style20"><strong><a name="1" id="1"></a>About</strong> </p> + + +<p class="style20"> Python FTP server library provides an high-level portable interface to easily write asynchronous FTP servers with Python.<br> + +Based on asyncore / asynchat frameworks pyftpdlib is actually the most +complete RFC959 FTP server implementation available for Python programming language.<br> + +<br> + + <strong><a name="2" id="2"></a></strong><strong>Features</strong> </p> + + +<ul class="style20"> + + <li>High portability: + + <ul> + + <li>Entirely +written in pure Python, no third party modules are used, it just works +on any system where select( ) or poll( ) are available.</li> + + <li>Abstracted file system interface is provided. It makes no difference if you're using a DOS-like or a UNIX-like filesystem.</li> + + <li>Extremely flexible system of "authorizers" able to +interact with account and password database of different systems +(Windows NT, Unix, OSx and so on...). </li> + </ul> + </li> + + <li>High performance: + + <ul> + + <li>thanks to asyncore / asynchat frameworks it permits multiplexing I/O with +various client connections within a single process / thread.</li> + </ul> + </li> + + <li>Extremely simple and highly customizable thanks to its simpler Object Oriented API.</li> + <li>Compact: the entire library is distributed in a single file (FTPServer.py).</li> +</ul> + + +<p class="style20"><strong><a name="3" id="3"></a>Download</strong></p> + + +<p class="style20"><a href="http://code.google.com/p/pyftpdlib/downloads/list">Google code download page</a></p> + + +<p class="style20"><strong><strong><a name="4" id="4"></a>Requirements</strong></strong></p> + + +<p class="style20">Python 2.3 or higher.</p> + + +<p class="style20"> <strong><a name="5" id="5"></a>Quick start</strong></p> + + +<pre class="python">Python 2.4.3 (#1, Oct 1 2006, 18:00:19)<br>[GCC 4.1.1 20060928 (Red Hat 4.1.1-28)] on linux2<br>Type "help", "copyright", "credits" or "license" for more information.<br>>>> <strong><span style="color: rgb(255, 119, 0);">from </span>pyftpdlib<span style="color: rgb(255, 119, 0);"> import</span> FTPServer</strong><br>>>> <strong>authorizer = FTPServer.dummy_authorizer()</strong><br>>>> <strong>authorizer.add_user(<span class="style32">'user'</span>, <span style="color: rgb(0, 170, 0);">'12345'</span>, <span style="color: rgb(0, 170, 0);">'/home/user'</span>, perm=(<span style="color: rgb(0, 170, 0);">'r'</span>, <span style="color: rgb(0, 170, 0);">'w'</span>))</strong><br>>>> <strong>authorizer.add_anonymous(<span style="color: rgb(0, 170, 0);">'/home/nobody'</span>)</strong><br>>>> <strong>ftp_handler = FTPServer.ftp_handler</strong><br>>>> <strong>ftp_handler.authorizer = authorizer<br>>>> ftp_handler.msg_connect = <span class="style32">"Hey there!"</span></strong><br>>>> <strong>address = (<span style="color: rgb(0, 170, 0);">"127.0.0.1"</span>, 21)</strong><br>>>> <strong>ftpd = FTPServer.ftp_server(address, ftp_handler)</strong><br>>>> <strong>ftpd.serve_forever()</strong><br>Serving FTP on 0.0.0.0:21.<br>[]10.0.0.1:1089 connected.<br>10.0.0.1:1089 ==> 220 Ready.<br>10.0.0.1:1089 <== USER anonymous<br>10.0.0.1:1089 ==> 331 Username ok, send passowrd.<br>10.0.0.1:1089 <== PASS ******<br>10.0.0.1:1089 ==> 230 User anonymous logged in.<br>[anonymous]@10.0.0.1:1089 User anonymous logged in.<br>10.0.0.1:1089 <== CWD /<br>[anonymous]@10.0.0.1:1089 OK CWD "/"<br>10.0.0.1:1089 ==> 250 "/" is the current directory.<br>10.0.0.1:1089 <== TYPE A<br>10.0.0.1:1089 ==> 200 Type set to: ASCII.<br>10.0.0.1:1089 <== PASV<br>10.0.0.1:1089 ==> 227 Entering passive mode (10,0,0,1,4,0)<br>10.0.0.1:1089 <== LIST<br>10.0.0.1:1089 ==> 125 Data connection already open; Transfer starting.<br>[anonymous]@10.0.0.1:1089 OK LIST. Transfer starting.<br>10.0.0.1:1089 ==> 226 Transfer complete.<br>[anonymous]@10.0.0.1:1089 Trasfer complete. 548 bytes transmitted.<br>10.0.0.1:1089 <== QUIT<br>10.0.0.1:1089 ==> 221 Goodbye.<br>[anonymous]@10.0.0.1:1089 Disconnected.</pre> + +<h2 class="style28"><strong><a name="6" id="6"></a></strong></h2> + +<h2 class="style28"> <br> + + Advanced usages</h2> + +<p class="style22"><strong><a name="7" id="7"></a><br> + + Logging management</strong></p> + +<p class="style22">FTP server library provides 3 different logging streams:</p> + +<ul class="style22"> + + <li>ftpd logging that notifies the most important messages for the end-user regarding the FTPd.</li> + + <li>line-logging that notifies commands and responses passing through the control FTP channel.</li> + + <li>debug-logging +used for debugging messages (function/method calls, traceback outputs, +low-level informational messages and so on...).</li> + +</ul> + +<p class="style22">This last one is disabled by default, the first and the second one are printed to stdout through <em><strong>log()</strong></em> and <em><strong>linelog()</strong></em> functions of FTPserver library.<br> + + Let's suppose you don't want to print FTPd messages to screen but you + want to write them into a file (for example <em>/var/log/ftpd.log</em>) and + 'line-logs' messages into a different one (<em>'/var/log/ftpd.lines.log'</em>). + Here's how you can do that:</p> + +<pre class="python"><span class="python style16"><font color="#ff7700"><font color="#dd0000">#!/usr/bin/env python</font><font color="#ff7700"> + +from </font></font>pyftpdlib<font color="#ff7700"> import</font> FTPServer<br><span class="style4 style30">import</span> os<br> <br><font color="#ff7700">def</font> <font color="#0000ff">standard_logger</font>(msg):<br> f = open(<font color="#00aa00">'/var/log/ftpd.log'</font>, <font color="#00aa00">'a'</font>)<br> f.write(msg + <font color="#00aa00">'\n'</font>)<br> f.close()<br> <br><font color="#ff7700">def</font> <font color="#0000ff">line_logger</font>(msg):<br> f = open(<font color="#00aa00">'/var/log/ftpd.lines.log'</font>, <font color="#00aa00">'a'</font>)<br> f.write(msg + <font color="#00aa00">'\n'</font>)<br> f.close()<br> <br><font color="#ff7700">if</font> __name__ == <font color="#00aa00">"__main__"</font>:<br> FTPServer.log = standard_logger<br> FTPServer.logline = line_logger<br> <br> authorizer = FTPServer.dummy_authorizer()<br> authorizer.add_anonymous(os.getcwd())<br> ftp_handler = FTPServer.ftp_handler<br> ftp_handler.authorizer = authorizer<br> address = (<font color="#00aa00">''</font>, 21)<br> ftpd = FTPServer.ftp_server(address, ftp_handler)<br> ftpd.serve_forever()</span></pre> + +<p class="style22"><strong><a name="8" id="8"></a><br> + + Storing passwords as hash digests </strong> </p> + +<p class="style22">Using FTP server lib with the default <em><strong>dummy_authorizer</strong></em> 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 <em><strong>dummy_authorizer</strong></em> class overriding its <em><strong>validate_authentication</strong></em> method:</p> + +<pre class="python"><font color="#dd0000">#!/usr/bin/env python<br># md5_ftpd.py<br><br># FTPd storing passwords as hash digest (platform independent).<br><br></font><font color="#ff7700">import</font> md5<br><font color="#ff7700">import</font> os<br><font color="#ff7700">from</font> pyftpdlib <font color="#ff7700">import</font> FTPServer<br><br><font color="#ff7700">class</font> <font color="#0000ff">dummy_encrypted_authorizer</font>(FTPServer.dummy_authorizer):<br><br> <font color="#ff7700">def</font> <font color="#0000ff">__init__</font>(self):<br> FTPServer.dummy_authorizer.__init__(self)<br><br> <font color="#ff7700">def</font> <font color="#0000ff">validate_authentication</font>(self, username, password):<br> <font color="#ff7700">if</font> username == <font color="#00aa00">'anonymous'</font>:<br> <font color="#ff7700">if</font> self.has_user(<font color="#00aa00">'anonymous'</font>):<br> <font color="#ff7700">return</font> 1<br> <font color="#ff7700">else</font>:<br> <font color="#ff7700">return</font> 0<br> hash = md5.new(password).hexdigest()<br> <font color="#ff7700">return</font> self.user_table[username][<font color="#00aa00">'pwd'</font>] == hash<br><br><font color="#ff7700">if</font> __name__ == <font color="#00aa00">"__main__"</font>:<br> <font color="#dd0000"># get an hash digest from a clear-text password<br></font> hash = md5.new(<font color="#00aa00">'12345'</font>).hexdigest()<br> authorizer = dummy_encrypted_authorizer()<br> authorizer.add_user(<font color="#00aa00">'user'</font>, hash, os.getcwd(), perm=(<font color="#00aa00">'r'</font>, <font color="#00aa00">'w'</font>))<br> authorizer.add_anonymous(os.getcwd())<br> ftp_handler = FTPServer.ftp_handler<br> ftp_handler.authorizer = authorizer<br> address = (<font color="#00aa00">''</font>, 21)<br> ftpd = FTPServer.ftp_server(address, ftp_handler)<br> ftpd.serve_forever()</pre> + +<p class="style20"><strong><a name="9" id="9"></a><br> + + Unix FTP Server </strong></p> + +<p class="style20">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 <em><strong>pwd / spwd </strong></em>modules +(available since Python 2.5) to interact with UNIX user account and +password database (users must be created previously).<br> + +This basic authorizer also gets the user's home directory.</p> + +<pre class="python"><font color="#dd0000">#!/usr/bin/env python<br># unix_ftpd.py<br><br># FTPd using local unix account database to authenticate users and get<br># their home directories (users must be created previously).</font><font color="#ff7700"> + +import</font> os<br><font color="#ff7700">import</font> pwd, spwd, crypt<br><font color="#ff7700">from</font> pyftpdlib <font color="#ff7700">import</font> FTPServer<br><br><font color="#ff7700">class</font> <font color="#0000ff">unix_authorizer</font>(FTPServer.dummy_authorizer):<br><br> <font color="#ff7700">def</font> <font color="#0000ff">__init__</font>(self):<br> FTPServer.dummy_authorizer.__init__(self)<br><br> <font color="#ff7700">def</font> <font color="#0000ff">add_user</font>(self, username, home=<font color="#00aa00">''</font>, perm=(<font color="#00aa00">'r'</font>)):<br> <font color="#ff7700">assert</font> username <font color="#ff7700">in</font> [i[0] <font color="#ff7700">for</font> i <font color="#ff7700">in</font> pwd.getpwall()], <font color="#00aa00">'No such user "%s".'</font> %username<br> pw = spwd.getspnam(username).sp_pwd<br> <font color="#ff7700">if</font> <font color="#ff7700">not</font> home:<br> home = pwd.getpwnam(username).pw_dir<br> <font color="#ff7700">assert</font> os.path.isdir(home), <font color="#00aa00">'No such directory "%s".'</font> %home<br> dic = {<font color="#00aa00">'pwd'</font> : pw,<br> <font color="#00aa00">'home'</font> : home,<br> <font color="#00aa00">'perm'</font> : perm<br> }<br> self.user_table[username] = dic<br><br> <font color="#ff7700">def</font> <font color="#0000ff">validate_authentication</font>(self, username, password):<br> <font color="#ff7700">if</font> username == <font color="#00aa00">'anonymous'</font>:<br> <font color="#ff7700">if</font> self.has_user(<font color="#00aa00">'anonymous'</font>):<br> <font color="#ff7700">return</font> 1<br> <font color="#ff7700">else</font>:<br> <font color="#ff7700">return</font> 0<br> <font color="#ff7700">else</font>:<br> pw1 = spwd.getspnam(username).sp_pwd<br> pw2 = crypt.crypt(password, pw1)<br> <font color="#ff7700">return</font> pw1 == pw2<br><br><font color="#ff7700">if</font> __name__ == <font color="#00aa00">"__main__"</font>:<br> authorizer = unix_authorizer()<br> authorizer.add_user(<font color="#00aa00">'user'</font>, perm=(<font color="#00aa00">'r'</font>, <font color="#00aa00">'w'</font>))<br> authorizer.add_anonymous(os.getcwd())<br> ftp_handler = FTPServer.ftp_handler<br> ftp_handler.authorizer = authorizer<br> address = (<font color="#00aa00">''</font>, 21)<br> ftpd = FTPServer.ftp_server(address, ftp_handler)<br> ftpd.serve_forever()<br></pre> + +<p class="style20"><strong><br> + + <a name="10" id="10"></a><br> + + Windows NT FTP Server</strong><span style="font-weight: bold;"><br> + +</span><br> +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. </p> + +<pre class="python"><font color="#dd0000"># winFTPserver.py<br># Basic authorizer for Windows NT accounts (users must be created previously).<br><br></font><font color="#ff7700">import</font> os<br><font color="#ff7700">import</font> win32security, win32net, pywintypes<br><font color="#ff7700">from</font> pyftpdlib <font color="#ff7700">import</font> FTPServer<br><br><font color="#ff7700">class</font> <font color="#0000ff">winNT_authorizer</font>(FTPServer.dummy_authorizer):<br><br> <font color="#ff7700">def</font> <font color="#0000ff">__init__</font>(self):<br> FTPServer.dummy_authorizer.__init__(self)<br><br> <font color="#ff7700">def</font> <font color="#0000ff">add_user</font>(self, username, home, perm=(<font color="#00aa00">'r'</font>)):<br> <font color="#dd0000"># check if user exists<br></font> users = [elem[<font color="#00aa00">'name'</font>] <font color="#ff7700">for</font> elem <font color="#ff7700">in</font> win32net.NetUserEnum(None, 0)[0]]<br> <font color="#ff7700">assert</font> username <font color="#ff7700">in</font> users, <font color="#00aa00">'No such user "%s".'</font> %username<br> <font color="#ff7700">assert</font> os.path.isdir(home), <font color="#00aa00">'No such directory "%s".'</font> %home<br> dic = {<font color="#00aa00">'pwd'</font> : None,<br> <font color="#00aa00">'home'</font> : home,<br> <font color="#00aa00">'perm'</font> : perm<br> }<br> self.user_table[username] = dic<br><br> <font color="#ff7700">def</font> <font color="#0000ff">validate_authentication</font>(self, username, password):<br> <font color="#ff7700">if</font> username == <font color="#00aa00">'anonymous'</font>:<br> <font color="#ff7700">if</font> self.has_user(<font color="#00aa00">'anonymous'</font>):<br> <font color="#ff7700">return</font> 1<br> <font color="#ff7700">else</font>:<br> <font color="#ff7700">return</font> 0<br> <font color="#ff7700">else</font>:<br> <font color="#ff7700">try</font>:<br> <font color="#dd0000"># check credentials<br></font> win32security.LogonUser (<br> username,<br> None,<br> password,<br> win32security.LOGON32_LOGON_NETWORK,<br> win32security.LOGON32_PROVIDER_DEFAULT<br> )<br> <font color="#ff7700">return</font> 1<br> <font color="#ff7700">except</font> pywintypes.error, err:<br> <font color="#ff7700">return</font> 0<br><br><font color="#ff7700">if</font> __name__ == <font color="#00aa00">"__main__"</font>:<br> authorizer = winNT_authorizer()<br> authorizer.add_user (<font color="#00aa00">'user'</font>, os.getcwd(),perm=(<font color="#00aa00">'r'</font>, <font color="#00aa00">'w'</font>))<br> authorizer.add_anonymous (os.getcwd())<br> ftp_handler = FTPServer.ftp_handler<br> ftp_handler.authorizer = authorizer<br> address = (<font color="#00aa00">''</font>, 21)<br> ftpd = FTPServer.ftp_server(address, ftp_handler)<br> ftpd.serve_forever()<br></pre> + +<p> </p> + +<p class="style29">EOF<br> + +billiejoex 2007-02-20 05:28PM </p> + +<p> </p> + +</div> + +</body></html> \ No newline at end of file
HISTORY+20 −0 added@@ -0,0 +1,20 @@ + +History +======= + + +Version: v0.1.1 - Date: 2007-02-22 +---------------------------------- + + - port selection on PASV command has been randomized (this to prevent a remote user + to know how many data connections are in progress on the server). + - fixed bug in demo/unix_ftpd.py script (reported by Roger Erens). + - (little) modification to add_anonymous method of dummy_authorizer class. + - "ftp_server.serve_forever" automatically re-use address if current system is unix. + - license changed into a MIT style one. + + +Version: v0.1.0 - Date: 2007-02-22 +---------------------------------- + + - first proof of concept beta release.
LICENSE+22 −0 added@@ -0,0 +1,22 @@ +====================================================================== +Copyright (C) 2007 billiejoex <billiejoex@gmail.com> + + All Rights Reserved + +Permission to use, copy, modify, and distribute this software and +its documentation for any purpose and without fee is hereby +granted, provided that the above copyright notice appear in all +copies and that both that copyright notice and this permission +notice appear in supporting documentation, and that the name of +billiejoex not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +billiejoex DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN +NO EVENT billiejoex BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +====================================================================== \ No newline at end of file
pyftpdlib/FTPServer.py+1771 −0 added@@ -0,0 +1,1771 @@ +#!/usr/bin/env python +# FTPServer.py + +# ====================================================================== +# Copyright (C) 2007 billiejoex <billiejoex@gmail.com> +# +# All Rights Reserved +# +# Permission to use, copy, modify, and distribute this software and +# its documentation for any purpose and without fee is hereby +# granted, provided that the above copyright notice appear in all +# copies and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of +# billiejoex not be used in advertising or publicity pertaining to +# distribution of the software without specific, written prior +# permission. +# +# billiejoex DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN +# NO EVENT billiejoex BE LIABLE FOR ANY SPECIAL, INDIRECT OR +# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# ====================================================================== + + +""" +RFC959 asynchronous FTP server. + +Usage: +>>> from pyftpdlib import FTPServer +>>> authorizer = FTPServer.dummy_authorizer() +>>> authorizer.add_user('user', '12345', '/home/user', perm=('r', 'w', 'wd')) +>>> authorizer.add_anonymous('/home/nobody') +>>> ftp_handler = FTPServer.ftp_handler +>>> ftp_handler.authorizer = authorizer +>>> address = ("", 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. +""" + +# Overview: +# +# This file implements a fully functioning asynchronous FTP server as defined in RFC 959. +# It has a hierarchy of classes which implement the backend functionality for the +# ftpd. +# +# A number of classes are provided: +# +# [ftp_server] - the base class for the backend. +# +# [ftp_handler] - a class representing the server-protocol-interpreter (server-PI, see RFC 959). +# Every time a new connection occurs ftp_server class will create a new ftp_handler instance +# that will handle the current PI session. +# +# [active_dtp], [passive_dtp] - base classes for active/passive-DTP backend. +# +# [dtp_handler] - class handling server-data-transfer-process (server-DTP, see RFC 959) +# managing data-transfer operations. +# +# [dummy_authorizer] - an "authorizer" is a class handling authentication and permissions of ftpd. +# It is used inside ftp_handler class to verify user passwords, to get user's home-directory +# and to get permissions when a r/w I/O filesystem event occurs. +# "dummy_authorizer" is the base authorizer class providing a platform independent interface +# for managing "virtual-users". +# +# [abstracted_fs] - class used to interact with file-system providing an high-level platform-independent +# interface able to work on both DOS/UNIX-like file systems. +# +# [error] - base class for module exceptions. +# +# +# Moreover, FTPServer provides 3 different logging streams trough 3 functions: +# +# [log] - the main logger that notifies the most important messages for the end-user regarding the FTPd. +# +# [logline] - that notifies commands and responses passing through the control FTP channel. +# +# [debug] - used for debugging messages (function/method calls, traceback outputs, +# low-level informational messages and so on...). Disabled by default. +# +# +# +# Tested under Windows XP sp2, Linux Fedora 6, Linux Debian Sarge, Linux Ubuntu Breezy. +# +# Author: billiejoex < billiejoex@gmail.com > + + +# ----------------- +# TODO: +# ----------------- +# +# - LIST/NLST commands are currently the mostly CPU-intensive blocking operations. +# A sort of cache could be implemented (take a look at dircache module). +# +# - brute force protection: 'freeze'/'sleep' (without blocking the main loop) +# PI session for a certain amount of time if authentication fails. +# +# - check MDTM command's specifications (RFC959 doesn't talk about it). + + +# ----------------- +# OPEN PROBLEMS: +# ----------------- +# +# - I didn't well understood when and why asyncore.handle_expt() is called. +# Out Of Band data? How do I have to manage that? +# +# - actually RNFR/RNTO commands could also be used to *move* a file/directory +# instead of just renaming them. RFC959 doesn't tell if this must be allowed or not +# (I believe not). +# +# - What to do if more than one PASV/PORT cmds are received? And what if they're received +# during a transfer? RFC959 doesn't tell anything about it. +# Actually data-channel is just restarted. +# +# - DoS/asyncore vulnerability: select() supports only a limited variable number of socket +# descriptors (aka simultaneous connections). When this number is reached select() +# raises a ValueError exception but asyncore doesn't handle it (a crash occurs). + + +# ----------------- +# INTERFACE +# (discussions/problems about the interface to provide to the end user) +# ----------------- +# +# - [winNT_authorizer] and [unix_authorizer] classes - would it be a good idea adding them +# inside the module or would be enough just showing them in documentation/advanced usages? +# +# - higher authorizers customization: actually authorizers understand only read and write +# permissions and they make no difference if objects are files or directories. +# Would it be a good idea providing additional permission levels? For example: +# +# / files +# (permit? (y/n)) renaming / creation / deletion +# \ directories + + +# -------- +# TIMELINE +# -------- +# 0.1.1 : 2007-03-07 +# 0.1.0 : 2007-02-22 + + +__pname__ = 'Python FTP server library (pyftpdlib)' +__ver__ = '0.1.1' +__state__ = 'beta' +__date__ = '2007-03-07' +__author__ = 'billiejoex (ITA)' +__mail__ = 'billiejoex@gmail.com' +__web__ = 'http://billiejoex.altervista.org' +__license__ = 'see LICENSE file' + + +import asyncore +import asynchat +import socket +import os +import sys +import traceback +import time +try: + import cStringIO as StringIO +except ImportError: + import StringIO + + +proto_cmds = { + 'ABOR' : "abort data-channel transfer", + 'ALLO' : "allocate space for file about to be sent (obsolete)", + 'APPE' : "* resume upload", + 'CDUP' : "go to parent directory", + 'CWD' : "[*] change current directory", + 'DELE' : "* remove file", + 'HELP' : "print this help", + 'LIST' : "list files", + 'MDTM' : "* get last modification time", + 'MODE' : "* set data transfer mode (obsolete)", + 'MKD' : "* create directory", + 'NLST' : "list file names", + 'NOOP' : "just do nothing", + 'PASS' : "* user's password", + 'PASV' : "start passive data channel", + 'PORT' : "start active data channel", + 'PWD' : "get current dir", + 'QUIT' : "quit", + 'REIN' : "flush account informations", + 'REST' : "* restart file position (transfer resuming)", + 'RETR' : "* download file", + 'RMD' : "* remove directory", + 'RNFR' : "* file/directory renaming (source name)", + 'RNTO' : "* file/directory renaming (destination name)", + 'SIZE' : "* get file size", + 'STAT' : "status information", + 'STOR' : "* upload file", + 'STOU' : 'store a file with a unique name', + 'STRU' : "* set file transfer structure (obsolete)", + 'SYST' : "get system type", + 'TYPE' : "* set transfer type (I=binary, A=ASCII)", + 'USER' : "* set username", + # * argument required + } + +deprecated_cmds = { + 'XCUP' : '== CDUP (deprecated)', + 'XCWD' : '== CWD (deprecated)', + 'XMKD' : '== MKD (deprecated)', + 'XPWD' : '== LIST (deprecated)', + 'XRMD' : '== RMD (deprecated)' + } + +proto_cmds.update(deprecated_cmds) + +# Not implemented commands: I've not implemented (and a lot of other FTP server +# did the same) ACCT, SITE and SMNT because I find them useless. +not_implemented_cmds = { + 'ACCT' : 'account permissions', + 'SITE' : 'site specific server services', + 'SMNT' : 'structure mount' + } + +type_map = {'a':'ASCII', + 'i':'Binary'} + + +class error(Exception): + """Base class for module exceptions.""" + + def __init__(self, msg=''): + self.message = msg + Exception.__init__(self, msg) + + def __repr__(self): + return self.message + + +def __get_hs(): + s = "" + l = proto_cmds.keys() + l.sort() + for cmd in l: + s += '\t%-5s %s\r\n' %(cmd, proto_cmds[cmd]) + return s +helper_string = __get_hs() + + +# --- loggers + +def log(msg): + "FTPd logger: log messages about FTPd for the end user." + print msg + +def logline(msg): + "Lines logger: log commands and responses passing through the control channel." + print msg + +def debug(msg): + "Debugger: log debugging messages (function/method calls, traceback outputs and so on...)." + pass + #print "\t%s" %msg + + +# --- authorizers + +class basic_authorizer: + """This class exists just for documentation. + If you want to write your own authorizer you must provide all + the following methods. + """ + def __init__(self): + "" + def add_user(self, username, password, homedir, perm=('r')): + "" + def add_anonymous(self, homedir, perm=('r')): + "" + def validate_authentication(self, username, password): + "" + def has_user(self, username): + "" + def get_home_dir(self, username): + "" + def r_perm(self, username, obj): + "" + def w_perm(self, username, obj): + "" + +class dummy_authorizer: + """Dummy authorizer base class providing a basic portable + interface for handling "virtual" users. + """ + + user_table = {} + + def __init__(self): + pass + + def add_user(self, username, password, homedir, perm=('r')): + assert os.path.isdir(homedir), 'No such directory: "%s".' %homedir + for i in perm: + if i not in ('r', 'w'): + raise error, 'No such permission "%s".' %i + if self.has_user(username): + raise error, 'User "%s" already exists.' %username + dic = {'pwd' : str(password), + 'home' : str(homedir), + 'perm' : perm + } + self.user_table[username] = dic + + def add_anonymous(self, homedir, perm=('r')): + if perm not in ('', 'r'): + if perm == 'w': + raise error, "Anonymous aims to be a read-only user." + else: + raise error, 'No such permission "%s".' %perm + assert os.path.isdir(homedir), 'No such directory: "%s".' %homedir + if self.has_user('anonymous'): + raise error, 'User anonymous already exists.' + dic = {'pwd' : '', + 'home' : homedir, + 'perm' : perm + } + self.user_table['anonymous'] = dic + + def validate_authentication(self, username, password): + return self.user_table[username]['pwd'] == password + + def has_user(self, username): + return self.user_table.has_key(username) + + def get_home_dir(self, username): + return self.user_table[username]['home'] + + def r_perm (self, username, obj): + return 'r' in self.user_table[username]['perm'] + + def w_perm (self, username, obj): + return 'w' in self.user_table[username]['perm'] + + +# system dependent authorizers + +## if os.name == 'posix': +## class unix_authorizer: +## """Interface to UNIX user account and password database +## (users must be created previously).""" +## +## def __init__(self): +## raise NotImplementedError +## +## +## if os.name == 'nt': +## class winNT_authorizer: +## """Interface to Windows NT user account and password +## database (users must be created previously). +## """ +## +## def __init__(self): +## raise NotImplementedError + + +# --- FTP + +class ftp_handler(asynchat.async_chat): + """A class representing the server-protocol-interpreter (server-PI, see RFC 959). + Every time a new connection occurs ftp_server class will create a new + instance of this class that will handle the current PI session. + """ + + authorizer = dummy_authorizer() + msg_connect = "Pyftpd %s" %__ver__ + msg_login = "" + msg_quit = "" + + def __init__(self): + + # session attributes + self.fs = abstracted_fs() + self.in_producer_queue = None + self.out_producer_queue = None + self.authenticated = False + self.username = "" + self.max_login_attempts = [3, 0] # (value, counter) + self.current_type = 'a' + self.restart_position = 0 + self.quit_pending = False + + # dtp attributes + self.dtp_ready = False + self.dtp_server = None + self.data_channel = None + + def __del__(self): + self.debug("ftp_handler.__del__()") + + def __str__(self): + return "<ftp_handler listening on %s:%s (fd=%d)>" %(self.remote_ip, + self.remote_port, + self._fileno) + + def handle(self, socket_object): + asynchat.async_chat.__init__(self, conn=socket_object) + self.remote_ip, self.remote_port = self.socket.getpeername() + self.in_buffer = "" + self.out_buffer = "" + self.ac_in_buffer_size = 4096 + self.ac_out_buffer_size = 4096 + self.set_terminator("\r\n") + + self.push('220-%s.\r\n' %self.msg_connect) + self.respond("220 Ready.") + + # --- asynchat/asyncore overridden methods + + def readable (self): + return (len(self.ac_in_buffer) <= self.ac_in_buffer_size) + + def writable(self): + return len(self.ac_out_buffer) or len(self.producer_fifo) or (not self.connected) + + def collect_incoming_data(self, data): + self.in_buffer = self.in_buffer + data + + def found_terminator(self): + line = self.in_buffer.strip() + self.in_buffer = "" + + cmd = line.split(' ')[0].upper() + space = line.find(' ') + if space != -1: + arg = line[space + 1:] + else: + arg = "" + + if cmd != 'PASS': + self.logline("<== %s" %line) + else: + self.logline("<== %s %s" %(cmd, '*'*6)) + + if (not self.authenticated): + if cmd in ('USER', 'PASS', 'HELP', 'QUIT'): + method = getattr(self, 'ftp_'+cmd, None) + method(arg) # callback + elif cmd in proto_cmds: + self.respond("530 Log in with USER and PASS first") + else: + self.cmd_not_understood(line) + + elif (self.authenticated) and (cmd in proto_cmds): + method = getattr(self, 'ftp_'+cmd, None) + if not method: + self.log('warning: not implemented method "ftp_%s"' %cmd) + self.cmd_not_understood(line) + else: + method(arg) # callback + + else: + # recognize "abor" command: + # ['\xff', '\xf4', '\xf2', 'A', 'B', 'O', 'R'] + # if map(ord, line.upper()) == [255, 244, 242, 65, 66, 79, 82]: + # self.ftp_ABOR("") + # return + if line.upper().find('ABOR') != -1: + self.ftp_ABOR("") + else: + self.cmd_not_understood(line) + + def handle_expt(self): + # I didn't well understood when and why it is called and I'm not sure what + # could I do here. asyncore documentation says: + # > Called when there is out of band (OOB) data for a socket connection. + # > This will almost never happen, as OOB is tenuously supported and rarely used. + # OOB? How do I have to manage that? + # I made a research but still can't know what to do. Even in SocketServer module + # OOB is an open unsolved problem. + # Anyway, I assume this as a bad event, so I close the current session. + self.debug("ftp_handler.handle_expt()") + self.close() + + def handle_error(self): + self.debug("ftp_handler.handle_error()") + f = StringIO.StringIO() + traceback.print_exc(file=f) + self.debug(f.getvalue()) + + asynchat.async_chat.close(self) + + def handle_close(self): + self.debug("ftp_handler.handle_close()") + self.close() + + def close(self): + self.debug("ftp_handler.close()") + + if self.dtp_server: + self.dtp_server.close() + self.dtp_server = None + + if self.data_channel: + self.data_channel.close() + self.data_channel = None + + self.log("Disconnected.") + asynchat.async_chat.close(self) + + + # --- callbacks + + def on_dtp_connection(self): + # Called every time data channel connects (does not matter + # if active or passive). Here we check for data queues. + # If we got data to send we just push it into data channel. + # If we got data to receive we enable data channel for receiving it. + + self.debug("ftp_handler.on_dtp_connection()") + + if self.dtp_server: + self.dtp_server.close() + self.dtp_server = None + + if self.out_producer_queue: + if self.out_producer_queue[1]: + self.log(self.out_producer_queue[1]) + self.data_channel.push_with_producer(self.out_producer_queue[0]) + self.out_producer_queue = None + + elif self.in_producer_queue: + if self.in_producer_queue[1]: + self.log(self.in_producer_queue[1]) + self.data_channel.file_obj = self.in_producer_queue[0] + self.data_channel.enable_receiving() + self.in_producer_queue = None + + def on_dtp_close(self): + # called every time close() method of dtp_handler() class + # is called. + + self.debug("ftp_handler.on_dtp_close()") + if self.data_channel: + self.data_channel = None + if self.quit_pending: + self.close_when_done() + + # --- utility + + def respond(self, resp): + self.push(resp + '\r\n') + self.logline('==> %s' % resp) + + def push_dtp_data(self, file_obj, msg=''): + # Called every time a RETR, LIST or NLST is received, push data into + # data channel. If data channel does not exists yet we queue up data + # to send later. Data will then be pushed into data channel when + # "on_dtp_connection()" method will be called. + + if self.data_channel: + self.respond("125 Data connection already open. Transfer starting.") + if msg: + self.log(msg) + self.data_channel.push_with_producer(file_obj) + else: + self.respond("150 File status okay. About to open data connection.") + self.debug("info: new producer queue added") + self.out_producer_queue = (file_obj, msg) + + def cmd_not_understood(self, line): + self.respond('500 Command "%s" not understood.' %line) + + def cmd_missing_arg(self): + self.respond("501 Syntax error: command needs an argument.") + + def log(self, msg): + log("[%s]@%s:%s %s" %(self.username, self.remote_ip, self.remote_port, msg)) + + def logline(self, msg): + logline("%s:%s %s" %(self.remote_ip, self.remote_port, msg)) + + def debug(self, msg): + debug(msg) + + + # --- ftp + + # --- connection + + def ftp_PORT(self, line): + try: + line = line.split(',') + ip = ".".join(line[:4]).replace(',','.') + port = (int(line[4]) * 256) + int(line[5]) + except: + self.respond("500 Invalid PORT format.") + return + + # FTP bouncing protection: drop if IP address does not match + # the client's IP address. + if ip != self.remote_ip: + self.respond("500 No FTP bouncing allowed.") + return + + # if more than one PORT is received we create a new data + # channel instance closing the older one + if self.data_channel: + asynchat.async_chat.close(self.data_channel) + active_dtp(ip, port, self) + + def ftp_PASV(self, line): + # if more than one PASV is received we create a new data + # channel instance closing the older one + if self.data_channel: + asynchat.async_chat.close(self.data_channel) + self.dtp_server = passive_dtp(self) + return + + if not self.dtp_ready: + self.dtp_server = passive_dtp(self) + else: + asynchat.async_chat.close(self.dtp_server) + self.dtp_server = passive_dtp(self) + + def ftp_QUIT(self, line): + if not self.msg_quit: + self.respond("221 Goodbye.") + else: + self.push("221-%s\r\n" %self.msg_quit) + self.respond("221 Goodbye.") + + # From RFC959 about 'QUIT' command: + # > This command terminates a USER and if file transfer is not + # > in progress, the server closes the control connection. If + # > file transfer is in progress, the connection will remain + # > open for result response and the server will then close it. + if not self.data_channel: + self.close_when_done() + else: + self.quit_pending = True + + + # --- data transferring + + def ftp_LIST(self, line): + # TODO: LIST/NLST commands are currently the mostly CPU-intensive blocking operations. + # A sort of cache could be implemented (take a look at dircache module). + + if line: + # some FTP clients (like Konqueror or Nautilus) erroneously use + # /bin/ls-like LIST formats (e.g. "LIST -l", "LIST -al" and so on...). + # If this happens we LIST the current working directory. + + if line.lower() in ("-a", "-l", "-al", "-la"): + path = self.fs.normalize(self.fs.cwd) + line = self.fs.cwd + else: + path = self.fs.normalize(line) + line = self.fs.translate(line) + else: + path = self.fs.normalize(self.fs.cwd) + line = self.fs.cwd + + if not self.fs.exists(path): + self.log('FAIL LIST "%s". No such directory.' %line) + self.respond('550 No such directory: "%s".' %line) + return + + try: + file_obj = self.fs.get_list_dir(path) + except OSError, err: + self.log('FAIL LIST "%s". %s: "%s".' % (line, err.strerror, err.filename)) + self.respond ('550 I/O server side error: %s' %err.strerror) + return + + self.push_dtp_data(file_obj, 'OK LIST "%s". Transfer starting.' %line) + + def ftp_NLST(self, line): + if line: + path = self.fs.normalize(line) + line = self.fs.translate(line) + else: + path = self.fs.normalize(self.fs.cwd) + line = self.fs.cwd + + if not self.fs.is_dir(path): + self.log('FAIL NLST "%s". No such directory.' %line) + self.respond('550 No such directory: "%s".' %line) + return + + try: + file_obj = self.fs.get_list_dir(path) + except OSError, err: + self.log('FAIL NLST "%s". %s: "%s".' % (line, err.strerror, err.filename)) + self.respond ('550 I/O server side error: %s' %err.strerror) + return + + file_obj = self.fs.get_nlst_dir(path) + self.push_dtp_data(file_obj, 'OK NLST "%s". Transfer starting.' %line) + + def ftp_RETR(self, line): + if not line: + self.cmd_missing_arg() + return + + file = self.fs.normalize(line) + + if not self.fs.is_file(file): + self.log('FAIL RETR "%s". No such file.' %line) + self.respond('550 No such file: "%s".' %line) + return + + if not self.authorizer.r_perm(self.username, file): + self.log('FAIL RETR "s". Not enough priviledges' %line) + self.respond ("553 Can't RETR: not enough priviledges.") + return + + try: + file_obj = open(file, 'rb') + except IOError, err: + self.log('FAIL RETR "%s". I/O error: %s' %(line, err.strerror)) + self.respond ('553 I/O server side error: %s' %err.strerror) + return + + if self.restart_position: + try: + file_obj.seek(self.restart_position) + except: + pass + self.restart_position = 0 + + self.push_dtp_data(file_obj, 'OK RETR "%s". Download starting.' %self.fs.translate(line)) + + def ftp_STOR(self, line, rwa='w', mode='b'): + if not line: + self.cmd_missing_arg() + return + + # A resume could occur in case of APPE or REST commands. + # In that case we have to open file object in different ways: + # STOR: rwa = 'w' + # APPE: rwa = 'a' + # REST: rwa = 'r+' (to permit seeking on file object) + + file = self.fs.normalize(line) + + if not self.authorizer.w_perm(self.username, os.path.split(file)[0]): + self.log('FAIL STOR "%s". Not enough priviledges' %line) + self.respond ("553 Can't STOR: not enough priviledges.") + return + + if self.restart_position: + rwa = 'r+' + + try: + file_obj = open(file, rwa + mode) + except IOError, err: + self.log('FAIL STOR "%s". I/O error: %s' %(line, err.strerror)) + self.respond ('553 I/O server side error: %s' %err.strerror) + return + + if self.restart_position: + try: + file_obj.seek(self.restart_position) + except: + pass + self.restart_position = 0 + + if self.data_channel: + self.respond("125 Data connection already open. Transfer starting.") + self.log('OK STOR "%s". Upload starting.' %self.fs.translate(line)) + self.data_channel.file_obj = file_obj + self.data_channel.enable_receiving() + else: + self.debug("info: new producer queue added.") + self.respond("150 File status okay. About to open data connection.") + self.in_producer_queue = (file_obj, 'OK STOR "%s". Upload starting.' %self.fs.translate(line)) + + def ftp_STOU(self, line): + "store a file with a unique name" + # note: RFC 959 prohibited STOU parameters, but this prohibition is obsolete. + # note2: RFC 959 wants ftpd to respond with code 250 but I've seen a + # lot of FTP servers responding with 125 or 150, and this is a better choice, imho, + # because STOU works just like STOR. + + # create file with a suggested name + if line: + file = (self.fs.normalize (line)) + if not self.fs.exists (file): + resp = line + else: + x = 0 + while 1: + file = self.fs.normalize (line + '.' + str(x)) + if not self.fs.exists(file): + resp = line + '.' + str(x) + break + else: + x += 1 + + # create file with a brand new name + else: + x = 0 + while 1: + file = self.fs.normalize (self.fs.cwd + '.' + str(x)) + if not self.fs.exists(file): + resp = '.' + str(x) + break + else: + x += 1 + + # now just acts like STOR excepting that restarting isn't allowed + if not self.authorizer.w_perm(self.username, os.path.split(file)[0]): + self.log('FAIL STOU "%s". Not enough priviledges' %line) + self.respond ("553 Can't STOU: not enough priviledges.") + return + try: + file_obj = open(file, 'wb') + except IOError, err: + self.log('FAIL STOU "%s". I/O error: %s' %(line, err.strerror)) + self.respond ('553 I/O server side error: %s' %err.strerror) + return + + if self.data_channel: + self.respond("125 %s" %resp) + self.log('OK STOU "%s". Upload starting.' %self.fs.translate(line)) + self.data_channel.file_obj = file_obj + self.data_channel.enable_receiving() + else: + self.debug("info: new producer queue added.") + self.respond("150 %s" %resp) + self.in_producer_queue = (file_obj, 'OK STOU "%s". Upload starting.' %self.fs.translate(line)) + + + def ftp_APPE(self, line): + if not line: + self.cmd_missing_arg() + return + self.ftp_STOR(line, rwa='a') + + def ftp_REST(self, line): + if not line: + self.cmd_missing_arg() + return + try: + value = int(line) + if value < 0: + raise + self.respond("350 Restarting at position %s. Now use RETR/STOR for resuming." %value) + self.restart_position = value + except: + self.respond("501 Invalid number of parameters.") + + def ftp_ABOR(self, line): + if self.dtp_server: + self.dtp_server.close() + self.dtp_server = None + + if self.data_channel: + self.data_channel.close() + self.data_channel = None + + self.log("ABOR received.") + self.respond('226 ABOR command successful.') + + + # --- authentication + + def ftp_USER(self, line): + if not line: + self.cmd_missing_arg() + return + # warning: we always treat anonymous user as lower-case string. + if line.lower() == "anonymous": + self.username = "anonymous" + else: + self.username = line + self.respond('331 Username ok, send passowrd.') + + def ftp_PASS(self, line): + # TODO - brute force protection: 'freeze'/'sleep' (without blocking the main loop) + # PI for a certain amount of time if authentication fails. + + if not self.username: + self.respond("503 Login with USER first") + return + + if self.username == 'anonymous': + line = '' + + if self.authorizer.has_user(self.username): + + if self.authorizer.validate_authentication(self.username, line): + if not self.msg_login: + self.respond("230 User %s logged in." %self.username) + else: + self.push("230-%s\r\n" %self.msg_login) + self.respond("230 Welcome.") + + self.authenticated = True + self.max_login_attempts[1] = 0 + self.fs.root = self.authorizer.get_home_dir(self.username) + self.log("User %s logged in." %self.username) + + else: + self.max_login_attempts[1] += 1 + + if self.max_login_attempts[0] == self.max_login_attempts[1]: + self.log("Maximum login attempts. Disconnecting.") + self.respond("530 Maximum login attempts. Disconnecting.") + self.close() + else: + self.respond("530 Authentication failed.") + self.username = "" + + else: + if self.username.lower() == 'anonymous': + self.respond("530 Anonymous access not allowed.") + self.log('Authentication failed: anonymous access not allowed.') + else: + self.respond("530 Authentication failed.") + self.log('Authentication failed: unknown username "%s".' %self.username) + self.username = "" + + def ftp_REIN(self, line): + self.authenticated = False + self.username = "" + self.max_login_attempts[1] = 0 + self.respond("230 Ready for new user.") + self.log("REIN account information was flushed.") + + + # --- filesystem operations + + def ftp_PWD(self, line): + self.respond('257 "%s" is the current directory.' %self.fs.cwd) + + def ftp_CWD(self, line): + if not line: + line = '/' + if self.fs.change_dir(line): + self.log('OK CWD "%s"' %self.fs.cwd) + self.respond('250 "%s" is the current directory.' %self.fs.cwd) + else: + self.respond("550 No such directory.") + + def ftp_CDUP(self, line): + if self.fs.cwd == '/': + self.respond('250 "/" is the current directory.') + else: + self.fs.cdup() + self.respond('257 "%s" is the current directory.' %self.fs.cwd) + self.log('OK CWD "%s"' %self.fs.cwd) + + def ftp_SIZE(self, line): + if not line: + self.cmd_missing_arg() + return + + size = self.fs.get_size(self.fs.normalize(line)) + if size >= 0: + self.log('OK SIZE "%s"' %self.fs.translate(line)) + self.respond("213 %s" %size) + else: + self.log('FAIL SIZE "%s". No such file.' %self.fs.translate(line)) + self.respond("550 No such file.") + + def ftp_MDTM(self, line): + # get file's last modification time (not documented inside RFC959 + # but used in a lot of ftpd implementations) + if not line: + self.cmd_missing_arg() + return + path = self.fs.normalize(line) + if not self.fs.is_file(path): + self.respond("550 No such file.") + else: + lmt = time.strftime("%Y%m%d%H%M%S", time.localtime(self.fs.get_lastm_time (path))) + self.respond("213 %s" %lmt) + + def ftp_MKD(self, line): + if not line: + self.cmd_missing_arg() + return + + path = self.fs.normalize(line) + + if not self.authorizer.w_perm(self.username, os.path.split(path)[0]): + self.log('FAIL MKD "%s". Not enough priviledges.' %line) + self.respond ("553 Can't MKD: not enough priviledges.") + return + + if self.fs.create_dir(path): + self.log('OK MKD "%s".' %self.fs.translate(line)) + self.respond("257 Directory created.") + else: + self.log('FAIL MKD "%s".' %self.fs.translate(line)) + self.respond("550 Can't create directory.") + + def ftp_RMD(self, line): + if not line: + self.cmd_missing_arg() + return + + path = self.fs.normalize(line) + + if path == self.fs.root: + self.respond("550 Can't remove root directory.") + return + + if not self.authorizer.w_perm(self.username, path): + self.log('FAIL RMD "%s". Not enough priviledges.' %line) + self.respond ("553 Can't RMD: not enough priviledges.") + return + + if self.fs.remove_dir(path): + self.log('OK RMD "%s".' %self.fs.translate(line)) + self.respond("250 Directory removed.") + else: + self.log('FAIL RMD "%s".' %self.fs.translate(line)) + self.respond("550 Can't remove directory.") + + def ftp_DELE(self, line): + if not line: + self.cmd_missing_arg() + return + + path = self.fs.normalize(line) + + if not self.authorizer.w_perm(self.username, path): + self.log('FAIL DELE "%s". Not enough priviledges.' % self.fs.translate(line)) + self.respond ("553 Can't DELE: not enough priviledges.") + return + + if self.fs.remove_file(path): + self.log('OK DELE "%s".' %self.fs.translate(line)) + self.respond("250 File removed.") + else: + self.log('FAIL DELE "%s".' %self.fs.translate(line)) + self.respond("550 Can't remove file.") + + def ftp_RNFR(self, line): + if not line: + self.cmd_missing_arg() + return + + if self.fs.exists(self.fs.normalize(line)): + self.fs.rnfr = self.fs.translate(line) + self.respond("350 Ready for destination name") + else: + self.respond("550 No such file/directory.") + + def ftp_RNTO(self, line): + # TODO - actually RNFR/RNTO commands could also be used to *move* a file/directory + # instead of just renaming them. RFC959 doesn't tell if this must be allowed or not + # (I believe not). Check about it. + + if not line: + self.cmd_missing_arg() + return + + if not self.fs.rnfr: + self.respond("503 Bad sequence of commands: use RNFR first.") + return + + if not self.authorizer.w_perm(self.username, self.fs.normalize(self.fs.rnfr)): + self.log('FAIL RNFR/RNTO "%s ==> %s". Not enough priviledges for renaming.' + %(self.fs.rnfr, self.fs.translate(line))) + self.respond ("553 Can't RNTO: not enough priviledges.") + self.fs.rnfr = None + return + + src = self.fs.normalize(self.fs.rnfr) + dst = self.fs.normalize(line) + + if self.fs.rename(src, dst): + self.log('OK RNFR/RNTO "%s ==> %s".' %(self.fs.rnfr, self.fs.translate(line))) + self.respond("250 Renaming ok.") + else: + self.log('FAIL RNFR/RNTO "%s ==> %s".' %(self.fs.rnfr, self.fs.translate(line))) + self.respond("550 Renaming failed.") + self.fs.rnfr = None + + # --- others + + def ftp_TYPE(self, line): + if not line: + self.cmd_missing_arg() + return + + line = line.lower() + if line in type_map: + self.respond("200 Type set to: %s." %type_map[line]) + self.current_type = line + else: + self.respond('550 Unknown / unsupported type "%s".' %line) + + def ftp_STRU(self, line): + # obsolete (backward compatibility with older ftp clients) + if not line: + self.cmd_missing_arg() + return + if line in ('f','F'): + self.respond ('200 File transfer structure set to: F.') + else: + self.respond ('504 Unimplemented STRU type.') + + def ftp_MODE(self, line): + # obsolete (backward compatibility with older ftp clients) + if not line: + self.cmd_missing_arg() + return + if line in ('s', 'S'): + self.respond('200 Trasfer mode set to: S') + else: + self.respond('504 Unimplemented MODE type.') + + def ftp_STAT(self, line): + self.push('211-%s %s status:\r\n' %(__pname__, __ver__)) + s = '\tConnected to: %s:%s\r\n' %(self.socket.getpeername()[0], self.socket.getpeername()[1]) + s+= '\tLogged in as: %s\r\n' %self.username + s+= '\tCurrent type: %s\r\n' %type_map[self.current_type] + s+= '\tCurrent STRUcture: File\r\n' + s+= '\tTransfer MODE: Stream\r\n' + self.push(s) + self.respond("211 End of status.") + + def ftp_NOOP(self, line): + self.respond("250 I succesfully done nothin'.") + + def ftp_SYST(self, line): + # we always assume that the running system is unix(like) + # even if it's different because we always respond to LIST + # command with a "/bin/ls -al" like output. + self.respond("215 UNIX Type: L8") + + def ftp_ALLO(self, line): + # obsolete (always respond with 202) + self.respond("202 ALLO command succesful.") + + def ftp_HELP(self, line): + self.push('214-The following commands are recognized:\r\n') + self.push(helper_string) + self.push("* argument required.\r\n") + self.respond("214 Help command succesful.") + + # --- support for deprecated cmds + + def ftp_XCUP(self, line): + self.ftp_CDUP(line) + + def ftp_XCWD(self, line): + self.ftp_CWD(line) + + def ftp_XMKD(self, line): + self.ftp_MKD(line) + + def ftp_XPWD(self, line): + self.ftp_PWD(line) + + def ftp_XRMD(self, line): + self.ftp_RMD(line) + + + +class ftp_server(asynchat.async_chat): + """The base class for the backend.""" + + def __init__(self, address, handler): + asynchat.async_chat.__init__(self) + self.address = address + self.handler = handler + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.bind(self.address) + self.listen(5) + + def __del__(self): + debug("ftp_server.__del__()") + + def serve_forever(self): + log("Serving FTP on %s:%s." %self.socket.getsockname()) + if os.name == 'posix': + self.set_reuse_addr() + # here we try to use poll(), if it exists, else we'll use select() + asyncore.loop(timeout=1, use_poll=True) + + def handle_accept(self): + debug("handle_accept()") + sock_obj, addr = self.accept() + log("[]%s:%s connected." %addr) + handler = self.handler().handle(sock_obj) + + def writable(self): + return 0 + + def readable(self): + return self.accepting + + def handle_error(self): + debug("ftp_server.handle_error()") + f = StringIO.StringIO() + traceback.print_exc(file=f) + debug(f.getvalue()) + self.close() + + +class passive_dtp(asynchat.async_chat): + "Base class for passive-DTP backend" + + def __init__(self, cmd_channel): + asynchat.async_chat.__init__(self) + + self.cmd_channel = cmd_channel + self.debug = self.cmd_channel.debug + + ip = self.cmd_channel.getsockname()[0] + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + # by using 0 as port number value we let socket choose a free random port + self.bind((ip, 0)) + self.listen(5) + self.cmd_channel.dtp_ready = True + port = self.socket.getsockname()[1] + self.cmd_channel.respond('227 Entering passive mode (%s,%d,%d)' %( + ip.replace('.', ','), + port / 256, + port % 256 + )) + + def __del__(self): + debug("passive_dtp.__del__()") + + + # --- connection / overridden + + def handle_accept(self): + sock_obj, addr = self.accept() + + # PASV connection theft protection: check the origin of data connection. + # We have to drop the incoming data connection if remote IP address + # does not match the client's IP address. + if self.cmd_channel.remote_ip != addr[0]: + log("info: PASV connection theft attempt occurred from %s:%s." %(addr[0], addr[1])) + try: + # sock_obj.send('500 Go hack someone else, dude.') + sock_obj.close() + except: + pass + else: + debug("passive_dtp.handle_accept()") + self.cmd_channel.dtp_ready = False + handler = dtp_handler(sock_obj, self.cmd_channel) + self.cmd_channel.data_channel = handler + self.cmd_channel.on_dtp_connection() + # self.close() + + def writable(self): + return 0 + + def handle_expt(self): + debug("passive_dtp.handle_expt()") + self.close() + + def handle_error(self): + debug("passive_dtp.handle_error()") + f = StringIO.StringIO() + traceback.print_exc(file=f) + debug(f.getvalue()) + self.close() + + def handle_close(self): + debug("passive_dtp.handle_close()") + self.close() + + def close(self): + debug("passive_dtp.close()") + self.del_channel() + self.socket.close() + + + +class active_dtp(asynchat.async_chat): + "Base class for active-DTP backend" + + def __init__(self, ip, port, cmd_channel): + asynchat.async_chat.__init__(self) + self.cmd_channel = cmd_channel + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + try: + self.connect((ip, port)) + except: + self.cmd_channel.respond("500 Can't connect to %s:%s." %(ip, port)) + self.close() + + def __del__(self): + debug("active_dtp.__del__()") + + + # --- connection / overridden + + def handle_connect(self): + debug("active_dtp.handle_connect()") + self.cmd_channel.respond('200 PORT command successful.') + handler = dtp_handler(self.socket, self.cmd_channel) + self.cmd_channel.data_channel = handler + self.cmd_channel.on_dtp_connection() + # self.close() --> (done automatically) + + + def handle_expt(self): + debug("active_dtp.handle_expt()") + self.cmd_channel.respond ("425 Can't establish data connection.") + self.close() + + def handle_error(self): + debug("active_dtp.handle_error()") + f = StringIO.StringIO() + traceback.print_exc(file=f) + debug(f.getvalue()) + self.close() + + def handle_close(self): + debug("active_dtp.handle_close()") + self.close() + + def close(self): + debug("active_dtp.close()") + self.del_channel() + self.socket.close() + + + +class dtp_handler(asynchat.async_chat): + """class handling server-data-transfer-process (server-DTP, see RFC 959) + managing data-transfer operations. + """ + + def __init__(self, sock_obj, cmd_channel): + asynchat.async_chat.__init__(self, conn=sock_obj) + + self.cmd_channel = cmd_channel + + self.file_obj = None + self.in_buffer_size = 8192 + self.out_buffer_size = 8192 + + self.enable_receive = False + self.transfer_finished = False + self.tot_bytes_sent = 0 + self.tot_bytes_received = 0 + self.data_wrapper = self.binary_data_wrapper + + def log(self, msg): + log(msg) + + def debug(self, msg): + debug(msg) + + def __del__(self): + debug("dtp_handler.__del__()") + + + # --- utility methods + + def enable_receiving(self): + if self.cmd_channel.current_type == 'a': + self.data_wrapper = self.ASCII_data_wrapper + else: + self.data_wrapper = self.binary_data_wrapper + self.enable_receive = True + + def ASCII_data_wrapper(self, data): + return data.replace('\r\n', os.linesep) + + def binary_data_wrapper(self, data): + return data + + # --- connection / overridden + + def readable (self): + return (len(self.ac_in_buffer) <= self.ac_in_buffer_size) and self.enable_receive + + def writable(self): + return len(self.ac_out_buffer) or len(self.producer_fifo) or (not self.connected) + + def push_with_producer (self, file_obj): + self.file_obj = file_obj + producer = file_producer (self.file_obj, self.cmd_channel.current_type) + self.producer_fifo.push (producer) + self.close_when_done() + self.initiate_send() + + def initiate_send (self): + obs = self.ac_out_buffer_size + # try to refill the buffer + if (len (self.ac_out_buffer) < obs): + self.refill_buffer() + + if self.ac_out_buffer and self.connected: + # try to send the buffer + try: + num_sent = self.send (self.ac_out_buffer[:obs]) + if num_sent: + self.ac_out_buffer = self.ac_out_buffer[num_sent:] + + # --- edit + self.tot_bytes_sent += num_sent + # --- /edit + + except socket.error, why: + self.handle_error() + return + + def refill_buffer (self): + while 1: + if len(self.producer_fifo): + p = self.producer_fifo.first() + if p is None: + if not self.ac_out_buffer: + self.producer_fifo.pop() + + # --- edit + self.transfer_finished = True + # --- /edit + + self.close() + return + elif isinstance(p, str): + self.producer_fifo.pop() + self.ac_out_buffer = self.ac_out_buffer + p + return + data = p.more() + if data: + self.ac_out_buffer = self.ac_out_buffer + data + return + else: + self.producer_fifo.pop() + else: + return + + def handle_read (self): + chunk = self.recv(self.in_buffer_size) + self.tot_bytes_received += len(chunk) + if not chunk: + self.transfer_finished = True + # self.close() <-- asyncore.recv() already do that... + return + + # --- Writing on file + # While we're writing on the file an exception could occur in case that + # filesystem gets full but this rarely happens and a "try/except" + # statement seems wasted to me. + # Anyway if this happens we let handle_error() method handle this exception. + # Remote client will just receive a generic 426 response then it will be + # disconnected. + self.file_obj.write(self.data_wrapper(chunk)) + + def handle_expt(self): + debug("dtp_handler.handle_expt()") + self.close() + + def handle_error(self): + debug("dtp_handler.handle_error()") + f = StringIO.StringIO() + traceback.print_exc(file=f) + debug(f.getvalue()) + self.close() + + def handle_close(self): + debug("dtp_handler.handle_close()") + self.transfer_finished = True + self.close() + + def close(self): + debug("dtp_handler.close()") + tot_bytes = self.tot_bytes_sent + self.tot_bytes_received + + # If we used channel for receiving we assume that transfer is finished + # when client close connection, if we used channel for sending we have + # to check that all data has been sent (responding with 226) or not + # (responding with 426). + if self.enable_receive: + self.cmd_channel.respond("226 Transfer complete.") + self.cmd_channel.log("Trasfer complete. %d bytes transmitted." %tot_bytes) + else: + if self.transfer_finished: + self.cmd_channel.respond("226 Transfer complete.") + self.cmd_channel.log("Trasfer complete. %d bytes transmitted." %tot_bytes) + else: + self.cmd_channel.respond("426 Connection closed, transfer aborted.") + self.cmd_channel.log("Trasfer aborted. %d bytes transmitted." %tot_bytes) + + try: self.file_obj.close() + except: pass + + # to permit gc... + del self.data_wrapper + + asynchat.async_chat.close(self) + + self.cmd_channel.data_channel = None + self.cmd_channel.on_dtp_close() + + + +# --- file producer + +# I get it from Sam Rushing's Medusa-framework. +# It's like asynchat.simple_producer class excepting that it works +# with file(-like) objects instead of strings. + +class file_producer: + "Producer wrapper for file[-like] objects." + + out_buffer_size = 65536 + + def __init__ (self, file, type=''): + self.done = 0 + self.file = file + if type == 'a': + self.data_wrapper = self.ASCII_data_wrapper + else: + self.data_wrapper = self.binary_data_wrapper + + def more (self): + if self.done: + return '' + else: + data = self.data_wrapper() + if not data: + # to permit gc... + self.file = self.data_wrapper = None + self.done = 1 + return '' + else: + return data + + def binary_data_wrapper(self): + return self.file.read (self.out_buffer_size) + + def ASCII_data_wrapper(self): + return self.file.read (self.out_buffer_size).replace(os.linesep, '\r\n') + + + +# --- filesystem + +def __test_compatibility(): + try: + # Availability Macintosh, Unix, Windows. + os.rmdir + os.mkdir + os.remove + os.rename + os.listdir + os.stat + # Availability Python >= 1.5.2 + os.path.getsize + os.path.getmtime + except AttributeError: + raise error, "Incompatible Python release." +# __test_compatibility() + + +class abstracted_fs: + + def __init__(self): + self.root = None + self.cwd = '/' + self.rnfr = None + + # def __del__(self): + # debug("abstracted_fs.__del__()") + + def normalize(self, path): + if path == '': + return '' + # absolute pathname + elif path.startswith('/'): + if path == '/': + return self.root + else: + return self.root + os.path.normpath(path) + # relative pathname + else: + if self.cwd == '/': + return self.root + os.path.normpath(self.cwd) + os.path.normpath(path) + else: + return self.root + os.path.normpath(self.cwd) + os.path.normpath('/' + path) + + def translate(self, path): + if not path: + return self.cwd + # absolute pathname + elif path.startswith('/'): + if path.endswith('/') and len(path) > 1: + return path[:-1] + else: + return path + # relative pathname + else: + if self.cwd == '/': + return self.cwd + path + else: + return self.cwd + '/' + path + + def exists(self, path): + return os.path.exists(path) + + def is_file(self, file): + if not self.exists(file): + return 0 + return os.path.isfile(file) + + def is_dir(self, path): + if not self.exists(path): + return 0 + return os.path.isdir(path) + + def change_dir(self, line): + if line == '/': + self.cwd = '/' + return 1 + else: + path = self.normalize(line) + if self.is_dir(path): + self.cwd = self.translate(line) + return 1 + else: + return 0 + + def cdup(self): + parent = os.path.split(self.cwd)[0] + self.cwd = parent + + def create_dir(self, path): + try: + os.mkdir(path) + return 1 + except: + return 0 + + def remove_dir(self, path): + try: + os.rmdir(path) + return 1 + except: + return 0 + + def remove_file(self, path): + try: + os.remove(path) + return 1 + except: + return 0 + + def get_size(self, path): + try: + return os.path.getsize(path) + except: + return -1 + + def get_lastm_time(self, path): + try: + return os.path.getmtime(path) + except: + return 0 + + def rename(self, src, dst): + try: + os.rename(src, dst) + return 1 + except: + return 0 + + def get_nlst_dir(self, path): + # ** warning: CPU-intensive blocking operation. You could want to override this method. + + f = StringIO.StringIO() + # if this fails we handle exception in ftp_handler class + listing = os.listdir(path) + for elem in listing: + f.write(elem + '\r\n') + f.seek(0) + return f + + def get_list_dir(self, path): + 'Emulates unix "ls" command' + + # ** warning: CPU-intensive blocking operation. You could want to override this method. + + # For portability reasons permissions, hard links numbers, owners and groups listed + # by this method are static and unreliable but it shouldn't represent a problem for + # most ftp clients around. + # If you want reliable values on unix systems override this method and use other attributes + # provided by os.stat() + # + # How LIST appears to client: + # -rwxrwxrwx 1 owner group 7045120 Sep 02 3:47 music.mp3 + # drwxrwxrwx 1 owner group 0 Aug 31 18:50 e-books + # -rwxrwxrwx 1 owner group 380 Sep 02 3:40 module.py + + # if path is a file we return information about it + if not self.is_dir(path): + root, filename = os.path.split(path) + path = root + listing = [filename] + else: + # if this fails we handle exception in ftp_handler class + listing = os.listdir(path) + + f = StringIO.StringIO() + for obj in listing: + name = os.path.join(path, obj) + stat = os.stat(name) + + # stat.st_mtime could fail (-1) if file's last modification time is + # too old, in that case we return local time as last modification time. + try: + mtime = time.strftime("%b %d %H:%M", time.localtime(stat.st_mtime)) + except ValueError: + mtime = time.strftime("%b %d %H:%M") + + + if os.path.isfile(name) or os.path.islink(name): + f.write("-rw-rw-rw- 1 owner group %15s %s %s\r\n" %( + stat.st_size, + mtime, + obj)) + else: + f.write("drwxrwxrwx 1 owner group %15s %s %s\r\n" %( + '0', # no size + mtime, + obj)) + f.seek(0) + return f + + +def test(): + # cmd line usage (provide a read-only anonymous ftp server): + # python -m pyftpdlib.FTPServer + authorizer = dummy_authorizer() + authorizer.add_anonymous(os.getcwd()) + ftp_handler.authorizer = authorizer + address = ('', 21) + ftpd = ftp_server(address, ftp_handler) + ftpd.serve_forever() + + +if __name__ == '__main__': + test()
pyftpdlib/FTPServer.pyc+0 −0 addedpyftpdlib/__init__.py+0 −0 addedpyftpdlib/__init__.pyc+0 −0 addedREADME+18 −0 added@@ -0,0 +1,18 @@ +Documentation +============= + + - Check doc/pyftpdlib.html. + + +Requirements +============ + + - Python 2.2 or higher. + + +Install +======= + + - Just run: + > python setup.py install +
setup.py+25 −0 added@@ -0,0 +1,25 @@ +#!/usr/bin/env python +# setup.py + +from distutils.core import setup + +setup(name='pyftpdlib', + version="0.1.1", + author='billiejoex', + author_email='billiejoex@gmail.com', + maintainer='billiejoex', + maintainer_email='billiejoex@gmail.com', + url='http://billiejoex.altervista.org', + description='High-level asynchronous FTP server library', + classifiers=[ + 'Development Status :: Beta', + 'Environment :: Networking', + 'Intended Audience :: Network programmers', + 'License :: GNU', + 'Operating System :: MacOS :: MacOS X', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: POSIX', + 'Programming Language :: Python', + ], + packages = ['pyftpdlib'], + )
test/test_ftpd.py+471 −0 added@@ -0,0 +1,471 @@ +#!/usr/bin/env python +# test_ftpd.py + +# ====================================================================== +# Copyright (C) 2007 billiejoex <billiejoex@gmail.com> +# +# All Rights Reserved +# +# Permission to use, copy, modify, and distribute this software and +# its documentation for any purpose and without fee is hereby +# granted, provided that the above copyright notice appear in all +# copies and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of +# billiejoex not be used in advertising or publicity pertaining to +# distribution of the software without specific, written prior +# permission. +# +# billiejoex DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN +# NO EVENT billiejoex BE LIABLE FOR ANY SPECIAL, INDIRECT OR +# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# ====================================================================== + + +import threading +import unittest +import socket +import os +import atexit +import time +import tempfile +import ftplib +from pyftpdlib import FTPServer + +__revision__ = '2 (pyftpdlib 0.1.1)' + +# TODO: +# - test ABOR +# - test QUIT while a transfer is in progress +# - test data transfer in ASCII and binary MODE + +class test_classes(unittest.TestCase): + + def test_abstracetd_fs(self): + ae = self.assertEquals + fs = FTPServer.abstracted_fs() + + # translate method + fs.cwd = '/' + ae(fs.translate(''), '/') + ae(fs.translate('/'), '/') + ae(fs.translate('a'), '/a') + ae(fs.translate('/a'), '/a') + ae(fs.translate('a/b'), '/a/b') + fs.cwd = '/sub' + ae(fs.translate(''), '/sub') + ae(fs.translate('a'), '/sub/a') + ae(fs.translate('a/b'), '/sub/a/b') + ae(fs.translate('//'), '/') + ae(fs.translate('/a/'), '/a') + + # normalize method + if os.sep == '/': + fs.root = '/home/user' + fs.cwd = '/' + ae(fs.normalize('/'), '/home/user') + ae(fs.normalize('a'), '/home/user/a') + ae(fs.normalize('/a'), '/home/user/a') + ae(fs.normalize('a/b'), '/home/user/a/b') + ae(fs.normalize('/a/b'), '/home/user/a/b') + fs.cwd = '/sub' + ae(fs.normalize('a'), '/home/user/sub/a') + ae(fs.normalize('a/b'), '/home/user/sub/a/b') + ae(fs.normalize('/a'), '/home/user/a') + ae(fs.normalize('/'), '/home/user') + + elif os.sep == '\\': + fs.root = r'C:\dir' + fs.cwd = '/' + ae(fs.normalize('/'), r'C:\dir') + ae(fs.normalize('a'), r'C:\dir\a') + ae(fs.normalize('/a'), r'C:\dir\a') + ae(fs.normalize('a/b'), r'C:\dir\a\b') + ae(fs.normalize('/a/b'), r'C:\dir\a\b') + fs.cwd = '/sub' + ae(fs.normalize('a'), r'C:\dir\sub\a') + ae(fs.normalize('a/b'), r'C:\dir\sub\a\b') + ae(fs.normalize('/a'), r'C:\dir\a') + ae(fs.normalize('/'), r'C:\dir') + + def test_dummy_authorizer(self): + auth = FTPServer.dummy_authorizer() + auth.user_table = {} + if os.sep == '\\': + home = 'C:\\' + elif os.sep == '/': + home = '/tmp' + else: + raise Exception, 'Not supported system' + # raise exc if path does not exist + self.assertRaises(AssertionError, auth.add_user, 'ftpuser', '12345', 'ox:\\?', perm=('r', 'w')) + self.assertRaises(AssertionError, auth.add_anonymous, 'ox:\\?') + # raise exc if user already exists + auth.add_user('ftpuser', '12345', home, perm=('r', 'w')) + self.assertRaises(FTPServer.error, auth.add_user, 'ftpuser', '12345', home, perm=('r', 'w')) + # ...even anonymous + auth.add_anonymous(home) + self.assertRaises(FTPServer.error, auth.add_anonymous, home) + # raise on wrong permission + self.assertRaises(FTPServer.error, auth.add_user, 'ftpuser2', '12345', home, perm=('x')) + del auth.user_table['anonymous'] + self.assertRaises(FTPServer.error, auth.add_anonymous, home, perm=('w')) + self.assertRaises(FTPServer.error, auth.add_anonymous, home, perm=('%&')) + self.assertRaises(FTPServer.error, auth.add_anonymous, home, perm=(None)) + auth.add_anonymous(home, perm=('')) + # raise on 'w' permission given to anonymous user + self.assertRaises(FTPServer.error, auth.add_anonymous, home, perm=('w')) + + +class ftp_authentication(unittest.TestCase): + "test: USER, PASS, REIN" + + def setUp(self): + global ftp + ftp = ftplib.FTP() + ftp.connect(host=host, port=port) + + def tearDown(self): + ftp.close() + + def test_auth_ok(self): + ftp.login(user=user, passwd=pwd) + + def test_auth_failed(self): + self.failUnlessRaises(ftplib.error_perm, ftp.login, user=user, passwd='wrong') + + def test_anon_auth(self): + ftp.login(user='anonymous', passwd='anon@') + ftp.login(user='AnonYmoUs', passwd='anon@') + ftp.login(user='anonymous', passwd='') + + def test_rein(self): + self.failUnlessRaises(ftplib.error_perm, ftp.sendcmd, 'pwd') + ftp.login(user='anonymous', passwd='anon@') + ftp.sendcmd('pwd') + ftp.sendcmd('rein') + self.failUnlessRaises(ftplib.error_perm, ftp.sendcmd, 'pwd') + + def test_max_auth(self): + # if authentication fails for 3 times ftpd disconnect us. + # we can check if this happen by using ftp.sendcmd() on the 'dead' socket object. + # If socket object is really dead it should be raised + # socket.error exception (Windows) or EOFError exception (Linux). + self.failUnlessRaises(ftplib.error_perm, ftp.login, user=user, passwd='wrong') + self.failUnlessRaises(ftplib.error_perm, ftp.login, user=user, passwd='wrong') + self.failUnlessRaises(ftplib.error_perm, ftp.login, user=user, passwd='wrong') + self.failUnlessRaises((socket.error, EOFError), ftp.sendcmd, '') + + +class ftp_dummy_cmds(unittest.TestCase): + "test: TYPE, STRU, MODE, STAT, NOOP, SYST, ALLO, HELP" + + def setUp(self): + global ftp + ftp = ftplib.FTP() + ftp.connect(host=host, port=port) + ftp.login(user=user, passwd=pwd) + + def tearDown(self): + ftp.close() + + def test_type(self): +## for _type in ('a', 'i'): +## ftp.sendcmd('type %s' %_type) +## ftp.sendcmd('type %s' %_type.upper()) + self.failUnlessRaises(ftplib.error_perm, ftp.sendcmd, 'type') + self.failUnlessRaises(ftplib.error_perm, ftp.sendcmd, 'type x') + + def test_stru(self): + ftp.sendcmd('stru f') + ftp.sendcmd('stru F') + self.failUnlessRaises(ftplib.error_perm, ftp.sendcmd, 'stru') + self.failUnlessRaises(ftplib.error_perm, ftp.sendcmd, 'stru x') + + def test_mode(self): + ftp.sendcmd('mode s') + ftp.sendcmd('mode S') + self.failUnlessRaises(ftplib.error_perm, ftp.sendcmd, 'mode') + self.failUnlessRaises(ftplib.error_perm, ftp.sendcmd, 'mode x') + + def test_stat(self): + ftp.sendcmd('stat') + + def test_noop(self): + ftp.sendcmd('noop') + + def test_syst(self): + ftp.sendcmd('syst') + + def test_allo(self): + ftp.sendcmd('allo') + + def test_help(self): + ftp.sendcmd('help') + + def test_quit(self): + ftp.sendcmd('quit') + + +class ftp_fs_operations(unittest.TestCase): + "test: PWD, CWD, CDUP, SIZE, RNFR, RNTO, DELE, MKD, RMD, MDTM" + + def test_it(self): + ftp = ftplib.FTP() + ftp.connect(host=host, port=port) + ftp.login(user=user, passwd=pwd) + ftp.sendcmd('pwd') + ftp.sendcmd('cwd') + ftp.sendcmd('cdup') + f = open(os.path.join(home, '1.tmp'), 'w+') + f.write('x' * 123) + f.close() + self.assertEqual (ftp.sendcmd('size 1.tmp')[4:], '123') + ftp.sendcmd('mdtm 1.tmp') + ftp.sendcmd('rnfr 1.tmp') + ftp.sendcmd('rnto 2.tmp') + ftp.sendcmd('dele 2.tmp') + ftp.sendcmd('mkd 1') + ftp.sendcmd('mkd 1/2') + self.assertRaises(ftplib.error_perm, ftp.sendcmd, 'rmd a') + ftp.sendcmd('rmd 1/2') + ftp.sendcmd('rmd 1') + self.assertRaises(ftplib.error_perm, ftp.sendcmd, 'rmd /') + ftp.close() + +class ftp_retrieve_data(unittest.TestCase): + "test: RETR, REST, LIST, NLST" + def setUp(self): + global ftp + ftp = ftplib.FTP() + ftp.connect(host=host, port=port) + ftp.login(user=user, passwd=pwd) + + def tearDown(self): + ftp.close() + + def test_retr(self): + data = 'abcde12345' * 100000 + f1 = open(os.path.join(home, '1.tmp'), 'wb') + f1.write(data) + f1.close() + f2 = open(os.path.join(home, '2.tmp'), "w+b") + ftp.retrbinary("retr 1.tmp", f2.write) + f2.seek(0) + self.assertEqual(hash(data), hash (f2.read())) + f2.close() + os.remove(os.path.join(home, '1.tmp')) + os.remove(os.path.join(home, '2.tmp')) + + def test_restore_on_retr(self): + data = 'abcde12345' * 100000 + f1 = open(os.path.join(home, '1.tmp'), 'wb') + f1.write(data) + f1.close() + f2 = open(os.path.join(home, '2.tmp'), "wb") + + # look at ftplib.FTP.retrbinary method to understand this mess + ftp.voidcmd('TYPE I') + conn = ftp.transfercmd('retr 1.tmp', rest=None) + bytes_recv = 0 + while 1: + chunk = conn.recv(8192) + # stop transfer while it isn't finished yet + if bytes_recv >= 524288: # 2^19 + break + elif not chunk: + break + f2.write(chunk) + bytes_recv += len(chunk) + conn.close() + # trasnfer isn't finished yet so ftpd should respond with 426 + self.failUnlessRaises(ftplib.error_temp, ftp.voidresp) + f2.close() + + # resuming + ftp.sendcmd('rest %s' %bytes_recv) + f2 = open(os.path.join(home, '2.tmp'), 'a+') + ftp.retrbinary("retr 1.tmp", f2.write) + f2.seek(0) + self.assertEqual(hash(data), hash (f2.read())) + f2.close() + os.remove(os.path.join(home, '1.tmp')) + os.remove(os.path.join(home, '2.tmp')) + + def test_rest(self): + # just test rest's semantic without using data-transfer + ftp.sendcmd('rest 3123') + self.failUnlessRaises(ftplib.error_perm, ftp.sendcmd, 'rest') + self.failUnlessRaises(ftplib.error_perm, ftp.sendcmd, 'rest str') + self.failUnlessRaises(ftplib.error_perm, ftp.sendcmd, 'rest -1') + + def test_list(self): + l = [] + f = open(os.path.join(home, '1.tmp'), 'w') + f.close() + ftp.retrlines('LIST 1.tmp', l.append) + self.assertEqual(len(l), 1) + os.remove(os.path.join(home, '1.tmp')) + l = [] + l1, l2, l3, l4 = [], [], [], [] + ftp.retrlines('LIST', l.append) + ftp.retrlines('LIST -a', l1.append) + ftp.retrlines('LIST -l', l2.append) + ftp.retrlines('LIST -al', l3.append) + ftp.retrlines('LIST -la', l4.append) + x = [l, l1, l2, l3, l4] + for i in range(0,4): + self.assertEqual(x[i], x[i+1]) + + def test_nlst(self): + l = [] + ftp.retrlines('NLST', l.append) + self.failUnlessRaises(ftplib.error_perm, ftp.retrlines, 'NLST 1.tmp', l.append) + + +class ftp_store_data(unittest.TestCase): + "test: STOR, STOU, APPE, REST" + def setUp(self): + global ftp + ftp = ftplib.FTP() + ftp.connect(host=host, port=port) + ftp.login(user=user, passwd=pwd) + + def tearDown(self): + ftp.close() + + def test_stor(self): + data = 'abcde12345' * 100000 + f1 = tempfile.TemporaryFile(mode='w+b') + f1.write(data) + f1.seek(0) + ftp.storbinary('stor 1.tmp', f1) + f1.close() + f2 = open(os.path.join(home, '2.tmp'), "w+b") + ftp.retrbinary("retr 1.tmp", f2.write) + f2.seek(0) + self.assertEqual(hash(data), hash (f2.read())) + f2.close() + os.remove(os.path.join(home, '1.tmp')) + os.remove(os.path.join(home, '2.tmp')) + + def test_stou(self): + data = 'abcde12345' * 100000 + f1 = tempfile.TemporaryFile(mode='w+b') + f1.write(data) + f1.seek(0) + + ftp.voidcmd('TYPE I') + filename = ftp.sendcmd('stou')[4:] + sock = ftp.makeport() + conn, sockaddr = sock.accept() + while 1: + buf = f1.read(8192) + if not buf: + break + conn.sendall(buf) + conn.close() + ftp.voidresp() + f1.close() + os.remove (os.path.join(home, filename)) + + def test_appe(self): + data1 = 'abcde12345' * 100000 + f1 = tempfile.TemporaryFile(mode='w+b') + f1.write(data1) + f1.seek(0) + ftp.storbinary('stor 1.tmp', f1) + + data2 = 'fghil67890' * 100000 + f1.write(data2) + size = ftp.sendcmd('size 1.tmp')[4:] + f1.seek(int(size)) + ftp.storbinary('appe 1.tmp', f1) + + f2 = open(os.path.join(home, '2.tmp'), "w+b") + ftp.retrbinary("retr 1.tmp", f2.write) + f2.seek(0) + self.assertEqual(hash(data1 + data2), hash (f2.read())) + f1.close() + f2.close() + os.remove(os.path.join(home, '1.tmp')) + os.remove(os.path.join(home, '2.tmp')) + + def test_rest_on_stor(self): + data = 'abcde12345' * 100000 + f1 = tempfile.TemporaryFile(mode='w+b') + f1.write(data) + f1.seek(0) + + ftp.voidcmd('TYPE I') + conn = ftp.transfercmd('stor 1.tmp', rest=None) + bytes_sent = 0 + while 1: + chunk = f1.read(8192) + conn.send(chunk) + bytes_sent += len(chunk) + # stop transfer while it isn't finished yet + if bytes_sent >= 524288: # 2^19 + break + elif not chunk: + break + conn.close() + ftp.voidresp() + + ftp.sendcmd('rest %s' %bytes_sent) + ftp.storbinary('appe 1.tmp', f1) + f2 = open(os.path.join(home, '2.tmp'), "w+b") + ftp.retrbinary("retr 1.tmp", f2.write) + f1.seek(0) + f2.seek(0) + self.assertEqual(hash(data), hash (f2.read())) + f1.close() + f2.close() + + +def run(): + class ftpd(threading.Thread): + def __init__(self): + threading.Thread.__init__(self) + + def run(self): + def logger(msg): + pass + def linelogger(msg): + pass + def debugger(msg): + pass + + FTPServer.log = logger + FTPServer.logline = linelogger + FTPServer.debug = debugger + authorizer = FTPServer.dummy_authorizer() + authorizer.add_user(user, pwd, home, perm=('r', 'w')) + authorizer.add_anonymous(home) + ftp_handler = FTPServer.ftp_handler + ftp_handler.authorizer = authorizer + address = (host, port) + ftpd = FTPServer.ftp_server(address, ftp_handler) + ftpd.serve_forever() + + def exit_fun(): + os._exit(0) + atexit.register(exit_fun) + + f = ftpd() + f.start() + time.sleep(0.3) + unittest.main() + + +host = '127.0.0.1' +port = 54321 +user = 'user' +pwd = '12345' +home = os.getcwd() + +if __name__ == '__main__': + run()
TODO+5 −0 added@@ -0,0 +1,5 @@ + +TODO +==== + + - Check pyftpdlib/FTPServer.py \ No newline at end of file
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-gh7c-cg3x-pmcrghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2007-6738ghsaADVISORY
- code.google.com/p/pyftpdlib/source/browse/trunk/HISTORYnvdWEB
- github.com/giampaolo/pyftpdlib/commit/d171bdc4ef7ac769671946a8a3e5eaafc39a9202ghsaWEB
- github.com/pypa/advisory-database/tree/main/vulns/pyftpdlib/PYSEC-2010-22.yamlghsaWEB
News mentions
0No linked articles in our index yet.