From d90cc93e850048668c411af7502c81c1ab093fce Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 16 Sep 2024 10:48:26 +0200 Subject: [PATCH] Update doc (#649) --- CREDITS | 2 +- HISTORY.rst | 10 +- MANIFEST.in | 4 +- README.rst | 70 ++-- demo/tls_ftpd.py | 2 +- demo/unix_daemon.py | 2 +- demo/{winnt_ftpd.py => win_ftpd.py} | 0 docs/README | 2 +- docs/adoptions.rst | 160 ++++----- docs/api.rst | 457 ++++++++++++------------ docs/benchmarks.rst | 57 ++- docs/conf.py | 379 +------------------- docs/faqs.rst | 333 +++++++---------- docs/images/adcast.png | Bin 0 -> 6769 bytes docs/images/debian.png | Bin 8915 -> 0 bytes docs/images/farmanager.png | Bin 0 -> 7482 bytes docs/images/fedora.png | Bin 4850 -> 0 bytes docs/images/google-pages.gif | Bin 0 -> 4731 bytes docs/images/netplay.jpg | Bin 0 -> 14601 bytes docs/images/peerscape.gif | Bin 0 -> 3297 bytes docs/images/putio.png | Bin 0 -> 15478 bytes docs/images/pyfilesystem.svg | 1 + docs/images/rackspace-cloud-hosting.jpg | Bin 0 -> 12810 bytes docs/images/symbianftp.png | Bin 0 -> 3695 bytes docs/index.rst | 2 +- docs/install.rst | 18 +- docs/rfc-compliance.rst | 124 ++++--- docs/tutorial.rst | 358 ++++++++++--------- pyftpdlib/handlers.py | 20 +- pyftpdlib/ioloop.py | 8 +- pyftpdlib/log.py | 2 +- pyftpdlib/test/__init__.py | 2 +- pyftpdlib/test/test_functional.py | 2 +- pyproject.toml | 3 + scripts/ftpbench | 6 +- scripts/internal/print_announce.py | 4 +- 36 files changed, 812 insertions(+), 1216 deletions(-) rename demo/{winnt_ftpd.py => win_ftpd.py} (100%) create mode 100644 docs/images/adcast.png delete mode 100644 docs/images/debian.png create mode 100644 docs/images/farmanager.png delete mode 100644 docs/images/fedora.png create mode 100644 docs/images/google-pages.gif create mode 100644 docs/images/netplay.jpg create mode 100644 docs/images/peerscape.gif create mode 100644 docs/images/putio.png create mode 100644 docs/images/pyfilesystem.svg create mode 100644 docs/images/rackspace-cloud-hosting.jpg create mode 100644 docs/images/symbianftp.png diff --git a/CREDITS b/CREDITS index 9a96e68b..bea7529b 100644 --- a/CREDITS +++ b/CREDITS @@ -23,7 +23,7 @@ D: Original pyftpdlib author and maintainer N: Jay Loden C: NJ, USA E: jloden@gmail.com -W: http://www.jayloden.com +W: http://jayloden.com/About.htm D: OS X and Linux platform development/testing N: Silas Sewell diff --git a/HISTORY.rst b/HISTORY.rst index dca2a5cd..f216e241 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -21,7 +21,7 @@ Version: 2.0.0 - 2024-09-04 * #629: Python 2.7 is no longer supported. * #629: pysendfile module is no longer a required dependency, because we ceased support for Python 2. -* #639: (FTPS)SSLv2 and SSLv3 connections are no longer accepted when client +* #639: (FTPS) SSLv2 and SSLv3 connections are no longer accepted when client connects. Version: 1.5.10 - 2024-06-23 @@ -126,7 +126,7 @@ Version: 1.5.3 - 2017-11-04 - #201: implemented SITE MFMT command which changes file modification time. (patch by Tahir Ijaz) - #327: add username and password command line options -- #433: documentation moved to readthedocs: http://pyftpdlib.readthedocs.io +- #433: documentation moved to readthedocs: https://pyftpdlib.readthedocs.io **Bug fixes** @@ -258,7 +258,7 @@ Version: 1.3.0 - Date: 2013-11-07 Juan J. Martinez) - #265: FTPServer class cannot be used with Circus. - #272: pyftpdlib fails when imported on OpenBSD because of Python bug - http://bugs.python.org/issue3770 + https://bugs.python.org/issue3770 - #273: IOLoop.fileno() on BSD systems raises AttributeError. (patch by Michael Ross) @@ -450,7 +450,7 @@ namespaces has been moved here: (see issue 213) - ftpserver.py's log(), logline() and logerror() functions are deprecated. logging module is now used instead. See: - http://code.google.com/p/billiejoex/wiki/Tutorial#4.2_-_Logging_management + https://pyftpdlib.readthedocs.io/en/latest/tutorial.html#logging-management - Unicode is now used instead of bytes pretty much everywhere. - FTPHandler.__init__() and TLS_FTPHandler.__init__() signatures have changed: from __init__(conn, server) @@ -729,7 +729,7 @@ Version: 0.5.1 - Date: 2009-01-21 - #98: added preliminary support for SITE command. - #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 + https://code.google.com/p/pyftpdlib/source/browse/trunk/demo/tls_ftpd.py **Bug fixes** diff --git a/MANIFEST.in b/MANIFEST.in index dfaeec26..31e8cff6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -15,7 +15,7 @@ include demo/throttled_ftpd.py include demo/tls_ftpd.py include demo/unix_daemon.py include demo/unix_ftpd.py -include demo/winnt_ftpd.py +include demo/win_ftpd.py include docs/.readthedocs.yaml include docs/Makefile include docs/README @@ -25,6 +25,8 @@ include docs/benchmarks.rst include docs/conf.py include docs/faqs.rst include docs/images/freebsd.gif +include docs/images/google-pages.gif +include docs/images/peerscape.gif include docs/index.rst include docs/install.rst include docs/make.bat diff --git a/README.rst b/README.rst index ac898d86..4229bc26 100644 --- a/README.rst +++ b/README.rst @@ -48,54 +48,50 @@ Quick links =========== -- `Home `__ -- `Documentation `__ -- `Download `__ -- `Blog `__ -- `Mailing list `__ -- `What's new `__ +- `Home`_ +- `Documentation`_ +- `Download`_ +- `Mailing list`_ +- `What's new`_ About ===== Python FTP server library provides a high-level portable interface to easily write very efficient, scalable and asynchronous FTP servers with Python. It is -the most complete `RFC-959 `__ FTP server -implementation available for `Python `__ programming -language. +the most complete `RFC-959`_ FTP server implementation available for `Python`_ +programming language. Features ======== - Extremely **lightweight**, **fast** and **scalable** (see `why `__ and - `benchmarks `__). + `benchmarks`__). - Uses **sendfile(2)** (see `pysendfile `__) - system call for uploads. -- Uses epoll() / kqueue() / select() to handle concurrency asynchronously. -- ...But can optionally skip to a - `multiple thread / process `__ - model (as in: you'll be free to block or use slow filesystems). + system call for uploads (Linux only). +- Uses ``epoll()`` / ``kqueue()`` / ``select()`` to handle concurrency + asynchronously. +- ...But can optionally skip to a `multiple thread / process`_ model (as in: + you'll be free to block or use slow filesystems). - Portable: entirely written in pure Python. -- Supports **FTPS** (`RFC-4217 `__), - **IPv6** (`RFC-2428 `__), - **Unicode** file names (`RFC-2640 `__), - **MLSD/MLST** commands (`RFC-3659 `__). +- Supports **FTPS** (`RFC-4217`_), **IPv6** (`RFC-2428`_), **Unicode** file + names (`RFC-2640`_), **MLSD/MLST** commands (`RFC-3659`_). - Support for virtual users and virtual filesystem. - Flexible system of "authorizers" able to manage both "virtual" and "real" users on on both - `UNIX `__ + `UNIX `__ and - `Windows `__. + `Windows `__. Performances ============ Despite being written in an interpreted language, pyftpdlib has transfer rates -comparable or superior to common UNIX FTP servers written in C. It usually tends -to scale better (see `benchmarks `__) -because whereas vsftpd and proftpd use multiple processes to -achieve concurrency, pyftpdlib only uses one (see `the C10K problem `__). +comparable or superior to common UNIX FTP servers written in C. It usually +tends to scale better (see `benchmarks`_) because whereas vsftpd and proftpd +use multiple processes to achieve concurrency, pyftpdlib only uses one (see +`the C10K problem`_). pyftpdlib vs. proftpd 1.3.4 --------------------------- @@ -143,14 +139,14 @@ pyftpdlib vs. vsftpd 2.3.5 | 300 concurrent clients (QUIT) | 0.03 secs | 0.01 secs | +0.14x | +-----------------------------------------+----------------+----------------+-------------+ -For more benchmarks see `here `__. +For more benchmarks see `here `__. Command line usage ================== Start a FTP server, with an anonymous user with write permissions, on port 2121: -.. code-block:: +.. code-block:: sh $ python3 -m pyftpdlib --write RuntimeWarning: write permissions assigned to anonymous user. @@ -188,11 +184,27 @@ API usage [I 13-02-19 10:56:27] 127.0.0.1:34179-[user] RETR /home/giampaolo/.vimrc completed=1 bytes=1700 seconds=0.001 [I 13-02-19 10:56:39] 127.0.0.1:34179-[user] FTP session closed (disconnect). -`other code samples `__ +For other code samples read the `tutorial `__ Donate ====== A lot of time and effort went into making pyftpdlib as it is right now. If you feel pyftpdlib is useful to you or your business and want to support its -future development please consider `donating `__ me some money. +future development please consider `donating`_ me some money. + +.. _`benchmarks`: https://pyftpdlib.readthedocs.io/en/latest/benchmarks.html +.. _`Documentation`: https://pyftpdlib.readthedocs.io +.. _`donating`: https://gmpy.dev/donate +.. _`Download`: https://pypi.org/project/pyftpdlib/ +.. _`Home`: https://github.com/giampaolo/pyftpdlib +.. _`Mailing list`: https://groups.google.com/group/pyftpdlib/topics +.. _`multiple thread / process`: https://pyftpdlib.readthedocs.io/en/latest/tutorial.html#changing-the-concurrency-model +.. _`Python`: https://www.python.org/ +.. _`RFC-2428`: https://datatracker.ietf.org/doc/html/rfc2428 +.. _`RFC-2640`: https://datatracker.ietf.org/doc/html/rfc2640 +.. _`RFC-3659`: https://datatracker.ietf.org/doc/html/rfc3659 +.. _`RFC-4217`: https://datatracker.ietf.org/doc/html/rfc4217 +.. _`RFC-959`: https://datatracker.ietf.org/doc/html/rfc959.html +.. _`the C10K problem`: http://www.kegel.com/c10k.html +.. _`What's new`: https://github.com/giampaolo/pyftpdlib/blob/master/HISTORY.rst diff --git a/demo/tls_ftpd.py b/demo/tls_ftpd.py index 0b7f3851..4ed2884a 100755 --- a/demo/tls_ftpd.py +++ b/demo/tls_ftpd.py @@ -6,7 +6,7 @@ """ An RFC-4217 asynchronous FTPS server supporting both SSL and TLS. -Requires PyOpenSSL module (http://pypi.python.org/pypi/pyOpenSSL). +Requires PyOpenSSL module (https://pypi.org/project/pyOpenSSL). """ import os diff --git a/demo/unix_daemon.py b/demo/unix_daemon.py index 5a721811..50615a6a 100755 --- a/demo/unix_daemon.py +++ b/demo/unix_daemon.py @@ -5,7 +5,7 @@ # found in the LICENSE file. """A basic unix daemon using the python-daemon library: -http://pypi.python.org/pypi/python-daemon. +https://pypi.org/project/python-daemon. Example usages: diff --git a/demo/winnt_ftpd.py b/demo/win_ftpd.py similarity index 100% rename from demo/winnt_ftpd.py rename to demo/win_ftpd.py diff --git a/docs/README b/docs/README index f624b154..dcc2b4d9 100644 --- a/docs/README +++ b/docs/README @@ -3,7 +3,7 @@ About This directory contains the reStructuredText (reST) sources to the pyftpdlib documentation. You don't need to build them yourself, prebuilt versions are -available at http://pyftpdlib.readthedocs.io. +available at https://pyftpdlib.readthedocs.io. In case you want, you need to install sphinx first: $ pip install sphinx diff --git a/docs/adoptions.rst b/docs/adoptions.rst index ce7a779e..1cef0b9e 100644 --- a/docs/adoptions.rst +++ b/docs/adoptions.rst @@ -4,8 +4,9 @@ Adoptions .. contents:: Table of Contents -Here comes a list of softwares and systems using pyftpdlib. -In case you want to add your software to such list add a comment below. +Here comes a (mostly outdated) list of softwares and systems using pyftpdlib. +In case you want to add your software to such list, make a PR or create a +ticket on the bug tracker. Please help us in keeping such list updated. Packages @@ -13,152 +14,135 @@ Packages Following lists the packages of pyftpdlib from various platforms. -Debian ------- - -.. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/debian.png - -A `.deb packaged version of pyftpdlib `__ -is available. - -Fedora ------- +Various Linux Distros +--------------------- -.. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/fedora.png +pyftpdlib has been packaged for different Linux distros, see `repology.org `__. -A `RPM packaged version `__ -is available. +.. image:: https://repology.org/badge/vertical-allrepos/python:pyftpdlib.svg FreeBSD ------- -.. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/freebsd.gif +.. image:: https://github.com/giampaolo/pyftpdlib/blob/master/docs/images/freebsd.gif?raw=true A `freshport `__ is available. -GNU Darwin ----------- - -.. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/gnudarwin.png - -`GNU Darwin `__ is a Unix distribution which focuses -on the porting of free software to Darwin and Mac OS X. pyftpdlib has been -recently included in the official repositories to make users can easily install -and use it on GNU Darwin systems. - Softwares ========= Following lists the softwares adopting pyftpdlib. -Google Chrome -------------- +Google Chromium +--------------- -.. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/chrome.jpg +.. image:: https://github.com/giampaolo/pyftpdlib/blob/master/docs/images/chrome.jpg?raw=true -`Google Chrome `__ is the new free and open -source web browser developed by Google. -`Google Chromium `__, the open -source project behind Google Chrome, included pyftpdlib in the code base to -develop Google Chrome's FTP client unit tests. +`Google Chromium `__, the open +source project behind Google Chrome, uses pyftpdlib for unit tests of the +FTP client included in the browser. Smartfile --------- -.. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/smartfile.jpg +.. image:: https://github.com/giampaolo/pyftpdlib/blob/master/docs/images/smartfile.png?raw=true -`Smartfile `__ is a market leader in FTP and online +`Smartfile `__ is a market leader in FTP and online file storage that has a robust and easy-to-use web interface. We utilize pyftpdlib as the underpinnings of our FTP service. Pyftpdlib gives us the flexibility we require to integrate FTP with the rest of our application. +Pyfilesystem +------------ + +.. image:: https://github.com/giampaolo/pyftpdlib/blob/master/docs/images/images/pyfilesystem.svg?raw=true + +`Pyfilesystem `__ is a Python module +that provides a common interface to many types of filesystem, and provides some +powerful features such as exposing filesystems over an internet connection, or +to the native filesystem. It uses pyftpdlib as a backend for testing its FTP +component. + Bazaar ------ -.. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/bazaar.jpg +.. image:: https://github.com/giampaolo/pyftpdlib/blob/master/docs/images/bazaar.jpg?raw=true -`Bazaar `__ is a distributed version control system -similar to Subversion which supports different protocols among which FTP. -As for `Google Chrome `__, Bazaar recently -adopted pyftpdlib as base FTP server to implement internal FTP unit tests. +`Bazaar `__ is a distributed version control +system similar to GIT which supports different protocols among which FTP. Same +as Google Chromium, Bazaar uses pyftpdlib as the base FTP server to implement +internal FTP unit tests. Python for OpenVMS ------------------ -.. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/pyopenvms.png +.. image:: https://github.com/giampaolo/pyftpdlib/blob/master/docs/images/openvms.png?raw=true -`OpenVMS `__ is an +`OpenVMS `__ is an operating system that runs on the `VAX `__ -and `Alpha `__ families of computers, +and `Alpha `__ computer families, now owned by Hewlett-Packard. `vmspython `__ is a porting of the original cPython interpreter that runs on OpenVMS platforms. -pyftpdlib recently became a standard library module installed by default on +pyftpdlib became a standard library module installed by default on every new vmspython installation. -http://www.vmspython.org/DownloadAndInstallationPython - OpenERP ------- -.. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/openerp.jpg +.. image:: https://github.com/giampaolo/pyftpdlib/blob/master/docs/images/openerp.jpg?raw=true `OpenERP `__ is an Open Source enterprise management software. It covers and integrates most enterprise needs and processes: accounting, hr, sales, crm, purchase, stock, production, services management, -project management, marketing campaign, management by affairs. OpenERP recently +project management, marketing campaign, management by affairs. OpenERP included pyftpdlib as plug in to serve documents via FTP. Plumi ----- -.. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/plumi.jpg - -`Plumi `__ is a video sharing Content Management System -based on `Plone `__ that enables you to create your own +`Plumi `__ is a video sharing Content Management System +based on `Plone `__ that enables you to create your own sophisticated video sharing site. pyftpdlib has been included in Plumi to allow resumable large video file uploads -into `Zope `__. +into `Zope `__. put.io FTP connector -------------------- -.. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/putio.png +.. image:: https://github.com/giampaolo/pyftpdlib/blob/master/docs/images/putio.png?raw=true -A proof of concept FTP server that proxies FTP clients requests to -`putio `__ via HTTP, or in other words an FTP interface to -put.io Put.io is a storage service that fetches media files remotely and lets -you stream them immediately. More info can be found -`here `__. See -https://github.com/ybrs/putio-ftp-connector -`blog entry `__ +`put.io `__ is a storage service that fetches media files +remotely and lets you stream them immediately. They wrote a PoC based on +pyftplidb that proxies FTP clients requests to put.io via HTTP. More info can +be found `here `__. See +https://github.com/ybrs/putio-ftp-connector. Rackspace Cloud's FTP --------------------- -.. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/rackspace-cloud-hosting.jpg +.. image:: https://github.com/giampaolo/pyftpdlib/blob/master/docs/images/rackspace-cloud-hosting.jpg?raw=true -`ftp-cloudfs `__ is a ftp server acting -as a proxy to Rackspace `Cloud Files `__. It +`ftp-cloudfs `__ is a FTP server acting +as a proxy to `Rackspace Cloud `__. It allows you to connect via any FTP client to do upload/download or create containers. Far Manager ----------- -.. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/farmanager.png +.. image:: https://github.com/giampaolo/pyftpdlib/blob/master/docs/images/farmanager.png?raw=true `Far Manager `__ is a program for managing files and -archives in Windows operating systems. -Far Manager recently included pyftpdlib as a plug-in for making the current -directory accessible through FTP. Convenient for exchanging files with virtual -machines. +archives on Windows. Far Manager included pyftpdlib as a plug-in for making the +current directory accessible through FTP, which is convenient for exchanging +files with virtual machines. Google Pages FTPd ----------------- -.. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/google-pages.gif +.. image:: https://github.com/giampaolo/pyftpdlib/blob/master/docs/images/google-pages.gif?raw=true `gpftpd `__ is a pyftpdlib based FTP server you can connect to using your Google e-mail @@ -170,7 +154,7 @@ download them and upload new ones. Peerscape --------- -.. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/peerscape.gif +.. image:: https://github.com/giampaolo/pyftpdlib/blob/master/docs/images/peerscape.gif?raw=true `Peerscape `__ is an experimental peer-to-peer social network implemented as an extension to the Firefox web browser. It implements a @@ -191,17 +175,10 @@ performances. Symbian Python FTP server ------------------------- -.. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/symbianftp.png +.. image:: https://github.com/giampaolo/pyftpdlib/blob/master/docs/images/symbianftp.png?raw=true An FTP server for Symbian OS: http://code.google.com/p/sypftp/ -ftp-cloudfs ------------ - -An FTP server acting as a proxy to Rackspace Cloud Files or to OpenStack Swift. -It allow you to connect via any FTP client to do upload/download or create -containers: https://github.com/chmouel/ftp-cloudfs - Sierramobilepos --------------- @@ -219,23 +196,10 @@ Faetus server that translates FTP commands into Amazon S3 API calls providing an FTP interface on top of Amazon S3 storage. - - -Pyfilesystem ------------- - -`Pyfilesystem `__ is a Python module -that provides a common interface to many types of filesystem, and provides some -powerful features such as exposing filesystems over an internet connection, or -to the native filesystem. It uses pyftpdlib as a backend for testing its FTP -component. - - - Manent ------ -`Manent `__ is an algorithmically strong +`Manent `__ is an algorithmically strong backup and archival program which can offer remote backup via a pyftpdlib-based S/FTP server. @@ -247,8 +211,6 @@ control S5000/S6000, Z4/Z8 and MPC4000 Akai sampler models with System Exclusive over USB. Aksy introduced the possibility to mount samplers as web folders and manage files on the sampler via FTP. - - Imgserve -------- @@ -321,14 +283,14 @@ Web sites using pyftpdlib www.bitsontherun.com -------------------- -.. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/bitsontherun.png +.. image:: https://github.com/giampaolo/pyftpdlib/blob/master/docs/images/bitsontherun.png?raw=true http://www.bitsontherun.com www.adcast.tv ------------- -.. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/adcast.png +.. image:: https://github.com/giampaolo/pyftpdlib/blob/master/docs/images/adcast.png?raw=true http://www.adcast.tv http://www.adcast.tv @@ -337,4 +299,4 @@ www.netplay.it .. image:: http://pyftpdlib.googlecode.com/svn/wiki/images/netplay.jpg -http://netplay.it/ \ No newline at end of file +http://netplay.it/ diff --git a/docs/api.rst b/docs/api.rst index 58fba7cc..77c37d22 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -5,37 +5,30 @@ API reference .. contents:: Table of Contents pyftpdlib implements the server side of the FTP protocol as defined in -`RFC-959 `_. This document is intended to +`RFC-959 `_. This document is intended to serve as a simple API reference of most important classes and functions. -After reading this you will probably want to read the -`tutorial `_ including customization through the use of some -example scripts. +Also see the `tutorial `_ document. Modules and classes hierarchy ============================= :: - pyftpdlib.authorizers pyftpdlib.authorizers.AuthenticationFailed pyftpdlib.authorizers.DummyAuthorizer pyftpdlib.authorizers.UnixAuthorizer pyftpdlib.authorizers.WindowsAuthorizer - pyftpdlib.handlers pyftpdlib.handlers.FTPHandler pyftpdlib.handlers.TLS_FTPHandler pyftpdlib.handlers.DTPHandler pyftpdlib.handlers.TLS_DTPHandler pyftpdlib.handlers.ThrottledDTPHandler - pyftpdlib.filesystems pyftpdlib.filesystems.FilesystemError pyftpdlib.filesystems.AbstractedFS pyftpdlib.filesystems.UnixFilesystem - pyftpdlib.servers pyftpdlib.servers.FTPServer pyftpdlib.servers.ThreadedFTPServer pyftpdlib.servers.MultiprocessFTPServer - pyftpdlib.ioloop pyftpdlib.ioloop.IOLoop pyftpdlib.ioloop.Connector pyftpdlib.ioloop.Acceptor @@ -46,15 +39,13 @@ Users .. class:: pyftpdlib.authorizers.DummyAuthorizer() - Basic "dummy" authorizer class, suitable for subclassing to create your own - custom authorizers. An "authorizer" is a class handling authentications and - permissions of the FTP server. It is used inside - :class:`pyftpdlib.handlers.FTPHandler` class for verifying user's password, - getting users home directory, checking user permissions when a filesystem - read/write event occurs and changing user before accessing the filesystem. - DummyAuthorizer is the base authorizer, providing a platform independent - interface for managing "virtual" FTP users. Typically the first thing you - have to do is create an instance of this class and start adding ftp users: + Basic "dummy" authorizer class which lets you create virtual users. + It is also suitable for subclassing to create your own custom authorizer. + The "authorizer" is a class handling authentications and + permissions of the FTP server. It is used by + :class:`pyftpdlib.handlers.FTPHandler` class for verifying user passwords, + getting users home directory and checking user permissions when a filesystem + event occurs. Example usage: >>> from pyftpdlib.authorizers import DummyAuthorizer >>> authorizer = DummyAuthorizer() @@ -63,12 +54,10 @@ Users .. method:: add_user(username, password, homedir, perm="elr", msg_login="Login successful.", msg_quit="Goodbye.") - Add a user to the virtual users table. AuthorizerError exception is raised + Add a user to the virtual users table. ``AuthorizerError`` exception is raised on error conditions such as insufficient permissions or duplicate usernames. - Optional *perm* argument is a set of letters referencing the user's - permissions. Every letter is used to indicate that the access rights the - current FTP user has over the following specific actions are granted. The - available permissions are the following listed below: + The *perm* argument is a set of letters indicating the user's + permissions: Read permissions: @@ -86,30 +75,24 @@ Users - ``"M"`` = change file mode / permission (SITE CHMOD command) *New in 0.7.0* - ``"T"`` = change file modification time (SITE MFMT command) *New in 1.5.3* - Optional *msg_login* and *msg_quit* arguments can be specified to provide - customized response strings when user log-in and quit. The *perm* argument - of the :meth:`add_user()` method refers to user's permissions. Every letter - is used to indicate that the access rights the current FTP user has over - the following specific actions are granted. + *msg_login* and *msg_quit* arguments can be specified to provide + customized response strings when the user logs in and quits. .. method:: add_anonymous(homedir, **kwargs) - Add an anonymous user to the virtual users table. AuthorizerError exception - is raised on error conditions such as insufficient permissions, missing - home directory, or duplicate anonymous users. The keyword arguments in - kwargs are the same expected by :meth:`add_user()` method: *perm*, - *msg_login* and *msg_quit*. The optional perm keyword argument is a string - defaulting to "elr" referencing "read-only" anonymous user's permission. - Using a "write" value results in a RuntimeWarning. + Add an anonymous user to the virtual users table. + The keyword arguments are the same expected by :meth:`add_user()` method. + The only difference is that if write permissions are passed as *perm* + a ``RuntimeWarning`` will be raised. .. method:: override_perm(username, directory, perm, recursive=False) - Override user permissions for a given directory. + Override user permissions for a specific directory. .. method:: validate_authentication(username, password, handler) Raises :class:`pyftpdlib.authorizers.AuthenticationFailed` if the supplied - username and password doesn't match the stored credentials. + username and password don't match the stored credentials. *Changed in 1.0.0: new handler parameter.* @@ -119,14 +102,13 @@ Users 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. + may provide a mechanism to change the current user. .. method:: terminate_impersonation(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. + method may provide a mechanism to switch back to the original user. .. method:: remove_user(username) @@ -137,36 +119,37 @@ Control connection .. class:: pyftpdlib.handlers.FTPHandler(conn, server) - This class implements the FTP server Protocol Interpreter (see - `RFC-959 `_), handling commands received - from the client on the control channel by calling the command's corresponding - method (e.g. for received command "MKD pathname", ftp_MKD() method is called - with pathname as the argument). All relevant session information are stored - in instance variables. conn is the underlying socket object instance of the - newly established connection, server is the - :class:`pyftpdlib.servers.FTPServer` class instance. Basic usage simply - requires creating an instance of FTPHandler class and specify which - authorizer instance it will going to use: + This class implements the "FTP server Protocol Interpreter" as defined in + `RFC-959 `_, commonly known as + the FTP "control connection". + It handles the commands received from the client. + E.g. if command "MKD pathname" is received, ``ftp_MKD()`` method is called + with ``pathname`` as the argument. + ``conn`` argument is a socket object instance of the newly established connection. + ``server`` is a reference to the :class:`pyftpdlib.servers.FTPServer` class + instance. + Basic usage requires creating an instance of this class and specify which + authorizer it is going to use: >>> from pyftpdlib.handlers import FTPHandler >>> handler = FTPHandler >>> handler.authorizer = authorizer - All relevant session information is stored in class attributes reproduced - below and can be modified before instantiating this class: + Configurable class attributes: .. data:: 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). + commands. If the timeout triggers, the remote client will be kicked off. + Default: ``300`` seconds. *New in version 5.0* .. data:: banner - String sent when client connects (default - ``"pyftpdlib %s ready." %__ver__``). + The string sent when client connects. The default is + ``"pyftpdlib %s ready." %__ver__``. If you want to make this dynamic you + can define this as a `property `__. .. data:: max_login_attempts @@ -175,52 +158,66 @@ Control connection .. data:: permit_foreign_addresses - Whether enable FXP feature (default ``False``). + Also known as "FXP" or "site-to-site transfer feature". If ``True`` + it allows for transferring a file between two remote FTP servers, + without the transfer going through the client's host. This is 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. Default: ``False``. .. data:: permit_privileged_ports Set to ``True`` if you want to permit active connections (PORT) over - privileged ports (not recommended, default ``False``). + privileged ports. Not recommended for security reason. Default: ``False``. .. data:: 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). + will hide its local address and instead use the public address of your NAT. + Use this if you're behing a NAT. Default: ``None``. .. data:: 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 + In case the server has multiple IP addresses which are all behind a NAT, + 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 ``{}``). *New in version 0.6.0* + and their corresponding public (masquerade) addresses as values. + Default: ``{}`` (empty dict). + + *New in version 0.6.0* .. data:: passive_ports - What ports ftpd will use for its passive data transfers. Value expected is - a list of integers (e.g. ``range(60000, 65535)``). When configured - pyftpdlib will no longer use kernel-assigned random ports (default - ``None``). + What TCP ports the FTP server will use for passive (PASV) data transfers. + The value expected is a list of integers (e.g. ``list(range(60000, 65535))``). + When configured, pyftpdlib will no longer use kernel-assigned random TCP ports. + Default: ``None``. .. data:: use_gmt_times - When ``True`` causes the server to report all ls and MDTM times in GMT and - not local time (default ``True``). *New in version 0.6.0* + When ``True`` causes the FTP server to report all times as GMT. This + affects MDTM, MFMT, LIST, MLSD and MLST commands. + If set to ``False``, the times will be expressed in the server local time + (not recommended). Default: ``True``. + + *New in version 0.6.0* .. data:: 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 platforms where it is supported). *New in version 0.6.0* + Controls the use of the TCP_NODELAY socket option, which disables the Nagle + algorithm. It usually result in significantly better performances. + Default ``True`` on all platforms where it is supported. + + *New in version 0.6.0* .. data:: use_sendfile - When ``True`` uses sendfile(2) 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. + When ``True`` uses the ``sendfile(2)`` system call when sending file, + resulting in considerable faster uploads (from server to client). + Works on Linux only, and only for clear-text (non FTPS) transfers. + Default: ``True`` on Linux. *New in version 0.7.0* @@ -234,7 +231,8 @@ Control connection .. data:: auth_failed_timeout The amount of time the server waits before sending a response in case of - failed authentication. + failed authentication. This is useful to prevent password-guessing attacks. + Default: ``3`` seconds. *New in version 1.5.0* @@ -267,44 +265,48 @@ Control connection .. method:: on_logout(username) - Called when user logs out due to QUIT or USER issued twice. This is not - called if client just disconnects without issuing QUIT first. + Called when user logs out due to QUIT or USER commands issued twice. This + is not called if the client just disconnects without issuing QUIT first. *New in version 0.6.0* .. method:: on_file_sent(file) - Called every time a file has been successfully sent. *file* is the - absolute name of that file. + Called when a file has been successfully sent. ``file`` is the absolute + path of that file. .. method:: on_file_received(file) - Called every time a file has been successfully received. *file* is the - absolute name of that file. + Called when a file has been successfully received. ``file`` is the + absolute path of that file. .. method:: on_incomplete_file_sent(file) - Called every time a file has not been entirely sent (e.g. transfer aborted - by client). *file* is the absolute name of that file. + Called when time a file has not been entirely sent (e.g. transfer aborted + by client). ``file`` is the absolute path of that file. *New in version 0.6.0* .. method:: on_incomplete_file_received(file) - Called every time a file has not been entirely received (e.g. transfer - aborted by client). *file* is the absolute name of that file. *New in - version 0.6.0* + Called when a file has not been entirely received (e.g. transfer + aborted by client). *file* is the absolute path of that file. + + *New in version 0.6.0* Data connection =============== .. class:: pyftpdlib.handlers.DTPHandler(sock_obj, cmd_channel) - This class handles the server-data-transfer-process (server-DTP, see `RFC-959 - `_) managing all transfer operations - regarding the data channel. *sock_obj* is the underlying socket object - instance of the newly established connection, cmd_channel is the - :class:`pyftpdlib.handlers.FTPHandler` class instance. + This class handles the server-data-transfer-process (server-DTP) as defined + in `RFC-959 `_, commonly known as + "data connection". + It manages all the transfer operations like sending or receiving files and + also transmitting the directory listing. + ``sock_obj`` is the underlying socket object instance of the newly established + connection, ``cmd_channel`` is the + corresponding :class:`pyftpdlib.handlers.FTPHandler` class instance. *Changed in version 1.0.0: added ioloop argument.* @@ -312,22 +314,21 @@ Data connection 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 (default ``300`` seconds). + be kicked off. Default: ``300`` seconds. .. data:: ac_in_buffer_size .. data:: ac_out_buffer_size The buffer sizes to use when receiving and sending data (both defaulting to ``65536`` bytes). For LANs you may want this to be fairly large. Depending - on available memory and number of connected clients setting them to a lower + on available memory and number of connected clients, setting them to a lower value can result in better performances. - .. class:: pyftpdlib.handlers.ThrottledDTPHandler(sock_obj, cmd_channel) A :class:`pyftpdlib.handlers.DTPHandler` subclass which wraps sending and - receiving in a data counter and temporarily "sleeps" the channel so that you - burst to no more than x Kb/sec average. Use it instead of + receiving in a data counter, and temporarily "sleeps" the transmission of data + so that you burst to no more than x Kb/sec average. Use it instead of :class:`pyftpdlib.handlers.DTPHandler` to set transfer rates limits for both downloads and/or uploads (see the `demo script `__ @@ -335,26 +336,24 @@ Data connection .. data:: read_limit - The maximum number of bytes to read (receive) in one second (defaults to - ``0`` == no limit) + The maximum number of bytes to read (receive) in one second. Defaults to + ``0``, meaning no limit. .. data:: write_limit - The maximum number of bytes to write (send) in one second (defaults to - ``0`` == no limit). + The maximum number of bytes to write (send) in one second. Defaults to + ``0``, meaning no limit. Server (acceptor) ================= .. class:: pyftpdlib.servers.FTPServer(address_or_socket, handler, ioloop=None, backlog=100) - Creates a socket listening on *address* (an ``(host, port)`` tuple) or a - pre- existing socket object, dispatching the requests to *handler* (typically - :class:`pyftpdlib.handlers.FTPHandler` class). Also, starts the asynchronous - IO loop. *backlog* is the maximum number of queued connections passed to - `socket.listen() `_. - If a connection request arrives when the queue is full the client may raise - ECONNRESET. + Creates a socket listening on ``address`` (an ``(host, port)`` tuple) or a + pre-existing socket object, dispatching the requests to ``handler`` (typically + a :class:`pyftpdlib.handlers.FTPHandler` class). Also, it starts the main asynchronous + IO loop. ``backlog`` is the maximum number of queued connections passed to + `socket.listen() `_. *Changed in version 1.0.0: added ioloop argument.* @@ -371,7 +370,7 @@ Server (acceptor) >>> server = FTPServer(address, handler) >>> server.serve_forever() - It can also be used as a context manager. Exiting the context manager is + ``FTPServer`` can also be used as a context manager. Exiting the context manager is equivalent to calling :meth:`close_all`. >>> with FTPServer(address, handler) as server: @@ -379,61 +378,62 @@ Server (acceptor) .. data:: max_cons - Number of maximum simultaneous connections accepted (default ``512``). + The number of maximum simultaneous connections accepted by the server + (both control and data connections). Default: ``512``. .. data:: max_cons_per_ip - Number of maximum connections accepted for the same IP address (default - ``0`` == no limit). + Then number of maximum connections accepted for the same IP address. + Default: ``0``, meaning no limit. .. method:: serve_forever(timeout=None, blocking=True, handle_exit=True, worker_processes=1) Starts the asynchronous IO loop. - - (float) timeout: the timeout passed to the underlying IO + - ``timeout``: the timeout passed to the underlying IO loop expressed in seconds. - - (bool) blocking: if False loop once and then return the + - ``blocking``: if ``False`` loop once and then return the timeout of the next scheduled call next to expire soonest (if any). - - (bool) handle_exit: when True catches ``KeyboardInterrupt`` and + - ``handle_exit``: when ``True`` catches ``KeyboardInterrupt`` and ``SystemExit`` exceptions (caused by SIGTERM / SIGINT signals) and - gracefully exits after cleaning up resources. Also, logs server start and - stop. + gracefully exits after cleaning up resources. + Also, logs server start and stop. - - (int) worker_processes: pre-fork a certain number of child - processes before starting. See: :ref:`pre-fork-model`. + - ``worker_processes``: pre-forks a certain number of child + processes before starting. See: :ref:`pre-fork-model` for more info. Each child process will keep using a 1-thread, async concurrency model, handling multiple concurrent connections. - If the number is None or <= 0 the number of usable cores + If the number is ``None`` or <= ``0``, the number of usable CPUs available on this machine is detected and used. - It is a good idea to use this option in case the app risks - blocking for too long on a single function call (e.g. - hard-disk is slow, long DB query on auth etc.). + It is a good idea to use this option in case the server risks + blocking for too long on a single function call, typically if the + filesystem is slow or the are long DB query executed on user login. By splitting the work load over multiple processes the delay introduced by a blocking function call is amortized and divided - by the number of worker processes. + by the number of the worker processes. *Changed in version 1.0.0*: no longer a classmethod - *Changed in version 1.0.0*: 'use_poll' and 'count' parameters were removed + *Changed in version 1.0.0*: ``use_poll`` and ``count`` parameters were removed - *Changed in version 1.0.0*: 'blocking' and 'handle_exit' parameters were + *Changed in version 1.0.0*: ``blocking`` and ``handle_exit`` parameters were added .. method:: close() - Stop accepting connections without disconnecting currently connected - clients. :meth:`server_forever` loop will automatically stop when there are - no more connected clients. + Stop accepting connections without disconnecting the clients currently + connected. :meth:`server_forever` loop will automatically stop when the last + client disconnects. .. method:: close_all() Disconnect all clients, tell :meth:`server_forever` loop to stop and wait until it does. - *Changed in version 1.0.0: 'map' and 'ignore_all' parameters were removed.* + *Changed in version 1.0.0: ``map`` and ``ignore_all`` parameters were removed.* Filesystem ========== @@ -441,29 +441,32 @@ Filesystem .. class:: pyftpdlib.filesystems.FilesystemError Exception class which can be raised from within - :class:`pyftpdlib.filesystems.AbstractedFS` in order to send custom error - messages to client. *New in version 1.0.0* + :class:`pyftpdlib.filesystems.AbstractedFS` in order to send a custom error + messages to the client. + + *New in version 1.0.0* .. class:: pyftpdlib.filesystems.AbstractedFS(root, cmd_channel) - 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. The contructor accepts two arguments: - root which is the user "real" home directory (e.g. '/home/user') and - cmd_channel which is the :class:`pyftpdlib.handlers.FTPHandler` class - instance. + A class used to interact with the filesystem, providing a cross-platform + interface compatible with both Windows and UNIX paths. All paths use ``"/"`` + as the separator, including on Windows. ``AbstractedFS`` distinguishes + between "real" filesystem paths and "virtual" FTP paths, emulating a UNIX + chroot jail where the user can not escape his/her home directory (example: + real "/home/user" path will be seen as "/" by the client). It also provides + wrappers around all ``os.*`` calls (``mkdir``, ``rename``, etc) and ``open`` + builtin. The contructor accepts two arguments which are passed by the + ``FTPHandler``: ``root``, which is the user "real" home + directory (e.g. '/home/user') and ``cmd_channel`` which is a + :class:`pyftpdlib.handlers.FTPHandler` class instance. *Changed in version 0.6.0: root and cmd_channel arguments were added.* .. data:: root - User's home directory ("real"). *Changed in version 0.7.0: support - setattr()* + User's home directory ("real"). + + *Changed in version 0.7.0: support setattr()* .. data:: cwd @@ -473,33 +476,32 @@ Filesystem .. method:: ftpnorm(ftppath) - Normalize a "virtual" ftp pathname depending on the current working - directory (e.g. having ``"/foo"`` as current working directory ``"bar"`` - becomes ``"/foo/bar"``). + Normalize a "virtual" FTP pathname depending on the current working + directory. E.g. having ``"/foo"`` as current working directory, ``"bar"`` + is translated to ``"/foo/bar"``. .. method:: ftp2fs(ftppath) - Translate a "virtual" ftp pathname into equivalent absolute "real" - filesystem pathname (e.g. having ``"/home/user"`` as root directory - ``"foo"`` becomes ``"/home/user/foo"``). + Translate a "virtual" FTP pathname into the equivalent absolute "real" + filesystem pathname. E.g. having ``"/home/user"`` as the root directory, + ``"foo"`` is translated to ``"/home/user/foo"``. .. method:: fs2ftp(fspath) Translate a "real" filesystem pathname into equivalent absolute "virtual" - ftp pathname depending on the user's root directory (e.g. having - ``"/home/user"`` as root directory ``"/home/user/foo"`` becomes ``"/foo"``. + FTP pathname depending on the user's root directory. E.g. having + ``"/home/user"`` as root directory, ``"/home/user/foo"`` is translated to + ``"/foo"``. .. method:: validpath(path) - - Check whether the path belongs to user's home directory. Expected argument - is a "real" filesystem path. If path is a symbolic link it is resolved to - check its real destination. Pathnames escaping from user's root directory - are considered not valid (return ``False``). - + Check whether the path belongs to the 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. Resolved symlinks which escape the + user's root directory are considered not valid (return ``False``). .. method:: open(filename, mode) Wrapper around - `open() `_ builtin. + `open() `_ builtin. .. method:: mkdir(path) .. method:: chdir(path) @@ -511,8 +513,8 @@ Filesystem .. method:: lstat(path) .. method:: readlink(path) - Wrappers around corresponding - `os `_ module functions. + Wrappers around the corresponding + `os `_ module functions. .. method:: isfile(path) .. method:: islink(path) @@ -522,29 +524,27 @@ Filesystem .. method:: realpath(path) .. method:: lexists(path) - Wrappers around corresponding - `os.path `_ module functions. + Wrappers around the corresponding + `os.path `_ module functions. .. method:: mkstemp(suffix='', prefix='', dir=None, mode='wb') Wrapper around - `tempfile.mkstemp `_. + `tempfile.mkstemp `_. .. method:: listdir(path) Wrapper around - `os.listdir `_. + `os.listdir `_. It is expected to return a list of strings or a generator yielding strings. .. versionchanged:: 1.6.0 can also return a generator. - Extended classes ================ - We are about to introduces are extensions (subclasses) of the ones explained - so far. They usually require third-party modules to be installed separately - or are specific for a given Python version or operating system. + Classes that require third-party modules to be installed separately, or a + specific to a given operating system. Extended handlers ----------------- @@ -552,92 +552,97 @@ Extended handlers .. class:: pyftpdlib.handlers.TLS_FTPHandler(conn, server) A :class:`pyftpdlib.handlers.FTPHandler` subclass implementing FTPS (FTP over - SSL/TLS) as described in `RFC-4217 `_ - implementing AUTH, PBSZ and PROT commands. - `PyOpenSSL `_ module is required to be - installed. Example below shows how to setup an FTPS server. Configurable - attributes: + SSL/TLS) as described in `RFC-4217 `_. + Implements AUTH, PBSZ and PROT commands. + `PyOpenSSL `_ module is required to be + installed. See :ref:`ftps-server` tutorial. + Configurable attributes: .. data:: certfile 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. + local side of the connection. This must always be specified, unless + a :ref`:`ssl_context` is provided instead. See :ref:`ftps-server` on how to + generate SSL certificates. Default: ``None``. .. data:: keyfile - The path of the file containing the private RSA key; can be omittetted if - certfile already contains the private key (defaults: ``None``). + The path of the file containing the private RSA key. It can be omittetted + if the :ref`:`certfile` already contains the private key. + See :ref:`ftps-server` on how to generate SSL certificates. + Default: ``None``. .. data:: ssl_protocol The desired SSL protocol version to use. This defaults to - `TLS_SERVER_METHOD`, which at the time of writing (year 2024) includes + ``TLS_SERVER_METHOD``, which at the time of writing (year 2024) includes TLSv1, TLSv1.1, TLSv1.2 and TLSv1.3. The actual protocol version used will be negotiated to the highest version mutually supported by the client and - the server. + the server when the client connects. - .. versionchanged:: 2.0.0 set default to `TLS_SERVER_METHOD` + .. versionchanged:: 2.0.0 set default to ``TLS_SERVER_METHOD`` .. data:: ssl_options - specific OpenSSL options. These default to: `OP_NO_SSLv2 | OP_NO_SSLv3 | - OP_NO_COMPRESSION`, which are all considered unsecure settings. Can be set - to None in order to improve compatibilty with older (insecure) FTP - clients. + Specific OpenSSL options. This defaults to: ``OP_NO_SSLv2 | OP_NO_SSLv3 | + OP_NO_COMPRESSION``, which are all considered unsecure settings. It can be + set to ``None`` in order to improve compatibilty with older (insecure) FTP + clients (not recommended). .. versionadded:: 1.6.0 .. data:: ssl_context - A `SSL.Context `__ + A `SSL.Context `__ instance which was previously configured. - If specified :data:`ssl_protocol` and :data:`ssl_options` parameters will - be ignored. + When specified, :data:`ssl_protocol` and :data:`ssl_options` parameters + are ignored. .. data:: 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``). + If ``True`` it requires the client to secure the control connection with + TLS before logging in. This means the client will have to issue the AUTH + command before USER and PASS. Default: ``False``. .. data:: 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``). + If ``True`` it requires the client to secure the data connection with TLS + before logging in. This means the clie will have to issue the PROT command + before PASV or PORT. Default: ``False``. Extended authorizers -------------------- .. class:: pyftpdlib.authorizers.UnixAuthorizer(global_perm="elradfmwMT", allowed_users=None, rejected_users=None, require_valid_shell=True, anonymous_user=None, ,msg_login="Login successful.", msg_quit="Goodbye.") - Authorizer which interacts with the UNIX password database. Users are no - longer supposed to be explicitly added as when using - :class:`pyftpdlib.authorizers.DummyAuthorizer`. All FTP users are the same - defined on the UNIX 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. The user home directories will - be automatically determined when user logins. 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. - - *global_perm* is a series of letters referencing the users permissions; - defaults to "elradfmwMT" which means full read and write access for everybody - (except anonymous). *allowed_users* and *rejected_users* options expect a - list of users which are accepted or rejected for authenticating against the - FTP server; defaults both to ``[]`` (no restrictions). *require_valid_shell* - denies 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). *anonymous_user* - 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 ``None`` (anonymous access disabled). Note that in order to use - this class super user privileges are required. + An authorizer which interacts with the UNIX password database. Users are no + longer supposed to be explicitly added as when using the + :class:`pyftpdlib.authorizers.DummyAuthorizer`. All FTP users (and passwords) + are the ones already defined on the UNIX system. + The user home directory is automatically determined when user logins. + 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. + In order to use this class super user privileges (root) are required. + + ``global_perm`` is a series of letters indicating the users permissions. It + defaults to ``"elradfmwMT"`` which means full read and write access are + granted to everybody (except the anonymous user). + + ``allowed_users`` and ``rejected_users`` are a list of users which are + accepted or rejected for authenticating against the FTP server. Both + parameters default to to ``[]`` (no restrictions). + + ``require_valid_shell`` denies 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``, meaning a valid shell + is required for login). + + ``anonymous_user`` 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. It defaults to ``None``, meaning anonymous + access is disabled. *New in version 0.6.0* @@ -655,9 +660,9 @@ Extended authorizers .. class:: pyftpdlib.authorizers.WindowsAuthorizer(global_perm="elradfmwMT", allowed_users=None, rejected_users=None, anonymous_user=None, anonymous_password="", msg_login="Login successful.", msg_quit="Goodbye.") Same as :class:`pyftpdlib.authorizers.UnixAuthorizer` except for - *anonymous_password* argument which must be specified when defining the - *anonymous_user*. Also requires_valid_shell option is not available. In - order to use this class pywin32 extension must be installed. + ``anonymous_password`` argument which must be specified when defining the + ``anonymous_user``. Also, ``requires_valid_shell`` option is not available. In + order to use this class ``pywin32`` extension must be installed. *New in version 0.6.0* diff --git a/docs/benchmarks.rst b/docs/benchmarks.rst index c99e98eb..e1dc63b1 100644 --- a/docs/benchmarks.rst +++ b/docs/benchmarks.rst @@ -135,37 +135,29 @@ Memory usage Interpreting the results ------------------------ -pyftpdlib and `proftpd `__ / `vsftpd `__ -look pretty much equally fast. The huge difference is noticeable in scalability -though, because of the concurrency model adopted. -Both proftpd and vsftpd spawn a new process for every connected client, where -pyftpdlib doesn't (see `the C10k problem `__). -The outcome is well noticeable on connect/login benchmarks and memory -benchmarks. - -The huge differences between -`0.7.0 `__ and -`1.0.0 `__ -versions of pyftpdlib are due to fix of issue 203. -On Linux we now use `epoll() `__ which scales -considerably better than `select() `__. -The fact that we're downloading a file with 300 idle clients doesn't make any -difference for *epoll()*. We might as well had 5000 idle clients and the result -would have been the same. -On Windows, where we still use select(), 1.0.0 still wins hands down as the -asyncore loop was reimplemented from scratch in order to support fd -un/registration and modification -(see `issue 203 `__). -All the benchmarks were conducted on a Linux Ubuntu 12.04 Intel core duo - 3.1 -Ghz box. +pyftpdlib, `proftpd`_ and `vsftpd`_ look pretty much equally fast. The huge +difference is noticeable in scalability though, because of the concurrency +model adopted. Proftpd and vsftpd spawn a new process for every connected +client, whereas pyftpdlib doesn't (see `the C10k problem`_). The difference +can be noticed on connect/login benchmarks and memory benchmarks. + +The huge differences between 0.7.0 and 1.0.0 versions of pyftpdlib are due to +fix of `issue 203`_ . On Linux we now use `epoll()`_ which scales considerably +better than `select()`_. The fact that we're downloading a file with 300 idle +clients doesn't make any difference for `epoll()`. We might as well had 5000 +idle clients and the result would have been the same. On Windows, where we +still use select(), 1.0.0 still wins hands down as the asyncore loop was +reimplemented from scratch in order to support fd un/registration and +modification. Benchmarks were conducted on Linux Ubuntu 12.04, Intel core duo - +3.1 Ghz box. Setup ----- The following setup was used before running every benchmark: -proftpd -^^^^^^^ +proftpd config +^^^^^^^^^^^^^^ :: @@ -181,8 +173,8 @@ proftpd $ sudo service proftpd restart -vsftpd -^^^^^^ +vsftpd config +^^^^^^^^^^^^^ :: @@ -259,8 +251,7 @@ The following patch was applied first: $ sudo python3 demo/unix_daemon.py -The `benchmark script `__ -was run as: +The `benchmark script`_ was run as: :: @@ -272,3 +263,11 @@ was run as: :: python3 scripts/ftpbench -u USERNAME -p PASSWORD -b all -n 300 -k FTP_SERVER_PID + +.. _`benchmark script`: https://github.com/giampaolo/pyftpdlib/blob/master/scripts/ftpbench +.. _`epoll()`: https://linux.die.net/man/4/epoll +.. _`issue 203`: https://github.com/giampaolo/pyftpdlib/issues/203 +.. _`proftpd`: http://www.proftpd.org/ +.. _`select()`: https://linux.die.net/man/2/select +.. _`the C10k problem`: http://www.kegel.com/c10k.html +.. _`vsftpd`: https://security.appspot.com/vsftpd.html diff --git a/docs/conf.py b/docs/conf.py index b2abbb5d..cd530779 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -2,381 +2,28 @@ # Use of this source code is governed by MIT license that can be # found in the LICENSE file. -# pyftpdlib documentation build configuration file, created by -# sphinx-quickstart on Wed Oct 19 21:54:30 2016. +# Configuration file for the Sphinx documentation builder. # -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ------------------------------------------------ - -import ast -import datetime -import os - - -PROJECT_NAME = "pyftpdlib" -AUTHOR = "Giampaolo Rodola" -THIS_YEAR = str(datetime.datetime.now().year) -HERE = os.path.abspath(os.path.dirname(__file__)) +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -def get_version(): - INIT = os.path.abspath( - os.path.join(HERE, '..', 'pyftpdlib', '__init__.py') - ) - with open(INIT) as f: - for line in f: - if line.startswith('__ver__'): - ret = ast.literal_eval(line.strip().split(' = ')[1]) - assert ret.count('.') == 2, ret - for num in ret.split('.'): - assert num.isdigit(), ret - return ret - else: - raise ValueError("couldn't find version string") +project = 'pyftpdlib' +copyright = '2007, Giampaolo Rodola' +author = 'Giampaolo Rodola' +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -VERSION = get_version() +extensions = [] -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.coverage', - 'sphinx.ext.imgmath', - 'sphinx.ext.viewcode', - 'sphinx.ext.intersphinx', -] - -# Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The encoding of source files. -# -# source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = PROJECT_NAME -copyright = f'2009-{THIS_YEAR}, {AUTHOR}' -author = AUTHOR - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = VERSION -# The full version, including alpha/beta/rc tags. -release = VERSION - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -# -# today = '' -# -# Else, today_fmt is used as the format for a strftime call. -# -# today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] -# The reST default role (used for this markup: `text`) to use for all -# documents. -# -# default_role = None -# If true, '()' will be appended to :func: etc. cross-reference text. -# -# add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -# -# add_module_names = True +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# -# show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -# modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -# keep_warnings = False - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# html_theme = 'sphinx_rtd_theme' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -# html_theme_path = [] - -# The name for this set of Sphinx documents. -# " v documentation" by default. -# -# html_title = u'pyftpdlib v1.0' - -# A shorter title for the navigation bar. Default is the same as html_title. -# -# html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# -# html_logo = None - -# The name of an image file (relative to this directory) to use as a favicon of -# the docs. This file should be a Windows icon file (.ico) being 16x16 or -# 32x32 pixels large. - - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -# html_static_path = ['_static'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -# -# html_extra_path = [] - -# If not None, a 'Last updated on:' timestamp is inserted at every page -# bottom, using the given strftime format. -# The empty string is equivalent to '%b %d, %Y'. -# -# html_last_updated_fmt = None - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -# -# html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -# -# html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -# -# html_additional_pages = {} - -# If false, no module index is generated. -# -# html_domain_indices = True - -# If false, no index is generated. -# -# html_use_index = True - -# If true, the index is split into individual pages for each letter. -# -# html_split_index = False - -# If true, links to the reST sources are added to the pages. -# -# html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -# -# html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -# -# html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# -# html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' -# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' -# -# html_search_language = 'en' - -# A dictionary with options for the search language support, empty by default. -# 'ja' uses this config value. -# 'zh' user can custom change `jieba` dictionary path. -# -# html_search_options = {'type': 'default'} - -# The name of a javascript file (relative to the configuration directory) that -# implements a search results scorer. If empty, the default will be used. -# -# html_search_scorer = 'scorer.js' - -# Output file base name for HTML help builder. -htmlhelp_basename = f'{PROJECT_NAME}-doc' - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'pyftpdlib.tex', 'pyftpdlib Documentation', AUTHOR, 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -# -# latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# -# latex_use_parts = False - -# If true, show page references after internal links. -# -# latex_show_pagerefs = False - -# If true, show URL addresses after external links. -# -# latex_show_urls = False - -# Documents to append as an appendix to all manuals. -# -# latex_appendices = [] - -# It false, will not define \strong, \code, itleref, \crossref ... but only -# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added -# packages. -# -# latex_keep_old_macro_names = True - -# If false, no module index is generated. -# -# latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [(master_doc, 'pyftpdlib', 'pyftpdlib Documentation', [author], 1)] - -# If true, show URL addresses after external links. -# -# man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ( - master_doc, - 'pyftpdlib', - 'pyftpdlib Documentation', - author, - 'pyftpdlib', - 'One line description of project.', - 'Miscellaneous', - ), -] - -# Documents to append as an appendix to all manuals. -# -# texinfo_appendices = [] - -# If false, no module index is generated. -# -# texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -# -# texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -# -# texinfo_no_detailmenu = False - - -html_context = { - 'css_files': [ - 'https://media.readthedocs.org/css/sphinx_rtd_theme.css', - 'https://media.readthedocs.org/css/readthedocs-doc-embed.css', - # '_static/css/custom.css', - ], -} diff --git a/docs/faqs.rst b/docs/faqs.rst index a10ed8d1..a7aa2f62 100644 --- a/docs/faqs.rst +++ b/docs/faqs.rst @@ -11,66 +11,27 @@ What is pyftpdlib? ------------------ pyftpdlib is a high-level library to easily write asynchronous portable FTP -servers with `Python `__. +servers with `Python`_. I'm not a python programmer. Can I use it anyway? ------------------------------------------------- -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: +Yes. Pyftpdlib is a fully working FTP server implementation that can be run +"as is". For example, you could run an anonymous FTP server with write access +from command line by running: .. code-block:: sh - $ sudo python3 -m pyftpdlib - [I 13-02-20 14:16:36] >>> starting FTP server on 0.0.0.0:8021 <<< + $ python3 -m pyftpdlib -w + RuntimeWarning: write permissions assigned to anonymous user. + [I 13-02-20 14:16:36] >>> starting FTP server on 0.0.0.0:2121 <<< [I 13-02-20 14:16:36] poller: [I 13-02-20 14:16:36] masquerade (NAT) address: None [I 13-02-20 14:16:36] passive ports: None [I 13-02-20 14:16:36] use sendfile(2): True 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 ``python3 -m pyftpdlib --help`` -to see all available options). Examples: - -Anonymous FTP server with write access: - -.. code-block:: sh - - $ sudo python3 -m pyftpdlib -w - ~pyftpdlib-1.3.1-py2.7.egg/pyftpdlib/authorizers.py:265: RuntimeWarning: write permissions assigned to anonymous user. - [I 13-02-20 14:16:36] >>> starting FTP server on 0.0.0.0:8021 <<< - [I 13-02-20 14:16:36] poller: - [I 13-02-20 14:16:36] masquerade (NAT) address: None - [I 13-02-20 14:16:36] passive ports: None - [I 13-02-20 14:16:36] use sendfile(2): True - -Listen on a different ip/port: - -.. code-block:: sh - - $ python3 -m pyftpdlib -i 127.0.0.1 -p 8021 - [I 13-02-20 14:16:36] >>> starting FTP server on 0.0.0.0:8021 <<< - [I 13-02-20 14:16:36] poller: - [I 13-02-20 14:16:36] masquerade (NAT) address: None - [I 13-02-20 14:16:36] passive ports: None - [I 13-02-20 14:16:36] use sendfile(2): True - - -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 -`demo directory `__. - -Getting help ------------- - -There's a mailing list available at: -http://groups.google.com/group/pyftpdlib/topics +without, say, installing and configuring Samba. Installing and compatibility ============================ @@ -78,8 +39,11 @@ Installing and compatibility How do I install pyftpdlib? --------------------------- -If you are not new to Python you probably don't need that, otherwise follow the -`install instructions `__. +.. code-block:: sh + + $ python3 -m pip install pyftpdlib + +Also see `install instructions`_. Which Python versions are compatible? ------------------------------------- @@ -94,25 +58,14 @@ with: .. code-block:: sh - python3 -m pip install pyftpdlib==1.5.10 + python2 -m pip install pyftpdlib==1.5.10 On which platforms can pyftpdlib be used? ----------------------------------------- pyftpdlib should work on any platform where **select()**, **poll()**, -**epoll()** or **kqueue()** system calls are available. -The development team has mainly tested it under various *Linux*, *Windows*, -*OSX* and *FreeBSD* systems. -For FreeBSD is also available a -`pre-compiled package `__ -maintained by Li-Wen Hsu (lwhsu@freebsd.org). -Other Python implementation like -`PythonCE `__ are known to work with -pyftpdlib and every new version is usually tested against it. -pyftpdlib currently does not work on `Jython `__ -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. +**epoll()** or **kqueue()** system calls are available, namely UNIX and +Windows. Usage ===== @@ -121,13 +74,11 @@ How can I run long-running tasks without blocking the server? ------------------------------------------------------------- pyftpdlib is an *asynchronous* 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. +time consuming task you have to use a separate Python process or thread, +otherwise the entire asynchronous loop will be blocked. 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. - +receives a file. The code snippet below shows how. With ``self.del_channel()`` we temporarily "sleep" the connection handler which will be removed from the async IO poller loop and won't be able to send or receive any more data. It won't be closed (disconnected) as long as we don't @@ -146,106 +97,66 @@ avoid race conditions, dead locks etc. self.del_channel() threading.Thread(target=blocking_task).start() -Another possibility is to -`change the default concurrency model `__. +Another possibility is to `change the default concurrency model`_. Why do I get "Permission denied" error on startup? -------------------------------------------------- -Probably because you're on a Unix system and you're trying to start ftpd as an -unprivileged user. FTP servers bind 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. - -How can I prevent the server version from being displayed? ----------------------------------------------------------- +Probably because you're on a UNIX system and you're trying to start the FTP +server as an unprivileged user. FTP servers bind on port 21 by default, and +only the root user can bind sockets on such ports. If you want to bind the +socket as non-privileged user you should set a port higher than 1024. -Just modify `FTPHandler.banner `__. - -Can control upload/download ratios? ------------------------------------ +Can I control upload/download ratios? +------------------------------------- -Yes. Starting from version 0.5.2 pyftpdlib provides a new class called -`ThrottledDTPHandler `__. -You can set speed limits by modifying -`read_limit `__ -and -`write_limit `__ -class attributes as it is shown in -`throttled_ftpd.py `__ -demo script. +Yes. Pyftpdlib provides a new class called `ThrottledDTPHandler`_. You can set +speed limits by modifying `ThrottledDTPHandler.read_limit`_ and +`ThrottledDTPHandler.write_limit`_ class attributes as it is shown in +`demo/throttled_ftpd.py`_ script. Are there ways to limit connections? ------------------------------------ -`FTPServer `__. class comes with two -overridable attributes defaulting to zero -(no limit): `max_cons `__, -which sets a limit for maximum simultaneous -connection to handle by ftpd and -`max_cons_per_ip `__ -which set a limit for connections from the same IP address. Overriding these -variables is always recommended to avoid DoS attacks. +The `FTPServer`_. class comes with two overridable attributes defaulting to +zero (no limit): `FTPServer.max_cons`_, which sets a limit for maximum +simultaneous connection to handle, and `FTPServer.max_cons_per_ip`_ which sets +a limit for the connections from the same IP address. I'm behind a NAT / gateway -------------------------- -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 -`masquerade_address `__ -attribute of `FTPHandler `__ -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 -`passive_ports `__ -attribute of `FTPHandler `__ -class to control what ports pyftpdlib will use for its passive data transfers. -Value expected by `passive_ports `__ -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 -`passive_ports `__ option -which allows you to define multiple 1 to 1 mappings (**New in 0.6.0**). - -What is FXP? ------------- +The FTP protocol uses 2 TCP connections: a "control" connection to exchange +protocol messages (LIST, RETR, etc.), and a "data" connection for transfering +data (files). In order to open the data connection the FTP server must +communicate its **public** IP address in the PASV response. If you're behind a +NAT, this address must be explicitly configured by setting the +`FTPHandler.masquerade_address`_ attribute. -FXP is part of the name of a popular Windows FTP client: -`http://www.flashfxp.com `__. 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 `RFC-959 `__. More info can be found -here: `http://www.proftpd.org/docs/howto/FXP.html -`__. - -Does pyftpdlib support FXP? ---------------------------- +You can get your public IP address by using services like +https://www.whatismyip.com/. + +In addition, you also probably want to configure a given range of TCP ports for +such incoming "data" connections, otherwise a random TCP port will be picked up +every time. You can do so by using the `FTPHandler.passive_ports`_ attribute. +The value expected by `FTPHandler.passive_ports`_ attribute is a list of +integers (e.g. ``range(60000, 65535)``). -Yes. It is disabled by default for security reasons (see -`RFC-2257 `__ and -`FTP bounce attack description `__) -but in case you want to enable it just set to True the -`permit_foreign_addresses `__ -attribute of `FTPHandler `__ class. +This also means that you must configure your router so the it will forward the +incoming connections to such TCP ports from the router to your FTP server +behind the NAT. Why timestamps shown by MDTM and ls commands (LIST, MLSD, MLST) are wrong? -------------------------------------------------------------------------- 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 -`GMT times `__ as recommended -in `RFC-3659 `__. -In case you want such commands to report local times instead just set the -`use_gmt_times `__ attribute to ``False``. -For further information you might want to take a look at -`this `__ +machine", then that is the expected behavior. pyftpdlib uses `GMT times`_ as +recommended in `RFC-3659`_. Any client complying with RFC-3659 should be able +to convert the GMT time to your local time and show the correct timestamp. In +case you want LIST, MLSD, MLST commands to report local times instead, just set +the `FTPHandler.use_gmt_times`_ attribute to ``False``. For further information +you might want to take a look at +http://www.proftpd.org/docs/howto/Timestamps.html. Implementation ============== @@ -253,16 +164,16 @@ Implementation sendfile() ---------- -Starting from version 0.7.0, sendfile(2) system call be used when uploading -files (from server to client) via RETR command. -Using sendfile(2) usually results in transfer rates from 2x to 3x faster -and less CPU usage. -Note: use of sendfile() might introduce some unexpected issues with "non -regular filesystems" such as NFS, SMBFS/Samba, CIFS and network mounts in -general, see: http://www.proftpd.org/docs/howto/Sendfile.html. If you bump into -one this problems the fix consists in disabling sendfile() usage via -`FTPHandler.use_sendfile `__ -option: +On Linux, and only when doing transfer in clear text (aka no FTPS), the +``sendfile(2)`` system call be used when uploading files (from server to +client) via RETR command. Using ``sendfile(2)`` is more efficient, and usually +results in transfer rates that are from 2x to 3x faster. + +In the past some cases were reported that using ``sendfile(2)`` with "non +regular" filesystems such as NFS, SMBFS/Samba, CIFS or network mounts in +general may cause some issues, see +http://www.proftpd.org/docs/howto/Sendfile.html. If you bump into one these +issues you can set `FTPHandler.use_sendfile`_ to ``False``: .. code-block:: python @@ -274,40 +185,36 @@ option: Globbing / STAT command implementation -------------------------------------- -Globbing is a common Unix shell mechanism for expanding wildcard patterns to +Globbing is a common UNIX shell mechanism for expanding wildcard patterns to match multiple filenames. When an argument is provided to the *STAT* command, -ftpd should return directory listing over the command channel. -`RFC-959 `__ 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 *does not* support globbing. +the FTP server should return a directory listing over the command channel. +`RFC-959`_ 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 (and slow) pyftpdlib *does not* support globbing. ASCII transfers / SIZE command implementation --------------------------------------------- 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 -`RFC-3659 `__. +(file.read().replace(os.linesep, '\r\n')), and then calculating the length of +such data which may be different than the actual size of the file on the +server. Considering that calculating such a result could be resource-intensive, +it could be easy for a malicious client to use this as a DoS attack. As such +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 `RFC-3659`_. IPv6 support ------------ -Starting from version 0.4.0 pyftpdlib *supports* IPv6 -(`RFC-2428 `__). If you use IPv6 and want -your FTP daemon to do so just pass a valid IPv6 address to the -`FTPServer `__ class constructor. -Example: +Pyftpdlib does support IPv6 (`RFC-2428`_). If you want your FTP server to +explicitly use IPv6 you can do so by passing a valid IPv6 address to the +`FTPServer`_ class constructor. Example: .. code-block:: python @@ -317,48 +224,39 @@ Example: >>> ftpd.serve_forever() Serving FTP on ::1:21 -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 (**New in 0.6.0**). - -How do I install IPv6 support on my system? -------------------------------------------- - -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. -On Windows (XP SP2 and higher) run "netsh int ipv6 install". Again, you should -be able to use IPv6 loopback afterwards. +If the OS supports an hybrid dual-stack IPv6/IPv4 implementation (e.g. Linux), +the code above will automatically listen on both IPv4 and IPv6 by using the +same TCP socket. Can pyftpdlib be integrated with "real" users existing on the system? --------------------------------------------------------------------- -Yes. Starting from version 0.6.0 pyftpdlib provides the new `UnixAuthorizer `__ -and `WindowsAuthorizer `__ 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 +Yes. See `UnixAuthorizer`_ and `WindowsAuthorizer`_ classes. By using them you +can authenticate to the FTP server by using the credentials of the users +defined on the operating system + +Furthermore: every time the FTP server accesses 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 -`demo directory `__ -shows how to use `UnixAuthorizer `__ and `WindowsAuthorizer `__ classes. +the user who originally started the server. It will do so by setting the +effective user or group ID of the current process. That means that you probably +want to run the FTP as root. See: + +* https://github.com/giampaolo/pyftpdlib/blob/master/demo/unix_ftpd.py +* https://github.com/giampaolo/pyftpdlib/blob/master/demo/win_ftpd.py Does pyftpdlib support FTP over TLS/SSL (FTPS)? ----------------------------------------------- -Yes, starting from version 0.6.0, see: -`Does pyftpdlib support FTP over TLS/SSL (FTPS)?`_ +Yes. Checkout `TLS_FTPHandler`_. What about SITE commands? ------------------------- The only supported SITE command is *SITE CHMOD* (change file mode). The user willing to add support for other specific SITE commands has to define a new -``ftp_SITE_CMD`` method in the -`FTPHandler `__ subclass and add a new -entry in ``proto_cmds`` dictionary. Example: +``ftp_SITE_CMD`` method in the `FTPHandler`_ subclass and add a new entry in +``proto_cmds`` dictionary. Example: .. code-block:: python @@ -377,3 +275,26 @@ entry in ``proto_cmds`` dictionary. Example: """Recursively remove a directory tree.""" # implementation here # ... + +.. _`change the default concurrency model`: tutorial.html#changing-the-concurrency-model +.. _`demo/throttled_ftpd.py`: https://github.com/giampaolo/pyftpdlib/blob/master/demo/throttled_ftpd.py +.. _`FTPHandler.masquerade_address`: api.html#pyftpdlib.handlers.FTPHandler.masquerade_address +.. _`FTPHandler.passive_ports`: api.html#pyftpdlib.handlers.FTPHandler.passive_ports +.. _`FTPHandler.use_gmt_times`: api.html#pyftpdlib.handlers.FTPHandler.use_gmt_times +.. _`FTPHandler.use_sendfile`: api.html#pyftpdlib.handlers.FTPHandler.use_sendfile +.. _`FTPHandler`: api.html#pyftpdlib.handlers.FTPHandler +.. _`FTPServer.max_cons_per_ip`: api.html#pyftpdlib.servers.FTPServer.max_cons_per_ip +.. _`FTPServer.max_cons`: api.html#pyftpdlib.servers.FTPServer.max_cons +.. _`FTPServer`: api.html#pyftpdlib.servers.FTPServer +.. _`GMT times`: https://en.wikipedia.org/wiki/Greenwich_Mean_Time +.. _`install instructions`: install.html +.. _`Python`: https://www.python.org/ +.. _`RFC-2428`: https://datatracker.ietf.org/doc/html/rfc2428 +.. _`RFC-3659`: https://datatracker.ietf.org/doc/html/rfc3659 +.. _`RFC-959`: https://datatracker.ietf.org/doc/html/rfc959 +.. _`ThrottledDTPHandler.read_limit`: api.html#pyftpdlib.handlers.ThrottledDTPHandler.read_limit +.. _`ThrottledDTPHandler.write_limit`: api.html#pyftpdlib.handlers.ThrottledDTPHandler.write_limit +.. _`ThrottledDTPHandler`: api.html#pyftpdlib.handlers.ThrottledDTPHandler +.. _`TLS_FTPHandler`: api.html#pyftpdlib.handlers.TLS_FTPHandler +.. _`UnixAuthorizer`: api.html#pyftpdlib.authorizers.UnixAuthorizer +.. _`WindowsAuthorizer`: api.html#pyftpdlib.authorizers.WindowsAuthorizer diff --git a/docs/images/adcast.png b/docs/images/adcast.png new file mode 100644 index 0000000000000000000000000000000000000000..01c899f73c6837046ba05ad089429a42945aa571 GIT binary patch literal 6769 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ξoOS^y`P{`G*bYvjK zfCeE^p*I=N2~?p4+%{Z>w>Q-^E?)+$BgQ2Zh5S%q{uR$^so>WdotSf zxTR^jRoA4mE#w#2+$Pw5Qgj$d0%vS7c8EnnAP+hN?eo-${lackYtRUkgoOyCe!ij6 z;gfx3A=-MnMiqcy+jZ((z33Fr{rA&k;oe7ZUB$%2Z<2h$0Tp%jEF}iV6=k5t)Ktn1 zP~~6v?_>-=-!Tz)D3CD=3+WPv!-x4(jNn2j$K}hZ!<;+!OTW?yOx*cELOgMT3aRXD z{#r()&LvAI6riO-LcSu`JAsKi7ElSu5fIwBxtw2CVPei4y0T%>B68PVW0r0cnE09n zg!b{{bb2BuhZb#l6}c%>XkCBlQo0^4Jp8Nq4JI(*ra)2vD;zvXCnu|__>VxU)wIU2 zem#|tfWnujA15$z=K~J>{CO%Azzt1J{K{-(Bz=ey;&Jg}TDAK^Dozuaa2B9zi-p!X z&{kc|wfSweV5}Fa>asGT-g>?JR0Zw3I4AIj=*95)s z2Ayk#jNiDCP8TVaZomOl0yHB$Bya#$C@JBrACOAvqO!O+>LkGmkd0pxN^lzApqiD{ zN~ctwdycd7-h1h~#c}))DfEF}Pv>8Xis;h`g@u$7*eXFHp}(yY989Mn($eU5XMVwG z0u#3pM~{+KtDNkA=_UT}WH=4n2M7Xe8V#N3zjB2_;o7yKyg+R8c~FShU>_I|;2+j< z(QihGzBwepdk&F}6&@zg{h$GZfo?h-{n16_8khzAy?&i8?_dXVP(%cscSuU2(+~*> z6bE1D)bL+}31oMJ!TDP`IrLFUs6S`|$j_D*dL;f~0Kr?(78*+X9!voD7#~j;dLcUi z7Wn(sHjopTa0sZploTgRSc&byWw;M?e`hDP486Vdkr?zbjtA{46x14^*C9pmkDniX z7k{1oSjT@!e1r4^n1A(Z&azMjE60yhPY5(H&$u$Oz`jj{LwJ01z6$tY*DlU)awGA- z{We{k0iKY_sQUmhV8j1#4HJO)=JNy-Pzf)*K-Y;aTSkBDg7@PH6X8PqzW@UO4?K=& TUo@`H00000NkvXXu0mjfSa}uU literal 0 HcmV?d00001 diff --git a/docs/images/debian.png b/docs/images/debian.png deleted file mode 100644 index f6edaf0764221ea9a6d69b0825b8bd1b607f845a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8915 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 diff --git a/docs/images/farmanager.png b/docs/images/farmanager.png new file mode 100644 index 0000000000000000000000000000000000000000..ab2486738ad07e2cc0f82303961d84e7db8ad3c9 GIT binary patch literal 7482 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 U-!r`=40-@6aD+n13$uWK0fF*oKmY&$ diff --git a/docs/images/google-pages.gif b/docs/images/google-pages.gif new file mode 100644 index 0000000000000000000000000000000000000000..b5a518b55af2b6ea9369053d0b625a22c33101b1 GIT binary patch literal 4731 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 O1Lmy@b^8d?)c*l>YQw$& literal 0 HcmV?d00001 diff --git a/docs/images/peerscape.gif b/docs/images/peerscape.gif new file mode 100644 index 0000000000000000000000000000000000000000..1ffd55b6b47fe010cf582e1808e075d424cf0786 GIT binary patch literal 3297 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 literal 0 HcmV?d00001 diff --git a/docs/images/pyfilesystem.svg b/docs/images/pyfilesystem.svg new file mode 100644 index 00000000..5769e9fd --- /dev/null +++ b/docs/images/pyfilesystem.svg @@ -0,0 +1 @@ +Artboard 1 \ No newline at end of file diff --git a/docs/images/rackspace-cloud-hosting.jpg b/docs/images/rackspace-cloud-hosting.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d6380bf7922bcf18b39171423104224dabb91d7a GIT binary patch literal 12810 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 Va6Z44fcV~N)W53i+nBs8{4Z7~z%Bp) literal 0 HcmV?d00001 diff --git a/docs/images/symbianftp.png b/docs/images/symbianftp.png new file mode 100644 index 0000000000000000000000000000000000000000..7376a90e2f24ecb0bc23efadf8a43d19706f53d2 GIT binary patch literal 3695 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 N002ovPDHLkV1kI842S># literal 0 HcmV?d00001 diff --git a/docs/index.rst b/docs/index.rst index 0ef233d3..f60f5734 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,7 +7,7 @@ Welcome to pyftpdlib's documentation ==================================== -If you're in a hurry just skip to the `Tutorial `__. +If you're in a hurry just read the `Tutorial `__. .. toctree:: :maxdepth: 2 diff --git a/docs/install.rst b/docs/install.rst index 9ec32f0b..26ed02cc 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -5,7 +5,7 @@ By using pip: .. code-block:: sh - $ pip install pyftpdlib + $ pip3 install pyftpdlib From sources: @@ -26,9 +26,19 @@ You might want to run tests to make sure pyftpdlib works: Additional dependencies ----------------------- -`PyOpenSSL `__, to support -`FTPS `__: +`PyOpenSSL`_, to support `FTPS`_: .. code-block:: sh - $ pip install PyOpenSSL + $ pip3 install PyOpenSSL + +`pywin32`_ if you want to use `WindowsAuthorizer`_ on Windows: + +.. code-block:: sh + + $ pip3 install pypiwin32 + +.. _`FTPS`: https://pyftpdlib.readthedocs.io/en/latest/tutorial.html#ftps-ftp-over-tls-ssl-server +.. _`PyOpenSSL`: https://pypi.org/project/pyOpenSSL +.. _`WindowsAuthorizer`: api.html#pyftpdlib.authorizers.UnixAuthorizer +.. _`pywin32`: https://pypi.org/project/pywin32/ diff --git a/docs/rfc-compliance.rst b/docs/rfc-compliance.rst index 475a5bba..48eb7df8 100644 --- a/docs/rfc-compliance.rst +++ b/docs/rfc-compliance.rst @@ -1,30 +1,32 @@ -======================== -pyftpdlib RFC compliance -======================== +============== +RFC compliance +============== .. contents:: Table of Contents Introduction ============ -This page lists current standard Internet RFCs that define the FTP protocol. - -pyftpdlib conforms to the FTP protocol standard as defined in `RFC-959 `__ and `RFC-1123 `__ implementing all the fundamental commands and features described in them. It also implements some more recent features such as OPTS and FEAT commands (`RFC-2398 `__), EPRT and EPSV commands covering the IPv6 support (`RFC-2428 `__) and MDTM, MLSD, MLST and SIZE commands defined in `RFC-3659 `__. - -Future plans for pyftpdlib include the gradual implementation of other standards track RFCs. - -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 (`RFC-4217 `__) are now implemented as a `demo script `__, waiting to reach the proper level of stability to be then included in the standard code base. +This page lists the standard Internet RFCs that define the FTP protocol. +pyftpdlib conforms to the FTP protocol standard as defined in `RFC-959`_ and +`RFC-1123`_, implementing all the fundamental commands and features described +in them. It also implements some more (relatively) recent features such as OPTS +and FEAT commands (`RFC-2398`_), EPRT and EPSV commands to implement IPv6 +support (`RFC-2428`_) and MDTM, MLSD, MLST and SIZE commands defined in +`RFC-3659`_ that standardize directory listing. TLS/SSL support (FTPS) as +defined in `RFC-4217`_ is also implemented. Some features like ACCT or SMNT +commands are deliberately not implemented. RFC-959 - File Transfer Protocol ================================ The base specification of the current File Transfer Protocol. +- `RFC-959`_ - Issued: October 1985 - Status: STANDARD -- Obsoletes: `RFC-765 `__ -- Updated by: `RFC-1123 `__, `RFC-2228 `__, `RFC-2640 `__, `RFC-2773 `__ -- `Link `__ +- Obsoletes: `RFC-765`_ +- Updated by: `RFC-1123`_, `RFC-2228`_, `RFC-2640`_, `RFC-2773`_ +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | *Command* | *Implemented* | *Milestone* | *Description* | *Notes* | @@ -99,11 +101,12 @@ The base specification of the current File Transfer Protocol. RFC-1123 - Requirements for Internet Hosts ========================================== -Extends and clarifies some aspects of `RFC-959 `__. Introduces new response codes 554 and 555. +Extends and clarifies some aspects of `RFC-959`_. Introduces new response codes +554 and 555. +- `RFC-1123`_ - Issued: October 1989 - Status: STANDARD -- `Link `__ +--------------------------------------+---------------+-------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------+ | *Feature* | *Implemented* | *Milestone* | *Description* | *Notes* | @@ -116,7 +119,7 @@ Extends and clarifies some aspects of `RFC-959 `__ has been declared incorrect in `RFC-1123 `__ which requires 125/150 instead. | | +| Avoid 250 response type on STOU | YES | 0.2.0 | The 250 positive response indicated in `RFC-959`_ has been declared incorrect in `RFC-1123`_ which requires 125/150 instead. | | +--------------------------------------+---------------+-------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------+ | Handle "Experimental" directory cmds | YES | 0.1.0 | 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. | | +--------------------------------------+---------------+-------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------+ @@ -130,29 +133,32 @@ Extends and clarifies some aspects of `RFC-959 `__. 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. +Specifies several security extensions to the base FTP protocol defined in +`RFC-959`_. 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. +- `RFC-2228`_ - Issued: October 1997 - Status: PROPOSED STANDARD -- Updates: `RFC-959 `__ -- `Link `__ +- Updates: `RFC-959`_ +-----------+---------------+-------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | *Command* | *Implemented* | *Milestone* | *Description* | *Notes* | +===========+===============+=============+====================================+====================================================================================================================================================================================================================================+ -| AUTH | NO | --- | Authentication/Security Mechanism. | Implemented as `demo script `__ by following the `RFC=4217 `__ guide line. | +| AUTH | YES | 1.5.2 | Secure Control Connection | | +-----------+---------------+-------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| CCC | NO | --- | Clear Command Channel. | | +| CCC | NO | --- | Unsecure Control Connection | | +-----------+---------------+-------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| CONF | NO | --- | Confidentiality Protected Command. | Somewhat obsoleted by `RFC-4217 `__. | +| CONF | NO | --- | Confidentiality Protected Command. | Somewhat obsoleted by `RFC-4217`_. | +-----------+---------------+-------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| EENC | NO | --- | Privacy Protected Command. | Somewhat obsoleted by `RFC-4217 `__. | +| EENC | NO | --- | Privacy Protected Command. | Somewhat obsoleted by `RFC-4217`_. | +-----------+---------------+-------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| MIC | NO | --- | Integrity Protected Command. | Somewhat obsoleted by `RFC-4217 `__. | +| MIC | NO | --- | Integrity Protected Command. | Somewhat obsoleted by `RFC-4217`_. | +-----------+---------------+-------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| PBSZ | NO | --- | Protection Buffer Size. | Implemented as `demo script `__ by following the `RFC-4217 `__ guide line as a no-op command. | +| PBSZ | YES | 1.5.2 | Protection Buffer Size. | As per `RFC-4217`_ recommendation, basically a no-op command. | +-----------+---------------+-------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| PROT | NO | --- | Data Channel Protection Level. | Implemented as `demo script `__ by following the `RFC-4217 `__ guide line supporting only "P" and "C" protection levels. | +| PROT | YES | 1.5.2 | Data Channel Protection Level. | As per `RFC-4217`_ guide recommendation, only supports "P" and "C" protection levels. | +-----------+---------------+-------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ RFC-2389 - Feature negotiation mechanism for the File Transfer Protocol @@ -160,14 +166,14 @@ RFC-2389 - Feature negotiation mechanism for the File Transfer Protocol Introduces the new FEAT and OPTS commands. +- `RFC-2389`_ - Issued: August 1998 - Status: PROPOSED STANDARD -- `Link `__ +-----------+---------------+-------------+-----------------------------------------------------------------------------------------+---------------------------------------------------------+ | *Command* | *Implemented* | *Milestone* | *Description* | *Notes* | +===========+===============+=============+=========================================================================================+=========================================================+ -| FEAT | YES | 0.3.0 | List new supported commands subsequent `RFC-959 `__ | | +| FEAT | YES | 0.3.0 | List new supported commands subsequent `RFC-959`_ | | +-----------+---------------+-------------+-----------------------------------------------------------------------------------------+---------------------------------------------------------+ | OPTS | YES | 0.3.0 | Set options for certain commands. | MLST is the only command which could be used with OPTS. | +-----------+---------------+-------------+-----------------------------------------------------------------------------------------+---------------------------------------------------------+ @@ -175,11 +181,12 @@ Introduces the new FEAT and OPTS commands. RFC-2428 - FTP Extensions for IPv6 and NATs =========================================== -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. +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. +- `RFC-2428`_ - Issued: September 1998 - Status: PROPOSED STANDARD -- `Link `__ +-----------+---------------+-------------+-----------------------------------------------+---------+ | *Command* | *Implemented* | *Milestone* | *Description* | *Notes* | @@ -192,11 +199,13 @@ Introduces the new commands EPRT and EPSV extending FTP to enable its use over v RFC-2577 - FTP Security Considerations ====================================== -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". +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". +- `RFC-2577`_ - Issued: May 1999 - Status: INFORMATIONAL -- `Link `__ +--------------------------------------------+---------------+-------------+-------------------------------------------------------------------------------------------------------------------------------------------+---------------+ | *Feature* | *Implemented* | *Milestone* | *Description* | *Notes* | @@ -219,12 +228,13 @@ Provides several configuration and implementation suggestions to mitigate some s RFC-2640 - Internationalization of the File Transfer Protocol ============================================================= -Extends the FTP protocol to support multiple character sets, in addition to the original 7-bit ASCII. Introduces the new LANG command. +Extends the FTP protocol to support multiple character sets, in addition to the +original 7-bit ASCII. Introduces the new LANG command. +- `RFC-2640`_ - Issued: July 1999 - Status: PROPOSED STANDARD -- Updates: `RFC-959 `__ -- `Link `__ +- Updates: `RFC-959`_ +----------------------+---------------+-------------+-------------------------------------------------------------------------------------------------------------------------------+---------+ | *Feature* | *Implemented* | *Milestone* | *Description* | *Notes* | @@ -237,12 +247,13 @@ Extends the FTP protocol to support multiple character sets, in addition to the RFC-3659 - Extensions to FTP ============================ -Four new commands are added: "SIZE", "MDTM", "MLST", and "MLSD". The existing command "REST" is modified. +Four new commands are added: "SIZE", "MDTM", "MLST", and "MLSD". The existing +command "REST" is modified. +- `RFC-3659`_ - Issued: March 2007 - Status: PROPOSED STANDARD -- Updates: `RFC-959 `__ -- `Link `__ +- Updates: `RFC-959`_ +------------------------------------+---------------+-------------+------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------+ | *Feature* | *Implemented* | *Milestone* | *Description* | *Notes* | @@ -265,32 +276,57 @@ Four new commands are added: "SIZE", "MDTM", "MLST", and "MLSD". The existing c RFC-4217 - Securing FTP with TLS ================================ -Provides a description on how to implement TLS as a security mechanism to secure FTP clients and/or servers. +Provides a description on how to implement TLS as a security mechanism to +secure FTP clients and/or servers. +- `RFC-4217`_ - Issued: October 2005 - Status: STANDARD -- Updates: `RFC-959 `__, `RFC-2246 `__, `RFC-2228 `__ -- `Link `__ +- Updates: `RFC-959`_, `RFC-2246`_, `RFC-2228`_ +-----------+---------------+-------------+------------------------------------+---------------------------------------------+ | *Command* | *Implemented* | *Milestone* | *Description* | *Notes* | +===========+===============+=============+====================================+=============================================+ -| AUTH | YES | --- | Authentication/Security Mechanism. | | +| AUTH | YES | --- | Secure control connection | | +-----------+---------------+-------------+------------------------------------+---------------------------------------------+ -| CCC | NO | --- | Clear Command Channel. | | +| CCC | NO | --- | Unsecure control connection | | +-----------+---------------+-------------+------------------------------------+---------------------------------------------+ | PBSZ | YES | --- | Protection Buffer Size. | Implemented as as a no-op as recommended. | +-----------+---------------+-------------+------------------------------------+---------------------------------------------+ | PROT | YES | --- | Data Channel Protection Level. | Support only "P" and "C" protection levels. | +-----------+---------------+-------------+------------------------------------+---------------------------------------------+ +RFC-8996 - Deprecate TLS 1.0 and 1.1 +==================================== + +- `RFC-8996`_ +- Issued: March 2021 +- Status: STANDARD +- Implemented by pyftpdlib: NO (not by default). + Unofficial commands =================== -These are commands not officialy included in any RFC but many FTP servers implement them. +These are commands not officialy included in any RFC but many FTP servers +implement them. +------------+---------------+-------------+-------------------+---------+ | *Command* | *Implemented* | *Milestone* | *Description* | *Notes* | +============+===============+=============+===================+=========+ | SITE CHMOD | YES | 0.7.0 | Change file mode. | | +------------+---------------+-------------+-------------------+---------+ + +.. _`RFC-1123`: https://datatracker.ietf.org/doc/html/rfc1123 +.. _`RFC-2228`: https://datatracker.ietf.org/doc/html/rfc2228 +.. _`RFC-2246`: https://datatracker.ietf.org/doc/html/rfc2246 +.. _`RFC-2389`: https://datatracker.ietf.org/doc/html/rfc2389 +.. _`RFC-2398`: https://datatracker.ietf.org/doc/html/rfc2389 +.. _`RFC-2428`: https://datatracker.ietf.org/doc/html/rfc2428 +.. _`RFC-2577`: https://datatracker.ietf.org/doc/html/rfc2577 +.. _`RFC-2640`: https://datatracker.ietf.org/doc/html/rfc2640 +.. _`RFC-2773`: https://datatracker.ietf.org/doc/html/rfc2773 +.. _`RFC-3659`: https://datatracker.ietf.org/doc/html/rfc3659 +.. _`RFC-4217`: https://datatracker.ietf.org/doc/html/rfc4217 +.. _`RFC-765`: https://datatracker.ietf.org/doc/html/rfc765 +.. _`RFC-8996`: https://datatracker.ietf.org/doc/html/rfc8996 +.. _`RFC-959`: https://datatracker.ietf.org/doc/html/rfc959 diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 1d8d4012..34d9ee61 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -5,21 +5,16 @@ Tutorial .. contents:: Table of Contents Below is a set of example scripts showing some of the possible customizations -that can be done with pyftpdlib. Some of them are included in -`demo `__ -directory of pyftpdlib source distribution. +that can be done with pyftpdlib. Some of them are included in `demo +`__ directory. -A Base FTP server +A base FTP server ================= -The script below uses a basic configuration and it's probably the best -starting point to understand how things work. It uses the base -`DummyAuthorizer `__ -for adding a bunch of "virtual" users, sets a limit for -`incoming connections `__ -and a range of `passive ports `__. - -`source code `__ +This is probably the best starting point to understand how things work. We use +the base `DummyAuthorizer`_ for adding a bunch of virtual users, we set a limit +for `incoming connections`_ and a range of `passive ports`_. See +`demo/basic_ftpd.py`_. .. code-block:: python @@ -29,51 +24,44 @@ and a range of `passive ports `__ -module to handle logging. If you don't configure logging pyftpdlib will write -logs to stderr. -In order to configure logging you should do it *before* calling serve_forever(). -Example logging to a file: +pyftpdlib uses the stdlib `logging`_ module to handle logs. If you don't +configure logging pyftpdlib will do it for you. In order to configure logging +you should do it *before* calling `FTPServer.serve_forever`_. Example which +logs to a file: .. code-block:: python @@ -98,8 +86,8 @@ DEBUG logging You may want to enable DEBUG logging to observe commands and responses exchanged by client and server. DEBUG logging will also log internal errors -which may occur on socket related calls such as ``send()`` and ``recv()``. -To enable DEBUG logging from code use: +which may occur on socket related calls such as ``send()`` and ``recv()``. To +enable DEBUG logging from code use: .. code-block:: python @@ -169,17 +157,13 @@ Logs will now look like this: Storing passwords as hash digests ================================= -Using FTP server library with the default -`DummyAuthorizer `__ means that -passwords will be stored in clear-text. An end-user ftpd using the default -dummy authorizer would typically require a configuration file for -authenticating users and their passwords but storing clear-text passwords is of -course undesirable. The most common way to do things in such case would be -first creating new users and then storing their usernames + passwords as hash -digests into a file or wherever you find it convenient. The example below shows -how to store passwords as one-way hashes by using md5 algorithm. - -`source code `__ +By using the default `DummyAuthorizer`_ you typically store passwords in +clear-text. A FTP server using the default dummy authorizer would typically +require a configuration file for authenticating users and their passwords, but +storing clear-text passwords is undesirable. You may want to store passwords as +hash digests into a file or wherever you find it convenient. The example below +shows how to store passwords as one-way hashes by using md5 algorithm. See +`demo/md5_ftpd.py`_. .. code-block:: python @@ -218,17 +202,13 @@ how to store passwords as one-way hashes by using md5 algorithm. if __name__ == "__main__": main() - - -Unix FTP Server +Unix FTP server =============== -If you're running a Unix system you may want to configure your ftpd to include -support for "real" users existing on the system and navigate the real -filesystem. The example below uses -`UnixAuthorizer `__ and -`UnixFilesystem `__ -classes to do so. +If you're on UNIX you may want to configure your FTP server to include support +for "real" users existing on the system, and navigate the real filesystem. The +example below uses `UnixAuthorizer`_ and `UnixFilesystem`_ classes to do so. +See `demo/unix_ftpd.py`_. .. code-block:: python @@ -248,17 +228,11 @@ classes to do so. if __name__ == "__main__": main() - -Windows FTP Server +Windows FTP server ================== -The following code shows how to implement a basic authorizer for a Windows NT -workstation to authenticate against existing Windows user accounts. This code -requires Mark Hammond's -`pywin32 `__ extension to be -installed. - -`source code `__ +Same as above, but for Windows. This code requires `pywin32`_ extension to be +installed. See `demo/win_ftpd.py`_. .. code-block:: python @@ -280,28 +254,25 @@ installed. if __name__ == "__main__": main() +.. _changing-the-concurrency-model: + Changing the concurrency model ============================== -By nature pyftpdlib is asynchronous. That means it uses a single process/thread -to handle multiple client connections and file transfers. This is why it is so -fast, lightweight and scalable (see `benchmarks `__). The -async model has one big drawback though: the code cannot contain instructions -which blocks for a long period of time, otherwise the whole FTP server will -hang. -As such the user should avoid calls such as ``time.sleep(3)``, heavy db -queries, etc. Moreover, there are cases where the async model is not -appropriate, and that is when you're dealing with a particularly slow -filesystem (say a network filesystem such as samba). If the filesystem is slow -(say, a ``open(file, 'r').read(8192)`` takes 2 secs to complete) then you are -stuck. -Starting from version 1.0.0 pyftpdlib can change the concurrency model by using -multiple processes or threads instead. -In technical (internal) terms that means that every time a client connects a -separate thread/process is spawned and internally it will run its own IO loop. -In practical terms this means that you can block as long as you want. -Changing the concurrency module is easy: you just need to import a substitute -for `FTPServer `__. class: +By nature pyftpdlib is asynchronous. That means that it uses a single +process/thread to handle multiple client connections and file transfers. This +is why it is so fast, lightweight and scalable (see `benchmarks`_). The async +model has one big drawback though: the code cannot contain instructions that +block for a long period of time, otherwise the whole FTP server will hang. As +such, the user should avoid calls such as ``time.sleep(3)``, heavy DB queries, +etc. at all costs. There are cases where the async model is not appropriate, +e.g. if you're dealing with a particularly slow disk or a network filesystem. +If the calls that interact with the filesystem are slow (e.g., ``open(file, +'r').read(8192)`` takes 2 seconds to complete) then you are stuck. In such +cases you can change the concurrency model from async to multi processes or +multi threads. In practice this means that every time a client connects, a +separate thread or process is spawned, and internally it will run its own IO +loop. Multiple threads ^^^^^^^^^^^^^^^^ @@ -344,28 +315,35 @@ Multiple processes if __name__ == "__main__": main() -Pre fork -^^^^^^^^ +It must be noted that the multi-thread approach should NOT be used with +`UnixAuthorizer`_ or `WindowsAuthorizer`_ . Reason: every time the FTP server +accesses the filesystem (e.g. for creating or renaming a file) the authorizer +will temporarily impersonate the currently logged on user by changing effective +user or group ID of the current process. + +.. _pre-fork-model: + +Pre fork model +^^^^^^^^^^^^^^ -There also exists a third option (UNIX only): the pre-fork model. -Pre-fork means that a certain number of worker processes are ``spawn()``-ed -before starting the server. -Each worker process will keep using a 1-thread, async concurrency model, -handling multiple concurrent connections, but the workload is split. +There is also a third option (UNIX only): the pre-fork model. Pre-fork means +that a certain number of worker processes are ``spawn()``-ed before starting +the server. Each worker process will keep using a 1-thread, async concurrency +model, handling multiple concurrent connections, but the workload is split. This way the delay introduced by a blocking function call is amortized and divided by the number of workers, and thus also the disk I/O latency is -minimized. -Every time a new connection comes in, the parent process will automatically -delegate the connection to one of the subprocesses, so from the app standpoint -this is completely transparent. -As a general rule, it is always a good idea to use this model in production. -The optimal value depends on many factors including (but not limited to) the -number of CPU cores, the number of hard disk drives that store data, and load -pattern. When one is in doubt, setting it to the number of available CPU cores -would be a good start. +minimized. Every time a new connection comes in, the parent process will +automatically delegate the connection to one of the worker processes, so from +the app standpoint this is completely transparent. As a general rule, it is +always a good idea to use this model in production. The optimal value depends +on many factors including (but not limited to) the number of CPU cores, the +number of hard disk drives that store data, and load pattern. When one is in +doubt, setting it to the number of available CPU cores would be a good start. .. code-block:: python + import os + from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer from pyftpdlib.authorizers import DummyAuthorizer @@ -376,77 +354,37 @@ would be a good start. handler = FTPHandler handler.authorizer = authorizer server = FTPServer(('', 2121), handler) - server.serve_forever(worker_processes=4) # <- + server.serve_forever(worker_processes=os.cpu_count()) # <- if __name__ == "__main__": main() -Throttle bandwidth -================== - -An important feature for an ftpd is limiting the speed for downloads and -uploads affecting the data channel. -`ThrottledDTPHandler.banner `__ -can be used to set such limits. -The basic idea behind ``ThrottledDTPHandler`` is to wrap sending and receiving -in a data counter and temporary "sleep" the data channel so that you burst to -no more than x Kb/sec average. When it realizes that more than x Kb in a second -are being transmitted it temporary blocks the transfer for a certain number of -seconds. - -.. code-block:: python - - import os - - from pyftpdlib.handlers import FTPHandler, ThrottledDTPHandler - from pyftpdlib.servers import FTPServer - from pyftpdlib.authorizers import DummyAuthorizer +.. _ftps-server: - def main(): - authorizer = DummyAuthorizer() - authorizer.add_user('user', '12345', os.getcwd(), perm='elradfmwMT') - authorizer.add_anonymous(os.getcwd()) - - dtp_handler = ThrottledDTPHandler - dtp_handler.read_limit = 30720 # 30 Kb/sec (30 * 1024) - dtp_handler.write_limit = 30720 # 30 Kb/sec (30 * 1024) - - ftp_handler = FTPHandler - ftp_handler.authorizer = authorizer - # have the ftp handler use the alternative dtp handler class - ftp_handler.dtp_handler = dtp_handler +FTPS (FTP over TLS/SSL) server +============================== - server = FTPServer(('', 2121), ftp_handler) - server.serve_forever() - - if __name__ == '__main__': - main() +pyftpdlib implements FTP over TLS, also known as FTPS, as defined in +`RFC-4217`_. This requires installing `PyOpenSSL`_ third party module. +`TLS_FTPHandler`_ class requires a ``certfile`` and a ``keyfile``. You can +generate self-signed SSL certificates like this (see also `Apache FAQs`_): +.. code-block:: sh -FTPS (FTP over TLS/SSL) server -============================== + $ openssl req -x509 -newkey rsa:2048 -keyout ftpd.key -out ftpd.crt -nodes + $ ls + ftpd.crt ftpd.key -Starting from version 0.6.0 pyftpdlib finally includes full FTPS support -implementing both TLS and SSL protocols and *AUTH*, *PBSZ* and *PROT* commands -as defined in `RFC-4217 `__. This has been -implemented by using `PyOpenSSL `__ -module, which is required in order to run the code below. -`TLS_FTPHandler `__ -class requires at least a ``certfile`` to be specified and optionally a -``keyfile``. -`Apache FAQs `__ 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 -`here `__. - -`source code `__ +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 +`here `__ +(not recommended). See `demo/tls_ftpd.py`_. .. code-block:: python """ An RFC-4217 asynchronous FTPS server supporting both SSL and TLS. - Requires PyOpenSSL module (http://pypi.python.org/pypi/pyOpenSSL). + Requires PyOpenSSL module (https://pypi.org/project/pyOpenSSL). """ from pyftpdlib.servers import FTPServer @@ -458,9 +396,10 @@ which include both and is available authorizer.add_user('user', '12345', '.', perm='elradfmwMT') authorizer.add_anonymous('.') handler = TLS_FTPHandler - handler.certfile = 'keycert.pem' + handler.certfile = '/path/to/ftpd.crt' # <-- + handler.keyfile = '/path/to/ftpd.key' # <-- handler.authorizer = authorizer - # requires SSL for both control and data channel + # optionally require SSL for both control and data channel #handler.tls_control_required = True #handler.tls_data_required = True server = FTPServer(('', 21), handler) @@ -469,12 +408,11 @@ which include both and is available if __name__ == '__main__': main() - Event callbacks =============== -A small example which shows how to use callback methods via -`FTPHandler `__ subclassing: +Here's an example which shows how to use callback methods via `FTPHandler`_ +subclassing: .. code-block:: python @@ -531,13 +469,49 @@ A small example which shows how to use callback methods via if __name__ == "__main__": main() +Throttle bandwidth +================== + +If desired, you can limit the transfer speed for downloads and uploads by using +the `ThrottledDTPHandler`_ class. The basic idea behind ``ThrottledDTPHandler`` +is to wrap sending and receiving in a data counter, and temporary "sleep" the +data channel so that you burst to no more than X Kb/sec on average. + +.. code-block:: python + + import os + + from pyftpdlib.handlers import FTPHandler, ThrottledDTPHandler + from pyftpdlib.servers import FTPServer + from pyftpdlib.authorizers import DummyAuthorizer + + def main(): + authorizer = DummyAuthorizer() + authorizer.add_user('user', '12345', os.getcwd(), perm='elradfmwMT') + authorizer.add_anonymous(os.getcwd()) + + dtp_handler = ThrottledDTPHandler + dtp_handler.read_limit = 30720 # 30 Kb/sec (30 * 1024) + dtp_handler.write_limit = 30720 # 30 Kb/sec (30 * 1024) + + ftp_handler = FTPHandler + ftp_handler.authorizer = authorizer + # have the ftp handler use the alternative dtp handler class + ftp_handler.dtp_handler = dtp_handler + + server = FTPServer(('', 2121), ftp_handler) + server.serve_forever() + + if __name__ == '__main__': + main() Command line usage ================== -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. +Pyftpdlib can also be run as a simple stand-alone server from command line. +This is useful when you want to quickly share a directory. Here's some +examples. + Anonymous server, listening on port 2121, sharing the current directory: .. code-block:: sh @@ -555,10 +529,38 @@ Anonymous server with write permission: $ python3 -m pyftpdlib -w +Specify a user with write permissions: + +.. code-block:: sh + + $ python3 -m pyftpdlib -u bob -P mypassword + Set a different address/port and home directory: .. code-block:: sh - $ python3 -m pyftpdlib -i localhost -p 8021 -d /home/bob + $ python3 -m pyftpdlib -i localhost -p 2121 -d /home/bob See ``python3 -m pyftpdlib -h`` for a complete list of options. + +.. _`Apache FAQs`: https://httpd.apache.org/docs/2.4/ssl/ssl_faq.html#selfcert +.. _`benchmarks`: benchmarks.html +.. _`demo/basic_ftpd.py`: https://github.com/giampaolo/pyftpdlib/blob/master/demo/basic_ftpd.py +.. _`demo/md5_ftpd.py`: https://github.com/giampaolo/pyftpdlib/blob/master/demo/md5_ftpd.py +.. _`demo/tls_ftpd.py`: https://github.com/giampaolo/pyftpdlib/blob/master/demo/tls_ftpd.py +.. _`demo/unix_ftpd.py`: https://github.com/giampaolo/pyftpdlib/blob/master/demo/unix_ftpd.py +.. _`demo/win_ftpd.py`: https://github.com/giampaolo/pyftpdlib/blob/master/demo/win_ftpd.py +.. _`DummyAuthorizer`: api.html#pyftpdlib.authorizers.DummyAuthorizer +.. _`FTPHandler`: api.html#pyftpdlib.handlers.FTPHandler +.. _`FTPServer.serve_forever`: api.html#pyftpdlib.servers.FTPServer.serve_forever +.. _`incoming connections`: api.html#pyftpdlib.servers.FTPServer.max_cons +.. _`logging`: https://docs.python.org/3/library/logging.html +.. _`passive ports`: api.html#pyftpdlib.handlers.FTPHandler.passive_ports +.. _`PyOpenSSL`: https://pypi.org/project/pyOpenSSL +.. _`pywin32`: https://pypi.org/project/pywin32/ +.. _`RFC-4217`: https://www.ietf.org/rfc/rfc4217.txt +.. _`ThrottledDTPHandler`: api.html#pyftpdlib.handlers.ThrottledDTPHandler +.. _`TLS_FTPHandler`: api.html#pyftpdlib.handlers.TLS_FTPHandler +.. _`UnixAuthorizer`: api.html#pyftpdlib.authorizers.UnixAuthorizer +.. _`UnixFilesystem`: api.html#pyftpdlib.filesystems.UnixFilesystem +.. _`WindowsAuthorizer`: api.html#pyftpdlib.authorizers.WindowsAuthorizer diff --git a/pyftpdlib/handlers.py b/pyftpdlib/handlers.py index ec06938b..c1ae74f6 100644 --- a/pyftpdlib/handlers.py +++ b/pyftpdlib/handlers.py @@ -355,7 +355,7 @@ def _support_hybrid_ipv6(): on this platform. """ # Note: IPPROTO_IPV6 constant is broken on Windows, see: - # http://bugs.python.org/issue6926 + # https://bugs.python.org/issue6926 try: if not socket.has_ipv6: return False @@ -472,8 +472,8 @@ def __init__(self, cmd_channel, extmode=False): # 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 + # https://en.wikipedia.org/wiki/IPv6#IPv4-mapped_addresses + # https://datatracker.ietf.org/doc/html/rfc3493.html#section-3.7 # We truncate the first bytes to make it look like a # common IPv4 address. ip = ip[7:] @@ -883,7 +883,7 @@ def send(self, data): return result def refill_buffer(self): # pragma: no cover - """Overridden as a fix around http://bugs.python.org/issue1740572 + """Overridden as a fix around https://bugs.python.org/issue1740572 (when the producer is consumed, close() was called instead of handle_close()). """ @@ -1277,11 +1277,7 @@ class FTPHandler(AsyncChat): - (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: - https://github.com/giampaolo/pysendfile/ - Automatically defaults to True if pysendfile module is - installed. + Linux only. - (bool) tcp_no_delay: controls the use of the TCP_NODELAY socket option which disables the Nagle algorithm resulting in @@ -1293,7 +1289,7 @@ class FTPHandler(AsyncChat): - (str) unicode_errors: the error handler passed to ''.encode() and ''.decode(): - http://docs.python.org/library/stdtypes.html#str.decode + https://docs.python.org/library/stdtypes.html#str.decode (detaults to 'replace'). - (str) log_prefix: @@ -2097,8 +2093,8 @@ def _make_eport(self, ip, port): # 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 + # https://en.wikipedia.org/wiki/IPv6#IPv4-mapped_addresses + # https://datatracker.ietf.org/doc/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:] diff --git a/pyftpdlib/ioloop.py b/pyftpdlib/ioloop.py index 735f27e9..8e6e6c6d 100644 --- a/pyftpdlib/ioloop.py +++ b/pyftpdlib/ioloop.py @@ -52,7 +52,7 @@ def __init__(self, host, port): def handle_accepted(self, sock, addr): Handler(sock) -server = Server('localhost', 8021) +server = Server('localhost', 2121) IOLoop.instance().loop() """ @@ -899,9 +899,9 @@ def connect_af_unspecified(self, addr, source_address=None): # 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\ + # https://en.wikipedia.org/wiki/IPv6\ # IPv4-mapped_addresses - # http://tools.ietf.org/html/rfc3493.html#section-3.7 + # https://datatracker.ietf.org/doc/html/rfc3493.html#section-3.7 # We truncate the first bytes to make it look like a # common IPv4 address. source_address = ( @@ -924,7 +924,7 @@ def connect_af_unspecified(self, addr, source_address=None): return af # send() and recv() overridden as a fix around various bugs: - # - http://bugs.python.org/issue1736101 + # - https://bugs.python.org/issue1736101 # - https://github.com/giampaolo/pyftpdlib/issues/104 # - https://github.com/giampaolo/pyftpdlib/issues/109 diff --git a/pyftpdlib/log.py b/pyftpdlib/log.py index 5de1fb14..c73f5ccf 100644 --- a/pyftpdlib/log.py +++ b/pyftpdlib/log.py @@ -4,7 +4,7 @@ """ Logging support for pyftpdlib, inspired from Tornado's -(http://www.tornadoweb.org/). +(https://www.tornadoweb.org/). This is not supposed to be imported/used directly. Instead you should use logging.basicConfig before serve_forever(). diff --git a/pyftpdlib/test/__init__.py b/pyftpdlib/test/__init__.py index 1d91df9c..945d7762 100644 --- a/pyftpdlib/test/__init__.py +++ b/pyftpdlib/test/__init__.py @@ -296,7 +296,7 @@ def get_server_handler(): raise RuntimeError("can't find any FTPHandler instance") -# commented out as per bug http://bugs.python.org/issue10354 +# commented out as per bug https://bugs.python.org/issue10354 # tempfile.template = 'tmp-pyftpdlib' diff --git a/pyftpdlib/test/test_functional.py b/pyftpdlib/test/test_functional.py index 19b8be44..b386ad44 100644 --- a/pyftpdlib/test/test_functional.py +++ b/pyftpdlib/test/test_functional.py @@ -2474,7 +2474,7 @@ def test_error_on_callback(self): def test_active_conn_error(self): # we open a socket() but avoid to invoke accept() to # reproduce this error condition: - # http://code.google.com/p/pyftpdlib/source/detail?r=905 + # https://code.google.com/p/pyftpdlib/source/detail?r=905 with contextlib.closing(socket.socket()) as sock: sock.bind((HOST, 0)) port = sock.getsockname()[1] diff --git a/pyproject.toml b/pyproject.toml index 834b1c20..bf52a012 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -139,6 +139,9 @@ disable = [ [tool.rstcheck] ignore_messages = [ "Unexpected possible title overline or transition", + 'Hyperlink target "changing-the-concurrency-model" is not referenced.', + 'Hyperlink target "ftps-server" is not referenced.', + 'Hyperlink target "pre-fork-model" is not referenced.', ] [tool.tomlsort] diff --git a/scripts/ftpbench b/scripts/ftpbench index ed6e4a90..2146f1c9 100755 --- a/scripts/ftpbench +++ b/scripts/ftpbench @@ -106,7 +106,7 @@ if not sys.stdout.isatty() or os.name != 'posix': return s else: - # http://goo.gl/6V8Rm + # https://goo.gl/6V8Rm def hilite(string, ok=True, bold=False): """Return an highlighted version of 'string'.""" attr = [] @@ -132,7 +132,7 @@ def print_bench(what, value, unit=""): print(s.strip()) -# http://goo.gl/zeJZl +# https://goo.gl/zeJZl def bytes2human(n, format="%(value).1f%(symbol)s"): """ >>> bytes2human(10000) @@ -151,7 +151,7 @@ def bytes2human(n, format="%(value).1f%(symbol)s"): return format % dict(symbol=symbols[0], value=n) -# http://goo.gl/zeJZl +# https://goo.gl/zeJZl def human2bytes(s): """ >>> human2bytes('1M') diff --git a/scripts/internal/print_announce.py b/scripts/internal/print_announce.py index 19edcec5..21a8e9eb 100755 --- a/scripts/internal/print_announce.py +++ b/scripts/internal/print_announce.py @@ -19,8 +19,8 @@ PRJ_NAME = 'pyftpdlib' PRJ_URL_HOME = 'https://github.com/giampaolo/pyftpdlib' -PRJ_URL_DOC = 'http://pyftpdlib.readthedocs.io' -PRJ_URL_DOWNLOAD = 'https://pypi.python.org/pypi/pyftpdlib' +PRJ_URL_DOC = 'https://pyftpdlib.readthedocs.io' +PRJ_URL_DOWNLOAD = 'https://pypi.org/project/pyftpdlib' PRJ_URL_WHATSNEW = ( 'https://github.com/giampaolo/pyftpdlib/blob/master/HISTORY.rst' )