diff options
author | Zeno Albisser <zeno.albisser@digia.com> | 2013-08-15 21:46:11 +0200 |
---|---|---|
committer | Zeno Albisser <zeno.albisser@digia.com> | 2013-08-15 21:46:11 +0200 |
commit | 679147eead574d186ebf3069647b4c23e8ccace6 (patch) | |
tree | fc247a0ac8ff119f7c8550879ebb6d3dd8d1ff69 /chromium/third_party/pyftpdlib | |
download | qtwebengine-chromium-679147eead574d186ebf3069647b4c23e8ccace6.tar.gz |
Initial import.
Diffstat (limited to 'chromium/third_party/pyftpdlib')
32 files changed, 7661 insertions, 0 deletions
diff --git a/chromium/third_party/pyftpdlib/OWNERS b/chromium/third_party/pyftpdlib/OWNERS new file mode 100644 index 00000000000..b7565e764ca --- /dev/null +++ b/chromium/third_party/pyftpdlib/OWNERS @@ -0,0 +1,2 @@ +phajdan.jr@chromium.org +wtc@chromium.org diff --git a/chromium/third_party/pyftpdlib/README.chromium b/chromium/third_party/pyftpdlib/README.chromium new file mode 100644 index 00000000000..7780c6ade62 --- /dev/null +++ b/chromium/third_party/pyftpdlib/README.chromium @@ -0,0 +1,15 @@ +Name: Python FTP server library +Short Name: pyftpdlib +URL: http://code.google.com/p/pyftpdlib/ +License: MIT +License File: src/LICENSE +Version: unknown +Security Critical: no + +Description: +Python FTP server library provides an high-level portable interface to easily +write asynchronous FTP servers with Python. Based on asyncore framework +pyftpdlib is currently the most complete RFC-959 FTP server implementation +available for Python programming language. + +Learn more by visiting: http://code.google.com/p/pyftpdlib/wiki/FAQ diff --git a/chromium/third_party/pyftpdlib/src/CREDITS b/chromium/third_party/pyftpdlib/src/CREDITS new file mode 100644 index 00000000000..12ee43b55e2 --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/CREDITS @@ -0,0 +1,149 @@ + +Intro +===== + +I would like to recognize some of the people who have been +instrumental in the development of pyftpdlib. +I'm sure I am forgetting some people (feel free to email +me), but here is a short list. +It's modeled after the Linux CREDITS file where the fields +are: +name (N), e-mail (E), web-address (W), country (C) description (D). +Really thanks to all of you. + + +Maintainers +=========== + +N: Giampaolo Rodola' +C: Italy +E: g.rodola@gmail.com +D: Original pyftpdlib author and maintainer + +N: Jay Loden +C: NJ, USA +E: jloden@gmail.com +W: http://www.jayloden.com +D: OS X and Linux platform development/testing + +N: Silas Sewell +C: Denver, USA +E: silas@sewell.ch +W: http://www.silassewell.com +D: Fedora port maintainer + +N: Li-Wen Hsu +C: Taiwan +E: lwhsu@lwhsu.org +W: http://lwhsu.org +D: FreeBSD port maintainer + + +Contributors +============ + +N: Anatoly Techtonik +C: Belarus +E: techtonik@gmail.com +D: Inclusion of pyftpdlib in Far Manager, a file and archive manager for Windows + http://www.farmanager.com/enforum/viewtopic.php?t=640&highlight=&sid=12d4d90f27f421243bcf7a0e3c516efb. + +N: Arkadiusz Wahlig +C: Germany +W: http://arkadiusz-wahlig.blogspot.com +D: Inclusion of pyftpdlib in gpftpd project, an FTP daemon for managing files + hosted on Google Pages + (http://arkadiusz-wahlig.blogspot.com/2008/04/hosting-files-on-google.html). + +N: Walco van Loon +C: Netherlands +E: walco@n--tree.net +D: Inclusion of pyftpdlib in aksy project + (http://walco.n--tree.net/projects/aksy). + +N: Stephane Travostino +E: stephane.travostino@combo.cc +D: Inclusion of pyftpdlib in Shareme project + (http://bbs.archlinux.org/viewtopic.php?pid=431474). + +N: Shinya Okano +C: Japan +E: xxshss@yahoo.co.jp +D: Japanese translation of pyftpdlib announces. Inclusion of pyftpdlib in + unbox-ftpd project (http://code.google.com/p/unboxftpd). + +N: Yan Raber +C: Italy +E: yanraber@gmail.com +D: Fix of Issue #9 (Path traversal vulnerability) + +N: Alex Martelli +C: Italy +E: aleax@gmail.com +D: Various useful suggestions + +N: Knic +C: Redmond, USA +E: oneeyedelf1@googlemail.com +D: Bug report #24 (some troubles on PythonCE), tester for various platforms + including Windows Mobile, Windows Server 2008 and various 64 bit OSes. + +N: Greg Copeland +E: gcopeland@efjohnson.com +D: Bug report #16 (Extending compatibility with older python versions) + +N: Roger Erens +E: rogererens@gmail.com +D: Bug report affecting unix_ftpd.py's authorizer + +N: Coronado Ivan +D: Bug report #70 (Wrong NOOP response code) + +N: Rauli Ruohonen +D: Bug report #71 (Socket handles are leaked when a data transfer is in progress + and user QUITs) + +N: Equand +E: equand@gmail.com +D: Bug report #77 (incorrect OOB data management on FreeBSD). + +N: fogwraith +E: fogwraith@gmail.com +D: Bug report #80 (demo/md5_ftpd.py should use hashlib module instead of the + deprecated md5 module) + +N: Bram Neijt +E: bneijt@gmail.com +D: Bug report #100, author of ShareFTP project: + http://git.logfish.net/shareftp.git/ + +N: Michele Petrazzo +C: Italy +E: michele.petrazzo@gmail.com +D: Creation of the demo/unix_daemon.py code. + +N: Wentao Han +D: Bug report #104 (socket.accept() might return None instead of a valid address + and EPIPE might be thrown by asyncore on OS X). + +N: Ben Timby +E: btimby@gmail.com +C: USA +D: (issue #127) multiple masqueraded IP addresses patch. + +N: Bernd Deichmann +W: http://deichmann-edv.de/ +D: issue 156 + +N: Andrew Scheller +E: gcode@loowis.durge.org +C: UK +D: issue 158, 161, 163, 167, 175 + +N: guppyism +E: guppyism@gmail.com +D: issue 187 + +N: Darren Worrall +E: darren.worrall@gmail.com +D: issue 198 diff --git a/chromium/third_party/pyftpdlib/src/HISTORY b/chromium/third_party/pyftpdlib/src/HISTORY new file mode 100644 index 00000000000..dcef1a36abc --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/HISTORY @@ -0,0 +1,649 @@ +Bug tracker at http://code.google.com/p/pyftpdlib/issues/list + + +History +======= + +Version: 0.7.0 - Date: XXXX-XX-XX +--------------------------------- + +Enhancements: + + * Issue #152: uploads (from server to client) on UNIX are now from 2x (Linux) + to 3x (OSX) faster because of sendfile(2) system call usage. + + * Issue #155: AbstractedFS "root" and "cwd" are no longer read-only properties + but can be set via setattr(). + + * Issue #168: added FTPHandler.logerror() method. It can be overridden to + provide more information (e.g. username) when logging exception tracebacks. + + * Issue #174: added support for SITE CHMOD command (change file mode). + + * Issue #177: setuptools is now used in setup.py + + * Issue #178: added anti flood script in demo directory. + + * Issue #181: added CallEvery class to call a function every x seconds. + + * Issue #185: pass Debian licenscheck tool. + + * Issue #189: the internal scheduler has been rewritten from scratch and it is + an order of magnitude faster, especially for operations like cancel() which + are involved when clients are disconnected (hence invoked very often). + Some benchmarks: + schedule : +0.5x + reschedule : +1.7x + cancel : +477x (with 1 milion scheduled functions) + run: : +8x + Also, a single scheduled function now consumes 1/3 of the memory thanks + to __slots__ usage. + + * Issue #196: added callback for failed login attempt. + + * Issue #200: FTPServer.server_forever() is now a class method. + +Bugfixes: + + * Issue #156: data connection must be closed before sending 226/426 reply. + This was against RFC-959 and was causing problems with older FTP clients. + + * Issue #161: MLSD 'unique' fact can provide the same value for files having a + similar device/inode but that in fact are different. + (patch by Andrew Scheller) + + * Issue #162: (FTPS) SSL shutdown() is not invoked for the control connection. + + * Issue #163: FEAT erroneously reports MLSD. (patch by Andrew Scheller) + + * Issue #166: (FTPS) an exception on send() can cause server to crash (DoS). + + * Issue #167: fix some typos returned on HELP. + + * Issue #170: PBSZ and PROT commands are now allowed before authentication + fixing problems with non-compliant FTPS clients. + + * Issue #171: (FTPS) an exception when shutting down the SSL layer can cause + server to crash (DoS). + + * Issue #173: file last modification time shown in LIST response might be in a + language different than English causing problems with some clients. + + * Issue #175: FEAT response now omits to show those commands which are removed + from proto_cmds map. + + * Issue #176: SO_REUSEADDR option is now used for passive data sockets to + prevent server running out of free ports when using passive_ports directive. + + * Issue #187: match proftpd LIST format for files having last modification time + > 6 months. + + * Issue #188: fix maximum recursion depth exceeded exception occurring if + client quickly connects and disconnects data channel. + + * Issue #191: (FTPS) during SSL shutdown() operation the server can end up in + an infinite loop hogging CPU resources. + + * Issue #199: UnixAuthorizer with require_valid_shell option is broken. + + * Issue #202: added benchmark script. + +Major API changes since 0.6.0: + + * New FTPHandler.use_sendfile attribute. + * sendfile() is now automatically used instead of plain send() if + pysendfile module is installed. + * FTPServer.serve_forever() is a classmethod. + * AbstractedFS root and cwd properties can now be set via setattr(). + * New CallLater class. + * New FTPHandler.on_login_failed(username, password) method. + * New FTPHandler.logerror(msg) method. + * New FTPHandler.log_exception(instance) method. + + +Version: 0.6.0 - Date: 2011-01-24 +--------------------------------- + +Enhancements: + + * Issue #68: added full FTPS (FTP over SSL/TLS) support provided by new + TLS_FTPHandler class defined in pyftpdlib.contrib.handlers module. + + * Issue #86: pyftpdlib now reports all ls and MDTM timestamps as GMT times, + as recommended in RFC-3659. A FTPHandler.use_gmt_times attributed has been + added and can be set to False in case local times are desired instead. + + * Issue #124: pyftpdlib now accepts command line options to configure a stand + alone anonymous FTP server when running pyftpdlib with python's -m option. + + * Issue #125: logs are now provided in a standardized format parsable by log + analyzers. FTPHandler class provides two new methods to standardize both + commands and transfers logging: log_cmd() and log_transfer(). + + * Issue #127: added FTPHandler.masquerade_address_map option which allows you + to define multiple 1 to 1 mappings in case you run a FTP server with + multiple private IP addresses behind a NAT firewall with multiple public + IP addresses. + + * Issue #128: files and directories owner and group names and os.readlink are + now resolved via AbstractedFS methods instead of in format_list(). + + * Issue #129 and #139: added 4 new callbacks to FTPHandler class: + on_incomplete_file_sent(), on_incomplete_file_received(), on_login() and + on_logout(). + + * Issue #130: added UnixAuthorizer and WindowsAuthorizer classes defined in the + new pyftpdlib.contrib.authorizers module. + + * Issue #131: pyftpdlib is now able to serve both IPv4 and IPv6 at the same + time by using a single socket. + + * Issue #133: AbstractedFS constructor now accepts two argumets: root and + cmd_channel breaking compatibility with previous version. Also, root and and + cwd attributes became properties. The previous bug consisting in re-setting + the root from the ftp handler after user login has been fixed to ease the + development of subclasses. + + * Issue #134: enabled TCP_NODELAY socket option for the FTP command channels + resulting in pyftpdlib being twice faster. + + * Issue #135: Python 2.3 support has been removed. + + * Issue #137: added new pyftpdlib.contrib.filesystems module within + UnixFilesystem class which permits the client to escape its home directory + and navigate the real filesystem. + + * Issue #138: added DTPHandler.get_elapsed_time() method which returns the + transfer elapsed time in seconds. + + * Issue #144: a "username" parameter is now passed to authorizer's + terminate_impersonation() method. + + * Issue #149: ftpserver.proto_cmds dictionary refactoring and get rid of + _CommandProperty class. + +Bugfixes: + + * Issue #120: an ActiveDTP() instance is not garbage collected in case a + client issuing PORT disconnects before establishing the data connection. + + * Issue #122: a wrong variable name was used in AbstractedFS.validpath method. + + * Issue #123: PORT command doesn't bind to correct address in case an alias + is created for the local network interface. + + * Issue #140: pathnames returned in PWD response should have double-quotes '"' + escaped. + + * Issue #143: EINVAL not properly handled causes server crash on OSX. + + * Issue #146: SIZE and MDTM commands are now rejected unless the "l" permission + has been specified for the user. + + * Issue #150: path traversal bug: it is possible to move/rename a file outside + of the user home directory. + +Major API changes since 0.5.2 + + * removed support for Python 2.3. + + * all classes are now new-style classes. + + * AbstractedFS class: + * __init__ now accepts two arguments: root and cmd_channel. + * root and cwd attributes are now read-only properties. + * 3 new methods have been added: + - get_user_by_uid() + - get_group_by_gid() + - readlink() + + * FTPHandler class: + * new class attributes: + - use_gmt_times + - tcp_no_delay + - masquerade_address_map + * new methods: + - on_incomplete_file_sent() + - on_incomplete_file_received() + - on_login() + - on_logout() + - log_cmd() + - log_transfer() + * proto_cmds class attribute has been added. The FTPHandler class no longer + relies on "ftpserver.proto_cmds" global dictionary but on + "ftpserver.FTPHandler.proto_cmds" instead. + + * FTPServer class: + - max_cons attribute defaults to 512 by default instead of 0 (unlimited). + - server_forever()'s map argument is gone. + + * DummyAuthorizer: + - ValueError exceptions are now raised instead of AuthorizerError. + - terminate_impersonation() method now expects a "username" parameter. + + * DTPHandler.get_elapsed_time() method has been added. + + * Added a new package in pyftpdlib namespace: "contrib". Modules (and classes) + defined here: + - pyftpdlib.contrib.handlers.py (TLS_FTPHandler) + - pyftpdlib.contrib.authorizers.py (UnixAuthorizer, WindowsAuthorizer) + - pyftpdlib.contrib.filesystems (UnixFilesystem) + +Minor API changes since 0.5.2 + + * FTPHandler renamed objects: + data_server -> _dtp_acceptor + current_type -> _current_type + restart_position -> _restart_position + quit_pending -> _quit_pending + af -> _af + on_dtp_connection -> _on_dtp_connection + on_dtp_close -> _on_dtp_close + idler -> _idler + + * AbstractedFS.rnfr attribute moved to FTPHandler._rnfr. + + +Version: 0.5.2 - Date: 2009-09-14 +--------------------------------- + +Enhancements: + + * Issue #103: added unix_daemon.py script. + + * Issue #108: a new ThrottledDTPHandler class has been added for limiting the + speed of downloads and uploads. + +Bugfixes: + + * Issue #100: fixed a race condition in FTPHandler constructor which could + throw an exception in case of connection bashing (DoS). (thanks Bram Neijt) + + * Issue #102: FTPServer.close_all() now removes any unfired delayed call left + behind to prevent potential memory leaks. + + * Issue #104: fixed a bug in FTPServer.handle_accept() where socket.accept() + could return None instead of a valid address causing the server to crash. + (OS X only, reported by Wentao Han) + + * Issue #104: an unhandled EPIPE exception might be thrown by asyncore.recv() + when dealing with ill-behaved clients on OS X . (reported by Wentao Han) + + * Issue #105: ECONNABORTED might be thrown by socket.accept() on FreeBSD + causing the server to crash. + + * Issue #109: an unhandled EBADF exception might be thrown when using poll() on + OS X and FreeBSD. + + * Issue #111: the license used was not MIT as stated in source files. + + * Issue #112: fixed a MDTM related test case failure occurring on 64 bit OSes. + + * Issue #113: fixed unix_ftp.py which was treating anonymous as a normal user. + + * Issue #114: MLST is now denied unless the "l" permission has been specified + for the user. + + * Issue #115: asyncore.dispatcher.close() is now called before doing any other + cleanup operation when client disconnects. This way we avoid an endless loop + which hangs the server in case an exception is raised in close() method. + (thanks Arkadiusz Wahlig) + + * Issue #116: extra carriage returns were added to files transferred in ASCII + mode. + + * Issue #118: CDUP always changes to "/". + + * Issue #119: QUIT sent during a transfer caused a memory leak. + + +API changes since 0.5.1: + + * ThrottledDTPHandler class has been added. + + * FTPHandler.process_command() method has been added. + + +Version: 0.5.1 - Date: 2009-01-21 +--------------------------------- + +Enhancements: + + * Issue #79: added two new callback methods to FTPHandler class to handle + "on_file_sent" and "on_file_received" events. + + * Issue #82: added table of contents in documentation. + + * Issue #92: ASCII transfers are now 200% faster on those systems using + "\r\n" as line separator (typically Windows). + + * Issue #94: a bigger buffer size for send() and recv() has been set resulting + in a considerable speedup (about 40% faster) for both incoming and outgoing + data transfers. + + * Issue #98: added preliminary support for SITE command. + + * Issue #99: a new script implementing FTPS (FTP over TLS/SSL) has been added + to the demo directory. See: + http://code.google.com/p/pyftpdlib/source/browse/trunk/demo/tls_ftpd.py + +Bugfixes: + + * Issue #78: the idle timeout of passive data connections gets stopped in case + of rejected "site-to-site" connections. + + * Issue #80: demo/md5_ftpd.py should use hashlib module instead of the + deprecated md5 module. + + * Issue #81: fixed some tests which were failing on SunOS. + + * Issue #84: fixed a very rare unhandled exception which could occur when + retrieving the first bytes of a corrupted file. + + * Issue #85: a positive MKD response is supposed to include the name of the + new directory. + + * Issue #87: SIZE should be rejected when the current TYPE is ASCII. + + * Issue #88: REST should be rejected when the current TYPE is ASCII. + + * Issue #89: "TYPE AN" was erroneously treated as synonym for "TYPE A" when + "TYPE L7" should have been used instead. + + * Issue #90: an unhandled exception can occur when using MDTM against a file + modified before year 1900. + + * Issue #91: an unhandled exception can occur in case accept() returns None + instead of a socket (it happens sometimes). + + * Issue #95: anonymous is now treated as any other case-sensitive user. + +API changes since 0.5.0: + + * FTPHandler gained a new "_extra_feats" private attribute. + + * FTPHandler gained two new methods: "on_file_sent" and "on_file_received". + + +Version: 0.5.0 - Date: 2008-09-20 +--------------------------------- + +Enhancements: + + * Issue #72: pyftpdlib now provides configurable idle timeouts to disconnect + client after a long time of inactivity. + + * Issue #73: imposed a delay before replying for invalid credentials to + minimize the risk of brute force password guessing (RFC-1123). + + * Issue #74: it is now possible to define permission exceptions for certain + directories (e.g. creating a user which does not have write permission except + for one sub-directory in FTP root). + + * Improved bandwidth throttling capabilities of demo/throttled_ftpd.py script + by having used the new CallLater class which drastically reduces the number + of time.time() calls. + +Bugfixes: + + * Issue #62: some unit tests were failing on certain dual core machines. + + * Issue #71: socket handles are leaked when a data transfer is in progress and + user QUITs. + + * Issue #75: orphaned file was left behind in case STOU failed for insufficient + user permissions. + + * Issue #77: incorrect OOB data management on FreeBSD. + +API changes since 0.4.0: + + * FTPHandler, DTPHandler, PassiveDTP and ActiveDTP classes gained a new timeout + class attribute. + + * DummyAuthorizer class gained a new override_perm method. + + * A new class called CallLater has been added. + + * AbstractedFS.get_stat_dir method has been removed. + + +Version: 0.4.0 - Date: 2008-05-16 +--------------------------------- + +Enhancements: + + * Issue #65: It is now possible to assume the id of real users when using + system dependent authorizers. + + * Issue #67: added IPv6 support. + +Bugfixes: + + * Issue #64: Issue #when authenticating as anonymous user when using UNIX and + Windows authorizers. + + * Issue #66: WinNTAuthorizer does not determine the real user home directory. + + * Issue #69: DummyAuthorizer incorrectly uses class attribute instead of + instance attribute for user_table dictionary. + + * Issue #70: a wrong NOOP response code was given. + +API changes since 0.3.0: + + * DummyAuthorizer class has now two new methods: impersonate_user() and + terminate_impersonation(). + + +Version: 0.3.0 - Date: 2008-01-17 +--------------------------------- + +Enhancements: + + * Issue #42: implemented FEAT command (RFC-2389). + + * Issue #48: real permissions, owner, and group for files on UNIX platforms are + now provided when processing LIST command. + + * Issue #51: added the new demo/throttled_ftpd.py script. + + * Issue #52: implemented MLST and MLSD commands (RFC-3659). + + * Issue #58: implemented OPTS command (RFC-2389). + + * Issue #59: iterators are now used for calculating requests requiring long + time to complete (LIST and MLSD commands) drastically increasing the daemon + scalability when dealing with many connected clients. + + * Issue #61: extended the set of assignable user permissions. + +Bugfixes: + + * Issue #41: an unhandled exception occurred on QUIT if user was not yet + authenticated. + + * Issue #43: hidden the server identifier returned in STAT response. + + * Issue #44: a wrong response code was given on PORT in case of failed + connection attempt. + + * Issue #45: a wrong response code was given on HELP if the provided argument + wasn't recognized as valid command. + + * Issue #46: a wrong response code was given on PASV in case of unauthorized + FXP connection attempt. + + * Issue #47: can't use FTPServer.max_cons option on Python 2.3. + + * Issue #49: a "550 No such file or directory" was returned when LISTing + a directory containing a broken symbolic link. + + * Issue #50: DTPHandler class did not respect what specified in + ac_out_buffer_size attribute. + + * Issue #53: received strings having trailing white spaces was erroneously + stripped. + + * Issue #54: LIST/NLST/STAT outputs are now sorted by file name. + + * Issue #55: path traversal vulnerability in case of symbolic links escaping + user's home directory. + + * Issue #56: can't rename broken symbolic links. + + * Issue #57: invoking LIST/NLST over a symbolic link which points to a + direoctory shouldn't list its content. + + * Issue #60: an unhandled IndexError exception error was raised in case of + certain bad formatted PORT requests. + +API changes since 0.2.0: + + * New IteratorProducer and BufferedIteratorProducer classes have been added. + + * DummyAuthorizer class changes: + * The permissions management has been changed and the set of available + permissions have been extended (see Issue #61). add_user() method + now accepts "eladfm" permissions beyond the old "r" and "w". + * r_perm() and w_perm() methods have been removed. + * New has_perm() and get_perms() methods have been added. + + * AbstractedFS class changes: + * normalize() method has been renamed in ftpnorm(). + * translate() method has been renamed in ftp2fs(). + * New methods: fs2ftp(), stat(), lstat(), islink(), realpath(), lexists(), + validpath(). + * get_list_dir(), get_stat_dir() and format_list() methods now return an + iterator object instead of a string. + * format_list() method has a new "ignore_err" keyword argument. + + * global debug() function has been removed. + + +Version: 0.2.0 - Date: 2007-09-17 +--------------------------------- + +Major enhancements: + + * Issue #5: it is now possible to set a maximum number of connecions and a + maximum number of connections from the same IP address. + + * Issue #36: added support for FXP site-to-site transfer. + + * Issue #39: added NAT/Firewall support with PASV (passive) mode connections. + + * Issue #40: it is now possible to set a range of ports to use for passive + connections. + +RFC-related enhancements: + + * Issue #6: accept TYPE AN and TYPE L8 as synonyms for TYPE ASCII and TYPE + Binary. + + * Issue #7: a new USER command can now be entered at any point to begin the + login sequence again. + + * Issue #10: HELP command arguments are now accepted. + + * Issue #12: 554 error response is now returned on RETR/STOR if RESTart fails. + + * Issue #15: STAT used with an argument now returns directory LISTing over the + command channel (RFC-959). + +Security enhancements: + + * Issue #3: stop buffering when extremely long lines are received over the + command channel. + + * Issue #11: data connection is now rejected in case a privileged port is + specified in PORT command. + + * Issue #25: limited the number of attempts to find a unique filename when + processing STOU command. + +Usability enhancements: + + * Provided an overridable attribute to easily set number of maximum login + attempts before disconnecting. + + * Docstrings are now provided for almost every method and function. + + * Issue #30: HELP response now includes the command syntax. + + * Issue #31: a compact list of recognized commands is now provided on HELP. + + * Issue #32: a detailed error message response is not returned to client in + case the transfer is interrupted for some unexpected reason. + + * Issue #38: write access can now be optionally granted for anonymous user. + +Test suite enhancements: + + * File creation/removal moved into setUp and tearDown methods to avoid leaving + behind orphaned temporary files in the event of a test suite failure. + + * Issue #7: added test case for USER provided while already authenticated. + + * Issue #7: added test case for REIN while a transfer is in progress. + + * Issue #28: added ABOR tests. + +Bugfixes: + + * Issue #4: socket's "reuse_address" feature was used after the socket's + binding. + + * Issue #8: STOU string response didn't follow RFC-1123 specifications. + + * Issue #9: corrected path traversal vulnerability affecting file-system path + translations. + + * Issue #14: a wrong response code was returned on CDUP. + + * Issue #17: SIZE is now rejected for not regular files. + + * Issue #18: a wrong ABOR response code type was returned. + + * Issue #19: watch for STOU preceded by REST which makes no sense. + + * Issue #20: "attempted login" counter wasn't incremented on wrong username. + + * Issue #21: STAT wasn't permitted if user wasn't authenticated yet. + + * Issue #22: corrected memory leaks occurring on KeyboardInterrupt/SIGTERM. + + * Issue #23: PASS wasn't rejected when user was already authenticated. + + * Issue #24: Implemented a workaround over os.strerror() for those systems + where it is not available (Python CE). + + * Issue #24: problem occurred on Windows when using '\\' as user's home + directory. + + * Issue #26: select() in now used by default instead of poll() because of a + bug inherited from asyncore. + + * Issue #33: some FTPHandler class attributes wasn't resetted on REIN. + + * Issue #35: watch for APPE preceded by REST which makes no sense. + + +Version: 0.1.1 - Date: 2007-03-27 +---------------------------------- + + * Port selection on PASV command has been randomized to prevent a remote user + to guess how many data connections are in progress on the server. + + * Fixed bug in demo/unix_ftpd.py script. + + * ftp_server.serve_forever now automatically re-use address if current system + is posix. + + * License changed to MIT. + + +Version: 0.1.0 - Date: 2007-02-26 +---------------------------------- + + * First proof of concept beta release. diff --git a/chromium/third_party/pyftpdlib/src/INSTALL b/chromium/third_party/pyftpdlib/src/INSTALL new file mode 100644 index 00000000000..a5165fb1e89 --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/INSTALL @@ -0,0 +1,4 @@ +Install +======= + +See doc/install.html diff --git a/chromium/third_party/pyftpdlib/src/LICENSE b/chromium/third_party/pyftpdlib/src/LICENSE new file mode 100644 index 00000000000..a4d4ccaec05 --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/LICENSE @@ -0,0 +1,22 @@ +====================================================================== +Copyright (C) 2007-2012 Giampaolo Rodola' <g.rodola@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 +Giampaolo Rodola' not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +Giampaolo Rodola' DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN +NO EVENT Giampaolo Rodola' 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. +====================================================================== diff --git a/chromium/third_party/pyftpdlib/src/README b/chromium/third_party/pyftpdlib/src/README new file mode 100644 index 00000000000..c23b1ae9b39 --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/README @@ -0,0 +1,12 @@ + +About +===== + +Python FTP server library provides a high-level portable interface to easily +write asynchronous FTP servers with Python. +pyftpdlib is currently the most complete RFC-959 FTP server implementation +available for Python programming language. +It is used in projects like Google Chromium and Bazaar and included in Linux +Fedora and FreeBSD package repositories. + +Learn more by visiting: http://code.google.com/p/pyftpdlib/ diff --git a/chromium/third_party/pyftpdlib/src/demo/anti_flood_ftpd.py b/chromium/third_party/pyftpdlib/src/demo/anti_flood_ftpd.py new file mode 100644 index 00000000000..1b4565c9e7c --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/demo/anti_flood_ftpd.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# $Id$ + +# pyftpdlib is released under the MIT license, reproduced below: +# ====================================================================== +# Copyright (C) 2007-2012 Giampaolo Rodola' <g.rodola@gmail.com> +# +# All Rights Reserved +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ====================================================================== + +""" +A FTP server banning clients in case of commands flood. + +If client sends more than 300 requests per-second it will be +disconnected and won't be able to re-connect for 1 hour. +""" + +from pyftpdlib.ftpserver import FTPHandler, FTPServer, DummyAuthorizer, CallEvery + + +class AntiFloodHandler(FTPHandler): + + cmds_per_second = 300 # max number of cmds per second + ban_for = 60 * 60 # 1 hour + banned_ips = [] + + def __init__(self, *args, **kwargs): + super(AntiFloodHandler, self).__init__(*args, **kwargs) + self.processed_cmds = 0 + self.pcmds_callback = CallEvery(1, self.check_processed_cmds) + + def handle(self): + # called when client connects. + if self.remote_ip in self.banned_ips: + self.respond('550 you are banned') + self.close() + else: + super(AntiFloodHandler, self).handle() + + def check_processed_cmds(self): + # called every second; checks for the number of commands + # sent in the last second. + if self.processed_cmds > self.cmds_per_second: + self.ban(self.remote_ip) + else: + self.processed_cmds = 0 + + def process_command(self, *args, **kwargs): + # increase counter for every received command + self.processed_cmds += 1 + super(AntiFloodHandler, self).process_command(*args, **kwargs) + + def ban(self, ip): + # ban ip and schedule next un-ban + if ip not in self.banned_ips: + self.log('banned IP %s for command flooding' % ip) + self.respond('550 you are banned for %s seconds' % self.ban_for) + self.close() + self.banned_ips.append(ip) + + def unban(self, ip): + # unban ip + try: + self.banned_ips.remove(ip) + except ValueError: + pass + else: + self.log('unbanning IP %s' % ip) + + def close(self): + super(AntiFloodHandler, self).close() + if not self.pcmds_callback.cancelled: + self.pcmds_callback.cancel() + + +def main(): + authorizer = DummyAuthorizer() + authorizer.add_user('user', '12345', '.', perm='elradfmw') + authorizer.add_anonymous('.') + handler = AntiFloodHandler + handler.authorizer = authorizer + ftpd = FTPServer(('', 21), handler) + ftpd.serve_forever(timeout=1) + +if __name__ == '__main__': + main() diff --git a/chromium/third_party/pyftpdlib/src/demo/basic_ftpd.py b/chromium/third_party/pyftpdlib/src/demo/basic_ftpd.py new file mode 100644 index 00000000000..9743fe33005 --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/demo/basic_ftpd.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python +# $Id$ + +# pyftpdlib is released under the MIT license, reproduced below: +# ====================================================================== +# Copyright (C) 2007-2012 Giampaolo Rodola' <g.rodola@gmail.com> +# +# All Rights Reserved +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ====================================================================== + +"""A basic FTP server which uses a DummyAuthorizer for managing 'virtual +users', setting a limit for incoming connections. +""" + +import os + +from pyftpdlib import ftpserver + + +def main(): + # Instantiate a dummy authorizer for managing 'virtual' users + authorizer = ftpserver.DummyAuthorizer() + + # Define a new user having full r/w permissions and a read-only + # anonymous user + authorizer.add_user('user', '12345', os.getcwd(), perm='elradfmwM') + authorizer.add_anonymous(os.getcwd()) + + # Instantiate FTP handler class + ftp_handler = ftpserver.FTPHandler + ftp_handler.authorizer = authorizer + + # Define a customized banner (string returned when client connects) + ftp_handler.banner = "pyftpdlib %s based ftpd ready." %ftpserver.__ver__ + + # Specify a masquerade address and the range of ports to use for + # passive connections. Decomment in case you're behind a NAT. + #ftp_handler.masquerade_address = '151.25.42.11' + #ftp_handler.passive_ports = range(60000, 65535) + + # Instantiate FTP server class and listen to 0.0.0.0:21 + address = ('', 21) + ftpd = ftpserver.FTPServer(address, ftp_handler) + + # set a limit for connections + ftpd.max_cons = 256 + ftpd.max_cons_per_ip = 5 + + # start ftp server + ftpd.serve_forever() + +if __name__ == '__main__': + main() diff --git a/chromium/third_party/pyftpdlib/src/demo/keycert.pem b/chromium/third_party/pyftpdlib/src/demo/keycert.pem new file mode 100644 index 00000000000..bfa28def126 --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/demo/keycert.pem @@ -0,0 +1,32 @@ +-----BEGIN RSA PRIVATE KEY-----
+MIICXwIBAAKBgQC8ddrhm+LutBvjYcQlnH21PPIseJ1JVG2HMmN2CmZk2YukO+9L
+opdJhTvbGfEj0DQs1IE8M+kTUyOmuKfVrFMKwtVeCJphrAnhoz7TYOuLBSqt7lVH
+fhi/VwovESJlaBOp+WMnfhcduPEYHYx/6cnVapIkZnLt30zu2um+DzA9jQIDAQAB
+AoGBAK0FZpaKj6WnJZN0RqhhK+ggtBWwBnc0U/ozgKz2j1s3fsShYeiGtW6CK5nU
+D1dZ5wzhbGThI7LiOXDvRucc9n7vUgi0alqPQ/PFodPxAN/eEYkmXQ7W2k7zwsDA
+IUK0KUhktQbLu8qF/m8qM86ba9y9/9YkXuQbZ3COl5ahTZrhAkEA301P08RKv3KM
+oXnGU2UHTuJ1MAD2hOrPxjD4/wxA/39EWG9bZczbJyggB4RHu0I3NOSFjAm3HQm0
+ANOu5QK9owJBANgOeLfNNcF4pp+UikRFqxk5hULqRAWzVxVrWe85FlPm0VVmHbb/
+loif7mqjU8o1jTd/LM7RD9f2usZyE2psaw8CQQCNLhkpX3KO5kKJmS9N7JMZSc4j
+oog58yeYO8BBqKKzpug0LXuQultYv2K4veaIO04iL9VLe5z9S/Q1jaCHBBuXAkEA
+z8gjGoi1AOp6PBBLZNsncCvcV/0aC+1se4HxTNo2+duKSDnbq+ljqOM+E7odU+Nq
+ewvIWOG//e8fssd0mq3HywJBAJ8l/c8GVmrpFTx8r/nZ2Pyyjt3dH1widooDXYSV
+q6Gbf41Llo5sYAtmxdndTLASuHKecacTgZVhy0FryZpLKrU=
+-----END RSA PRIVATE KEY-----
+-----BEGIN CERTIFICATE-----
+MIICpzCCAhCgAwIBAgIJAP+qStv1cIGNMA0GCSqGSIb3DQEBBQUAMIGJMQswCQYD
+VQQGEwJVUzERMA8GA1UECBMIRGVsYXdhcmUxEzARBgNVBAcTCldpbG1pbmd0b24x
+IzAhBgNVBAoTGlB5dGhvbiBTb2Z0d2FyZSBGb3VuZGF0aW9uMQwwCgYDVQQLEwNT
+U0wxHzAdBgNVBAMTFnNvbWVtYWNoaW5lLnB5dGhvbi5vcmcwHhcNMDcwODI3MTY1
+NDUwWhcNMTMwMjE2MTY1NDUwWjCBiTELMAkGA1UEBhMCVVMxETAPBgNVBAgTCERl
+bGF3YXJlMRMwEQYDVQQHEwpXaWxtaW5ndG9uMSMwIQYDVQQKExpQeXRob24gU29m
+dHdhcmUgRm91bmRhdGlvbjEMMAoGA1UECxMDU1NMMR8wHQYDVQQDExZzb21lbWFj
+aGluZS5weXRob24ub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8ddrh
+m+LutBvjYcQlnH21PPIseJ1JVG2HMmN2CmZk2YukO+9LopdJhTvbGfEj0DQs1IE8
+M+kTUyOmuKfVrFMKwtVeCJphrAnhoz7TYOuLBSqt7lVHfhi/VwovESJlaBOp+WMn
+fhcduPEYHYx/6cnVapIkZnLt30zu2um+DzA9jQIDAQABoxUwEzARBglghkgBhvhC
+AQEEBAMCBkAwDQYJKoZIhvcNAQEFBQADgYEAF4Q5BVqmCOLv1n8je/Jw9K669VXb
+08hyGzQhkemEBYQd6fzQ9A/1ZzHkJKb1P6yreOLSEh4KcxYPyrLRC1ll8nr5OlCx
+CMhKkTnR6qBsdNV0XtdU2+N25hqW+Ma4ZeqsN/iiJVCGNOZGnvQuvCAGWF8+J/f/
+iHkC6gGdBJhogs4=
+-----END CERTIFICATE-----
diff --git a/chromium/third_party/pyftpdlib/src/demo/md5_ftpd.py b/chromium/third_party/pyftpdlib/src/demo/md5_ftpd.py new file mode 100644 index 00000000000..e9b2540cfcb --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/demo/md5_ftpd.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +# $Id$ + +# pyftpdlib is released under the MIT license, reproduced below: +# ====================================================================== +# Copyright (C) 2007-2012 Giampaolo Rodola' <g.rodola@gmail.com> +# +# All Rights Reserved +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ====================================================================== + +"""A basic ftpd storing passwords as hash digests (platform independent). +""" + +import os +try: + from hashlib import md5 +except ImportError: + # backward compatibility with Python < 2.5 + from md5 import new as md5 + +from pyftpdlib import ftpserver + + +class DummyMD5Authorizer(ftpserver.DummyAuthorizer): + + def validate_authentication(self, username, password): + hash = md5(password).hexdigest() + return self.user_table[username]['pwd'] == hash + + +def main(): + # get a hash digest from a clear-text password + hash = md5('12345').hexdigest() + authorizer = DummyMD5Authorizer() + authorizer.add_user('user', hash, os.getcwd(), perm='elradfmw') + authorizer.add_anonymous(os.getcwd()) + ftp_handler = ftpserver.FTPHandler + ftp_handler.authorizer = authorizer + address = ('', 21) + ftpd = ftpserver.FTPServer(address, ftp_handler) + ftpd.serve_forever() + +if __name__ == "__main__": + main() diff --git a/chromium/third_party/pyftpdlib/src/demo/throttled_ftpd.py b/chromium/third_party/pyftpdlib/src/demo/throttled_ftpd.py new file mode 100644 index 00000000000..c12bf9022c4 --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/demo/throttled_ftpd.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# $Id$ + +# pyftpdlib is released under the MIT license, reproduced below: +# ====================================================================== +# Copyright (C) 2007-2012 Giampaolo Rodola' <g.rodola@gmail.com> +# +# All Rights Reserved +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ====================================================================== + +"""An FTP server which uses the ThrottledDTPHandler class for limiting the +speed of downloads and uploads. +""" + +import os + +from pyftpdlib import ftpserver + + +def main(): + authorizer = ftpserver.DummyAuthorizer() + authorizer.add_user('user', '12345', os.getcwd(), perm='elradfmw') + authorizer.add_anonymous(os.getcwd()) + + dtp_handler = ftpserver.ThrottledDTPHandler + dtp_handler.read_limit = 30720 # 30 Kb/sec (30 * 1024) + dtp_handler.write_limit = 30720 # 30 Kb/sec (30 * 1024) + + ftp_handler = ftpserver.FTPHandler + ftp_handler.authorizer = authorizer + # have the ftp handler use the alternative dtp handler class + ftp_handler.dtp_handler = dtp_handler + + ftpd = ftpserver.FTPServer(('', 21), ftp_handler) + ftpd.serve_forever() + +if __name__ == '__main__': + main() diff --git a/chromium/third_party/pyftpdlib/src/demo/tls_ftpd.py b/chromium/third_party/pyftpdlib/src/demo/tls_ftpd.py new file mode 100644 index 00000000000..71ed8205833 --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/demo/tls_ftpd.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# $Id$ + +# pyftpdlib is released under the MIT license, reproduced below: +# ====================================================================== +# Copyright (C) 2007-2012 Giampaolo Rodola' <g.rodola@gmail.com> +# +# All Rights Reserved +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ====================================================================== + +"""An RFC-4217 asynchronous FTPS server supporting both SSL and TLS. + +Requires PyOpenSSL module (http://pypi.python.org/pypi/pyOpenSSL). +""" + +import os + +from pyftpdlib import ftpserver +from pyftpdlib.contrib.handlers import TLS_FTPHandler + +CERTFILE = os.path.abspath(os.path.join(os.path.dirname(__file__), + "keycert.pem")) + +def main(): + authorizer = ftpserver.DummyAuthorizer() + authorizer.add_user('user', '12345', '.', perm='elradfmw') + authorizer.add_anonymous('.') + ftp_handler = TLS_FTPHandler + ftp_handler.certfile = CERTFILE + ftp_handler.authorizer = authorizer + # requires SSL for both control and data channel + #ftp_handler.tls_control_required = True + #ftp_handler.tls_data_required = True + ftpd = ftpserver.FTPServer(('', 8021), ftp_handler) + ftpd.serve_forever() + +if __name__ == '__main__': + main() diff --git a/chromium/third_party/pyftpdlib/src/demo/unix_daemon.py b/chromium/third_party/pyftpdlib/src/demo/unix_daemon.py new file mode 100644 index 00000000000..3a848b328ae --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/demo/unix_daemon.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python +# $Id$ + +# pyftpdlib is released under the MIT license, reproduced below: +# ====================================================================== +# Copyright (C) 2007-2012 Giampaolo Rodola' <g.rodola@gmail.com> +# +# All Rights Reserved +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ====================================================================== + +"""A basic unix daemon using the python-daemon library: +http://pypi.python.org/pypi/python-daemon + +Example usages: + + $ python unix_daemon.py start + $ python unix_daemon.py stop + $ python unix_daemon.py status + $ python unix_daemon.py # foreground (no daemon) + $ python unix_daemon.py --logfile /var/log/ftpd.log start + $ python unix_daemon.py --pidfile /var/run/ftpd.pid start + +This is just a proof of concept which demonstrates how to daemonize +the FTP server. +You might want to use this as an example and provide the necessary +customizations. + +Parts you might want to customize are: + - UMASK, WORKDIR, HOST, PORT constants + - get_server() function (to define users and customize FTP handler) + +Authors: + - Ben Timby - btimby <at> gmail.com + - Giampaolo Rodola' - g.rodola <at> gmail.com +""" + +from __future__ import with_statement + +import os +import errno +import sys +import time +import optparse +import signal + +from pyftpdlib import ftpserver +from pyftpdlib.contrib.authorizers import UnixAuthorizer + +# http://pypi.python.org/pypi/python-daemon +import daemon +import daemon.pidfile + + +# overridable options +HOST = "" +PORT = 21 +PID_FILE = "/var/run/pyftpdlib.pid" +LOG_FILE = "/var/log/pyftpdlib.log" +WORKDIR = os.getcwd() +UMASK = 0 + + +def pid_exists(pid): + """Return True if a process with the given PID is currently running.""" + try: + os.kill(pid, 0) + except OSError, e: + return e.errno == errno.EPERM + else: + return True + +def get_pid(): + """Return the PID saved in the pid file if possible, else None.""" + try: + with open(PID_FILE) as f: + return int(f.read().strip()) + except IOError, err: + if err.errno != errno.ENOENT: + raise + +def stop(): + """Keep attempting to stop the daemon for 5 seconds, first using + SIGTERM, then using SIGKILL. + """ + pid = get_pid() + if not pid or not pid_exists(pid): + print "daemon not running" + return + sig = signal.SIGTERM + i = 0 + while True: + sys.stdout.write('.') + sys.stdout.flush() + try: + os.kill(pid, sig) + except OSError, e: + if e.errno == errno.ESRCH: + print "\nstopped (pid %s)" % pid + return + else: + raise + i += 1 + if i == 25: + sig = signal.SIGKILL + elif i == 50: + sys.exit("\ncould not kill daemon (pid %s)" % pid) + time.sleep(0.1) + +def status(): + """Print daemon status and exit.""" + pid = get_pid() + if not pid or not pid_exists(pid): + print "daemon not running" + else: + print "daemon running with pid %s" % pid + sys.exit(0) + +def get_server(): + """Return a pre-configured FTP server instance.""" + # when daemonized, it seems we need to flush() stdout explicitly + # in order to get the log file written in real time + def log(s): + sys.stdout.write(s + "\n") + sys.stdout.flush() + ftpserver.log = ftpserver.logline = log + ftp_handler = ftpserver.FTPHandler + ftp_handler.authorizer = UnixAuthorizer() + server = ftpserver.FTPServer((HOST, PORT), ftp_handler) + return server + +def daemonize(): + """A wrapper around python-daemonize context manager.""" + pid = get_pid() + if pid and pid_exists(pid): + sys.exit('daemon already running (pid %s)' % pid) + # instance FTPd before daemonizing, so that in case of problems we + # get an exception here and exit immediately + server = get_server() + logfile = open(LOG_FILE, 'a+') + ctx = daemon.DaemonContext( + stdout=logfile, + stderr=logfile, + working_directory=WORKDIR, + umask=UMASK, + pidfile=daemon.pidfile.TimeoutPIDLockFile(PID_FILE), + files_preserve=[server], + ) + with ctx: + server.serve_forever() + +def main(): + global PID_FILE, LOG_FILE + USAGE = "python [-p PIDFILE] [-l LOGFILE]\n\n" \ + "Commands:\n - start\n - stop\n - status" + parser = optparse.OptionParser(usage=USAGE) + parser.add_option('-l', '--logfile', dest='logfile', + help='the log file location') + parser.add_option('-p', '--pidfile', dest='pidfile', default=PID_FILE, + help='file to store/retreive daemon pid') + options, args = parser.parse_args() + + if options.pidfile: + PID_FILE = options.pidfile + if options.logfile: + LOG_FILE = options.logfile + + if not args: + server = get_server() + server.serve_forever() + else: + if len(args) != 1: + sys.exit('too many commands') + elif args[0] == 'start': + daemonize() + elif args[0] == 'stop': + stop() + elif args[0] == 'status': + status() + else: + sys.exit('invalid command') + +if __name__ == '__main__': + sys.exit(main()) diff --git a/chromium/third_party/pyftpdlib/src/demo/unix_ftpd.py b/chromium/third_party/pyftpdlib/src/demo/unix_ftpd.py new file mode 100644 index 00000000000..2ccbae857d1 --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/demo/unix_ftpd.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# $Id$ + +# pyftpdlib is released under the MIT license, reproduced below: +# ====================================================================== +# Copyright (C) 2007-2012 Giampaolo Rodola' <g.rodola@gmail.com> +# +# All Rights Reserved +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ====================================================================== + +"""A FTPd using local UNIX account database to authenticate users. + +It temporarily impersonate the system users every time they are going +to perform a filesystem operations. +""" + +from pyftpdlib import ftpserver +from pyftpdlib.contrib.authorizers import UnixAuthorizer +from pyftpdlib.contrib.filesystems import UnixFilesystem + + +def main(): + authorizer = UnixAuthorizer(rejected_users=["root"], require_valid_shell=True) + ftp_handler = ftpserver.FTPHandler + ftp_handler.authorizer = authorizer + ftp_handler.abstracted_fs = UnixFilesystem + address = ('', 21) + ftpd = ftpserver.FTPServer(address, ftp_handler) + ftpd.serve_forever() + +if __name__ == "__main__": + main() diff --git a/chromium/third_party/pyftpdlib/src/demo/winnt_ftpd.py b/chromium/third_party/pyftpdlib/src/demo/winnt_ftpd.py new file mode 100644 index 00000000000..3ac2c28aedf --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/demo/winnt_ftpd.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# $Id$ + +# pyftpdlib is released under the MIT license, reproduced below: +# ====================================================================== +# Copyright (C) 2007-2012 Giampaolo Rodola' <g.rodola@gmail.com> +# +# All Rights Reserved +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ====================================================================== + +"""A ftpd using local Windows NT account database to authenticate users +(users must already exist). + +It also provides a mechanism to (temporarily) impersonate the system +users every time they are going to perform filesystem operations. +""" + +from pyftpdlib import ftpserver +from pyftpdlib.contrib.authorizers import WindowsAuthorizer + + +def main(): + authorizer = WindowsAuthorizer() + # Use Guest user with empty password to handle anonymous sessions. + # Guest user must be enabled first, empty password set and profile + # directory specified. + #authorizer = WindowsAuthorizer(anonymous_user="Guest", anonymous_password="") + ftp_handler = ftpserver.FTPHandler + ftp_handler.authorizer = authorizer + address = ('', 21) + ftpd = ftpserver.FTPServer(address, ftp_handler) + ftpd.serve_forever() + +if __name__ == "__main__": + main() diff --git a/chromium/third_party/pyftpdlib/src/doc/adoptions.lnk.html b/chromium/third_party/pyftpdlib/src/doc/adoptions.lnk.html new file mode 100644 index 00000000000..e979ee31cd9 --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/doc/adoptions.lnk.html @@ -0,0 +1,4 @@ +<a href="http://code.google.com/p/pyftpdlib"><img id="logo" src="logo.png" /></a>
+<title>Redirecting to Adoptions List...</title>
+<p>Redirecting to: <a href="http://code.google.com/p/pyftpdlib/wiki/Adoptions">http://code.google.com/p/pyftpdlib/wiki/Adoptions</a><br></p>
+<p>You'll be redirected in 3 seconds...</p>
diff --git a/chromium/third_party/pyftpdlib/src/doc/faq.html b/chromium/third_party/pyftpdlib/src/doc/faq.html new file mode 100644 index 00000000000..4e7f81804c6 --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/doc/faq.html @@ -0,0 +1,167 @@ +<html><head> +<meta http-equiv="content-type" content="text/html; charset=UTF-8"> +<style> +.section_anchor { + font-size:0px; +} +</style> + + + + + + <title>FAQ</title> + </head><body> + + + + + <div id="wikicontent"> + <table border="0" cellpadding="0" cellspacing="0" width="100%"> + <tbody><tr> + + <td class="vt" id="wikimaincol" width="100%"> + + <div id="wikiheader" style="margin-bottom: 1em;"> + + + <span style="font-size: 120%; font-weight: bold;">FAQ</span> + + + + <div style="font-style: italic; margin-top: 3px;">Frequently Asked Questions</div> + + </div> + <h1><a name="Table_of_contents"></a>Table of contents<a href="#Table_of_contents" class="section_anchor">¶</a></h1><p></p><ul><li><a href="#Table_of_contents">Table of contents</a></li><li><a href="#Introduction">Introduction</a></li><ul><li><a href="#What_is_pyftpdlib?">What is pyftpdlib?</a></li><li><a href="#What_is_Python?">What is Python?</a></li><li><a href="#I%27m_not_a_python_programmer._Can_I_use_it_anyway?">I'm not a python programmer. Can I use it anyway?</a></li><li><a href="#Documentation">Documentation</a></li><li><a href="#Mailing_lists">Mailing lists</a></li><li><a href="#Bug_reporting">Bug reporting</a></li></ul><li><a href="#Installing_and_compatibility">Installing and compatibility</a></li><ul><li><a href="#How_do_I_install_pyftpdlib?">How do I install pyftpdlib?</a></li><li><a href="#Which_Python_versions_are_compatible?">Which Python versions are compatible?</a></li><li><a href="#What_about_Python_3.x?">What about Python 3.x?</a></li><li><a href="#On_which_platforms_can_pyftpdlib_be_used?">On which platforms can pyftpdlib be used?</a></li></ul><li><a href="#Usage">Usage</a></li><ul><li><a href="#How_can_I_run_long-running_tasks_without_blocking_the_server?">How can I run long-running tasks without blocking the server?</a></li><li><a href="#Why_do_I_get_socket.error_%22Permission_denied%22_error_on">Why do I get socket.error "Permission denied" error on ftpd starting?</a></li><li><a href="#How_can_I_prevent_the_server_version_from_being_displayed?">How can I prevent the server version from being displayed?</a></li><li><a href="#Can_control_upload/download_ratios?">Can control upload/download ratios?</a></li><li><a href="#Are_there_ways_to_limit_connections?">Are there ways to limit connections?</a></li><li><a href="#I%27m_behind_a_NAT_/_gateway">I'm behind a NAT / gateway</a></li><li><a href="#What_is_FXP?">What is FXP?</a></li><li><a href="#Does_pyftpdlib_support_FXP?">Does pyftpdlib support FXP?</a></li><li><a href="#Why_timestamps_shown_by_MDTM_and_ls_commands_%28LIST,_MLSD,_MLST%29">Why timestamps shown by MDTM and ls commands (LIST, MLSD, MLST) are wrong?</a></li></ul><li><a href="#Implementation">Implementation</a></li><ul><li><a href="#Globbing_/_STAT_command_implementation">Globbing / STAT command implementation</a></li><li><a href="#ASCII_transfers_/_SIZE_command_implementation">ASCII transfers / SIZE command implementation</a></li><li><a href="#IPv6_support">IPv6 support</a></li><li><a href="#How_do_I_install_IPv6_support_on_my_system?">How do I install IPv6 support on my system?</a></li><li><a href="#Can_pyftpdlib_be_integrated_with_%22real%22_users_existing">Can pyftpdlib be integrated with "real" users existing on the system?</a></li><li><a href="#Does_pyftpdlib_support_FTP_over_TLS/SSL_%28FTPS%29">Does pyftpdlib support FTP over TLS/SSL (FTPS)</a></li><li><a href="#What_about_SITE_commands?">What about SITE commands?</a></li></ul></ul> <p></p><h1><a name="Introduction"></a>Introduction<a href="#Introduction" class="section_anchor">¶</a></h1><h2><a name="What_is_pyftpdlib?"></a>What is pyftpdlib?<a href="#What_is_pyftpdlib?" class="section_anchor">¶</a></h2><p>pyftpdlib is a high-level library to easily write asynchronous portable FTP servers with <a href="http://www.python.org/" rel="nofollow">Python</a>. </p><h2><a name="What_is_Python?"></a>What is Python?<a href="#What_is_Python?" class="section_anchor">¶</a></h2><p>Python is an interpreted, interactive, object-oriented, easy-to-learn programming language. It is often compared to <i>Tcl, Perl, Scheme</i> or <i>Java</i>. </p><h2><a name="I'm_not_a_python_programmer._Can_I_use_it_anyway?"></a>I'm not a python programmer. Can I use it anyway?<a href="#I%27m_not_a_python_programmer._Can_I_use_it_anyway?" class="section_anchor">¶</a></h2><p>Yes. + pyftpdlib is a fully working FTP server implementation that can be run +"as is". For example you could run an anonymous ftp server from cmd-line + by running: </p><pre class="prettyprint">giampaolo@ubuntu:~$ sudo python -m pyftpdlib.ftpserver +Serving FTP on 0.0.0.0:21</pre><p>This is useful in case you want a +quick and dirty way to share a directory without, say, installing and +configuring samba. Starting from version 0.6.0 options can be passed to +the command line (see <tt>python -m pyftpdlib.ftpserver --help</tt> to see all available options). Examples: </p><p>Anonymous FTP server with write access: </p><pre class="prettyprint">giampaolo@ubuntu:~$ sudo python -m pyftpdlib.ftpserver -w +/usr/local/lib/python2.6/site-packages/pyftpdlib/ftpserver.py:520: RuntimeWarning: Write permissions assigned to anonymous user. + RuntimeWarning) +Serving FTP on 0.0.0.0:21</pre><p>Listen on a different ip/port: </p><pre class="prettyprint">giampaolo@ubuntu:~$ python -m pyftpdlib.ftpserver -i 127.0.0.1 -p 8021 +Serving FTP on 127.0.0.1:8021</pre><p>Customizing ftpd for basic tasks +like adding users or deciding where log file should be placed is mostly +simply editing variables. This is basically like learning how to edit a +common unix ftpd.conf file and doesn't really require Python knowledge. +Customizing ftpd more deeply requires a python script which imports +pyftpdlib to be written separately. An example about how this could be +done are the scripts contained in the <a href="http://code.google.com/p/pyftpdlib/source/browse/trunk/demo" rel="nofollow">demo directory</a>. </p><h2><a name="Documentation"></a>Documentation<a href="#Documentation" class="section_anchor">¶</a></h2><p><a href="http://code.google.com/p/pyftpdlib/" rel="nofollow">http://code.google.com/p/pyftpdlib/</a> is the primary source for all information about the project including <a href="http://code.google.com/p/pyftpdlib/wiki/Install" rel="nofollow">Install instructions</a>, <a href="http://code.google.com/p/pyftpdlib/wiki/Tutorial" rel="nofollow">Tutorial</a>, <a href="http://code.google.com/p/pyftpdlib/wiki/RFCsCompliance" rel="nofollow">RFCs Compliance paper</a>, <a href="http://code.google.com/p/pyftpdlib/w/list" rel="nofollow">Wikis</a> and the <a href="http://code.google.com/p/pyftpdlib/issues/list" rel="nofollow">Bug Tracker</a>. </p><h2><a name="Mailing_lists"></a>Mailing lists<a href="#Mailing_lists" class="section_anchor">¶</a></h2><p>There are a number of mailing lists for pyftpdlib: </p><p><table><tbody><tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Name</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>E-mail</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Web Interface</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Description</strong> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <a href="http://groups.google.com/group/pyftpdlib" rel="nofollow">pyftpdlib</a> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> pyftpdlib@googlegroups.com </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <a href="http://groups.google.com/group/pyftpdlib/topics" rel="nofollow">topics</a> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> This is intended for end user support. </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <a href="http://groups.google.com/group/pyftpdlib-commit" rel="nofollow">pyftpdlib-commit</a> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> pyftpdlib-commits@googlegroups.com </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <a href="http://groups.google.com/group/pyftpdlib-commit/topics" rel="nofollow">topics</a> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> + This list receives all change notifications for code in the Subversion +repository. Unless you're a pyftpdlib developer you will probably not be + interested in it. </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <a href="http://groups.google.com/group/pyftpdlib-issues" rel="nofollow">pyftpdlib-issues</a> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> pyftpdlib-issues@googlegroups.com </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <a href="http://groups.google.com/group/pyftpdlib-issues/topics" rel="nofollow">topics</a> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> This list receives all change notifications from the <a href="http://code.google.com/p/pyftpdlib/issues/list" rel="nofollow">Bug Tracker</a>. Unless you are involved into pyftpdlib development you will probably not find this useful. </td></tr> </tbody></table></p><h2><a name="Bug_reporting"></a>Bug reporting<a href="#Bug_reporting" class="section_anchor">¶</a></h2><p>Bug reports should be made via Google Code <a href="http://code.google.com/p/pyftpdlib/issues/list" rel="nofollow">Issue Tracker</a>. Patches should be attached to the appropriate bug and not mailed directly to the mailing lists or any given team member. </p><hr><h1><a name="Installing_and_compatibility"></a>Installing and compatibility<a href="#Installing_and_compatibility" class="section_anchor">¶</a></h1><h2><a name="How_do_I_install_pyftpdlib?"></a>How do I install pyftpdlib?<a href="#How_do_I_install_pyftpdlib?" class="section_anchor">¶</a></h2><p>If you are not new to Python you probably don't need that, otherwise follow the <a href="http://code.google.com/p/pyftpdlib/wiki/Install" rel="nofollow">instructions</a>. </p><h2><a name="Which_Python_versions_are_compatible?"></a>Which Python versions are compatible?<a href="#Which_Python_versions_are_compatible?" class="section_anchor">¶</a></h2><p><strong>2.4</strong>, <strong>2.5</strong>, <strong>2.6</strong> and <strong>2.7</strong>. Python 2.3 support has been removed starting from version 0.6.0. The latest version supporting Python 2.3 is <a href="http://code.google.com/p/pyftpdlib/downloads/detail?name=pyftpdlib-0.5.2.tar.gz" rel="nofollow">pyftpdlib 0.5.2</a>. </p><h2><a name="What_about_Python_3.x?"></a>What about Python 3.x?<a href="#What_about_Python_3.x?" class="section_anchor">¶</a></h2><p>Python + 3.x is not yet covered because I still have to figure out how to deal +with the str/bytes and encoding differences introduced in the new Python + version =). A porting to Python 3.x is surely in plan, anyway (see <a href="http://code.google.com/p/pyftpdlib/issues/detail?id=76" rel="nofollow">issue #76</a>. </p><h2><a name="On_which_platforms_can_pyftpdlib_be_used?"></a>On which platforms can pyftpdlib be used?<a href="#On_which_platforms_can_pyftpdlib_be_used?" class="section_anchor">¶</a></h2><p>pyftpdlib should work on any platform where <strong><i>select()</i></strong> or <strong><i>poll()</i></strong> system calls are available and on any Python implementation which refers to <strong>cPython 2.4</strong> or superior (e.g cPython 2.6 or PythonCE 2.5). The development team has mainly tested it under various <strong>Linux</strong>, <strong>Windows</strong>, <strong>OS X</strong> and <strong>FreeBSD</strong> systems. For FreeBSD is also available a <a href="http://www.freshports.org/ftp/py-pyftpdlib/" rel="nofollow">pre-compiled package</a> maintained by Li-Wen Hsu <lwhsu@freebsd.org>. Other Python implementation like <strong><a href="http://pythonce.sourceforge.net/" rel="nofollow">PythonCE</a></strong> are known to work with pyftpdlib and every new version is usually tested against it. pyftpdlib currently does not work on <strong><a href="http://www.jython.org/" rel="nofollow">Jython</a></strong> + since the latest Jython release refers to CPython 2.2.x serie. The best + way to know whether pyftpdlib works on your platform is installing it +and running its test suite. </p><hr><h1><a name="Usage"></a>Usage<a href="#Usage" class="section_anchor">¶</a></h1><h2><a name="How_can_I_run_long-running_tasks_without_blocking_the_server?"></a>How can I run long-running tasks without blocking the server?<a href="#How_can_I_run_long-running_tasks_without_blocking_the_server?" class="section_anchor">¶</a></h2><p>pyftpdlib is an <strong>asynchronous</strong> + FTP server. That means that if you need to run a time consuming task +you have to use a separate Python process or thread for the actual +processing work otherwise the entire asynchronous loop will be blocked. </p><p>Let's + suppose you want to implement a long-running task every time the server + receives a file. The code snippet below shows the correct way to do it +by using a thread. </p><p>Notice how we first set <tt>FTPHandler.sleeping</tt> attribute to <tt>True</tt>, then we set it back to <tt>False</tt> when the long-running task has completed.<br> By setting <tt>sleeping</tt> attribute to <tt>False</tt> + we temporarily "sleep" the channel which won't be able to send or +receive any more data and won't be closed (disconnected) as long as we +don't set it back to <tt>True</tt>. This is fundamental when working with threads to avoid race conditions, dead locks etc. </p><pre class="prettyprint">class MyHandler(ftpserver.FTPHandler): + + def on_file_received(self, file): + """Called every time a file has been received""" + + def blocking_task(): + time.sleep(5) + self.sleeping = False + + self.sleeping = True + threading.Thread(target=blocking_task).start()</pre><h2><a name="Why_do_I_get_socket.error_"Permission_denied"_error_on"></a>Why do I get socket.error "Permission denied" error on ftpd starting?<a href="#Why_do_I_get_socket.error_%22Permission_denied%22_error_on" class="section_anchor">¶</a></h2><p>Probably because you're on a Unix system and you're trying to start ftpd as an unprivileged user. <i>ftpserver.py</i> + binds on port 21 by default and only super-user account (e.g. root) can + bind sockets on such ports. If you want to bind ftpd as non-privileged +user you should set a port higher than 1024. </p><h2><a name="How_can_I_prevent_the_server_version_from_being_displayed?"></a>How can I prevent the server version from being displayed?<a href="#How_can_I_prevent_the_server_version_from_being_displayed?" class="section_anchor">¶</a></h2><p>Just modify <tt>banner</tt> attribute of <tt>FTPHandler</tt> class. </p><h2><a name="Can_control_upload/download_ratios?"></a>Can control upload/download ratios?<a href="#Can_control_upload/download_ratios?" class="section_anchor">¶</a></h2><p>Yes. Starting from version 0.5.2 ftpserver.py provides a new class called <tt>ThrottledDTPHandler</tt>. You can set speed limits by modifying <tt>read_limit</tt> and <tt>write_limit</tt> class attributes as it is shown in <a href="http://pyftpdlib.googlecode.com/svn/trunk/demo/throttled_ftpd.py" rel="nofollow">throttled_ftpd.py</a> demo script. </p><h2><a name="Are_there_ways_to_limit_connections?"></a>Are there ways to limit connections?<a href="#Are_there_ways_to_limit_connections?" class="section_anchor">¶</a></h2><p><tt>FTPServer</tt> class comes with two overridable attributes defaulting to zero (no limit): <tt>max_cons</tt>, which sets a limit for maximum simultaneous connection to handle by ftpd and <tt>max_cons_per_ip</tt> + which set a limit for connections from the same IP address. Overriding +these variables is always recommended to avoid DoS attacks. </p><h2><a name="I'm_behind_a_NAT_/_gateway"></a>I'm behind a NAT / gateway<a href="#I%27m_behind_a_NAT_/_gateway" class="section_anchor">¶</a></h2><p>When + behind a NAT a ftp server needs to replace the IP local address +displayed in PASV replies and instead use the public address of the NAT +to allow client to connect. By overriding <tt>masquerade_address</tt> attribute of <tt>FTPHandler</tt> + class you will force pyftpdlib to do such replacement. However, one +problem still exists. The passive FTP connections will use ports from +1024 and up, which means that you must forward all ports 1024-65535 from + the NAT to the FTP server! And you have to allow many (possibly) +dangerous ports in your firewalling rules! To resolve this, simply +override <tt>passive_ports</tt> attribute of <tt>FTPHandler</tt> class to control what ports pyftpdlib will use for its passive data transfers. Value expected by <tt>passive_ports</tt> + attribute is a list of integers (e.g. range(60000, 65535)) indicating +which ports will be used for initializing the passive data channel. In +case you run a FTP server with multiple private IP addresses behind a +NAT firewall with multiple public IP addresses you can use <tt>FTPHandler.masquerade_address_map</tt> option which allows you to define multiple 1 to 1 mappings (<strong><i>New in 0.6.0</i></strong>). </p><h2><a name="What_is_FXP?"></a>What is FXP?<a href="#What_is_FXP?" class="section_anchor">¶</a></h2><p>FXP is part of the name of a popular Windows FTP client: <a href="http://www.flashfxp.com/" rel="nofollow">http://www.flashfxp.com</a>. + This client has made the name "FXP" commonly used as a synonym for +site-to-site FTP transfers, for transferring a file between two remote +FTP servers without the transfer going through the client's host. +Sometimes "FXP" is referred to as a protocol; in fact, it is not. The +site-to-site transfer capability was deliberately designed into <a href="http://www.faqs.org/rfcs/rfc959.html" rel="nofollow">RFC-959</a>. More info can be found here: <a href="http://www.proftpd.org/docs/howto/FXP.html" rel="nofollow">http://www.proftpd.org/docs/howto/FXP.html</a>. </p><h2><a name="Does_pyftpdlib_support_FXP?"></a>Does pyftpdlib support FXP?<a href="#Does_pyftpdlib_support_FXP?" class="section_anchor">¶</a></h2><p>Yes. It is disabled by default for security reasons (see <a href="http://gim.org.pl/rfcs/rfc2577.html" rel="nofollow">RFC-2257</a> and <a href="http://www.cert.org/advisories/CA-1997-27.html" rel="nofollow">FTP bounce attack description</a>) but in case you want to enable it just set to True the <tt>permit_foreign_addresses</tt> attribute of <tt>FTPHandler</tt> class. </p><h2><a name="Why_timestamps_shown_by_MDTM_and_ls_commands_(LIST,_MLSD,_MLST)"></a>Why timestamps shown by MDTM and ls commands (LIST, MLSD, MLST) are wrong?<a href="#Why_timestamps_shown_by_MDTM_and_ls_commands_%28LIST,_MLSD,_MLST%29" class="section_anchor">¶</a></h2><p>If + by "wrong" you mean "different from the timestamp of that file on my +client machine", then that is the expected behavior. Starting from +version 0.6.0 pyftpdlib uses <a href="http://en.wikipedia.org/wiki/Greenwich_Mean_Time" rel="nofollow">GMT times</a> as recommended in <a href="http://tools.ietf.org/html/rfc3659" rel="nofollow">RFC-3659</a>. In case you want such commands to report local times instead just set the <tt>FTPHandler.use_gmt_times</tt> attribute to <tt>False</tt>. For further information you might want to take a look at <a href="http://www.proftpd.org/docs/howto/Timestamps.html" rel="nofollow">this</a> Proftpd FAQ. </p><hr><h1><a name="Implementation"></a>Implementation<a href="#Implementation" class="section_anchor">¶</a></h1><h2><a name="Globbing_/_STAT_command_implementation"></a>Globbing / STAT command implementation<a href="#Globbing_/_STAT_command_implementation" class="section_anchor">¶</a></h2><p>Globbing + is a common Unix shell mechanism for expanding wildcard patterns to +match multiple filenames. When an argument is provided to the <strong>STAT</strong> command, ftpd should return directory listing over the command channel. <a href="http://tools.ietf.org/html/rfc959" rel="nofollow">RFC-959</a> + does not explicitly mention globbing; this means that FTP servers are +not required to support globbing in order to be compliant. However, +many FTP servers do support globbing as a measure of convenience for FTP + clients and users. In order to search for and match the given globbing +expression, the code has to search (possibly) many directories, examine +each contained filename, and build a list of matching files in memory. +Since this operation can be quite intensive, both CPU- and memory-wise, +pyftpdlib <i>does not</i> support globbing. </p><h2><a name="ASCII_transfers_/_SIZE_command_implementation"></a>ASCII transfers / SIZE command implementation<a href="#ASCII_transfers_/_SIZE_command_implementation" class="section_anchor">¶</a></h2><p>Properly + handling the SIZE command when TYPE ASCII is used would require to scan + the entire file to perform the ASCII translation logic +(file.read().replace(os.linesep, '\r\n')) and then calculating the len +of such data which may be different than the actual size of the file on +the server. Considering that calculating such result could be very +resource-intensive it could be easy for a malicious client to try a DoS +attack, thus pyftpdlib rejects SIZE when the current TYPE is ASCII. +However, clients in general should not be resuming downloads in ASCII +mode. Resuming downloads in binary mode is the recommended way as +specified in <a href="http://tools.ietf.org/html/rfc3659" rel="nofollow">RFC-3659</a>. </p><h2><a name="IPv6_support"></a>IPv6 support<a href="#IPv6_support" class="section_anchor">¶</a></h2><p>Starting from version 0.4.0 pyftpdlib <i>supports</i> IPv6 (<a href="http://tools.ietf.org/html/rfc2428" rel="nofollow">RFC-2428</a>). + If you use IPv6 and want your FTP daemon to do so just pass a valid +IPv6 address to the FTPServer class constructor. Example: </p><pre class="prettyprint">>>> from pyftpdlib import ftpserver +>>> address = ("::1", 21) # listen on localhost, port 21 +>>> ftpd = ftpserver.FTPServer(address, ftpserver.FTPHandler) +>>> ftpd.serve_forever() +Serving FTP on ::1:21</pre><p>If your OS (for example: all recent UNIX +systems) have an hybrid dual-stack IPv6/IPv4 implementation the code +above will listen on both IPv4 and IPv6 by using a single IPv6 socket (<i><strong>New in 0.6.0</strong></i>). </p><h2><a name="How_do_I_install_IPv6_support_on_my_system?"></a>How do I install IPv6 support on my system?<a href="#How_do_I_install_IPv6_support_on_my_system?" class="section_anchor">¶</a></h2><p>If + you want to install IPv6 support on Linux run "modprobe ipv6", then +"ifconfig". This should display the loopback adapter, with the address +"::1". You should then be able to listen the server on that address, and + connect to it. </p><p>On Windows (XP SP2 and higher) run "netsh int ipv6 install". Again, you should be able to use IPv6 loopback afterwards. </p><h2><a name="Can_pyftpdlib_be_integrated_with_"real"_users_existing"></a>Can pyftpdlib be integrated with "real" users existing on the system?<a href="#Can_pyftpdlib_be_integrated_with_%22real%22_users_existing" class="section_anchor">¶</a></h2><p>Yes. Starting from version 0.6.0 pyftpdlib provides the new <tt>UnixAuthorizer</tt> and <tt>WindowsAuthorizer</tt> + classes. By using them pyftpdlib can look into the system account +database to authenticate users. They also assume the id of real users +every time the FTP server is going to access the filesystem (e.g. for +creating or renaming a file) the authorizer will temporarily impersonate + the currently logged on user, execute the filesystem call and then +switch back to the user who originally started the server. Example UNIX +and Windows FTP servers contained in the <a href="http://code.google.com/p/pyftpdlib/source/browse/#svn/trunk/demo" rel="nofollow">demo directory</a> shows how to use <tt>UnixAuthorizer</tt> and <tt>WindowsAuthorizer</tt> classes. </p><h2><a name="Does_pyftpdlib_support_FTP_over_TLS/SSL_(FTPS)"></a>Does pyftpdlib support FTP over TLS/SSL (FTPS)<a href="#Does_pyftpdlib_support_FTP_over_TLS/SSL_%28FTPS%29" class="section_anchor">¶</a></h2><p>Yes, starting from version 0.6.0, see: <a href="http://code.google.com/p/billiejoex/wiki/Tutorial#3.7_-_FTPS_%28FTP_over_TLS/SSL%29_server" rel="nofollow">http://code.google.com/p/billiejoex/wiki/Tutorial#3.7_-_FTPS_(FTP_over_TLS/SSL)_server</a> </p><h2><a name="What_about_SITE_commands?"></a>What about SITE commands?<a href="#What_about_SITE_commands?" class="section_anchor">¶</a></h2><p>No SITE commands aside from <strong>SITE HELP</strong> are implemented by default. The user willing to add support for a specific SITE command (e.g. <strong>SITE CHMOD</strong>) has to define a new <tt>ftp_SITE_%CMD%</tt> method in the <tt>FTPHandler</tt> subclass and add a new entry in <tt>ftpserver.proto_cmds</tt> dictionary. Example: </p><pre class="prettyprint">from pyftpdlib import ftpserver + +prop = (None, True, None, False, 'Syntax: SITE CHMOD <SP> path (change file permission).') +ftpserver.proto_cmds['SITE CHMOD'] = ftpserver._CommandProperty(*prop) + +class CustomizedFTPHandler(ftpserver.FTPHandler): + + def ftp_SITE_CHMOD(self, line): + """Change file permissions.""" + ...</pre> + </td> + </tr> + </tbody></table> + </div> + + + + +<script type="text/javascript" src="faq_files/dit_scripts.js"></script> + + + + </body></html> diff --git a/chromium/third_party/pyftpdlib/src/doc/index.html b/chromium/third_party/pyftpdlib/src/doc/index.html new file mode 100644 index 00000000000..94e0a2b9b90 --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/doc/index.html @@ -0,0 +1,100 @@ +<html><head> +<meta http-equiv="content-type" content="text/html; charset=UTF-8"> +<style> +.section_anchor { + font-size:0px; +} +</style> + + + + <title>Home</title> + </head><body> + + + + + <div id="wikicontent"> + <table border="0" cellpadding="0" cellspacing="0" width="100%"> + <tbody><tr> + + <td class="vt" id="wikimaincol" width="100%"> + + <div id="wikiheader" style="margin-bottom: 1em;"> + + + <span style="font-size: 120%; font-weight: bold;">Home</span> + + + + </div> + <h1><a name="Python_FTP_server_library_(pyftpdlib)"></a>Python FTP server library (pyftpdlib)<a href="#Python_FTP_server_library_%28pyftpdlib%29" class="section_anchor">¶</a></h1><h2><a name="About"></a>About<a href="#About" class="section_anchor">¶</a></h2><p>Python FTP server library provides a high-level portable interface to easily write asynchronous FTP servers with Python.<br>pyftpdlib is currently the most complete <a href="http://www.faqs.org/rfcs/rfc959.html" rel="nofollow">RFC-959</a> FTP server implementation available for <a href="http://www.python.org/" rel="nofollow">Python</a> programming language.<br>It is used in projects like <a href="http://www.code.google.com/chromium/" rel="nofollow">Google Chromium</a> and <a href="http://bazaar-vcs.org/" rel="nofollow">Bazaar</a> and included in <a href="http://packages.debian.org/sid/python-pyftpdlib" rel="nofollow">Debian</a>, <a href="https://admin.fedoraproject.org/pkgdb/packages/name/pyftpdlib" rel="nofollow">Fedora</a> and <a href="http://www.freshports.org/ftp/py-pyftpdlib/" rel="nofollow">FreeBSD</a> package repositories. </p><h2><a name="Features"></a>Features<a href="#Features" class="section_anchor">¶</a></h2><ul><li>Support for <strong><a href="http://en.wikipedia.org/wiki/FTPS" rel="nofollow">FTPS</a></strong> (FTP over TLS/SSL). <i><strong>New in 0.6.0</strong></i> </li><li>Native support for virtual users and virtual filesystem. </li><li>Support for recent FTP commands like <strong>MLSD</strong> and <strong>MLST</strong> (<a href="http://www.faqs.org/rfcs/rfc3659.html" rel="nofollow">RFC-3659</a>). </li><li>Support for <strong><a href="http://www.proftpd.org/docs/howto/FXP.html" rel="nofollow">FXP</a></strong>, site-to-site transfers. </li><li>Support for <strong>IPv6</strong> (<a href="ftp://ftp.rfc-editor.org/in-notes/rfc2428.txt" rel="nofollow">RFC-2428</a>). </li><li>NAT/Firewall support with <strong>PASV/EPSV</strong> passive mode connections. </li><li>Bandwidth throttling. </li><li>Support for resumed transfers. </li><li>Per-user permissions configurability. </li><li>Maximum connections limit. </li><li>Per-source-IP limits. </li><li>Configurable idle timeouts for both control and data channels. </li><li>Compact: main library is distributed as a single stand-alone module (<a href="http://pyftpdlib.googlecode.com/svn/trunk/pyftpdlib/ftpserver.py" rel="nofollow">ftpserver.py</a>). </li><li>High portability: </li><ul><li>Entirely written in pure Python, no third party modules are used. It works on any system where <i>select( )</i> or <i>poll( )</i> is available. </li><li>Extremely flexible system of "authorizers" able to manage both "virtual" and "real" users on different platforms (<strong>Windows</strong>, <strong>UNIX</strong>, <strong>OSX</strong>). </li><li>Works with Python 2.4, 2.5, 2.6 and 2.7. </li></ul></ul><h2><a name="Quick_start"></a>Quick start<a href="#Quick_start" class="section_anchor">¶</a></h2><pre class="prettyprint">>>> from pyftpdlib import ftpserver +>>> authorizer = ftpserver.DummyAuthorizer() +>>> authorizer.add_user("user", "12345", "/home/user", perm="elradfmw") +>>> authorizer.add_anonymous("/home/nobody") +>>> handler = ftpserver.FTPHandler +>>> handler.authorizer = authorizer +>>> address = ("127.0.0.1", 21) +>>> ftpd = ftpserver.FTPServer(address, handler) +>>> ftpd.serve_forever() +Serving FTP on 127.0.0.1:21 +[]127.0.0.1:2503 connected. +127.0.0.1:2503 ==> 220 Ready. +127.0.0.1:2503 <== USER anonymous +127.0.0.1:2503 ==> 331 Username ok, send password. +127.0.0.1:2503 <== PASS ****** +127.0.0.1:2503 ==> 230 Login successful. +[anonymous]@127.0.0.1:2503 User anonymous logged in. +127.0.0.1:2503 <== TYPE A +127.0.0.1:2503 ==> 200 Type set to: ASCII. +127.0.0.1:2503 <== PASV +127.0.0.1:2503 ==> 227 Entering passive mode (127,0,0,1,9,201). +127.0.0.1:2503 <== LIST +127.0.0.1:2503 ==> 150 File status okay. About to open data connection. +[anonymous]@127.0.0.1:2503 OK LIST "/". Transfer starting. +127.0.0.1:2503 ==> 226 Transfer complete. +[anonymous]@127.0.0.1:2503 Transfer complete. 706 bytes transmitted. +127.0.0.1:2503 <== QUIT +127.0.0.1:2503 ==> 221 Goodbye. +[anonymous]@127.0.0.1:2503 Disconnected.</pre><h2><a name="Discussion_group"></a>Discussion group<a href="#Discussion_group" class="section_anchor">¶</a></h2><p><a href="http://groups.google.com/group/pyftpdlib/topics" rel="nofollow">http://groups.google.com/group/pyftpdlib/topics</a> </p><h2><a name="Timeline"></a>Timeline<a href="#Timeline" class="section_anchor">¶</a></h2><ul><li>2010-11-07: version <a href="http://pyftpdlib.googlecode.com/files/pyftpdlib-0.6.0.tar.gz" rel="nofollow">0.6.0</a> released. </li><li>2010-08-24: pyftpdlib included in <a href="http://www.peerscape.org/" rel="nofollow">peerscape</a> project. </li><li>2010-07-15: pyftpdlib included in <a href="http://tomatohater.com/faetus/" rel="nofollow">Faetus</a> project. </li><li>2010-07-11: pyftpdlib included in <a href="http://code.google.com/p/pyfilesystem" rel="nofollow">Pyfilesystem</a> project. </li><li>2010-06-28: pyftpdlib has been <a href="http://packages.debian.org/sid/python-pyftpdlib" rel="nofollow">packaged for Debian</a> </li><li>2010-04-28: pyftpdlib included in <a href="http://forge.openbravo.com/plugins/mwiki/index.php/MobilePOS" rel="nofollow">sierramodulepos</a> project. </li><li>2010-03-20: <a href="http://www.smartfile.com/" rel="nofollow">http://www.smartfile.com</a> uses pyftpdlib. </li><li>2010-01-13: pyftpdlib included in <a href="http://code.irondojo.com/" rel="nofollow">zenftp</a> project. </li><li>2009-12-26: pyftpdlib included in <a href="http://code.google.com/p/sypftp" rel="nofollow">Symbian Python FTP server</a> project. </li><li>2009-11-04: <a href="http://www.netplay.it/" rel="nofollow">www.netplay.it</a> uses pyftpdlib. </li><li>2009-11-04: <a href="http://www.adcast.tv/" rel="nofollow">www.adcast.tv</a> uses pyftpdlib. </li><li>2009-11-04: <a href="http://www.bitsontherun.com/" rel="nofollow">www.bitsontherun.com</a> uses pyftpdlib. </li><li>2009-11-02: pyftpdlib included in <a href="http://github.com/chmouel/ftp-cloudfs" rel="nofollow">ftp-cloudfs</a> project. </li><li>2009-09-14: version <a href="http://pyftpdlib.googlecode.com/files/pyftpdlib-0.5.2.tar.gz" rel="nofollow">0.5.2</a> released. </li><li>2009-08-10: pyftpdlib included in <a href="http://github.com/wuzhe/imgserve/tree/master" rel="nofollow">Imgserve</a> project. </li><li>2009-07-22: pyftpdlib included in <a href="http://plumi.org/wiki" rel="nofollow">Plumi</a> project. </li><li>2009-04-02: pyftpdlib RPM-packaged and ported on <a href="https://admin.fedoraproject.org/pkgdb/packages/name/pyftpdlib" rel="nofollow">Fedora</a> to make users can easily install on it via <i>yum install pyftpdlib</i>. </li><li>2009-03-28: pyftpdlib included in <a href="http://bazaar-vcs.org/" rel="nofollow">Bazaar</a> project. </li><li>2009-02-23: pyftpdlib included in <a href="http://git.logfish.net/shareftp.git/" rel="nofollow">ShareFTP</a> project. </li><li>2009-01-21: version <a href="http://pyftpdlib.googlecode.com/files/pyftpdlib-0.5.1.tar.gz" rel="nofollow">0.5.1</a> released. </li><li>2008-12-27: pyftpdlib included in <a href="http://code.google.com/intl/it-IT/chromium/" rel="nofollow">Google Chromium</a>, the open source project behind <a href="http://www.google.com/chrome" rel="nofollow">Google Chrome</a>. </li><li>2008-12-27: pyftpdlib ported on <a href="http://www.gnu-darwin.org/" rel="nofollow">GNU Darwin</a> systems to make users can easily install on it. </li><li>2008-11-26: pyftpdlib included in <a href="http://openerp.com/" rel="nofollow">OpenERP</a>. </li><li>2008-10-26: pyftpdlib included in <a href="http://www.vmspython.org/" rel="nofollow">Python for OpenVMS</a> as standard package. </li><li>2008-10-09: pyftpdlib included in <a href="http://bbs.archlinux.org/viewtopic.php?pid=431474" rel="nofollow">Shareme</a> project. </li><li>2008-09-20: version <a href="http://pyftpdlib.googlecode.com/files/pyftpdlib-0.5.0.tar.gz" rel="nofollow">0.5.0</a> released. </li><li>2008-08-10: pyftpdlib included in <a href="http://trac.manent-backup.com/" rel="nofollow">Manent</a> project. </li><li>2008-05-16: version <a href="http://pyftpdlib.googlecode.com/files/pyftpdlib-0.4.0.tar.gz" rel="nofollow">0.4.0</a> released. </li><li>2008-04-09: pyftpdlib used as backend for <a href="http://arkadiusz-wahlig.blogspot.com/2008/04/hosting-files-on-google.html" rel="nofollow">gpftpd</a>, an FTP server for managing files hosted on <a href="http://pages.google.com/" rel="nofollow">Google Pages</a>. </li><li>2008-01-17: version <a href="http://pyftpdlib.googlecode.com/files/pyftpdlib-0.3.0.tar.gz" rel="nofollow">0.3.0</a> released. </li><li>2007-10-14: pyftpdlib included in <a href="http://walco.n--tree.net/projects/aksy/wiki" rel="nofollow">Aksy</a> project. </li><li>2007-09-17: version <a href="http://pyftpdlib.googlecode.com/files/pyftpdlib_0.2.0.tar.gz" rel="nofollow">0.2.0</a> released. </li><li>2007-09-08: pyftpdlib included as <a href="http://farmanager.com/" rel="nofollow">FarManager</a> <a href="http://www.farmanager.com/enforum/viewtopic.php?t=640&highlight=&sid=12d4d90f27f421243bcf7a0e3c516efb" rel="nofollow">plug-in</a>. </li><li>2007-03-06: pyftpdlib <a href="http://www.freshports.org/ftp/py-pyftpdlib/" rel="nofollow">ported on FreeBSD</a> systems to make users can easily install on it. </li><li>2007-03-07: version <a href="http://pyftpdlib.googlecode.com/files/pyftpdlib_0.1.1.tar.gz" rel="nofollow">0.1.1</a> released. </li><li>2007-02-26: version <a href="http://pyftpdlib.googlecode.com/files/pyftpdlib_0.1.tar.gz" rel="nofollow">0.1.0</a> released. </li></ul><h2><a name="Contribute"></a>Contribute<a href="#Contribute" class="section_anchor">¶</a></h2><p>If you want to help or just give us suggestions about the project and other related things, subscribe to the <a href="http://groups.google.com/group/pyftpdlib" rel="nofollow">discussion mailing list</a>. + If you want to talk with project team members about pyftpdlib and other + related things feel free to contact us at the following addresses: </p><p><table><tbody><tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Name</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Country</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>E-mail</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Description</strong> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Giampaolo Rodola' </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Turin (Italy) </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> g.rodola at gmail dot com </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Original pyftpdlib author and main maintainer. </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Jay Loden </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> New Jersey (USA) </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> jloden at gmail dot com </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> OS X and Linux platform development/testing </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Janos Guljas </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> janos at janos.in dot rs </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Debian package maintainer </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Silas Sewell </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Denver (USA) </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> silas at sewell dot ch </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Fedora package maintainer </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Li-Wen Hsu </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Taiwan </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> lwhsu at freebsd dot org </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> FreeBSD package maintainer </td></tr> </tbody></table></p><p>Feedbacks and suggestions are greatly appreciated as well as new testers and coders willing to join the development.<br> For any bug report, patch proposal or feature request, add an entry into the <a href="http://code.google.com/p/pyftpdlib/issues/list" rel="nofollow">Issue Tracker</a>.<br> In case you're using pyftpdlib into a software or website of yours, please update the pyftpdlib <a href="http://code.google.com/p/pyftpdlib/wiki/Adoptions" rel="nofollow">Adoptions List</a> by adding a comment in the Wiki. </p><p>Thank you. </p><h2><a name="Statistics"></a>Statistics<a href="#Statistics" class="section_anchor">¶</a></h2><p><table><tbody><tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> +<script src="index_files/rpc.js"></script> +<script type="text/javascript"> +function resizeIframeHandler(opt_height) { + var elem = document.getElementById(this.f); + if (!elem) return; + if (!opt_height) { + elem.style.height = undefined; + } + else { + opt_height = Math.max(10, opt_height); + elem.style.height = opt_height + 'px'; + } +} +gadgets.rpc.register("resize_iframe", resizeIframeHandler); + +gadgets.rpc.register('set_title', function(title) { + var elem = document.getElementById(this.f + '_title'); + if (elem) { + elem.innerHTML = gadgets.util.escape(title); + } +}); +</script> +<h2 id="gadget0_title" class="gadget-title"></h2><iframe src="index_files/ifr.html" id="gadget0" name="gadget0" frameborder="0" height="200" width="340"></iframe> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <h2 id="gadget1_title" class="gadget-title"></h2><iframe src="index_files/ifr_002.html" id="gadget1" name="gadget1" frameborder="0" height="220"></iframe> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <h2 id="gadget2_title" class="gadget-title"></h2><iframe src="index_files/ifr_003.html" id="gadget2" name="gadget2" frameborder="0" height="250"></iframe> </td></tr> </tbody></table></p><h2><a name="Trademarks"></a>Trademarks<a href="#Trademarks" class="section_anchor">¶</a></h2><p>Some famous trademarks which adopted pyftpdlib (<a href="http://code.google.com/p/pyftpdlib/wiki/Adoptions" rel="nofollow">complete list</a>). </p><p><p></p><p></p><p></p><p></p><p></p><table> <tbody><tr><td valign="bottom"> <a href="http://www.google.com/chrome" title="Google Chrome" rel="nofollow"> <img src="index_files/logo_sm.jpg" alt="Google Chrome"> </a> <br><br><br><br> </td> <td valign="bottom"> <a href="http://www.debian.org/" title="Linux Debian" rel="nofollow"> <img src="index_files/100px_debian_logo.png" alt="Linux Debian"> </a> </td> <td valign="bottom"> <a href="http://fedoraproject.org/" title="Linux Fedora" rel="nofollow"> <img src="index_files/fedora-logo.png" alt="Linux Fedora"> </a> </td> <td valign="bottom"> <a href="http://www.freebsd.org/" title="Free BSD" rel="nofollow"> <img src="index_files/beastie.gif" alt="Free BSD"> </a> </td> <td valign="bottom"> <a href="http://openerp.com/" title="Open ERP" rel="nofollow"> <blockquote><img src="index_files/openlogo.png" alt="Open ERP"> +</blockquote></a> </td> </tr></tbody></table> </p><p><p></p><p></p><p></p><p></p><p></p><table> <tbody><tr><td valign="top"> <br><br> <a href="http://bazaar-vcs.org/" title="Bazaar control revision system" rel="nofollow"> <img src="index_files/bazaar-logo.jpg" alt="Bazaar control revision system"> </a> </td> <td valign="top"> <a href="http://www.bitsontherun.com/" title="Bits On The Run" rel="nofollow"> <blockquote><img src="index_files/bits-on-the-run-logo-200x411.xml" alt="Bits On The Run"> +</blockquote></a> </td> <td> <a href="http://www.bitsontherun.com/" title="Python for OpenVMS" rel="nofollow"> <img src="index_files/wwwovmsorg_logo.png" alt="Python for OpenVMS"> </a> </td> <td> <a href="http://www.smartfile.com/" title="Smartfile" rel="nofollow"> <blockquote><img src="index_files/logo.gif" alt="Smartfile"> +</blockquote></a> </td> </tr></tbody></table> </p> + </td> + </tr> + </tbody></table> + </div> + + + + +<script type="text/javascript" src="index_files/dit_scripts.js"></script> + + + + </body></html> diff --git a/chromium/third_party/pyftpdlib/src/doc/install.html b/chromium/third_party/pyftpdlib/src/doc/install.html new file mode 100644 index 00000000000..d60cec7820a --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/doc/install.html @@ -0,0 +1,51 @@ +<html><head> +<meta http-equiv="content-type" content="text/html; charset=UTF-8"> +<style> +.section_anchor { + font-size:0px; +} +</style> + + + + + + <title>Install</title> + </head><body> + + + + + <div id="wikicontent"> + <table border="0" cellpadding="0" cellspacing="0" width="100%"> + <tbody><tr> + + <td class="vt" id="wikimaincol" width="100%"> + + <div id="wikiheader" style="margin-bottom: 1em;"> + + + <span style="font-size: 120%; font-weight: bold;">Install</span> + + + + <div style="font-style: italic; margin-top: 3px;">Instructions for installing pyftpdlib (for those really new to Python).</div> + + </div> + <h1><a name="Requirements"></a>Requirements<a href="#Requirements" class="section_anchor">¶</a></h1><p>Python 2.3 or higher (not Python 3.x). You can get the latest 2.x stable Python release from here: <a href="http://www.python.org/download/" rel="nofollow">http://www.python.org/download/</a>. </p><h1><a name="Using_easy_install_/_setuptools"></a>Using easy_install / setuptools<a href="#Using_easy_install_/_setuptools" class="section_anchor">¶</a></h1><p>If you have <a href="http://pypi.python.org/pypi/setuptools" rel="nofollow">easy_install / setuptools</a> on your system, installing pyftpdlib is quite simple. Just run: </p><pre class="prettyprint">easy_install pyftpdlib</pre><p>This will get the most updated pyftpdlib from the Python <a href="http://pypi.python.org/pypi" rel="nofollow">pypi repository</a>, unpack it and install it automatically. </p><p>Note: + if you already have an old version of pyftpdlib installed, easy_install + will not automatically download the latest version. You can ask for a +particular version by running, for example: </p><pre class="prettyprint">easy_install.py pyftpdlib==0.3.0</pre><h1><a name="Manual_installation"></a>Manual installation<a href="#Manual_installation" class="section_anchor">¶</a></h1><p>If you have downloaded a pyftpdlib package, follow the following steps: </p><p>Unpack it (Windows users could use 7Zip, WinRar or other similar program): </p><pre class="prettyprint">tar zxvf pyftpdlib-0.3.0.tar.gz</pre><p>Change to the pyftpdlib directory: </p><pre class="prettyprint">cd pyftpdlib</pre><p>Run setup.py to install pyftpdlib. This step need to be run as root. </p><pre class="prettyprint">python setup.py install</pre><p>If you're on Windows just run: </p><pre class="prettyprint">setup.py install</pre> + </td> + </tr> + </tbody></table> + </div> + + + + +<script type="text/javascript" src="install_files/dit_scripts.js"></script> + + + + </body></html> diff --git a/chromium/third_party/pyftpdlib/src/doc/logo.png b/chromium/third_party/pyftpdlib/src/doc/logo.png Binary files differnew file mode 100644 index 00000000000..f4943d7baff --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/doc/logo.png diff --git a/chromium/third_party/pyftpdlib/src/doc/release-notes.html b/chromium/third_party/pyftpdlib/src/doc/release-notes.html new file mode 100644 index 00000000000..e51759057f6 --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/doc/release-notes.html @@ -0,0 +1,63 @@ + +<!-- saved from url=(0067)http://code.google.com/p/pyftpdlib/wiki/ReleaseNotes06?show=content --> +<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> + <title>ReleaseNotes06</title> + </head> + <body> +<style> +.section_anchor { + font-size:0px; +} +</style> + + + + + <div id="wikicontent"> + <table width="100%" border="0" cellspacing="0" cellpadding="0"> + <tbody><tr> + + <td class="vt" id="wikimaincol" width="100%"> + + <div id="wikiheader" style="margin-bottom:1em"> + + <img width="15" height="15" id="star_img" src="./release-notes_files/star_off.gif" style="cursor:pointer" onclick="_CS_toggleStar(this, + '', + {'scope': 'wiki', + 'user': '_CURRENT_USER', + 'item': 'pyftpdlib:ReleaseNotes06', + 'token': codesite_token + });"> + + + <span style="font-size:120%;font-weight:bold">ReleaseNotes06</span> + + + + <div style="font-style:italic; margin-top: 3px">pyftpdlib 0.6.x series release notes.</div> + + </div> + <h1><a name="Version:_0.6.0_-_Date:_2010-01-24"></a>Version: 0.6.0 - Date: 2010-01-24<a href="http://code.google.com/p/pyftpdlib/wiki/ReleaseNotes06?show=content#Version:_0.6.0_-_Date:_2010-01-24" class="section_anchor">¶</a></h1><h2><a name="Enhancements"></a>Enhancements<a href="http://code.google.com/p/pyftpdlib/wiki/ReleaseNotes06?show=content#Enhancements" class="section_anchor">¶</a></h2><ul><li><a title="Add SSL/TLS support to pyftpdlib [RFCs 2228 & 4217]" href="http://code.google.com/p/pyftpdlib/issues/detail?id=68">Issue 68</a>: added full FTPS (FTP over SSL/TLS) support. </li><li><a title="MDTM should return time in GMT" href="http://code.google.com/p/pyftpdlib/issues/detail?id=86">Issue 86</a>: pyftpdlib now reports all ls and MDTM timestamps as GMT times, as recommended in RFC-3659. A <tt>FTPHandler.use_gmt_times</tt> attributed has been added and can be set to False in case local times are desired instead. </li><li><a title="Provide a command line parser to configure the FTP server when run with python's -m option" href="http://code.google.com/p/pyftpdlib/issues/detail?id=124">Issue 124</a>: pyftpdlib now accepts command line options to configure a stand alone anonymous FTP server when running pyftpdlib with python's -m option. </li><li><a title="ECONNREFUSED when using masquerade_address with multiple listening IPs." href="http://code.google.com/p/pyftpdlib/issues/detail?id=127">Issue 127</a>: <tt>added FTPHandler.masquerade_address_map</tt> option which allows you to define multiple 1 to 1 mappings in case you run a FTP server with multiple private IP addresses behind a NAT firewall with multiple public IP addresses. </li><li><a title="user name and group name should be resolved via AbstractedFS." href="http://code.google.com/p/pyftpdlib/issues/detail?id=128">Issue 128</a>: files and directories owner and group names and <tt>os.readlink</tt> are now resolved via <tt>AbstractedFS</tt> methods instead of in <tt>format_list()</tt>. </li><li><a title="Callbacks for incomplete file transfers and user login" href="http://code.google.com/p/pyftpdlib/issues/detail?id=129">Issue 129</a>: added 3 new callbacks to <tt>FTPHandler</tt> class: <tt>on_incomplete_file_sent()</tt>, <tt>on_incomplete_file_received()</tt> and <tt>on_login()</tt>. </li><li><a title="Move Unix and Windows authorizers from demo directory to pyftpdlib/contrib" href="http://code.google.com/p/pyftpdlib/issues/detail?id=130">Issue 130</a>: added <tt>UnixAuthorizer</tt> and <tt>WindowsAuthorizer</tt> classes defined in the new <tt>pyftpdlib.contrib.authorizers</tt> module. </li><li><a title="Support both IPv4 and IPv6 by using a single socket" href="http://code.google.com/p/pyftpdlib/issues/detail?id=131">Issue 131</a>: pyftpdlib is now able to serve both IPv4 and IPv6 at the same time by using a single socket. </li><li><a title="AbstractedFS changes" href="http://code.google.com/p/pyftpdlib/issues/detail?id=133">Issue 133</a>: <tt>AbstractedFS</tt> constructor now accepts two argumets: <tt>root</tt> and <tt>cmd_channel</tt> breaking compatibility with previous version. Also, <tt>root</tt> and <tt>cwd</tt> attributes became properties. The previous bug consisting in re-setting the root from the ftp handler after user login has been fixed to ease the development of subclasses. </li><li><a title="Enable TCP_NODELAY socket option" href="http://code.google.com/p/pyftpdlib/issues/detail?id=134">Issue 134</a>: enabled TCP_NODELAY socket option for the FTP command channels resulting in pyftpdlib being twice faster. </li><li><a title="Remove python 2.3 support" href="http://code.google.com/p/pyftpdlib/issues/detail?id=135">Issue 135</a>: Python 2.3 support has been removed. </li><li><a title="Implement UnixFilesystem class" href="http://code.google.com/p/pyftpdlib/issues/detail?id=137">Issue 137</a>: added new <tt>pyftpdlib.contrib.filesystems</tt> module within <tt>UnixFilesystem</tt> class which permits the client to escape its home directory and navigate the real filesystem. </li><li><a title="Calculate data transfer elapsed time" href="http://code.google.com/p/pyftpdlib/issues/detail?id=138">Issue 138</a>: added <tt>DTPHandler.get_elapsed_time()</tt> method which returns the transfer elapsed time in seconds. </li></ul><h2><a name="Bugfixes"></a>Bugfixes<a href="http://code.google.com/p/pyftpdlib/wiki/ReleaseNotes06?show=content#Bugfixes" class="section_anchor">¶</a></h2><ul><li><a title="race condition on PORT connections" href="http://code.google.com/p/pyftpdlib/issues/detail?id=120">Issue 120</a>: an <tt>ActiveDTP</tt> instance is not garbage collected in case a client issuing PORT disconnects before establishing the data connection. </li><li><a title="a bug in verifying path" href="http://code.google.com/p/pyftpdlib/issues/detail?id=122">Issue 122</a>: a wrong variable name was used in <tt>AbstractedFS.validpath</tt> method. </li><li><a title="PORT commands fail to bind to command channel ip address" href="http://code.google.com/p/pyftpdlib/issues/detail?id=123">Issue 123</a>: PORT command doesn't bind to correct address in case an alias is created for the local network interface. </li><li><a title="PWD response should escape double quotes" href="http://code.google.com/p/pyftpdlib/issues/detail?id=140">Issue 140</a>: pathnames returned in PWD response should have double-quotes '"' escaped. </li></ul><h2><a name="API_changes_since_0.5.2"></a>API changes since 0.5.2<a href="http://code.google.com/p/pyftpdlib/wiki/ReleaseNotes06?show=content#API_changes_since_0.5.2" class="section_anchor">¶</a></h2><ul><li>removed support for Python 2.3. </li><li>all classes are now new-style classes. </li><li>Added a new package in pyftpdlib namespace: "contrib". Modules (and classes) defined here: </li><ul><li><tt>pyftpdlib.contrib.handlers.py</tt> (<tt>TLS_FTPHandler</tt>, <tt>TLS_FTPHandlerFactory</tt>) </li><li><tt>pyftpdlib.contrib.authorizers.py</tt> (<tt>UnixAuthorizer</tt>, <tt>WindowsAuthorizer</tt>) </li><li><tt>pyftpdlib.contrib.filesystems</tt> (<tt>UnixFilesystem</tt>) </li></ul><li><tt>AbstractedFS</tt> class: </li><ul><li><tt>__init__</tt> method now accepts two arguments: <tt>root</tt> and <tt>cmd_channel</tt>. </li><li><tt>root</tt> and <tt>cwd</tt> attributes are now read-only properties. </li><li>3 new methods have been added: </li><ul><li><tt>get_user_by_uid()</tt> </li><li><tt>get_group_by_gid()</tt> </li><li><tt>readlink()</tt> </li></ul></ul><li><tt>FTPHandler</tt> class: </li><ul><li>new class attributes: </li><ul><li><tt>use_gmt_times</tt> </li><li><tt>tcp_no_delay</tt> </li><li><tt>masquerade_address_map</tt> </li></ul><li>new methods: </li><ul><li><tt>on_incomplete_file_sent()</tt> </li><li><tt>on_incomplete_file_received()</tt> </li><li><tt>on_login()</tt> </li></ul><li><tt>proto_cmds</tt> class attribute has been added. The <tt>FTPHandler</tt> class no longer relies on <tt>ftpserver.proto_cmds</tt> global dictionary but on <tt>ftpserver.FTPHandler.proto_cmds</tt> instead. </li></ul><li><tt>FTPServer</tt> class: </li><ul><li><tt>max_cons</tt> attribute defaults to 512 by default instead of 0 (unlimited). </li></ul><li><tt>DummyAuthorizer</tt> class: </li><ul><li><tt>ValueError</tt> exceptions are now raised instead of <tt>AuthorizerError</tt> </li></ul><li><tt>DTPHandler</tt> class: </li><ul><li><tt>get_elapsed_time()</tt> method has been added. </li></ul></ul><h2><a name="Migration_notes"></a>Migration notes<a href="http://code.google.com/p/pyftpdlib/wiki/ReleaseNotes06?show=content#Migration_notes" class="section_anchor">¶</a></h2><p>Some of the changes introduced in 0.6.0 break compatibility with previous 0.5.x serie. In particular you should be careful in case you're using a customized file system class defining an <tt>__init__</tt> method since <tt>AbstractedFS</tt> constructor now expects two arguments. </p> + </td> + </tr> + </tbody></table> + </div> + + + + +<script type="text/javascript" src="./release-notes_files/dit_scripts.js"></script> + + + <script type="text/javascript"> + _fetchOptions( + "", "pyftpdlib", "wikiOptions", + codesite_token, 1240514232); + _onload(); + </script> + + + + + +</body></html> diff --git a/chromium/third_party/pyftpdlib/src/doc/rfcs-compliance.html b/chromium/third_party/pyftpdlib/src/doc/rfcs-compliance.html new file mode 100644 index 00000000000..a8da3f6551c --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/doc/rfcs-compliance.html @@ -0,0 +1,84 @@ +<html><head> +<meta http-equiv="content-type" content="text/html; charset=UTF-8"> +<style> +.section_anchor { + font-size:0px; +} +</style> + + + + + <title>RFCsCompliance</title> + </head><body> + + + + + <div id="wikicontent"> + <table border="0" cellpadding="0" cellspacing="0" width="100%"> + <tbody><tr> + + <td class="vt" id="wikimaincol" width="100%"> + + <div id="wikiheader" style="margin-bottom: 1em;"> + + + <span style="font-size: 120%; font-weight: bold;">RFCsCompliance</span> + + + + <div style="font-style: italic; margin-top: 3px;">A paper showing pyftpdlib compliance against FTP protocol standard RFCs.</div> + + </div> + <h1><a name="Table_of_contents"></a>Table of contents<a href="#Table_of_contents" class="section_anchor">¶</a></h1><p></p><ul><li><a href="#Table_of_contents">Table of contents</a></li><li><a href="#Introduction">Introduction</a></li><li><a href="#RFC-959_-_File_Transfer_Protocol">RFC-959 - File Transfer Protocol</a></li><li><a href="#RFC-1123_-_Requirements_for_Internet_Hosts">RFC-1123 - Requirements for Internet Hosts</a></li><li><a href="#RFC-2228_-_FTP_Security_Extensions">RFC-2228 - FTP Security Extensions</a></li><li><a href="#RFC-2389_-_Feature_negotiation_mechanism_for_the_File_Transfer_P">RFC-2389 - Feature negotiation mechanism for the File Transfer Protocol</a></li><li><a href="#RFC-2428_-_FTP_Extensions_for_IPv6_and_NATs">RFC-2428 - FTP Extensions for IPv6 and NATs</a></li><li><a href="#RFC-2577_-_FTP_Security_Considerations">RFC-2577 - FTP Security Considerations</a></li><li><a href="#RFC-2640_-_Internationalization_of_the_File_Transfer_Protocol">RFC-2640 - Internationalization of the File Transfer Protocol</a></li><li><a href="#RFC-3659_-_Extensions_to_FTP">RFC-3659 - Extensions to FTP</a></li><li><a href="#RFC-4217_-_Securing_FTP_with_TLS">RFC-4217 - Securing FTP with TLS</a></li></ul> <p></p><h1><a name="Introduction"></a>Introduction<a href="#Introduction" class="section_anchor">¶</a></h1><p>This page lists current standard Internet RFCs that define the FTP protocol. </p><p>pyftpdlib conforms to the FTP protocol standard as defined in <a href="http://www.ietf.org/rfc/rfc959.txt" rel="nofollow">RFC-959</a> and <a href="http://www.ietf.org/rfc/rfc1123.txt" rel="nofollow">RFC-1123</a> + implementing all the fundamental commands and features described in +them. It also implements some more recent features such as OPTS and FEAT + commands (<a href="http://www.ietf.org/rfc/rfc2389.txt" rel="nofollow">RFC-2398</a>), EPRT and EPSV commands covering the IPv6 support (<a href="ftp://ftp.rfc-editor.org/in-notes/rfc2428.txt" rel="nofollow">RFC-2428</a>) and MDTM, MLSD, MLST and SIZE commands defined in <a href="http://www.ietf.org/rfc/rfc3659.txt" rel="nofollow">RFC-3659</a>. </p><p>Future plans for pyftpdlib include the gradual implementation of other standards track RFCs. </p><p>Some + of the features like ACCT or SMNT commands will never be implemented +deliberately. Other features described in more recent RFCs like the +TLS/SSL support for securing FTP (<a href="http://www.ietf.org/rfc/rfc4217.txt" rel="nofollow">RFC-4217</a>) are now implemented as a <a href="http://code.google.com/p/pyftpdlib/source/browse/trunk/demo/tls_ftpd.py" rel="nofollow">demo script</a>, waiting to reach the proper level of stability to be then included in the standard code base. </p><h1><a name="RFC-959_-_File_Transfer_Protocol"></a>RFC-959 - File Transfer Protocol<a href="#RFC-959_-_File_Transfer_Protocol" class="section_anchor">¶</a></h1><p>The base specification of the current File Transfer Protocol. </p><ul><li>Issued: October 1985 </li><li>Status: STANDARD </li><li>Obsoletes: <a href="http://www.ietf.org/rfc/rfc765.txt" rel="nofollow">RFC-765</a> </li><li>Updated by: <a href="http://www.ietf.org/rfc/rfc1123.txt" rel="nofollow">RFC-1123</a>, <a href="http://www.ietf.org/rfc/rfc2228.txt" rel="nofollow">RFC-2228</a>, <a href="http://www.ietf.org/rfc/rfc2640.txt" rel="nofollow">RFC-2640</a>, <a href="http://www.ietf.org/rfc/rfc2773.txt" rel="nofollow">RFC-2773</a> </li><li><a href="http://www.ietf.org/rfc/rfc959.txt" rel="nofollow">Link</a> </li></ul><p></p><p><table><tbody><tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Command</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Implemented</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Milestone</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Description</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Notes</strong> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> ABOR </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Abort data transfer. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> ACCT </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> NO </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> --- </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Specify account information. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> It will never be implemented (useless). </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> ALLO </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Ask for server to allocate enough storage space. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Treated as a NOOP (no operation). </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> APPE </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Append data to an existing file. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> CDUP </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Go to parent directory. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> CWD </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Change current working directory. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> DELE </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Delete file. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> HELP </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Show help. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Accept also arguments. </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> LIST </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> List files. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Accept also bad arguments like "-ls", "-la", ... </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> MKD </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Create directory. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> MODE </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Set data transfer mode. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> "STREAM" mode is supported, "Block" and "Compressed" aren't. </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> NLST </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> List files in a compact form. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> NOOP </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> NOOP (no operation), just do nothing. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> PASS </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Set user password. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> PASV </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Set server in passive connection mode. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> PORT </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Set server in active connection mode. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> PWD </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Get current working directory. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> QUIT </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Quit session. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> If file transfer is in progress, the connection will remain open until it is finished. </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> REIN </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Reinitialize user's current session. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> REST </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Restart file position. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> RETR </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Retrieve a file (client's download). </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> RMD </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Remove directory. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> RNFR </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> File renaming (source) </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> RNTO </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> File renaming (destination) </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> SITE </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.5.1 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Site specific server services. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> + No SITE commands aside from "SITE HELP" are implemented by default. +The user willing to add support for a specific SITE command has to +define a new <tt>ftp_SITE_%CMD%</tt> method in the <tt>FTPHandler</tt> subclass. </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> SMNT </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> NO </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> --- </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Mount file-system structure. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Will never be implemented (too much system-dependent and almost never used). </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> STAT </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Server's status information / File LIST </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> STOR </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Store a file (client's upload). </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> STOU </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Store a file with a unique name. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> STRU </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Set file structure. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Supports only File type structure by doing a NOOP (no operation). Other structure types (Record and Page) are not implemented. </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> SYST </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Get system type. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Always return "UNIX Type: L8" because of the LIST output provided. </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> TYPE </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Set current type (Binary/ASCII). </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Accept only Binary and ASII TYPEs. Other TYPEs such as EBCDIC are obsoleted, system-dependent and thus not implemented. </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> USER </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Set user. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> + A new USER command could be entered at any point in order to change the + access control flushing any user, password, and account information +already supplied and beginning the login sequence again. </td></tr> </tbody></table></p><p><br> </p><h1><a name="RFC-1123_-_Requirements_for_Internet_Hosts"></a>RFC-1123 - Requirements for Internet Hosts<a href="#RFC-1123_-_Requirements_for_Internet_Hosts" class="section_anchor">¶</a></h1><p>Extends and clarifies some aspects of <a href="http://www.ietf.org/rfc/rfc959.txt" rel="nofollow">RFC-959</a>. Introduces new response codes 554 and 555. </p><ul><li>Issued: October 1989 </li><li>Status: STANDARD </li><li><a href="http://www.ietf.org/rfc/rfc1123.txt" rel="nofollow">Link</a> </li></ul><p><table><tbody><tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Feature</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Implemented</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Milestone</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Description</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Notes</strong> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> TYPE L 8 as synonym of TYPE I </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.2.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> TYPE L 8 command should be treated as synonym of TYPE I ("IMAGE" or binary type). </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> PASV is per-transfer </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> PASV must be used for a unique transfer. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> If PASV is issued twice data-channel is restarted. </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Implied type for LIST and NLST </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> The data returned by a LIST or NLST command SHOULD use an implied TYPE AN. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> STOU format output </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.2.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Defined the exact format output which STOU response must respect ("125/150 FILE filename"). </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Avoid 250 response type on STOU </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.2.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> The 250 positive response indicated in <a href="http://www.ietf.org/rfc/rfc959.txt" rel="nofollow">RFC-959</a> has been declared incorrect in <a href="http://www.ietf.org/rfc/rfc1123.txt" rel="nofollow">RFC-1123</a> which requires 125/150 instead. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Handle "Experimental" directory cmds </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> + The server should support XCUP, XCWD, XMKD, XPWD and XRMD obsoleted +commands and treat them as synonyms for CDUP, CWD, MKD, LIST and RMD +commands. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Idle timeout </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.5.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> + A Server-FTP process SHOULD have a configurable idle timeout of 5 +minutes, which will terminate the process and close the control +connection if the server is inactive (i.e., no command or data transfer +in progress) for a long period of time. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Concurrency of data and control </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Server-FTP should be able to process STAT or ABOR while a data transfer is in progress </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Feature granted natively for ALL commands since we're in an asynchronous environment. </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 554 response on wrong REST </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.2.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> + Return a 554 reply may for a command that follows a REST command. The +reply indicates that the existing file at the Server-FTP cannot be +repositioned as specified in the REST. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> </tbody></table></p><p><br> </p><h1><a name="RFC-2228_-_FTP_Security_Extensions"></a>RFC-2228 - FTP Security Extensions<a href="#RFC-2228_-_FTP_Security_Extensions" class="section_anchor">¶</a></h1><p>Specifies several security extensions to the base FTP protocol defined in <a href="http://www.ietf.org/rfc/rfc959.txt" rel="nofollow">RFC-959</a>. + New commands: AUTH, ADAT, PROT, PBSZ, CCC, MIC, CONF, and ENC. New +response codes: 232, 234, 235, 334, 335, 336, 431, 533, 534, 535, 536, +537, 631, 632, and 633. </p><ul><li>Issued: October 1997 </li><li>Status: PROPOSED STANDARD </li><li>Updates: <a href="http://www.ietf.org/rfc/rfc959.txt" rel="nofollow">RFC-959</a> </li><li><a href="http://www.ietf.org/rfc/rfc2228.txt" rel="nofollow">Link</a> </li></ul><p></p><p><table><tbody><tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Command</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Implemented</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Milestone</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Description</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Notes</strong> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> AUTH </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> NO </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> --- </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Authentication/Security Mechanism. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Implemented as <a href="http://code.google.com/p/pyftpdlib/source/browse/trunk/demo/tls_ftpd.py" rel="nofollow">demo script</a> by following the <a href="http://www.ietf.org/rfc/rfc4217.txt" rel="nofollow">RFC-4217</a> guide line. </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> CCC </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> NO </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> --- </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Clear Command Channel. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> CONF </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> NO </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> --- </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Confidentiality Protected Command. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Somewhat obsoleted by <a href="http://www.ietf.org/rfc/rfc4217.txt" rel="nofollow">RFC-4217</a>. </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> EENC </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> NO </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> --- </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Privacy Protected Command. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Somewhat obsoleted by <a href="http://www.ietf.org/rfc/rfc4217.txt" rel="nofollow">RFC-4217</a>. </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> MIC </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> NO </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> --- </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Integrity Protected Command. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Somewhat obsoleted by <a href="http://www.ietf.org/rfc/rfc4217.txt" rel="nofollow">RFC-4217</a>. </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> PBSZ </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> NO </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> --- </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Protection Buffer Size. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Implemented as <a href="http://code.google.com/p/pyftpdlib/source/browse/trunk/demo/tls_ftpd.py" rel="nofollow">demo script</a> by following the <a href="http://www.ietf.org/rfc/rfc4217.txt" rel="nofollow">RFC-4217</a> guide line as a no-op command. </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> PROT </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> NO </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> --- </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Data Channel Protection Level. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Implemented as <a href="http://code.google.com/p/pyftpdlib/source/browse/trunk/demo/tls_ftpd.py" rel="nofollow">demo script</a> by following the <a href="http://www.ietf.org/rfc/rfc4217.txt" rel="nofollow">RFC-4217</a> guide line supporting only "P" and "C" protection levels. </td></tr> </tbody></table></p><p><br> </p><h1><a name="RFC-2389_-_Feature_negotiation_mechanism_for_the_File_Transfer_P"></a>RFC-2389 - Feature negotiation mechanism for the File Transfer Protocol<a href="#RFC-2389_-_Feature_negotiation_mechanism_for_the_File_Transfer_P" class="section_anchor">¶</a></h1><p>Introduces the new FEAT and OPTS commands. </p><ul><li>Issued: August 1998 </li><li>Status: PROPOSED STANDARD </li><li><a href="http://www.ietf.org/rfc/rfc2389.txt" rel="nofollow">Link</a> </li></ul><p></p><p><table><tbody><tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Command</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Implemented</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Milestone</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Description</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Notes</strong> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> FEAT </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.3.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> List new supported commands subsequent <a href="http://www.ietf.org/rfc/rfc959.txt" rel="nofollow">RFC-959</a> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> OPTS </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.3.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Set options for certain commands. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> MLST is the only command which could be used with OPTS. </td></tr> </tbody></table></p><p><br> </p><h1><a name="RFC-2428_-_FTP_Extensions_for_IPv6_and_NATs"></a>RFC-2428 - FTP Extensions for IPv6 and NATs<a href="#RFC-2428_-_FTP_Extensions_for_IPv6_and_NATs" class="section_anchor">¶</a></h1><p>Introduces + the new commands EPRT and EPSV extending FTP to enable its use over +various network protocols, and the new response codes 522 and 229. </p><ul><li>Issued: September 1998 </li><li>Status: PROPOSED STANDARD </li><li><a href="http://www.ietf.org/rfc/rfc2428.txt" rel="nofollow">Link</a> </li></ul><p></p><p><table><tbody><tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Command</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Implemented</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Milestone</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Description</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Notes</strong> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> EPRT </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.4.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Set active data connection over IPv4 or IPv6 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> EPSV </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.4.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Set passive data connection over IPv4 or IPv6 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> </tbody></table></p><p><br> </p><h1><a name="RFC-2577_-_FTP_Security_Considerations"></a>RFC-2577 - FTP Security Considerations<a href="#RFC-2577_-_FTP_Security_Considerations" class="section_anchor">¶</a></h1><p>Provides + several configuration and implementation suggestions to mitigate some +security concerns, including limiting failed password attempts and +third-party "proxy FTP" transfers, which can be used in "bounce +attacks". </p><ul><li>Issued: May 1999 </li><li>Status: INFORMATIONAL </li><li><a href="http://www.ietf.org/rfc/rfc2577.txt" rel="nofollow">Link</a> </li></ul><p></p><p><table><tbody><tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Feature</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Implemented</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Milestone</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Description</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Notes</strong> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> FTP bounce protection </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.2.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> + Reject PORT if IP address specified in it does not match client IP +address. Drop the incoming (PASV) data connection for the same reason. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Configurable. </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Restrict PASV/PORT to non privileged ports </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.2.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Reject connections to privileged ports. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Configurable. </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Brute force protection (1) </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Disconnect client after a certain number (3 or 5) of wrong authentications. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Configurable. </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Brute force protection (2) </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.5.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Impose a 5 second delay before replying to an invalid "PASS" command to diminish the efficiency of a brute force attack. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Per-source-IP limit </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.2.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Limit the total number of per-ip control connections to avoid parallel brute-force attack attempts. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Configurable. </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Do not reject wrong usernames </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Always return 331 to the USER command to prevent client from determining valid usernames on the server. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Port stealing protection </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.1 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Use random-assigned local ports for data connections. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> </tbody></table></p><p><br> </p><h1><a name="RFC-2640_-_Internationalization_of_the_File_Transfer_Protocol"></a>RFC-2640 - Internationalization of the File Transfer Protocol<a href="#RFC-2640_-_Internationalization_of_the_File_Transfer_Protocol" class="section_anchor">¶</a></h1><p>Extends + the FTP protocol to support multiple character sets, in addition to the + original 7-bit ASCII. Introduces the new LANG command. </p><ul><li>Issued: July 1999 </li><li>Status: PROPOSED STANDARD </li><li>Updates: <a href="http://www.ietf.org/rfc/rfc959.txt" rel="nofollow">RFC-959</a> </li><li><a href="http://www.ietf.org/rfc/rfc2640.txt" rel="nofollow">Link</a> </li></ul><p></p><p><table><tbody><tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Feature</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Implemented</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Milestone</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Description</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Notes</strong> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> LANG command </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> NO </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> --- </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Set current response's language. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Support for UNICODE </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> NO </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> --- </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> For support of global compatibility it is rencommended that clients and servers use UTF-8 encoding when exchanging pathnames. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> </tbody></table></p><p><br> </p><h1><a name="RFC-3659_-_Extensions_to_FTP"></a>RFC-3659 - Extensions to FTP<a href="#RFC-3659_-_Extensions_to_FTP" class="section_anchor">¶</a></h1><p>Four new commands are added: "SIZE", "MDTM", "MLST", and "MLSD". The existing command "REST" is modified. </p><ul><li>Issued: March 2007 </li><li>Status: PROPOSED STANDARD </li><li>Updates: <a href="http://www.ietf.org/rfc/rfc959.txt" rel="nofollow">RFC-959</a> </li><li><a href="http://www.ietf.org/rfc/rfc3659.txt" rel="nofollow">Link</a> </li></ul><p></p><p><table><tbody><tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Feature</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Implemented</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Milestone</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Description</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Notes</strong> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> MDTM command </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Get file's last modification time </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> MLSD command </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.3.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Get directory list in a standardized form. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> MLST command </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.3.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Get file information in a standardized form. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> SIZE command </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Get file size. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> In case of ASCII TYPE it does not perform the ASCII conversion to avoid DoS conditions (see FAQs for more details). </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> TVSF mechanism </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.1.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> + Provide a file system naming conventions modeled loosely upon those of +the Unix file system supporting relative and absolute path names. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Minimum required set of MLST facts </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.3.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> If conceivably possible, support at least the type, perm, size, unique, and modify MLSX command facts. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> GMT should be used for timestamps </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> 0.6.0 </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> All times reported by MDTM, LIST, MLSD and MLST commands must be in GMT times </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Possibility to change time display between GMT and local time provided as <tt>FTPHandler.use_gmt_times</tt> attribute </td></tr> </tbody></table></p><p><br> </p><h1><a name="RFC-4217_-_Securing_FTP_with_TLS"></a>RFC-4217 - Securing FTP with TLS<a href="#RFC-4217_-_Securing_FTP_with_TLS" class="section_anchor">¶</a></h1><p>Provides a description on how to implement TLS as a security mechanism to secure FTP clients and/or servers. </p><ul><li>Issued: October 2005 </li><li>Status: STANDARD </li><li>Updates: <a href="http://www.ietf.org/rfc/rfc959.txt" rel="nofollow">RFC-959</a>, <a href="http://www.ietf.org/rfc/rfc2246.txt" rel="nofollow">RFC-2246</a>, <a href="http://www.ietf.org/rfc/rfc2228.txt" rel="nofollow">RFC-2228</a> </li><li><a href="http://www.ietf.org/rfc/rfc4217.txt" rel="nofollow">Link</a> </li></ul><p><table><tbody><tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Command</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Implemented</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Milestone</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Description</strong> </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> <strong>Notes</strong> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> AUTH </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> --- </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Authentication/Security Mechanism. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> CCC </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> NO </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> --- </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Clear Command Channel. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> PBSZ </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> --- </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Protection Buffer Size. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Implemented as as a no-op as recommended. </td></tr> <tr><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> PROT </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> YES </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> --- </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Data Channel Protection Level. </td><td style="border: 1px solid rgb(170, 170, 170); padding: 5px;"> Support only "P" and "C" protection levels. </td></tr> </tbody></table></p> + </td> + </tr> + </tbody></table> + </div> + + + + +<script type="text/javascript" src="rfcs-compliance_files/dit_scripts.js"></script> + + + + </body></html> diff --git a/chromium/third_party/pyftpdlib/src/doc/roadmap.lnk.html b/chromium/third_party/pyftpdlib/src/doc/roadmap.lnk.html new file mode 100644 index 00000000000..5a5fbf1afe5 --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/doc/roadmap.lnk.html @@ -0,0 +1,5 @@ +<title>Redirecting to Roadmap...</title>
+<a href="http://code.google.com/p/pyftpdlib"><img id="logo" src="logo.png" /></a>
+
+<p>Redirecting to: <a href="http://code.google.com/p/pyftpdlib/wiki/Roadmap">http://code.google.com/p/pyftpdlib/wiki/Roadmap</a><br></p>
+<p>You'll be redirected in 3 seconds...</p>
diff --git a/chromium/third_party/pyftpdlib/src/doc/tutorial.html b/chromium/third_party/pyftpdlib/src/doc/tutorial.html new file mode 100644 index 00000000000..c842da03e0c --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/doc/tutorial.html @@ -0,0 +1,427 @@ +<html><head> +<meta http-equiv="content-type" content="text/html; charset=UTF-8"> +<style> +.section_anchor { + font-size:0px; +} +</style> + + + + + + <title>Tutorial</title> + </head><body> + + + + + <div id="wikicontent"> + <table border="0" cellpadding="0" cellspacing="0" width="100%"> + <tbody><tr> + + <td class="vt" id="wikimaincol" width="100%"> + + <div id="wikiheader" style="margin-bottom: 1em;"> + + + <span style="font-size: 120%; font-weight: bold;">Tutorial</span> + + + + <div style="font-style: italic; margin-top: 3px;">Tutorial containing API reference and example usages</div> + + </div> + <h1><a name="Table_of_contents"></a>Table of contents<a href="#Table_of_contents" class="section_anchor">¶</a></h1><p></p><ul><li><a href="#Table_of_contents">Table of contents</a></li><li><a href="#1.0_-_Introduction">1.0 - Introduction</a></li><li><a href="#2.0_-_API_reference">2.0 - API reference</a></li><li><a href="#2.1_-_Contrib_package">2.1 - Contrib package</a></li><ul><li><a href="#2.2_-_pyftpdlib.contrib.handlers_module">2.2 - pyftpdlib.contrib.handlers module</a></li><li><a href="#2.3_-_pyftpdlib.contrib.authorizers_module">2.3 - pyftpdlib.contrib.authorizers module</a></li><li><a href="#2.4_-_pyftpdlib.contrib.filesystems_module">2.4 - pyftpdlib.contrib.filesystems module</a></li></ul><li><a href="#3.0_-_Customizing_your_FTP_server">3.0 - Customizing your FTP server</a></li><ul><li><a href="#3.1_-_Building_a_Base_FTP_server">3.1 - Building a Base FTP server</a></li><li><a href="#3.2_-_Logging_management">3.2 - Logging management</a></li><li><a href="#3.3_-_Storing_passwords_as_hash_digests">3.3 - Storing passwords as hash digests</a></li><li><a href="#3.4_-_Unix_FTP_Server">3.4 - Unix FTP Server</a></li><li><a href="#3.5_-_Windows_NT_FTP_Server">3.5 - Windows NT FTP Server</a></li><li><a href="#3.6_-_Throttle_bandwidth">3.6 - Throttle bandwidth</a></li><li><a href="#3.7_-_FTPS_%28FTP_over_TLS/SSL%29_server">3.7 - FTPS (FTP over TLS/SSL) server</a></li><li><a href="#3.8_-_Event_callbacks">3.8 - Event callbacks</a></li><li><a href="#3.9_-_Command_line_usage">3.9 - Command line usage</a></li></ul></ul> <p></p><h1><a name="1.0_-_Introduction"></a>1.0 - Introduction<a href="#1.0_-_Introduction" class="section_anchor">¶</a></h1><p>pyftpdlib implements the server side of the FTP protocol as defined in <a href="http://www.faqs.org/rfcs/rfc959.html" rel="nofollow">RFC-959</a>. pyftpdlib consist of a single file, <a href="http://code.google.com/p/pyftpdlib/source/browse/trunk/pyftpdlib/ftpserver.py" rel="nofollow">ftpserver.py</a>, which contains a hierarchy of classes, functions and variables which implement the backend functionality for the ftpd. <br> This document is intended to serve as a simple <a href="http://code.google.com/p/billiejoex/wiki/Tutorial#2.0_-_API_reference" rel="nofollow">API reference</a> of most important classes and functions. Also included is an introduction to <a href="http://code.google.com/p/billiejoex/wiki/Tutorial#3.0_-_Customizing_your_FTP_server" rel="nofollow">customization</a> through the use of some example scripts. Some of them are included in <a href="http://code.google.com/p/pyftpdlib/source/browse/#svn/trunk/demo" rel="nofollow">demo</a> directory of pyftpdlib source distribution. </p><p>If + you have written a customized configuration you think could be useful +to the community feel free to share it by adding a comment at the end of + this document. </p><h1><a name="2.0_-_API_reference"></a>2.0 - API reference<a href="#2.0_-_API_reference" class="section_anchor">¶</a></h1><p><i>function</i> pyftpdlib.ftpserver.<strong>log</strong><font size="3"><strong><tt>(</tt></strong></font><i>msg</i><font size="3"><strong><tt>)</tt></strong></font> </p><blockquote>Log messages intended for the end user. +</blockquote><hr><i>function</i> pyftpdlib.ftpserver.<strong>logline</strong><font size="3"><strong><tt>(</tt></strong></font><i>msg</i><font size="3"><strong><tt>)</tt></strong></font> <blockquote>Log commands and responses passing through the command channel. +</blockquote><hr><i>function</i> pyftpdlib.ftpserver.<strong>logerror</strong><font size="3"><strong><tt>(</tt></strong></font><i>msg</i><font size="3"><strong><tt>)</tt></strong></font> <blockquote>Log traceback outputs occurring in case of errors. +</blockquote><hr><i>class</i> pyftpdlib.ftpserver.<strong>AuthorizerError</strong><font size="3"><strong><tt>()</tt></strong></font> <blockquote>Base class for authorizers exceptions. +</blockquote><hr><i>class</i> pyftpdlib.ftpserver.<strong>DummyAuthorizer</strong><font size="3"><strong><tt>()</tt></strong></font> <p></p><blockquote>Basic + "dummy" authorizer class, suitable for subclassing to create your own +custom authorizers. An "authorizer" is a class handling authentications +and permissions of the FTP server. It is used inside <tt>FTPHandler</tt> + class for verifying user's password, getting users home directory, +checking user permissions when a filesystem read/write event occurs and +changing user before accessing the filesystem. <tt>DummyAuthorizer</tt> +is the base authorizer, providing a platform independent interface for +managing "virtual" FTP users. Typically the first thing you have to do +is create an instance of this class and start adding ftp users: +</blockquote><pre class="prettyprint">>>> from pyftpdlib import ftpserver +>>> authorizer = ftpserver.DummyAuthorizer() +>>> authorizer.add_user('user', 'password', '/home/user', perm='elradfmw') +>>> authorizer.add_anonymous('/home/nobody')</pre><ul><li><strong>add_user</strong><font size="3"><strong><tt>(</tt></strong></font><i>username</i>, <i>password</i>, <i>homedir</i><font size="3"><strong><tt>[</tt></strong></font>, <i>perm="elr"</i><font size="3"><strong><tt>[</tt></strong></font>, <i>msg_login="Login successful."</i><font size="3"><strong><tt>[</tt></strong></font>, <i>msg_quit="Goodbye."</i> <font size="3"><strong><tt>]]])</tt></strong></font> <br>Add a user to the virtual users table. <tt>AuthorizerError</tt> exceptions raised on error conditions such as insufficient permissions or duplicate usernames. Optional <tt>perm</tt> + argument is a set of letters referencing the user's permissions. Every + letter is used to indicate that the access rights the current FTP user +has over the following specific actions are granted. The available +permissions are the following listed below: </li></ul><blockquote>Read permissions: +<ul><li><strong>"e"</strong> = change directory (CWD command) </li><li><strong>"l"</strong> = list files (LIST, NLST, STAT, MLSD, MLST commands) </li><li><strong>"r"</strong> = retrieve file from the server (RETR command) </li></ul>Write permissions +<ul><li><strong>"a"</strong> = append data to an existing file (APPE command) </li><li><strong>"d"</strong> = delete file or directory (DELE, RMD commands) </li><li><strong>"f"</strong> = rename file or directory (RNFR, RNTO commands) </li><li><strong>"m"</strong> = create directory (MKD command) </li><li><strong>"w"</strong> = store a file to the server (STOR, STOU commands) </li></ul></blockquote><blockquote>Optional <tt>msg_login</tt> and <tt>msg_quit</tt> arguments can be specified to provide customized response strings when user log-in and quit. The <tt>perm</tt> argument of the <tt>add_user()</tt> + method refers to user's permissions. Every letter is used to indicate +that the access rights the current FTP user has over the following +specific actions are granted. +</blockquote><ul><li><strong>add_anonymous</strong><font size="3"><strong><tt>(</tt></strong></font><i>homedir</i><font size="3"><strong><tt>[</tt></strong></font>, <tt>**</tt><i>kwargs</i><font size="3"><strong><tt>])</tt></strong></font><br>Add an anonymous user to the virtual users table. <tt>AuthorizerError</tt> + exception raised on error conditions such as insufficient permissions, +missing home directory, or duplicate anonymous users. The keyword +arguments in <tt>kwargs</tt> are the same expected by <tt>add_user()</tt> method: <tt>perm</tt>, <tt>msg_login</tt> and <tt>msg_quit</tt>. The optional <i>perm</i> keyword argument is a string defaulting to <tt>"elr"</tt> referencing "read-only" anonymous user's permission. Using a "write" value results in a <tt>RuntimeWarning</tt>. </li></ul><ul><li><strong>override_perm</strong><font size="3"><strong><tt>(</tt></strong></font><i>directory</i>, <i>perm</i><font size="3"><strong><tt>[</tt></strong></font>, <i>recursive=False</i><font size="3"><strong><tt>])</tt></strong></font><br>Override permissions for a given directory. <i><strong>New in version 0.5.0</strong></i> </li></ul><ul><li><strong>validate_authentication</strong><font size="3"><strong><tt>(</tt></strong></font><i>username</i>, <i>password</i><font size="3"><strong><tt>)</tt></strong></font><br>Return <tt>True</tt> if the supplied <tt>username</tt> and <tt>password</tt> match the stored credentials. </li></ul><ul><li><strong>impersonate_user</strong><font size="3"><strong><tt>(</tt></strong></font><i>username</i>, <i>password</i><font size="3"><strong><tt>)</tt></strong></font><br>Impersonate + another user (noop). It is always called before accessing the +filesystem. By default it does nothing. The subclass overriding this +method is expected to provide a mechanism to change the current user. </li></ul><ul><li><strong>terminate_impersonation</strong><font size="3"><strong><tt>(</tt></strong></font><i>username</i>, <i>password</i><font size="3"><strong><tt>)</tt></strong></font><br>Terminate + impersonation (noop). It is always called after having accessed the +filesystem. By default it does nothing. The subclass overriding this +method is expected to provide a mechanism to switch back to the original + user. </li></ul><ul><li><strong>remove_user</strong><font size="3"><strong><tt>(</tt></strong></font><i>username</i><font size="3"><strong><tt>)</tt></strong></font><br>Remove a user from the virtual user table. </li></ul><hr><p>class pyftpdlib.ftpserver.<strong>FTPHandler</strong><font size="3"><strong><tt>(</tt></strong></font><i>conn, server</i><font size="3"><strong><tt>)</tt></strong></font> </p><blockquote>This class implements the FTP server Protocol Interpreter (see <a href="http://www.faqs.org/rfcs/rfc959.html" rel="nofollow">RFC-959</a>), + handling commands received from the client on the control channel by +calling the command's corresponding method (e.g. for received command +"MKD pathname", <tt>ftp_MKD()</tt> method is called with <tt>pathname</tt> as the argument). All relevant session information are stored in instance variables. <tt>conn</tt> is the underlying socket object instance of the newly established connection, <tt>server</tt> is the <tt>FTPServer</tt> class instance. Basic usage simply requires creating an instance of <tt>FTPHandler</tt> class and specify which authorizer instance it will going to use: +</blockquote><pre class="prettyprint">>>> ftp_handler = ftpserver.FTPHandler +>>> ftp_handler.authorizer = authorizer</pre><blockquote>All +relevant session information is stored in class attributes reproduced +below and can be modified before instantiating this class: +</blockquote><ul><li><strong>timeout</strong><br>The timeout which is +the maximum time a remote client may spend between FTP commands. If the +timeout triggers, the remote client will be kicked off (defaults to <tt>300</tt> seconds). <i><strong>New in version 5.0</strong></i> </li></ul><ul><li><strong>banner</strong><br>String sent when client connects (default <tt>"pyftpdlib %s ready." %__ver__</tt>). </li></ul><ul><li><strong>max_login_attempts</strong><br>Maximum number of wrong authentications before disconnecting (default <tt>3</tt>). </li></ul><ul><li><strong>permit_foreign_addresses</strong><br>Whether enable <a href="http://www.proftpd.org/docs/howto/FXP.html" rel="nofollow">FXP</a> feature (default <tt>False</tt>). </li></ul><ul><li><strong>permit_privileged_ports</strong><br>Set to <tt>True</tt> if you want to permit active connections (PORT) over privileged ports (not recommended, default <tt>False</tt>). </li></ul><ul><li><strong>masquerade_address</strong><br>The + "masqueraded" IP address to provide along PASV reply when pyftpdlib is +running behind a NAT or other types of gateways. When configured +pyftpdlib will hide its local address and instead use the public address + of your NAT (default <tt>None</tt>). </li></ul><ul><li><strong>masquerade_address_map</strong><br>In + case the server has multiple IP addresses which are all behind a NAT +router, you may wish to specify individual masquerade_addresses for each + of them. The map expects a dictionary containing private IP addresses +as keys, and their corresponding public (masquerade) addresses as values + (defaults to <tt>{}</tt>). <i><strong>New in version 0.6.0</strong></i> </li></ul><ul><li><strong>passive_ports</strong><br>What ports ftpd will use for its passive data transfers. Value expected is a list of integers (e.g. <tt>range(60000, 65535)</tt>). When configured pyftpdlib will no longer use kernel-assigned random ports (default <tt>None</tt>). </li></ul><ul><li><strong>use_gmt_times</strong><br>When True causes the server to report all ls and MDTM times in GMT and not local time (default <tt>True</tt>). <i><strong>New in version 0.6.0</strong></i> </li></ul><ul><li><strong>tcp_no_delay</strong><br>Controls + the use of the TCP_NODELAY socket option which disables the Nagle +algorithm resulting in significantly better performances (default <tt>True</tt> on all platforms where it is supported). <i><strong>New in version 0.6.0</strong></i> </li></ul><blockquote><br>Follows a list of callback methods that can be overridden in a subclass. For blocking operations read the FAQ on how to <a href="http://code.google.com/p/pyftpdlib/wiki/FAQ#How_can_I_run_long-running_tasks_without_blocking_the_server?" rel="nofollow">run time consuming tasks</a> +</blockquote><ul><li><strong>on_login</strong><font size="3"><strong><tt>(</tt></strong></font><i>username</i><font size="3"><strong><tt>)</tt></strong></font><br>Called on user login. <i><strong>New in version 0.6.0</strong></i> </li></ul><ul><li><strong>on_file_sent</strong><font size="3"><strong><tt>(</tt></strong></font><i>file</i><font size="3"><strong><tt>)</tt></strong></font><br>Called every time a file has been successfully sent. <i><strong>New in version 0.5.1</strong></i> </li></ul><ul><li><strong>on_file_received</strong><font size="3"><strong><tt>(</tt></strong></font><i>file</i><font size="3"><strong><tt>)</tt></strong></font><br>Called every time a file has been successfully received. <i><strong>New in version 0.5.1</strong></i> </li></ul><ul><li><strong>on_incomplete_file_sent</strong><font size="3"><strong><tt>(</tt></strong></font><i>file</i><font size="3"><strong><tt>)</tt></strong></font><br>Called every time a file has not been entirely sent (e.g. transfer aborted via ABOR or client disconnected abruptly). <tt>file</tt> is the absolute name of that file. <i><strong>New in version 0.6.0</strong></i> </li></ul><ul><li><strong>on_incomplete_file_received</strong><font size="3"><strong><tt>(</tt></strong></font><i>file</i><font size="3"><strong><tt>)</tt></strong></font><br>Called every time a file has not been entirely received (e.g. transfer aborted via ABOR or client disconnected abruptly). <tt>file</tt> is the absolute name of that file. <i><strong>New in version 0.6.0</strong></i> </li></ul><hr><p><i>class</i> pyftpdlib.ftpserver.<strong>DTPHandler</strong><font size="3"><strong><tt>(</tt></strong></font><i>sock_obj, cmd_channel</i><font size="3"><strong><tt>)</tt></strong></font> </p><blockquote>This class handles the server-data-transfer-process (server-DTP, see <a href="http://www.faqs.org/rfcs/rfc959.html" rel="nofollow">RFC-959</a>) managing all transfer operations regarding the data channel. <tt>sock_obj</tt> is the underlying socket object instance of the newly established connection, <tt>cmd_channel</tt> is the <tt>FTPHandler</tt> class instance. +</blockquote><ul><li><strong>timeout</strong><br>The timeout which +roughly is the maximum time we permit data transfers to stall for with +no progress. If the timeout triggers, the remote client will be kicked +off. <i><strong>New in version 5.0</strong></i> </li></ul><ul><li><strong>ac_in_buffer_size</strong><br> </li><li><strong>ac_out_buffer_size</strong><br>The buffer sizes to use when receiving and sending data (both defaulting to <tt>65536</tt> + bytes). For LANs you may want this to be fairly large. Depending on +available memory and number of connected clients setting them to a lower + value can result in better performances. </li></ul><hr><p><i>class</i> pyftpdlib.ftpserver.<strong>ThrottledDTPHandler</strong><font size="3"><strong><tt>(</tt></strong></font><i>sock_obj, cmd_channel</i><font size="3"><strong><tt>)</tt></strong></font> </p><blockquote>A <tt>DTPHandler</tt> + subclass which wraps sending and receiving in a data counter and +temporarily "sleeps" the channel so that you burst to no more than x +Kb/sec average. Use it instead of <tt>DTPHandler</tt> to set transfer rates limits for both downloads and/or uploads (see the <a href="http://code.google.com/p/pyftpdlib/source/browse/trunk/demo/throttled_ftpd.py" rel="nofollow">demo script</a> showing the example usage). <strong><i>New in version 0.5.2</i></strong> +</blockquote><ul><li><strong>read_limit</strong><br>The maximum number of bytes to read (receive) in one second (defaults to 0 == no limit) </li></ul><ul><li><strong>write_limit</strong><br>The maximum number of bytes to write (send) in one second (defaults to 0 == no limit). </li></ul><hr><p><i>class</i> pyftpdlib.ftpserver.<strong>FTPServer</strong><font size="3"><strong><tt>(</tt></strong></font><i>address, handler</i><font size="3"><strong><tt>)</tt></strong></font> </p><blockquote>This class is an <tt>asyncore.dispatcher</tt> subclass. It creates a FTP socket listening on <tt>address</tt> (a tuple containing the ip:port pair), dispatching the requests to a "handler" (typically <tt>FTPHandler</tt> class object). It is typically used for starting asyncore polling loop: +</blockquote><pre class="prettyprint">>>> address = ('127.0.0.1', 21) +>>> ftpd = ftpserver.FTPServer(address, ftp_handler) +>>> ftpd.serve_forever()</pre><ul><li><strong>max_cons</strong><br>Number of maximum simultaneous connections accepted (default <tt>512</tt>). </li></ul><ul><li><strong>max_cons_per_ip</strong><br>Number of maximum connections accepted for the same IP address (default <tt>0</tt> == <i>no limit</i>). </li></ul><ul><li><strong>serve_forever(</strong><font size="3"><strong><tt>[</tt></strong></font><i>timeout=1</i><font size="3"><strong><tt>[</tt></strong></font>, <i>use_poll=False</i><font size="3"><strong><tt>[</tt></strong></font>, <i>map=None</i><font size="3"><strong><tt>[</tt></strong></font>, <i>count=None</i><font size="3"><strong><tt>]]])</tt></strong></font><br>A + wrap around asyncore.loop(); starts the asyncore polling loop including + running the scheduler. The arguments are the same expected by original <a href="http://docs.python.org/library/asyncore.html#asyncore.loop" rel="nofollow">asyncore.loop()</a> function. </li></ul><ul><li><strong>close</strong><font size="3"><strong><tt>()</tt></strong></font><br>Stop serving without disconnecting currently connected clients. </li></ul><ul><li><strong>close_all(</strong><font size="3"><strong><tt>[</tt></strong></font><i>map=None</i><font size="3"><strong><tt>[</tt></strong></font>, <i>ignore_all=False</i><font size="3"><strong><tt>]])</tt></strong></font><br>Stop serving disconnecting also the currently connected clients. The <tt>map</tt> parameter is a dictionary whose items are the channels to close. If <tt>map</tt> is omitted, the default <tt>asyncore.socket_map</tt> is used. Having <tt>ignore_all</tt> parameter set to <tt>False</tt> results in raising exception in case of unexpected errors. </li></ul><hr><p><i>class</i> pyftpdlib.ftpserver.<strong>AbstractedFS(</strong><i>root</i>, <i>cmd_channel</i><strong>)</strong> </p><blockquote>A + class used to interact with the file system, providing a cross-platform + interface compatible with both Windows and UNIX style filesystems where + all paths use "/" separator. <br><tt>AbstractedFS</tt> distinguishes +between "real" filesystem paths and "virtual" ftp paths emulating a UNIX + chroot jail where the user can not escape its home directory (example: +real "/home/user" path will be seen as "/" by the client). <br>It also provides some utility methods and wraps around all <tt>os.*</tt> + calls involving operations against the filesystem like creating files +or removing directories. The contructor accepts two arguments: <tt>root</tt> which is the user "real" home directory (e.g. '/home/user') and <tt>cmd_channel</tt> which is the <tt>FTPHandler</tt> class instance. +</blockquote><blockquote><i><strong>Changed in version 0.6.0</strong>: <tt>root</tt> and <tt>cmd_channel</tt> arguments were added.</i> +</blockquote><ul><li><strong>root</strong><br>User's home directory ("real"). </li></ul><ul><li><strong>cwd</strong><br>User's current working directory ("virtual"). </li></ul><ul><li><strong>ftpnorm</strong><font size="3"><strong><tt>(</tt></strong></font><i>ftppath</i><font size="3"><strong><tt>)</tt></strong></font><br>Normalize + a "virtual" ftp pathname depending on the current working directory +(e.g. having "/foo" as current working directory "x" becomes "/foo/x"). </li></ul><ul><li><strong>ftp2fs</strong><font size="3"><strong><tt>(</tt></strong></font><i>ftppath</i><font size="3"><strong><tt>)</tt></strong></font><br>Translate + a "virtual" ftp pathname into equivalent absolute "real" filesystem +pathname (e.g. having "/home/user" as root directory "x" becomes +"/home/user/x"). </li></ul><ul><li><strong>fs2ftp</strong><font size="3"><strong><tt>(</tt></strong></font><i>fspath</i><font size="3"><strong><tt>)</tt></strong></font><br>Translate + a "real" filesystem pathname into equivalent absolute "virtual" ftp +pathname depending on the user's root directory (e.g. having +"/home/user" as root directory "/home/user/x" becomes "/x". </li></ul><ul><li><strong>validpath</strong><font size="3"><strong><tt>(</tt></strong></font><i>path</i><font size="3"><strong><tt>)</tt></strong></font><br>Check + whether the path belongs to user's home directory. Expected argument is + a "real" filesystem path. If path is a symbolic link it is resolved to +check its real destination. Pathnames escaping from user's root +directory are considered not valid (return <tt>False</tt>). </li></ul><p> </p><hr><p></p><h1><a name="2.1_-_Contrib_package"></a>2.1 - Contrib package<a href="#2.1_-_Contrib_package" class="section_anchor">¶</a></h1><p>Starting from version 0.6.0 a new <tt>contrib</tt> package has been added to <tt>pyftpdlib</tt> namespace which extends base <tt>ftpserver.py</tt> + module. Modules contained in here usually requires third-party modules +to be installed separately or are specific for a given Python version or + operating system. </p><hr><h2><a name="2.2_-_pyftpdlib.contrib.handlers_module"></a>2.2 - pyftpdlib.contrib.handlers module<a href="#2.2_-_pyftpdlib.contrib.handlers_module" class="section_anchor">¶</a></h2><p>This module provides basic support for FTPS (FTP over SSL/TLS) as described in <a href="http://www.ietf.org/rfc/rfc4217.txt" rel="nofollow">RFC-4217</a> implementing AUTH, PBSZ and PROT commands. In order to make it work <a href="http://pypi.python.org/pypi/pyOpenSSL" rel="nofollow">PyOpenSSL module</a> is required to be installed. <a href="http://code.google.com/p/billiejoex/wiki/Tutorial?ts=1284112249&updated=Tutorial#3.7_-_FTPS_%28FTP_over_TLS/SSL%29_server" rel="nofollow">Example below</a> shows how to setup an FTPS server. </p><p><i>class</i> pyftpdlib.contrib.handlers.<strong>TLS_FTPHandler(</strong><i>conn</i>, <i>server</i><strong>)</strong> </p><blockquote>A <tt>ftpserver.FTPHandler</tt> subclass supporting TLS/SSL. Configurable attributes: +</blockquote><p></p><ul><li><strong>certfile</strong><br>The path to a +file which contains a certificate to be used to identify the local side +of the connection. This must always be specified, unless context is +provided instead. </li></ul><ul><li><strong>keyfile</strong><br>The path of the file containing the private RSA key; can be omittetted if certfile already contains the private key (defaults: <tt>None</tt>). </li></ul><ul><li><strong>tls_control_required</strong><br>When <tt>True</tt> + requires SSL/TLS to be established on the control channel, before +logging in. This means the user will have to issue AUTH before USER/PASS + (default <tt>False</tt>). </li></ul><ul><li><strong>tls_data_required</strong><br>When <tt>True</tt> + requires SSL/TLS to be established on the data channel. This means the +user will have to issue PROT before PASV or PORT (default <tt>False</tt>). </li></ul><p> </p><hr><p></p><h2><a name="2.3_-_pyftpdlib.contrib.authorizers_module"></a>2.3 - pyftpdlib.contrib.authorizers module<a href="#2.3_-_pyftpdlib.contrib.authorizers_module" class="section_anchor">¶</a></h2><p>This + module contains two classes for handling users on Unix and Windows +systems. Users are no longer supposed to be explicitly added as when +using <tt>DummyAuthorizer</tt>.<br>All FTP users are the same defined on + the UNIX or Windows system so if you access on your system by using +"john" as username and "12345" as password those same credentials can be + used for accessing the FTP server as well.<br>The user home directories + will be automatically determined when user logins (e.g. /home/user on +Unix, C:\Documents and settings\user on Windows).<br>Every time a +filesystem operation occurs (e.g. a file is created or deleted) the id +of the process is temporarily changed to the effective user id and +whether the operation will succeed depends on user and file permissions. + This is why full read and write permissions are granted by default in +the class constructors. </p><p><i>class</i> pyftpdlib.contrib.authorizers.<strong>UnixAuthorizer(</strong><i>global_perm="elradfmw"</i>, <i>allowed_users=<tt>[]</tt></i>, <i>rejected_users=<tt>[]</tt></i>, <i>require_valid_shell=True</i>, <i>anonymous_user=None</i>, ,<i>msg_login="Login successful."</i>, <i>msg_quit="Goodbye."</i><strong>)</strong> </p><blockquote><tt>global_perm</tt> is a series of letters referencing the users permissions; defaults to <tt>"elradfmw"</tt> which means full read and write access for everybody (except anonymous). <tt>allowed_users</tt> and <tt>rejected_users</tt> options expect a list of users which are accepted or rejected for authenticating against the FTP server; defaults both to <tt>[]</tt> (no restrictions). <tt>require_valid_shell</tt> + deny access for those users which do not have a valid shell binary +listed in /etc/shells. If /etc/shells cannot be found this is a no-op. +Anonymous user is not subject to this option, and is free to not have a +valid shell defined. Defaults to <tt>True</tt> (a valid shell is required for login). <tt>anonymous_user</tt> + can be specified if you intend to provide anonymous access. The value +expected is a string representing the system user to use for managing +anonymous sessions; defaults to <tt>None</tt> (anonymous access disabled).<br> Note that in order to use this class super user privileges are required. +</blockquote><p> </p><blockquote><i><strong>New in version 0.6.0</strong></i> +</blockquote><p></p><ul><li><strong>override_user(</strong><i>username=None</i>, <i>password=None</i>, <i>homedir=None</i>, <i>perm=None</i>, <i>anonymous_user=None</i>, <i>msg_login=None</i>, <i>msg_quit=None</i><strong>)</strong><br>Overrides one or more options specified in the class constructor for a specific user. </li></ul><blockquote>Examples: +</blockquote><pre class="prettyprint">>>> from pyftpdlib.contrib.authorizers import UnixAuthorizer +>>> # accept all except root +>>> auth = UnixAuthorizer(rejected_users=["root"]) +>>> # accept some users only +>>> auth = UnixAuthorizer(allowed_users=["matt", "jay"]) +>>> # accept everybody and don't care if they have not a valid shell +>>> auth = UnixAuthorizer(require_valid_shell=False) +>>> # set specific options for a user +>>> auth.override_user("matt", password="foo", perm="elr")</pre><hr><p><i>class</i> pyftpdlib.contrib.authorizers.<strong>WindowsAuthorizer(</strong><i>global_perm="elradfmw"</i>, <i>allowed_users=<tt>[]</tt></i>, <i>rejected_users=<tt>[]</tt></i>, <i>anonymous_user=None</i>, <i>anonymous_password=""</i>, <i>msg_login="Login successful."</i>, <i>msg_quit="Goodbye."</i><strong>)</strong>: </p><blockquote>Same as <tt>UnixAuthorizer</tt> except for <i>anonymous_password</i> argument which must be specified when defining the <i>anonymous_user</i>.<br>Also <i>requires_valid_shell</i> option is not available. In order to use this class <a href="http://sourceforge.net/projects/pywin32/" rel="nofollow">pywin32 extension</a> must be installed. +</blockquote><blockquote><i><strong>New in version 0.6.0</strong></i> +</blockquote><p> </p><hr><p></p><h2><a name="2.4_-_pyftpdlib.contrib.filesystems_module"></a>2.4 - pyftpdlib.contrib.filesystems module<a href="#2.4_-_pyftpdlib.contrib.filesystems_module" class="section_anchor">¶</a></h2><p>class pyftpdlib.contrib.filesystems.<strong>UnixFilesystem(</strong><i>root</i>, <i>cmd_channel</i><strong>)</strong> </p><blockquote>Represents the real UNIX filesystem. Differently from AbstractedFS the client will login into <tt>/home/<username></tt> and will be able to escape its home directory and navigate the real filesystem. Use it in conjuction with <tt>UnixAuthorizer</tt> to implement a "real" UNIX FTP server (see <a href="http://pyftpdlib.googlecode.com/svn/trunk/demo/unix_ftpd.py" rel="nofollow">demo/unix_ftpd.py</a>). +</blockquote><p></p><blockquote><i><strong>New in version 0.6.0</strong></i> +</blockquote><hr><h1><a name="3.0_-_Customizing_your_FTP_server"></a>3.0 - Customizing your FTP server<a href="#3.0_-_Customizing_your_FTP_server" class="section_anchor">¶</a></h1><p>Below + is a set of example scripts showing some of the possible customizations + that can be done with pyftpdlib. Some of them are included in <a href="http://code.google.com/p/pyftpdlib/source/browse/#svn/trunk/demo" rel="nofollow">demo</a> directory of pyftpdlib source distribution. </p><h2><a name="3.1_-_Building_a_Base_FTP_server"></a>3.1 - Building a Base FTP server<a href="#3.1_-_Building_a_Base_FTP_server" class="section_anchor">¶</a></h2><p>The + script below is a basic configuration, and it's probably the best +starting point for understanding how things work. It uses the base <tt>DummyAuthorizer</tt> for adding a bunch of "virtual" users. </p><p>It also sets a limit for connections by overriding <tt>FTPServer.max_cons</tt> and <tt>FTPServer.max_cons_per_ip</tt> + attributes which are intended to set limits for maximum connections to +handle simultaneously and maximum connections from the same IP address. +Overriding these variables is always a good idea (they default to <tt>0</tt>, or "no limit") since they are a good workaround for avoiding DoS attacks. </p><p><a href="http://pyftpdlib.googlecode.com/svn/trunk/demo/basic_ftpd.py" rel="nofollow">download script</a> </p><pre class="prettyprint">#!/usr/bin/env python + +"""A basic FTP server which uses a DummyAuthorizer for managing 'virtual +users', setting a limit for incoming connections. +""" + +from pyftpdlib import ftpserver + + +def main(): + # Instantiate a dummy authorizer for managing 'virtual' users + authorizer = ftpserver.DummyAuthorizer() + + # Define a new user having full r/w permissions and a read-only + # anonymous user + authorizer.add_user('user', password="12345", homedir='.', perm='elradfmw') + authorizer.add_anonymous(homedir='.') + + # Instantiate FTP handler class + handler = ftpserver.FTPHandler + handler.authorizer = authorizer + + # Define a customized banner (string returned when client connects) + handler.banner = "pyftpdlib %s based ftpd ready." %ftpserver.__ver__ + + # Specify a masquerade address and the range of ports to use for + # passive connections. Decomment in case you're behind a NAT. + #ftp_handler.masquerade_address = '151.25.42.11' + #ftp_handler.passive_ports = range(60000, 65535) + + # Instantiate FTP server class and listen to 0.0.0.0:21 + address = ('', 21) + server = ftpserver.FTPServer(address, handler) + + # set a limit for connections + server.max_cons = 256 + server.max_cons_per_ip = 5 + + # start ftp server + server.serve_forever() + +if __name__ == '__main__': + main()</pre><h2><a name="3.2_-_Logging_management"></a>3.2 - Logging management<a href="#3.2_-_Logging_management" class="section_anchor">¶</a></h2><p>As mentioned, ftpserver.py comes with 3 different functions intended for a separate logging system: <tt>log()</tt>, <tt>logline()</tt> and <tt>logerror()</tt>. Let's suppose you don't want to print FTPd messages on screen but you want to write them into different files: <i>"/var/log/ftpd.log"</i> will be main log file, <i>"/var/log/ftpd.lines.log"</i> the one where you'll want to store commands and responses passing through the control connection. </p><p>Here's one method this could be implemented: </p><pre class="prettyprint">#!/usr/bin/env python + +import os +import time + +from pyftpdlib import ftpserver + +now = lambda: time.strftime("[%Y-%b-%d %H:%M:%S]") + +def standard_logger(msg): + f1.write("%s %s\n" %(now(), msg)) + +def line_logger(msg): + f2.write("%s %s\n" %(now(), msg)) + +if __name__ == "__main__": + f1 = open('ftpd.log', 'a') + f2 = open('ftpd.lines.log', 'a') + ftpserver.log = standard_logger + ftpserver.logline = line_logger + + authorizer = ftpserver.DummyAuthorizer() + authorizer.add_anonymous(os.getcwd()) + handler = ftpserver.FTPHandler + handler.authorizer = authorizer + address = ('', 21) + server = ftpserver.FTPServer(address, handler) + server.serve_forever()</pre><h2><a name="3.3_-_Storing_passwords_as_hash_digests"></a>3.3 - Storing passwords as hash digests<a href="#3.3_-_Storing_passwords_as_hash_digests" class="section_anchor">¶</a></h2><p>Using FTP server library with the default <tt>DummyAuthorizer</tt> + means that password will be stored in clear-text. An end-user ftpd +using the default dummy authorizer would typically require a +configuration file for authenticating users and their passwords but +storing clear-text passwords is of course undesirable. </p><p>The most +common way to do things in such case would be first creating new users +and then storing their usernames + passwords as hash digests into a file + or wherever you find it convenient. </p><p>The example below shows how +to easily create an encrypted account storage system by storing +passwords as one-way hashes by using md5 algorithm. This could be easily + done by using the <strong>hashlib</strong> module included with Python stdlib and by sub-classing the original <tt>DummyAuthorizer</tt> class overriding its <tt>validate_authentication()</tt> method. </p><p><a href="http://pyftpdlib.googlecode.com/svn/trunk/demo/md5_ftpd.py" rel="nofollow">download script</a> </p><pre class="prettyprint">#!/usr/bin/env python + +"""A basic ftpd storing passwords as hash digests.""" + +import os +try: + from hashlib import md5 +except ImportError: + # backward compatibility with Python < 2.5 + from md5 import new as md5 + +from pyftpdlib import ftpserver + + +class DummyMD5Authorizer(ftpserver.DummyAuthorizer): + + def validate_authentication(self, username, password): + hash = md5(password).hexdigest() + return self.user_table[username]['pwd'] == hash + +if __name__ == "__main__": + # get a hash digest from a clear-text password + hash = md5('12345').hexdigest() + authorizer = DummyMD5Authorizer() + authorizer.add_user('user', hash, os.getcwd(), perm='elradfmw') + authorizer.add_anonymous(os.getcwd()) + handler = ftpserver.FTPHandler + handler.authorizer = authorizer + address = ('', 21) + server = ftpserver.FTPServer(address, handler) + server.serve_forever()</pre><h2><a name="3.4_-_Unix_FTP_Server"></a>3.4 - Unix FTP Server<a href="#3.4_-_Unix_FTP_Server" class="section_anchor">¶</a></h2><p>If + you're running a Unix system you may want to configure your ftpd to +include support for "real" users existing on the system and navigate the + real filesystem. </p><p>The example below uses <tt>UnixAuthorizer</tt> and <tt>UnixFilesystem</tt> classes to do so. </p><pre class="prettyprint">#!/usr/bin/env python + +"""A FTPd using local UNIX account database to authenticate users. + +It temporarily impersonate the system users every time they are going +to perform a filesystem operations. +""" + +from pyftpdlib import ftpserver +from pyftpdlib.contrib.authorizers import UnixAuthorizer +from pyftpdlib.contrib.filesystems import UnixFilesystem + + +def main(): + authorizer = UnixAuthorizer(rejected_users=["root"], require_valid_shell=True) + handler = ftpserver.FTPHandler + handler.authorizer = authorizer + handler.abstracted_fs = UnixFilesystem + address = ('', 21) + server = ftpserver.FTPServer(address, handler) + server.serve_forever() + +if __name__ == "__main__": + main()</pre><h2><a name="3.5_-_Windows_NT_FTP_Server"></a>3.5 - Windows NT FTP Server<a href="#3.5_-_Windows_NT_FTP_Server" class="section_anchor">¶</a></h2><p>The + following code shows how to implement a basic authorizer for a Windows +NT workstation to authenticate against existing Windows user accounts. +This code requires Mark Hammond's <a href="http://starship.python.net/crew/mhammond/win32/" rel="nofollow">pywin32</a> extension to be installed. </p><p><a href="http://pyftpdlib.googlecode.com/svn/trunk/demo/winnt_ftpd.py" rel="nofollow">download script</a> </p><pre class="prettyprint">#!/usr/bin/env python + +from pyftpdlib import ftpserver +from pyftpdlib.contrib.authorizers import WindowsAuthorizer + +if __name__ == "__main__": + authorizer = WindowsAuthorizer() + # Use Guest user with empty password to handle anonymous sessions. + # Guest user must be enabled first, empty password set and profile + # directory specified. + #authorizer = WindowsAuthorizer(anonymous_user="Guest", anonymous_password="") + ftp_handler = ftpserver.FTPHandler + ftp_handler.authorizer = authorizer + address = ('', 21) + ftpd = ftpserver.FTPServer(address, ftp_handler) + ftpd.serve_forever()</pre><h2><a name="3.6_-_Throttle_bandwidth"></a>3.6 - Throttle bandwidth<a href="#3.6_-_Throttle_bandwidth" class="section_anchor">¶</a></h2><p>An + important feature for an ftpd is limiting the speed for downloads and +uploads affecting the data channel. Starting from version 0.5.2 it is +possible to use the new <tt>ThrottledDTPHandler</tt> class to set such limits. The basic idea behind <tt>ThrottledDTPHandler</tt> + is to wrap sending and receiving in a data counter and temporary +"sleep" the data channel so that you burst to no more than x Kb/sec +average. When it realizes that more than x Kb in a second are being +transmitted it temporary blocks the transfer for a certain number of +seconds. </p><p>See <a href="http://pyftpdlib.googlecode.com/svn/trunk/demo/throttled_ftpd.py" rel="nofollow">throttled_ftpd.py</a> demo script providing an example on how to use it. </p><h2><a name="3.7_-_FTPS_(FTP_over_TLS/SSL)_server"></a>3.7 - FTPS (FTP over TLS/SSL) server<a href="#3.7_-_FTPS_%28FTP_over_TLS/SSL%29_server" class="section_anchor">¶</a></h2><p>Starting from version 0.6.0 pyftpdlib finally includes full FTPS support implementing both TLS and SSL protocols and <strong>AUTH</strong>, <strong>PBSZ</strong> and <strong>PROT</strong> commands as defined in <a href="http://www.ietf.org/rfc/rfc4217.txt" rel="nofollow">RFC-4217</a>. This has been implemented by using <a href="http://pypi.python.org/pypi/pyOpenSSL" rel="nofollow">PyOpenSSL</a> module, which is required in order to run the code below. <tt>TLS_FTPHandlerFactory</tt> class requires a <tt>certfile</tt> and a <tt>keyfile</tt> to be specified. <a href="http://www.modssl.org/docs/2.7/ssl_faq.html#ToC24" rel="nofollow">Apache FAQs</a> + provide instructions on how to generate them. If you don't care about +having your personal self-signed certificates you can use the one in the + demo directory which include both and is available <a href="http://pyftpdlib.googlecode.com/svn/trunk/demo/keycert.pem" rel="nofollow">here</a>. </p><p><a href="http://pyftpdlib.googlecode.com/svn/trunk/demo/tls_ftpd.py" rel="nofollow">download script</a> </p><pre class="prettyprint">#!/usr/bin/env python + +"""An RFC-4217 asynchronous FTPS server supporting both SSL and TLS. + +Requires PyOpenSSL module (http://pypi.python.org/pypi/pyOpenSSL). +""" + +from pyftpdlib import ftpserver +from pyftpdlib.contrib.handlers import TLS_FTPHandlerFactory + + +if __name__ == '__main__': + authorizer = ftpserver.DummyAuthorizer() + authorizer.add_user('user', '12345', '.', perm='elradfmw') + authorizer.add_anonymous('.') + handler = TLS_FTPHandlerFactory('demo/keycert.pem') + handler.authorizer = authorizer + # requires SSL for both control and data channel + #ftp_handler.tls_control_required = True + #ftp_handler.tls_data_required = True + server = ftpserver.FTPServer(('', 8021), handler) + server.serve_forever()</pre><h2><a name="3.8_-_Event_callbacks"></a>3.8 - Event callbacks<a href="#3.8_-_Event_callbacks" class="section_anchor">¶</a></h2><p>A small example which shows how to use callback methods via <tt>FTPHandler</tt> subclassing: </p><pre class="prettyprint">from pyftpdlib import ftpserver + +class MyHandler(ftpserver.FTPHandler): + + def on_login(self, username): + # do something when user login + pass + + def on_file_sent(self, file): + self.log("user %s just finished downloading file %s" % (self.username, file)) + + def on_file_received(self, file): + import shutil + if file.endswith('.avi'): + shutil.move(file, "my-videos") + + def on_incomplete_file_sent(self, file): + # do something when a file is partially sent + pass + + def on_incomplete_file_received(self, file): + # remove partially uploaded files + os.remove(file) + + +def run(): + authorizer = ftpserver.DummyAuthorizer() + authorizer.add_user('user', '12345', homedir='.', perm='elradfmw') + authorizer.add_anonymous(homedir='.') + + handler = MyHandler + handler.authorizer = authorizer + server = ftpserver.FTPServer(('', 21), handler) + server.serve_forever() + +if __name__ == "__main__": + run() +</pre><h2><a name="3.9_-_Command_line_usage"></a>3.9 - Command line usage<a href="#3.9_-_Command_line_usage" class="section_anchor">¶</a></h2><p>Starting + from version 0.6.0 pyftpdlib can be run as a simple stand-alone server +via Python's -m option, which is particularly useful when you want to +quickly share a directory. Some examples. </p><p>Anonymous FTPd sharing current directory: </p><p><tt>python -m pyftpdlib.ftpserver</tt> </p><p>Anonymous FTPd with write permission: </p><p><tt>python -m pyftpdlib.ftpserver -w</tt> </p><p>Set a different address/port and home directory: </p><p><tt>python -m pyftpdlib.ftpserver -i localhost -p 8021 -d /home/someone</tt> </p><p>See <tt>python -m pyftpdlib.ftpserver -h</tt> for a complete list of options. </p> + </td> + </tr> + </tbody></table> + </div> + + + + +<script type="text/javascript" src="tutorial_files/dit_scripts.js"></script> + + + + </body></html> diff --git a/chromium/third_party/pyftpdlib/src/pyftpdlib/__init__.py b/chromium/third_party/pyftpdlib/src/pyftpdlib/__init__.py new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/pyftpdlib/__init__.py diff --git a/chromium/third_party/pyftpdlib/src/pyftpdlib/contrib/__init__.py b/chromium/third_party/pyftpdlib/src/pyftpdlib/contrib/__init__.py new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/pyftpdlib/contrib/__init__.py diff --git a/chromium/third_party/pyftpdlib/src/pyftpdlib/contrib/authorizers.py b/chromium/third_party/pyftpdlib/src/pyftpdlib/contrib/authorizers.py new file mode 100644 index 00000000000..8ad2aa91340 --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/pyftpdlib/contrib/authorizers.py @@ -0,0 +1,620 @@ +#!/usr/bin/env python +# $Id$ + +# pyftpdlib is released under the MIT license, reproduced below: +# ====================================================================== +# Copyright (C) 2007-2012 Giampaolo Rodola' <g.rodola@gmail.com> +# +# All Rights Reserved +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ====================================================================== + +"""An "authorizer" is a class handling authentications and permissions +of the FTP server. It is used by pyftpdlib.ftpserver.FTPHandler +class for: + +- verifying user password +- getting user home directory +- checking user permissions when a filesystem read/write event occurs +- changing user when accessing the filesystem + +This module contains two classes which implements such functionalities +in a system-specific way for both Unix and Windows. +""" + +__all__ = [] + + +import os +import errno + +from pyftpdlib.ftpserver import DummyAuthorizer, AuthorizerError + + +def replace_anonymous(callable): + """A decorator to replace anonymous user string passed to authorizer + methods as first arugument with the actual user used to handle + anonymous sessions. + """ + def wrapper(self, username, *args, **kwargs): + if username == 'anonymous': + username = self.anonymous_user or username + return callable(self, username, *args, **kwargs) + return wrapper + + +class _Base(object): + """Methods common to both Unix and Windows authorizers. + Not supposed to be used directly. + """ + + def __init__(self): + """Check for errors in the constructor.""" + if self.rejected_users and self.allowed_users: + raise ValueError("rejected_users and allowed_users options are " + "mutually exclusive") + + users = self._get_system_users() + for user in (self.allowed_users or self.rejected_users): + if user == 'anonymous': + raise ValueError('invalid username "anonymous"') + if user not in users: + raise ValueError('unknown user %s' % user) + + if self.anonymous_user is not None: + if not self.has_user(self.anonymous_user): + raise ValueError('no such user %s' % self.anonymous_user) + home = self.get_home_dir(self.anonymous_user) + if not os.path.isdir(home): + raise ValueError('no valid home set for user %s' + % self.anonymous_user) + + def override_user(self, username, password=None, homedir=None, perm=None, + msg_login=None, msg_quit=None): + """Overrides the options specified in the class constructor + for a specific user. + """ + if not password and not homedir and not perm and not msg_login \ + and not msg_quit: + raise ValueError("at least one keyword argument must be specified") + if self.allowed_users and username not in self.allowed_users: + raise ValueError('%s is not an allowed user' % username) + if self.rejected_users and username in self.rejected_users: + raise ValueError('%s is not an allowed user' % username) + if username == "anonymous" and password: + raise ValueError("can't assign password to anonymous user") + if not self.has_user(username): + raise ValueError('no such user %s' % username) + + if username in self._dummy_authorizer.user_table: + # re-set parameters + del self._dummy_authorizer.user_table[username] + self._dummy_authorizer.add_user(username, password or "", + homedir or os.getcwd(), + perm or "", + msg_login or "", + msg_quit or "") + if homedir is None: + self._dummy_authorizer.user_table[username]['home'] = "" + + def get_msg_login(self, username): + return self._get_key(username, 'msg_login') or self.msg_login + + def get_msg_quit(self, username): + return self._get_key(username, 'msg_quit') or self.msg_quit + + def get_perms(self, username): + overridden_perms = self._get_key(username, 'perm') + if overridden_perms: + return overridden_perms + if username == 'anonymous': + return 'elr' + return self.global_perm + + def has_perm(self, username, perm, path=None): + return perm in self.get_perms(username) + + def _get_key(self, username, key): + if self._dummy_authorizer.has_user(username): + return self._dummy_authorizer.user_table[username][key] + + def _is_rejected_user(self, username): + """Return True if the user has been black listed via + allowed_users or rejected_users options. + """ + if self.allowed_users and username not in self.allowed_users: + return True + if self.rejected_users and username in self.rejected_users: + return True + return False + + +# Note: requires python >= 2.5 +try: + import pwd, spwd, crypt +except ImportError: + pass +else: + __all__.extend(['BaseUnixAuthorizer', 'UnixAuthorizer']) + + # the uid/gid the server runs under + PROCESS_UID = os.getuid() + PROCESS_GID = os.getgid() + + class BaseUnixAuthorizer(object): + """An authorizer compatible with Unix user account and password + database. + This class should not be used directly unless for subclassing. + Use higher-level UnixAuthorizer class instead. + """ + + def __init__(self, anonymous_user=None): + if os.geteuid() != 0 or not spwd.getspall(): + raise AuthorizerError("super user privileges are required") + self.anonymous_user = anonymous_user + + if self.anonymous_user is not None: + if not self.anonymous_user in self._get_system_users(): + raise ValueError('no such user %s' % self.anonymous_user) + try: + pwd.getpwnam(self.anonymous_user).pw_dir + except KeyError: + raise ValueError('no such user %s' % anonymous_user) + + # --- overridden / private API + + def validate_authentication(self, username, password): + """Authenticates against shadow password db; return + True on success. + """ + if username == "anonymous": + return self.anonymous_user is not None + try: + pw1 = spwd.getspnam(username).sp_pwd + pw2 = crypt.crypt(password, pw1) + except KeyError: # no such username + return False + else: + return pw1 == pw2 + + @replace_anonymous + def impersonate_user(self, username, password): + """Change process effective user/group ids to reflect + logged in user. + """ + try: + pwdstruct = pwd.getpwnam(username) + except KeyError: + raise AuthorizerError('no such user %s' % username) + else: + os.setegid(pwdstruct.pw_gid) + os.seteuid(pwdstruct.pw_uid) + + def terminate_impersonation(self, username): + """Revert process effective user/group IDs.""" + os.setegid(PROCESS_GID) + os.seteuid(PROCESS_UID) + + @replace_anonymous + def has_user(self, username): + """Return True if user exists on the Unix system. + If the user has been black listed via allowed_users or + rejected_users options always return False. + """ + return username in self._get_system_users() + + @replace_anonymous + def get_home_dir(self, username): + """Return user home directory.""" + try: + return pwd.getpwnam(username).pw_dir + except KeyError: + raise AuthorizerError('no such user %s' % username) + + @staticmethod + def _get_system_users(): + """Return all users defined on the UNIX system.""" + return [entry.pw_name for entry in pwd.getpwall()] + + def get_msg_login(self, username): + return "Login successful." + + def get_msg_quit(self, username): + return "Goodbye." + + def get_perms(self, username): + return "elradfmw" + + def has_perm(self, username, perm, path=None): + return perm in self.get_perms(username) + + + class UnixAuthorizer(_Base, BaseUnixAuthorizer): + """A wrapper on top of BaseUnixAuthorizer providing options + to specify what users should be allowed to login, per-user + options, etc. + + Example usages: + + >>> from pyftpdlib.contrib.authorizers import UnixAuthorizer + >>> # accept all except root + >>> auth = UnixAuthorizer(rejected_users=["root"]) + >>> + >>> # accept some users only + >>> auth = UnixAuthorizer(allowed_users=["matt", "jay"]) + >>> + >>> # accept everybody and don't care if they have not a valid shell + >>> auth = UnixAuthorizer(require_valid_shell=False) + >>> + >>> # set specific options for a user + >>> auth.override_user("matt", password="foo", perm="elr") + """ + + # --- public API + + def __init__(self, global_perm="elradfmw", + allowed_users=[], + rejected_users=[], + require_valid_shell=True, + anonymous_user=None, + msg_login="Login successful.", + msg_quit="Goodbye."): + """Parameters: + + - (string) global_perm: + a series of letters referencing the users permissions; + defaults to "elradfmw" which means full read and write + access for everybody (except anonymous). + + - (list) allowed_users: + a list of users which are accepted for authenticating + against the FTP server; defaults to [] (no restrictions). + + - (list) rejected_users: + a list of users which are not accepted for authenticating + against the FTP server; defaults to [] (no restrictions). + + - (bool) require_valid_shell: + Deny access for those users which do not have a valid shell + binary listed in /etc/shells. + If /etc/shells cannot be found this is a no-op. + Anonymous user is not subject to this option, and is free + to not have a valid shell defined. + Defaults to True (a valid shell is required for login). + + - (string) anonymous_user: + specify it if you intend to provide anonymous access. + The value expected is a string representing the system user + to use for managing anonymous sessions; defaults to None + (anonymous access disabled). + + - (string) msg_login: + the string sent when client logs in. + + - (string) msg_quit: + the string sent when client quits. + """ + BaseUnixAuthorizer.__init__(self, anonymous_user) + self.global_perm = global_perm + self.allowed_users = allowed_users + self.rejected_users = rejected_users + self.anonymous_user = anonymous_user + self.require_valid_shell = require_valid_shell + self.msg_login = msg_login + self.msg_quit = msg_quit + + self._dummy_authorizer = DummyAuthorizer() + self._dummy_authorizer._check_permissions('', global_perm) + _Base.__init__(self) + if require_valid_shell: + for username in self.allowed_users: + if not self._has_valid_shell(username): + raise ValueError("user %s has not a valid shell" + % username) + + def override_user(self, username, password=None, homedir=None, perm=None, + msg_login=None, msg_quit=None): + """Overrides the options specified in the class constructor + for a specific user. + """ + if self.require_valid_shell and username != 'anonymous': + if not self._has_valid_shell(username): + raise ValueError("user %s has not a valid shell" + % username) + _Base.override_user(self, username, password, homedir, perm, + msg_login, msg_quit) + + # --- overridden / private API + + def validate_authentication(self, username, password): + if username == "anonymous": + return self.anonymous_user is not None + if self._is_rejected_user(username): + return False + if self.require_valid_shell and username != 'anonymous': + if not self._has_valid_shell(username): + return False + overridden_password = self._get_key(username, 'pwd') + if overridden_password: + return overridden_password == password + + return BaseUnixAuthorizer.validate_authentication(self, username, password) + + @replace_anonymous + def has_user(self, username): + if self._is_rejected_user(username): + return False + return username in self._get_system_users() + + @replace_anonymous + def get_home_dir(self, username): + overridden_home = self._get_key(username, 'home') + if overridden_home: + return overridden_home + return BaseUnixAuthorizer.get_home_dir(self, username) + + @staticmethod + def _has_valid_shell(username): + """Return True if the user has a valid shell binary listed + in /etc/shells. If /etc/shells can't be found return True. + """ + try: + file = open('/etc/shells', 'r') + except IOError, err: + if err.errno == errno.ENOENT: + return True + raise + else: + try: + try: + shell = pwd.getpwnam(username).pw_shell + except KeyError: # invalid user + return False + for line in file: + if line.startswith('#'): + continue + line = line.strip() + if line == shell: + return True + return False + finally: + file.close() + + +# Note: requires pywin32 extension +try: + import _winreg + import win32security, win32net, pywintypes, win32con, win32api +except ImportError: + pass +else: + __all__.extend(['BaseWindowsAuthorizer', 'WindowsAuthorizer']) + + class BaseWindowsAuthorizer(object): + """An authorizer compatible with Windows user account and + password database. + This class should not be used directly unless for subclassing. + Use higher-level WinowsAuthorizer class instead. + """ + + def __init__(self, anonymous_user=None, anonymous_password=None): + # actually try to impersonate the user + self.anonymous_user = anonymous_user + self.anonymous_password = anonymous_password + if self.anonymous_user is not None: + self.impersonate_user(self.anonymous_user, + self.anonymous_password) + self.terminate_impersonation() + + def validate_authentication(self, username, password): + if username == "anonymous": + return self.anonymous_user is not None + try: + win32security.LogonUser(username, None, password, + win32con.LOGON32_LOGON_INTERACTIVE, + win32con.LOGON32_PROVIDER_DEFAULT) + except pywintypes.error: + return False + else: + return True + + @replace_anonymous + def impersonate_user(self, username, password): + """Impersonate the security context of another user.""" + handler = win32security.LogonUser(username, None, password, + win32con.LOGON32_LOGON_INTERACTIVE, + win32con.LOGON32_PROVIDER_DEFAULT) + win32security.ImpersonateLoggedOnUser(handler) + handler.Close() + + def terminate_impersonation(self, username): + """Terminate the impersonation of another user.""" + win32security.RevertToSelf() + + @replace_anonymous + def has_user(self, username): + return username in self._get_system_users() + + @replace_anonymous + def get_home_dir(self, username): + """Return the user's profile directory, the closest thing + to a user home directory we have on Windows. + """ + try: + sid = win32security.ConvertSidToStringSid( + win32security.LookupAccountName(None, username)[0]) + except pywintypes.error, err: + raise AuthorizerError(err) + path = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" + \ + "\\" + sid + try: + key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, path) + except WindowsError: + raise AuthorizerError("No profile directory defined for user %s" + % username) + value = _winreg.QueryValueEx(key, "ProfileImagePath")[0] + return win32api.ExpandEnvironmentStrings(value) + + @classmethod + def _get_system_users(cls): + """Return all users defined on the Windows system.""" + return [entry['name'] for entry in win32net.NetUserEnum(None, 0)[0]] + + def get_msg_login(self, username): + return "Login successful." + + def get_msg_quit(self, username): + return "Goodbye." + + def get_perms(self, username): + return "elradfmw" + + def has_perm(self, username, perm, path=None): + return perm in self.get_perms(username) + + + class WindowsAuthorizer(_Base, BaseWindowsAuthorizer): + """A wrapper on top of BaseWindowsAuthorizer providing options + to specify what users should be allowed to login, per-user + options, etc. + + Example usages: + + >>> from pyftpdlib.contrib.authorizers import WindowsAuthorizer + >>> # accept all except Administrator + >>> auth = UnixAuthorizer(rejected_users=["Administrator"]) + >>> + >>> # accept some users only + >>> auth = UnixAuthorizer(allowed_users=["matt", "jay"]) + >>> + >>> # set specific options for a user + >>> auth.override_user("matt", password="foo", perm="elr") + """ + + # --- public API + + def __init__(self, global_perm="elradfmw", + allowed_users=[], + rejected_users=[], + anonymous_user=None, + anonymous_password=None, + msg_login="Login successful.", + msg_quit="Goodbye."): + """Parameters: + + - (string) global_perm: + a series of letters referencing the users permissions; + defaults to "elradfmw" which means full read and write + access for everybody (except anonymous). + + - (list) allowed_users: + a list of users which are accepted for authenticating + against the FTP server; defaults to [] (no restrictions). + + - (list) rejected_users: + a list of users which are not accepted for authenticating + against the FTP server; defaults to [] (no restrictions). + + - (string) anonymous_user: + specify it if you intend to provide anonymous access. + The value expected is a string representing the system user + to use for managing anonymous sessions. + As for IIS, it is recommended to use Guest account. + The common practice is to first enable the Guest user, which + is disabled by default and then assign an empty password. + Defaults to None (anonymous access disabled). + + - (string) anonymous_password: + the password of the user who has been chosen to manage the + anonymous sessions. Defaults to None (empty password). + + - (string) msg_login: + the string sent when client logs in. + + - (string) msg_quit: + the string sent when client quits. + """ + self.global_perm = global_perm + self.allowed_users = allowed_users + self.rejected_users = rejected_users + self.anonymous_user = anonymous_user + self.anonymous_password = anonymous_password + self.msg_login = msg_login + self.msg_quit = msg_quit + self._dummy_authorizer = DummyAuthorizer() + self._dummy_authorizer._check_permissions('', global_perm) + _Base.__init__(self) + # actually try to impersonate the user + if self.anonymous_user is not None: + self.impersonate_user(self.anonymous_user, + self.anonymous_password) + self.terminate_impersonation() + + def override_user(self, username, password=None, homedir=None, perm=None, + msg_login=None, msg_quit=None): + """Overrides the options specified in the class constructor + for a specific user. + """ + _Base.override_user(self, username, password, homedir, perm, + msg_login, msg_quit) + + # --- overridden / private API + + def validate_authentication(self, username, password): + """Authenticates against Windows user database; return + True on success. + """ + if username == "anonymous": + return self.anonymous_user is not None + if self.allowed_users and username not in self.allowed_users: + return False + if self.rejected_users and username in self.rejected_users: + return False + + overridden_password = self._get_key(username, 'pwd') + if overridden_password: + return overridden_password == password + else: + return BaseWindowsAuthorizer.validate_authentication(self, + username, password) + + def impersonate_user(self, username, password): + """Impersonate the security context of another user.""" + if username == "anonymous": + username = self.anonymous_user or "" + password = self.anonymous_password or "" + return BaseWindowsAuthorizer.impersonate_user(self, username, password) + + @replace_anonymous + def has_user(self, username): + if self._is_rejected_user(username): + return False + return username in self._get_system_users() + + @replace_anonymous + def get_home_dir(self, username): + overridden_home = self._get_key(username, 'home') + if overridden_home: + return overridden_home + return BaseWindowsAuthorizer.get_home_dir(self, username) diff --git a/chromium/third_party/pyftpdlib/src/pyftpdlib/contrib/filesystems.py b/chromium/third_party/pyftpdlib/src/pyftpdlib/contrib/filesystems.py new file mode 100644 index 00000000000..b81b1b57565 --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/pyftpdlib/contrib/filesystems.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# $Id$ + +# pyftpdlib is released under the MIT license, reproduced below: +# ====================================================================== +# Copyright (C) 2007-2012 Giampaolo Rodola' <g.rodola@gmail.com> +# +# All Rights Reserved +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ====================================================================== + +from pyftpdlib.ftpserver import AbstractedFS + +__all__ = ['UnixFilesystem'] + + +class UnixFilesystem(AbstractedFS): + """Represents the real UNIX filesystem. + + Differently from AbstractedFS the client will login into + /home/<username> and will be able to escape its home directory + and navigate the real filesystem. + """ + + def __init__(self, root, cmd_channel): + AbstractedFS.__init__(self, root, cmd_channel) + # initial cwd was set to "/" to emulate a chroot jail + self.cwd = root + + def ftp2fs(self, ftppath): + return self.ftpnorm(ftppath) + + def fs2ftp(self, fspath): + return fspath + + def validpath(self, path): + # validpath was used to check symlinks escaping user home + # directory; this is no longer necessary. + return True diff --git a/chromium/third_party/pyftpdlib/src/pyftpdlib/contrib/handlers.py b/chromium/third_party/pyftpdlib/src/pyftpdlib/contrib/handlers.py new file mode 100644 index 00000000000..efc17bd21ef --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/pyftpdlib/contrib/handlers.py @@ -0,0 +1,448 @@ +#!/usr/bin/env python +# $Id$ + +# ====================================================================== +# Copyright (C) 2007-2012 Giampaolo Rodola' <g.rodola@gmail.com> +# +# All Rights Reserved +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ====================================================================== + + +"""This module is supposed to contain a series of classes which extend +base pyftpdlib.ftpserver's FTPHandler and DTPHandler classes. + +As for now only one class is provided: TLS_FTPHandler. +It is supposed to provide basic support for FTPS (FTP over SSL/TLS) as +described in RFC-4217. + +Requires PyOpenSSL module (http://pypi.python.org/pypi/pyOpenSSL). +For Python versions prior to 2.6 ssl module must be installed separately, +see: http://pypi.python.org/pypi/ssl/ + +Development status: experimental. +""" + + +import os +import asyncore +import socket +import warnings +import errno + +from pyftpdlib.ftpserver import FTPHandler, DTPHandler, proto_cmds, _DISCONNECTED + +__all__ = [] + +# requires PyOpenSSL - http://pypi.python.org/pypi/pyOpenSSL +try: + from OpenSSL import SSL +except ImportError: + pass +else: + __all__.extend(['SSLConnection', 'TLS_FTPHandler', 'TLS_DTPHandler']) + + + new_proto_cmds = proto_cmds.copy() + new_proto_cmds.update({ + 'AUTH': dict(perm=None, auth=False, arg=True, + help='Syntax: AUTH <SP> TLS|SSL (set up secure control channel).'), + 'PBSZ': dict(perm=None, auth=False, arg=True, + help='Syntax: PBSZ <SP> 0 (negotiate TLS buffer).'), + 'PROT': dict(perm=None, auth=False, arg=True, + help='Syntax: PROT <SP> [C|P] (set up un/secure data channel).'), + }) + + + class SSLConnection(object, asyncore.dispatcher): + """An asyncore.dispatcher subclass supporting TLS/SSL.""" + + _ssl_accepting = False + _ssl_established = False + _ssl_closing = False + + def __init__(self, *args, **kwargs): + super(SSLConnection, self).__init__(*args, **kwargs) + self._error = False + + def secure_connection(self, ssl_context): + """Secure the connection switching from plain-text to + SSL/TLS. + """ + try: + self.socket = SSL.Connection(ssl_context, self.socket) + except socket.error: + self.close() + else: + self.socket.set_accept_state() + self._ssl_accepting = True + + def _do_ssl_handshake(self): + self._ssl_accepting = True + try: + self.socket.do_handshake() + except (SSL.WantReadError, SSL.WantWriteError): + return + except SSL.SysCallError, (retval, desc): + if (retval == -1 and desc == 'Unexpected EOF') or retval > 0: + return self.handle_close() + raise + except SSL.Error: + return self.handle_failed_ssl_handshake() + else: + self._ssl_accepting = False + self._ssl_established = True + self.handle_ssl_established() + + def handle_ssl_established(self): + """Called when SSL handshake has completed.""" + pass + + def handle_ssl_shutdown(self): + """Called when SSL shutdown() has completed.""" + super(SSLConnection, self).close() + + def handle_failed_ssl_handshake(self): + raise NotImplementedError("must be implemented in subclass") + + def handle_read_event(self): + if self._ssl_accepting: + self._do_ssl_handshake() + elif self._ssl_closing: + self._do_ssl_shutdown() + else: + super(SSLConnection, self).handle_read_event() + + def handle_write_event(self): + if self._ssl_accepting: + self._do_ssl_handshake() + elif self._ssl_closing: + self._do_ssl_shutdown() + else: + super(SSLConnection, self).handle_write_event() + + def handle_error(self): + self._error = True + try: + raise + except (KeyboardInterrupt, SystemExit, asyncore.ExitNow): + raise + except: + self.log_exception(self) + # when facing an unhandled exception in here it's better + # to rely on base class (FTPHandler or DTPHandler) + # close() method as it does not imply SSL shutdown logic + super(SSLConnection, self).close() + + def send(self, data): + try: + return super(SSLConnection, self).send(data) + except (SSL.WantReadError, SSL.WantWriteError): + return 0 + except SSL.ZeroReturnError: + super(SSLConnection, self).handle_close() + return 0 + except SSL.SysCallError, (errnum, errstr): + if errnum == errno.EWOULDBLOCK: + return 0 + elif errnum in _DISCONNECTED or errstr == 'Unexpected EOF': + super(SSLConnection, self).handle_close() + return 0 + else: + raise + + def recv(self, buffer_size): + try: + return super(SSLConnection, self).recv(buffer_size) + except (SSL.WantReadError, SSL.WantWriteError): + return '' + except SSL.ZeroReturnError: + super(SSLConnection, self).handle_close() + return '' + except SSL.SysCallError, (errnum, errstr): + if errnum in _DISCONNECTED or errstr == 'Unexpected EOF': + super(SSLConnection, self).handle_close() + return '' + else: + raise + + def _do_ssl_shutdown(self): + """Executes a SSL_shutdown() call to revert the connection + back to clear-text. + twisted/internet/tcp.py code has been used as an example. + """ + self._ssl_closing = True + # since SSL_shutdown() doesn't report errors, an empty + # write call is done first, to try to detect if the + # connection has gone away + try: + os.write(self.socket.fileno(), '') + except (OSError, socket.error), err: + if err.args[0] in (errno.EINTR, errno.EWOULDBLOCK, errno.ENOBUFS): + return + elif err.args[0] in _DISCONNECTED: + return super(SSLConnection, self).close() + else: + raise + # Ok, this a mess, but the underlying OpenSSL API simply + # *SUCKS* and I really couldn't do any better. + # + # Here we just want to shutdown() the SSL layer and then + # close() the connection so we're not interested in a + # complete SSL shutdown() handshake, so let's pretend + # we already received a "RECEIVED" shutdown notification + # from the client. + # Once the client received our "SENT" shutdown notification + # then we close() the connection. + # + # Since it is not clear what errors to expect during the + # entire procedure we catch them all and assume the + # following: + # - WantReadError and WantWriteError means "retry" + # - ZeroReturnError, SysCallError[EOF], Error[] are all + # aliases for disconnection + try: + laststate = self.socket.get_shutdown() + self.socket.set_shutdown(laststate | SSL.RECEIVED_SHUTDOWN) + done = self.socket.shutdown() + if not (laststate & SSL.RECEIVED_SHUTDOWN): + self.socket.set_shutdown(SSL.SENT_SHUTDOWN) + except (SSL.WantReadError, SSL.WantWriteError): + pass + except SSL.ZeroReturnError: + super(SSLConnection, self).close() + except SSL.SysCallError, (errnum, errstr): + if errnum in _DISCONNECTED or errstr == 'Unexpected EOF': + super(SSLConnection, self).close() + else: + raise + except SSL.Error, err: + # see: + # http://code.google.com/p/pyftpdlib/issues/detail?id=171 + # https://bugs.launchpad.net/pyopenssl/+bug/785985 + if err.args and not err.args[0]: + pass + else: + raise + except socket.error, err: + if err.args[0] in _DISCONNECTED: + super(SSLConnection, self).close() + else: + raise + else: + if done: + self._ssl_established = False + self._ssl_closing = False + self.handle_ssl_shutdown() + + def close(self): + if self._ssl_established and not self._error: + self._do_ssl_shutdown() + else: + self._ssl_accepting = False + self._ssl_established = False + self._ssl_closing = False + super(SSLConnection, self).close() + + + class TLS_DTPHandler(SSLConnection, DTPHandler): + """A ftpserver.DTPHandler subclass supporting TLS/SSL.""" + + def __init__(self, sock_obj, cmd_channel): + super(TLS_DTPHandler, self).__init__(sock_obj, cmd_channel) + if self.cmd_channel._prot: + self.secure_connection(self.cmd_channel.ssl_context) + + def _use_sendfile(self, producer): + return False + + def handle_failed_ssl_handshake(self): + # TLS/SSL handshake failure, probably client's fault which + # used a SSL version different from server's. + # RFC-4217, chapter 10.2 expects us to return 522 over the + # command channel. + self.cmd_channel.respond("522 SSL handshake failed.") + self.cmd_channel.log_cmd("PROT", "P", 522, "SSL handshake failed.") + self.close() + + + class TLS_FTPHandler(SSLConnection, FTPHandler): + """A ftpserver.FTPHandler subclass supporting TLS/SSL. + Implements AUTH, PBSZ and PROT commands (RFC-2228 and RFC-4217). + + Configurable attributes: + + - (bool) tls_control_required: + When True requires SSL/TLS to be established on the control + channel, before logging in. This means the user will have + to issue AUTH before USER/PASS (default False). + + - (bool) tls_data_required: + When True requires SSL/TLS to be established on the data + channel. This means the user will have to issue PROT + before PASV or PORT (default False). + + SSL-specific options: + + - (string) certfile: + the path to the file which contains a certificate to be + used to identify the local side of the connection. + This must always be specified, unless context is provided + instead. + + - (string) keyfile: + the path to the file containing the private RSA key; + can be omittetted if certfile already contains the + private key (defaults: None). + + - (int) protocol: + specifies which version of the SSL protocol to use when + establishing SSL/TLS sessions; clients can then only + connect using the configured protocol (defaults to SSLv23, + allowing SSLv3 and TLSv1 protocols). + + Possible values: + * SSL.SSLv2_METHOD - allow only SSLv2 + * SSL.SSLv3_METHOD - allow only SSLv3 + * SSL.SSLv23_METHOD - allow both SSLv3 and TLSv1 + * SSL.TLSv1_METHOD - allow only TLSv1 + + - (instance) context: + a SSL Context object previously configured; if specified + all other parameters will be ignored. + (default None). + """ + + # configurable attributes + tls_control_required = False + tls_data_required = False + certfile = None + keyfile = None + ssl_protocol = SSL.SSLv23_METHOD + ssl_context = None + + # overridden attributes + proto_cmds = new_proto_cmds + dtp_handler = TLS_DTPHandler + + def __init__(self, conn, server): + super(TLS_FTPHandler, self).__init__(conn, server) + if not self.connected: + return + self._extra_feats = ['AUTH TLS', 'AUTH SSL', 'PBSZ', 'PROT'] + self._pbsz = False + self._prot = False + self.ssl_context = self.get_ssl_context() + + @classmethod + def get_ssl_context(cls): + if cls.ssl_context is None: + if cls.certfile is None: + raise ValueError("at least certfile must be specified") + cls.ssl_context = SSL.Context(cls.ssl_protocol) + if cls.ssl_protocol != SSL.SSLv2_METHOD: + cls.ssl_context.set_options(SSL.OP_NO_SSLv2) + else: + warnings.warn("SSLv2 protocol is insecure", RuntimeWarning) + cls.ssl_context.use_certificate_file(cls.certfile) + if not cls.keyfile: + cls.keyfile = cls.certfile + cls.ssl_context.use_privatekey_file(cls.keyfile) + TLS_FTPHandler.ssl_context = cls.ssl_context + return cls.ssl_context + + # --- overridden methods + + def flush_account(self): + FTPHandler.flush_account(self) + self._pbsz = False + self._prot = False + + def process_command(self, cmd, *args, **kwargs): + if cmd in ('USER', 'PASS'): + if self.tls_control_required and not self._ssl_established: + msg = "SSL/TLS required on the control channel." + self.respond("550 " + msg) + self.log_cmd(cmd, args[0], 550, msg) + return + elif cmd in ('PASV', 'EPSV', 'PORT', 'EPRT'): + if self.tls_data_required and not self._prot: + msg = "SSL/TLS required on the data channel." + self.respond("550 " + msg) + self.log_cmd(cmd, args[0], 550, msg) + return + FTPHandler.process_command(self, cmd, *args, **kwargs) + + # --- new methods + + def handle_failed_ssl_handshake(self): + # TLS/SSL handshake failure, probably client's fault which + # used a SSL version different from server's. + # We can't rely on the control connection anymore so we just + # disconnect the client without sending any response. + self.log("SSL handshake failed.") + self.close() + + def ftp_AUTH(self, line): + """Set up secure control channel.""" + arg = line.upper() + if isinstance(self.socket, SSL.Connection): + self.respond("503 Already using TLS.") + elif arg in ('TLS', 'TLS-C', 'SSL', 'TLS-P'): + # From RFC-4217: "As the SSL/TLS protocols self-negotiate + # their levels, there is no need to distinguish between SSL + # and TLS in the application layer". + self.respond('234 AUTH %s successful.' %arg) + self.secure_connection(self.ssl_context) + else: + self.respond("502 Unrecognized encryption type (use TLS or SSL).") + + def ftp_PBSZ(self, line): + """Negotiate size of buffer for secure data transfer. + For TLS/SSL the only valid value for the parameter is '0'. + Any other value is accepted but ignored. + """ + if not isinstance(self.socket, SSL.Connection): + self.respond("503 PBSZ not allowed on insecure control connection.") + else: + self.respond('200 PBSZ=0 successful.') + self._pbsz = True + + def ftp_PROT(self, line): + """Setup un/secure data channel.""" + arg = line.upper() + if not isinstance(self.socket, SSL.Connection): + self.respond("503 PROT not allowed on insecure control connection.") + elif not self._pbsz: + self.respond("503 You must issue the PBSZ command prior to PROT.") + elif arg == 'C': + self.respond('200 Protection set to Clear') + self._prot = False + elif arg == 'P': + self.respond('200 Protection set to Private') + self._prot = True + elif arg in ('S', 'E'): + self.respond('521 PROT %s unsupported (use C or P).' %arg) + else: + self.respond("502 Unrecognized PROT type (use C or P).") diff --git a/chromium/third_party/pyftpdlib/src/pyftpdlib/ftpserver.py b/chromium/third_party/pyftpdlib/src/pyftpdlib/ftpserver.py new file mode 100644 index 00000000000..33a54d38a1f --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/pyftpdlib/ftpserver.py @@ -0,0 +1,3966 @@ +#!/usr/bin/env python +# $Id$ + +# pyftpdlib is released under the MIT license, reproduced below: +# ====================================================================== +# Copyright (C) 2007-2012 Giampaolo Rodola' <g.rodola@gmail.com> +# +# All Rights Reserved +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ====================================================================== + + +"""pyftpdlib: RFC-959 asynchronous FTP server. + +pyftpdlib implements a fully functioning asynchronous FTP server as +defined in RFC-959. A hierarchy of classes outlined below implement +the backend functionality for the FTPd: + + [FTPServer] - the base class for the backend. + + [FTPHandler] - a class representing the server-protocol-interpreter + (server-PI, see RFC-959). Each time a new connection occurs + FTPServer will create a new FTPHandler instance to handle the + current PI session. + + [ActiveDTP], [PassiveDTP] - base classes for active/passive-DTP + backends. + + [DTPHandler] - this class handles processing of data transfer + operations (server-DTP, see RFC-959). + + [ThrottledDTPHandler] - a DTPHandler subclass implementing transfer + rates limits. + + [DummyAuthorizer] - an "authorizer" is a class handling FTPd + authentications and permissions. It is used inside FTPHandler class + to verify user passwords, to get user's home directory and to get + permissions when a filesystem read/write occurs. "DummyAuthorizer" + is the base authorizer class providing a platform independent + interface for managing virtual users. + + [AbstractedFS] - class used to interact with the file system, + providing a high level, cross-platform interface compatible + with both Windows and UNIX style filesystems. + + [CallLater] - calls a function at a later time whithin the polling + loop asynchronously. + + [AuthorizerError] - base class for authorizers exceptions. + + +pyftpdlib also provides 3 different logging streams through 3 functions +which can be overridden to allow for custom logging. + + [log] - the main logger that logs the most important messages for + the end user regarding the FTPd. + + [logline] - this function is used to log commands and responses + passing through the control FTP channel. + + [logerror] - log traceback outputs occurring in case of errors. + + +Usage example: + +>>> from pyftpdlib import ftpserver +>>> authorizer = ftpserver.DummyAuthorizer() +>>> authorizer.add_user('user', 'password', '/home/user', perm='elradfmw') +>>> authorizer.add_anonymous('/home/nobody') +>>> ftp_handler = ftpserver.FTPHandler +>>> ftp_handler.authorizer = authorizer +>>> address = ("127.0.0.1", 21) +>>> ftpd = ftpserver.FTPServer(address, ftp_handler) +>>> ftpd.serve_forever() +Serving FTP on 127.0.0.1:21 +[]127.0.0.1:2503 connected. +127.0.0.1:2503 ==> 220 Ready. +127.0.0.1:2503 <== USER anonymous +127.0.0.1:2503 ==> 331 Username ok, send password. +127.0.0.1:2503 <== PASS ****** +127.0.0.1:2503 ==> 230 Login successful. +[anonymous]@127.0.0.1:2503 User anonymous logged in. +127.0.0.1:2503 <== TYPE A +127.0.0.1:2503 ==> 200 Type set to: ASCII. +127.0.0.1:2503 <== PASV +127.0.0.1:2503 ==> 227 Entering passive mode (127,0,0,1,9,201). +127.0.0.1:2503 <== LIST +127.0.0.1:2503 ==> 150 File status okay. About to open data connection. +[anonymous]@127.0.0.1:2503 OK LIST "/". Transfer starting. +127.0.0.1:2503 ==> 226 Transfer complete. +[anonymous]@127.0.0.1:2503 Transfer complete. 706 bytes transmitted. +127.0.0.1:2503 <== QUIT +127.0.0.1:2503 ==> 221 Goodbye. +[anonymous]@127.0.0.1:2503 Disconnected. +""" + + +import asyncore +import asynchat +import socket +import os +import sys +import traceback +import errno +import time +import glob +import tempfile +import warnings +import random +import stat +import heapq +import optparse +from tarfile import filemode as _filemode + +try: + import pwd + import grp +except ImportError: + pwd = grp = None + +# http://code.google.com/p/pysendfile/ +try: + from sendfile import sendfile +except ImportError: + sendfile = None + + +__all__ = ['proto_cmds', 'Error', 'log', 'logline', 'logerror', 'DummyAuthorizer', + 'AuthorizerError', 'FTPHandler', 'FTPServer', 'PassiveDTP', + 'ActiveDTP', 'DTPHandler', 'ThrottledDTPHandler', 'FileProducer', + 'BufferedIteratorProducer', 'AbstractedFS', 'CallLater', 'CallEvery'] + + +__pname__ = 'Python FTP server library (pyftpdlib)' +__ver__ = '0.7.0' +__date__ = 'XXXX-XX-XX' +__author__ = "Giampaolo Rodola' <g.rodola@gmail.com>" +__web__ = 'http://code.google.com/p/pyftpdlib/' + + +_DISCONNECTED = frozenset((errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN, + errno.ECONNABORTED, errno.EPIPE, errno.EBADF)) + +proto_cmds = { + 'ABOR' : dict(perm=None, auth=True, arg=False, + help='Syntax: ABOR (abort transfer).'), + 'ALLO' : dict(perm=None, auth=True, arg=True, + help='Syntax: ALLO <SP> bytes (noop; allocate storage).'), + 'APPE' : dict(perm='a', auth=True, arg=True, + help='Syntax: APPE <SP> file-name (append data to file).'), + 'CDUP' : dict(perm='e', auth=True, arg=False, + help='Syntax: CDUP (go to parent directory).'), + 'CWD' : dict(perm='e', auth=True, arg=None, + help='Syntax: CWD [<SP> dir-name] (change working directory).'), + 'DELE' : dict(perm='d', auth=True, arg=True, + help='Syntax: DELE <SP> file-name (delete file).'), + 'EPRT' : dict(perm=None, auth=True, arg=True, + help='Syntax: EPRT <SP> |proto|ip|port| (extended active mode).'), + 'EPSV' : dict(perm=None, auth=True, arg=None, + help='Syntax: EPSV [<SP> proto/"ALL"] (extended passive mode).'), + 'FEAT' : dict(perm=None, auth=False, arg=False, + help='Syntax: FEAT (list all new features supported).'), + 'HELP' : dict(perm=None, auth=False, arg=None, + help='Syntax: HELP [<SP> cmd] (show help).'), + 'LIST' : dict(perm='l', auth=True, arg=None, + help='Syntax: LIST [<SP> path] (list files).'), + 'MDTM' : dict(perm='l', auth=True, arg=True, + help='Syntax: MDTM [<SP> path] (file last modification time).'), + 'MLSD' : dict(perm='l', auth=True, arg=None, + help='Syntax: MLSD [<SP> path] (list directory).'), + 'MLST' : dict(perm='l', auth=True, arg=None, + help='Syntax: MLST [<SP> path] (show information about path).'), + 'MODE' : dict(perm=None, auth=True, arg=True, + help='Syntax: MODE <SP> mode (noop; set data transfer mode).'), + 'MKD' : dict(perm='m', auth=True, arg=True, + help='Syntax: MKD <SP> path (create directory).'), + 'NLST' : dict(perm='l', auth=True, arg=None, + help='Syntax: NLST [<SP> path] (list path in a compact form).'), + 'NOOP' : dict(perm=None, auth=False, arg=False, + help='Syntax: NOOP (just do nothing).'), + 'OPTS' : dict(perm=None, auth=True, arg=True, + help='Syntax: OPTS <SP> cmd [<SP> option] (set option for command).'), + 'PASS' : dict(perm=None, auth=False, arg=True, + help='Syntax: PASS <SP> password (set user password).'), + 'PASV' : dict(perm=None, auth=True, arg=False, + help='Syntax: PASV (open passive data connection).'), + 'PORT' : dict(perm=None, auth=True, arg=True, + help='Syntax: PORT <sp> h1,h2,h3,h4,p1,p2 (open active data connection).'), + 'PWD' : dict(perm=None, auth=True, arg=False, + help='Syntax: PWD (get current working directory).'), + 'QUIT' : dict(perm=None, auth=False, arg=False, + help='Syntax: QUIT (quit current session).'), + 'REIN' : dict(perm=None, auth=True, arg=False, + help='Syntax: REIN (flush account).'), + 'REST' : dict(perm=None, auth=True, arg=True, + help='Syntax: REST <SP> offset (set file offset).'), + 'RETR' : dict(perm='r', auth=True, arg=True, + help='Syntax: RETR <SP> file-name (retrieve a file).'), + 'RMD' : dict(perm='d', auth=True, arg=True, + help='Syntax: RMD <SP> dir-name (remove directory).'), + 'RNFR' : dict(perm='f', auth=True, arg=True, + help='Syntax: RNFR <SP> file-name (rename (source name)).'), + 'RNTO' : dict(perm='f', auth=True, arg=True, + help='Syntax: RNTO <SP> file-name (rename (destination name)).'), + 'SITE' : dict(perm=None, auth=False, arg=True, + help='Syntax: SITE <SP> site-command (execute SITE command).'), + 'SITE HELP' : dict(perm=None, auth=False, arg=None, + help='Syntax: SITE HELP [<SP> site-command] (show SITE command help).'), + 'SITE CHMOD': dict(perm='M', auth=True, arg=True, + help='Syntax: SITE CHMOD <SP> mode path (change file mode).'), + 'SIZE' : dict(perm='l', auth=True, arg=True, + help='Syntax: SIZE <SP> file-name (get file size).'), + 'STAT' : dict(perm='l', auth=False, arg=None, + help='Syntax: STAT [<SP> path name] (server stats [list files]).'), + 'STOR' : dict(perm='w', auth=True, arg=True, + help='Syntax: STOR <SP> file-name (store a file).'), + 'STOU' : dict(perm='w', auth=True, arg=None, + help='Syntax: STOU [<SP> file-name] (store a file with a unique name).'), + 'STRU' : dict(perm=None, auth=True, arg=True, + help='Syntax: STRU <SP> type (noop; set file structure).'), + 'SYST' : dict(perm=None, auth=False, arg=False, + help='Syntax: SYST (get operating system type).'), + 'TYPE' : dict(perm=None, auth=True, arg=True, + help='Syntax: TYPE <SP> [A | I] (set transfer type).'), + 'USER' : dict(perm=None, auth=False, arg=True, + help='Syntax: USER <SP> user-name (set username).'), + 'XCUP' : dict(perm='e', auth=True, arg=False, + help='Syntax: XCUP (obsolete; go to parent directory).'), + 'XCWD' : dict(perm='e', auth=True, arg=None, + help='Syntax: XCWD [<SP> dir-name] (obsolete; change directory).'), + 'XMKD' : dict(perm='m', auth=True, arg=True, + help='Syntax: XMKD <SP> dir-name (obsolete; create directory).'), + 'XPWD' : dict(perm=None, auth=True, arg=False, + help='Syntax: XPWD (obsolete; get current dir).'), + 'XRMD' : dict(perm='d', auth=True, arg=True, + help='Syntax: XRMD <SP> dir-name (obsolete; remove directory).'), + } + +if not hasattr(os, 'chmod'): + del proto_cmds['SITE CHMOD'] + + +# A wrapper around os.strerror() which may be not available +# on all platforms (e.g. pythonCE). Expected arg is a +# EnvironmentError or derived class instance. +if hasattr(os, 'strerror'): + _strerror = lambda err: os.strerror(err.errno) +else: + _strerror = lambda err: err.strerror + + +class _Scheduler(object): + """Run the scheduled functions due to expire soonest (if any).""" + + def __init__(self): + # the heap used for the scheduled tasks + self._tasks = [] + self._cancellations = 0 + + def __call__(self): + now = time.time() + calls = [] + while self._tasks: + if now < self._tasks[0].timeout: + break + call = heapq.heappop(self._tasks) + if not call.cancelled: + calls.append(call) + else: + self._cancellations -= 1 + + for call in calls: + if call._repush: + heapq.heappush(self._tasks, call) + call._repush = False + continue + try: + call.call() + except (KeyboardInterrupt, SystemExit, asyncore.ExitNow): + raise + except: + logerror(traceback.format_exc()) + + # remove cancelled tasks and re-heapify the queue if the + # number of cancelled tasks is more than the half of the + # entire queue + if self._cancellations > 512 \ + and self._cancellations > (len(self._tasks) >> 1): + self._cancellations = 0 + self._tasks = [x for x in self._tasks if not x.cancelled] + self.reheapify() + + def register(self, what): + heapq.heappush(self._tasks, what) + + def unregister(self, what): + self._cancellations += 1 + + def reheapify(self): + heapq.heapify(self._tasks) + + +_scheduler = _Scheduler() + +# dirty hack to support property.setter on python < 2.6 +if not hasattr(property, "setter"): + class property(property): + def setter(self, value): + cls_ns = sys._getframe(1).f_locals + for k, v in cls_ns.iteritems(): + if v == self: + name = k + break + cls_ns[name] = property(self.fget, value, self.fdel, self.__doc__) + return cls_ns[name] + +_months_map = {1:'Jan', 2:'Feb', 3:'Mar', 4:'Apr', 5:'May', 6:'Jun', 7:'Jul', + 8:'Aug', 9:'Sep', 10:'Oct', 11:'Nov', 12:'Dec'} + + +class CallLater(object): + """Calls a function at a later time. + + It can be used to asynchronously schedule a call within the polling + loop without blocking it. The instance returned is an object that + can be used to cancel or reschedule the call. + """ + __slots__ = ('_delay', '_target', '_args', '_kwargs', '_errback', + '_repush', 'timeout', 'cancelled') + + def __init__(self, seconds, target, *args, **kwargs): + """ + - (int) seconds: the number of seconds to wait + - (obj) target: the callable object to call later + - args: the arguments to call it with + - kwargs: the keyword arguments to call it with; a special + '_errback' parameter can be passed: it is a callable + called in case target function raises an exception. + """ + assert callable(target), "%s is not callable" % target + assert sys.maxint >= seconds >= 0, "%s is not greater than or equal " \ + "to 0 seconds" % seconds + self._delay = seconds + self._target = target + self._args = args + self._kwargs = kwargs + self._errback = kwargs.pop('_errback', None) + self._repush = False + # seconds from the epoch at which to call the function + self.timeout = time.time() + self._delay + self.cancelled = False + _scheduler.register(self) + + def __lt__(self, other): + return self.timeout < other.timeout + + def __le__(self, other): + return self.timeout <= other.timeout + + def _post_call(self, exc): + if not self.cancelled: + self.cancel() + + def call(self): + """Call this scheduled function.""" + assert not self.cancelled, "Already cancelled" + exc = None + try: + try: + self._target(*self._args, **self._kwargs) + except (KeyboardInterrupt, SystemExit, asyncore.ExitNow): + raise + except Exception, exc: + if self._errback is not None: + self._errback() + else: + raise + finally: + self._post_call(exc) + + def reset(self): + """Reschedule this call resetting the current countdown.""" + assert not self.cancelled, "Already cancelled" + self.timeout = time.time() + self._delay + self._repush = True + + def delay(self, seconds): + """Reschedule this call for a later time.""" + assert not self.cancelled, "Already cancelled." + assert sys.maxint >= seconds >= 0, "%s is not greater than or equal " \ + "to 0 seconds" % seconds + self._delay = seconds + newtime = time.time() + self._delay + if newtime > self.timeout: + self.timeout = newtime + self._repush = True + else: + # XXX - slow, can be improved + self.timeout = newtime + _scheduler.reheapify() + + def cancel(self): + """Unschedule this call.""" + assert not self.cancelled, "Already cancelled" + self.cancelled = True + del self._target, self._args, self._kwargs, self._errback + _scheduler.unregister(self) + + +class CallEvery(CallLater): + """Calls a function every x seconds. + It accepts the same arguments as CallLater and shares the same API. + """ + + def _post_call(self, exc): + if not self.cancelled: + if exc: + self.cancel() + else: + self.timeout = time.time() + self._delay + _scheduler.register(self) + + +# --- library defined exceptions + +class Error(Exception): + """Base class for module exceptions.""" + +class AuthorizerError(Error): + """Base class for authorizer exceptions.""" + +class _FileReadWriteError(OSError): + """Exception raised when reading or writing a file during a transfer.""" + + +# --- loggers + +def log(msg): + """Log messages intended for the end user.""" + print msg + +def logline(msg): + """Log commands and responses passing through the command channel.""" + print msg + +def logerror(msg): + """Log traceback outputs occurring in case of errors.""" + sys.stderr.write(str(msg) + '\n') + sys.stderr.flush() + + +# --- authorizers + +class DummyAuthorizer(object): + """Basic "dummy" authorizer class, suitable for subclassing to + create your own custom authorizers. + + An "authorizer" is a class handling authentications and permissions + of the FTP server. It is used inside FTPHandler class for verifying + user's password, getting users home directory, checking user + permissions when a file read/write event occurs and changing user + before accessing the filesystem. + + DummyAuthorizer is the base authorizer, providing a platform + independent interface for managing "virtual" FTP users. System + dependent authorizers can by written by subclassing this base + class and overriding appropriate methods as necessary. + """ + + read_perms = "elr" + write_perms = "adfmwM" + + def __init__(self): + self.user_table = {} + + def add_user(self, username, password, homedir, perm='elr', + msg_login="Login successful.", msg_quit="Goodbye."): + """Add a user to the virtual users table. + + AuthorizerError exceptions raised on error conditions such as + invalid permissions, missing home directory or duplicate usernames. + + Optional perm argument is a string referencing the user's + permissions explained below: + + Read permissions: + - "e" = change directory (CWD command) + - "l" = list files (LIST, NLST, STAT, MLSD, MLST, SIZE, MDTM commands) + - "r" = retrieve file from the server (RETR command) + + Write permissions: + - "a" = append data to an existing file (APPE command) + - "d" = delete file or directory (DELE, RMD commands) + - "f" = rename file or directory (RNFR, RNTO commands) + - "m" = create directory (MKD command) + - "w" = store a file to the server (STOR, STOU commands) + - "M" = change file mode (SITE CHMOD command) + + Optional msg_login and msg_quit arguments can be specified to + provide customized response strings when user log-in and quit. + """ + if self.has_user(username): + raise ValueError('user "%s" already exists' % username) + if not os.path.isdir(homedir): + raise ValueError('no such directory: "%s"' % homedir) + homedir = os.path.realpath(homedir) + self._check_permissions(username, perm) + dic = {'pwd': str(password), + 'home': homedir, + 'perm': perm, + 'operms': {}, + 'msg_login': str(msg_login), + 'msg_quit': str(msg_quit) + } + self.user_table[username] = dic + + def add_anonymous(self, homedir, **kwargs): + """Add an anonymous user to the virtual users table. + + AuthorizerError exception raised on error conditions such as + invalid permissions, missing home directory, or duplicate + anonymous users. + + The keyword arguments in kwargs are the same expected by + add_user method: "perm", "msg_login" and "msg_quit". + + The optional "perm" keyword argument is a string defaulting to + "elr" referencing "read-only" anonymous user's permissions. + + Using write permission values ("adfmwM") results in a + RuntimeWarning. + """ + DummyAuthorizer.add_user(self, 'anonymous', '', homedir, **kwargs) + + def remove_user(self, username): + """Remove a user from the virtual users table.""" + del self.user_table[username] + + def override_perm(self, username, directory, perm, recursive=False): + """Override permissions for a given directory.""" + self._check_permissions(username, perm) + if not os.path.isdir(directory): + raise ValueError('no such directory: "%s"' % directory) + directory = os.path.normcase(os.path.realpath(directory)) + home = os.path.normcase(self.get_home_dir(username)) + if directory == home: + raise ValueError("can't override home directory permissions") + if not self._issubpath(directory, home): + raise ValueError("path escapes user home directory") + self.user_table[username]['operms'][directory] = perm, recursive + + def validate_authentication(self, username, password): + """Return True if the supplied username and password match the + stored credentials.""" + if not self.has_user(username): + return False + if username == 'anonymous': + return True + return self.user_table[username]['pwd'] == password + + def impersonate_user(self, username, password): + """Impersonate another user (noop). + + It is always called before accessing the filesystem. + By default it does nothing. The subclass overriding this + method is expected to provide a mechanism to change the + current user. + """ + + def terminate_impersonation(self, username): + """Terminate impersonation (noop). + + It is always called after having accessed the filesystem. + By default it does nothing. The subclass overriding this + method is expected to provide a mechanism to switch back + to the original user. + """ + + def has_user(self, username): + """Whether the username exists in the virtual users table.""" + return username in self.user_table + + def has_perm(self, username, perm, path=None): + """Whether the user has permission over path (an absolute + pathname of a file or a directory). + + Expected perm argument is one of the following letters: + "elradfmwM". + """ + if path is None: + return perm in self.user_table[username]['perm'] + + path = os.path.normcase(path) + for dir in self.user_table[username]['operms'].keys(): + operm, recursive = self.user_table[username]['operms'][dir] + if self._issubpath(path, dir): + if recursive: + return perm in operm + if (path == dir) or (os.path.dirname(path) == dir \ + and not os.path.isdir(path)): + return perm in operm + + return perm in self.user_table[username]['perm'] + + def get_perms(self, username): + """Return current user permissions.""" + return self.user_table[username]['perm'] + + def get_home_dir(self, username): + """Return the user's home directory.""" + return self.user_table[username]['home'] + + def get_msg_login(self, username): + """Return the user's login message.""" + return self.user_table[username]['msg_login'] + + def get_msg_quit(self, username): + """Return the user's quitting message.""" + return self.user_table[username]['msg_quit'] + + def _check_permissions(self, username, perm): + warned = 0 + for p in perm: + if p not in self.read_perms + self.write_perms: + raise ValueError('no such permission "%s"' % p) + if (username == 'anonymous') and (p in self.write_perms) and not warned: + warnings.warn("write permissions assigned to anonymous user.", + RuntimeWarning) + warned = 1 + + def _issubpath(self, a, b): + """Return True if a is a sub-path of b or if the paths are equal.""" + p1 = a.rstrip(os.sep).split(os.sep) + p2 = b.rstrip(os.sep).split(os.sep) + return p1[:len(p2)] == p2 + + + +# --- DTP classes + +class PassiveDTP(object, asyncore.dispatcher): + """This class is an asyncore.dispatcher subclass. It creates a + socket listening on a local port, dispatching the resultant + connection to DTPHandler. + + - (int) timeout: the timeout for a remote client to establish + connection with the listening socket. Defaults to 30 seconds. + """ + timeout = 30 + + def __init__(self, cmd_channel, extmode=False): + """Initialize the passive data server. + + - (instance) cmd_channel: the command channel class instance. + - (bool) extmode: wheter use extended passive mode response type. + """ + self.cmd_channel = cmd_channel + self.log = cmd_channel.log + self.log_exception = cmd_channel.log_exception + self._closed = False + asyncore.dispatcher.__init__(self) + if self.timeout: + self._idler = CallLater(self.timeout, self.handle_timeout, + _errback=self.handle_error) + else: + self._idler = None + + local_ip = self.cmd_channel.socket.getsockname()[0] + if local_ip in self.cmd_channel.masquerade_address_map: + masqueraded_ip = self.cmd_channel.masquerade_address_map[local_ip] + elif self.cmd_channel.masquerade_address: + masqueraded_ip = self.cmd_channel.masquerade_address + else: + masqueraded_ip = None + + self.create_socket(self.cmd_channel._af, socket.SOCK_STREAM) + + if self.cmd_channel.passive_ports is None: + # By using 0 as port number value we let kernel choose a + # free unprivileged random port. + self.bind((local_ip, 0)) + else: + ports = list(self.cmd_channel.passive_ports) + while ports: + port = ports.pop(random.randint(0, len(ports) -1)) + self.set_reuse_addr() + try: + self.bind((local_ip, port)) + except socket.error, err: + if err.args[0] == errno.EADDRINUSE: # port already in use + if ports: + continue + # If cannot use one of the ports in the configured + # range we'll use a kernel-assigned port, and log + # a message reporting the issue. + # By using 0 as port number value we let kernel + # choose a free unprivileged random port. + else: + self.bind((local_ip, 0)) + self.log( + "Can't find a valid passive port in the " + "configured range. A random kernel-assigned " + "port will be used." + ) + else: + raise + else: + break + self.listen(5) + + port = self.socket.getsockname()[1] + if not extmode: + ip = masqueraded_ip or local_ip + if ip.startswith('::ffff:'): + # In this scenario, the server has an IPv6 socket, but + # the remote client is using IPv4 and its address is + # represented as an IPv4-mapped IPv6 address which + # looks like this ::ffff:151.12.5.65, see: + # http://en.wikipedia.org/wiki/IPv6#IPv4-mapped_addresses + # http://tools.ietf.org/html/rfc3493.html#section-3.7 + # We truncate the first bytes to make it look like a + # common IPv4 address. + ip = ip[7:] + # The format of 227 response in not standardized. + # This is the most expected: + self.cmd_channel.respond('227 Entering passive mode (%s,%d,%d).' % ( + ip.replace('.', ','), port // 256, port % 256)) + else: + self.cmd_channel.respond('229 Entering extended passive mode ' + '(|||%d|).' % port) + + def set_reuse_addr(self): + # overridden for convenience; avoid to reuse address on Windows + if (os.name in ('nt', 'ce')) or (sys.platform == 'cygwin'): + return + asyncore.dispatcher.set_reuse_addr(self) + + # --- connection / overridden + + def handle_accept(self): + """Called when remote client initiates a connection.""" + if not self.cmd_channel.connected: + return self.close() + try: + sock, addr = self.accept() + except TypeError: + # sometimes accept() might return None (see issue 91) + return + except socket.error, err: + # ECONNABORTED might be thrown on *BSD (see issue 105) + if err.args[0] != errno.ECONNABORTED: + self.log_exception(self) + return + else: + # sometimes addr == None instead of (ip, port) (see issue 104) + if addr == None: + return + + # Check the origin of data connection. If not expressively + # configured we 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]: + if not self.cmd_channel.permit_foreign_addresses: + try: + sock.close() + except socket.error: + pass + msg = 'Rejected data connection from foreign address %s:%s.' \ + %(addr[0], addr[1]) + self.cmd_channel.respond("425 %s" % msg) + self.log(msg) + # do not close listening socket: it couldn't be client's blame + return + else: + # site-to-site FTP allowed + msg = 'Established data connection with foreign address %s:%s.'\ + % (addr[0], addr[1]) + self.log(msg) + # Immediately close the current channel (we accept only one + # connection at time) and avoid running out of max connections + # limit. + self.close() + # delegate such connection to DTP handler + if self.cmd_channel.connected: + handler = self.cmd_channel.dtp_handler(sock, self.cmd_channel) + if handler.connected: + self.cmd_channel.data_channel = handler + self.cmd_channel._on_dtp_connection() + + def handle_timeout(self): + if self.cmd_channel.connected: + self.cmd_channel.respond("421 Passive data channel timed out.") + self.close() + + def writable(self): + return 0 + + def handle_error(self): + """Called to handle any uncaught exceptions.""" + try: + raise + except (KeyboardInterrupt, SystemExit, asyncore.ExitNow): + raise + except: + logerror(traceback.format_exc()) + self.close() + + def close(self): + if not self._closed: + self._closed = True + asyncore.dispatcher.close(self) + if self._idler is not None and not self._idler.cancelled: + self._idler.cancel() + + +class ActiveDTP(object, asyncore.dispatcher): + """This class is an asyncore.disptacher subclass. It creates a + socket resulting from the connection to a remote user-port, + dispatching it to DTPHandler. + + - (int) timeout: the timeout for us to establish connection with + the client's listening data socket. + """ + timeout = 30 + + def __init__(self, ip, port, cmd_channel): + """Initialize the active data channel attemping to connect + to remote data socket. + + - (str) ip: the remote IP address. + - (int) port: the remote port. + - (instance) cmd_channel: the command channel class instance. + """ + self.cmd_channel = cmd_channel + self.log = cmd_channel.log + self.log_exception = cmd_channel.log_exception + self._closed = True + asyncore.dispatcher.__init__(self) + if self.timeout: + self._idler = CallLater(self.timeout, self.handle_timeout, + _errback=self.handle_error) + else: + self._idler = None + if ip.count('.') == 4: + self._cmd = "PORT" + self._normalized_addr = "%s:%s" % (ip, port) + else: + self._cmd = "EPRT" + self._normalized_addr = "[%s]:%s" % (ip, port) + + self.create_socket(self.cmd_channel._af, socket.SOCK_STREAM) + # Have the active connection come from the same IP address + # as the command channel, see: + # http://code.google.com/p/pyftpdlib/issues/detail?id=123 + source_ip = self.cmd_channel.socket.getsockname()[0] + self.bind((source_ip, 0)) + try: + self.connect((ip, port)) + except (socket.gaierror, socket.error): + self.handle_expt() + + # overridden to prevent unhandled read/write event messages to + # be printed by asyncore on Python < 2.6 + + def handle_write(self): + pass + + def handle_read(self): + pass + + def handle_connect(self): + """Called when connection is established.""" + if self._idler is not None and not self._idler.cancelled: + self._idler.cancel() + if not self.cmd_channel.connected: + return self.close() + # fix for asyncore on python < 2.6, meaning we aren't + # actually connected. + # test_active_conn_error tests this condition + err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + if err != 0: + raise socket.error(err) + # + msg = 'Active data connection established.' + self.cmd_channel.respond('200 ' + msg) + self.cmd_channel.log_cmd(self._cmd, self._normalized_addr, 200, msg) + # + if not self.cmd_channel.connected: + return self.close() + # delegate such connection to DTP handler + handler = self.cmd_channel.dtp_handler(self.socket, self.cmd_channel) + self.cmd_channel.data_channel = handler + self.cmd_channel._on_dtp_connection() + # Can't close right now as the handler would have the socket + # object disconnected. This class will be "closed" once the + # data transfer is completed or the client disconnects. + #self.close() + + def handle_timeout(self): + if self.cmd_channel.connected: + msg = "Active data channel timed out." + self.cmd_channel.respond("421 " + msg) + self.cmd_channel.log_cmd(self._cmd, self._normalized_addr, 421, msg) + self.close() + + def handle_expt(self): + if self.cmd_channel.connected: + msg = "Can't connect to specified address." + self.cmd_channel.respond("425 " + msg) + self.cmd_channel.log_cmd(self._cmd, self._normalized_addr, 425, msg) + self.close() + + def handle_error(self): + """Called to handle any uncaught exceptions.""" + try: + raise + except (KeyboardInterrupt, SystemExit, asyncore.ExitNow): + raise + except (socket.gaierror, socket.error): + pass + except: + self.log_exception(self) + self.handle_expt() + + def close(self): + if not self._closed: + self._closed = True + asyncore.dispatcher.close(self) + if self._idler is not None and not self._idler.cancelled: + self._idler.cancel() + + +class DTPHandler(object, asynchat.async_chat): + """Class handling server-data-transfer-process (server-DTP, see + RFC-959) managing data-transfer operations involving sending + and receiving data. + + Class attributes: + + - (int) timeout: the timeout which roughly is the maximum time we + permit data transfers to stall for with no progress. If the + timeout triggers, the remote client will be kicked off + (defaults 300). + + - (int) ac_in_buffer_size: incoming data buffer size (defaults 65536) + + - (int) ac_out_buffer_size: outgoing data buffer size (defaults 65536) + """ + + timeout = 300 + ac_in_buffer_size = 65536 + ac_out_buffer_size = 65536 + + def __init__(self, sock_obj, cmd_channel): + """Initialize the command channel. + + - (instance) sock_obj: the socket object instance of the newly + established connection. + - (instance) cmd_channel: the command channel class instance. + """ + self.cmd_channel = cmd_channel + self.file_obj = None + self.receive = False + self.transfer_finished = False + self.tot_bytes_sent = 0 + self.tot_bytes_received = 0 + self.cmd = None + self.log = cmd_channel.log + self.log_exception = cmd_channel.log_exception + self._data_wrapper = lambda x: x + self._lastdata = 0 + self._closed = False + self._had_cr = False + self._start_time = time.time() + self._resp = None + self._offset = None + self._filefd = None + if self.timeout: + self._idler = CallEvery(self.timeout, self.handle_timeout, + _errback=self.handle_error) + else: + self._idler = None + try: + asynchat.async_chat.__init__(self, sock_obj) + except socket.error, err: + # if we get an exception here we want the dispatcher + # instance to set socket attribute before closing, see: + # http://code.google.com/p/pyftpdlib/issues/detail?id=188 + asynchat.async_chat.__init__(self, socket.socket()) + # http://code.google.com/p/pyftpdlib/issues/detail?id=143 + self.close() + if err.args[0] == errno.EINVAL: + return + self.handle_error() + return + # remove this instance from asyncore socket map + if not self.connected: + self.close() + + def _use_sendfile(self, producer): + return self.cmd_channel.use_sendfile \ + and isinstance(producer, FileProducer) \ + and producer.type == 'i' + + def push_with_producer(self, producer): + if self._use_sendfile(producer): + self._offset = producer.file.tell() + self._filefd = self.file_obj.fileno() + self.initiate_sendfile() + self.initiate_send = self.initiate_sendfile + else: + asynchat.async_chat.push_with_producer(self, producer) + + def initiate_sendfile(self): + """A wrapper around sendfile.""" + try: + sent = sendfile(self._fileno, self._filefd, self._offset, + self.ac_out_buffer_size) + except OSError, err: + if err.errno in (errno.EAGAIN, errno.EWOULDBLOCK, errno.EBUSY): + return + elif err.errno in _DISCONNECTED: + self.handle_close() + else: + raise + else: + if sent == 0: + # this signals the channel that the transfer is completed + self.discard_buffers() + self.handle_close() + else: + self._offset += sent + self.tot_bytes_sent += sent + + # --- utility methods + + def _posix_ascii_data_wrapper(self, chunk): + """The data wrapper used for receiving data in ASCII mode on + systems using a single line terminator, handling those cases + where CRLF ('\r\n') gets delivered in two chunks. + """ + if self._had_cr: + chunk = '\r' + chunk + + if chunk.endswith('\r'): + self._had_cr = True + chunk = chunk[:-1] + else: + self._had_cr = False + + return chunk.replace('\r\n', os.linesep) + + def enable_receiving(self, type, cmd): + """Enable receiving of data over the channel. Depending on the + TYPE currently in use it creates an appropriate wrapper for the + incoming data. + + - (str) type: current transfer type, 'a' (ASCII) or 'i' (binary). + """ + self.cmd = cmd + if type == 'a': + if os.linesep == '\r\n': + self._data_wrapper = lambda x: x + else: + self._data_wrapper = self._posix_ascii_data_wrapper + elif type == 'i': + self._data_wrapper = lambda x: x + else: + raise TypeError("unsupported type") + self.receive = True + + def get_transmitted_bytes(self): + "Return the number of transmitted bytes." + return self.tot_bytes_sent + self.tot_bytes_received + + def get_elapsed_time(self): + "Return the transfer elapsed time in seconds." + return time.time() - self._start_time + + def transfer_in_progress(self): + "Return True if a transfer is in progress, else False." + return self.get_transmitted_bytes() != 0 + + # --- connection + + def send(self, data): + result = asyncore.dispatcher.send(self, data) + self.tot_bytes_sent += result + return result + + def refill_buffer (self): + """Overridden as a fix around http://bugs.python.org/issue1740572 + (when the producer is consumed, close() was called instead of + handle_close()). + """ + while 1: + if len(self.producer_fifo): + p = self.producer_fifo.first() + # a 'None' in the producer fifo is a sentinel, + # telling us to close the channel. + if p is None: + if not self.ac_out_buffer: + self.producer_fifo.pop() + #self.close() + self.handle_close() + return + elif isinstance(p, str): + self.producer_fifo.pop() + 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): + """Called when there is data waiting to be read.""" + try: + chunk = self.recv(self.ac_in_buffer_size) + except socket.error: + self.handle_error() + else: + self.tot_bytes_received += len(chunk) + if not chunk: + self.transfer_finished = True + #self.close() # <-- asyncore.recv() already do that... + return + try: + self.file_obj.write(self._data_wrapper(chunk)) + except OSError, err: + raise _FileReadWriteError(err) + + def readable(self): + """Predicate for inclusion in the readable for select().""" + # we don't use asynchat's find terminator feature so we can + # freely avoid to call the original implementation + return self.receive + + def writable(self): + """Predicate for inclusion in the writable for select().""" + return not self.receive and asynchat.async_chat.writable(self) + + def handle_timeout(self): + """Called cyclically to check if data trasfer is stalling with + no progress in which case the client is kicked off. + """ + if self.get_transmitted_bytes() > self._lastdata: + self._lastdata = self.get_transmitted_bytes() + else: + msg = "Data connection timed out." + self.log(msg) + self._resp = "421 " + msg + self.close() + self.cmd_channel.close_when_done() + + def handle_expt(self): + """Called on "exceptional" data events.""" + self.cmd_channel.respond("426 Connection error; transfer aborted.") + self.close() + + def handle_error(self): + """Called when an exception is raised and not otherwise handled.""" + try: + raise + except (KeyboardInterrupt, SystemExit, asyncore.ExitNow): + raise + except socket.error, err: + # fixes around various bugs: + # - http://bugs.python.org/issue1736101 + # - http://code.google.com/p/pyftpdlib/issues/detail?id=104 + # - http://code.google.com/p/pyftpdlib/issues/detail?id=109 + if err.args[0] in _DISCONNECTED: + self.handle_close() + return + else: + self.log_exception(self) + error = str(err.args[1]) + # an error could occur in case we fail reading / writing + # from / to file (e.g. file system gets full) + except _FileReadWriteError, err: + error = _strerror(err.args[0]) + except: + # some other exception occurred; we don't want to provide + # confidential error messages + self.log_exception(self) + error = "Internal error" + self._resp = "426 %s; transfer aborted." % error + self.close() + + def handle_close(self): + """Called when the socket is closed.""" + # If we used channel for receiving we assume that transfer is + # finished when client closes the 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). + # In both cases handle_close() is automatically called by the + # underlying asynchat module. + if self.receive: + self.transfer_finished = True + else: + self.transfer_finished = len(self.producer_fifo) == 0 + if self.transfer_finished: + self._resp = "226 Transfer complete." + else: + tot_bytes = self.get_transmitted_bytes() + self._resp = "426 Transfer aborted; %d bytes transmitted." % tot_bytes + self.close() + + def close(self): + """Close the data channel, first attempting to close any remaining + file handles.""" + if not self._closed: + self._closed = True + # RFC-959 says we must close the connection before replying + asyncore.dispatcher.close(self) + if self._resp: + self.cmd_channel.respond(self._resp) + + if self.file_obj is not None and not self.file_obj.closed: + self.file_obj.close() + if self._idler is not None and not self._idler.cancelled: + self._idler.cancel() + if self.file_obj is not None: + filename = self.file_obj.name + elapsed_time = round(self.get_elapsed_time(), 3) + self.cmd_channel.log_transfer(cmd=self.cmd, + filename=self.file_obj.name, + receive=self.receive, + completed=self.transfer_finished, + elapsed=elapsed_time, + bytes=self.get_transmitted_bytes()) + if self.transfer_finished: + if self.receive: + self.cmd_channel.on_file_received(filename) + else: + self.cmd_channel.on_file_sent(filename) + else: + if self.receive: + self.cmd_channel.on_incomplete_file_received(filename) + else: + self.cmd_channel.on_incomplete_file_sent(filename) + self.cmd_channel._on_dtp_close() + + +class ThrottledDTPHandler(DTPHandler): + """A DTPHandler subclass which wraps sending and receiving in a data + counter and temporarily "sleeps" the channel so that you burst to no + more than x Kb/sec average. + + - (int) read_limit: the maximum number of bytes to read (receive) + in one second (defaults to 0 == no limit). + + - (int) write_limit: the maximum number of bytes to write (send) + in one second (defaults to 0 == no limit). + + - (bool) auto_sized_buffers: this option only applies when read + and/or write limits are specified. When enabled it bumps down + the data buffer sizes so that they are never greater than read + and write limits which results in a less bursty and smoother + throughput (default: True). + """ + read_limit = 0 + write_limit = 0 + auto_sized_buffers = True + + def __init__(self, sock_obj, cmd_channel): + super(ThrottledDTPHandler, self).__init__(sock_obj, cmd_channel) + self._timenext = 0 + self._datacount = 0 + self.sleeping = False + self._throttler = None + + if self.auto_sized_buffers: + if self.read_limit: + while self.ac_in_buffer_size > self.read_limit: + self.ac_in_buffer_size /= 2 + if self.write_limit: + while self.ac_out_buffer_size > self.write_limit: + self.ac_out_buffer_size /= 2 + + def _use_sendfile(self, producer): + return False + + def readable(self): + return not self.sleeping and super(ThrottledDTPHandler, self).readable() + + def writable(self): + return not self.sleeping and super(ThrottledDTPHandler, self).writable() + + def recv(self, buffer_size): + chunk = super(ThrottledDTPHandler, self).recv(buffer_size) + if self.read_limit: + self._throttle_bandwidth(len(chunk), self.read_limit) + return chunk + + def send(self, data): + num_sent = super(ThrottledDTPHandler, self).send(data) + if self.write_limit: + self._throttle_bandwidth(num_sent, self.write_limit) + return num_sent + + def _throttle_bandwidth(self, len_chunk, max_speed): + """A method which counts data transmitted so that you burst to + no more than x Kb/sec average. + """ + self._datacount += len_chunk + if self._datacount >= max_speed: + self._datacount = 0 + now = time.time() + sleepfor = self._timenext - now + if sleepfor > 0: + # we've passed bandwidth limits + def unsleep(): + self.sleeping = False + self.sleeping = True + self._throttler = CallLater(sleepfor * 2, unsleep, + _errback=self.handle_error) + self._timenext = now + 1 + + def close(self): + if self._throttler is not None and not self._throttler.cancelled: + self._throttler.cancel() + super(ThrottledDTPHandler, self).close() + + +# --- producers + + +class FileProducer(object): + """Producer wrapper for file[-like] objects.""" + + buffer_size = 65536 + + def __init__(self, file, type): + """Initialize the producer with a data_wrapper appropriate to TYPE. + + - (file) file: the file[-like] object. + - (str) type: the current TYPE, 'a' (ASCII) or 'i' (binary). + """ + self.done = False + self.file = file + self.type = type + if type == 'a': + if os.linesep == '\r\n': + self._data_wrapper = lambda x: x + else: + self._data_wrapper = lambda x: x.replace(os.linesep, '\r\n') + elif type == 'i': + self._data_wrapper = lambda x: x + else: + raise TypeError("unsupported type") + + def more(self): + """Attempt a chunk of data of size self.buffer_size.""" + if self.done: + return '' + try: + data = self._data_wrapper(self.file.read(self.buffer_size)) + except OSError, err: + raise _FileReadWriteError(err) + if not data: + self.done = True + if not self.file.closed: + self.file.close() + return data + + +class BufferedIteratorProducer(object): + """Producer for iterator objects with buffer capabilities.""" + # how many times iterator.next() will be called before + # returning some data + loops = 20 + + def __init__(self, iterator): + self.iterator = iterator + + def more(self): + """Attempt a chunk of data from iterator by calling + its next() method different times. + """ + buffer = [] + for x in xrange(self.loops): + try: + buffer.append(self.iterator.next()) + except StopIteration: + break + return ''.join(buffer) + + +# --- filesystem + +class AbstractedFS(object): + """A class used to interact with the file system, providing a + cross-platform interface compatible with both Windows and + UNIX style filesystems where all paths use "/" separator. + + AbstractedFS distinguishes between "real" filesystem paths and + "virtual" ftp paths emulating a UNIX chroot jail where the user + can not escape its home directory (example: real "/home/user" + path will be seen as "/" by the client) + + It also provides some utility methods and wraps around all os.* + calls involving operations against the filesystem like creating + files or removing directories. + """ + + def __init__(self, root, cmd_channel): + """ + - (str) root: the user "real" home directory (e.g. '/home/user') + - (instance) cmd_channel: the FTPHandler class instance + """ + # Set initial current working directory. + # By default initial cwd is set to "/" to emulate a chroot jail. + # If a different behavior is desired (e.g. initial cwd = root, + # to reflect the real filesystem) users overriding this class + # are responsible to set _cwd attribute as necessary. + self._cwd = '/' + self._root = root + self.cmd_channel = cmd_channel + + @property + def root(self): + """The user home directory.""" + return self._root + + @property + def cwd(self): + """The user current working directory.""" + return self._cwd + + @root.setter + def root(self, path): + self._root = path + + @cwd.setter + def cwd(self, path): + self._cwd = path + + # --- Pathname / conversion utilities + + def ftpnorm(self, ftppath): + """Normalize a "virtual" ftp pathname (tipically the raw string + coming from client) depending on the current working directory. + + Example (having "/foo" as current working directory): + >>> ftpnorm('bar') + '/foo/bar' + + Note: directory separators are system independent ("/"). + Pathname returned is always absolutized. + """ + if os.path.isabs(ftppath): + p = os.path.normpath(ftppath) + else: + p = os.path.normpath(os.path.join(self.cwd, ftppath)) + # normalize string in a standard web-path notation having '/' + # as separator. + p = p.replace("\\", "/") + # os.path.normpath supports UNC paths (e.g. "//a/b/c") but we + # don't need them. In case we get an UNC path we collapse + # redundant separators appearing at the beginning of the string + while p[:2] == '//': + p = p[1:] + # Anti path traversal: don't trust user input, in the event + # that self.cwd is not absolute, return "/" as a safety measure. + # This is for extra protection, maybe not really necessary. + if not os.path.isabs(p): + p = "/" + return p + + def ftp2fs(self, ftppath): + """Translate a "virtual" ftp pathname (tipically the raw string + coming from client) into equivalent absolute "real" filesystem + pathname. + + Example (having "/home/user" as root directory): + >>> ftp2fs("foo") + '/home/user/foo' + + Note: directory separators are system dependent. + """ + # as far as I know, it should always be path traversal safe... + if os.path.normpath(self.root) == os.sep: + return os.path.normpath(self.ftpnorm(ftppath)) + else: + p = self.ftpnorm(ftppath)[1:] + return os.path.normpath(os.path.join(self.root, p)) + + def fs2ftp(self, fspath): + """Translate a "real" filesystem pathname into equivalent + absolute "virtual" ftp pathname depending on the user's + root directory. + + Example (having "/home/user" as root directory): + >>> fs2ftp("/home/user/foo") + '/foo' + + As for ftpnorm, directory separators are system independent + ("/") and pathname returned is always absolutized. + + On invalid pathnames escaping from user's root directory + (e.g. "/home" when root is "/home/user") always return "/". + """ + if os.path.isabs(fspath): + p = os.path.normpath(fspath) + else: + p = os.path.normpath(os.path.join(self.root, fspath)) + if not self.validpath(p): + return '/' + p = p.replace(os.sep, "/") + p = p[len(self.root):] + if not p.startswith('/'): + p = '/' + p + return p + + def validpath(self, path): + """Check whether the path belongs to user's home directory. + Expected argument is a "real" filesystem pathname. + + If path is a symbolic link it is resolved to check its real + destination. + + Pathnames escaping from user's root directory are considered + not valid. + """ + root = self.realpath(self.root) + path = self.realpath(path) + if not root.endswith(os.sep): + root = root + os.sep + if not path.endswith(os.sep): + path = path + os.sep + if path[0:len(root)] == root: + return True + return False + + # --- Wrapper methods around open() and tempfile.mkstemp + + def open(self, filename, mode): + """Open a file returning its handler.""" + return open(filename, mode) + + def mkstemp(self, suffix='', prefix='', dir=None, mode='wb'): + """A wrap around tempfile.mkstemp creating a file with a unique + name. Unlike mkstemp it returns an object with a file-like + interface. + """ + class FileWrapper: + def __init__(self, fd, name): + self.file = fd + self.name = name + def __getattr__(self, attr): + return getattr(self.file, attr) + + text = not 'b' in mode + # max number of tries to find out a unique file name + tempfile.TMP_MAX = 50 + fd, name = tempfile.mkstemp(suffix, prefix, dir, text=text) + file = os.fdopen(fd, mode) + return FileWrapper(file, name) + + # --- Wrapper methods around os.* calls + + def chdir(self, path): + """Change the current directory.""" + # temporarily join the specified directory to see if we have + # permissions to do so + basedir = os.getcwd() + try: + os.chdir(path) + except OSError: + raise + else: + os.chdir(basedir) + self._cwd = self.fs2ftp(path) + + def mkdir(self, path): + """Create the specified directory.""" + os.mkdir(path) + + def listdir(self, path): + """List the content of a directory.""" + return os.listdir(path) + + def rmdir(self, path): + """Remove the specified directory.""" + os.rmdir(path) + + def remove(self, path): + """Remove the specified file.""" + os.remove(path) + + def rename(self, src, dst): + """Rename the specified src file to the dst filename.""" + os.rename(src, dst) + + def chmod(self, path, mode): + """Change file/directory mode.""" + if not hasattr(os, 'chmod'): + raise NotImplementedError + os.chmod(path, mode) + + def stat(self, path): + """Perform a stat() system call on the given path.""" + return os.stat(path) + + def lstat(self, path): + """Like stat but does not follow symbolic links.""" + return os.lstat(path) + + if not hasattr(os, 'lstat'): + lstat = stat + + # --- Wrapper methods around os.path.* calls + + def isfile(self, path): + """Return True if path is a file.""" + return os.path.isfile(path) + + def islink(self, path): + """Return True if path is a symbolic link.""" + return os.path.islink(path) + + def isdir(self, path): + """Return True if path is a directory.""" + return os.path.isdir(path) + + def getsize(self, path): + """Return the size of the specified file in bytes.""" + return os.path.getsize(path) + + def getmtime(self, path): + """Return the last modified time as a number of seconds since + the epoch.""" + return os.path.getmtime(path) + + def realpath(self, path): + """Return the canonical version of path eliminating any + symbolic links encountered in the path (if they are + supported by the operating system). + """ + return os.path.realpath(path) + + def lexists(self, path): + """Return True if path refers to an existing path, including + a broken or circular symbolic link. + """ + return os.path.lexists(path) + + def get_user_by_uid(self, uid): + """Return the username associated with user id. + If this can't be determined return raw uid instead. + On Windows just return "owner". + """ + if pwd is not None: + try: + return pwd.getpwuid(uid).pw_name + except KeyError: + return uid + else: + return "owner" + + def get_group_by_gid(self, gid): + """Return the groupname associated with group id. + If this can't be determined return raw gid instead. + On Windows just return "group". + """ + if grp is not None: + try: + return grp.getgrgid(gid).gr_name + except KeyError: + return gid + else: + return "group" + + if hasattr(os, 'readlink'): + def readlink(self, path): + """Return a string representing the path to which a + symbolic link points. + """ + return os.readlink(path) + + # --- Listing utilities + + def get_list_dir(self, path): + """"Return an iterator object that yields a directory listing + in a form suitable for LIST command. + """ + if self.isdir(path): + listing = self.listdir(path) + listing.sort() + return self.format_list(path, listing) + # if path is a file or a symlink we return information about it + else: + basedir, filename = os.path.split(path) + self.lstat(path) # raise exc in case of problems + return self.format_list(basedir, [filename]) + + def format_list(self, basedir, listing, ignore_err=True): + """Return an iterator object that yields the entries of given + directory emulating the "/bin/ls -lA" UNIX command output. + + - (str) basedir: the absolute dirname. + - (list) listing: the names of the entries in basedir + - (bool) ignore_err: when False raise exception if os.lstat() + call fails. + + On platforms which do not support the pwd and grp modules (such + as Windows), ownership is printed as "owner" and "group" as a + default, and number of hard links is always "1". On UNIX + systems, the actual owner, group, and number of links are + printed. + + This is how output appears to client: + + -rw-rw-rw- 1 owner group 7045120 Sep 02 3:47 music.mp3 + drwxrwxrwx 1 owner group 0 Aug 31 18:50 e-books + -rw-rw-rw- 1 owner group 380 Sep 02 3:40 module.py + """ + if self.cmd_channel.use_gmt_times: + timefunc = time.gmtime + else: + timefunc = time.localtime + now = time.time() + for basename in listing: + file = os.path.join(basedir, basename) + try: + st = self.lstat(file) + except OSError: + if ignore_err: + continue + raise + perms = _filemode(st.st_mode) # permissions + nlinks = st.st_nlink # number of links to inode + if not nlinks: # non-posix system, let's use a bogus value + nlinks = 1 + size = st.st_size # file size + uname = self.get_user_by_uid(st.st_uid) + gname = self.get_group_by_gid(st.st_gid) + mtime = timefunc(st.st_mtime) + # if modificaton time > 6 months shows "month year" + # else "month hh:mm"; this matches proftpd format, see: + # http://code.google.com/p/pyftpdlib/issues/detail?id=187 + if (now - st.st_mtime) > 180 * 24 * 60 * 60: + fmtstr = "%d %Y" + else: + fmtstr = "%d %H:%M" + try: + mtimestr = "%s %s" % (_months_map[mtime.tm_mon], + time.strftime(fmtstr, mtime)) + except ValueError: + # It could be raised if last mtime happens to be too + # old (prior to year 1900) in which case we return + # the current time as last mtime. + mtime = timefunc() + mtimestr = "%s %s" % (_months_map[mtime.tm_mon], + time.strftime("%d %H:%M", mtime)) + + # if the file is a symlink, resolve it, e.g. "symlink -> realfile" + if stat.S_ISLNK(st.st_mode) and hasattr(self, 'readlink'): + basename = basename + " -> " + self.readlink(file) + + # formatting is matched with proftpd ls output + yield "%s %3s %-8s %-8s %8s %s %s\r\n" % (perms, nlinks, uname, gname, + size, mtimestr, basename) + + def format_mlsx(self, basedir, listing, perms, facts, ignore_err=True): + """Return an iterator object that yields the entries of a given + directory or of a single file in a form suitable with MLSD and + MLST commands. + + Every entry includes a list of "facts" referring the listed + element. See RFC-3659, chapter 7, to see what every single + fact stands for. + + - (str) basedir: the absolute dirname. + - (list) listing: the names of the entries in basedir + - (str) perms: the string referencing the user permissions. + - (str) facts: the list of "facts" to be returned. + - (bool) ignore_err: when False raise exception if os.stat() + call fails. + + Note that "facts" returned may change depending on the platform + and on what user specified by using the OPTS command. + + This is how output could appear to the client issuing + a MLSD request: + + type=file;size=156;perm=r;modify=20071029155301;unique=801cd2; music.mp3 + type=dir;size=0;perm=el;modify=20071127230206;unique=801e33; ebooks + type=file;size=211;perm=r;modify=20071103093626;unique=801e32; module.py + """ + if self.cmd_channel.use_gmt_times: + timefunc = time.gmtime + else: + timefunc = time.localtime + permdir = ''.join([x for x in perms if x not in 'arw']) + permfile = ''.join([x for x in perms if x not in 'celmp']) + if ('w' in perms) or ('a' in perms) or ('f' in perms): + permdir += 'c' + if 'd' in perms: + permdir += 'p' + for basename in listing: + file = os.path.join(basedir, basename) + retfacts = dict() + # in order to properly implement 'unique' fact (RFC-3659, + # chapter 7.5.2) we are supposed to follow symlinks, hence + # use os.stat() instead of os.lstat() + try: + st = self.stat(file) + except OSError: + if ignore_err: + continue + raise + # type + perm + if stat.S_ISDIR(st.st_mode): + if 'type' in facts: + if basename == '.': + retfacts['type'] = 'cdir' + elif basename == '..': + retfacts['type'] = 'pdir' + else: + retfacts['type'] = 'dir' + if 'perm' in facts: + retfacts['perm'] = permdir + else: + if 'type' in facts: + retfacts['type'] = 'file' + if 'perm' in facts: + retfacts['perm'] = permfile + if 'size' in facts: + retfacts['size'] = st.st_size # file size + # last modification time + if 'modify' in facts: + try: + retfacts['modify'] = time.strftime("%Y%m%d%H%M%S", + timefunc(st.st_mtime)) + # it could be raised if last mtime happens to be too old + # (prior to year 1900) + except ValueError: + pass + if 'create' in facts: + # on Windows we can provide also the creation time + try: + retfacts['create'] = time.strftime("%Y%m%d%H%M%S", + timefunc(st.st_ctime)) + except ValueError: + pass + # UNIX only + if 'unix.mode' in facts: + retfacts['unix.mode'] = oct(st.st_mode & 0777) + if 'unix.uid' in facts: + retfacts['unix.uid'] = st.st_uid + if 'unix.gid' in facts: + retfacts['unix.gid'] = st.st_gid + + # We provide unique fact (see RFC-3659, chapter 7.5.2) on + # posix platforms only; we get it by mixing st_dev and + # st_ino values which should be enough for granting an + # uniqueness for the file listed. + # The same approach is used by pure-ftpd. + # Implementors who want to provide unique fact on other + # platforms should use some platform-specific method (e.g. + # on Windows NTFS filesystems MTF records could be used). + if 'unique' in facts: + retfacts['unique'] = "%xg%x" % (st.st_dev, st.st_ino) + + # facts can be in any order but we sort them by name + factstring = "".join(["%s=%s;" % (x, retfacts[x]) \ + for x in sorted(retfacts.keys())]) + yield "%s %s\r\n" % (factstring, basename) + + +# --- FTP + +class FTPHandler(object, asynchat.async_chat): + """Implements the FTP server Protocol Interpreter (see RFC-959), + handling commands received from the client on the control channel. + + All relevant session information is stored in class attributes + reproduced below and can be modified before instantiating this + class. + + - (int) timeout: + The timeout which is the maximum time a remote client may spend + between FTP commands. If the timeout triggers, the remote client + will be kicked off. Defaults to 300 seconds. + + - (str) banner: the string sent when client connects. + + - (int) max_login_attempts: + the maximum number of wrong authentications before disconnecting + the client (default 3). + + - (bool)permit_foreign_addresses: + FTP site-to-site transfer feature: also referenced as "FXP" it + permits for transferring a file between two remote FTP servers + without the transfer going through the client's host (not + recommended for security reasons as described in RFC-2577). + Having this attribute set to False means that all data + connections from/to remote IP addresses which do not match the + client's IP address will be dropped (defualt False). + + - (bool) permit_privileged_ports: + set to True if you want to permit active data connections (PORT) + over privileged ports (not recommended, defaulting to False). + + - (str) masquerade_address: + the "masqueraded" IP address to provide along PASV reply when + pyftpdlib is running behind a NAT or other types of gateways. + When configured pyftpdlib will hide its local address and + instead use the public address of your NAT (default None). + + - (dict) masquerade_address_map: + in case the server has multiple IP addresses which are all + behind a NAT router, you may wish to specify individual + masquerade_addresses for each of them. The map expects a + dictionary containing private IP addresses as keys, and their + corresponding public (masquerade) addresses as values. + + - (list) passive_ports: + what ports the ftpd will use for its passive data transfers. + Value expected is a list of integers (e.g. range(60000, 65535)). + When configured pyftpdlib will no longer use kernel-assigned + random ports (default None). + + - (bool) use_gmt_times: + when True causes the server to report all ls and MDTM times in + GMT and not local time (default True). + + - (bool) use_sendfile: when True uses sendfile() system call to + send a file resulting in faster uploads (from server to client). + Works on UNIX only and requires pysendfile module to be + installed separately: + http://code.google.com/p/pysendfile/ + Automatically defaults to True if pysendfile module is + installed. + + - (bool) tcp_no_delay: controls the use of the TCP_NODELAY socket + option which disables the Nagle algorithm resulting in + significantly better performances (default True on all systems + where it is supported). + + All relevant instance attributes initialized when client connects + are reproduced below. You may be interested in them in case you + want to subclass the original FTPHandler. + + - (bool) authenticated: True if client authenticated himself. + - (str) username: the name of the connected user (if any). + - (int) attempted_logins: number of currently attempted logins. + - (str) current_type: the current transfer type (default "a") + - (int) af: the connection's address family (IPv4/IPv6) + - (instance) server: the FTPServer class instance. + - (instance) data_channel: the data channel instance (if any). + """ + # these are overridable defaults + + # default classes + authorizer = DummyAuthorizer() + active_dtp = ActiveDTP + passive_dtp = PassiveDTP + dtp_handler = DTPHandler + abstracted_fs = AbstractedFS + proto_cmds = proto_cmds + + # session attributes (explained in the docstring) + timeout = 300 + banner = "pyftpdlib %s ready." % __ver__ + max_login_attempts = 3 + permit_foreign_addresses = False + permit_privileged_ports = False + masquerade_address = None + masquerade_address_map = {} + passive_ports = None + use_gmt_times = True + use_sendfile = sendfile is not None + tcp_no_delay = hasattr(socket, "TCP_NODELAY") + + def __init__(self, conn, server): + """Initialize the command channel. + + - (instance) conn: the socket object instance of the newly + established connection. + - (instance) server: the ftp server class instance. + """ + # public session attributes + self.server = server + self.fs = None + self.authenticated = False + self.username = "" + self.password = "" + self.attempted_logins = 0 + self.sleeping = False + self.data_channel = None + self.remote_ip = "" + self.remote_port = "" + + # private session attributes + self._last_response = "" + self._current_type = 'a' + self._restart_position = 0 + self._quit_pending = False + self._af = -1 + self._in_buffer = [] + self._in_buffer_len = 0 + self._epsvall = False + self._dtp_acceptor = None + self._dtp_connector = None + self._in_dtp_queue = None + self._out_dtp_queue = None + self._closed = False + self._extra_feats = [] + self._current_facts = ['type', 'perm', 'size', 'modify'] + self._rnfr = None + if self.timeout: + self._idler = CallLater(self.timeout, self.handle_timeout, + _errback=self.handle_error) + else: + self._idler = None + if os.name == 'posix': + self._current_facts.append('unique') + self._available_facts = self._current_facts[:] + if pwd and grp: + self._available_facts += ['unix.mode', 'unix.uid', 'unix.gid'] + if os.name == 'nt': + self._available_facts.append('create') + + try: + asynchat.async_chat.__init__(self, conn) + except socket.error, err: + # if we get an exception here we want the dispatcher + # instance to set socket attribute before closing, see: + # http://code.google.com/p/pyftpdlib/issues/detail?id=188 + asynchat.async_chat.__init__(self, socket.socket()) + self.close() + if err.args[0] == errno.EINVAL: + # http://code.google.com/p/pyftpdlib/issues/detail?id=143 + return + self.handle_error() + return + self.set_terminator("\r\n") + + # connection properties + try: + self.remote_ip, self.remote_port = self.socket.getpeername()[:2] + except socket.error, err: + # A race condition may occur if the other end is closing + # before we can get the peername, hence ENOTCONN (see issue + # #100) while EINVAL can occur on OSX (see issue #143). + self.connected = False + if err.args[0] in (errno.ENOTCONN, errno.EINVAL): + self.close() + else: + self.handle_error() + return + + if hasattr(self.socket, 'family'): + self._af = self.socket.family + else: # python < 2.5 + ip, port = self.socket.getsockname()[:2] + self._af = socket.getaddrinfo(ip, port, socket.AF_UNSPEC, + socket.SOCK_STREAM)[0][0] + + # try to handle urgent data inline + try: + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_OOBINLINE, 1) + except socket.error: + pass + + # disable Nagle algorithm for the control socket only, resulting + # in significantly better performances + if self.tcp_no_delay: + try: + self.socket.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) + except socket.error: + pass + + # remove this instance from asyncore socket_map + if not self.connected: + self.close() + + def handle(self): + """Return a 220 'ready' response to the client over the command + channel. + """ + if len(self.banner) <= 75: + self.respond("220 %s" % str(self.banner)) + else: + self.push('220-%s\r\n' % str(self.banner)) + self.respond('220 ') + + def handle_max_cons(self): + """Called when limit for maximum number of connections is reached.""" + msg = "Too many connections. Service temporarily unavailable." + self.respond("421 %s" % msg) + self.log(msg) + # If self.push is used, data could not be sent immediately in + # which case a new "loop" will occur exposing us to the risk of + # accepting new connections. Since this could cause asyncore to + # run out of fds (...and exposes the server to DoS attacks), we + # immediately close the channel by using close() instead of + # close_when_done(). If data has not been sent yet client will + # be silently disconnected. + self.close() + + def handle_max_cons_per_ip(self): + """Called when too many clients are connected from the same IP.""" + msg = "Too many connections from the same IP address." + self.respond("421 %s" % msg) + self.log(msg) + self.close_when_done() + + def handle_timeout(self): + """Called when client does not send any command within the time + specified in <timeout> attribute.""" + msg = "Control connection timed out." + self.log(msg) + self.respond("421 " + msg) + self.close_when_done() + + # --- asyncore / asynchat overridden methods + + def readable(self): + # Checking for self.connected seems to be necessary as per: + # http://code.google.com/p/pyftpdlib/issues/detail?id=188#c18 + # In contrast to DTPHandler, here we are not interested in + # attempting to receive any further data from a closed socket. + return not self.sleeping and self.connected \ + and asynchat.async_chat.readable(self) + + def writable(self): + return not self.sleeping and self.connected \ + and asynchat.async_chat.writable(self) + + def collect_incoming_data(self, data): + """Read incoming data and append to the input buffer.""" + self._in_buffer.append(data) + self._in_buffer_len += len(data) + # Flush buffer if it gets too long (possible DoS attacks). + # RFC-959 specifies that a 500 response could be given in + # such cases + buflimit = 2048 + if self._in_buffer_len > buflimit: + self.respond('500 Command too long.') + self.log('Command received exceeded buffer limit of %s.' % buflimit) + self._in_buffer = [] + self._in_buffer_len = 0 + + def found_terminator(self): + r"""Called when the incoming data stream matches the \r\n + terminator. + """ + if self._idler is not None and not self._idler.cancelled: + self._idler.reset() + + line = ''.join(self._in_buffer) + self._in_buffer = [] + self._in_buffer_len = 0 + + cmd = line.split(' ')[0].upper() + arg = line[len(cmd)+1:] + kwargs = {} + if cmd == "SITE" and arg: + cmd = "SITE %s" % arg.split(' ')[0].upper() + arg = line[len(cmd)+1:] + + if cmd != 'PASS': + self.logline("<== %s" % line) + else: + self.logline("<== %s %s" % (line.split(' ')[0], '*' * 6)) + + # Recognize those commands having a "special semantic". They + # should be sent by following the RFC-959 procedure of sending + # Telnet IP/Synch sequence (chr 242 and 255) as OOB data but + # since many ftp clients don't do it correctly we check the + # last 4 characters only. + if not cmd in self.proto_cmds: + if cmd[-4:] in ('ABOR', 'STAT', 'QUIT'): + cmd = cmd[-4:] + else: + msg = 'Command "%s" not understood.' % cmd + self.respond('500 ' + msg) + if cmd: + self.log_cmd(cmd, arg, 500, msg) + return + + if not arg and self.proto_cmds[cmd]['arg'] == True: + msg = "Syntax error: command needs an argument." + self.respond("501 " + msg) + self.log_cmd(cmd, "", 501, msg) + return + if arg and self.proto_cmds[cmd]['arg'] == False: + msg = "Syntax error: command does not accept arguments." + self.respond("501 " + msg) + self.log_cmd(cmd, arg, 501, msg) + return + + if not self.authenticated: + if self.proto_cmds[cmd]['auth'] or (cmd == 'STAT' and arg): + msg = "Log in with USER and PASS first." + self.respond("530 " + msg) + self.log_cmd(cmd, arg, 530, msg) + else: + # call the proper ftp_* method + self.process_command(cmd, arg) + return + else: + if (cmd == 'STAT') and not arg: + self.ftp_STAT('') + return + + # for file-system related commands check whether real path + # destination is valid + if self.proto_cmds[cmd]['perm'] and (cmd != 'STOU'): + if cmd in ('CWD', 'XCWD'): + arg = self.fs.ftp2fs(arg or '/') + elif cmd in ('CDUP', 'XCUP'): + arg = self.fs.ftp2fs('..') + elif cmd == 'LIST': + if arg.lower() in ('-a', '-l', '-al', '-la'): + arg = self.fs.ftp2fs(self.fs.cwd) + else: + arg = self.fs.ftp2fs(arg or self.fs.cwd) + elif cmd == 'STAT': + if glob.has_magic(arg): + msg = 'Globbing not supported.' + self.respond('550 ' + msg) + self.log_cmd(cmd, arg, 550, msg) + return + arg = self.fs.ftp2fs(arg or self.fs.cwd) + elif cmd == 'SITE CHMOD': + if not ' ' in arg: + msg = "Syntax error: command needs two arguments." + self.respond("501 " + msg) + self.log_cmd(cmd, "", 501, msg) + return + else: + mode, arg = arg.split(' ', 1) + arg = self.fs.ftp2fs(arg) + kwargs = dict(mode=mode) + else: # LIST, NLST, MLSD, MLST + arg = self.fs.ftp2fs(arg or self.fs.cwd) + + if not self.fs.validpath(arg): + line = self.fs.fs2ftp(arg) + msg = '"%s" points to a path which is outside ' \ + "the user's root directory" % line + self.respond("550 %s." % msg) + self.log_cmd(cmd, arg, 550, msg) + return + + # check permission + perm = self.proto_cmds[cmd]['perm'] + if perm is not None and cmd != 'STOU': + if not self.authorizer.has_perm(self.username, perm, arg): + msg = "Not enough privileges." + self.respond("550 " + msg) + self.log_cmd(cmd, arg, 550, msg) + return + + # call the proper ftp_* method + self.process_command(cmd, arg, **kwargs) + + def process_command(self, cmd, *args, **kwargs): + """Process command by calling the corresponding ftp_* class + method (e.g. for received command "MKD pathname", ftp_MKD() + method is called with "pathname" as the argument). + """ + self._last_response = "" + method = getattr(self, 'ftp_' + cmd.replace(' ', '_')) + method(*args, **kwargs) + if self._last_response: + code = int(self._last_response[:3]) + resp = self._last_response[4:] + self.log_cmd(cmd, args[0], code, resp) + + def handle_expt(self): + """Called when there is out of band (OOB) data to be read. + This might happen in case of such clients strictly following + the RFC-959 directives of sending Telnet IP and Synch as OOB + data before issuing ABOR, STAT and QUIT commands. + It should never be called since the SO_OOBINLINE option is + enabled except on some systems like FreeBSD where it doesn't + seem to have effect. + """ + if hasattr(socket, 'MSG_OOB'): + try: + data = self.socket.recv(1024, socket.MSG_OOB) + except socket.error, err: + if err.args[0] == errno.EINVAL: + return + else: + self._in_buffer.append(data) + return + self.log("Can't handle OOB data.") + self.close() + + def handle_error(self): + try: + raise + except (KeyboardInterrupt, SystemExit, asyncore.ExitNow): + raise + except socket.error, err: + # fixes around various bugs: + # - http://bugs.python.org/issue1736101 + # - http://code.google.com/p/pyftpdlib/issues/detail?id=104 + # - http://code.google.com/p/pyftpdlib/issues/detail?id=109 + if err.args[0] in _DISCONNECTED: + self.handle_close() + return + else: + self.log_exception(self) + except: + self.log_exception(self) + self.close() + + def handle_close(self): + self.close() + + def close(self): + """Close the current channel disconnecting the client.""" + if not self._closed: + self._closed = True + asynchat.async_chat.close(self) + self.connected = False + + self._shutdown_connecting_dtp() + + if self.data_channel is not None: + self.data_channel.close() + del self.data_channel + + del self._out_dtp_queue + del self._in_dtp_queue + + if self._idler is not None and not self._idler.cancelled: + self._idler.cancel() + + # remove client IP address from ip map + if self.remote_ip in self.server.ip_map: + self.server.ip_map.remove(self.remote_ip) + + if self.fs is not None: + self.fs.cmd_channel = None + self.fs = None + self.log("Disconnected.") + + def _shutdown_connecting_dtp(self): + """Close any ActiveDTP or PassiveDTP instance waiting to + establish a connection (passive or active). + """ + if self._dtp_acceptor is not None: + self._dtp_acceptor.close() + self._dtp_acceptor = None + if self._dtp_connector is not None: + self._dtp_connector.close() + self._dtp_connector = None + + # --- public callbacks + # Note: to run a time consuming task make sure to use a separate + # process or thread (see FAQs). + + def on_file_sent(self, file): + """Called every time a file has been succesfully sent. + "file" is the absolute name of the file just being sent. + """ + + def on_file_received(self, file): + """Called every time a file has been succesfully received. + "file" is the absolute name of the file just being received. + """ + + def on_incomplete_file_sent(self, file): + """Called every time a file has not been entirely sent. + (e.g. ABOR during transfer or client disconnected). + "file" is the absolute name of that file. + """ + + def on_incomplete_file_received(self, file): + """Called every time a file has not been entirely received + (e.g. ABOR during transfer or client disconnected). + "file" is the absolute name of that file. + """ + + def on_login(self, username): + """Called on user login.""" + + def on_login_failed(self, username, password): + """Called on failed user login. + At this point client might have already been disconnected if it + failed too many times. + """ + + def on_logout(self, username): + """Called when user logs out due to QUIT or USER issued twice.""" + + + # --- internal callbacks + + def _on_dtp_connection(self): + """Called every time data channel connects, either active or + passive. + + Incoming and outgoing queues are checked for pending data. + If outbound data is pending, it is pushed into the data channel. + If awaiting inbound data, the data channel is enabled for + receiving. + """ + # Close accepting DTP only. By closing ActiveDTP DTPHandler + # would receive a closed socket object. + #self._shutdown_connecting_dtp() + if self._dtp_acceptor is not None: + self._dtp_acceptor.close() + self._dtp_acceptor = None + + # stop the idle timer as long as the data transfer is not finished + if self._idler is not None and not self._idler.cancelled: + self._idler.cancel() + + # check for data to send + if self._out_dtp_queue is not None: + data, isproducer, file, cmd = self._out_dtp_queue + self._out_dtp_queue = None + self.data_channel.cmd = cmd + if file: + self.data_channel.file_obj = file + try: + if not isproducer: + self.data_channel.push(data) + else: + self.data_channel.push_with_producer(data) + if self.data_channel is not None: + self.data_channel.close_when_done() + except: + # dealing with this exception is up to DTP (see bug #84) + self.data_channel.handle_error() + + # check for data to receive + elif self._in_dtp_queue is not None: + file, cmd = self._in_dtp_queue + self.data_channel.file_obj = file + self._in_dtp_queue = None + self.data_channel.enable_receiving(self._current_type, cmd) + + def _on_dtp_close(self): + """Called every time the data channel is closed.""" + self.data_channel = None + if self._quit_pending: + self.close() + elif self.timeout: + # data transfer finished, restart the idle timer + self._idler = CallLater(self.timeout, self.handle_timeout, + _errback=self.handle_error) + + # --- utility + + def respond(self, resp): + """Send a response to the client using the command channel.""" + self._last_response = resp + self.push(resp + '\r\n') + self.logline('==> %s' % resp) + + def push_dtp_data(self, data, isproducer=False, file=None, cmd=None): + """Pushes data into the data channel. + + It is usually called for those commands requiring some data to + be sent over the data channel (e.g. RETR). + If data channel does not exist yet, it queues the data to send + later; data will then be pushed into data channel when + _on_dtp_connection() will be called. + + - (str/classobj) data: the data to send which may be a string + or a producer object). + - (bool) isproducer: whether treat data as a producer. + - (file) file: the file[-like] object to send (if any). + """ + if self.data_channel is not None: + self.respond("125 Data connection already open. Transfer starting.") + if file: + self.data_channel.file_obj = file + try: + if not isproducer: + self.data_channel.push(data) + else: + self.data_channel.push_with_producer(data) + if self.data_channel is not None: + self.data_channel.cmd = cmd + self.data_channel.close_when_done() + except: + # dealing with this exception is up to DTP (see bug #84) + self.data_channel.handle_error() + else: + self.respond("150 File status okay. About to open data connection.") + self._out_dtp_queue = (data, isproducer, file, cmd) + + def flush_account(self): + """Flush account information by clearing attributes that need + to be reset on a REIN or new USER command. + """ + self._shutdown_connecting_dtp() + # if there's a transfer in progress RFC-959 states we are + # supposed to let it finish + if self.data_channel is not None: + if not self.data_channel.transfer_in_progress(): + self.data_channel.close() + self.data_channel = None + + username = self.username + self.authenticated = False + self.username = "" + self.password = "" + self.attempted_logins = 0 + self._current_type = 'a' + self._restart_position = 0 + self._quit_pending = False + self.sleeping = False + self._in_dtp_queue = None + self._rnfr = None + self._out_dtp_queue = None + if username: + self.on_logout(username) + + def run_as_current_user(self, function, *args, **kwargs): + """Execute a function impersonating the current logged-in user.""" + self.authorizer.impersonate_user(self.username, self.password) + try: + return function(*args, **kwargs) + finally: + self.authorizer.terminate_impersonation(self.username) + + # --- logging wrappers + + def log(self, msg): + """Log a message, including additional identifying session data.""" + log("[%s]@%s:%s %s" % (self.username, self.remote_ip, + self.remote_port, msg)) + + def logline(self, msg): + """Log a line including additional indentifying session data.""" + logline("%s:%s %s" % (self.remote_ip, self.remote_port, msg)) + + def logerror(self, msg): + """Log an error including additional indentifying session data.""" + logerror("[%s]@%s:%s %s" % (self.username, self.remote_ip, + self.remote_port, msg)) + + def log_exception(self, instance): + """Log an unhandled exception. 'instance' is the instance + where the exception was generated. + """ + self.logerror("unhandled exception in instance %r\n%s" \ + % (instance, traceback.format_exc())) + + def log_cmd(self, cmd, arg, respcode, respstr): + """Log commands and responses in a standardized format. + + - (str) cmd: + the command sent by client + + - (str) arg: + the command argument sent by client. + For filesystem commands such as DELE, MKD, etc. this is + already represented as an absolute real filesystem path + like "/home/user/file.ext". + + - (int) respcode: + the response code as being sent by server. Response codes + starting with 4xx or 5xx are returned if the command has + been rejected for some reason. + + - (str) respstr: + the response string as being sent by server. + + By default only DELE, RMD, RNFR, RNTO, MKD commands are logged + and the output is redirected to self.log method (the main logger). + + Can be overridden to provide alternate formats or to log + further commands. + """ + if cmd in ("DELE", "RMD", "RNFR", "RNTO", "MKD"): + line = '"%s" %s' % (' '.join([cmd, str(arg)]).strip(), respcode) + self.log(line) + + def log_transfer(self, cmd, filename, receive, completed, elapsed, bytes): + """Log all file transfers in a standardized format. + + - (str) cmd: + the original command who caused the tranfer. + + - (str) filename: + the absolutized name of the file on disk. + + - (bool) receive: + True if the transfer was used for client uploading (STOR, + STOU, APPE), False otherwise (RETR). + + - (bool) completed: + True if the file has been entirely sent, else False. + + - (float) elapsed: + transfer elapsed time in seconds. + + - (int) bytes: + number of bytes transmitted. + """ + line = '"%s %s" completed=%s bytes=%s seconds=%s' % \ + (cmd, filename, completed and 1 or 0, bytes, elapsed) + self.log(line) + + + # --- connection + + def _make_eport(self, ip, port): + """Establish an active data channel with remote client which + issued a PORT or EPRT command. + """ + # FTP bounce attacks protection: according to RFC-2577 it's + # recommended to reject PORT if IP address specified in it + # does not match client IP address. + remote_ip = self.remote_ip + if remote_ip.startswith('::ffff:'): + # In this scenario, the server has an IPv6 socket, but + # the remote client is using IPv4 and its address is + # represented as an IPv4-mapped IPv6 address which + # looks like this ::ffff:151.12.5.65, see: + # http://en.wikipedia.org/wiki/IPv6#IPv4-mapped_addresses + # http://tools.ietf.org/html/rfc3493.html#section-3.7 + # We truncate the first bytes to make it look like a + # common IPv4 address. + remote_ip = remote_ip[7:] + if not self.permit_foreign_addresses and ip != remote_ip: + self.log("Rejected data connection to foreign address %s:%s." + % (ip, port)) + self.respond("501 Can't connect to a foreign address.") + return + + # ...another RFC-2577 recommendation is rejecting connections + # to privileged ports (< 1024) for security reasons. + if not self.permit_privileged_ports and port < 1024: + self.log('PORT against the privileged port "%s" refused.' % port) + self.respond("501 Can't connect over a privileged port.") + return + + # close establishing DTP instances, if any + self._shutdown_connecting_dtp() + + if self.data_channel is not None: + self.data_channel.close() + self.data_channel = None + + # make sure we are not hitting the max connections limit + if self.server.max_cons: + if len(asyncore.socket_map) >= self.server.max_cons: + msg = "Too many connections. Can't open data channel." + self.respond("425 %s" %msg) + self.log(msg) + return + + # open data channel + self._dtp_connector = self.active_dtp(ip, port, self) + + def _make_epasv(self, extmode=False): + """Initialize a passive data channel with remote client which + issued a PASV or EPSV command. + If extmode argument is True we assume that client issued EPSV in + which case extended passive mode will be used (see RFC-2428). + """ + # close establishing DTP instances, if any + self._shutdown_connecting_dtp() + + # close established data connections, if any + if self.data_channel is not None: + self.data_channel.close() + self.data_channel = None + + # make sure we are not hitting the max connections limit + if self.server.max_cons: + if len(asyncore.socket_map) >= self.server.max_cons: + msg = "Too many connections. Can't open data channel." + self.respond("425 %s" %msg) + self.log(msg) + return + + # open data channel + self._dtp_acceptor = self.passive_dtp(self, extmode) + + def ftp_PORT(self, line): + """Start an active data channel by using IPv4.""" + if self._epsvall: + self.respond("501 PORT not allowed after EPSV ALL.") + return + # Parse PORT request for getting IP and PORT. + # Request comes in as: + # > h1,h2,h3,h4,p1,p2 + # ...where the client's IP address is h1.h2.h3.h4 and the TCP + # port number is (p1 * 256) + p2. + try: + addr = map(int, line.split(',')) + if len(addr) != 6: + raise ValueError + for x in addr[:4]: + if not 0 <= x <= 255: + raise ValueError + ip = '%d.%d.%d.%d' % tuple(addr[:4]) + port = (addr[4] * 256) + addr[5] + if not 0 <= port <= 65535: + raise ValueError + except (ValueError, OverflowError): + self.respond("501 Invalid PORT format.") + return + self._make_eport(ip, port) + + def ftp_EPRT(self, line): + """Start an active data channel by choosing the network protocol + to use (IPv4/IPv6) as defined in RFC-2428. + """ + if self._epsvall: + self.respond("501 EPRT not allowed after EPSV ALL.") + return + # Parse EPRT request for getting protocol, IP and PORT. + # Request comes in as: + # <d>proto<d>ip<d>port<d> + # ...where <d> is an arbitrary delimiter character (usually "|") and + # <proto> is the network protocol to use (1 for IPv4, 2 for IPv6). + try: + af, ip, port = line.split(line[0])[1:-1] + port = int(port) + if not 0 <= port <= 65535: + raise ValueError + except (ValueError, IndexError, OverflowError): + self.respond("501 Invalid EPRT format.") + return + + if af == "1": + if self._af != socket.AF_INET: + self.respond('522 Network protocol not supported (use 2).') + else: + try: + octs = map(int, ip.split('.')) + if len(octs) != 4: + raise ValueError + for x in octs: + if not 0 <= x <= 255: + raise ValueError + except (ValueError, OverflowError): + self.respond("501 Invalid EPRT format.") + else: + self._make_eport(ip, port) + elif af == "2": + if self._af == socket.AF_INET: + self.respond('522 Network protocol not supported (use 1).') + else: + self._make_eport(ip, port) + else: + if self._af == socket.AF_INET: + self.respond('501 Unknown network protocol (use 1).') + else: + self.respond('501 Unknown network protocol (use 2).') + + def ftp_PASV(self, line): + """Start a passive data channel by using IPv4.""" + if self._epsvall: + self.respond("501 PASV not allowed after EPSV ALL.") + return + self._make_epasv(extmode=False) + + def ftp_EPSV(self, line): + """Start a passive data channel by using IPv4 or IPv6 as defined + in RFC-2428. + """ + # RFC-2428 specifies that if an optional parameter is given, + # we have to determine the address family from that otherwise + # use the same address family used on the control connection. + # In such a scenario a client may use IPv4 on the control channel + # and choose to use IPv6 for the data channel. + # But how could we use IPv6 on the data channel without knowing + # which IPv6 address to use for binding the socket? + # Unfortunately RFC-2428 does not provide satisfing information + # on how to do that. The assumption is that we don't have any way + # to know wich address to use, hence we just use the same address + # family used on the control connection. + if not line: + self._make_epasv(extmode=True) + # IPv4 + elif line == "1": + if self._af != socket.AF_INET: + self.respond('522 Network protocol not supported (use 2).') + else: + self._make_epasv(extmode=True) + # IPv6 + elif line == "2": + if self._af == socket.AF_INET: + self.respond('522 Network protocol not supported (use 1).') + else: + self._make_epasv(extmode=True) + elif line.lower() == 'all': + self._epsvall = True + self.respond('220 Other commands other than EPSV are now disabled.') + else: + if self._af == socket.AF_INET: + self.respond('501 Unknown network protocol (use 1).') + else: + self.respond('501 Unknown network protocol (use 2).') + + def ftp_QUIT(self, line): + """Quit the current session disconnecting the client.""" + if self.authenticated: + msg_quit = self.authorizer.get_msg_quit(self.username) + else: + msg_quit = "Goodbye." + if len(msg_quit) <= 75: + self.respond("221 %s" % msg_quit) + else: + self.push("221-%s\r\n" % msg_quit) + self.respond("221 ") + + # From RFC-959: + # If file transfer is in progress, the connection must remain + # open for result response and the server will then close it. + # We also stop responding to any further command. + if self.data_channel: + self._quit_pending = True + self.sleeping = True + else: + self._shutdown_connecting_dtp() + self.close_when_done() + if self.username: + self.on_logout(self.username) + + # --- data transferring + + def ftp_LIST(self, path): + """Return a list of files in the specified directory to the + client. + """ + # - If no argument, fall back on cwd as default. + # - Some older FTP clients erroneously issue /bin/ls-like LIST + # formats in which case we fall back on cwd as default. + try: + iterator = self.run_as_current_user(self.fs.get_list_dir, path) + except OSError, err: + why = _strerror(err) + self.respond('550 %s.' % why) + else: + producer = BufferedIteratorProducer(iterator) + self.push_dtp_data(producer, isproducer=True, cmd="LIST") + + def ftp_NLST(self, path): + """Return a list of files in the specified directory in a + compact form to the client. + """ + try: + if self.fs.isdir(path): + listing = self.run_as_current_user(self.fs.listdir, path) + else: + # if path is a file we just list its name + self.fs.lstat(path) # raise exc in case of problems + listing = [os.path.basename(path)] + except OSError, err: + self.respond('550 %s.' % _strerror(err)) + else: + data = '' + if listing: + listing.sort() + data = '\r\n'.join(listing) + '\r\n' + self.push_dtp_data(data, cmd="NLST") + + # --- MLST and MLSD commands + + # The MLST and MLSD commands are intended to standardize the file and + # directory information returned by the server-FTP process. These + # commands differ from the LIST command in that the format of the + # replies is strictly defined although extensible. + + def ftp_MLST(self, path): + """Return information about a pathname in a machine-processable + form as defined in RFC-3659. + """ + line = self.fs.fs2ftp(path) + basedir, basename = os.path.split(path) + perms = self.authorizer.get_perms(self.username) + try: + iterator = self.run_as_current_user(self.fs.format_mlsx, basedir, + [basename], perms, self._current_facts, ignore_err=False) + data = ''.join(iterator) + except OSError, err: + self.respond('550 %s.' % _strerror(err)) + else: + # since TVFS is supported (see RFC-3659 chapter 6), a fully + # qualified pathname should be returned + data = data.split(' ')[0] + ' %s\r\n' % line + # response is expected on the command channel + self.push('250-Listing "%s":\r\n' % line) + # the fact set must be preceded by a space + self.push(' ' + data) + self.respond('250 End MLST.') + + def ftp_MLSD(self, path): + """Return contents of a directory in a machine-processable form + as defined in RFC-3659. + """ + # RFC-3659 requires 501 response code if path is not a directory + if not self.fs.isdir(path): + self.respond("501 No such directory.") + return + try: + listing = self.run_as_current_user(self.fs.listdir, path) + except OSError, err: + why = _strerror(err) + self.respond('550 %s.' % why) + else: + perms = self.authorizer.get_perms(self.username) + iterator = self.fs.format_mlsx(path, listing, perms, + self._current_facts) + producer = BufferedIteratorProducer(iterator) + self.push_dtp_data(producer, isproducer=True, cmd="MLSD") + + def ftp_RETR(self, file): + """Retrieve the specified file (transfer from the server to the + client) + """ + rest_pos = self._restart_position + self._restart_position = 0 + try: + fd = self.run_as_current_user(self.fs.open, file, 'rb') + except IOError, err: + why = _strerror(err) + self.respond('550 %s.' % why) + return + + if rest_pos: + # Make sure that the requested offset is valid (within the + # size of the file being resumed). + # According to RFC-1123 a 554 reply may result in case that + # the existing file cannot be repositioned as specified in + # the REST. + ok = 0 + try: + if rest_pos > self.fs.getsize(file): + raise ValueError + fd.seek(rest_pos) + ok = 1 + except ValueError: + why = "Invalid REST parameter" + except IOError, err: + why = _strerror(err) + if not ok: + self.respond('554 %s' % why) + return + producer = FileProducer(fd, self._current_type) + self.push_dtp_data(producer, isproducer=True, file=fd, cmd="RETR") + + def ftp_STOR(self, file, mode='w'): + """Store a file (transfer from the client to the server).""" + # A resume could occur in case of APPE or REST commands. + # In that case we have to open file object in different ways: + # STOR: mode = 'w' + # APPE: mode = 'a' + # REST: mode = 'r+' (to permit seeking on file object) + if 'a' in mode: + cmd = 'APPE' + else: + cmd = 'STOR' + rest_pos = self._restart_position + self._restart_position = 0 + if rest_pos: + mode = 'r+' + try: + fd = self.run_as_current_user(self.fs.open, file, mode + 'b') + except IOError, err: + why = _strerror(err) + self.respond('550 %s.' %why) + return + + if rest_pos: + # Make sure that the requested offset is valid (within the + # size of the file being resumed). + # According to RFC-1123 a 554 reply may result in case + # that the existing file cannot be repositioned as + # specified in the REST. + ok = 0 + try: + if rest_pos > self.fs.getsize(file): + raise ValueError + fd.seek(rest_pos) + ok = 1 + except ValueError: + why = "Invalid REST parameter" + except IOError, err: + why = _strerror(err) + if not ok: + self.respond('554 %s' %why) + return + + if self.data_channel is not None: + resp = "Data connection already open. Transfer starting." + self.respond("125 " + resp) + self.data_channel.file_obj = fd + self.data_channel.enable_receiving(self._current_type, cmd) + else: + resp = "File status okay. About to open data connection." + self.respond("150 " + resp) + self._in_dtp_queue = (fd, cmd) + + + def ftp_STOU(self, line): + """Store a file on the server with a unique name.""" + # Note 1: RFC-959 prohibited STOU parameters, but this + # prohibition is obsolete. + # Note 2: 250 response wanted by RFC-959 has been declared + # incorrect in RFC-1123 that wants 125/150 instead. + # Note 3: RFC-1123 also provided an exact output format + # defined to be as follow: + # > 125 FILE: pppp + # ...where pppp represents the unique path name of the + # file that will be written. + + # watch for STOU preceded by REST, which makes no sense. + if self._restart_position: + self.respond("450 Can't STOU while REST request is pending.") + return + + if line: + basedir, prefix = os.path.split(self.fs.ftp2fs(line)) + prefix = prefix + '.' + else: + basedir = self.fs.ftp2fs(self.fs.cwd) + prefix = 'ftpd.' + try: + fd = self.run_as_current_user(self.fs.mkstemp, prefix=prefix, + dir=basedir) + except IOError, err: + # hitted the max number of tries to find out file with + # unique name + if err.errno == errno.EEXIST: + why = 'No usable unique file name found' + # something else happened + else: + why = _strerror(err) + self.respond("450 %s." % why) + return + + if not self.authorizer.has_perm(self.username, 'w', fd.name): + try: + fd.close() + self.run_as_current_user(self.fs.remove, fd.name) + except OSError: + pass + self.respond("550 Not enough privileges.") + return + + # now just acts like STOR except that restarting isn't allowed + filename = os.path.basename(fd.name) + if self.data_channel is not None: + self.respond("125 FILE: %s" % filename) + self.data_channel.file_obj = fd + self.data_channel.enable_receiving(self._current_type, "STOU") + else: + self.respond("150 FILE: %s" % filename) + self._in_dtp_queue = (fd, "STOU") + + def ftp_APPE(self, file): + """Append data to an existing file on the server.""" + # watch for APPE preceded by REST, which makes no sense. + if self._restart_position: + self.respond("450 Can't APPE while REST request is pending.") + else: + self.ftp_STOR(file, mode='a') + + def ftp_REST(self, line): + """Restart a file transfer from a previous mark.""" + if self._current_type == 'a': + self.respond('501 Resuming transfers not allowed in ASCII mode.') + return + try: + marker = int(line) + if marker < 0: + raise ValueError + except (ValueError, OverflowError): + self.respond("501 Invalid parameter.") + else: + self.respond("350 Restarting at position %s." % marker) + self._restart_position = marker + + def ftp_ABOR(self, line): + """Abort the current data transfer.""" + # ABOR received while no data channel exists + if (self._dtp_acceptor is None) and (self._dtp_connector is None) \ + and (self.data_channel is None): + self.respond("225 No transfer to abort.") + return + else: + # a PASV or PORT was received but connection wasn't made yet + if self._dtp_acceptor is not None or self._dtp_connector is not None: + self._shutdown_connecting_dtp() + resp = "225 ABOR command successful; data channel closed." + + # If a data transfer is in progress the server must first + # close the data connection, returning a 426 reply to + # indicate that the transfer terminated abnormally, then it + # must send a 226 reply, indicating that the abort command + # was successfully processed. + # If no data has been transmitted we just respond with 225 + # indicating that no transfer was in progress. + if self.data_channel is not None: + if self.data_channel.transfer_in_progress(): + self.data_channel.close() + self.data_channel = None + self.respond("426 Connection closed; transfer aborted.") + self.log("Transfer aborted via ABOR.") + resp = "226 ABOR command successful." + else: + self.data_channel.close() + self.data_channel = None + resp = "225 ABOR command successful; data channel closed." + self.respond(resp) + + + # --- authentication + + def ftp_USER(self, line): + """Set the username for the current session.""" + # RFC-959 specifies a 530 response to the USER command if the + # username is not valid. If the username is valid is required + # ftpd returns a 331 response instead. In order to prevent a + # malicious client from determining valid usernames on a server, + # it is suggested by RFC-2577 that a server always return 331 to + # the USER command and then reject the combination of username + # and password for an invalid username when PASS is provided later. + if not self.authenticated: + self.respond('331 Username ok, send password.') + else: + # a new USER command could be entered at any point in order + # to change the access control flushing any user, password, + # and account information already supplied and beginning the + # login sequence again. + self.flush_account() + msg = 'Previous account information was flushed' + self.log(msg) + self.respond('331 %s, send password.' % msg) + self.username = line + + _auth_failed_timeout = 5 + + def ftp_PASS(self, line): + """Check username's password against the authorizer.""" + if self.authenticated: + self.respond("503 User already authenticated.") + return + if not self.username: + self.respond("503 Login with USER first.") + return + + def auth_failed(username, password, msg): + self.sleeping = False + if hasattr(self, '_closed') and not self._closed: + self.attempted_logins += 1 + if self.attempted_logins >= self.max_login_attempts: + msg += " Disconnecting." + self.respond("530 " + msg) + self.close_when_done() + else: + self.respond("530 " + msg) + self.log_cmd("PASS", line, 530, msg) + self.on_login_failed(username, password) + + if self.authorizer.validate_authentication(self.username, line): + msg_login = self.authorizer.get_msg_login(self.username) + if len(msg_login) <= 75: + self.respond('230 %s' % msg_login) + else: + self.push("230-%s\r\n" % msg_login) + self.respond("230 ") + self.authenticated = True + self.password = line + self.attempted_logins = 0 + + home = self.authorizer.get_home_dir(self.username) + self.fs = self.abstracted_fs(home, self) + self.on_login(self.username) + else: + self.sleeping = True + if self.username == 'anonymous': + msg = "Anonymous access not allowed." + else: + msg = "Authentication failed." + CallLater(self._auth_failed_timeout, auth_failed, self.username, + line, msg, _errback=self.handle_error) + self.username = "" + + def ftp_REIN(self, line): + """Reinitialize user's current session.""" + # From RFC-959: + # REIN command terminates a USER, flushing all I/O and account + # information, except to allow any transfer in progress to be + # completed. All parameters are reset to the default settings + # and the control connection is left open. This is identical + # to the state in which a user finds himself immediately after + # the control connection is opened. + self.log("Previous account information was flushed.") + self.flush_account() + # Note: RFC-959 erroneously mention "220" as the correct response + # code to be given in this case, but this is wrong... + self.respond("230 Ready for new user.") + + + # --- filesystem operations + + def ftp_PWD(self, line): + """Return the name of the current working directory to the client.""" + # The 257 response is supposed to include the directory + # name and in case it contains embedded double-quotes + # they must be doubled (see RFC-959, chapter 7, appendix 2). + self.respond('257 "%s" is the current directory.' + % self.fs.cwd.replace('"', '""')) + + def ftp_CWD(self, path): + """Change the current working directory.""" + try: + self.run_as_current_user(self.fs.chdir, path) + except OSError, err: + why = _strerror(err) + self.respond('550 %s.' % why) + else: + self.respond('250 "%s" is the current directory.' % self.fs.cwd) + + def ftp_CDUP(self, path): + """Change into the parent directory.""" + # Note: RFC-959 says that code 200 is required but it also says + # that CDUP uses the same codes as CWD. + self.ftp_CWD(path) + + def ftp_SIZE(self, path): + """Return size of file in a format suitable for using with + RESTart as defined in RFC-3659.""" + + # Implementation note: properly handling the SIZE command when + # TYPE ASCII is used would require to scan the entire file to + # perform the ASCII translation logic + # (file.read().replace(os.linesep, '\r\n')) and then calculating + # the len of such data which may be different than the actual + # size of the file on the server. Considering that calculating + # such result could be very resource-intensive and also dangerous + # (DoS) we reject SIZE when the current TYPE is ASCII. + # However, clients in general should not be resuming downloads + # in ASCII mode. Resuming downloads in binary mode is the + # recommended way as specified in RFC-3659. + + line = self.fs.fs2ftp(path) + if self._current_type == 'a': + why = "SIZE not allowed in ASCII mode" + self.respond("550 %s." %why) + return + if not self.fs.isfile(self.fs.realpath(path)): + why = "%s is not retrievable" % line + self.respond("550 %s." % why) + return + try: + size = self.run_as_current_user(self.fs.getsize, path) + except OSError, err: + why = _strerror(err) + self.respond('550 %s.' % why) + else: + self.respond("213 %s" % size) + + def ftp_MDTM(self, path): + """Return last modification time of file to the client as an ISO + 3307 style timestamp (YYYYMMDDHHMMSS) as defined in RFC-3659. + """ + line = self.fs.fs2ftp(path) + if not self.fs.isfile(self.fs.realpath(path)): + self.respond("550 %s is not retrievable" % line) + return + if self.use_gmt_times: + timefunc = time.gmtime + else: + timefunc = time.localtime + try: + secs = self.run_as_current_user(self.fs.getmtime, path) + lmt = time.strftime("%Y%m%d%H%M%S", timefunc(secs)) + except (OSError, ValueError), err: + if isinstance(err, OSError): + why = _strerror(err) + else: + # It could happen if file's last modification time + # happens to be too old (prior to year 1900) + why = "Can't determine file's last modification time" + self.respond('550 %s.' % why) + else: + self.respond("213 %s" % lmt) + + def ftp_MKD(self, path): + """Create the specified directory.""" + line = self.fs.fs2ftp(path) + try: + self.run_as_current_user(self.fs.mkdir, path) + except OSError, err: + why = _strerror(err) + self.respond('550 %s.' %why) + else: + # The 257 response is supposed to include the directory + # name and in case it contains embedded double-quotes + # they must be doubled (see RFC-959, chapter 7, appendix 2). + self.respond('257 "%s" directory created.' % line.replace('"', '""')) + + def ftp_RMD(self, path): + """Remove the specified directory.""" + if self.fs.realpath(path) == self.fs.realpath(self.fs.root): + msg = "Can't remove root directory." + self.respond("550 %s" % msg) + return + try: + self.run_as_current_user(self.fs.rmdir, path) + except OSError, err: + why = _strerror(err) + self.respond('550 %s.' % why) + else: + self.respond("250 Directory removed.") + + def ftp_DELE(self, path): + """Delete the specified file.""" + try: + self.run_as_current_user(self.fs.remove, path) + except OSError, err: + why = _strerror(err) + self.respond('550 %s.' % why) + else: + self.respond("250 File removed.") + + def ftp_RNFR(self, path): + """Rename the specified (only the source name is specified + here, see RNTO command)""" + if not self.fs.lexists(path): + self.respond("550 No such file or directory.") + elif self.fs.realpath(path) == self.fs.realpath(self.fs.root): + self.respond("550 Can't rename the home directory.") + else: + self._rnfr = path + self.respond("350 Ready for destination name.") + + def ftp_RNTO(self, path): + """Rename file (destination name only, source is specified with + RNFR). + """ + if not self._rnfr: + self.respond("503 Bad sequence of commands: use RNFR first.") + return + src = self._rnfr + self._rnfr = None + try: + self.run_as_current_user(self.fs.rename, src, path) + except OSError, err: + why = _strerror(err) + self.respond('550 %s.' % why) + else: + self.respond("250 Renaming ok.") + + + # --- others + + def ftp_TYPE(self, line): + """Set current type data type to binary/ascii""" + type = line.upper().replace(' ', '') + if type in ("A", "L7"): + self.respond("200 Type set to: ASCII.") + self._current_type = 'a' + elif type in ("I", "L8"): + self.respond("200 Type set to: Binary.") + self._current_type = 'i' + else: + self.respond('504 Unsupported type "%s".' % line) + + def ftp_STRU(self, line): + """Set file structure ("F" is the only one supported (noop)).""" + stru = line.upper() + if stru == 'F': + self.respond('200 File transfer structure set to: F.') + elif stru in ('P', 'R'): + # R is required in minimum implementations by RFC-959, 5.1. + # RFC-1123, 4.1.2.13, amends this to only apply to servers + # whose file systems support record structures, but also + # suggests that such a server "may still accept files with + # STRU R, recording the byte stream literally". + # Should we accept R but with no operational difference from + # F? proftpd and wu-ftpd don't accept STRU R. We just do + # the same. + # + # RFC-1123 recommends against implementing P. + self.respond('504 Unimplemented STRU type.') + else: + self.respond('501 Unrecognized STRU type.') + + def ftp_MODE(self, line): + """Set data transfer mode ("S" is the only one supported (noop)).""" + mode = line.upper() + if mode == 'S': + self.respond('200 Transfer mode set to: S') + elif mode in ('B', 'C'): + self.respond('504 Unimplemented MODE type.') + else: + self.respond('501 Unrecognized MODE type.') + + def ftp_STAT(self, path): + """Return statistics about current ftp session. If an argument + is provided return directory listing over command channel. + + Implementation note: + + RFC-959 does not explicitly mention globbing but many FTP + servers do support it as a measure of convenience for FTP + clients and users. + + In order to search for and match the given globbing expression, + the code has to search (possibly) many directories, examine + each contained filename, and build a list of matching files in + memory. Since this operation can be quite intensive, both CPU- + and memory-wise, we do not support globbing. + """ + # return STATus information about ftpd + if not path: + s = [] + s.append('Connected to: %s:%s' % self.socket.getsockname()[:2]) + if self.authenticated: + s.append('Logged in as: %s' % self.username) + else: + if not self.username: + s.append("Waiting for username.") + else: + s.append("Waiting for password.") + if self._current_type == 'a': + type = 'ASCII' + else: + type = 'Binary' + s.append("TYPE: %s; STRUcture: File; MODE: Stream" % type) + if self._dtp_acceptor is not None: + s.append('Passive data channel waiting for connection.') + elif self.data_channel is not None: + bytes_sent = self.data_channel.tot_bytes_sent + bytes_recv = self.data_channel.tot_bytes_received + elapsed_time = self.data_channel.get_elapsed_time() + s.append('Data connection open:') + s.append('Total bytes sent: %s' % bytes_sent) + s.append('Total bytes received: %s' % bytes_recv) + s.append('Transfer elapsed time: %s secs' % elapsed_time) + else: + s.append('Data connection closed.') + + self.push('211-FTP server status:\r\n') + self.push(''.join([' %s\r\n' % item for item in s])) + self.respond('211 End of status.') + # return directory LISTing over the command channel + else: + line = self.fs.fs2ftp(path) + try: + iterator = self.run_as_current_user(self.fs.get_list_dir, path) + except OSError, err: + why = _strerror(err) + self.respond('550 %s.' %why) + else: + self.push('213-Status of "%s":\r\n' % line) + self.push_with_producer(BufferedIteratorProducer(iterator)) + self.respond('213 End of status.') + + def ftp_FEAT(self, line): + """List all new features supported as defined in RFC-2398.""" + features = ['TVFS'] + features += [feat for feat in ('EPRT', 'EPSV', 'MDTM', 'SIZE') \ + if feat in self.proto_cmds] + features.extend(self._extra_feats) + if 'MLST' in self.proto_cmds or 'MLSD' in self.proto_cmds: + facts = '' + for fact in self._available_facts: + if fact in self._current_facts: + facts += fact + '*;' + else: + facts += fact + ';' + features.append('MLST ' + facts) + if 'REST' in self.proto_cmds: + features.append('REST STREAM') + features.sort() + self.push("211-Features supported:\r\n") + self.push("".join([" %s\r\n" % x for x in features])) + self.respond('211 End FEAT.') + + def ftp_OPTS(self, line): + """Specify options for FTP commands as specified in RFC-2389.""" + try: + if line.count(' ') > 1: + raise ValueError('Invalid number of arguments') + if ' ' in line: + cmd, arg = line.split(' ') + if ';' not in arg: + raise ValueError('Invalid argument') + else: + cmd, arg = line, '' + # actually the only command able to accept options is MLST + if cmd.upper() != 'MLST' or 'MLST' not in self.proto_cmds: + raise ValueError('Unsupported command "%s"' % cmd) + except ValueError, err: + self.respond('501 %s.' % err) + else: + facts = [x.lower() for x in arg.split(';')] + self._current_facts = [x for x in facts if x in self._available_facts] + f = ''.join([x + ';' for x in self._current_facts]) + self.respond('200 MLST OPTS ' + f) + + def ftp_NOOP(self, line): + """Do nothing.""" + self.respond("200 I successfully done nothin'.") + + def ftp_SYST(self, line): + """Return system type (always returns UNIX type: L8).""" + # This command is used to find out the type of operating system + # at the server. The reply shall have as its first word one of + # the system names listed in RFC-943. + # Since that we always return a "/bin/ls -lA"-like output on + # LIST we prefer to respond as if we would on Unix in any case. + self.respond("215 UNIX Type: L8") + + def ftp_ALLO(self, line): + """Allocate bytes for storage (noop).""" + # not necessary (always respond with 202) + self.respond("202 No storage allocation necessary.") + + def ftp_HELP(self, line): + """Return help text to the client.""" + if line: + line = line.upper() + if line in self.proto_cmds: + self.respond("214 %s" % self.proto_cmds[line]['help']) + else: + self.respond("501 Unrecognized command.") + else: + # provide a compact list of recognized commands + def formatted_help(): + cmds = [] + keys = [x for x in self.proto_cmds.keys() if not x.startswith('SITE ')] + keys.sort() + while keys: + elems = tuple((keys[0:8])) + cmds.append(' %-6s' * len(elems) % elems + '\r\n') + del keys[0:8] + return ''.join(cmds) + + self.push("214-The following commands are recognized:\r\n") + self.push(formatted_help()) + self.respond("214 Help command successful.") + + # --- site commands + + # The user willing to add support for a specific SITE command must + # update self.proto_cmds dictionary and define a new ftp_SITE_%CMD% + # method in the subclass. + + def ftp_SITE_CHMOD(self, path, mode): + """Change file mode.""" + # Note: although most UNIX servers implement it, SITE CHMOD is not + # defined in any official RFC. + try: + assert len(mode) in (3, 4) + for x in mode: + assert 0 <= int(x) <= 7 + mode = int(mode, 8) + except (AssertionError, ValueError): + self.respond("501 Invalid SITE CHMOD format.") + else: + try: + self.run_as_current_user(self.fs.chmod, path, mode) + except OSError, err: + why = _strerror(err) + self.respond('550 %s.' % why) + else: + self.respond('200 SITE CHMOD successful.') + + def ftp_SITE_HELP(self, line): + """Return help text to the client for a given SITE command.""" + if line: + line = line.upper() + if line in self.proto_cmds: + self.respond("214 %s" % self.proto_cmds[line]['help']) + else: + self.respond("501 Unrecognized SITE command.") + else: + self.push("214-The following SITE commands are recognized:\r\n") + site_cmds = [] + keys = self.proto_cmds.keys() + keys.sort() + for cmd in keys: + if cmd.startswith('SITE '): + site_cmds.append(' %s\r\n' % cmd[5:]) + self.push(''.join(site_cmds)) + self.respond("214 Help SITE command successful.") + + # --- support for deprecated cmds + + # RFC-1123 requires that the server treat XCUP, XCWD, XMKD, XPWD + # and XRMD commands as synonyms for CDUP, CWD, MKD, LIST and RMD. + # Such commands are obsoleted but some ftp clients (e.g. Windows + # ftp.exe) still use them. + + def ftp_XCUP(self, line): + """Change to the parent directory. Synonym for CDUP. Deprecated.""" + self.ftp_CDUP(line) + + def ftp_XCWD(self, line): + """Change the current working directory. Synonym for CWD. Deprecated.""" + self.ftp_CWD(line) + + def ftp_XMKD(self, line): + """Create the specified directory. Synonym for MKD. Deprecated.""" + self.ftp_MKD(line) + + def ftp_XPWD(self, line): + """Return the current working directory. Synonym for PWD. Deprecated.""" + self.ftp_PWD(line) + + def ftp_XRMD(self, line): + """Remove the specified directory. Synonym for RMD. Deprecated.""" + self.ftp_RMD(line) + + +class FTPServer(object, asyncore.dispatcher): + """This class is an asyncore.disptacher subclass. It creates a FTP + socket listening on <address>, dispatching the requests to a <handler> + (typically FTPHandler class). + + Depending on the type of address specified IPv4 or IPv6 connections + (or both, depending from the underlying system) will be accepted. + + All relevant session information is stored in class attributes + described below. + + - (int) max_cons: + number of maximum simultaneous connections accepted (defaults + to 512). Can be set to 0 for unlimited but it is recommended + to always have a limit to avoid running out of file descriptors + (DoS). + + - (int) max_cons_per_ip: + number of maximum connections accepted for the same IP address + (defaults to 0 == unlimited). + """ + + max_cons = 512 + max_cons_per_ip = 0 + + def __init__(self, address, handler): + """Initiate the FTP server opening listening on address. + + - (tuple) address: the host:port pair on which the command + channel will listen. + + - (classobj) handler: the handler class to use. + """ + asyncore.dispatcher.__init__(self) + self.handler = handler + self.ip_map = [] + host, port = address + # in case of FTPS class not properly configured we want errors + # to be raised here rather than later, when client connects + if hasattr(handler, 'get_ssl_context'): + handler.get_ssl_context() + + # AF_INET or AF_INET6 socket + # Get the correct address family for our host (allows IPv6 addresses) + try: + info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM, 0, socket.AI_PASSIVE) + except socket.gaierror: + # Probably a DNS issue. Assume IPv4. + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.set_reuse_addr() + self.bind((host, port)) + else: + for res in info: + af, socktype, proto, canonname, sa = res + try: + self.create_socket(af, socktype) + self.set_reuse_addr() + self.bind(sa) + except socket.error, msg: + if self.socket: + self.socket.close() + self.socket = None + continue + break + if not self.socket: + raise socket.error(msg) + self.listen(5) + + @property + def address(self): + return self.socket.getsockname()[:2] + + def set_reuse_addr(self): + # Overridden for convenience. Avoid to reuse address on Windows. + if (os.name in ('nt', 'ce')) or (sys.platform == 'cygwin'): + return + asyncore.dispatcher.set_reuse_addr(self) + + @classmethod + def serve_forever(cls, timeout=1.0, use_poll=False, count=None): + """A wrap around asyncore.loop(); starts the asyncore polling + loop including running the scheduler. + The arguments are the same expected by original asyncore.loop() + function: + + - (float) timeout: the timeout passed to select() or poll() + system calls expressed in seconds (default 1.0). + + - (bool) use_poll: when True use poll() instead of select() + (default False). + + - (int) count: how many times the polling loop gets called + before returning. If None loops forever (default None). + """ + if use_poll and hasattr(asyncore.select, 'poll'): + poll_fun = asyncore.poll2 + else: + poll_fun = asyncore.poll + + if count is None: + log("starting FTP server") + try: + try: + while asyncore.socket_map or _scheduler._tasks: + poll_fun(timeout) + _scheduler() + except (KeyboardInterrupt, SystemExit, asyncore.ExitNow): + pass + finally: + log("shutting down FTP server") + cls.close_all() + else: + while (asyncore.socket_map or _scheduler._tasks) and count > 0: + if asyncore.socket_map: + poll_fun(timeout) + if _scheduler._tasks: + _scheduler() + count -= 1 + + def handle_accept(self): + """Called when remote client initiates a connection.""" + try: + sock, addr = self.accept() + except TypeError: + # sometimes accept() might return None (see issue 91) + return + except socket.error, err: + # ECONNABORTED might be thrown on *BSD (see issue 105) + if err.args[0] != errno.ECONNABORTED: + logerror(traceback.format_exc()) + return + else: + # sometimes addr == None instead of (ip, port) (see issue 104) + if addr is None: + return + + handler = None + ip = None + try: + handler = self.handler(sock, self) + if not handler.connected: + return + log("[]%s:%s Connected." % addr[:2]) + ip = addr[0] + self.ip_map.append(ip) + + # For performance and security reasons we should always set a + # limit for the number of file descriptors that socket_map + # should contain. When we're running out of such limit we'll + # use the last available channel for sending a 421 response + # to the client before disconnecting it. + if self.max_cons and (len(asyncore.socket_map) > self.max_cons): + handler.handle_max_cons() + return + + # accept only a limited number of connections from the same + # source address. + if self.max_cons_per_ip: + if self.ip_map.count(ip) > self.max_cons_per_ip: + handler.handle_max_cons_per_ip() + return + + try: + handler.handle() + except: + handler.handle_error() + except (KeyboardInterrupt, SystemExit, asyncore.ExitNow): + raise + except: + # This is supposed to be an application bug that should + # be fixed. We do not want to tear down the server though + # (DoS). We just log the exception, hoping that someone + # will eventually file a bug. References: + # - http://code.google.com/p/pyftpdlib/issues/detail?id=143 + # - http://code.google.com/p/pyftpdlib/issues/detail?id=166 + # - https://groups.google.com/forum/#!topic/pyftpdlib/h7pPybzAx14 + logerror(traceback.format_exc()) + if handler is not None: + handler.close() + else: + if ip is not None and ip in self.ip_map: + self.ip_map.remove(ip) + + def writable(self): + return 0 + + def handle_error(self): + """Called to handle any uncaught exceptions.""" + try: + raise + except (KeyboardInterrupt, SystemExit, asyncore.ExitNow): + raise + except: + logerror(traceback.format_exc()) + self.close() + + @classmethod + def close_all(cls, ignore_all=False): + """Stop serving and also disconnects all currently connected + clients. + + - (bool) ignore_all: + having it set to False results in raising exception in case + of unexpected errors. + + Implementation note: + + This is how asyncore.close_all() is implemented starting from + Python 2.6. + The previous versions of close_all() instead of iterating over + all opened channels and calling close() method for each one + of them only closed sockets generating memory leaks. + """ + values = asyncore.socket_map.values() + # We sort the list so that we close all FTP handler instances + # first since FTPHandler.close() has the peculiarity of + # automatically closing all its children (DTPHandler, ActiveDTP + # and PassiveDTP). + # This should minimize the possibility to incur in race + # conditions or memory leaks caused by orphaned references + # left behind in case of error. + values.sort(key=lambda inst: isinstance(inst, FTPHandler), reverse=True) + for x in values: + try: + x.close() + except OSError, x: + if x[0] == errno.EBADF: + pass + elif not ignore_all: + raise + except (asyncore.ExitNow, KeyboardInterrupt, SystemExit): + raise + except: + if not ignore_all: + asyncore.socket_map.clear() + del _scheduler._tasks[:] + raise + asyncore.socket_map.clear() + + for x in _scheduler._tasks: + try: + if not x.cancelled: + x.cancel() + except (asyncore.ExitNow, KeyboardInterrupt, SystemExit): + raise + except: + if not ignore_all: + del _scheduler._tasks[:] + raise + del _scheduler._tasks[:] + + +def main(): + """Start a stand alone anonymous FTP server.""" + + class CustomizedOptionFormatter(optparse.IndentedHelpFormatter): + """Formats options shown in help in a prettier way.""" + + def format_option(self, option): + result = [] + opts = self.option_strings[option] + result.append(' %s\n' % opts) + if option.help: + help_text = ' %s\n\n' % self.expand_default(option) + result.append(help_text) + return ''.join(result) + + usage = "python -m pyftpdlib.ftpserver [options]" + parser = optparse.OptionParser(usage=usage, description=main.__doc__, + formatter=CustomizedOptionFormatter()) + parser.add_option('-i', '--interface', default='0.0.0.0', metavar="ADDRESS", + help="specify the interface to run on (default all " + "interfaces)") + parser.add_option('-p', '--port', type="int", default=21, metavar="PORT", + help="specity port number to run on (default 21)") + parser.add_option('-w', '--write', action="store_true", default=False, + help="grants write access for the anonymous user " + "(default read-only)") + parser.add_option('-d', '--directory', default=os.getcwd(), metavar="FOLDER", + help="specify the directory to share (default current " + "directory)") + parser.add_option('-n', '--nat-address', default=None, metavar="ADDRESS", + help="the NAT address to use for passive connections") + parser.add_option('-r', '--range', default=None, metavar="FROM-TO", + help="the range of TCP ports to use for passive " + "connections (e.g. -r 8000-9000)") + parser.add_option('-v', '--version', action='store_true', + help="print pyftpdlib version and exit") + + options, args = parser.parse_args() + if options.version: + sys.exit("pyftpdlib %s" % __ver__) + passive_ports = None + if options.range: + try: + start, stop = options.range.split('-') + start = int(start) + stop = int(stop) + except ValueError: + parser.error('invalid argument passed to -r option') + else: + passive_ports = range(start, stop + 1) + # On recent Windows versions, if address is not specified and IPv6 + # is installed the socket will listen on IPv6 by default; in this + # case we force IPv4 instead. + if os.name in ('nt', 'ce') and not options.interface: + options.interface = '0.0.0.0' + + authorizer = DummyAuthorizer() + perm = options.write and "elradfmwM" or "elr" + authorizer.add_anonymous(options.directory, perm=perm) + handler = FTPHandler + handler.authorizer = authorizer + handler.masquerade_address = options.nat_address + handler.passive_ports = passive_ports + ftpd = FTPServer((options.interface, options.port), FTPHandler) + ftpd.serve_forever() + +if __name__ == '__main__': + main() diff --git a/chromium/third_party/pyftpdlib/src/setup.py b/chromium/third_party/pyftpdlib/src/setup.py new file mode 100644 index 00000000000..b3f43dc83e0 --- /dev/null +++ b/chromium/third_party/pyftpdlib/src/setup.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python +# $Id$ +# +# pyftpdlib is released under the MIT license, reproduced below: +# ====================================================================== +# Copyright (C) 2007-2012 Giampaolo Rodola' <g.rodola@gmail.com> +# +# All Rights Reserved +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ====================================================================== + +"""pyftpdlib installer. + +To install pyftpdlib just open a command shell and run: +> python setup.py install +""" + +import os +import sys +try: + from setuptools import setup +except ImportError: + from distutils.core import setup + +name = 'pyftpdlib' +version = '0.7.0' +download_url = "http://pyftpdlib.googlecode.com/files/" + name + "-" + \ + version + ".tar.gz" + +setup( + name=name, + version=version, + description='High-level asynchronous FTP server library', + long_description="Python FTP server library provides an high-level portable " + "interface to easily write asynchronous FTP servers with " + "Python.", + license='License :: OSI Approved :: MIT License', + platforms='Platform Independent', + author="Giampaolo Rodola'", + author_email='g.rodola@gmail.com', + url='http://code.google.com/p/pyftpdlib/', + download_url=download_url, + packages=['pyftpdlib', 'pyftpdlib/contrib'], + keywords=['ftp', 'ftps', 'server', 'ftpd', 'daemon', 'python', 'ssl', + 'sendfile', 'rfc959', 'rfc1123', 'rfc2228', 'rfc2428', 'rfc3659'], + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Console', + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Internet :: File Transfer Protocol (FTP)', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Topic :: System :: Filesystems', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.4', + 'Programming Language :: Python :: 2.5', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + ], + ) + +if os.name == 'posix': + try: + import sendfile + except ImportError: + msg = "\nYou might want to install pysendfile module to speedup " \ + "transfers:\nhttp://code.google.com/p/pysendfile/\n" + if sys.stderr.isatty(): + sys.stderr.write('\x1b[1m%s\x1b[0m' % msg) + else: + sys.stderr.write(msg) |