pax_global_header00006660000000000000000000000064132731455420014520gustar00rootroot0000000000000052 comment=322699b5931552f3a5abbc32f2633ae2c963f5c5 pyftpdlib-release-1.5.4/000077500000000000000000000000001327314554200151425ustar00rootroot00000000000000pyftpdlib-release-1.5.4/.ci/000077500000000000000000000000001327314554200156135ustar00rootroot00000000000000pyftpdlib-release-1.5.4/.ci/README000066400000000000000000000002571327314554200164770ustar00rootroot00000000000000This directory contains support scripts for Travis and Appveyor continuous integration services. Travis is used to run tests on Linux and OSX, Appveyor runs tests on Windows. pyftpdlib-release-1.5.4/.ci/appveyor/000077500000000000000000000000001327314554200174605ustar00rootroot00000000000000pyftpdlib-release-1.5.4/.ci/appveyor/README000066400000000000000000000002001327314554200203300ustar00rootroot00000000000000This directory contains support files for appveyor, a continuous integration service which runs tests on Windows on every push. pyftpdlib-release-1.5.4/.ci/appveyor/install.ps1000066400000000000000000000053421327314554200215570ustar00rootroot00000000000000# Sample script to install Python and pip under Windows # Authors: Olivier Grisel and Kyle Kastner # License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ $BASE_URL = "https://www.python.org/ftp/python/" $GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" $GET_PIP_PATH = "C:\get-pip.py" function DownloadPython ($python_version, $platform_suffix) { $webclient = New-Object System.Net.WebClient $filename = "python-" + $python_version + $platform_suffix + ".msi" $url = $BASE_URL + $python_version + "/" + $filename $basedir = $pwd.Path + "\" $filepath = $basedir + $filename if (Test-Path $filename) { Write-Host "Reusing" $filepath return $filepath } # Download and retry up to 5 times in case of network transient errors. Write-Host "Downloading" $filename "from" $url $retry_attempts = 3 for($i=0; $i -lt $retry_attempts; $i++){ try { $webclient.DownloadFile($url, $filepath) break } Catch [Exception]{ Start-Sleep 1 } } Write-Host "File saved at" $filepath return $filepath } function InstallPython ($python_version, $architecture, $python_home) { Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home if (Test-Path $python_home) { Write-Host $python_home "already exists, skipping." return $false } if ($architecture -eq "32") { $platform_suffix = "" } else { $platform_suffix = ".amd64" } $filepath = DownloadPython $python_version $platform_suffix Write-Host "Installing" $filepath "to" $python_home $args = "/qn /i $filepath TARGETDIR=$python_home" Write-Host "msiexec.exe" $args Start-Process -FilePath "msiexec.exe" -ArgumentList $args -Wait -Passthru Write-Host "Python $python_version ($architecture) installation complete" return $true } function InstallPip ($python_home) { $pip_path = $python_home + "/Scripts/pip.exe" $python_path = $python_home + "/python.exe" if (-not(Test-Path $pip_path)) { Write-Host "Installing pip..." $webclient = New-Object System.Net.WebClient $webclient.DownloadFile($GET_PIP_URL, $GET_PIP_PATH) Write-Host "Executing:" $python_path $GET_PIP_PATH Start-Process -FilePath "$python_path" -ArgumentList "$GET_PIP_PATH" -Wait -Passthru } else { Write-Host "pip already installed." } } function InstallPackage ($python_home, $pkg) { $pip_path = $python_home + "/Scripts/pip.exe" & $pip_path install $pkg } function main () { InstallPython $env:PYTHON_VERSION $env:PYTHON_ARCH $env:PYTHON InstallPip $env:PYTHON InstallPackage $env:PYTHON wheel } main pyftpdlib-release-1.5.4/.ci/appveyor/run_with_compiler.cmd000066400000000000000000000034621327314554200237030ustar00rootroot00000000000000:: To build extensions for 64 bit Python 3, we need to configure environment :: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: :: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1) :: :: To build extensions for 64 bit Python 2, we need to configure environment :: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of: :: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0) :: :: 32 bit builds do not require specific environment configurations. :: :: Note: this script needs to be run with the /E:ON and /V:ON flags for the :: cmd interpreter, at least for (SDK v7.0) :: :: More details at: :: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows :: http://stackoverflow.com/a/13751649/163740 :: :: Author: Olivier Grisel :: License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ @ECHO OFF SET COMMAND_TO_RUN=%* SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows SET MAJOR_PYTHON_VERSION="%PYTHON_VERSION:~0,1%" IF %MAJOR_PYTHON_VERSION% == "2" ( SET WINDOWS_SDK_VERSION="v7.0" ) ELSE IF %MAJOR_PYTHON_VERSION% == "3" ( SET WINDOWS_SDK_VERSION="v7.1" ) ELSE ( ECHO Unsupported Python version: "%MAJOR_PYTHON_VERSION%" EXIT 1 ) IF "%PYTHON_ARCH%"=="64" ( ECHO Configuring Windows SDK %WINDOWS_SDK_VERSION% for Python %MAJOR_PYTHON_VERSION% on a 64 bit architecture SET DISTUTILS_USE_SDK=1 SET MSSdk=1 "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release ECHO Executing: %COMMAND_TO_RUN% call %COMMAND_TO_RUN% || EXIT 1 ) ELSE ( ECHO Using default MSVC build environment for 32 bit architecture ECHO Executing: %COMMAND_TO_RUN% call %COMMAND_TO_RUN% || EXIT 1 ) pyftpdlib-release-1.5.4/.ci/travis/000077500000000000000000000000001327314554200171235ustar00rootroot00000000000000pyftpdlib-release-1.5.4/.ci/travis/README000066400000000000000000000002101327314554200177740ustar00rootroot00000000000000This directory contains support files for Travis, a continuous integration service which runs tests on Linux and Windows on every push. pyftpdlib-release-1.5.4/.ci/travis/install.sh000077500000000000000000000023631327314554200211340ustar00rootroot00000000000000#!/bin/bash set -e set -x uname -a python -c "import sys; print(sys.version)" if [[ "$(uname -s)" == 'Darwin' ]]; then brew update || brew update brew outdated pyenv || brew upgrade pyenv brew install pyenv-virtualenv if which pyenv > /dev/null; then eval "$(pyenv init -)" fi case "${PYVER}" in # py26) # pyenv install 2.6.9 # pyenv virtualenv 2.6.9 pyftpdlib # ;; py27) pyenv install 2.7.10 pyenv virtualenv 2.7.10 pyftpdlib ;; # py32) # pyenv install 3.2.6 # pyenv virtualenv 3.2.6 pyftpdlib # ;; # py33) # pyenv install 3.3.6 # pyenv virtualenv 3.3.6 pyftpdlib # ;; py34) pyenv install 3.4.3 pyenv virtualenv 3.4.3 pyftpdlib ;; esac pyenv rehash pyenv activate pyftpdlib fi # It appears it's necessary to first upgrade setuptools separately: # https://github.com/pyexcel/pyexcel/issues/49 pip install -U setuptools pip install -U pip six pyopenssl pysendfile flake8 mock coveralls psutil if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]] || [[ $PYVER == 'py26' ]]; then pip install -U unittest2 fi pyftpdlib-release-1.5.4/.ci/travis/run.sh000077500000000000000000000007001327314554200202630ustar00rootroot00000000000000#!/bin/bash set -e set -x if [[ "$(uname -s)" == 'Darwin' ]]; then if which pyenv > /dev/null; then eval "$(pyenv init -)" fi pyenv activate pyftpdlib fi python setup.py install python pyftpdlib/test/runner.py # run linter only on Linux and on latest python versions if [ "$PYVER" == "2.7" ] || [ "$PYVER" == "3.6" ]; then if [[ "$(uname -s)" != 'Darwin' ]]; then rm -rf build python -m flake8 fi fi pyftpdlib-release-1.5.4/.coveragerc000066400000000000000000000007121327314554200172630ustar00rootroot00000000000000[report] include = *pyftpdlib* omit = *pyftpdlib/_compat.py *pyftpdlib/test/* setup.py exclude_lines = pragma: no cover if __name__ == .__main__.: except ImportError: if os.name == 'nt': raise NotImplementedError('must be implemented in subclass') if hasattr(select, 'epoll'): if hasattr(select, 'kqueue'): if PY3: if hasattr(select, 'devpoll'): if hasattr(select, 'kqueue'): if os.sep == "\\": pyftpdlib-release-1.5.4/.git-pre-commit000077500000000000000000000065661327314554200200200ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (c) 2009 Giampaolo Rodola'. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """ This gets executed on 'git commit' and rejects the commit in case the submitted code does not pass validation. Validation is run only against the *.py files which were modified in the commit. Checks: - assert no space at EOLs - assert not pdb.set_trace in code - assert no bare except clause ("except:") in code - assert "flake8" returns no warnings Install this with "make install-git-hooks". """ from __future__ import print_function import os import subprocess import sys def term_supports_colors(): try: import curses assert sys.stderr.isatty() curses.setupterm() assert curses.tigetnum("colors") > 0 except Exception: return False else: return True def hilite(s, ok=True, bold=False): """Return an highlighted version of 'string'.""" if not term_supports_colors(): return s attr = [] if ok is None: # no color pass elif ok: # green attr.append('32') else: # red attr.append('31') if bold: attr.append('1') return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), s) def exit(msg): msg = hilite(msg, ok=False) print(msg, file=sys.stderr) sys.exit(1) def sh(cmd): """run cmd in a subprocess and return its output. raises RuntimeError on error. """ p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) stdout, stderr = p.communicate() if p.returncode != 0: raise RuntimeError(stderr) if stderr: print(stderr, file=sys.stderr) if stdout.endswith('\n'): stdout = stdout[:-1] return stdout def main(): out = sh("git diff --cached --name-only") py_files = [x for x in out.split('\n') if x.endswith('.py') and os.path.exists(x)] lineno = 0 for path in py_files: with open(path) as f: for line in f: lineno += 1 # space at end of line if line.endswith(' '): print("%s:%s %r" % (path, lineno, line)) return exit( "commit aborted: space at end of line") line = line.rstrip() # pdb if "pdb.set_trace" in line: print("%s:%s %s" % (path, lineno, line)) return exit( "commit aborted: you forgot a pdb in your python code") # bare except clause if "except:" in line and not line.endswith("# NOQA"): print("%s:%s %s" % (path, lineno, line)) return exit("commit aborted: bare except clause") # flake8 if py_files: try: import flake8 # NOQA except ImportError: return exit("commit aborted: flake8 is not installed; " "run 'make setup-dev-env'") # XXX: we should scape spaces and possibly other amenities here ret = subprocess.call( "%s -m flake8 %s" % (sys.executable, " ".join(py_files)), shell=True) if ret != 0: return exit("commit aborted: python code is not flake8 compliant") main() pyftpdlib-release-1.5.4/.gitignore000066400000000000000000000001671327314554200171360ustar00rootroot00000000000000syntax: glob *.al *.bak *.egg-info *.la *.lo *.o *.orig *.pyc *.pyd *.rej *.so *.swp .cache/ .idea/ .tox/ build/ dist/ pyftpdlib-release-1.5.4/.travis.yml000066400000000000000000000010611327314554200172510ustar00rootroot00000000000000sudo: false language: python cache: pip matrix: include: # XXX # - python: 2.6 - python: 2.7 # - python: 3.3 # - python: 3.4 # - python: 3.5 - python: 3.6 # XXX # - language: generic # os: osx # env: PYVER=py27 # - language: generic # os: osx # env: PYVER=py34 install: - ./.ci/travis/install.sh script: - ./.ci/travis/run.sh after_success: - | if [ "$(uname -s)" != 'Darwin' ]; then coveralls fi pyftpdlib-release-1.5.4/CREDITS000066400000000000000000000100511327314554200161570ustar00rootroot00000000000000 Intro ===== I would like to recognize some of the people who have been instrumental in the development of pyftpdlib. I'm sure I am forgetting some people (feel free to email me), but here is a short list. It's modeled after the Linux CREDITS file where the fields are: name (N), e-mail (E), web-address (W), country (C) description (D). Really thanks to all of you. Maintainers =========== N: Giampaolo Rodola' C: Italy E: g.rodola@gmail.com D: Original pyftpdlib author and maintainer N: Jay Loden C: NJ, USA E: jloden@gmail.com W: http://www.jayloden.com D: OS X and Linux platform development/testing N: Silas Sewell C: Denver, USA E: silas@sewell.ch W: http://www.silassewell.com D: Fedora port maintainer N: Li-Wen Hsu C: Taiwan E: lwhsu@lwhsu.org W: http://lwhsu.org D: FreeBSD port maintainer Contributors ============ N: Anatoly Techtonik C: Belarus E: techtonik@gmail.com D: Inclusion of pyftpdlib in Far Manager, a file and archive manager for Windows http://www.farmanager.com/enforum/viewtopic.php?t=640&highlight=&sid=12d4d90f27f421243bcf7a0e3c516efb. N: Arkadiusz Wahlig C: Germany W: http://arkadiusz-wahlig.blogspot.com D: Inclusion of pyftpdlib in gpftpd project, an FTP daemon for managing files hosted on Google Pages (http://arkadiusz-wahlig.blogspot.com/2008/04/hosting-files-on-google.html). N: Walco van Loon C: Netherlands E: walco@n--tree.net D: Inclusion of pyftpdlib in aksy project (http://walco.n--tree.net/projects/aksy). N: Stephane Travostino E: stephane.travostino@combo.cc D: Inclusion of pyftpdlib in Shareme project (http://bbs.archlinux.org/viewtopic.php?pid=431474). N: Shinya Okano C: Japan E: xxshss@yahoo.co.jp D: Japanese translation of pyftpdlib announces. Inclusion of pyftpdlib in unbox-ftpd project (http://code.google.com/p/unboxftpd). N: Yan Raber C: Italy E: yanraber@gmail.com D: Fix of Issue #9 (Path traversal vulnerability) N: Alex Martelli C: Italy E: aleax@gmail.com D: Various useful suggestions N: Knic C: Redmond, USA E: oneeyedelf1@googlemail.com D: Bug report #24 (some troubles on PythonCE), tester for various platforms including Windows Mobile, Windows Server 2008 and various 64 bit OSes. N: Greg Copeland E: gcopeland@efjohnson.com D: Bug report #16 (Extending compatibility with older python versions) N: Roger Erens E: rogererens@gmail.com D: Bug report affecting unix_ftpd.py's authorizer N: Coronado Ivan D: Bug report #70 (Wrong NOOP response code) N: Rauli Ruohonen D: Bug report #71 (Socket handles are leaked when a data transfer is in progress and user QUITs) N: Equand E: equand@gmail.com D: Bug report #77 (incorrect OOB data management on FreeBSD). N: fogwraith E: fogwraith@gmail.com D: Bug report #80 (demo/md5_ftpd.py should use hashlib module instead of the deprecated md5 module) N: Bram Neijt E: bneijt@gmail.com D: Bug report #100, author of ShareFTP project: http://git.logfish.net/shareftp.git/ N: Michele Petrazzo C: Italy E: michele.petrazzo@gmail.com D: Creation of the demo/unix_daemon.py code. N: Wentao Han D: Bug report #104 (socket.accept() might return None instead of a valid address and EPIPE might be thrown by asyncore on OS X). N: Ben Timby E: btimby@gmail.com C: USA D: issues 127, 229, 265 N: Bernd Deichmann W: http://deichmann-edv.de/ D: issue 156 N: Andrew Scheller E: gcode@loowis.durge.org C: UK D: issue 158, 161, 163, 167, 175 N: guppyism E: guppyism@gmail.com D: issue 187 N: Darren Worrall E: darren.worrall@gmail.com D: issue 198 N: Suzan Shakya E: suzan.shakya@gmail.com D: issue 211 N: Claus Klein E: claus.klein.sha@googlemail.com D: issue 232 N: Arfrever Frehtes Taifersar Arahesis E: arfrever.fta@gmail.com D: issue 239 N: tlockert D: issue 238 N: Juan J. Martinez E: juan@memset.com D: issue 263 N: Michael Ross E: michaelross757@gmail.com D: issue 273 E: dn@devicenull.org D: issue 280 N: Dmitry Panov C: UK E: dop251@gmail.com D: issue 262 M: PonyPC W: https://github.com/PonyPC D: issue 403 N: Tory Law E: git@torypages.com C: Canada D: Command line argument enhancements N: Tahir Ijaz W: https://github.com/tahirijaz24 D: issue 201, 435 pyftpdlib-release-1.5.4/HISTORY.rst000066400000000000000000001064021327314554200170400ustar00rootroot00000000000000Bug tracker at https://github.com/giampaolo/pyftpdlib/issues Version: 1.5.4 - 2018-05-04 =========================== **Enhancements** - #463: FTPServer class can now be used as a context manager. **Bug fixes** - #431: Ctrl-C doesn't exit `python -m pyftpdlib` on Windows. - #436: ThreadedFTPServer.max_cons is evaluated threading.activeCount(). If the user uses threads of its own it will consume the number of max_cons. - #447: ThreadedFTPServer and MultiprocessFTPServer do not join() tasks which are no longer consuming resources. Version: 1.5.3 - 2017-11-04 =========================== **Enhancements** - #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 **Bug fixes** - #403: fix duplicated output log. (path by PonyPC) - #414: Respond successfully to STOR only after closing file handle. Version: 1.5.2 - 2017-04-06 =========================== **Enhancements** - #378: SSL security was improved by disabling SSLv2, SSLv3 and SSL_COMPRESSION features. New TLS_FTPHandler's ssl_options class attribute was added. - #380: AbstractedFS.listdir() can now return also a generator (not only a list). **Bug fixes** - #367: ThreadedFTPServer no longer hangs if close_all() is called. - #394: ETIMEDOUT is not treated as an alias for "connection lost". - #400: QUIT can raise KeyError in case the user hasn't logged in yet and sends QUIT command. Version: 1.5.1 - 2016-05-02 =========================== **Bug fixes** - #381: an extraneous file was accidentally added to the tarball, causing issues with Python 3. Version: 1.5.0 - 2015-12-13 =========================== **Enhancements** - #304: remove deprecated items from 1.0.0 which were left in place for backward compatibility - #324: FTPHandler.started attribute, to figure out when client connected. - #340: dropped python 2.4 and 2.5 support. - #344: bench.py script --ssl option. - #346: provide more debugging info. - #348: FTPHandler has a new "auth_failed_timeout" class attribute (previously this was called _auth_failed_timeout). - #350: tests now live in pyftpdlib module namespace. - #351: fallback on using plain send() if sendfile() fails and no data has been transmitted yet. - #356: sendfile() is now used in case we're using SSL but data connection is in clear text. - #361: benchmark script now allows to benchmark downloads and uploads only (instead of both). - #362: 'ftpbench' script is now installed as a system script on 'setup.py install'. - #365: TLS FTP server is now 25% faster when dealing with clear-text connections. **Bug fixes** - #302: setup.py should not require pysendfile on Python >= 3.3. - #313: configuring root logger has no effect on pyftpdlib logging. - #329: IOLoop throws OSError on Linux. - #337: MultiprocessFTPServer and ThreadedFTPServer do not accept backlog argument. - #338: benchmark script uses old psutil API. - #343: recv() does not handle EBUSY. - #347: SSL WantReadError and WantWriteError errors are not properly taken into account. - #357: python -m pyftpdlib --verbose option doesn't work **Incompatible API changes** - FTPHandler._auth_failed_timeout has been renamed to FTPHandler.auth_failed_timeout. Version: 1.4.0 - Date: 2014-06-03 ================================= **Enhancements** - #284: documentation was turned into RsT and hosted on pythonhosted.org - #293: project was migrated from Google Code to Github. Code was migrated from SVN to GIT. - #294: use tox to automate testing on multiple python versions. - #295: use travis-ci for continuos test integration. - #298: pysendfile and PyOpenSSL are now listed as extra deps in setup.py. **Bug fixes** - #296: TypeError when using recent version of PyOpenSSL. - #297: listen() may raise EBADF in case of many connections. Version: 1.3.1 - Date: 2014-04-12 ================================= **Enhancements** - #262: FTPS is now able to load a certificate chain file. (patch by Dmitry Panov) - #277: added a make file for running tests and for other repetitive tasks (also for Windows). - #281: tarballs are now hosted on PYPI. - #282: support for /dev/poll on Solaris. - #285: test suite requires unittest2 module on python < 2.7. **Bug fixes** - #261: (FTPS) SSL shutdown does not properly work on Windows. - #280: (Python 2) unable to complete directory listing with invalid UTF8 characters. (patch by dn@devicenull.org) - #283: always use a single 'pyftpdlib' logger. Version: 1.3.0 - Date: 2013-11-07 ================================= **Enhancements** - #253: benchmark script's new --timeout option. - #270: new -V / --verbose cmdline option to enable a more verbose logging. **Bug fixes** - #254: bench.py script hadn't been ported to Python 3. - #263: MultiprocessFTPServer leaks memory and file descriptors. (patch by 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 - #273: IOLoop.fileno() on BSD systems raises AttributeError. (patch by Michael Ross) Version: 1.2.0 - Date: 2013-04-22 ================================= **Enhancements** - #250: added FTPServer's backlog argument controlling the queue of accepted connections. - #251: IOLoop.fileno() method for epoll() and kqueue() pollers. - #252: FTPServer 'address' parameter can also be an existent socket object. **Bug fixes** - #245: ThreadedFTPServer hogs all CPU resources after a client connects. Version: 1.1.0 - Date: 2013-04-09 ================================= **Enhancements** - #240: enabled "python -m pyftpdlib" cmdline syntax and got rid of "python -m pyftpdlib.ftpserver" syntax which was deprecated in 1.0.0. - #241: empty passwords are now allowed for anonymous and other users. - #244: pysendfile is no longer a dependency if we're on Python >= 3.3 as os.sendfile() will be used instead. - #247: on python 3.3 use time.monotonic() instead of time.time() so that the scheduler won't break in case of system clock updates. - #248: bench.py memory usage is highly overestimated. **Bug fixes** - #238: username is not logged in case of failed authentication. (patch by tlockert) - #243: an erroneous error message is given in case the address passed to bind() is already in use. - #245: ThreadedFTPServer hogs all CPU resources after a client connects. - #246: ThrottledDTPHandler was broken. **Incompatible API changes** - "python -m pyftpdlib.ftpserver" cmdline syntax doesn't work anymore Version: 1.0.1 - Date: 2013-02-22 ================================= **Bug fixes** - #236: MultiprocessFTPServer and ThreadedFTPServer hanging in case of failed authentication. Version: 1.0.0 - Date: 2013-02-19 ================================= **Enhancements** - #76: python 3.x porting. - #198: full unicode support (RFC-2640). - #203: asyncore IO loop has been rewritten from scratch and now supports epoll() on Linux and kqueue() on OSX/BSD. Also select() (Windows) and poll() pollers have been rewritten resulting in pyftpdlib being an order of magnitude faster and more scalable than ever. - #204: a new FilesystemError exception class is available in order send custom error strings to client from an AbstracteFS subclass. - #207: added on_connect() and on_disconnect() callback methods to FTPHandler class. - #212: provided two new classes: Logging_managementpyftpdlib.servers.ThreadedFTPServer and pyftpdlib.servers.MultiprocessFTPServer (POSIX only). They can be used to change the base async-based concurrecy model and use a multiple threads / processes based approach instead. Your FTPHandler subclasses will finally be free to block! ;) - #219: it is not possible to instantiate different FPTS classes using different SSL certificates. - #213: DummyAuthorizer.validate_authentication() has changed in that it no longer returns a bool but instead raises AuthenticationFailed() exception to signal a failed authentication. This has been done in order allow customized error messages on failed auth. Also it now expects a third 'handler' argument which is passed in order to allow IP-based authentication logic. Existing code overriding validate_authentication() must be changed in accordance. - #223: ftpserver.py has been split in submodules. - #225: logging module is now used for logging. ftpserver.py's log(), logline() and logerror() functions are deprecated. - #231: FTPHandler.ftp_* methods implementing filesystem-related commands now return a meaningful value on success (tipically the path name). - #234: FTPHandler and DTPHandler class provide a nice __repr__. - #235: FTPServer.serve_forever() has a new handle_exit parameter which can be set to False in order to avoid handling SIGTERM/SIGINT signals and logging server start and stop. - #236: big logging refactoring; by default only useful messages are logged (as opposed to *all* commands and responses exchanged by client and server). Also, FTPHandler has a new 'log_prefix' attribute which can be used to format every line logged. **Bug fixes** - #131: IPv6 dual-stack support was broken. - #206: can't change directory (CWD) when using UnixAuthorizer and process cwd is == "/root". - #211: pyftpdlib doesn't work if deprecated py-sendfile 1.2.4 module is installed. - #215: usage of FTPHandler.sleeping attribute could lead to 100% CPU usage. FTPHandler.sleeping is now removed. self.add_channel() / self.del_channel() should be used instead. - #222: an unhandled exception in handle_error() or close() can cause server to crash. - #229: backslashes on UNIX are not handled properly. - #232: hybrid IPv4/IPv6 support is broken. (patch by Claus Klein) **New modules** All the code contained in pyftpdlib/ftpserver.py and pyftpdlib/contrib namespaces has been moved here: - pyftpdlib.authorizers - pyftpdlib.filesystems - pyftpdlib.servers - pyftpdlib.handlers - pyftpdlib.log **New APIs** - pyftpdlib.authorizers.AuthenticationFailed - pyftpdlib.filesystems.FilesystemError - pyftpdlib.servers.ThreadedFTPServer - pyftpdlib.servers.MultiprocessFTPServer - pyftpdlib.handlers.FTPHandler's on_connect() and on_disconnect() callbacks. - pyftpdlib.handlers.FTPHandler.ftp_* methods return a meaningful value on success. - FTPServer, FTPHandler, DTPHandler new ioloop attribute. - pyftpdlib.lib.ioloop.IOLoop class (not supposed to be used directly) - pyftpdlib.handlers.FTPHandler.log_prefix **Deprecated name spaces** - pyftpdlib.ftpserver - pyftpdlib.contrib.* **Incompatible API changes** - All the main classes have been extracted from ftpserver.py and split into sub modules. +-------------------------------------+---------------------------------------+ | Before | After | +=====================================+=======================================+ | pyftpdlib.ftpserver.FTPServer | pyftpdlib.servers.FTPServer | +-------------------------------------+---------------------------------------+ | pyftpdlib.ftpserver.FTPHandler | pyftpdlib.handlers.FTPHandler | +-------------------------------------+---------------------------------------+ | pyftpdlib.ftpserver.DTPHandler | pyftpdlib.handlers.DTPHandler | +-------------------------------------+---------------------------------------+ | pyftpdlib.ftpserver.DummyAuthorizer | pyftpdlib.authorizers.DummyAuthorizer | +-------------------------------------+---------------------------------------+ | pyftpdlib.ftpserver.AbstractedFS | pyftpdlib.filesystems.AbstractedFS | +-------------------------------------+---------------------------------------+ Same for pyftpflib.contribs namespace which is deprecated. +-------------------------------------------------+-----------------------------------------+ | Before | After | +=================================================+=========================================+ | pyftpdlib.contrib.handlers.TLS_FTPHandler | pyftpdlib.handlers.TLS_FTPHandler | +-------------------------------------------------+-----------------------------------------+ | pyftpdlib.contrib.authorizers.UnixAuthorizer | pyftpdlib.authorizers.UnixAuthorizer | +-------------------------------------------------+-----------------------------------------+ | pyftpdlib.contrib.authorizers.WindowsAuthorizer | pyftpdlib.authorizers.WindowsAuthorizer | +-------------------------------------------------+-----------------------------------------+ | pyftpdlib.contrib.filesystems.UnixFilesystem | pyftpdlib.filesystems.UnixFilesystem | +-------------------------------------------------+-----------------------------------------+ Both imports from pyftpdlib.ftpserver and pyftpdlib.contrib.* will still work though and will raise a DeprecationWarning exception. **Other incompatible API changes** - DummyAuthorizer.validate_authentication() signature has changed. A third 'handler' argument is now expected. - DummyAuthorizer.validate_authentication() is no longer expected to return a bool. Instead it is supposed to raise AuthenticationFailed(msg) in case of failed authentication and return None otherwise. (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 - Unicode is now used instead of bytes pretty much everywhere. - FTPHandler.__init__() and TLS_FTPHandler.__init__() signatures have changed: from __init__(conn, server) to __init__(conn, server, ioloop=None) - FTPServer.server_forever() signature has changed: from serve_forever(timeout=1.0, use_poll=False, count=None) to serve_forever(timeout=1.0, blocking=True, handle_exit=True) - FTPServer.close_all() signature has changed: from close_all(ignore_all=False) to close_all() - FTPServer.serve_forever() and FTPServer.close_all() are no longer class methods. - asyncore.dispatcher and asynchat.async_chat classes has been replaced by: pyftpdlib.ioloop.Acceptor pyftpdlib.ioloop.Connector pyftpdlib.ioloop.AsyncChat Any customization relying on asyncore (e.g. use of asyncore.socket_map to figure out the number of connected clients) will no longer work. - pyftpdlib.ftpserver.CallLater and pyftpdlib.ftpserver.CallEvery are deprecated. Instead, use self.ioloop.call_later() and self.ioloop.call_every() from within the FTPHandler. Also delay() method of the returned object has been removed. - FTPHandler.sleeping attribute is removed. self.add_channel() and self.del_channel() should be used to pause and restart the handler. **Minor incompatible API changes** - FTPHandler.respond(resp) -> FTPHandler.respond(resp, logfun=logger.debug) - FTPHandler.log(resp) -> FTPHandler.log(resp, logfun=logger.info) - FTPHandler.logline(resp) -> FTPHandler.logline(resp, logfun=logger.debug) Version: 0.7.0 - Date: 2012-01-25 ================================= **Enhancements** - #152: uploads (from server to client) on UNIX are now from 2x (Linux) to 3x (OSX) faster because of sendfile(2) system call usage. - #155: AbstractedFS "root" and "cwd" are no longer read-only properties but can be set via setattr(). - #168: added FTPHandler.logerror() method. It can be overridden to provide more information (e.g. username) when logging exception tracebacks. - #174: added support for SITE CHMOD command (change file mode). - #177: setuptools is now used in setup.py - #178: added anti flood script in demo directory. - #181: added CallEvery class to call a function every x seconds. - #185: pass Debian licenscheck tool. - #189: the internal scheduler has been rewritten from scratch and it is an order of magnitude faster, especially for operations like cancel() which are involved when clients are disconnected (hence invoked very often). Some benchmarks: schedule: +0.5x, reschedule: +1.7x, cancel: +477x (with 1 milion scheduled functions), run: +8x Also, a single scheduled function now consumes 1/3 of the memory thanks to ``__slots__`` usage. - #195: enhanced unix_daemon.py script which (now uses python-daemon library). - #196: added callback for failed login attempt. - #200: FTPServer.server_forever() is now a class method. - #202: added benchmark script. **Bug fixes** - #156: data connection must be closed before sending 226/426 reply. This was against RFC-959 and was causing problems with older FTP clients. - #161: MLSD 'unique' fact can provide the same value for files having a similar device/inode but that in fact are different. (patch by Andrew Scheller) - #162: (FTPS) SSL shutdown() is not invoked for the control connection. - #163: FEAT erroneously reports MLSD. (patch by Andrew Scheller) - #166: (FTPS) an exception on send() can cause server to crash (DoS). - #167: fix some typos returned on HELP. - #170: PBSZ and PROT commands are now allowed before authentication fixing problems with non-compliant FTPS clients. - #171: (FTPS) an exception when shutting down the SSL layer can cause server to crash (DoS). - #173: file last modification time shown in LIST response might be in a language different than English causing problems with some clients. - #175: FEAT response now omits to show those commands which are removed from proto_cmds map. - #176: SO_REUSEADDR option is now used for passive data sockets to prevent server running out of free ports when using passive_ports directive. - #187: match proftpd LIST format for files having last modification time > 6 months. - #188: fix maximum recursion depth exceeded exception occurring if client quickly connects and disconnects data channel. - #191: (FTPS) during SSL shutdown() operation the server can end up in an infinite loop hogging CPU resources. - #199: UnixAuthorizer with require_valid_shell option is broken. **Major API changes since 0.6.0** - New FTPHandler.use_sendfile attribute. - sendfile() is now automatically used instead of plain send() if pysendfile module is installed. - FTPServer.serve_forever() is a classmethod. - AbstractedFS root and cwd properties can now be set via setattr(). - New CallLater class. - New FTPHandler.on_login_failed(username, password) method. - New FTPHandler.logerror(msg) method. - New FTPHandler.log_exception(instance) method. Version: 0.6.0 - Date: 2011-01-24 ================================= **Enhancements** - #68: added full FTPS (FTP over SSL/TLS) support provided by new TLS_FTPHandler class defined in pyftpdlib.contrib.handlers module. - #86: pyftpdlib now reports all ls and MDTM timestamps as GMT times, as recommended in RFC-3659. A FTPHandler.use_gmt_times attributed has been added and can be set to False in case local times are desired instead. - #124: pyftpdlib now accepts command line options to configure a stand alone anonymous FTP server when running pyftpdlib with python's -m option. - #125: logs are now provided in a standardized format parsable by log analyzers. FTPHandler class provides two new methods to standardize both commands and transfers logging: log_cmd() and log_transfer(). - #127: added FTPHandler.masquerade_address_map option which allows you to define multiple 1 to 1 mappings in case you run a FTP server with multiple private IP addresses behind a NAT firewall with multiple public IP addresses. - #128: files and directories owner and group names and os.readlink are now resolved via AbstractedFS methods instead of in format_list(). - #129, #139: added 4 new callbacks to FTPHandler class: on_incomplete_file_sent(), on_incomplete_file_received(), on_login() and on_logout(). - #130: added UnixAuthorizer and WindowsAuthorizer classes defined in the new pyftpdlib.contrib.authorizers module. - #131: pyftpdlib is now able to serve both IPv4 and IPv6 at the same time by using a single socket. - #133: AbstractedFS constructor now accepts two argumets: root and cmd_channel breaking compatibility with previous version. Also, root and and cwd attributes became properties. The previous bug consisting in resetting the root from the ftp handler after user login has been fixed to ease the development of subclasses. - #134: enabled TCP_NODELAY socket option for the FTP command channels resulting in pyftpdlib being twice faster. - #135: Python 2.3 support has been dropped. - #137: added new pyftpdlib.contrib.filesystems module within UnixFilesystem class which permits the client to escape its home directory and navigate the real filesystem. - #138: added DTPHandler.get_elapsed_time() method which returns the transfer elapsed time in seconds. - #144: a "username" parameter is now passed to authorizer's terminate_impersonation() method. - #149: ftpserver.proto_cmds dictionary refactoring and get rid of _CommandProperty class. **Bug fixes** - #120: an ActiveDTP() instance is not garbage collected in case a client issuing PORT disconnects before establishing the data connection. - #122: a wrong variable name was used in AbstractedFS.validpath method. - #123: PORT command doesn't bind to correct address in case an alias is created for the local network interface. - #140: pathnames returned in PWD response should have double-quotes '"' escaped. - #143: EINVAL not properly handled causes server crash on OSX. - #146: SIZE and MDTM commands are now rejected unless the "l" permission has been specified for the user. - #150: path traversal bug: it is possible to move/rename a file outside of the user home directory. **Major API changes since 0.5.2** - dropped Python 2.3 support. - all classes are now new-style classes. - AbstractedFS class: - __init__ now accepts two arguments: root and cmd_channel. - root and cwd attributes are now read-only properties. - 3 new methods have been added: - get_user_by_uid() - get_group_by_gid() - readlink() - FTPHandler class: - new class attributes: - use_gmt_times - tcp_no_delay - masquerade_address_map - new methods: - on_incomplete_file_sent() - on_incomplete_file_received() - on_login() - on_logout() - log_cmd() - log_transfer() - proto_cmds class attribute has been added. The FTPHandler class no longer relies on "ftpserver.proto_cmds" global dictionary but on "ftpserver.FTPHandler.proto_cmds" instead. - FTPServer class: - max_cons attribute defaults to 512 by default instead of 0 (unlimited). - server_forever()'s map argument is gone. - DummyAuthorizer: - ValueError exceptions are now raised instead of AuthorizerError. - terminate_impersonation() method now expects a "username" parameter. - DTPHandler.get_elapsed_time() method has been added. - Added a new package in pyftpdlib namespace: "contrib". Modules (and classes) defined here: - pyftpdlib.contrib.handlers.py (TLS_FTPHandler) - pyftpdlib.contrib.authorizers.py (UnixAuthorizer, WindowsAuthorizer) - pyftpdlib.contrib.filesystems (UnixFilesystem) **Minor API changes since 0.5.2** - FTPHandler renamed objects: - data_server -> _dtp_acceptor - current_type -> _current_type - restart_position -> _restart_position - quit_pending -> _quit_pending - af -> _af - on_dtp_connection -> _on_dtp_connection - on_dtp_close -> _on_dtp_close - idler -> _idler - AbstractedFS.rnfr attribute moved to FTPHandler._rnfr. Version: 0.5.2 - Date: 2009-09-14 ================================= **Enhancements** - #103: added unix_daemon.py script. - #108: a new ThrottledDTPHandler class has been added for limiting the speed of downloads and uploads. **Bug fixes** - #100: fixed a race condition in FTPHandler constructor which could throw an exception in case of connection bashing (DoS). (thanks Bram Neijt) - #102: FTPServer.close_all() now removes any unfired delayed call left behind to prevent potential memory leaks. - #104: fixed a bug in FTPServer.handle_accept() where socket.accept() could return None instead of a valid address causing the server to crash. (OS X only, reported by Wentao Han) - #104: an unhandled EPIPE exception might be thrown by asyncore.recv() when dealing with ill-behaved clients on OS X . (reported by Wentao Han) - #105: ECONNABORTED might be thrown by socket.accept() on FreeBSD causing the server to crash. - #109: an unhandled EBADF exception might be thrown when using poll() on OSX and FreeBSD. - #111: the license used was not MIT as stated in source files. - #112: fixed a MDTM related test case failure occurring on 64 bit OSes. - #113: fixed unix_ftp.py which was treating anonymous as a normal user. - #114: MLST is now denied unless the "l" permission has been specified for the user. - #115: asyncore.dispatcher.close() is now called before doing any other cleanup operation when client disconnects. This way we avoid an endless loop which hangs the server in case an exception is raised in close() method. (thanks Arkadiusz Wahlig) - #116: extra carriage returns were added to files transferred in ASCII mode. - #118: CDUP always changes to "/". - #119: QUIT sent during a transfer caused a memory leak. **API changes since 0.5.1** - ThrottledDTPHandler class has been added. - FTPHandler.process_command() method has been added. Version: 0.5.1 - Date: 2009-01-21 ================================= **Enhancements** - #79: added two new callback methods to FTPHandler class to handle "on_file_sent" and "on_file_received" events. - #82: added table of contents in documentation. - #92: ASCII transfers are now 200% faster on those systems using "\r\n" as line separator (typically Windows). - #94: a bigger buffer size for send() and recv() has been set resulting in a considerable speedup (about 40% faster) for both incoming and outgoing data transfers. - #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 **Bug fixes** - #78: the idle timeout of passive data connections gets stopped in case of rejected "site-to-site" connections. - #80: demo/md5_ftpd.py should use hashlib module instead of the deprecated md5 module. - #81: fixed some tests which were failing on SunOS. - #84: fixed a very rare unhandled exception which could occur when retrieving the first bytes of a corrupted file. - #85: a positive MKD response is supposed to include the name of the new directory. - #87: SIZE should be rejected when the current TYPE is ASCII. - #88: REST should be rejected when the current TYPE is ASCII. - #89: "TYPE AN" was erroneously treated as synonym for "TYPE A" when "TYPE L7" should have been used instead. - #90: an unhandled exception can occur when using MDTM against a file modified before year 1900. - #91: an unhandled exception can occur in case accept() returns None instead of a socket (it happens sometimes). - #95: anonymous is now treated as any other case-sensitive user. **API changes since 0.5.0** - FTPHandler gained a new "_extra_feats" private attribute. - FTPHandler gained two new methods: "on_file_sent" and "on_file_received". Version: 0.5.0 - Date: 2008-09-20 ================================= **Enhancements** - #72: pyftpdlib now provides configurable idle timeouts to disconnect client after a long time of inactivity. - #73: imposed a delay before replying for invalid credentials to minimize the risk of brute force password guessing (RFC-1123). - #74: it is now possible to define permission exceptions for certain directories (e.g. creating a user which does not have write permission except for one sub-directory in FTP root). - #: Improved bandwidth throttling capabilities of demo/throttled_ftpd.py script by having used the new CallLater class which drastically reduces the number of time.time() calls. **Bug fixes** - #62: some unit tests were failing on certain dual core machines. - #71: socket handles are leaked when a data transfer is in progress and user QUITs. - #75: orphaned file was left behind in case STOU failed for insufficient user permissions. - #77: incorrect OOB data management on FreeBSD. **API changes since 0.4.0** - FTPHandler, DTPHandler, PassiveDTP and ActiveDTP classes gained a new timeout class attribute. - DummyAuthorizer class gained a new override_perm method. - A new class called CallLater has been added. - AbstractedFS.get_stat_dir method has been removed. Version: 0.4.0 - Date: 2008-05-16 ================================= **Enhancements** - #65: It is now possible to assume the id of real users when using system dependent authorizers. - #67: added IPv6 support. **Bug fixes** - #64: Issue #when authenticating as anonymous user when using UNIX and Windows authorizers. - #66: WinNTAuthorizer does not determine the real user home directory. - #69: DummyAuthorizer incorrectly uses class attribute instead of instance attribute for user_table dictionary. - #70: a wrong NOOP response code was given. **API changes since 0.3.0** - DummyAuthorizer class has now two new methods: impersonate_user() and terminate_impersonation(). Version: 0.3.0 - Date: 2008-01-17 ================================= **Enhancements** - #42: implemented FEAT command (RFC-2389). - #48: real permissions, owner, and group for files on UNIX platforms are now provided when processing LIST command. - #51: added the new demo/throttled_ftpd.py script. - #52: implemented MLST and MLSD commands (RFC-3659). - #58: implemented OPTS command (RFC-2389). - #59: iterators are now used for calculating requests requiring long time to complete (LIST and MLSD commands) drastically increasing the daemon scalability when dealing with many connected clients. - #61: extended the set of assignable user permissions. **Bug fixes** - #41: an unhandled exception occurred on QUIT if user was not yet authenticated. - #43: hidden the server identifier returned in STAT response. - #44: a wrong response code was given on PORT in case of failed connection attempt. - #45: a wrong response code was given on HELP if the provided argument wasn't recognized as valid command. - #46: a wrong response code was given on PASV in case of unauthorized FXP connection attempt. - #47: can't use FTPServer.max_cons option on Python 2.3. - #49: a "550 No such file or directory" was returned when LISTing a directory containing a broken symbolic link. - #50: DTPHandler class did not respect what specified in ac_out_buffer_size attribute. - #53: received strings having trailing white spaces was erroneously stripped. - #54: LIST/NLST/STAT outputs are now sorted by file name. - #55: path traversal vulnerability in case of symbolic links escaping user's home directory. - #56: can't rename broken symbolic links. - #57: invoking LIST/NLST over a symbolic link which points to a direoctory shouldn't list its content. - #60: an unhandled IndexError exception error was raised in case of certain bad formatted PORT requests. **API changes since 0.2.0** - New IteratorProducer and BufferedIteratorProducer classes have been added. - DummyAuthorizer class changes: - The permissions management has been changed and the set of available permissions have been extended (see Issue #61). add_user() method now accepts "eladfm" permissions beyond the old "r" and "w". - r_perm() and w_perm() methods have been removed. - New has_perm() and get_perms() methods have been added. - AbstractedFS class changes: - normalize() method has been renamed in ftpnorm(). - translate() method has been renamed in ftp2fs(). - New methods: fs2ftp(), stat(), lstat(), islink(), realpath(), lexists(), validpath(). - get_list_dir(), get_stat_dir() and format_list() methods now return an iterator object instead of a string. - format_list() method has a new "ignore_err" keyword argument. - global debug() function has been removed. Version: 0.2.0 - Date: 2007-09-17 ================================= **Major enhancements** - #5: it is now possible to set a maximum number of connecions and a maximum number of connections from the same IP address. - #36: added support for FXP site-to-site transfer. - #39: added NAT/Firewall support with PASV (passive) mode connections. - #40: it is now possible to set a range of ports to use for passive connections. **RFC-related enhancements** - #6: accept TYPE AN and TYPE L8 as synonyms for TYPE ASCII and TYPE Binary. - #7: a new USER command can now be entered at any point to begin the login sequence again. - #10: HELP command arguments are now accepted. - #12: 554 error response is now returned on RETR/STOR if RESTart fails. - #15: STAT used with an argument now returns directory LISTing over the command channel (RFC-959). **Security Enhancements** - #3: stop buffering when extremely long lines are received over the command channel. - #11: data connection is now rejected in case a privileged port is specified in PORT command. - #25: limited the number of attempts to find a unique filename when processing STOU command. **Usability enhancements** - #: Provided an overridable attribute to easily set number of maximum login attempts before disconnecting. - #: Docstrings are now provided for almost every method and function. - #30: HELP response now includes the command syntax. - #31: a compact list of recognized commands is now provided on HELP. - #32: a detailed error message response is not returned to client in case the transfer is interrupted for some unexpected reason. - #38: write access can now be optionally granted for anonymous user. **Test suite enhancements** - # File creation/removal moved into setUp and tearDown methods to avoid leaving behind orphaned temporary files in the event of a test suite failure. - #7: added test case for USER provided while already authenticated. - #7: added test case for REIN while a transfer is in progress. - #28: added ABOR tests. **Bug fixes** - #4: socket's "reuse_address" feature was used after the socket's binding. - #8: STOU string response didn't follow RFC-1123 specifications. - #9: corrected path traversal vulnerability affecting file-system path translations. - #14: a wrong response code was returned on CDUP. - #17: SIZE is now rejected for not regular files. - #18: a wrong ABOR response code type was returned. - #19: watch for STOU preceded by REST which makes no sense. - #20: "attempted login" counter wasn't incremented on wrong username. - #21: STAT wasn't permitted if user wasn't authenticated yet. - #22: corrected memory leaks occurring on KeyboardInterrupt/SIGTERM. - #23: PASS wasn't rejected when user was already authenticated. - #24: Implemented a workaround over os.strerror() for those systems where it is not available (Python CE). - #24: problem occurred on Windows when using '\\' as user's home directory. - #26: select() in now used by default instead of poll() because of a bug inherited from asyncore. - #33: some FTPHandler class attributes wasn't resetted on REIN. - #35: watch for APPE preceded by REST which makes no sense. Version: 0.1.1 - Date: 2007-03-27 ================================= - Port selection on PASV command has been randomized to prevent a remote user to guess how many data connections are in progress on the server. - Fixed bug in demo/unix_ftpd.py script. - ftp_server.serve_forever now automatically re-use address if current system is posix. - License changed to MIT. Version: 0.1.0 - Date: 2007-02-26 ================================= - First proof of concept beta release. pyftpdlib-release-1.5.4/LICENSE000066400000000000000000000022151327314554200161470ustar00rootroot00000000000000====================================================================== Copyright (C) 2007-2016 Giampaolo Rodola' All Rights Reserved Permission to use, copy, modify, and distribute this software and its documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Giampaolo Rodola' not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. Giampaolo Rodola' DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT Giampaolo Rodola' BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ====================================================================== pyftpdlib-release-1.5.4/MANIFEST.in000066400000000000000000000031431327314554200167010ustar00rootroot00000000000000include .coveragerc include .git-pre-commit include .gitignore include CREDITS include HISTORY.rst include LICENSE include MANIFEST.in include Makefile include README.rst include demo/anti_flood_ftpd.py include demo/basic_ftpd.py include demo/keycert.pem include demo/md5_ftpd.py include demo/multi_proc_ftp.py include demo/multi_thread_ftp.py 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 docs/Makefile include docs/README include docs/adoptions.rst include docs/api.rst include docs/benchmarks.rst include docs/conf.py include docs/faqs.rst include docs/images/freebsd.gif include docs/index.rst include docs/install.rst include docs/make.bat include docs/rfc-compliance.rst include docs/tutorial.rst include make.bat include pyftpdlib/__init__.py include pyftpdlib/__main__.py include pyftpdlib/_compat.py include pyftpdlib/authorizers.py include pyftpdlib/filesystems.py include pyftpdlib/handlers.py include pyftpdlib/ioloop.py include pyftpdlib/log.py include pyftpdlib/servers.py include pyftpdlib/test/README include pyftpdlib/test/__init__.py include pyftpdlib/test/keycert.pem include pyftpdlib/test/runner.py include pyftpdlib/test/test_authorizers.py include pyftpdlib/test/test_filesystems.py include pyftpdlib/test/test_functional.py include pyftpdlib/test/test_functional_ssl.py include pyftpdlib/test/test_ioloop.py include pyftpdlib/test/test_misc.py include pyftpdlib/test/test_servers.py include scripts/ftpbench include scripts/generate_manifest.py include scripts/print_announce.py include setup.py include tox.ini pyftpdlib-release-1.5.4/Makefile000066400000000000000000000127611327314554200166110ustar00rootroot00000000000000# Shortcuts for various tasks (UNIX only). # To use a specific Python version run: # $ make install PYTHON=python3.3 PYTHON = python TSCRIPT = pyftpdlib/test/runner.py ARGS = DEV_DEPS = \ check-manifest \ coverage \ flake8 \ mock==1.0.1 \ pep8 \ pyflakes \ setuptools \ sphinx TEST_DEPS = \ nose \ psutil \ pyopenssl \ pysendfile \ unittest2 # In not in a virtualenv, add --user options for install commands. INSTALL_OPTS = `$(PYTHON) -c "import sys; print('' if hasattr(sys, 'real_prefix') else '--user')"` all: test clean: ## Remove all build files. rm -rf `find . -type d -name __pycache__ \ -o -type f -name \*.bak \ -o -type f -name \*.orig \ -o -type f -name \*.pyc \ -o -type f -name \*.pyd \ -o -type f -name \*.pyo \ -o -type f -name \*.rej \ -o -type f -name \*.so \ -o -type f -name \*.~ \ -o -type f -name \*\$testfn` rm -rf \ *.core \ *.egg-info \ *\$testfile* \ .coverage \ .tox \ build/ \ dist/ \ docs/_build/ \ htmlcov/ \ tmp/ install: ## Install this package. # make sure setuptools is installed (needed for 'develop' / edit mode) $(PYTHON) -c "import setuptools" $(PYTHON) setup.py develop $(INSTALL_OPTS) uninstall: ## Uninstall this package. cd ..; $(PYTHON) -m pip uninstall -y -v pyftpdlib || true $(PYTHON) scripts/purge_installation.py install-pip: ## (only if necessary) $(PYTHON) -c \ "import sys, ssl, os, pkgutil, tempfile, atexit; \ sys.exit(0) if pkgutil.find_loader('pip') else None; \ pyexc = 'from urllib.request import urlopen' if sys.version_info[0] == 3 else 'from urllib2 import urlopen'; \ exec(pyexc); \ ctx = ssl._create_unverified_context() if hasattr(ssl, '_create_unverified_context') else None; \ kw = dict(context=ctx) if ctx else {}; \ req = urlopen('https://bootstrap.pypa.io/get-pip.py', **kw); \ data = req.read(); \ f = tempfile.NamedTemporaryFile(suffix='.py'); \ atexit.register(f.close); \ f.write(data); \ f.flush(); \ print('downloaded %s' % f.name); \ code = os.system('%s %s --user' % (sys.executable, f.name)); \ f.close(); \ sys.exit(code);" setup-dev-env: ## Install GIT hooks, pip, test deps (also upgrades them). ${MAKE} install-git-hooks ${MAKE} install-pip $(PYTHON) -m pip install $(INSTALL_OPTS) --upgrade pip $(PYTHON) -m pip install $(INSTALL_OPTS) --upgrade $(TEST_DEPS) $(PYTHON) -m pip install $(INSTALL_OPTS) --upgrade $(DEV_DEPS) test: ## Run all tests. ${MAKE} install PYTHONWARNINGS=all $(PYTHON) $(TSCRIPT) test-functional: ## Run functional FTP tests. ${MAKE} install PYTHONWARNINGS=all $(PYTHON) pyftpdlib/test/test_functional.py test-functional-ssl: ## Run functional FTPS tests. ${MAKE} install PYTHONWARNINGS=all $(PYTHON) pyftpdlib/test/test_functional_ssl.py test-servers: ## Run tests for FTPServer and its subclasses. ${MAKE} install PYTHONWARNINGS=all $(PYTHON) pyftpdlib/test/test_servers.py test-authorizers: ## Run tests for authorizers. ${MAKE} install PYTHONWARNINGS=all $(PYTHON) pyftpdlib/test/test_authorizers.py test-filesystems: ## Run filesystem tests. ${MAKE} install PYTHONWARNINGS=all $(PYTHON) pyftpdlib/test/test_filesystems.py test-ioloop: ## Run IOLoop tests. ${MAKE} install PYTHONWARNINGS=all $(PYTHON) pyftpdlib/test/test_ioloop.py test-misc: ## Run miscellaneous tests. ${MAKE} install PYTHONWARNINGS=all $(PYTHON) pyftpdlib/test/test_misc.py test-by-name: ## e.g.: make test-by-name ARGS=pyftpdlib.test.test_functional.TestFtpStoreData ${MAKE} install PYTHONWARNINGS=all $(PYTHON) -m unittest -v $(ARGS) test-coverage: ## Run test coverage. ${MAKE} install rm -rf .coverage htmlcov PYTHONWARNINGS=all $(PYTHON) -m coverage run $(TSCRIPT) $(PYTHON) -m coverage report @echo "writing results to htmlcov/index.html" $(PYTHON) -m coverage html $(PYTHON) -m webbrowser -t htmlcov/index.html pep8: ## PEP8 linter. @git ls-files | grep \\.py$ | xargs $(PYTHON) -m pep8 pyflakes: ## Pyflakes linter. @export PYFLAKES_NODOCTEST=1 && \ git ls-files | grep \\.py$ | xargs $(PYTHON) -m pyflakes flake8: ## flake8 linter. @git ls-files | grep \\.py$ | xargs $(PYTHON) -m flake8 check-manifest: ## Inspect MANIFEST.in file. $(PYTHON) -m check_manifest -v $(ARGS) upload-src: ## Upload source on PYPI. ${MAKE} clean $(PYTHON) setup.py sdist upload git-tag-release: ## Git-tag a new release. git tag -a release-`python -c "import setup; print(setup.VERSION)"` -m `git rev-list HEAD --count`:`git rev-parse --short HEAD` git push --follow-tags install-git-hooks: ## Install GIT pre-commit hook ln -sf ../../.git-pre-commit .git/hooks/pre-commit chmod +x .git/hooks/pre-commit grep-todos: ## Look for TODOs in source files. git grep -EIn "TODO|FIXME|XXX" pre-release: ## All the necessary steps before making a release. ${MAKE} clean $(PYTHON) -c \ "from pyftpdlib import __ver__ as ver; \ doc = open('docs/index.rst').read(); \ history = open('HISTORY.rst').read(); \ assert ver in history, '%r not in HISTORY.rst' % ver; \ assert 'XXXX' not in history; \ " $(PYTHON) setup.py sdist release: ## Creates a release (tar.gz + upload + git tag release). ${MAKE} pre-release $(PYTHON) -m twine upload dist/* # upload tar on PYPI ${MAKE} git-tag-release generate-manifest: ## Generates MANIFEST.in file. $(PYTHON) scripts/generate_manifest.py > MANIFEST.in print-announce: ## Print announce of new release. @$(PYTHON) scripts/print_announce.py help: ## Display callable targets. @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' pyftpdlib-release-1.5.4/README.rst000066400000000000000000000421461327314554200166400ustar00rootroot00000000000000.. image:: https://img.shields.io/travis/giampaolo/pyftpdlib/master.svg?maxAge=3600&label=Linux%20/%20OSX :target: https://travis-ci.org/giampaolo/pyftpdlib :alt: Linux tests (Travis) .. image:: https://img.shields.io/appveyor/ci/giampaolo/pyftpdlib/master.svg?maxAge=3600&label=Windows :target: https://ci.appveyor.com/project/giampaolo/pyftpdlib :alt: Windows tests (Appveyor) .. image:: https://coveralls.io/repos/github/giampaolo/pyftpdlib/badge.svg?branch=master :target: https://coveralls.io/github/giampaolo/pyftpdlib?branch=master :alt: Test coverage (coverall.io) .. image:: https://readthedocs.org/projects/pyftpdlib/badge/?version=latest :target: http://pyftpdlib.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status .. image:: https://img.shields.io/pypi/v/pyftpdlib.svg?label=pypi :target: https://pypi.python.org/pypi/pyftpdlib/ :alt: Latest version .. image:: https://img.shields.io/github/stars/giampaolo/pyftpdlib.svg :target: https://github.com/giampaolo/pyftpdlib/ :alt: Github stars .. image:: https://img.shields.io/pypi/l/pyftpdlib.svg :target: https://pypi.python.org/pypi/pyftpdlib/ :alt: License Quick links =========== - `Home `__ - `Documentation `__ - `Download `__ - `Blog `__ - `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 and it's used in projects like `Google Chromium `__ and `Bazaar `__ and included in `Debian `__, `Fedora `__ and `FreeBSD `__ package repositories. Features ======== - Extremely **lightweight**, **fast** and **scalable** (see `why `__ and `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). - Portable: entirely written in pure Python; works with Python from **2.6** to **3.5** by using a single code base. - Supports **FTPS** (`RFC-4217 `__), **IPv6** (`RFC-2428 `__), **Unicode** file names (`RFC-2640 `__), **MLSD/MLST** commands (`RFC-3659 `__). - Support for virtual users and virtual filesystem. - Extremely flexible system of "authorizers" able to manage both "virtual" and "real" users on on both `UNIX `__ and `Windows `__. - `Test coverage `__ close to 100%. Performances ============ Despite being written in an intepreted language, pyftpdlib has transfer rates superior to most common UNIX FTP servers. It also scales better since whereas vsftpd and proftpd use multiple processes to achieve concurrency, pyftpdlib will only use one process and handle concurrency asynchronously (see `the C10K problem `__). Here are some `benchmarks `__ made against my Linux 3.0.0 box, Intel core-duo 3.1 Ghz: pyftpdlib vs. proftpd 1.3.4 --------------------------- +-----------------------------------------+----------------+----------------+-------------+ | **benchmark type** | **pyftpdlib** | **proftpd** | **speedup** | +-----------------------------------------+----------------+----------------+-------------+ | STOR (client -> server) | 585.90 MB/sec | 600.49 MB/sec | -0.02x | +-----------------------------------------+----------------+----------------+-------------+ | RETR (server -> client) | 1652.72 MB/sec | 1524.05 MB/sec | **+0.08** | +-----------------------------------------+----------------+----------------+-------------+ | 300 concurrent clients (connect, login) | 0.19 secs | 9.98 secs | **+51x** | +-----------------------------------------+----------------+----------------+-------------+ | STOR (1 file with 300 idle clients) | 585.59 MB/sec | 518.55 MB/sec | **+0.1x** | +-----------------------------------------+----------------+----------------+-------------+ | RETR (1 file with 300 idle clients) | 1497.58 MB/sec | 1478.19 MB/sec | 0x | +-----------------------------------------+----------------+----------------+-------------+ | 300 concurrent clients (RETR 10M file) | 3.41 secs | 3.60 secs | **+0.05x** | +-----------------------------------------+----------------+----------------+-------------+ | 300 concurrent clients (STOR 10M file) | 8.60 secs | 11.56 secs | **+0.3x** | +-----------------------------------------+----------------+----------------+-------------+ | 300 concurrent clients (QUIT) | 0.03 secs | 0.39 secs | **+12x** | +-----------------------------------------+----------------+----------------+-------------+ pyftpdlib vs. vsftpd 2.3.5 -------------------------- +-----------------------------------------+----------------+----------------+-------------+ | **benchmark type** | **pyftpdlib** | **vsftpd** | **speedup** | +-----------------------------------------+----------------+----------------+-------------+ | STOR (client -> server) | 585.90 MB/sec | 611.73 MB/sec | -0.04x | +-----------------------------------------+----------------+----------------+-------------+ | RETR (server -> client) | 1652.72 MB/sec | 1512.92 MB/sec | **+0.09** | +-----------------------------------------+----------------+----------------+-------------+ | 300 concurrent clients (connect, login) | 0.19 secs | 20.39 secs | **+106x** | +-----------------------------------------+----------------+----------------+-------------+ | STOR (1 file with 300 idle clients) | 585.59 MB/sec | 610.23 MB/sec | -0.04x | +-----------------------------------------+----------------+----------------+-------------+ | RETR (1 file with 300 idle clients) | 1497.58 MB/sec | 1493.01 MB/sec | 0x | +-----------------------------------------+----------------+----------------+-------------+ | 300 concurrent clients (RETR 10M file) | 3.41 secs | 3.67 secs | **+0.07x** | +-----------------------------------------+----------------+----------------+-------------+ | 300 concurrent clients (STOR 10M file) | 8.60 secs | 9.82 secs | **+0.07x** | +-----------------------------------------+----------------+----------------+-------------+ | 300 concurrent clients (QUIT) | 0.03 secs | 0.01 secs | +0.14x | +-----------------------------------------+----------------+----------------+-------------+ For more benchmarks see `here `__. Quick start =========== .. code-block:: python >>> from pyftpdlib.authorizers import DummyAuthorizer >>> from pyftpdlib.handlers import FTPHandler >>> from pyftpdlib.servers import FTPServer >>> >>> authorizer = DummyAuthorizer() >>> authorizer.add_user("user", "12345", "/home/giampaolo", perm="elradfmwMT") >>> authorizer.add_anonymous("/home/nobody") >>> >>> handler = FTPHandler >>> handler.authorizer = authorizer >>> >>> server = FTPServer(("127.0.0.1", 21), handler) >>> server.serve_forever() [I 13-02-19 10:55:42] >>> starting FTP server on 127.0.0.1:21 <<< [I 13-02-19 10:55:42] poller: [I 13-02-19 10:55:42] masquerade (NAT) address: None [I 13-02-19 10:55:42] passive ports: None [I 13-02-19 10:55:42] use sendfile(2): True [I 13-02-19 10:55:45] 127.0.0.1:34178-[] FTP session opened (connect) [I 13-02-19 10:55:48] 127.0.0.1:34178-[user] USER 'user' logged in. [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 `__ 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 (`Giampaolo Rodola' `_) some money. I only ask for a small donation, but of course I appreciate any amount. .. image:: https://www.paypal.com/en_US/i/btn/btn_donateCC_LG.gif :target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=ZSSF7G42VA2XE :alt: Donate via PayPal Don't want to donate money? Then maybe you could `write me a recommendation on Linkedin `_. In case you're using pyftpdlib into a software of yours `mail me `_ and I'll add your software to the `adoptions list `__. Timeline ======== - 2018-05-04: version `1.5.4 `__ released. - 2017-11-04: version `1.5.3 `__ released. - 2017-04-06: version `1.5.2 `__ released. - 2016-05-02: version `1.5.1 `__ released. - 2015-12-13: version `1.5.0 `__ released. - 2014-06-03: version `1.4.0 `__ released. - 2014-04-12: version `1.3.1 `__ released. - 2013-11-07: version `1.3.0 `__ released. - 2013-04-22: version `1.2.0 `__ released. - 2013-04-09: version `1.1.0 `__ released. - 2013-02-22: version `1.0.1 `__ released. - 2013-02-19: version `1.0.0 `__ released. - 2012-05-14: pyftpdlib included in `ftp-cloudfs `__ project. - 2012-01-25: version `0.7.0 `__ released. - 2011-12-01: pyftpdlib included in `feitp-server `__ project. - 2011-09-26: pyftpdlib included in `ftpmaster `__ project. - 2011-07-09: pyftpdlib included in `bftpd `__ project. - 2011-07-09: pyftpdlib included in `fastersync `__ project. - 2011-01-31: pyftpdlib included in `put.io FTP connector project `__. - 2011-01-24: version `0.6.0 `__ released. - 2010-12-14: added `donations `__. - 2010-08-24: pyftpdlib included in `peerscape `__ project. - 2010-07-15: pyftpdlib included in `Faetus `__ project. - 2010-07-11: pyftpdlib included in `Pyfilesystem `__ project. - 2010-06-28: pyftpdlib has been `packaged for Debian `__ - 2010-04-28: pyftpdlib included in `sierramodulepos `__ project. - 2010-03-20: `http://www.smartfile.com `__ uses pyftpdlib. - 2010-01-13: pyftpdlib included in `zenftp `__ project. - 2009-12-26: pyftpdlib included in `Symbian Python FTP server `__ project. - 2009-11-04: `www.netplay.it `__ uses pyftpdlib. - 2009-11-04: `www.adcast.tv `__ uses pyftpdlib. - 2009-11-04: `www.bitsontherun.com `__ uses pyftpdlib. - 2009-11-02: pyftpdlib included in `ftp-cloudfs `__ project. - 2009-09-14: version `0.5.2 `__ released. - 2009-08-10: pyftpdlib included in `Imgserve `__ project. - 2009-07-22: pyftpdlib included in `Plumi `__ project. - 2009-04-02: pyftpdlib RPM-packaged and ported on `Fedora `__ to make users can easily install on it via *yum install pyftpdlib*. - 2009-03-28: pyftpdlib included in `Bazaar `__ project. - 2009-02-23: pyftpdlib included in `ShareFTP `__ project. - 2009-01-21: version `0.5.1 `__ released. - 2008-12-27: pyftpdlib included in `Google Chromium `__, the open source project behind `Google Chrome `__. - 2008-12-27: pyftpdlib ported on `GNU Darwin `__ systems to make users can easily install on it. - 2008-11-26: pyftpdlib included in `OpenERP `__. - 2008-10-26: pyftpdlib included in `Python for OpenVMS `__ as standard package. - 2008-10-09: pyftpdlib included in `Shareme `__ project. - 2008-09-20: version `0.5.0 `__ released. - 2008-08-10: pyftpdlib included in `Manent `__ project. - 2008-05-16: version `0.4.0 `__ released. - 2008-04-09: pyftpdlib used as backend for `gpftpd `__, an FTP server for managing files hosted on `Google Pages `__. - 2008-01-17: version `0.3.0 `__ released. - 2007-10-14: pyftpdlib included in `Aksy `__ project. - 2007-09-17: version `0.2.0 `__ released. - 2007-09-08: pyftpdlib included as `FarManager `__ `plug-in `__. - 2007-03-06: pyftpdlib `ported on FreeBSD `__ systems to make users can easily install on it. - 2007-03-07: version `0.1.1 `__ released. - 2007-02-26: version `0.1.0 `__ released. - 2006-09-26: initial clunky thread-based progenitor `link `__. Trademarks ========== Some famous trademarks which adopted pyftpdlib (`complete list `__). .. image:: docs/images/chrome.jpg :target: http://www.google.com/chrome .. image:: docs/images/debian.png :target: http://www.debian.org .. image:: docs/images/fedora.png :target: http://fedoraproject.org/ .. image:: docs/images/freebsd.gif :target: http://www.freebsd.org .. image:: docs/images/openerp.jpg :target: http://openerp.com .. image:: docs/images/bazaar.jpg :target: http://bazaar-vcs.org .. image:: docs/images/bitsontherun.png :target: http://www.bitsontherun.com .. image:: docs/images/openvms.png :target: http://www.openvms.org/ .. image:: docs/images/smartfile.png :target: https://www.smartfile.com/ pyftpdlib-release-1.5.4/appveyor.yml000066400000000000000000000041401327314554200175310ustar00rootroot00000000000000environment: global: # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the # /E:ON and /V:ON options are not enabled in the batch script intepreter # See: http://stackoverflow.com/a/13751649/163740 WITH_COMPILER: "cmd /E:ON /V:ON /C .\\.ci\\appveyor\\run_with_compiler.cmd" matrix: # Pre-installed Python versions, which Appveyor may upgrade to # a later point release. - PYTHON: "C:\\Python27" PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "32" - PYTHON: "C:\\Python33" PYTHON_VERSION: "3.3.x" PYTHON_ARCH: "32" - PYTHON: "C:\\Python34" PYTHON_VERSION: "3.4.x" PYTHON_ARCH: "32" - PYTHON: "C:\\Python27-x64" PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "64" WINDOWS_SDK_VERSION: "v7.0" - PYTHON: "C:\\Python33-x64" PYTHON_VERSION: "3.3.x" PYTHON_ARCH: "64" WINDOWS_SDK_VERSION: "v7.1" - PYTHON: "C:\\Python34-x64" PYTHON_VERSION: "3.4.x" PYTHON_ARCH: "64" WINDOWS_SDK_VERSION: "v7.1" # Also build on a Python version not pre-installed by Appveyor. # See: https://github.com/ogrisel/python-appveyor-demo/issues/10 # - PYTHON: "C:\\Python266" # PYTHON_VERSION: "2.6.6" # PYTHON_ARCH: "32" init: - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" install: - "powershell .ci\\appveyor\\install.ps1" # - ps: (new-object net.webclient).DownloadFile('https://raw.github.com/pypa/pip/master/contrib/get-pip.py', 'C:/get-pip.py') # - "%PYTHON%/python.exe C:/get-pip.py" # - "%PYTHON%/python.exe -m pip install ..." - "%WITH_COMPILER% %PYTHON%/Scripts/pip.exe install unittest2 ipaddress pypiwin32 wmi wheel setuptools pyopenssl psutil --upgrade" # 1.0.1 is the latest release supporting python 2.6 - "%WITH_COMPILER% %PYTHON%/Scripts/pip.exe install mock==1.0.1" - "%WITH_COMPILER% %PYTHON%/python.exe setup.py install" build: off test_script: - "%WITH_COMPILER% %PYTHON%/python pyftpdlib/test/runner.py" artifacts: - path: dist\* # on_success: # - might want to upload the content of dist/*.whl to a public wheelhouse skip_commits: message: skip-ci pyftpdlib-release-1.5.4/demo/000077500000000000000000000000001327314554200160665ustar00rootroot00000000000000pyftpdlib-release-1.5.4/demo/anti_flood_ftpd.py000077500000000000000000000047171327314554200216070ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. """ A FTP server banning clients in case of commands flood. If client sends more than 300 requests per-second it will be disconnected and won't be able to re-connect for 1 hour. """ from pyftpdlib.authorizers import DummyAuthorizer from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer class AntiFloodHandler(FTPHandler): cmds_per_second = 300 # max number of cmds per second ban_for = 60 * 60 # 1 hour banned_ips = [] def __init__(self, *args, **kwargs): FTPHandler.__init__(self, *args, **kwargs) self.processed_cmds = 0 self.pcmds_callback = \ self.ioloop.call_every(1, self.check_processed_cmds) def on_connect(self): # called when client connects. if self.remote_ip in self.banned_ips: self.respond('550 You are banned.') self.close_when_done() def check_processed_cmds(self): # called every second; checks for the number of commands # sent in the last second. if self.processed_cmds > self.cmds_per_second: self.ban(self.remote_ip) else: self.processed_cmds = 0 def process_command(self, *args, **kwargs): # increase counter for every received command self.processed_cmds += 1 FTPHandler.process_command(self, *args, **kwargs) def ban(self, ip): # ban ip and schedule next un-ban if ip not in self.banned_ips: self.log('banned IP %s for command flooding' % ip) self.respond('550 You are banned for %s seconds.' % self.ban_for) self.close() self.banned_ips.append(ip) def unban(self, ip): # unban ip try: self.banned_ips.remove(ip) except ValueError: pass else: self.log('unbanning IP %s' % ip) def close(self): FTPHandler.close(self) if not self.pcmds_callback.cancelled: self.pcmds_callback.cancel() def main(): authorizer = DummyAuthorizer() authorizer.add_user('user', '12345', '.', perm='elradfmwMT') authorizer.add_anonymous('.') handler = AntiFloodHandler handler.authorizer = authorizer server = FTPServer(('', 2121), handler) server.serve_forever(timeout=1) if __name__ == '__main__': main() pyftpdlib-release-1.5.4/demo/basic_ftpd.py000077500000000000000000000030401327314554200205360ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. """A basic FTP server which uses a DummyAuthorizer for managing 'virtual users', setting a limit for incoming connections and a range of passive ports. """ import os from pyftpdlib.authorizers import DummyAuthorizer from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer def main(): # Instantiate a dummy authorizer for managing 'virtual' users authorizer = DummyAuthorizer() # Define a new user having full r/w permissions and a read-only # anonymous user authorizer.add_user('user', '12345', os.getcwd(), perm='elradfmwMT') authorizer.add_anonymous(os.getcwd()) # Instantiate FTP handler class handler = FTPHandler handler.authorizer = authorizer # Define a customized banner (string returned when client connects) handler.banner = "pyftpdlib based ftpd ready." # Specify a masquerade address and the range of ports to use for # passive connections. Decomment in case you're behind a NAT. # handler.masquerade_address = '151.25.42.11' handler.passive_ports = range(60000, 65535) # Instantiate FTP server class and listen on 0.0.0.0:2121 address = ('', 2121) server = FTPServer(address, handler) # set a limit for connections server.max_cons = 256 server.max_cons_per_ip = 5 # start ftp server server.serve_forever() if __name__ == '__main__': main() pyftpdlib-release-1.5.4/demo/keycert.pem000066400000000000000000000035601327314554200202430ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIICXwIBAAKBgQC8ddrhm+LutBvjYcQlnH21PPIseJ1JVG2HMmN2CmZk2YukO+9L opdJhTvbGfEj0DQs1IE8M+kTUyOmuKfVrFMKwtVeCJphrAnhoz7TYOuLBSqt7lVH fhi/VwovESJlaBOp+WMnfhcduPEYHYx/6cnVapIkZnLt30zu2um+DzA9jQIDAQAB AoGBAK0FZpaKj6WnJZN0RqhhK+ggtBWwBnc0U/ozgKz2j1s3fsShYeiGtW6CK5nU D1dZ5wzhbGThI7LiOXDvRucc9n7vUgi0alqPQ/PFodPxAN/eEYkmXQ7W2k7zwsDA IUK0KUhktQbLu8qF/m8qM86ba9y9/9YkXuQbZ3COl5ahTZrhAkEA301P08RKv3KM oXnGU2UHTuJ1MAD2hOrPxjD4/wxA/39EWG9bZczbJyggB4RHu0I3NOSFjAm3HQm0 ANOu5QK9owJBANgOeLfNNcF4pp+UikRFqxk5hULqRAWzVxVrWe85FlPm0VVmHbb/ loif7mqjU8o1jTd/LM7RD9f2usZyE2psaw8CQQCNLhkpX3KO5kKJmS9N7JMZSc4j oog58yeYO8BBqKKzpug0LXuQultYv2K4veaIO04iL9VLe5z9S/Q1jaCHBBuXAkEA z8gjGoi1AOp6PBBLZNsncCvcV/0aC+1se4HxTNo2+duKSDnbq+ljqOM+E7odU+Nq ewvIWOG//e8fssd0mq3HywJBAJ8l/c8GVmrpFTx8r/nZ2Pyyjt3dH1widooDXYSV q6Gbf41Llo5sYAtmxdndTLASuHKecacTgZVhy0FryZpLKrU= -----END RSA PRIVATE KEY----- -----BEGIN CERTIFICATE----- MIICpzCCAhCgAwIBAgIJAP+qStv1cIGNMA0GCSqGSIb3DQEBBQUAMIGJMQswCQYD VQQGEwJVUzERMA8GA1UECBMIRGVsYXdhcmUxEzARBgNVBAcTCldpbG1pbmd0b24x IzAhBgNVBAoTGlB5dGhvbiBTb2Z0d2FyZSBGb3VuZGF0aW9uMQwwCgYDVQQLEwNT U0wxHzAdBgNVBAMTFnNvbWVtYWNoaW5lLnB5dGhvbi5vcmcwHhcNMDcwODI3MTY1 NDUwWhcNMTMwMjE2MTY1NDUwWjCBiTELMAkGA1UEBhMCVVMxETAPBgNVBAgTCERl bGF3YXJlMRMwEQYDVQQHEwpXaWxtaW5ndG9uMSMwIQYDVQQKExpQeXRob24gU29m dHdhcmUgRm91bmRhdGlvbjEMMAoGA1UECxMDU1NMMR8wHQYDVQQDExZzb21lbWFj aGluZS5weXRob24ub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8ddrh m+LutBvjYcQlnH21PPIseJ1JVG2HMmN2CmZk2YukO+9LopdJhTvbGfEj0DQs1IE8 M+kTUyOmuKfVrFMKwtVeCJphrAnhoz7TYOuLBSqt7lVHfhi/VwovESJlaBOp+WMn fhcduPEYHYx/6cnVapIkZnLt30zu2um+DzA9jQIDAQABoxUwEzARBglghkgBhvhC AQEEBAMCBkAwDQYJKoZIhvcNAQEFBQADgYEAF4Q5BVqmCOLv1n8je/Jw9K669VXb 08hyGzQhkemEBYQd6fzQ9A/1ZzHkJKb1P6yreOLSEh4KcxYPyrLRC1ll8nr5OlCx CMhKkTnR6qBsdNV0XtdU2+N25hqW+Ma4ZeqsN/iiJVCGNOZGnvQuvCAGWF8+J/f/ iHkC6gGdBJhogs4= -----END CERTIFICATE----- pyftpdlib-release-1.5.4/demo/md5_ftpd.py000077500000000000000000000024611327314554200201500ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. """ A basic ftpd storing passwords as hash digests (platform independent). """ from hashlib import md5 import os import sys from pyftpdlib.authorizers import AuthenticationFailed from pyftpdlib.authorizers import DummyAuthorizer from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer class DummyMD5Authorizer(DummyAuthorizer): def validate_authentication(self, username, password, handler): if sys.version_info >= (3, 0): password = md5(password.encode('latin1')) hash = md5(password).hexdigest() try: if self.user_table[username]['pwd'] != hash: raise KeyError except KeyError: raise AuthenticationFailed def main(): # get a hash digest from a clear-text password hash = md5(b'12345').hexdigest() authorizer = DummyMD5Authorizer() authorizer.add_user('user', hash, os.getcwd(), perm='elradfmwMT') authorizer.add_anonymous(os.getcwd()) handler = FTPHandler handler.authorizer = authorizer server = FTPServer(('', 2121), handler) server.serve_forever() if __name__ == "__main__": main() pyftpdlib-release-1.5.4/demo/multi_proc_ftp.py000077500000000000000000000014401327314554200214700ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. """ A FTP server which handles every connection in a separate process. Useful if your handler class contains blocking calls or your filesystem is too slow. POSIX only; requires python >= 2.6. """ from pyftpdlib.authorizers import DummyAuthorizer from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import MultiprocessFTPServer def main(): authorizer = DummyAuthorizer() authorizer.add_user('user', '12345', '.') handler = FTPHandler handler.authorizer = authorizer server = MultiprocessFTPServer(('', 2121), handler) server.serve_forever() if __name__ == "__main__": main() pyftpdlib-release-1.5.4/demo/multi_thread_ftp.py000077500000000000000000000013621327314554200217770ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. """ A FTP server which handles every connection in a separate thread. Useful if your handler class contains blocking calls or your filesystem is too slow. """ from pyftpdlib.authorizers import DummyAuthorizer from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import ThreadedFTPServer def main(): authorizer = DummyAuthorizer() authorizer.add_user('user', '12345', '.') handler = FTPHandler handler.authorizer = authorizer server = ThreadedFTPServer(('', 2121), handler) server.serve_forever() if __name__ == "__main__": main() pyftpdlib-release-1.5.4/demo/throttled_ftpd.py000077500000000000000000000021171327314554200214720ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. """ An FTP server which uses the ThrottledDTPHandler class for limiting the speed of downloads and uploads. """ import os from pyftpdlib.authorizers import DummyAuthorizer from pyftpdlib.handlers import FTPHandler from pyftpdlib.handlers import ThrottledDTPHandler from pyftpdlib.servers import FTPServer 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() pyftpdlib-release-1.5.4/demo/tls_ftpd.py000077500000000000000000000020731327314554200202640ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. """ An RFC-4217 asynchronous FTPS server supporting both SSL and TLS. Requires PyOpenSSL module (http://pypi.python.org/pypi/pyOpenSSL). """ import os from pyftpdlib.authorizers import DummyAuthorizer from pyftpdlib.handlers import TLS_FTPHandler from pyftpdlib.servers import FTPServer CERTFILE = os.path.abspath(os.path.join(os.path.dirname(__file__), "keycert.pem")) def main(): authorizer = DummyAuthorizer() authorizer.add_user('user', '12345', '.', perm='elradfmwMT') authorizer.add_anonymous('.') handler = TLS_FTPHandler handler.certfile = CERTFILE handler.authorizer = authorizer # requires SSL for both control and data channel # handler.tls_control_required = True # handler.tls_data_required = True server = FTPServer(('', 2121), handler) server.serve_forever() if __name__ == '__main__': main() pyftpdlib-release-1.5.4/demo/unix_daemon.py000077500000000000000000000126251327314554200207570ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. """A basic unix daemon using the python-daemon library: http://pypi.python.org/pypi/python-daemon Example usages: $ python unix_daemon.py start $ python unix_daemon.py stop $ python unix_daemon.py status $ python unix_daemon.py # foreground (no daemon) $ python unix_daemon.py --logfile /var/log/ftpd.log start $ python unix_daemon.py --pidfile /var/run/ftpd.pid start This is just a proof of concept which demonstrates how to daemonize the FTP server. You might want to use this as an example and provide the necessary customizations. Parts you might want to customize are: - UMASK, WORKDIR, HOST, PORT constants - get_server() function (to define users and customize FTP handler) Authors: - Ben Timby - btimby gmail.com - Giampaolo Rodola' - g.rodola gmail.com """ import atexit import errno import optparse import os import signal import sys import time from pyftpdlib.authorizers import UnixAuthorizer from pyftpdlib.filesystems import UnixFilesystem from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer # overridable options HOST = "" PORT = 21 PID_FILE = "/var/run/pyftpdlib.pid" LOG_FILE = "/var/log/pyftpdlib.log" WORKDIR = os.getcwd() UMASK = 0 def pid_exists(pid): """Return True if a process with the given PID is currently running.""" try: os.kill(pid, 0) except OSError as err: return err.errno == errno.EPERM else: return True def get_pid(): """Return the PID saved in the pid file if possible, else None.""" try: with open(PID_FILE) as f: return int(f.read().strip()) except IOError as err: if err.errno != errno.ENOENT: raise def stop(): """Keep attempting to stop the daemon for 5 seconds, first using SIGTERM, then using SIGKILL. """ pid = get_pid() if not pid or not pid_exists(pid): sys.exit("daemon not running") sig = signal.SIGTERM i = 0 while True: sys.stdout.write('.') sys.stdout.flush() try: os.kill(pid, sig) except OSError as err: if err.errno == errno.ESRCH: print("\nstopped (pid %s)" % pid) return else: raise i += 1 if i == 25: sig = signal.SIGKILL elif i == 50: sys.exit("\ncould not kill daemon (pid %s)" % pid) time.sleep(0.1) def status(): """Print daemon status and exit.""" pid = get_pid() if not pid or not pid_exists(pid): print("daemon not running") else: print("daemon running with pid %s" % pid) sys.exit(0) def get_server(): """Return a pre-configured FTP server instance.""" handler = FTPHandler handler.authorizer = UnixAuthorizer() handler.abstracted_fs = UnixFilesystem server = FTPServer((HOST, PORT), handler) return server def daemonize(): """A wrapper around python-daemonize context manager.""" def _daemonize(): pid = os.fork() if pid > 0: # exit first parent sys.exit(0) # decouple from parent environment os.chdir(WORKDIR) os.setsid() os.umask(0) # do second fork pid = os.fork() if pid > 0: # exit from second parent sys.exit(0) # redirect standard file descriptors sys.stdout.flush() sys.stderr.flush() si = open(LOG_FILE, 'r') so = open(LOG_FILE, 'a+') se = open(LOG_FILE, 'a+', 0) os.dup2(si.fileno(), sys.stdin.fileno()) os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(se.fileno(), sys.stderr.fileno()) # write pidfile pid = str(os.getpid()) with open(PID_FILE, 'w') as f: f.write("%s\n" % pid) atexit.register(lambda: os.remove(PID_FILE)) pid = get_pid() if pid and pid_exists(pid): sys.exit('daemon already running (pid %s)' % pid) # instance FTPd before daemonizing, so that in case of problems we # get an exception here and exit immediately server = get_server() _daemonize() server.serve_forever() def main(): global PID_FILE, LOG_FILE USAGE = "python [-p PIDFILE] [-l LOGFILE]\n\n" \ "Commands:\n - start\n - stop\n - status" parser = optparse.OptionParser(usage=USAGE) parser.add_option('-l', '--logfile', dest='logfile', help='the log file location') parser.add_option('-p', '--pidfile', dest='pidfile', default=PID_FILE, help='file to store/retreive daemon pid') options, args = parser.parse_args() if options.pidfile: PID_FILE = options.pidfile if options.logfile: LOG_FILE = options.logfile if not args: server = get_server() server.serve_forever() else: if len(args) != 1: sys.exit('too many commands') elif args[0] == 'start': daemonize() elif args[0] == 'stop': stop() elif args[0] == 'restart': try: stop() finally: daemonize() elif args[0] == 'status': status() else: sys.exit('invalid command') if __name__ == '__main__': sys.exit(main()) pyftpdlib-release-1.5.4/demo/unix_ftpd.py000077500000000000000000000015601327314554200204450ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. """A FTPd using local UNIX account database to authenticate users. It temporarily impersonate the system users every time they are going to perform a filesystem operations. """ from pyftpdlib.authorizers import UnixAuthorizer from pyftpdlib.filesystems import UnixFilesystem from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer def main(): authorizer = UnixAuthorizer(rejected_users=["root"], require_valid_shell=True) handler = FTPHandler handler.authorizer = authorizer handler.abstracted_fs = UnixFilesystem server = FTPServer(('', 21), handler) server.serve_forever() if __name__ == "__main__": main() pyftpdlib-release-1.5.4/demo/winnt_ftpd.py000077500000000000000000000020451327314554200206200ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. """A ftpd using local Windows NT account database to authenticate users (users must already exist). It also provides a mechanism to (temporarily) impersonate the system users every time they are going to perform filesystem operations. """ from pyftpdlib.authorizers import WindowsAuthorizer from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer def main(): authorizer = WindowsAuthorizer() # Use Guest user with empty password to handle anonymous sessions. # Guest user must be enabled first, empty password set and profile # directory specified. # authorizer = WindowsAuthorizer(anonymous_user="Guest", # anonymous_password="") handler = FTPHandler handler.authorizer = authorizer ftpd = FTPServer(('', 21), handler) ftpd.serve_forever() if __name__ == "__main__": main() pyftpdlib-release-1.5.4/docs/000077500000000000000000000000001327314554200160725ustar00rootroot00000000000000pyftpdlib-release-1.5.4/docs/Makefile000066400000000000000000000167221327314554200175420ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. PYTHON = python SPHINXOPTS = SPHINXBUILD = $(PYTHON) -m sphinx PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " epub3 to make an epub3" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" @echo " dummy to check syntax errors of document sources" .PHONY: clean clean: rm -rf $(BUILDDIR) .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: pickle pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." .PHONY: qthelp qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/psutil.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/psutil.qhc" .PHONY: applehelp applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." .PHONY: devhelp devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/psutil" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/psutil" @echo "# devhelp" .PHONY: epub epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." .PHONY: epub3 epub3: $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 @echo @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." .PHONY: latex latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." .PHONY: latexpdf latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: latexpdfja latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: text text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." .PHONY: man man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." .PHONY: texinfo texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." .PHONY: info info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." .PHONY: gettext gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." .PHONY: changes changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." .PHONY: linkcheck linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." .PHONY: doctest doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." .PHONY: coverage coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." .PHONY: xml xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." .PHONY: pseudoxml pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." .PHONY: dummy dummy: $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy @echo @echo "Build finished. Dummy builder generates no files." pyftpdlib-release-1.5.4/docs/README000066400000000000000000000006071327314554200167550ustar00rootroot00000000000000About ===== 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. In case you want, you need to install sphinx first: $ pip install sphinx Then run: $ make html You'll then have an HTML version of the doc at _build/html/index.html. pyftpdlib-release-1.5.4/docs/adoptions.rst000066400000000000000000000255561327314554200206410ustar00rootroot00000000000000========= 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. Please help us in keeping such list updated. 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 ------ .. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/fedora.png A `RPM packaged version `__ is available. FreeBSD ------- .. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/freebsd.gif 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 ------------- .. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/chrome.jpg `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. Smartfile --------- .. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/smartfile.jpg `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. Bazaar ------ .. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/bazaar.jpg `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. Python for OpenVMS ------------------ .. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/pyopenvms.png `OpenVMS `__ is an operating system that runs on the `VAX `__ and `Alpha `__ families of computers, 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 every new vmspython installation. http://www.vmspython.org/DownloadAndInstallationPython OpenERP ------- .. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/openerp.jpg `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 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 sophisticated video sharing site. pyftpdlib has been included in Plumi to allow resumable large video file uploads into `Zope `__. put.io FTP connector -------------------- .. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/putio.png 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 `__ Rackspace Cloud's FTP --------------------- .. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/rackspace-cloud-hosting.jpg `ftp-cloudfs `__ is a ftp server acting as a proxy to Rackspace `Cloud Files `__. 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 `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. Google Pages FTPd ----------------- .. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/google-pages.gif `gpftpd `__ is a pyftpdlib based FTP server you can connect to using your Google e-mail account. It redirects you to all files hosted on your `Google Pages `__ account giving you access to download them and upload new ones. Peerscape --------- .. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/peerscape.gif `Peerscape `__ is an experimental peer-to-peer social network implemented as an extension to the Firefox web browser. It implements a kind of serverless read-write web supporting third-party AJAX application development. Under the hood, your computer stores copies of your data, the data of your friends and the groups you have joined, and some data about, e.g., friends of friends. It also caches copies of other data that you navigate to. Computers that store the same data establish connections among themselves to keep it in sync. feitp-server ------------ An `extra layer `__ on top of pyftpdlib introducing multi processing capabilities and overall higher performances. Symbian Python FTP server ------------------------- .. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/symbianftp.png 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 --------------- The goal of this project is to extend Openbravo POS to use Windows Mobile Professional or Standard devices. It will import the data from Ob POS (originally in Postgres, later MySql). This data will reside in a database using sqlite3. Later a program will allow to sync by FTP or using a USB cable connected to the WinMob device. `link `__ Faetus ------ `Faetus `__ is a FTP 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 backup and archival program which can offer remote backup via a pyftpdlib-based S/FTP server. Aksy ---- `Aksy `__ is a Python module to 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 -------- `Imgserve `__ is a python image processing server designed to provide image processing service. It can utilize modern multicore CPU to achieve higher throughput and possibly better performance. It uses pyftpdlib to permit image downloading/uploading through FTP/FTPS. Shareme ------- Ever needed to share a directory between two computers? Usually this is done using NFS, FTP or Samba, which could be a pain to setup when you just want to move some files around. `Shareme `__ is a small FTP server that, without configuration files or manuals to learn, will publish your directory, and users can download from it and upload files and directory. Just open a shell and run ``shareme -d ~/incoming/`` ...and that's it! Zenftp ------ A simple service that bridges an FTP client with zenfolio via SOAP. Start zenftp.py, providing the name of the target photoset on Zenfolio, and then connect to localhost with your FTP client. `link `__ ftpmaster --------- A very simple FTP-based content management system (CMS) including an LDAP authorizer. `link `__ ShareFTP -------- A program functionally equivalent to Shareme project. `link `__ EasyFTPd -------- An end-user UNIX FTP server with focus on simplicity. It basically provides a configuration file interface over pyftpdlib to easily set up an FTP daemon. `link `__. Eframe ------ `Eframe `__ offers Python support for the BT EFrame 1000 digital photo frame. Fastersync ---------- A tool to synchronize data between desktop PCs, laptops, USB drives, remote FTP/SFTP servers, and different online data storages. `link `__ bftpd ----- A small easy to configure FTP server. `link `__ Web sites using pyftpdlib ========================= www.bitsontherun.com -------------------- .. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/bitsontherun.png http://www.bitsontherun.com www.adcast.tv ------------- .. image:: http://pyftpdlib.googlecode.com/svn-history/wiki/images/adcast.png http://www.adcast.tv http://www.adcast.tv www.netplay.it -------------- .. image:: http://pyftpdlib.googlecode.com/svn/wiki/images/netplay.jpg http://netplay.it/pyftpdlib-release-1.5.4/docs/api.rst000066400000000000000000000616141327314554200174050ustar00rootroot00000000000000============= 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 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. 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 pyftpdlib.ioloop.AsyncChat 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: >>> from pyftpdlib.authorizers import DummyAuthorizer >>> authorizer = DummyAuthorizer() >>> authorizer.add_user('user', 'password', '/home/user', perm='elradfmwMT') >>> authorizer.add_anonymous('/home/nobody') .. 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 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: Read permissions: - ``"e"`` = change directory (CWD, CDUP commands) - ``"l"`` = list files (LIST, NLST, STAT, MLSD, MLST, SIZE commands) - ``"r"`` = retrieve file from the server (RETR command) Write permissions: - ``"a"`` = append data to an existing file (APPE command) - ``"d"`` = delete file or directory (DELE, RMD commands) - ``"f"`` = rename file or directory (RNFR, RNTO commands) - ``"m"`` = create directory (MKD command) - ``"w"`` = store a file to the server (STOR, STOU commands) - ``"M"`` = change file mode / 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. .. 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. .. method:: override_perm(username, directory, perm, recursive=False) Override user permissions for a given directory. .. method:: validate_authentication(username, password, handler) Raises :class:`pyftpdlib.authorizers.AuthenticationFailed` if the supplied username and password doesn't match the stored credentials. *Changed in 1.0.0: new handler parameter.* *Changed in 1.0.0: an exception is now raised for signaling a failed authenticaiton as opposed to returning a bool.* .. method:: impersonate_user(username, password) Impersonate another user (noop). It is always called before accessing the filesystem. By default it does nothing. The subclass overriding this method is expected to provide a mechanism to change the current user. .. 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:: remove_user(username) Remove a user from the virtual user table. 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: >>> 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: .. 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). *New in version 5.0* .. data:: banner String sent when client connects (default ``"pyftpdlib %s ready." %__ver__``). .. data:: max_login_attempts Maximum number of wrong authentications before disconnecting (default ``3``). .. data:: permit_foreign_addresses Whether enable FXP feature (default ``False``). .. data:: permit_privileged_ports Set to ``True`` if you want to permit active connections (PORT) over privileged ports (not recommended, 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). .. 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 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* .. 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``). .. 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* .. 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* .. 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. *New in version 0.7.0* .. data:: auth_failed_timeout The amount of time the server waits before sending a response in case of failed authentication. *New in version 1.5.0* Follows a list of callback methods that can be overridden in a subclass. For blocking operations read the FAQ on how to run time consuming tasks. .. method:: on_connect() Called when client connects. *New in version 1.0.0* .. method:: on_disconnect() Called when connection is closed. *New in version 1.0.0* .. method:: on_login(username) Called on user login. *New in version 0.6.0* .. method:: on_login_failed(username, password) Called on failed user login. *New in version 0.7.0* .. 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. *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. .. method:: on_file_received(file) Called every time a file has been successfully received. *file* is the absolute name 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. *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* 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. *Changed in version 1.0.0: added ioloop argument.* .. data:: timeout The timeout which roughly is the maximum time we permit data transfers to stall for with no progress. If the timeout triggers, the remote client will be kicked off (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 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 :class:`pyftpdlib.handlers.DTPHandler` to set transfer rates limits for both downloads and/or uploads (see the `demo script `__ showing the example usage). .. data:: read_limit The maximum number of bytes to read (receive) in one second (defaults to ``0`` == no limit) .. data:: write_limit The maximum number of bytes to write (send) in one second (defaults to ``0`` == 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. *Changed in version 1.0.0: added ioloop argument.* *Changed in version 1.2.0: address can also be a pre-existing socket object.* *Changed in version 1.2.0: Added backlog argument.* *Changed in version 1.5.4: Support for the context manager protocol was added. Exiting the context manager is equivalent to calling :meth:`close_all`.* >>> from pyftpdlib.servers import FTPServer >>> address = ('127.0.0.1', 21) >>> server = FTPServer(address, handler) >>> server.serve_forever() It 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: ... server.serve_forever() .. data:: max_cons Number of maximum simultaneous connections accepted (default ``512``). .. data:: max_cons_per_ip Number of maximum connections accepted for the same IP address (default ``0`` == no limit). .. method:: serve_forever(timeout=None, blocking=True, handle_exit=True) Starts the asynchronous IO loop. *Changed in version 1.0.0: no longer a classmethod; 'use_poll' and 'count' *parameters were removed. 'blocking' and 'handle_exit' parameters were *added* .. method:: close() Stop accepting connections without disconnecting currently connected clients. .. method:: close_all() Tell :meth:`server_forever` loop to stop and wait until it does. Also all connected clients will be closed. *Changed in version 1.0.0: 'map' and 'ignore_all' parameters were removed.* 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(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. *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()* .. data:: cwd User's current working directory ("virtual"). *Changed in version 0.7.0: support setattr()* .. 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"``). .. 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"``). .. 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"``. .. 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``). .. method:: open(filename, mode) Wrapper around `open() `_ builtin. .. method:: mkdir(path) .. method:: chdir(path) .. method:: rmdir(path) .. method:: remove(path) .. method:: rename(src, dst) .. method:: chmod(path, mode) .. method:: stat(path) .. method:: lstat(path) .. method:: readlink(path) Wrappers around corresponding `os `_ module functions. .. method:: isfile(path) .. method:: islink(path) .. method:: isdir(path) .. method:: getsize(path) .. method:: getmtime(path) .. method:: realpath(path) .. method:: lexists(path) Wrappers around corresponding `os.path `_ module functions. .. method:: mkstemp(suffix='', prefix='', dir=None, mode='wb') Wrapper around `tempfile.mkstemp `_. .. method:: listdir(path) Wrapper around `os.listdir `_. It is expected to return a list of unicode strings or a generator yielding unicode 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. 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: .. 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. .. data:: keyfile The path of the file containing the private RSA key; can be omittetted if certfile already contains the private key (defaults: ``None``). .. data:: ssl_protocol The desired SSL protocol version to use. This defaults to `SSL.SSLv23_METHOD` which will negotiate the highest protocol that both the server and your installation of OpenSSL support. .. data:: ssl_options specific OpenSSL options. These default to: `SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3 | SSL.OP_NO_COMPRESSION` disabling SSLv2 and SSLv3 versions and SSL compression algorithm which are considered insecure. Can be set to None in order to improve compatibilty with older (insecure) FTP clients. .. versionadded:: 1.6.0 .. data:: ssl_context A `SSL.Context `__ instance which was previously configured. If specified :data:`ssl_protocol` and :data:`ssl_options` parameters will be 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``). .. 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``). 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. *New in version 0.6.0* .. method:: override_user(username=None, password=None, homedir=None, perm=None, anonymous_user=None, msg_login=None, msg_quit=None) Overrides one or more options specified in the class constructor for a specific user. Example: >>> from pyftpdlib.authorizers import UnixAuthorizer >>> auth = UnixAuthorizer(rejected_users=["root"]) >>> auth = UnixAuthorizer(allowed_users=["matt", "jay"]) >>> auth = UnixAuthorizer(require_valid_shell=False) >>> auth.override_user("matt", password="foo", perm="elr") .. 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. *New in version 0.6.0* Extended filesystems -------------------- .. class:: pyftpdlib.filesystems.UnixFilesystem(root, cmd_channel) Represents the real UNIX filesystem. Differently from :class:`pyftpdlib.filesystems.AbstractedFS` the client will login into /home/ and will be able to escape its home directory and navigate the real filesystem. Use it in conjuction with :class:`pyftpdlib.authorizers.UnixAuthorizer` to implement a "real" UNIX FTP server (see `demo/unix_ftpd.py `__). *New in version 0.6.0* Extended servers ---------------- .. class:: pyftpdlib.servers.ThreadedFTPServer(address_or_socket, handler, ioloop=None, backlog=5) A modified version of base :class:`pyftpdlib.servers.FTPServer` class which spawns a thread every time a new connection is established. Differently from base FTPServer class, the handler will be free to block without hanging the whole IO loop. *New in version 1.0.0* *Changed in 1.2.0: added ioloop parameter; address can also be a pre-existing *socket.* .. class:: pyftpdlib.servers.MultiprocessFTPServer(address_or_socket, handler, ioloop=None, backlog=5) A modified version of base :class:`pyftpdlib.servers.FTPServer` class which spawns a process every time a new connection is established. Differently from base FTPServer class, the handler will be free to block without hanging the whole IO loop. *New in version 1.0.0* *Changed in 1.2.0: added ioloop parameter; address can also be a pre-existing socket.* *Availability: POSIX + Python >= 2.6* pyftpdlib-release-1.5.4/docs/benchmarks.rst000066400000000000000000000325061327314554200207470ustar00rootroot00000000000000========== Benchmarks ========== pyftpdlib 0.7.0 vs. pyftpdlib 1.0.0 ----------------------------------- +-----------------------------------------+-----------------+----------------+------------+ | *benchmark type* | *0.7.0* | *1.0.0* | *speedup* | +=========================================+=================+================+============+ | STOR (client -> server) | 528.63 MB/sec | 585.90 MB/sec | **+0.1x** | +-----------------------------------------+-----------------+----------------+------------+ | RETR (server -> client) | 1702.07 MB/sec | 1652.72 MB/sec | -0.02x | +-----------------------------------------+-----------------+----------------+------------+ | 300 concurrent clients (connect, login) | 1.70 secs | 0.19 secs | **+8x** | +-----------------------------------------+-----------------+----------------+------------+ | STOR (1 file with 300 idle clients) | 60.77 MB/sec | 585.59 MB/sec | **+8.6x** | +-----------------------------------------+-----------------+----------------+------------+ | RETR (1 file with 300 idle clients) | 63.46 MB/sec | 1497.58 MB/sec | **+22.5x** | +-----------------------------------------+-----------------+----------------+------------+ | 300 concurrent clients (RETR 10M file) | 4.68 secs | 3.41 secs | **+0.3x** | +-----------------------------------------+-----------------+----------------+------------+ | 300 concurrent clients (STOR 10M file) | 10.13 secs | 8.78 secs | **+0.1x** | +-----------------------------------------+-----------------+----------------+------------+ | 300 concurrent clients (QUIT) | 0.02 secs | 0.02 secs | 0x | +-----------------------------------------+-----------------+----------------+------------+ pyftpdlib vs. proftpd 1.3.4 --------------------------- +-----------------------------------------+-----------------+----------------+------------+ | *benchmark type* | *pyftpdlib* | *proftpd* | *speedup* | +=========================================+=================+================+============+ | STOR (client -> server) | 585.90 MB/sec | 600.49 MB/sec | -0.02x | +-----------------------------------------+-----------------+----------------+------------+ | RETR (server -> client) | 1652.72 MB/sec | 1524.05 MB/sec | **+0.08** | +-----------------------------------------+-----------------+----------------+------------+ | 300 concurrent clients (connect, login) | 0.19 secs | 9.98 secs | **+51x** | +-----------------------------------------+-----------------+----------------+------------+ | STOR (1 file with 300 idle clients) | 585.59 MB/sec | 518.55 MB/sec | **+0.1x** | +-----------------------------------------+-----------------+----------------+------------+ | RETR (1 file with 300 idle clients) | 1497.58 MB/sec | 1478.19 MB/sec | 0x | +-----------------------------------------+-----------------+----------------+------------+ | 300 concurrent clients (RETR 10M file) | 3.41 secs | 3.60 secs | **+0.05x** | +-----------------------------------------+-----------------+----------------+------------+ | 300 concurrent clients (STOR 10M file) | 8.60 secs | 11.56 secs | **+0.3x** | +-----------------------------------------+-----------------+----------------+------------+ | 300 concurrent clients (QUIT) | 0.03 secs | 0.39 secs | **+12x** | +-----------------------------------------+-----------------+----------------+------------+ pyftpdlib vs. vsftpd 2.3.5 -------------------------- +-----------------------------------------+----------------+----------------+-------------+ | *benchmark type* | *pyftpdlib* | *vsftpd* | *speedup* | +=========================================+================+================+=============+ | STOR (client -> server) | 585.90 MB/sec | 611.73 MB/sec | -0.04x | +-----------------------------------------+----------------+----------------+-------------+ | RETR (server -> client) | 1652.72 MB/sec | 1512.92 MB/sec | **+0.09** | +-----------------------------------------+----------------+----------------+-------------+ | 300 concurrent clients (connect, login) | 0.19 secs | 20.39 secs | **+106x** | +-----------------------------------------+----------------+----------------+-------------+ | STOR (1 file with 300 idle clients) | 585.59 MB/sec | 610.23 MB/sec | -0.04x | +-----------------------------------------+----------------+----------------+-------------+ | RETR (1 file with 300 idle clients) | 1497.58 MB/sec | 1493.01 MB/sec | 0x | +-----------------------------------------+----------------+----------------+-------------+ | 300 concurrent clients (RETR 10M file) | 3.41 secs | 3.67 secs | **+0.07x** | +-----------------------------------------+----------------+----------------+-------------+ | 300 concurrent clients (STOR 10M file) | 8.60 secs | 9.82 secs | **+0.07x** | +-----------------------------------------+----------------+----------------+-------------+ | 300 concurrent clients (QUIT) | 0.03 secs | 0.01 secs | +0.14x | +-----------------------------------------+----------------+----------------+-------------+ pyftpdlib vs. Twisted 12.3 -------------------------- By using *sendfile()* (Twisted *does not* support sendfile()): +-----------------------------------------+----------------+----------------+-------------+ | *benchmark type* | *pyftpdlib* | *twisted* | *speedup* | +=========================================+=================+================+============+ | STOR (client -> server) | 585.90 MB/sec | 496.44 MB/sec | **+0.01x** | +-----------------------------------------+----------------+----------------+-------------+ | RETR (server -> client) | 1652.72 MB/sec | 283.24 MB/sec | **+4.8x** | +-----------------------------------------+----------------+----------------+-------------+ | 300 concurrent clients (connect, login) | 0.19 secs | 0.19 secs | +0x | +-----------------------------------------+----------------+----------------+-------------+ | STOR (1 file with 300 idle clients) | 585.59 MB/sec | 506.55 MB/sec | **+0.16x** | +-----------------------------------------+----------------+----------------+-------------+ | RETR (1 file with 300 idle clients) | 1497.58 MB/sec | 280.63 MB/sec | **+4.3x** | +-----------------------------------------+----------------+----------------+-------------+ | 300 concurrent clients (RETR 10M file) | 3.41 secs | 11.40 secs | **+2.3x** | +-----------------------------------------+----------------+----------------+-------------+ | 300 concurrent clients (STOR 10M file) | 8.60 secs | 9.22 secs | **+0.07x** | +-----------------------------------------+----------------+----------------+-------------+ | 300 concurrent clients (QUIT) | 0.03 secs | 0.09 secs | **+2x** | +-----------------------------------------+----------------+----------------+-------------+ By using plain *send()*: +-----------------------------------------+----------------+---------------+--------------+ | *benchmark type* |tpdlib* | *twisted* | *speedup* | +=========================================+================+===============+==============+ | RETR (server -> client) | 894.29 MB/sec | 283.24 MB/sec | **+2.1x** | +-----------------------------------------+----------------+---------------+--------------+ | RETR (1 file with 300 idle clients) | 900.98 MB/sec | 280.63 MB/sec | **+2.1x** | +-----------------------------------------+----------------+---------------+--------------+ Memory usage ------------ *Values on UNIX are calculated as (rss - shared).* +------------------------------------------+-------------+-----------------+----------------+----------------+ | *benchmark type* | *pyftpdlib* | *proftpd 1.3.4* | *vsftpd 2.3.5* | *twisted 12.3* | +==========================================+=============+=================+================+================+ | Starting with | 6.7M | 1.4M | 352.0K | 13.4M | +------------------------------------------+-------------+-----------------+----------------+----------------+ | STOR (1 client) | 6.7M | 8.5M | 816.0K | 13.5M | +------------------------------------------+-------------+-----------------+----------------+----------------+ | RETR (1 client) | 6.8M | 8.5M | 816.0K | 13.5M | +------------------------------------------+-------------+-----------------+----------------+----------------+ | 300 concurrent clients (connect, login) | **8.8M** | 568.6M | 140.9M | 13.5M | +------------------------------------------+-------------+-----------------+----------------+----------------+ | STOR (1 file with 300 idle clients) | **8.8M** | 570.6M | 141.4M | 13.5M | +------------------------------------------+-------------+-----------------+----------------+----------------+ | RETR (1 file with 300 idle clients) | **8.8M** | 570.6M | 141.4M | 13.5M | +------------------------------------------+-------------+-----------------+----------------+----------------+ | 300 concurrent clients (RETR 10.0M file) | **10.8M** | 568.6M | 140.9M | 24.5M | +------------------------------------------+-------------+-----------------+----------------+----------------+ | 300 concurrent clients (STOR 10.0M file) | **12.6** | 568.7M | 140.9M | 24.7M | +------------------------------------------+-------------+-----------------+----------------+----------------+ 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. Setup ----- The following setup was used before running every benchmark: proftpd ^^^^^^^ :: # /etc/proftpd/proftpd.conf MaxInstances 2000 ...followed by: :: $ sudo service proftpd restart vsftpd ^^^^^^ :: # /etc/vsftpd.conf local_enable=YES write_enable=YES max_clients=2000 max_per_ip=2000 ...followed by: :: $ sudo service vsftpd restart twisted FTP server ^^^^^^^^^^^^^^^^^^ :: from twisted.protocols.ftp import FTPFactory, FTPRealm from twisted.cred.portal import Portal from twisted.cred.checkers import AllowAnonymousAccess, FilePasswordDB from twisted.internet import reactor import resource soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) resource.setrlimit(resource.RLIMIT_NOFILE, (hard, hard)) open('pass.dat', 'w').write('user:some-passwd') p = Portal(FTPRealm('./'), [AllowAnonymousAccess(), FilePasswordDB("pass.dat")]) f = FTPFactory(p) reactor.listenTCP(21, f) reactor.run() ...followed by: :: $ sudo python twist_ftpd.py pyftpdlib ^^^^^^^^^ The following patch was applied first: :: Index: pyftpdlib/servers.py =================================================================== --- pyftpdlib/servers.py (revisione 1154) +++ pyftpdlib/servers.py (copia locale) @@ -494,3 +494,10 @@ def _map_len(self): return len(multiprocessing.active_children()) + +import resource +soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) +resource.setrlimit(resource.RLIMIT_NOFILE, (hard, hard)) +FTPServer.max_cons = 0 ...followed by: :: $ sudo python demo/unix_daemon.py The `benchmark script `__ was run as: :: python scripts/ftpbench -u USERNAME -p PASSWORD -b all -n 300 ...and for the memory test: :: python scripts/ftpbench -u USERNAME -p PASSWORD -b all -n 300 -k FTP_SERVER_PID pyftpdlib-release-1.5.4/docs/conf.py000066400000000000000000000252411327314554200173750ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # pyftpdlib documentation build configuration file, created by # sphinx-quickstart on Wed Oct 19 21:54:30 2016. # # 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 datetime import os PROJECT_NAME = "pyftpdlib" AUTHOR = u"Giampaolo Rodola" THIS_YEAR = str(datetime.datetime.now().year) HERE = os.path.abspath(os.path.dirname(__file__)) def get_version(): INIT = os.path.abspath(os.path.join(HERE, '..', 'pyftpdlib', '__init__.py')) with open(INIT, 'r') as f: for line in f: if line.startswith('__ver__'): ret = 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") VERSION = get_version() # 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 = '2009-%s, %s' % (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 # 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 = '%s-doc' % PROJECT_NAME # -- 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', u'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', u'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', u'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', ], } pyftpdlib-release-1.5.4/docs/faqs.rst000066400000000000000000000406511327314554200175640ustar00rootroot00000000000000==== FAQs ==== .. contents:: Table of Contents Introduction ============ What is pyftpdlib? ------------------ pyftpdlib is a high-level library to easily write asynchronous portable FTP servers with `Python `__. What is Python? --------------- Python is an interpreted, interactive, object-oriented, easy-to-learn programming language. It is often compared to *Tcl, Perl, Scheme* or *Java*. 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: .. code-block:: sh $ sudo python -m pyftpdlib [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 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 ``python -m pyftpdlib --help`` to see all available options). Examples: Anonymous FTP server with write access: .. code-block:: sh $ sudo python -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 $ python -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 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 `__. Which Python versions are compatible? ------------------------------------- From *2.6* to *3.4*. Python 2.4 and 2.5 support has been removed starting from version 0.6.0. The latest version supporting Python 2.3 is `pyftpdlib 1.4.0 `__. Python 2.3 support has been removed starting from version 0.6.0. The latest version supporting Python 2.3 is `pyftpdlib 0.5.2 `__. On which platforms can pyftpdlib be used? ----------------------------------------- pyftpdlib should work on any platform where **select()**, **poll()**, **epoll()** or **kqueue()** system calls are available and on any Python implementation which refers to *cPython 2.6* or superior. 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. Usage ===== 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. 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. 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 invoke ``self.add_channel()``. This is fundamental when working with threads to avoid race conditions, dead locks etc. .. code-block:: python class MyHandler(FTPHandler): def on_file_received(self, file): def blocking_task(): time.sleep(5) self.add_channel() self.del_channel() threading.Thread(target=blocking_task).start() Another possibility is to `change the default concurrency model `__. Why do I get socket.error "Permission denied" error on ftpd starting? --------------------------------------------------------------------- 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? ---------------------------------------------------------- Just modify `FTPHandler.banner `__. Can 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. 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. 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? ------------ 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? --------------------------- 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. 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 `__ Implementation ============== sendfile() ---------- Starting from version 0.7.0 if `pysendfile `__ module is installed 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: .. code-block:: python from pyftpdlib.handlers import FTPHandler handler = FTPHandler handler.use_senfile = False ... Globbing / STAT command implementation -------------------------------------- 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. 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 `__. 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: .. code-block:: python >>> from pyftpdlib.servers import FTPServer >>> address = ("::1", 21) # listen on localhost, port 21 >>> ftpd = FTPServer(address, FTPHandler) >>> 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. 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 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. Does pyftpdlib support FTP over TLS/SSL (FTPS)? ----------------------------------------------- Yes, starting from version 0.6.0, see: `Does pyftpdlib support FTP over TLS/SSL (FTPS)?`_ 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: .. code-block:: python from pyftpdlib.handlers import FTPHandler proto_cmds = FTPHandler.proto_cmds.copy() proto_cmds.update( {'SITE RMTREE': dict(perm='R', auth=True, arg=True, help='Syntax: SITE RMTREE path (remove directory tree).')} ) class CustomizedFTPHandler(FTPHandler): proto_cmds = proto_cmds def ftp_SITE_RMTREE(self, line): """Recursively remove a directory tree.""" # implementation here # ... pyftpdlib-release-1.5.4/docs/images/000077500000000000000000000000001327314554200173375ustar00rootroot00000000000000pyftpdlib-release-1.5.4/docs/images/bazaar.jpg000066400000000000000000000126521327314554200213070ustar00rootroot00000000000000ÿØÿàJFIFHHÿá€ExifMM*JR(‡iZG€ÿG€ÿ d fÿÛC      ÿÛC  ÿÀfd"ÿÄ ÿĵ}!1AQa"q2‘¡#B±ÁRÑð$3br‚ %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚáâãäåæçèéêñòóôõö÷øùúÿÄ ÿĵw!1AQaq"2B‘¡±Á #3RðbrÑ $4á%ñ&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz‚ƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚâãäåæçèéêòóôõö÷øùúÿÚ ?ýü¢Š(¢Š(¯Î?ø*—üz÷áÿáIþÁv'ƵgR\[F.!Ðäa÷Bô’à›äŒ|Îp6šŸðVïø+Nµ xÍ?g¿ø'ÂIâ/‹Þ#o±_^ØâEЇ̪ßtM·,YˆX”b0(ÿ‚qÁ64/؇Âj¾&–/|M×ÐÉ­ëÒæFVs¹ ·gù„{¹g8iæl ª¿”xƒâ&…ð·ošR¿$³©ýæÖ±¤ŸU­G¤}ÔÛõ²ì¶¦2vZ%»íåç/Ë®§Ï·>ý³àž‡Æ[O‰ZÅ©}»Ç¼š[¨`Nw2NUžB…AÚñƒÒßø'ÏüCÀðQ?ƒ©âo„w&ÛR³Û±¢Ü0û^•1uÀûÈpJÈ8`;@§×­~{þØŸ±·?a¯ŒíûHÁ5ckëÓø«Â–èM®¡nNé^8ïFpKÄ9R7¦ ~eáÇ‹’Åb~£šrÂr~ì’P„®ô„’²ŒºB}tŒï¤—«šd¾ÊÒÚ[­Úó]ü×ͲWÏðNŸø(Ç‚à£añ'ÃY–Ë[²U‹\Ðå*r:üq¶ IÃØ‚Еý;‡ÄCxz5Õ>©®+(¸»Q[ˆ(¢Š(¢Š+ósþ ÕÿfÖ¼9ã4ýŸ?àŸHþ#øÁâ6ûíí–$_«˜û¢m¹bÌvÄ ³v{þ óÿ]Õ¾xŠ€°¤oâ/>+"Òil± ðò:ç$ýÑ9\·ÍòÆ Èø£ÿßÿ‚si±‚gÔ¼Q<~"ø™âEóµývBd}Ìw´;üÞXnYÍ# ÍÑU?&ñÄ<' aÓ”®¡ùøömö¤žïzÝ^íÛõ²Ü¶xÉÙh–ï·§÷¿-÷þ ÇÿâÑaÿO¨ø†tñÄ¿¯™¯kÒæF,Ç{A?Ì# ÉcóHÃsc ©©ÿý»,ÿc?Z¦ƒ®«ãMréV3a…în|µ$ ‚ì@̾ÉñkâžðK᮵âψ7"ÓGЭZêáøÜÀp¨€‘¹ÝŠ¢¯ñ3(šü9ý¡>SARI¾RH(Ïô]~üøÑ®þÎÿ´ü7”&©£K»Êf"+è[‰måÿa×cµ‡*+÷àGƽöˆøK¢xÇáìÆ]3[·ª¶<ËwlÈ;:8daꦼÏxpv=bpqÿd¬ß/÷%Öc~—Zò¶o’æ_^¥Ë7ïÇ5ßüÿàŸþÙÿ±§¿bMûIÿÁ6ƒØê,×)ð½ºm;¦‘ _¿c2D9SûÄÁ~†ÿÁ8ÿࣾ ÿ‚|‹Ä_¦[ zÁR-sC–@nt¹ÈÿÇâl’†³P•ùÑûl~É~2ÿ‚üpÿ†šÿ‚vÆlþÄÍ?‹¼3n‡ìÓÀNé¥XWï@ØÌˆ9Œ"à³î|*ñF£<«3Ÿï4Œ&Þ“[*soít§7þifyÙÆP¢z+N«·šòî¾hýŸ¢¼'þ õûx/þ ð.ÓÅß g[{øBÁ¬i¸7:MÎ9Çu=UÇ 9õÝ«ú›ˆ†& pÿ‚ŸT×Fº£äåfQEl ¯?à¯ðUÛßÙÚê×à×ìyx—ã—ŒBÚÛÁj‚oìq+¯O8Œ²«p o|(»¯ø+üîÓö øak¢|5€kÿû‡†š§MY#ó7þ eûOO¯|EÓ>xzåâÓt(£Õu•¯Ún¤\ÁqÊÇó:•-2÷ŒcáPC”æ¿wþ/þÌøõwmsñoº>·uh†8§¸·V‘œíÝŒ‘ž@=2qÔ×ã÷íõð×DøCûcøóÃßôø4½ÂâÓìö°¨T‹}…³¶é–f?Rø'ÆX,mpö á*tÜå>dÔ¥Í'k'«–—½’K¡ñüA€6ñSîì•¶Väy }‹ÿmýªßáwÇ ~øŽï:Ù¦±›"ÓQDÉÚ; cRmÑ©êN|ö?ðw‡þ!þÓþÐ~'[Áu¤k7FÑ’uVˆ;.C2·ÊpªøÜÜWŒâ¿^~þÂÿ ¾\Å7„<+akýªàøÏû)ÛÝ_|ñ5Ò[ø‡@G>M vÿw b`›ø˜Û†ÿ©|&ñ6¦>PÊó _•¢Ûþ*[E·ÿ/b¾ ?{’wå‘ñùÎT¨Þµ%îu_Ëçþë¦ëª?qè®#ösý¡ŽŸ?fö_|gðʭťͼŸgþÚÉ’.6L¤ÝAÀ'¯˜ÿÁ1?à¢òþÓ:]çÃßÚðÿÆ_îµÕ,.£û;ê‹ÚÓ¤gdbHÀàüÊ6¶ù7Æn ÆóÇ7¤Ü•8F#«åQÑT÷%öÖñ›mßšç×äXø+Ж—m§ÞýšéÝz^QEüì}@Wâ÷üþOïâ?ý|Xÿé¶Ò¿hkñƒþ ž_íýñÓ{ùé–gú×î¿Gßù(«ÿ׉éÊgÎñ7û¬Ä¿&x=Ž£u¢ê6×Úßg¿°™.­eÆ|¹Qƒ#c¸ Çûû$~Ж?µÀøÃFdY¯ _ÀZÒí>Y¢oB{{×áe}9ÿ¶ý±¿á˜>7aøÖëÊðO¦ŽÞíœüšmï ײ·¹ôØÇ„5úçŒü.%ÊÖ? ׿췔>ÒójÜËæ–²Xݺ €UÛ†#åH+÷ýôÜ1´ù%¿Gkz4Ö=ќП+?4?à›_ðQ«_ÛÃ÷žø©j<3ñsÂa ×tYÐÀÓ˜ÛcÜC|Ànáã<ÆÇ‚¤ýO_=ÿÁ\¿à“ڟů[üvý†¥o |mð¡l¶„D\ÐÛ¤.¯HF„¸9àðq‡ù¶ 'ÏêÕÇV8JŒ¢œä¢¯ÏM¥vÒ»I•Ä*b0Ñ(¹>dì•ÞÌðOØsàÖ‡ñ÷ö–Òü1ñÓíš]휦E Qã&kx÷Æã”p²¾r v?¶Gü+dzg‰Ý<#a¨øÛÁÚœ†;FÚg·ÝÀ†õW  ãÎâ6ëònÿÁ1¾øŸÂ?¶>‡âýRÒìŤ–â[” ¦FžÝ‚ŒÎ#søWëœÐ¥Äl“ªº0ÁVê+êø×Åw ñMjÙMxÖ¡(A8ósÓ½ž«•èÓzÙ«ìï¡ÍÉ©âpPŽ"2»ÖÖ{ù¯Ìó/ØÎo7ìÍá~(H×ÕžŸ­ÅÃgý(¢…ó9 àààd`÷¬¯ÛöØðŸì1ðnãŤ7× Ñô˜ä q«\‹×j.Ay!AîJ«]ý°ÿl?þÄ¿®|Wñ^çÖ7M…‡Úµ[ŒdEŸÀ³žr{óü“þ ÷ã?ø)_Ç[Ú[þ +dÑøf6Y|á9”‹y!VÝ´MÒÝ~ðff%ÛåÆÿˆðóÃúÜa‰úö2-a¹¶^ë©-Ü!Ú+íËh­½c»5Í#ƒ³¥ñýö]ߟeÔ×ÿ‚XÿÁ7ü[ûd|c‹ö¤ÿ‚ÛµÝÝë%Ï„<5qX-!SºšÎÈÓ9Š3É$ÈÙfÍ~²ª„PÀµ$Q%¼J*¢ ª£@ì:¿µ²¼²–YJ4ᬒÑY$¶ŒWHÇ¢õní¶|JŽ£»þ¼ß˜QEé™…Q@~zÿÁV¿à“ŸÄßCñßöŸþo¾"íãµ+^#T\p~_8®W-òȧcäWô*ŠâÇ`)ãé¸Mkf¶¾ú4ÓÑÅ­%£_&®pz_ðOÎ_ø'ü+Lý³|)y¤xÂÏþŠÍ¿ˆü=:´RE"6Æš›Ë/ÁSóFÇkuV¤«Ã¿à©ŸðJ}K⿊íþ9~Ã+á?Ž^ÅÈû9X¡ñ*"àÃ0?)”®P3|®§cü¸)ûþßšgí‰á{Ý'ÅÖG¿<*M·‰<7r S[J‡cM ?Ìb-ÁæŽÖþã|4©ÃUgÀCýŸíGg~©îé·ðËx¿vZÙËî2œÕbR¥UûÝø=×]×—Ñ5å¶/í‹àÿØŸá׋>,ÝdœÅ§iÑ0ûV­qŒˆ¢Sù³žr{SöÑý·|û|-“Ä_îüÛËÑéZD>Ù«LÝOÝA‘ºCò¨#©*­à°ü‹ÆÿðPÿv¿´_üÊÅàÑ#"_ø&u+Pgto4MÒ!ÃaºVùŸŒ)ó|;ðæ¿VŽ+°©ôÑÔkxDz_n{Gey4sLÒ84áMûÿ—›ý_B‡üïþ ÝãOø)—ÆkOÚ;þ Ghñø]—Áþ •X[´·FòDÝ èÀ0Ý1ù›åÀo×[khì­Ò8Ò(¢P¨ˆ0ª@è(‚µ…#¶EŽ8ÀUU @>¿µòœ¢ŽSF4©E+$’JÊ)m®‰}íêîÏ„«UÕm·ÿ÷aEW¬dQEQEQEñ¿ü7þ ;¥~Ó^+²øŸû×嬽ñk¢†ím .;LÓíò]¸'¤KØG>°ƒ¬J)ii4˜ìõTzúB§Y–62[¢š4K³Ê·$JûÀ±•J•Î 5Qb•.FãÒä…Š*¬© í¥ŠÒÒLx# jÊn®Éóaw±cûòääÕ«WÌzÆÍ—£·ßÞ|ˉuX<¨M‘¨@S'‹©4,, 7ƒr„*³‡G«—V8öÞ¸f:°‹E–ûªŒMc7õ<2¤pßi4çh…Þ’1ÃÞØ©qEVF룹]-·ÄŠŽ"­ÿ-±‹‰”+[¶¢_&„¹Á±Ù ` }`ÕĪmKOò§°Ã"=dÕÆVÔÇÂ>½˜Âo³6 ôÏ€ª-³¢iC\™•QÖü¨Md3»ÀÉHEw8PRÆ"™Lv¹-•‡KÎညöWÃbÖ6­ªŠY¤gí³!Ë2Fc%;Öª©†VxCék Ú#°ò‘Õ±íÀöC“GY…‰bÖJ7ÍÀ@ó¬ÙYGüu%(¡4•—ÄɰËÍÈš•ÖÚžfTžµî“¤Ï‰_î’$©¹·Ï;£ÄÅb õ6”úì™ý‰5oÀ~‚µ•gmEž[‘ú%ͰB_íY›/ç÷¨;ÔùÅuŒë%ü]æî•Â? $³6±¥BÀa††CW œBÖ Zõ¿Âг*.‹ó¬myÀúuw¨WW¯Ã~Ý¡¡Þ|%VNE*GÅ!kO¬‚aÐä*ÏJ็E׸j ¬ Vù|Ö[\ñ+«š°^¼¾¸¿CÔëk{²ßÐþ0ëHŽ©š²Ž‘•÷L«’Üx ÊÖ Ü–j¹æO³ŽÆKβrË“qeÉI\‘õüþ X– S9emµÒºiiÞ9V_˜3-¸â¹˜j$ÚEЬ©éa rO±fý˜ìa\hYKãLR=¹_« mˬ·!‡1ª ‚¥¼ÁÚ ºDÖ(½Ÿeå—Þ7¥óY›v&HžÕ™‚·Q>ËÚ¦iFJ‡P‡ÓUU‚YAºP*å-9W‡ Nñº€wõW)ïÊóóóR\+ÃÞ*ªÛÀ+èdìñô3Ëjvš"öR®YÛE&€µÊËàWî̾™cˆYƒŠÀ¥›aU†’²\}úíÓo«Õéécþ\ŸËqýb´[.÷Vœe–Õìâ‘ÂÖa\·±š²Åmäª9Ö¡ JC7…"•?±V£©gYE‹# b==>>ýðÇ®KQ½5Sñ~-$ü!jÝͰ¢Qµþ,Cû5ÛNs˜Xy±CnfYScjе)úˆ¬ìIY3˪ýÐìÓ§¶YƒÖ+¼žÞìÕëÕŸ±rNÈìÅb†•Zï!Éu8u¸=`Å“¹Þ=U‡ÿ÷ûUKE¿ôJÅ^w±–ч˜geg4ª@ºø……țijR¼¢xÖûUKA•‡¸ŠXGb%O¬ª¾9=&ÜãÕŒ;¿@èõïÏc5"°TnYµF×"°ºKë3Ï9pÂP›H´^›ã‡ì|ö·kJàÕwhþþ™íî'VŽ=ß&3¬¥Jvd(Ïj3ï?<«˜5VÛg²š.ó%è´ÄŠlùÄÍqž* +¢f—n¨ÆûŸâZH)‹Š¿9ÈÇYå"Õ²LJÕ†DPðÙÅ2¼H˜5P<ŸÕ(¬ß•¬R·| pÄê¢'úNg)½V§”Æ&¬á{4Iífê°¤QkÂIª¢,î𯳲ªg³†³šÀB ‹fG¬<ï•ð;œa gqn¯äÐ×iÆÐ º–ÀA]ÿO¾½*{Çnöµ((õʵ8Í\V¡:–Z•Í–aH…"ˆR!H‰¶õHk]›&;î0ôf”ëjmE7)O@mÖÕä‘l‘ÀJ»ÒE¦ Ð#^Êå²Ú#¤§%ÊóÊS®©KUƒ„ ‰°XM­}°I¬ÎξҒI'œŸTòÌ)¤B‘`vOårÙìcµü,¤EîråÜæÂåk?îZÝjÍí¾„F¯¬WÆ’4z’9GuZI D)¤B‘ D)¤JGÅÓÏù‹lÊ>y¹ µ+ô¤-iU‡â+³Õ-›imxi”vm©œñ¬»«/L¤¢Pô>âô‰_ì;fK#¼1¦aß1‚Tœ”¦Èîq%-¡D+‡Sõ/ì-U‹—} Ð çQ,-›ë LçË\€{{QNþÎó¥AodÂežŸ*Ò28É‹SÍ©µ¸)µ§¯Ccïz‡rÐÖ¤Óˆ8ËÝ,Úݱ±uT3+( kË Ê<™äÈ2µ\NK®ÜœûEPù€¾Ê€²¹BP¿§ñ©.î‚…ÿtòýBøÚÜÐØTùJy[’îä[Ùf‡”ß$©°¢8ömÕ=-é{Tßù›ŸgÝÖ2­;>“Ô¿Ò}ákÙòT.®5úË7Vò¦GÿIËMÙb–3ø$ÿ’ÂYî¥cûNp?mú…[§NŸ ÏHcº`¼øŒG„çõë6ͪÃk´F‡‘¯õI«ãÈ9};.70¶rOFýãq”vJJyöОBâIQ'Þ»{¶Eí ƒEiƽó…qECTøEúFÁ3;â|ÖRl×åLàôyMÈY_Z<:$¨ú~}*—]Olºá@8 ^ëjÚ±ÇÓ9ô]´jž‹•J²y:ÔVÍì][ª åoa\¥»¢ÊÓ3„ìt= ¼J®£Ü9O}’^f>÷6ioí‰B^#+¼I¥ÖSý†Q×§ ?KÊiEf/ýf2µ³p[UÛH»sÌPÿŽL~ `h¹æS^‡‘Ͳ¨y7á2±l8“nh°½–:Úý=*âV˜ç8·´k:+§…bý¿%™±cu,$³Žv[ÎÉd’”­Ö¢´¤¶Ð]ÓÍÇk‘Ð émm¢Ûk®5PÐg·ºT¸Ä°Q„ñ…Ý5Í‹ÊrRö- VÌÃ--—á¸T‡ƒ¥»¶¢8GN•³Ü³vÛ"Û®TÉ»;æeaAˆ0Øm-¼nñáåׇÅ# Ÿv"Peºé„ÈâàZ[BR/Ñ7&¬Ý»m,Ú%u¶“ž^£#UbíCA_²HtéyØncUÉä\˱4iðg>”%ðÜ…8…4ál%*⦺U]Ò£Z[ªºI$2Â’Kd†*Mg&îÙÜd\þc.„@mÉM¦ L7wÐÛÎ6yÛ]ÚÌ3ÕYØÑ?Å^XVÐ#­íc2¥ l^Áå8`ÛŸZõ*Ê2ïvá+K©ÏŒ®Àظ[û^˜Œî?0ó›•Ôý/0 T’G¸ ñPø(Wí—´ÔqC: á…A”ÿ•2[¯—Àcâgå¨e§4io¯¸[î¤) ‚IÆÀW_lUì\r«UËÊ%; ­ÅšñšgeÈÒ¼ã`Ëž Ó7ïÍÃm—I >‰âêo×ô›ükÉãjñcéií.ïd¨Ÿk‡1ÙðñwäýŸ Ö™6;/7:nE®Ül).8ò”EŠR’~‘êTzM¹`ÈTbL¡Ó-µ»Â㨘’pøwžR³Éè9ü75™QÒ§dâ’³-¤’KêîrߥWþ•[y¶:3Yæ#nÝQÄü§V @ÃJ g&ºâËå2—Šaßðÿqî$ ¨òû ¯\.§ü¯qeÒØ$å_¸gL©ô.Ò…ºä3çQ& ëÛ±ù˜¦£Ic°™ ëÅР´¨~*OZ»ü7mwe[çÔÜ.Þùg©•º4p‘w6mÇÆÚt}k8Û¥kýŽ+.ŽE-°£Ä­ÔZÄ´•\]@üëßÙ±orÍuAq+ÌöwÎ+Ük@!#+$Ñu|^Ÿâ\›(”•´û ìµ-%+S¶<¹‹$•íùW©ÝgWgõÞ(]»•$ %íƒÿÚ‡Šq2÷W%è­(-ÞÕÜqáV<‡N¶®{¡{ §5ïâÛËv˜>V]'²¼ä߯~GñþWS$ÃÆåã6:4•¡„w,§ )A õøz±gt„btŸ”µ¿éí¿”‰ÀŒp–d)±fÆD˜«Gp]· (|S{\cïVTNC¡SCœ„í“ZÖ÷üVÏ>íá$Áw6e‰Dw ©y•9oµ <“ËØÚº›d7l5¥õ†Ô<(e[‡K†9R“«þF…”Ùq˜mYÆ3 qJw1-•‹(6=Ä}Å®Á)½iú–ÙîÕ?Ò8“÷M½àXÆk¢6Íè‘`rŒ[çþ çåZüüŒÅ¯SwýÇ$ÿ̳nŸÂÀÿäH¥ÏÚ/ão …üÃÜ&¾!kŸŽ ´«§šæ¤üle¼+=PÿØ'ðÿˆ¿¢)Ðw=XÀ§UÙfµˆÊàJãå«´a+%—™*°ZV‚>ßz›{´¹yýÛcR¾8pñ–º±ý¢n·¹­ú§î_¾coèÞ%2|M…žüäbá³r'Ï€[î÷#&l„=ý²>¾(s•¾UÔ7iºeItÝ"’¾šÛ•¡ûcÄß»–u,>5Ÿž¶Û¸Í9bµk}(O&Є\«•ª+I» MÖtEÌÔøs›1·O(Ë[!Ž‘ˆä9ñÛ“áÅÆ]HRT>`׉rBÓá5$É‹\G¨Žó­§ð²UV†þøÖd'oo:Ö'tÈ®!Ôc›S­žIubë¸÷+ûæj—ÝýLL‘QW!d±8ÜœECŸc+Õ·?­E7ƒQã4}_ÒšMïÁ)JSÓÒá _ó­B¤Ýï;ú‰=æ±ÚÛmh-­!H"Å$\ðµm#‰‘¦kí­e¸å´¸nãIQQù§Ò£k(MJŠ÷BùrÂDü½µÉÔ°Xèx…¦òRSJY–Ê’¤ß PJ‰¹øW[§í…ÍlF­ s2®æéZ ÓQÎtHðþ±•ņrÅÉo¸›þín-×R£ú’ëŠZ¯ùÔ ¾¼­PiÝ€ùIÝ¡?áhRã/jÓ¦½üž/³yvRH#û¨OÈ*× UêÇTuvV¥¨-ß"Ú­€p–v'^Ãâ´ã¢¢2`´6, ¾UÌ–éJñž“'"r.bÙʹ—„‚Uñôõ­tŒé$œ TÓ¾I#Çj;Ii¤ñB}©üÉ­¤sgmÔ)·… ) ‚>`ÖA¤O8°¡DAn$vã¶MÊBP/ø$ Ë9lÍf{¤@ŸSñ­faÄ^öëézDX €¤NyXì|µ!R¢µ!Mý…Ô%e?‡ m[­Æ\‰=ÂRÄÆÖ·µ«I™`="a)J@ èAH™ X …"sÇÇcã:·cÅi—\ÿqÆÛJT¯Ä€ ­ÚãBLÀNŠÒfˆR!H…"ˆR"§UÂìø‡1Yvñ\!CÙHPôR:¤ˆ©-]km©M ÕÐ0¡‘,oˆZÇ´"3œÉ+‘Å0•.@l'ý"sIDATxÚíwt\×}ç?÷µièƒv , A‘)‘”‘¢,‹jk9¶bÅ*kk­È±²>–cÇÞ89ò99N|ÖIÖ>›dã8N\ÒÔ¬B˶HI”EK´EQT¡Ø 6°€ Ê ˜™W~ûÇ›÷8‚ šïÌk÷þî¯|í*.ãæÐ.NÁE‚\AlÉæ8öì ìû×G:ØrC14;‚©Wâ!âÿ&Nò”B+ºJ¦p1ˆ˜2 _‰5¹cb e[†½§iš§gAtl‡c¿ÞHßú×9üÔìÔh:RB€€0 ñÅ—J(…¦ëh‰8F,‚ŠFÁóp‡²`Ûx¹Äó†q…*IR¸Né± MÄ&5cÕ%1- «6Iù¼6¬º$Z4Šxîà nz€ìᣠlÝIêím ìØƒÊøÏÖ|•®ŠÈ]¬‹¬ÊrV.c½#ÚÜxa)õb3SÓ4×ÅN¥éyöö~ÿrݽ i¾®(ÑÇW°ÿ÷š¥ ˜þÅû‰57àJÓ0- Q ×s14¥i¸® ÐèÚ6ƒG»Ú²“žu¯ÑûëMd=n(ˆ>Lßx‚ÒÉ+/eÊç}6Žë‚º®hÇ5AŠÙ;@ØžçaY©ýü—G8úó_âf «Uµªp¥z¢0QâI*Σö#7P=g6âyds94¥°, §þyünš&ù|· Ì- µc7‡ž}‘Þç_!wø¨ÿ\¥…:I+^®KEÛ .ù“0gMGS Ó4C+, ÂŽ ¶m#€{à»þÏ?ÓóÊëˆ(ßá÷ÌD”òÖ)ˆRÔ\ÖFÍesˆÏmÃŰ ÖqTÁ=ây^ø>+%äHÇ ·, Çs±‡²Ø»öÐùÈ*º×þg`0ä–`a„:Ìó¨˜3ƒIò$fN'bZ!ÃäE@‰;}ýìüÆßѵf¢éEâÉÿ_ðñC´®š‰ÿýc4¬\Žºe¢Çã¾X+ruœLÉ–¼R3;$–®ã pì¥õìû×GIoß;̼.ŸJ„ä²+™þ•Ïo¬?ឣIQóö–º$Še«nô¿ö&Ç~µQˆ:.¨D|‰•ˆ2å¾3á÷nÃJÖ©©F/+Cp@0`¥Tˆ‚‰(žÒÏÁyÅ߉F"AýM+hýÓ?¢¢}âùï¢ DŠÌëžu¯Ó½êyÄuGmòÇŒ !¢xžG>ŸÇqPŠ¡CG8øôs¸Ùœo®p…YWCÕåsiþè ”-^€íyPðÒºŽZh£åÄó ÷rÙ,ÑY—0㫟£âÒY¡Iqœ(¾¹ìÙ6–ô;[¥°m×uGÝ÷eŒ&‡nîàEÇÁ0 Ž=÷}¿yg˜œVâeQwí4|ì&ªfNÇÕ^A”º0FsÐòžáº.e³[™üà=ìúÆß1Øq45ÜØÐ4†åØÚõ$Úfø†B‘¸W2L¦Êqð\Ý0Èwusä¹—ñòùá¢Ì4™ðѸä+HÕåó0««°ÊÊÐ Ã0†‰˜býp¾ƒ/q§W•óçÒ|÷í˜å1ù8$DG"ô¼ò:öÑc!QG[t A™&nŠüŽÝD#,Ë¢Ã[dvîEüR"`˜-wÜÄ”?¼—hmCס°Z-Ë mý±”ÕÁýõWºyå êo¼Öv‘&ñmÅྃd¶ì ‰„×f:¾õ]vî¡rþlj>ü;T-]ˆU—$ãØ¶Ÿ¼`P– bî,¼ÖiÈÁ#äöì#zÉT<Ã@/ž¥V˜+zYì¸A"««Æð¹Üó|[¸dŽÇœw^VðFm]ÓèügØé !†!±heóæœ€”Ϙe-‚îºtþ×3äÓLûü§h¸qªªÇuC<£”BÞ«à90£QŒÓÉõô†çœjL†iaTV"šV!‚ëA¤¥ 4 ݲPãM©ÄŒDpúúék«?`ÍGS‘úZ>v3ñššãÞÓ³|ƒRSŠžµ¯0¸÷3¿öGT/¾Ü7<&‹ ®ëú0Ã!RS}‚£r˜qR¸aTÏžÁÁx'í»ç‡fpÏb­Ó1eÃâÀãÊì\Ï%³{/™Ýû‚ªtÆ[–S~ùÛÉ ÎìØO´±ŽØ”‰¡Œ/5N§¨K±†Êd `Jè²*Ê1ÊâÇq‹ë‘z{+Êõ0ÌãcWÎÅ`–i¢ xžRo#C3Œ01à\½ÈNo]¿XK$Y¡ ~ªÒ‰<P)ŽÃÀ«¿AeÃw+ŽB˜–‰aÇc"ºNíò%D[š0sÓ˜$½¹\Ž\×QÄqC°¨ÕTƒa„âl}T®ëb»Ùýô¿µ Ï4|±Xÿgª“J•¶cÛ iMMØÈ°’…âwͦÒä†ü¸šxDk*¨¾l.J×±mûƒ`\(õÐÊrÈçÁó ¢tHm5ê<ØZ)ߤͧñÏvÂ$Þås±ÚD4è쨸ïu.. î-©4ÞÐP!¿XQyYæ„ÆpcÍ#À‡ž—Öc÷¥ñÐ0â"Mõ'xŠÇ•R«¶cãi…”QUå SÀg{oÛ¶Éås¸¶Ý¡¬Ÿ!_„²OöN^‘®qÇ‘"ä&»w?Þ`×ópç¸Î¢J‘íOlR ·¬@i©Í;pÒ™d) ”ÂJÎgsä Y1ŽR¤Øé d9ôìóô¾²Ý2¨»u9µ¿{‘–&ÛÆ)”50†­EF… ª¤þ[4ÌÞNºŸ{ '— ÑïÙ%œ°L†Ü‘.*¯[BÕí¤7o'õú[¾~),€àÞ¡’W £¡žŠk¯¦âúe•áÄ»…Ô£b¯àÇÞþçStüí?3ÔÙE¤®†XMvÞKç£lÅQ7{ýŸÚ°Š$/osäg/â=ìŸ 0ÈlÞ†®iLøôÔüÎ":ÿë²û†Y¥WT)ŧî¯Àäàö î»’É’ÕŠ'.pBú¢ÚGºÈïï¤ó…Wé|j5Nzøä¦>xõ7\‹W„ÈGô7‚”‚8t½Pnà¯>×vp‡üî 2‚‰y¦¦¯Õ܈JádÈìè ñÖ¤^ßD×þƒšÜBtêd?[ä4Å ÓõÈOˆµ·]tѪJ,Ë u„‰`Û6¶m£åmÜCGHoßÍÀν¤7¾CzÛnܼƒQ§þÃ×P÷‰[‰Nšà« _Š]4ïE”ó†áŠT â1D×ÀqAØÙ#G×ÃÓVŒ&+nX}ˆa Å¢”/\@îÀAŽüû“XÓ§’\8ŸÔ¯_çØ+©ºê jæÏA•'MÃÍæèØGfËúó&ïî w¬±BÜF›ØHó]·Ó|ëõØ†Ž†:ããιX,³5bUå膎—³Q ƒ[v!ŒdM8ÁrÞÒ› àS3 jÿÛM¸¢³ÿÇOÚ°™ªù³ˆ6Õ“Ùø6ýk_ŵº«±Ž¡‡H½µ•tw/F"†²üä{ËÀ¬¯¥¢íj/ bîlô²xXdÏ¿„Ÿw>U¸Å‡ãº¤·ïbëç¿NöhoZ`4âfÐ4¼¾¦ÿ¯ñL£(_êl1R §€mT6Gïo6qäç/’ÛÛ‰U]EÅÜVbÓ'cD£H.KÞq‰Ô×onD+/ƒh„H4ê—jÊ«‹K"ÞÏöL£fe9Žƒ*‹cÖU“ë:†oçúßu=»Ö·À<¡úõ7©¸bn‰yzºÁ/"aް‹R}íbâ—Í%»÷¹­»ÈlßÍÀšuÄ’ÕĦM$2}‘)‰ÔÕ`çóÃÐýh'*¼ï €˜Q^F|êD6ï<^›§”Ÿ«è¦Aÿ+¯‘˜7-?¯‰€^ —ôX„ò¶V’óçâ9ùŸ ƒœmû8¤Xôq¼üà\S”Æ%RbYñªJ*Ûg£L#Œ²y…²b€h} e-˜ž`eŸŸËóÂbÍÂgË´0 ¨ZÓu"¨hÔÿlYX¦9,m©ìbœ7AJcÛº¦S9o6‘d5J$ììüÌõ¦ÐÊ+0*+@ûóŠ?³ôJD`±Ë£qŸKÕÖ†C«çyÄ&O lNë°œ[U@ÉNfˆÎÿ|šþm;q7töÉ(5(õúŽžïǨ‰¬ bh”Å©¾fz,â7¢NpJ1°}½/­ÇuœR9Ï÷ù§?㉠Þ9žñ§¨^ºòöÖÐì uI`‘õ¥0‚€Ð8 Aй$ÑPOËí+ѱBŒÝ/ߥü^WÛv!})T‘b-¢”æè~Ð>ªÎšâˆ]âŠùT/YPPêÁÿ¾Å5°§“þ·¶ø´%~Ûýº÷,‰ZY‚†ß»•hs^qQRä{SìùÇc`ó6DqÖi¡ rŽ@±êÒ9Løý[1c‘¢Â?{0½k{þñÇäÞÍí·œKF]‡Ù"–eaF"4ÞzÉå‹¡¨s\àuí~y‡ÿíITQßßv?f¸*̪J&æNÊÛgøÅö¢¨‚ŸëÈ3ÏsøßŸD í›Fª÷»HQTðžë¢77Ð|ßD'6…€1Èm²3ƒøÉs¤·îDá9d ¸ü0¨BQµh>Ó¼‡Xc2l{t¼S[ûÿåœîž° g$ùmàýá‡~ø|'ýt>'A7 Ê/™ŠUSIúí­8™¡a×uÆîîÏ%1¡ÙoÃ1Ân8ãUÇ|p{¿».]¿XËîÿûC†:€¦|ÐèŸH¤º’Ö/}†º•+Â|®±èm8ÚâùÝŒßÎçé]÷ö}÷ßضÇï(­Žï\k®gÊ}Ÿ úº¥h嘅Ðjñ® ã‰c>ÐqÛ¶ýÏ{;éø‡Òý«7|.ÌDÄó°*ʘö…OSuãrb ?îmÛö°Í½ÆK,ãM ·ü;rGÑùÓ5ýé‹ îÜ®_0£4E|J uº†‰Ÿü(Fe9ù\>$HqÃý÷›c.ˆýCŠÃ¯®ë’ï<ÄÑçÅÑÕëÈìÞ‡—Í!ž`$bÔ-[LíÒË0ê’T.œï׊™ö%A²‹%¼âó» é¢ÿ7›è^»žôæíä»{ÇAFˆ4Ô2óO¤j颰™Íˆ{Y½ÇĹ`pGÀ-^áwË4±3ƒô¾»ô¦wxc3éww`÷ôSÑv Sÿð^â æ„ᤰCH]|Ï÷‚(AJ3ƒöäžç‘Ëû•²FÞ¦ÿÝí¤Þx‡ô›qûˆÏœFòª…T¶ÏÆHV“w¢ñhZ¨üGÚ|å"AÎPdÇÉvút‡|.‹–Ícï?Hß›É8ˆ‹«) bMÄæ¶¡jk†¿ÆÃ\p)Ýd$]0L/J <ÛAÇwLŠ_n¦LÓßì=ŒŽ»ÌÅÑðy‰X)nøï‰€i [&‹¾ÿì¢sqÔW]éç:45‹)Æ7ã)5t\sH±x*ÝŽètTŠ7Š“â4MóÅí@iŠHEâ"AÎD<åóyúûûéì줹¹™úúúSÂó<²Ù,GŽ¡§§‡9sæFOô´ƒGxóÛ '¢ÌûŸ#ÖXsQdîøùÏÎwÜÁ]wÝÅ;ï¼sZî8|ø0_þò—¹í¶Ûøó?ÿsÒéôˆ‰qžã²õ±çéô—ôüøyvüä%<×»È!§ã¥6l ‰„­‘NuÄãqvïÞÍæÍ›©¯¯qË ðû¡´,i§ç¹ (¥ÑÐ~ š¦.äT: ®®Žx¡Á²¬“žè—D"Á¤I“†õn,­qTJѸ´í…FÝ¥Ó‡e³”Þ{¤w<™Ø,}Î’ Å¥t:M&“A×u2?ÚD"A<îº.ýýýär9t]§¢¢‚X,†¦i¡Î¨¬ô«l‡††èïïG×uªªªŽs®¨›ßÊ@&CfhD"ºüÓé4Ùl6Ü´²²Ò¯+,"l>Ÿ'“ÉPQáw~èïïGD¨¬¬ ÑXÅKb¤R)}ôQžzê)8"ðþþ~jjjBßSww7?úÑXµj===X–Åe—]ƃ>ÈìÙ³C®°,‹;vð÷ÿ÷lܸ‘X,ÆÍ7ßÌg>ójjjxå•Wxúé§Ù½{7÷Þ{/+W®äå—_fÕªUlÚ´‰žžÇ¡¢¢‚Å‹sÿý÷sÉ%—ÏçyüñÇùÅ/~ëºÜ}÷Ý<öØclذMÓX°`üÇ̬Y³ÞSô¬ÏóÄu]ñ<ï¤ÿr¹œüå_þ¥TTTH<—+VÈÝwß-sçÎ¥”Ìœ9S¶mÛ&™LFzè!‰ÇãrõÕWË—¿üe¹öÚkE×u¹õÖ[åСCòõ¯]¹þúëåöÛo—êêj‰ÇãH<—o~ó›âyž<ùä“RVV&‰DBž~úiÉåròéOZt]—éӧ˲eËäÊ+¯”D"!J)ùÔ§>%étZ\ו‡zHihhk®¹F.\(óæÍÓ4E)%=ôär¹ÇÌÅhcB‘ 6È”)S$‰ÈŸýÙŸIWW—äóyyâ‰'¤¢¢Bf̘!Û·o—Õ«WKmm­Ìš5K6nÜ(""o¼ñ†Ìœ9SÊËËå©§ž’¿ú«¿MÓ¤ººZn¼ñFyæ™gä?ø´¶¶ ‹-’ýû÷ˆ ¤¾¾^***äÙgŸ ¯ë­·¤»»[:;;塇MÓdúôé²}ûvùö·¿-†aˆaòÀÈž={dË–-rà 7 þð‡¥¯¯/ÿXD+‘µnÝ:öïßÏÌ™3¹çž{¨««Ã4MjkkC§ŸmÛ¼ð tww³dÉÚÚÚ˜1cóæÍ#NóÎ;ï s<ÞÿýÜrË-Üu×]|ò“ŸDÓ4:::صk–ePÿ¾råJ¾øÅ/ÒÞÞN2™¤¹¹™}èC”••‘N§I¥R¡NÓ4d2ÉwÞÉäÉ“™5k—_~yè…ö<ïƒieÙ¶Í–-[p]—™3gÒÔÔ4bµk6›eëÖ­D£Q:;;CBÛÓÓCuu5"BKK ííí!:¿òÊ+©¨¨`hhˆcÇŽ‘L&O aÉdصkû÷ïǶmÞ|óͰ‘qä ªªŠ¦¦¦ðúòòò¾Ùë8}}}$“ÉaÛ •.X¡=ök×® ƒ‡Ï Làêêêa˜Ð½½½8Ž3bKóÍ›7ó7ó7¬]»Û¶‰Çãd³Yr¹Üˆ“mšæ°÷}¯CÂcfeOú™d644°dÉ’a²"ÂòåËy÷ÝwC. °Kèà ªp‹6º®O¥RüÅ_ü<ò+V¬àÁdêÔ©lÚ´‰ÏþóÃÚ5]ÐÀ0˜ €Ã‡344¢òt:M¾X­ë:±˜ßJöšk®á¯ÿú¯ÃöÁ¤jš¤tõg³YlÛ¦¬¬Œ†††aõŠš¦±yóf^|ñEêêêxøá‡¹êª«8zôh(ÎJýb¤/˲,&L˜@GG]]]áw©T*\É–e…(¼££ƒl6Š Ó4OØ’¢”»öìÙC*•bÒ¤ILž<ù„ﻺº ¦¦†É“'»nppp˜;ÿ‚¨S?Ù¡ë:óçÏ'‘H°{÷n~úÓŸúM‘s96mÚD.—c``ÇqX²d ±XŒ×^{Õ«W‡ ÅÞÞ\.7Œc2™ kÖ¬!›Í²|ùrOhóˆ´žž:::8rä«V­"_h¯„‰O&Jß ËjLEV`M-Y²„öövÖ¯_Ï·¾õ-vî܉ˆð / ë:CCCôöörýõ×sÍ5×ðÜsÏñÕ¯~•]»v±xñbLÓdÏž=Lš4)42™ ôôôðÈ#ðÄO°`Áîºë.4M#WÔp3—Ë1iÒ$jkkÙ»w/_ûÚ׸îºëxûí·Ù½{7±X ÇqÈŠ…òùüˆ5ó«'•J½7¥c…Ô]וÇ\f̘!š¦ ---òÙÏ~V$Ê÷¿ÿ}Y¿~½ÜtÓM’H$IJ,©®®–d2)ÉdR¾ûÝïÊ< š¦IYY™,X°@ÚÛÛ¥¦¦F–,Y"Ï=÷œ¸®+""«W¯–d2)±XLþéŸþI²Ù¬|å+_‘D"!€èº.‹/–ï|ç;ÒØØ(555²fÍùæ7¿)º®Ë¬Y³dË–-""â8Ž|á _@ÚÚÚd×®]c Gg§Ï“xJo»í6ZZZX»v-¹\ŽÅ‹3sæLÚÛÛ`æÌ™ˆW\qßûÞ÷X·nï¾û.ét]ש¯¯gÙ²e,Z´ˆ¹sçÒÙÙI6›Å²,Z[[Y¶lS§N ÅÛäÉ“ùÆ7¾çy´··‰DøÒ—¾D[[Û·o§¦¦†n¸!§Ùl––––.]Êw¾ób±ÕÕÕáøn¾ùfZ[[I$”——yU×9gœM äd]äFÚp¸ÔR*=¿8 õL\æ's³Ÿ.Óe¤1œÉXß‚\<. îÅãÔÇÿ?bU¨VˆIEND®B`‚pyftpdlib-release-1.5.4/docs/images/fedora.png000066400000000000000000000113621327314554200213100ustar00rootroot00000000000000‰PNG  IHDR t*ðâ pHYs  šœtIMEÖ ¤›‘IDATxÚíPgzÇ×Éf%!ÕgKF`û˜ÕAlŒ‘í€n\CÜ#’IrcÌ\&G:ƒÇ!išÁÎÌe.cÇw¹Nz&Ì¥Ór®™é1mody+í»Ú}W2?Ÿ¿@Zí»»ïû}ßç}ßçyvݽ{÷xY@':Ð €Nt :Ð €Nt è@'€Nt XqŠá«ŸH4þÑè¯çü¡IÚ3çy}Aæórƒn«^[C·êµ›M[õÚ<]À:È#¬fÞ·æ¢?ýµƒk(ã³VË“M{TÊÐ ð¨(dàÂHjèN©Š|ÎjùËÎVÐ °–™ó‡ÞîüÌ9-å$5”ñ{ÝmÕ”t¬A†Ç&Þåäl¯{ªãh3èXk¶ÖÛ½ƒÂl—é[O7íÛUa(S2Ÿø‚ —>sÿí?üëRôóÉsVË÷ºÛ@'À£(’RµêÔqhÿÎomÃî>Ûݹá^ŽGr"XV‘¹%|ù›7_H!™PFµL¥-*)]ø½k9ù…}¼TEJ™ÙÃ>#°Z&îÂG~‘A{ÂA¬/–)¶×•¨‚ø©møs vPÀ¼ôzŸðÕ­žwþ*Óܺ2é›rû·nù£ÒRåo½·m#îh<É|µ¼‹LQnÐýìÜI¼­°»€•Ÿ–Éc»Li"¹ôùïÞëû¹gvî~Ç_T¼¡lÛÆ¯mKP´”oÚš¸5çõlÃxÖØ]À 3paDøÁßzº)M$'¿û^J$AÜ[^ºsÃ÷ÿ–}˜|ÓVæŸÚ†çü!Ð Pxƒ‰¨÷}»*Øÿ¾×÷sÎÃ3Ë‹v`ŠÉÒuEÅ©A'@áéDÔñìéû•I{$I#¾Éþ·¨¤”ùã  °`ü±>åöó|›ÚgLãv$6<6: ‰\ü0ËÁÜ/NÐ °bLIL²R¬ÜÄþ÷n"žú{t^¬¥'"‘©Äl-/ÆØ:ÁÇ@'ÀZ³»Š•›ÈŠ]ú÷ƒK“¿úÏ+ìy¼ï«ù»‰8{ ˜“ñÉ%r Úþþ€Ó\Èz¤k:°tâ¤9üöœÓhîØƒIÊŸ—a1äMM¾Ÿž,@ãÎ!2õæõÅfwÉH(>÷E^KÄÐÉú‡&‚ ÖØTÈ i&‹™|—ødÓž‡ª“ÞÁkÂEbÐ)޵VC³2í®Ôßw—’(?ß\ñœÕ‚ú‹owõÛ§hÏ<*ÖéY#‰¢©N‹§?ËñÛ<ß–ªU+2˜àë$OÚÇg9¿R‘òö檣Í_‡ZDÏUT| çÛII<ÿãf“(w©:åÚ$aD; €(Ò–q™œœG~b/ûß9ñ)ºßÄÍv‡9?q!–AÛ›«@$€(Ò&$Ê*Ù3–ß4?}¨†ýɇ¿¼,ª Û[°_‚©“Ñ çœÌ-@,ÉðMv·¢ ¤b{}Ú"Ø‘#þÎGÙŸ|piòv8"Êâ’’Çîò‡¸s‰³'î œÅÀ Yþ`-tÝ«å ¥|Ý_Þ0lVcû×2³?þã?Ù…Ÿ¿†2þèô‹R®0—:1SePå‰[sE%ª Úr‚ ôZòt‡YEÊ ‚Ø[ÃÝó¾sî—<™V2Eò“ž—%¾a âO€UÁîuE2¹f³^K2"AñÏÿæ¼xñß…›[?:ý¢Ë=O{¯Kq/+F©ŠL½èÞòRlö7wñ/dD$–à”Š/ý»_}øcÂ'îÌœD¯K¨H™“Æ÷Á+Æ“ëÒ²Îݹáö®¿úÃ…ži|b«+“¾á#jyµ³5åÇŬÁÖSº5¥“hÂhoxlÂ76áGÍܯÕR‰±%ewÌöM1{ÁfSY÷‘Ú´Š`n<3¨îüÐdwÛî¬÷Ë´ ×t€s‡à¾`Œ³IÇÓTÊ’Ÿ;)ý•¥¥*òq³©š2>y°.O¯Ïòþ“hOŠ üä¿_<©ø‚ B.€2jºÛj3E"|ö˜õ2„×WÁØ]T¹º§«ñüÐFóR‘r«¥‚3OŒó«ƒuv—æÅÆ&üvÇ §eL{æÏMu©å¿ž~ûgÖgRÞ7‘X’öÎó¬G ç$¯H˜˜6f9õ¾éϺ_$–è¼–†YR3h̘¯$e™#‰Ý1Ë/’¦:eÔ¤¬_(F{æÇ‘L}õ^;Õa. à¼ï×EOö9„Oþþ\ÈT•ÇDæ/ÝŠ÷2ªæØËY!Çì…wVµ æñ<×lÐ):­Õ(ÿQäé¡xò¡zm(WkhüÏgjžuÅâtÇÞrœ]¼©ç‡¦P6Þ©3Ùi­înÛÍ=sà5¾m#nTe[-•çNài1ÖÆŠs'ˆÍá™jg¨ÀϦ:ùxÚJ=¥ëéj@™¾¶a7ž¡¯"å§;öž}©AH§†znÌI:­ÈõFe‰¬ÓZÝÓÕˆrÖê·O­ùIΛðsvÌ^˜3X+Ú[ª8 _DMÆ&ü(‘)7kNh±íŒ2j²v L¹ÝGj›ê ¨“cˆälWƒÀŽÜŠ¡,®Óf!'©§tg»P•̜٠“”N¸·oŠ$5ªtŠÌÏQ{è(eQ)`”%²Ó"Mj”>U¤¼§KDòÈî¶ZÎûµÏˆRDEe3 ”9Í-ᛞT¹úXk ·aY8ɪN¢ñ$gS¶Z*Å.@m®â¬DÕÇQ‘Û^z-)j¥O±y»•%2Îûå¹eTûåÃi12yBDVÖ×QýZ¡,|=T8§¹í"Œ¯&®AŸÙÓä(—ëCƒ'ATá´01Úcmr65Q¾âÖF"§½áœˆœ§_ãi´Nh,ñcFØi¨&±Úx¨~õ¨ì§Þø0WE0^ñB ¥Ê5xEPFµÀÝ$Î¥d½ÓÅA¯U fÛB ¶3Ê|’Ì`ˆ—OG¯% :Eæ‰ÄÀîZ „?w‰¹I‡ýCl#󾡈CžÜ@8ûN$áRyh:É‚ðç.¥Ê±. @Yk:Þú}yîÉ”$$S¬J¬–JáKIÑ<[Æ(;¾P,òµwñ3ç´P³rå©_§=a¼ˆHPÒ@D{çñ–ÚP y â§ŒêÌÅ4)ÞÖÒc™•ñÄÀµb‰%òºÙ„ê×±«Ü)xâÁ¹r½cÀyÁœ›*9êÔ¸Ï,jg“Õ;„sVðH褱49ŠõèÅt®Bq6ƒyÜÅN–hÏ–{aäËã=ã4µÆ N‰2åfUÆ&ü¨ÀzƒNáO)¶²PR´Ïð᩾¬Ð-.†È›ÚÞ\Åt‰%Nö9˜=”Ã’Ø ql{Ïl*ãüaª\ÎÌ‹L\¾”±»­–Sc‘X¢wðêØ„ÏÚX™i>e½SžXÂÒi­FEê;§ßþÁ%«¥"3ã#F.<Ð G/uªÃŒê&íã3öñ™¦:ƒ^G¦R+О°/´À“Ý” ³©Œß°ûH-O¨7S®Š”³©rb0Påêö–*T¤¸s:À”Â^D¦=YVQÛ[ªZpyw[-ª²"±„mØmvtŠÔÃÄ’Ò“Ú€Nî7ÎÖjž´q£"WÀT¤ŒéNku$–ä·@„—kµT¢2H¬Heá .HÏJónÕBtE5çNâ[.½PʨÁÛÚë>R+=!8 µU–Š”£–R˜|ÇÏ¿}i5{ŽæR'b=ÿ¬çNþ±Äåö–ªž®áHL•ó¿± …A§èn«˜&T¤¸ð›"¼»ËúÜx’§d½ž³] ¨U½–lo®ê>R›on)½õÖ[b£×’ï|=‘\NkCÆ€¶tãá}åÄ:â¿wKìoͦ²m»­•rY‘È1AÝTg˜ù***øÁl*c©Uo|ÿãßg~{x_yÖ“ìܱ©qçæ]+bDZ7žß#P¢Ì‹\Ò>|åÙÚÒRÚŠ^K¶Z*ý¡ØõQÏí‡û·mQmÛ¢úèÊ\š'eÓƒ™*S‘2½–[«]'A´ ö&ºŠ”¿òÌÎm[T§’ËŠÌTÙá}ÆÄÒ](ž&?Nšê ¯<»ó;Ja§?V‘²ÃûÊwíІn/fm²¢³µæ•gc*REÊ´êÇÿ|žýï>¿G`+dºE3 ùΟ™2_`ÂÓ"ÙO²½¥êðÞréÍE.+:XgصC›HÞͪ–ÔsKE¶iK7²› eÔœh«]ÍòH“2ÕiÙ3´'LÕ¢ÞÄC4žtNiï<í ³×|˜e(ʨ¦Ê5f“.·ë¡LRVÚ3Ï$üdw„L‰œ+KÌ{vü¡˜”Û÷‡bN:èšDâIö<ž2j Z’2j0Þn•z’öñY'ÐkÉܾY)óâÙÏ-USi™oSÐÞ0“NÉL•ᥤ(0À£Ãzx:Ð €Nt èÐ €Nt è@':t è@'°&ø?‡8 Á+í_ùIEND®B`‚pyftpdlib-release-1.5.4/docs/images/freebsd.gif000066400000000000000000000076121327314554200214460ustar00rootroot00000000000000GIF89aPX÷ÿÿÿïïïïçç÷ïïÿ÷÷÷ççïÞÞÆµµ÷ÞÞ¥””ïÖÖŒ{{εµ½¥¥ïÎÎ¥ŒŒÞ½½œ„„ÖµµÎ­­cRRÖ­­J99kRRœssZBB{ZZ!9)))!k!!µ11„!!Æ11!Œ!!Ö11Þ11¥!!ç))1Î!!Þ!!ç!!½BÖç÷çÞscZ½ÆÆÎÖç½½Ö¥¥ÖRJ¥B9Œµ­ÞZJœRBœJ9”{kµRBŒB1{9)kƽÞkR­{cµsZ­B1kÞÖïZJ{)!99)ZkRœsZœcJŒ¥ŒÆB)c­¥µRBc¥„ÆŒk­„c¥9)JZ9{)9cZk­ŒÆœ{µkJ„½¥Î¥„½„cœcB{9!JJ1Zµ”ÆR1c¥”­­Œ½œs­œ„¥Œs”sZ{19½œÆÆ¥Î{R„B!J”cœçÞç½­½Î½ÎÖ½Öενœ½µ”µ„Z„cBcB)B{J{))J)J!!Z)Zœs”sJkkJcÆ¥½B!9k)Z1))!½¥µœ„””c„9!1k9Zc1RZ)JµŒ¥ç­Îk1R÷çïÞÎÖsckÖµÆ1!)Z)Bk)J1!έ½cRZ¥„”B!1ÆŒ¥„9ZZ1BZ!9”{„„)Jœ„ŒZBJ„RcJ)çÎÖŒs{„ks”csB)1)k!9­s„k1B„1J޽ƽœ¥„JZZ!1„)BçÆÎÆ¥­µ”œ­Œ”„ckkJRÆ„””Rc1)ç½ÆÆœ¥ï¥µÞ”¥µk{s)9œ1Jk!1”)Bc)Z!­„Œç”¥­ZkœJZJ!)„1B­1J1Þ{ŒÆk{Ös„½cs½ZkµRcœ9J”1BJ!„)9ÆZk½Rc­9J¥1B”)9„!1Œ!1Æ{„½9JZ!½1B¥)9J¥!1BÖŒ”„19k!)k!µ)9{!”!Æ19Ö19½)1Æ)1µ!)¥!½!„Ö)1Ö!)!ù,PX@þH° Áƒ*\ȰáÂÆî…8&ŠÏ‡ tCXŒ a"Šq°€u¬7] 4{w­cÇÈéL–¬\9+V¸pA*Cš4uX²©ÀÉ¢µZd(”´h«:- % @\låc±/Ÿˆy˺i¢jÈ>‡4é™äÔaœDaV „¶‚…?³+üÝSGÊ‘#rÊÆ¡`Ç.ß½|òØ› /ªEk aáé®È(fД00€*=JQ ,_´f | +–(Q¶P-#çóثۢ¬ɜÀ³qƒµFà“'Í¡>}„{:&o¾|ôDx+çíØvä¢þy‚K÷¸ù„º<½Š„)ÎÛ„.Zƒ93C‡]<Ïßà/åøˆ:ãÜBÑ%}èA\JbHgýaD’L2"ht +Ä"Í/Ðhà 4ÐlóŠ-µ@cŒ6Û$ãM,NáÄ@+÷eF•&®p`JraAˆäBÇ$©ÐQÇT4ÁD=ø å”RêpEx”G]ØÒ iAÐ :ݘÒ´øæI(å9•-CJøŽ_÷¼‚ <ìÈã,êˆó:dFs›x„´ÅG/€ ÝâJ[ÆyÐä°2Øb -˜`€q˜É=øH&9óä”Ì<É€€*þ–XÒVR¦€)©@ ˆÀB þ °Ì2"°£Ž'ž,²H,])PG ã°p åÌ*GÙgÁ­•AH/‘P†™Lò 2¤ˆâ<õ`?ÊŽ8ã(ÓÝo„Ì7œ!”`Ë$¢3;Ç\²†lMòÎ8‘å“;(ä³OYÉxbÈ"ú:5-¹¬Rï‹,ƒ_,°Ô=Ðzs‰(„kŸ!{6 ,¤PÏ8ô¦lÈQI!5܇tðQËÆEr 2y‚Š'‚ÚÖĆzŸ[Ò€¾´Tr"•Y‡]L±Ç(  €©“@Ç ŠÄ 7ñÞr )¤ü]K¼!Ü  Ëþ(h²Î˜ÝtL4¼Tôqs4À‹%ä£ ôD^B<‚w 0pk ˜b¸‡KJËd¢,Òf]䃎¡t;Î’#J+Äò²J(ä¨#¬ðY¢ àÐ&©ek@ORN þŒÉäôB #Œ`B +x#(9<åFˆ%‹”2€¥ôbÒ>%4S@—’Œ:ðà‚©ã¤óL§L<ý¨£N9:!GNxCÈMΞÐR: ‰nU,EH…#V1: €P€ògA @0ã8<@` tHli(ÔD Bg°|mš€Ç;.c,TÌaÀ….²}°£þ" œ×ÀMðÁˆ¨Xê ‚l#,h öÑ‚*Ö¤–éÅ$¾‚†@”£þˆÌ“‘•¹…úšÄ$â ¤\LB¦Pã;ê‘oŒãäpÛ-d1Žzøã1«" À˜o$TYÃføp:IňèƒÄ€@è… 5`E(^Á yxRþ)"!Šnˆâ#„„CŸ|U¬БÒ„> bª0„’²ìõa®CON„ A½â˜¹ÉL)Z§PÄ,ŒÇ¨×pôp;iÜã+؇?²é°|À`ú°Å"Á>fdH<މ>¼abž@‡ ú‚o:'þ[9^1·°Îœ IÀ+εl®`aä°…(¢Q¯DÚç™X!@òŒˆâ˜p&Nf̰Å-ù9Ä a«‰*¤þ0?üQuH㱸„#.q²"ÑA|°DIMš¸G·ð&2:ÓèÜ˲ºO±ž„¼ø6´‘wØ"ÛØ7Èø6èjœhKfe ]4ò<6E%⇀­uØÂ"! YüÍÀ@ b! Yèð€G2râ[dTAox0 RZ¬Â Ô„Z’¢ `h¢¥8«B@ 9II]8C èP‰MÌ¢s„̰…&1 }(þD!ሹ… BBް1$Ñ3ÀÀ<æA&`4Õ)’¸"h†UàA jHD°Ü(h6$h7P zh`º¸]4Èä WשaE<ÈŠPôBªE)"]4 r8@ &¡9l`èåLá Bp%ÀÆ!@ÈñBXຠ@4â¡ä”°P‡P¬e(Cð€¼m„Œ ‹óòa "ËB¬ÅB-u@‹Ë'ŽÐG4ô¢e€*ä qÀC¾…#D‘ŒzXtÖɱ ñÔ§ˆ¾Õ$¢†4€ijÈ×$¶/Tx2òHG:Âè¯t¥ýÌ8V-žµÅÚ·J€ú †DFI H'@á[(ƒø¨€îákn9‚ Xél/ˆ'Ÿg5¨„þÑ&9! ‹C 2 Øâk#Lqotg£òÿ.}«$B ‚(€4  y°Ûr Ç!  “ ¼QãàÉð'#ž 40 Ôea¥@èþ òqe0¨$-Ët¢ ø‚` €0åP4¡°£@@€Ž‘M÷Pùi"P(‡Àc&5P4‚˜ o`a¨àÉ@­æ ÃôÀ0+À˰šÐ 6 µx¢à Ž`7Tô-ËpQ'ó 5 ™á¶†^ð êà αˆ äð1gwPì@yÅþNsׂ`¡ € ó€MÛÔE35c,\xŒ8 îq 2ÝÒd{pq|Љ¡ è0ES(Sc…ù†¬8€pì 2U[ ‚K“ð4‡`8¹Ð,+53ÞP†¨`be˜65VC—ŒÐ( R, â òò 4u ­€ªx‡ð[É(Ð'^&Ýa^%––PDÇŠÐ 3¥xŠw2–2lƒna‹šp^¦ |¹ ·0{¤xzàN¡PŽ|ªø3NEzÓ µ€ á° ïðŒÉ ï Ûp 5øN@‹ÐPþ ²Âu†²0 s`}žü‡‰àYsp{ ‘ ! áà ï u ½ µPË LÇ`XÀd2B"ÙVmA3©š`p:©´0k5 oÅV“Ðr ´À– *Á½ð ïܰïà6¤P°î´x  °—8~“ÀpfÅ“° tÀVq0 H€^Ð l5nn9u`gÀ±  ä2J…àbO¸Pp“Ä8K±0  }_@ e“ `x°jpÐybÐ[b P: >s @ I† Q¥Ñ ü@&PॠYÓfà?ð$P²ºuG jÇ1ÒPôqP ¡@^µ‰\¹ ýé8‘8ë` `ɈQÐ Ðj9g1B¯Â@Ð3ÐfÉ86æ;pyftpdlib-release-1.5.4/docs/images/logo.png000066400000000000000000000313201327314554200210040ustar00rootroot00000000000000‰PNG  IHDR“7hxísBIT|dˆ IDATxœÍyœU™÷¿Ï©åÞÛk’Ξ@è°„UÃ.ã´32‚. (ŒŽã,êkhõUDgttt7ÜF'- 0‚,¾5K MH:!„t’Nzï¾KÕ9ÏûGÕí¾ét6ç}ò©ôíêªS§ÎùgÎ^-Zv¿¿’ÍÍ®|î´{,W;40ÝxnzŒ4àl=V³gÄidqƒ@¯:¶­ÙñÄ×ß»}´E]ã±²9ÞÏ£ éï(¤Ÿ3éçÊëËçŒ.ÇÑ´˜­ƒ¯}jÅ+“ž+кðµ·U¦Û[‘æÝOé0µþÀî¯ïK~6¶#BÄ€¾†žx$ãû©ù‡dþåÙÙ«o­Š×¶nØlß]ÒÐÀÿìÚÅàä5<€¦å˽–¥K-ÀyÿöØ"‡=ÛÙègãFuv*H•x> ´ª¢êPçp6ÂÅq¬ª} ¯:u/€ÜúðÚßÞLK‹eÙ2Ÿæýêµ ìáÀy@CzÞ°ç€ïï÷ˆ Ï«€¿%´ÿÚ*ŸÓ´¿6VôíBàè´ms}e{èÖÏêÓ »ä¤Ör# þÈèý8YLã;O¯é7ÿ~´_ˆ†¡ˆ³à5øî²+_27ÞÚÕ2oº݆mñ3€óâ£Ô”é]߸ûí™lÕ7{’ˆ€Aœs[pšê\%¨ ê|Ä4ˆ1 ¾¯Î}è-ó?ø\ñê%Ÿzºù#²ðò€Ö¢1O.ƒapð4p.0xøUú·÷ÿ”ÙÎ,à¼åËùñÒ¥Ì=j^¶õ‹WΩŸRçc(÷9iYÇγî9óÖ*^ÖðÊÆ<÷õ Ï—bn¹hI×>úáY{"‚ÀŒÜ4ÚžŒ4VÙž*ˆ@)AÏ·~¸‰÷³&óó-;iÏøüôs—ÎúðY§OÀÄŠ12ÒOÕÑ6b«ìì‹x~í0÷>ÒÛ)'µ¶ÿ®Ê‘ƒ”!ßè }ÆÑ‡åü© ª‡é,f1Pê‰É9UçTyƒÃñÄòM ¦EËî÷W6/Ž—\wßÙ~&w‡³‘‹¥°ªÆUuq”4.*fAŒªú)‚ÊC—¾’`Ê:#§ªU'žbXUwß)Ÿûåûžü×ßÅ¢eþ‘gHVßtàgÀVàY`¸øÉ*¾>½î§é}—_kªiü´¿ÿŸ™[sþGfïbs¡šœwðB V¨ñÜÖ—Ìg®•Ði¬Ê¿cÉde[É5צESBwómÛÍ xëô©Ù;dg¡÷¬j?ðõo1LO¤xÆÛo[ª‚SSv3~u뎫?í†÷øâ®ž;—”80@Årn;ðäOnÚþp.kÎ<òð*=îð*Þtl#""€z~Y¾0­ä·ðú§éø¾sV\ ‹‘ªŽÇn£4ØËÔcÞBvâ4âBÁ©#A·€óHd3ˆ`£RQ<ß¼–Æ%WÖ~ç5mÐæA‹-Uús(ÿü2=w" °àGÀ•Œ‚éàw,iè¸åÞnïŒÓ'L -’Í;L*8¬K8g™ý™”a9EÊçpÂÈé¶‘¨jë´¹RwD¡7" Î&7•ï1F’߬ÓYå8½ñäSë•2‡L®Q•L©.3ùº·žˆçÏóÎËVçZöv¼øð ðÛMÞª'}â; «'ÔØ¨`Æ)áVn,¸TD2¶”/ùÙÚZõô÷ Y®E–î6—¤€ It•Lz~#‰²yðCà$âo'p2ðcIîþã/oëúÞŠÇ{/Éæ¼ÚŸ5î½uÑ$ =‘œÕKºaKÏ€KºeÊbÈN©®ò¸óG dúÔP߈ ª~`=qᤀÏ}ƒ¹eE·æ²†BÑ1¡ÎçÎ-ɳ2ºzÕ€œÕ0 E劥ÓäóW‚ |ã._°u{ô¹·_ö¿MŸš™{öi²ÿù•F…Ÿür«\ûÓW5›JQ§ÀfMËÈ»ß:Q>uÉ 7ÐYÌzæÄÒ÷¿0oʇ¿°îÿ쨢xžd!–Éž>½0"—·,h«øÓBhï{áá‡øÂ9WnøHmw-ªÑTÚı¹ú À´€cL`Œ™'ÆÃÏÕF=Û‚/>œª&/:ÿÆ®gZ– â…^&ûe[*€Üî JÁSþ7ú7qN%Tu…ª†ÙGcü³T®OS9ePy@1ý\•¾Ï àQ x/ðîJ¯+1ƒ¯®íŒè3ýCö4D¬µê¿¼1¯[K›çH¸ß0r@5P|9§Xr3llÕˆT¬N«‚ ›:‹¬ß\Ø™NÜÖê¬9'ŠÜ4DÜpÞyí› 1‰’ü<0¥³+z"«å÷’ ðb°^c#÷ ÷{Ù°©ðÁŽ9…Oݱ+’õ› ƒi_7‘ˆ/i[Ÿïy´÷ð[îÝõæ;~Œ+u•‚K.˜j¿ù³-oZÝžoc#ÛÛé*Ï¿.'¦ 'Òz8ðVàX`J2®­çXÄ]}ÿ‘8VÐÔÈHWfcîz¨ç… ƒÅ燈‡S'eõ¤‚ Ls§¯6@Ôzxx€ºÙGè‘ïþDæåfal]ô?5z‘ûh\¶ª&¦ÇX}‰ÝA”r*TQœ`­AÕ3“'«­Í0º>ü0@¢#ù$žÿÜÜTñ¾1‘kw‘eTIt«.vijggÖ®Z[<þŒkZÎ?ojÕÏ~³µw͆Â?êÓ ,'µnzù•üY3ϘPz±}[PŠˆXU5ŽâÎ)ÖQŒ­ëììaÓÔI.¯‰¼TUÅ:œ…ÞŸ|µôÊüs™áœ*¢Gšêµ#$€%ßfû†é±NEDÓgÄ¥Ømïd=‰«(±ñßKLý8ß{ôùiäBÉxvÆ”Ð3J‘›äô~ò²˜÷ߘûÅ¿_Ó8ém§Ö;DŠ˜tQ(œ¢JÓ¹S¼îA{ܲo¿r\6c4-lô¯LЀµ©3E5tQ‰¹g5½göiï½Ë©ë–È.A¤ÞF‘C0ê’1Ù«¾DFÿ.¨¢N÷Ö¿ 'š<™¾GðY`‰Xl%QÈÏnMïðµ ÓK}¬b í¡SŠ;ÛÚ¹îÇÍó«üÕäžÉN<ûò¶œûÞçÛ€Ÿ~ø ëÞyÖí]Óî\±‹ÐgG)¦³Xrâ×ø1%'ù¢KÌÖT1eì–-Ûç t” %²žó}ÜpÁÀÊ(÷r €¶äfÕÝ”^kñÒ÷›ÓgvõmÉÍœú˜Ôä‹ ÊÛIªäE8wѺ[ÿxã1Zñ ZpÉy™|wDO¿%„)%#:)4ñ÷¿6_‡{¢ ê5œˆÇZ=(0ulLå–µ6Šl\Ì{ˆˆ³±ø™ª³=ãåq6&u ¤ q †QPÁžbnÔEê›xÏ9¥Ÿ§ëIô$!Q* Ð,žJïëÝÍ úÆ„œv÷SÜ<±Þ;¶¾Æ¸RÇpýô)&mÚŸ\¿e{éÿýÃŽ‹©žÇ3žÇÝ·®ØuÞEõLèìýOö!Â@O¿Óï‘§‰ÿíg:rõõ´Ýv߮΋ì™1‘Ò>9ðøÎß]6"«xFž|aPxbדÍÚÃŽ<,û›;n<ÖåQk¶³;Òë~ü w?Ò«Ûv–\&4îÈCsÁ¥N•ËÞ?|_lª²†bQ%]í{àã ÀTÖóU4V×yd€¬:«ª©sêeªgI=I¨u¢‚–†úCyJÓóÍ$¢¹h-Ø6¦ß#«Ãܦ>`G__éŠSÿzÕW€Ù$ âé8y§‘H9ˆ‘Á¼£if͆R£Óöf’E58}þœÌ?hnœ{ÌÕÖ:‚M†Ý½õ:cè’Èn(i¾þˇO¨Ê™B\Ôì3mƒzÞ«ÙÑ÷íÀZ°=ÛwE¹[ûß|çƒ='üö{GÙ8ÒÄ&"»ñòŠÁß“š–{KΪñ;v ꦌ(¿ÁK®Ê~Û«Oß½yhç–CÄóP§ 䯂#QŽUKÝ! ÈN˜†‹Š’Ø{úž(ëRŒ"Kim½Þ_8ð_úý›_õ®ºp–ù7…»vÆÞÌYuÞÆ½^ú£blv ¢:àâtÇ ÏâgNc•"â‹âÇý±ÁjY‘§I¼êñ-äÙRÜP1ÑÒ´€;nßÀsjì(8Çc3fÐÙÙ9î«s©>ÒØÈݯ¾J—8=vã1`ìÉóWrfá15\tîäYÙŒ¹6ŠUŒÀÔ†€…ÇÔpî['QWëYá¹wÉ?¯£PÒ‰µòPG·Ö->¹öÂEo™è\Á…»úb}÷ÇV³³/Þ^W-÷D‘ÞfaµçÑ?uj&l¨ó&µÜµë3S¾Ü~ñ¾ud<¼µèg7–öÓÂëŸZ¯8)º³¥B¿¥ ýÎï¯> œO²ÚÃq®%lˆ,ý¹USæ4FżãyãùžFÅ\2pšøY®(‡Uâ«¿ß°˜¹qco'€ê2i®´ø¶Ÿ"o;`dâÆéá¨Å¹©}XJ‘z€gŒ0sJˆï e.™k•ï6ò¹¥˜D {‘„+wvîG«$1µ6q¶·‘ÄøV§×æÇ믳 Áö•ô=ïh÷œ=9m-m_°€¥à|2ÆÛ±­è.ºê%yäÙAchÍfô8ö¼Å Õøçe®»îvöÅý5U²¢H¯Oû‘èè(Ò›—œYó÷?\¾ýˆ«>4󔣿WÅDêÉ~À$ ´^!Ñ;¾óмŒã=h|Šsvš³.ÐTqvNQÔ ²µy/“—p&u¸ÔƒìRý3 ’ÄvåâÂb¼×ÇnbΪ ƒÍúuóÇ—0eê?Ó›/"„‰ËØDxÒ<Ê«#Ò\TňŒ¸òÀÓÏ“r ç~¢M×vœ@\Sm¼ço:Ñ›RcÄ¥Ú¦ÓÝt˜=š YXeQJS^KKyl÷ ²#¶|o‰Ý}iû&¡lœWÂÛ#V¿g(Öå7íÔeßííÝqoàË#Q¬Ë·þ‰gå$Þwô¼œ‚¸h Ö»ëŽ8Ò;H|Vù1O2w|w°Ë;‰_ÝóhïÉG[c±êÉ8h*ƒI@Y°¬%8´þ¡¯‹ò / «Õùˆµ¯l}9Œ&A[°¥|gãÑv»û“4Žp6FÁS7~ˆ…2˜D5 hÆ‘SoŽÉ„oÅ+@.Hf}TÅþ(§uµn™Ó$rýºåËñ–.YÑ!£ºÓÞhD1V…Rì4Šu}6àO}öÜ¡‚›7{nUq×C=À†¶xô}Á‚=ïÊŸTã‰;¼¬'>ÞËõHˆÄqbI æEž~a­;JÃÀ¦0à¡R¤7ϳÔUç²ÝÃyí"‰KŒÝÔ@[û˯äålU“›¸·Ÿ\oÖu¦zluøÐ3Cåq.“ilæF%Ûqñ9o™ ”œĈÆó|°PJÒI¾½bI˜­û@q gXÄÔª‹ýÀÙ=8ȾDÚž¢k¯»q­2D’ÏR¯—“÷RÃNÔ%Kº &çTŒËÑ—Ï/å¡g†Î»ã{//i¤ÄüôŽR«ÛÃ:þùÓM¿ðͧNˆ·ƒšYW*9ãä22¤Ë(I3ÆOF×\­`A½ñäTt b¤çGB!ûË^Ú)s¨T·]EEQ‡$q|P›‚IAqêJί›‘³gž<ýó?½óW«W7Å"-ãÍœÞà_tè,sdU’Ïzž0gFF¬å]a Ç©RˆœêÃO¶ß%±´öÆ=*;?u’÷ÞúZ¿&¶Hm•7ß÷Tƒlh˜=-Ìy÷=yowoÔ#ân!ÉÏÊ3 Ör_ç‡~Ç—çÌÊ7­!Rˆyë}fO gWåäQÌ?y‚ݾ3êq¿~›‚«²¿NOïg­,îÿã=÷w¿ÿ]ïl(Nr’yà·Çå.ÿbû²s¯n?¸Ø’>þ„:sÁ—>9ó-_ýìÜ8ê‹üLhÀ©™:)ÐÙÓÃ#ºz¢k¥™owÉ;¿yÏÌ@ÌKˆÔ9k¨¨sR©@Çqö8ïܘÊùcź÷óåìu ~¦èÇý™Öëÿþ!»ù“¿3 5? g°dwãLêPQJÅH3 {KË:ïÂ+\¤O/|HNjõÙ‹øz?±,æíMg7¬¸îKó¨1BuÖàùÉz/݈~o¢¾ðxk?ÿãÚƒ{é”)<ÕÙ9®’ £ÓK®úÐô_þÓ§ç’µŠç%®¬lhIk±ä’(µU¬ò¹¯¬ç–Ý_;ö0þãÅWè\SÞŽÈËmþ¿ùæQo>~A5ˈ#¥ëHò»sÊ®¼ãŠYËOõÿÃÉGqãSkèfŒØÒåhÍe;©6|ðñߟP7sZÆÆÃ60¾ðØ3<ûÒ]Ýa(:+Ë[Öqèœlç]à‡†RÞ"òEGäT7v–ä¢Ï¬^¿©ð1?@N1~XgK…˜ÄÒÚC§Ù'g©à0#çÇáPûj§¼¦Ë—ãSèV-Ï;Ⲇ_Á™*êðÂp1:éØ ^mµ÷WrRëC$†s%˜ðÜ5Ÿ¾d朋ß;…#ædê*Oñ 8‡–œ¼º3âáU|ó‡›Ía³2|îSsÈwG¾ˆ ¬ºq(û‡û»Y9çÚ{ïðÃÌ’¨˜/¡.,ëL㊳qÄW‰R“7ujŽ'ÎvUr¾r®UÁÅ$¬S¯¸+~öç_4ÿðñ#~ö­k_÷æC?@°N*EܨÄEÖï’?Üõë[77êêýrLÛ¾”>\ Ì%‰Ù™½ <‰îÑüø°k€*Oð'€‘Ä邽´_î—%‰å=ü'‰Ú1êÔ< ø ð 6mky§FøøãœAÍ8ë¨y¹ióÉj<âXyµ«ÄKëó¶»/ΓT½@’hXöO–ÝÛ€Û|p.ŽP§æ€¹N…xRÕ¤tɹ°“’»ÀÐݲPñÔ«žlMÔk×Þþ½LUÆ>ô­æE§Úb)ëû©uTHG9“¢Ø86çÙ¹³r“y„sŸKòÇ÷°¾LÓiÜúà+þ†ÞÞøX£¦þ8Eàá… ²ëž{¹pÒš ù…k6ä&1êôíÍfe³ª®‰"z2i@]Œv¨à¶YË*_aJ’2¢f¼ ÈÝ€4âoJ>¹8B$¬±™0ƒF”÷ÊÃîÆÝF‡3mÎY㊃ÞÀú‡ýµ÷þÎ7…ÞUŸ¾8 pÇ›RŒ!¬¢º;gJnNl@u‚µñœÙ8œRÇsc'1%×ò8Eˆ[Òr¯Ê•^Á͜擕±Kn´ý• CüðÌ^ÊÉF'¹äÊe¼Êlk[¡“$<”†öÒIÚŠÒ¶ö¤‘¾ÅÖ¶Â`ûGð µ™ qÞÕÆ±fâ×= Ú ôq¾è|À¤]Õ|‰ò3#§Õ.‘싞w(¢..Š «4~õÿÕõ/x;·Gé˜(å£ã§Z9Ž£sò”¶m.ÂÀæSO˜´þž›>ÚP;)8Íõçccð’€`¥ˆSÆ‚ÊàkuR/Àlò¹òCö6ˆ>4•X¾||'äj ¹lctè#íË4Éõ<¸6}Xæš–_ãZ j‚¨e)…r2ßAöç@ú[\»–"w²û¸}V‘½¯êŒºtÚUe_VWòYÔÅ%ñ²u:¸ù]w÷/ý¡Îµ;€§gNdݤvˆíU CÐpHUömšßpÙÅoš;kFöT<áúò±1øj+}L•úÒ(g‡sVP£™ 0Z©»7TcD`©T3Z²£c~8pŽ44KHEÓ´9Ò·–¥Íå¾Ñ2zÿë¨g?`zM`õ5Iá¬q#±µ}Xoˆj\/S«;ž¿Ë­½ý‡ÒPÃ=+îùpש gι¿Âj–ñ8B‡ Ôf©2 çiou΃¯Î¦Aº”+íƒ3%ú”%Ž,@.É—’ªA‘sƒê _ÎNœ>S]™u£¨ñü(Ê…][n–‘(ݪr5ÍUنٟr5`¢ü_صùvàË$¡Ž±m–ûöî´o3Ô¹u"~z¶ùÑ`Ï5$ÕÊüù9Ôë"Õn5¦RüìÅ-¸¨„ÉÖºÞöÇíÚÛ¨ïyçüÛnÿýûÉÈ»Ž .'@²û¡1¥¬@[G´+Æ÷‰ jĨ‡³*‰Ž$ãr£J`á½LŒôô•¢4ozì` ËÔÑ,sSæþêˆó?;ÉÏ&‹¿l (àgkè{å9Ö´|ã.Þo°oœç@³Lœ³è¢éõ‡½É¢ÖØÜÆš–o¼?móú½ômRnê¡¿>â}Ÿàg«ã£ÎÖLä•{ζ§þç“^uýZ;ÔwßAöé '£ê:Äx¨K“°ËîÊ1Ê9 ƈúíºÿùa°à¨†[oÿï ŽEãí®á"%WB‰QIÊ?Ó4Zué¡1Îþî\,>M!!{IDATâlŠÑuÖ& µZÆZn‰Ÿ©ò¼%)årŠ86¾: ÐMX‚=9¤p Ôgê&ÔÎ>*ïeªJ~¶6ò²Õ± sÖűUÇÎڨƹI<P®  ž_ô‚lAŒoÅø1I ÞÄqÚ,÷mB¶nr}í죆½LUIüк(²jãHµ@ƒJÕ#÷üD¾*/ŠñÎu vvOµÜb j'EÛk Šƒ}O¯~ðŸ”GiO<ì…~Ue "cï=üBûYãèC{çJ©ˆsQ'”b^\;¨À&Jaùá•äX*ÐÑ»áù–µ7믫§ŽªCL¡nî±€”³R-Æ8œ«Ô©†j Ý™¡êõNmd Ý)‹we?Òx}{¥gý37¯ýÿ¾¿zê¡dꨞ~8 ’ÄéQÏ÷Õ–F÷ÿò•ø)g£OºDùµè¨Ð¡TŸX:ž}TU{¸Ëè‹­x’Á:¥\Y?žXÛÆx²ÇÜxÚ„Î92¡j©§<õâ€m,oƒñSu“FÕ^¾ãùy~Å_‘ìœbL˜›vüeß 1hš0_ÜÄ "ðTÇŠŸæ3šUí¥½´oîc;ž½{=pÞ„Æ“ŽœÞq6Å6Ž…ÄqXÞ¾§R©°{yï7œü¨Àê %”@‡Žq^’z¾o]¡/(îØºcÙµ§vi•?Ýö–¬ïyieçn´/γ×c탓UœS”8¶„õÆ>ñTŸ¿e{é•eM¬M7ÌoP˽쟻hÙ5[Ÿÿáo]ÉNÃZTUýHDã[±z œIY´L‹¼ €›_:ÿ¹hxû$ù޸‚{i?}XôsýâÃWÖ=â…¹?(ª&I“QÀx^Fμ8žÚEÜÒ”>u,zLí‚–¥RÙ_TþÃá³ké‰m&Ìœ`‹ùX•`wŸ¨:0¾u66Pê8óä¹yñzž‰5¸J–½NrpÜj¼ûF¯uVñ$¾éž]pß5—5–š[ÚÇîO°u¬l.’Ôûàe2ý¨‚‘ÔuФ€®l–Ãh&Çèæ\Ï1 G²Ï€ÛWC+/ÅrÙÀKê¬ĨâDĈñ"[¶+Ë›€3I Q«i¦{e3/²L_æH¹ã_ P>ÍÍÎ}ñ¦›üŒbìÔ‘¤ÌŽèNå ,i½P¨¯ K¤$Be8u<î² l¿ânÿ`Ó”3!RÜyËïÚ)Àïy¢L,%â¡©ÉÐÒ¢b¤¼ïÍÁêG匀é&Ìý®þÐãO1~’È~I¸»Ãx}W•¢¡ÞË¡þVèëÝ[¿=€®O$I4¢¢êT­ñ«ë¾;á°ÓëfMX×€x>®˜'ßÓIï†ç†û›åNšù6Ëôqšå`w{ÍäR(ÙŸˆ þFBÇÄ$G…U—”lƒbæ\âY–”­¨©ðI÷‘ŠsêÒýüÜî‡1B®6TœË_ð‘¶ê-ÛKk›š¸>Í>¸È5×T†P^ E$ ÿÖßùŸç÷´·Î1~&É š0ïMÔÌšOD’ì© ‰¢½åÙ¢‰¡,^Õ¸0 ^˜óv­yL»^|P =¸8Ò°z‚ÔÍ=Ö›¾p bL("îÈüséÅ_|ásÅÞÍ»˜±ð{t¶î÷¯‡%ó†"^ÄHÅÈx)µêQúK"¢¶ò:[ÀI&@£È:çœ'â4 ˆ»{£àîÇ{n;8lÈÊñW’ãõå%tÍ5šÖrWºʤÇÁPºZÊlW”Ñ´w-×ì§]M¶ùL õ 0@à=‹zǹ_ii±@!. uØRaVPUï¼ ÅÙiŒîaõg§ý¥°9FW½½üâÓ Ê.§£@ªt Œr¨¤ ×Jàƒï©ç{êù¾z>ÎËÔ/“Ámë*xA† S-ê‹zñÂÀ3ovöeÄ•ý¨8þ<¬ºœ~’Ce7ˆ̦cieº§íˆKe$"Q´Oç¼¹—¤àXĨ—É H 'ûJó5^8Ñx¾ *..%Q$dÿsþšéàBëÒ•eS¹8­»Ù­Êv|+­\<)Š­JVdݺ~wé?µù/¿2/\P³ë†¯5N›3+ šŒ¹sšäM6ÿYßH÷.hjJ2q¢¡§ÇÉ=I¢£¸doÁó ³føz(Lª´´”½Ý{*>MM‰¸}ì1á<\«1^²á›œŠˆ ªª¼èìw{ìØ!¬XYvi(Ðr „¡ŸlQeÄÅEê9Fay½&;–hµ\,ס¶¥rzÍŒyG†Õ"gc¿Ð· 6ž7èbû†©¦%5és$¾&U›Ÿ½P‰óÓZ+¡g¢O_órðøó÷-<6üÔ½÷mîØ8ü±ÃæU}—aƒ£lÙ­rüõ‘°LK4 ´Œ”[­™ìT1FG2ãRLGGŽŽÑkUICø}R‡RªP°ó­' @)õFÕˆª‹Uõí,¦")¡$™À ZZx´¶4«U|Ûßcjfi§ÿöª®U÷]Dæ;÷.úg]Ÿ”XÙE©m©áåênœõ–&l© â‡ô¶·ô91›ÓwýË(àû¦ éäÚɲu¢eqv YÎúê,šu›†ó¾£þã7}}ʲU>~ö6¶ß#Ò7héL’¤äÏ $Pšå&[sfkpêá%¬ÃÏÕ•néæ? ¨®0þ• 8ŒØÒ`ïD~ü£ã@GøbX×0‡à‰Ä..Nö‚êâEüª:‚êúÆ §Î bGEÛ,O?:9ñ?ü‡°vò‘~®FÔ9/T›4sßþaç…¹E;V­¸ue³üšfž'Ù£` pfõÌ#þö°w^VTÕ:ñ¯Ó‹¶Ójƒ˜uD…Õé8¼!¾¦ƒ²,4)uS]×4߬WCVâØC“ý d?é"e}ÔZ{užÿùkÖ½úí;g´ÿÂiÿBÆÿ¿x ÝúÇÕç_½æQ}š3å¤?‹Ù°L#šåýSŽ{Ûïgž~A¢‹A?H^2ý*³Ñ—Oþ‹òl¸ózò;7]Í”)?§«+˜ zâc‡ýÕÇVM>„”sÜjŒxÉþ8¥NÛ´YçÏc狲呖ñ¼k&-?üœ+ÍL˜ ^¢ª?Dm„ ³®ÐÝi^]K±¿ œÃÏÕR5u.53烪3^`¢Â kû¯k(õïìÇÈu8÷]lK ×DÅ™DP}za ó[º´ý×IC櫺½8¤ªÕÆ$J{älïέTFÔ#Ç_ýì¡Ó¿xÅ!Ë&Ô÷!nq¬ò/¶à$“5æk×o• Œôóõ‚©¬xž>帷ë;a(¿sKñÃQiꬿ{\Nc,"\T¤nîññ®— ó;7}Ô,<£O€LÌ6ÌyÛI®T¸ªfZã¬)',Êwwz¢&£j1žO¡§?[‹Ÿ\ì¨jêa. 9 bªjK…Є¡Þ¹Ùµßöï”ú»"DîĹ›x”ŒÖk §Ÿ^,\ØobÉÍÓæ×œOwa°XŒ«=Qñ½²]¨J·ZNÊLªí«=È»Ä娀'ùüýšìÏoíê™3‡c7mb#¢äu‘Ï2i–3kfñàÌSÞ+âiæCåeã—Œ©*"†Bïv¶<´œ8ßÿðI¿ŠÜ<ëÔóϯ™u„Ž*ðãµYÑ®–Û…«î£ûå'ñþÍ™3gŸqá©USçj Füª:¶>qÅÞíÌ<õ}TO;Œ ª^Å÷UDPkÅÆ%J;u×KÉögþ$.. Þ¨ý6£ßÜð†Ñk lŠj“iÉþ ùØÛÿú½³7L#ò±¥û…BìƒÃˆ&Áó\‰N›|=RÙy†R’ŒQ ôÒúáàêæõÙ{אָæ}}CÜǟ׳]V<ÿ†äÛ æ2êa?Ð{c’mÿn¾ #{}O®ÞÁî{h^U‘¤¤û«$äÿ_à F#å> ƒ~®vz®aVmP31¶8L±¯‹ü®WuÀzDþˆê/óˆW¾ž´Ñû!‹Wú Ô|ý}çL»úüwN ›_5Æ¡.&²Bd Î *‚‘8¼ZùÇI§Ôu¿òü)¨›Áèw·í‡¬Ï¼‚‹ ëH7*­yrÃÀöU'¡ne0¹H0AÁ3›ãÂà,à’-êÓk†€m&̶¹RáI’¯Pë?€÷ú³ÐëÎqQ]hDZ°@„rBÝ»ß|tÍ TÓ8'ÃŒ†€êœ‡1B)rô X6o+²zý0O®à©Ù²­Ô<ˆÇ/ôÞE·§_¢ü†yj9øï¬=Pús™ÜPÄ Ì]tivçšgëbúª(äM±—Èw’Tý¾Qc÷ÆRS¡®[RÖcs$ß[ö%à6ÏcMmµéžPë•rY‘T¢¾ÜüpÁ‚¹UÓuõ‚rs£ßŽùÆ’¼ÎãÏÝîx[å¼Þ>üEéÿSÍIÔ'‹øHIEND®B`‚pyftpdlib-release-1.5.4/docs/images/openerp.jpg000066400000000000000000000076151327314554200215220ustar00rootroot00000000000000‰PNG  IHDR¾.¡ÌSìtEXtSoftwareAdobe ImageReadyqÉe<PLTEíííÝ:6àUR322¨¨¨óîîðÒÑóììÞEBÜ1-á]ZœœœïÎÍYYYŸŸŸ¤¤¤qqqÛ*&éèèµµµîÂÁÏÏÏ………¾¾¾’’’Þ>:¢¢¢áNJ•••]]]€€€888ꘖÜ/+âa^UUUEEEïËʼ¼¼òããâfcóèèðððóêêÔÔÔ¸¸¸²²²>>>°°°ÉÉÉQQQÞA> í¾½   éœšiiiÀÀÀ***ÚÚÚßIEÇÇÇ목áYVꢠòääèãifÛ,(ñÛÛ爆!!!lllôññxxxßKHàQNì°®|||è”’BBBºººãliæ‚€ÝÝÝÜ40îîîäpn¦¦¦ð××ꧥ춵uuuñÝÜæ„‚ÃÃÃåxvÞÞÞòçç™™™ê žzzz@@@åvtâ*&í»º555LLLIII%%%ì²±æ}ꨧ˜˜˜åzxïÇÇËËËÄÄÄè’çŠärpòààñØØé–”Ý62憄sssŽŽŽíº¹ôììbbbôêêNNNñßßðÔÔì·¶ì´²ã3/ãkh,,,———;;;â_\ÐÐÐooo¯¯¯ÿÿÿ‹‹‹ëë늊ŠüüüÒÒÒÓÓÓêêêâââæææØØØ÷÷÷­­­þþþŒŒŒÙÙÙÌÌÌúúúööö‚‚‚ôòòáááÖÖÖ‰‰‰ùùù×××ûûûnnnýýýøøøÍÍÍ®®®ÑÑÑäääòâᬬ¬Þ?<çççìµ´äolàààÜ30ëêêóééãol玌á[XÝ;8ãããîÅÄ狉úùùÓÒÒÙØØë¬ªïÈÇñÚÙØ××Ý74ë®­ðÐÏðÖÕèîÆÅë´²È[YîÃÂÁŽø÷÷¿<:뺹ôÈÇꥣöõõñÞÝÑÐÐñ®¬ÂcaÞ51÷öö£££ãmj000媩òááÅÅÅÕÌÌðÓÓäusÒÑÑàßßòååÂ=;æ~ÍÌÌå{yîÀ¿Ÿ­¬¬Ñ[XÛ)%fffÿÿÿHd¦ftRNSÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿS÷% IDATxÚì™yXT×À‘% 8A$( 2‰²‰Q6Gb¸+uEmÚ˜¤M5š(Ñ÷ffd`Ã. ÷Ä„,ÍfL[ÛÒ5iÒ´±­i{/·çÞ7oæ=fÀ¥ÿÄïó~óî»çÜsï½sÏ9ïáAîêæqÿþ=ü{øßü•¬Ë™üàô;±Æ-)´Š}mè1ø1+üÈ$×–Ù!h=rä°T^–¸¿3è˜ÔäÕÏÊd3·G^ÍÿóÙ™¾HuA…²&æÜwkÈqÁ eÊÄÊŠÜ:iV!c4ttc倻æbŸ®Q’T¨ËÂâæ—m¯¤ ®ŒŽðp‡?2§]xê­ìÞö.@ÈwÝ-À¿¹9|ix@ô’R…Õ÷EÒCÔ¶ç•Ï‚aÓ,÷ø'쪻œw¬°b©yyŒ+þ[é(ëâ¡ï3¡¬Ø›Ò§í˜£–‘¶A+䑤Êhrlûðø'RÝÊ£—‰KLp‡? NŒ¿3 ).:ŸÄ:„T%7Û9cDs¹ûì½5)“„HHHx"ñ`ø•^^ŸIZ™ºÄt7T,‘á¯ZÕ^É ¯÷¹6üÍÎ]9ëêzó„ÄèYK¼ƒÓúÿ"óËå¡p«ŒŽ¢ c†´!àI‡‚ØnO’áwHŽPþrþ!¸ùžRŸŸ „’}†Ã7l®X^eÍ ±§EËÁïÆ–š«'™ÐDO)¤,ü6^aøŸJ‡Î°¡0~«Ta ÅWKñGkÖoeø³a¨oªÐßû,=6O-ò+zÀ¡òü_~þÿþÚ~ò‹Of›ù¯üÕŸ$¼ø»ïÿ«r:”14~!ÅÏ•âë÷>wÁQJ»—ß@h"݇èE‰Þ5ÿÕKÛ¶}2ždÇnÛ6…Rÿf"ÚöÒn§!òt² 5ßvƒ/sžP œ;c(ßl óý5ü×ïdÊìúlÖK¡S´kñ…>èÙÃ飤w9B~Ídád–ÃèST·ïà`ü¥¹å¹¬•‡D¸Á?åhÝÌbÏ’`Yä±ö÷÷nlÓ°=z^‚ÿž/àËÓ”Ï|ÄBÏ´=ã§@{›ì1| c~Lü"B9pXP¦ÏÓ~ãý€>™¾5¹ä4Bž+ãK›¿üÔÂ-‰Ð¶$z… I6Õá-¹ûö ¶ã/‡Õ³VÉìN/ÔñÙärùnhéÚô¯ ¹ïBS¨tª/ò¤î#„‘÷ç“+*Ô·šl%×c…Çp{ø.ñ—#ÃâW\•eÝQÔ©åA~Üs”κW 3zÏ\"àŸ£B¸6ºz¯¡Q÷Û³†_oÁ£p]é¸à«—ŠM=ðòMñ+7Kª67ø;ÖËk ª¾”Ù^µ‡Å›ÅH±h]sx['“!Iøä ñ‚Ÿ¯BèÆÜؘSõuÁŸãü#Ö‚=ösƒ8º°°0z K¨åd…˜àû“ í-:jÎÆšÁ'¸>ò]+³;•î\2òu¤P ¿¶<……°›=›Éh?tîAûm4bÊ5¦`Çx>ã‚/ œ&“ü:ád 坸qÁ—F7õ>u• ßçu$DKxàF}{ÅÁt€ËöC(÷µ‰%â{!)Š©Ï\¿†ÐEâ‚«i+‘ÕO5®øÃâ—(;Ï4ˆ‘Å,¤“ì>û~%k3Y%£¡ÂH/x¢Ù¡ýþxð}šÏ¨ë÷öñ'ØÏ’réY¤Ù¿uXüRàÉ*•ÞüyIÚ iÈh›ÄÜÜ·ŽadžÔ 8•‚ååà]?»s|!eÉRíà7C^UäHƳ}‘"º#%¯S»¤yöË[s8ÚeÚƒBÉócW|ÍàuCǺÇ'L½"åöðiúQ$;á‡à¶GпëèC£(ÛÂgσôN…Yk§ÐðíáGÁ» û ÂŽ.”´è² ï!ðÓXÁãÕp{ød+xÏ.G-v )ü®‹ÛrRÝ`è)4ñ¢õÄ9öRzÆÙ?=[t:ÿ =ß Îô"ÀR /HŠj÷ïºêÙ»®¤æ!‘¬lhã«7ÃÿêfJš7%ÃÒ~ b=‹á-Ñù™¡y'­Â—F]º4×ñ*óOÄZŸ02ýÄQ¶;B;-Ý$Ðý—†»| KUQµÎBÕS&øÿ™ýì44ø¦ßyF–~?sTrúÑ¢³KkØ–ªüüü£’ÑçWe¦gî*µo–ë³×-®÷÷%ù›d_ˆÎ+£æ`×™gÿPò¯¼Hz¾?Á1#fÎ>¦²½ß@O{öí£fÜáW¶^ˆõ™wïGÂgTHµëîŇué½o<¾™3ÇA¼mà8Ž6˜…h‰’Y9a Bø…ц8ÌDz;¨sœ]H0jæ¨E3 ¼J¸w‰‰#µÔ*tè­å„°BO` ü¾ËÁP -òMLÆ4@г8×B?€©W¡×Rë4àÀש乓XÉ·#o4’:\GŒ´œÕÙ"v}¼Rc c±% pŸ¬ƒ~ ëT*°ÆH_WÁ€Fi€©u˜d`c ü`ÞÄaÒF­¶€ ù”gÊ MD\SÉgÀ$`Æp©q,‡Ár9ÜFêÚøaØÜÁ×àŽÜ ·VU-ÏôZZ=\@ Z¥Q¢zbÆ1U1 ¢'5¸F‹«¼qŽéê²á„ªFе8 k‰c˜©ÁÕ€_W ³ÊlìÍ”‰ Î Õö5»šÆi)~5Ìà#꫺™¸» ,0(VcžÓêøF«´âïÏ{è5uuDkµÖ1|ÈP¾¬:ÃkÂ`ývÒŽ{Dü:>¡F€H_Æð•|‚MÀ?Û8?O|Í©S‚ âý[ØÇåõVÀek6eð3E|·:Äþ­„&_4àÀl‚bÇce`-,¤…õõ|“–¹¾jÑj!â°õvŽñWà'ã±ákS~L oð-æ&lfø3¬mX°ÊlÆž–pµØ_Ï.[“7ÆÄ‰øi8É!ñûí€q¼Q/pâŸÀcÓ ÜqÜBôúÆFÀ·XéÌ {ÎÍöö›ÇãƒH,7ôð-nŒPãý ¸ÃÝ F‚4x„à4¸H‚«[1àëÛÛI µA8‹7- ƒø™ý8£ZXÓZÕNþá‚›”-Ä$ˆEüWp†© IÖ‹œø–˜Na¢‹¢‡$G=¬ƒÎt¶0Œ#â`Wá¦ZgÅšºÜ#Ø£¶ ÀÆð±&Áv^ƒõç‰?îô¶ ø6+¬ 64Xó(7¶烨¯ÃšzúÿPùÅ7E` }ôöó×/?›¾ý¸o®"ÆÃA”E?ýeüO”±ô˜­–a†ŒŽÈÿ®dvêÙOŽå™Òð‘;LRÓÌDz…O2£È*¼n9ÑM•2Î_UƒHâ×à„é^/¤½­i\cÎæéVSŽ¬Ò´Ö¿òÇ7ÿ@£ã£' ÑÄ1 p€M£' ÑÄ1 p€M£' ÑÄ1 p€M£' 2Ç9p‚ÿ°ÁÒ×G8¸wïÞׯ_¹¹¹ÿýÿÿû÷o qñu@Q/qüúÃðï?3(Y“ÈÏ? LŒ¤’ÑüûÇðÑ,ˆDqâøõGQR°=È,Ð@‘ ih9ÊLåןkÎÝ+_wòÉ˃ªùþû¯‚¤À"ß/?Ûvoºûâ#–bàçŸBo£2wƒ#w^¤,:š5ýù»-ÞÞUK(Ù¿çrÑâC ÌHSKÿsr°,Ir2òÒþþ6£ùû¯¨ w¯q¸±²(/Ê<Ë“÷_Þ}ѱãÂ矸xx==<äåå§OŸ”zõêÐR2¦¾©ˆ²8ûýWIR`o±‚0ï‡o?'ì½qèöóÏ?~ós±Ù«J¦Ûi“ˆ¾Œ°ï¤m÷”i°4~Ù˜™î<|óòówQ¾@CÅžMgP¢™T)póq†™(ór°î½ñôë¯?LL H‚GUŒ¹j÷®‹Ïß|FŠ¿ÿx›)CR0Á#õï? 1î-yžÀ2˜WŸ»ýÙ{ °ŠŸ™‚¨Š¸‰ò‹Oßg¼ÆÁËïããÑLJJJ4 €(JLÌL“£l€)ãÞ›OSw]º÷$ nsl>uwå™»«Ò\´¥'FÚøMÞÁðuâX3ÁÚ­þjbFpŒ'¯€ ÷ ¨˜×±¦¶¿`[ %¤ÑTÏÌŒ\6°23ýüøuã…Å®z~zòw^ü T‰\xüýo$/ª#%ÌýÀ 6Mÿì… ßA’•zëúSÐÄgaÎsÔ–7_~Е‚Ô) âßÿ:?`ʸúì}øŒ]WŸ¼ƒ†33++³¹‚h¥·áÏß™™P|M°Àø Lš-DAnæC/9`†ˆ˜½”28XAˆBž¾ö$bÎ>`øúêË»ëÉ2 /abbgg- –½?ÿˆ q(‹k³Ы¸Ö¼[¸ÿ%ŒT%4åD¸9Ø~ü†&$ q EÀß@†–œÐdQ~NväÔÉ̼ääí?ÿþY)‹©I ¢·ÿþ‹4U†û›Ï>ÿÀŠº†cþ±›Þ}ÉwÖ–.Püúk®&i«*1÷èËOß!óYÛSGÈ.^süêýW Äš”µgÿý{äÆSß);{v]ä#rq 8Y&>ú‹µå Vöÿ?¥ËˆÌ’ã8ûèËÙ›.><}ó9(A .ö“מì¼ú8À@!ÌXiç©;ÿ@Îýñû¯©²Øô(›#w_NÞw¥Ì]ßWO^’Ÿ X¼ÿªtõñ ^£˜.0¼M”JÝô-•Ä!-`Fœuøúô½W~ÃÒÇßÿÿØÙÖf¸ p±Ç/ØLµ‰Vjú²Â,LLw^}F[׎‹ˆF" ÓÕ§ïO?x 40@_áê½Wˆ âß?~>NwmPÛbã…‡ ?3¡fk`Ù°àØ­:£+õ©;.€ª$&F`!LjÓ^«ð0@%fnðª—w_¢uŒ ÞÜ¿?ÿ^¿ú¨.!@T°SÁÿÈ.+ú“D3¢¹Ÿx@d–ÀDËÄÅn$' d¯¿ð½Ê€FÆÍ—iMIAV^.ˆ×€)Ø¥–´ñªgªÓl5ß}ùqôÎ f&Mémù^ ü ¿‘Š™_+|¶äx˜)ˆ,?}§eã`²àšnÕe lêCôT›X(‰sð¶\Ï)‘ÖÊ¢|GF `ÕÞh^èª4 æo¦ßß~n½ôȶ-¸9Ù^øõ×F]JI„˜X7]zÀÀŽžè9ÙXg¹,/ œu@…ǯ?ê2¡FŠë/<¼|ÿRÓ›‰‰éë·_Ï?~²³´YF 0`,áe3¨#‹3œÅÃ?Pñ*-þae`–û ’ûV b„²ÿ`Y²‹€"9q@lú #uq> c¾ýŒ³¡ÀÈpýù l—ñ°ÿ…¹ò/8•³ø‰û¯¬:7š·¯wìÝbÚ¼X‹JOCD9ÿû“ž<0jÍóö 3÷Ô®>–>gŸeû†§¾æ8h»K¯Ÿ¿á~ýbós²¥.<¨V»Òµ«IÓšÞÝ—€‚‰–jL<ày˜¬­ %q¤tÃè­+¤·]yôØÏÄð3Ó“'oå%0ÍË<†ï¿2´€âS÷_e@ÍØ,À,ûù;0)Ùñ–jGª+ü½M•µäEùyØA©äçì#"`Iâ–ÍxâRË`UñÜ* €ÈL F##0Ãú'°‘ˆ«Übdüøý(˜˜X0Ú§¼öœ¸íøµ'_üùýû/°JÎ[y À^1«0² +Kž“Pqê¢Ã—¯? q°303ݸýØŸŠû( › ´X¼MÝ5g÷¥×¾ýøغ ,€¥—€×oøRJFÆkOÞ;#Àzʘ ^øû_H;læŠ3÷@í$éØ[’yÎ:²ò"ÉÖêÀïákAƒch€eê¾Ë];/Û^Àî 0•‹À@´¡È;ÆN“XœüÁÒÌ¢B“Õ42t鉃D+™qWx_~€Ò t´ˆ¸ØAËK?}ááP•üùX–þgçá6è€ ÀÏ?i¨J˜Z2B@RUIüÍ—@ÝÊ"|À’¹ÄfÅ—_¿ƒVž['@cY˜)XÌ0AÂ0Öÿv©€ÌcEVH“ð÷`cXzmÜwí –È†úŠùƒ7Û¯<6UïÀÃÎÚ»ç2°õ€e¼(ðŸ±|ÅQ»Î M[ε˙{¯ÎÜyÉÄÁÊÃÆ"ÌËlrùèÊEšª‹“Îp+`KYÐç®..@ÆšµkdKs3{úÌðÖE Q\|¼€¨ñ LÀQ__okg÷àþý)S¦ÀS… €ÈLÀxýýó÷Í€‰ÃBQlÏùØÕýùk©(¤o¿üøåÓwq^‚&ÿûØœl h~ñéûÂã· p),î¼þˆ3iöӋן¶]ye¦d¨¸óÔ]q1AOm É‹OÜ&0wÈÆ¼ãÂC`²FóÌC׿ùI`!1¸7lE|úþóÓן÷Ÿ¾_søúQ·³bí¼tdµ\!0elظáÂ… ¯ß¼õbìííEED€‰ÀßßÈUPPؽgD ŠŠŠòòókjk!*©çÀd* 2¨²øþsßͧÀ|m¡Ú¿û°Ý€^¨þþ+ Ìd¤dîzþ÷Fœmà†Îë/?4%æ½öÍÌŸ¿6Ÿýùé–à(d¢`,hõ¹ûÀĬMXy95¤e>°Ÿ|éñ»€Ùã÷ß²u']4d–ºƒ³Ø&4`Ï ÞOºˆ˜@#†ßŒ.<è µ6Ì9aÚÁMÿ—/]F3<ú (Iüüüp) 8xð`m]ÙÞLj‚A06ÖEÇokP qîPKPUÿÜCƒ Ь,Sg°¹Š(PÍ’“·17¹ ·®}B)A`=òòó÷»OÞ±³‚FKŸ¾ûÔìX«€ç¯!#HpÄÀ@é¨< ó¡[Ïî¼þ$#Èí¦-f ´v¿¿~þ}Þ°2ï»ú´jÕ±¯ßcU l­ó p»èÉaï˜üþ# L,̯?ÿ€4ÛÿAÚàƒÉ!Ë &Æg/?V®?dfÚk-ÍpÕUåæµ.¹9Yõ”ÅWd¸¤Ùje W÷î+f,2E€Aöû(oƒ‘±ÎÇØÂæ§_Ÿ¾2ƒÛÿ>_pì&Pq‹¿©(°ùòí'h¢þç- dfÆwo¿ì¶=ÁV˜(ˆchõ¹{Äîlcf„ wb•V‘¼<[s<'':©I°³° ºüˆT–™n ôïʳwo¾µàÍ/YYÙ‹à–&¹@A¬¶ÅÏ=K´‡IDrµò÷ï?Äá>ì,KŽÜâfo 0–ÌÀæÕ¹GÀlð˜ SP_~þ.Zu| °þÖÇCÝ&ò"sS¶_}òôýWq~®$K5_}y`&nßvQ¤31öî¾ä£'o©$~¬"`ÁÑ›Gî¾ºŽ”£†d¸‰òÄ}Wö€ÈÌLKOݦoˆ›Ü|váᬛ—ðô¼jP«¹Ÿ¿ÿ£?ÇAˆ€]÷+Oß>ûð (¢..h¥,.ÊËl\wRQRÐÙÙyÿþý¿ÁÝZÿ€€’’`¤··È…ô>€‚XmLJJjkkRPTÜ´i°­ª¯¯OfP €"«Íñï4˜ÀSe“¶_Øãi¦ƒ¶£º°uQrëåÇ·žÍ>xíÌíxZjñ–jIÖpî…Çocæí{üê"ã23øô=hꎾë…–SdíÞ~5åê{`) zÌ+ž’€‰éÔ½W—Ÿ¾Ó•òÖœ»ÿ˜¹1f€=a`»˜ö¿ *܉äõ—ïÀîÏÇï?e¸–¾~÷%gùQ}yUI`Ljà*ݽY‡¯×o8ýæÍg YdC€Mн{÷“ 0¦á\äv26Eßø’’Â:„ÃßÏßÿáƒ8ÝG  ²##¢Ú±Èrù᛬øx9yØ™@²ÿ?|ûõØÊbιÀÀ™¯“tє֓þýçß±{/·_~ôXð jä~?+óýWŸ§ìÔ“±U‘Pç&;¿ÿòÜÃ7@Ý A¶¿Cgíaef|üî <103~ûùh‘0ªØ1‹fÆßÿþ9ônæç us6ÖÔÌÍΚµø+ÐkØ—§€“dÑê5ϼ{óÙ4Ò”…~ÿºã°§*ÉÏ¥,ʧ"ÆlðûÕÀNÖ•§ïŸ¾¯d·Æ€Ñ<‹¦§¯ì­› à>\˜ dÐ 6°[«o`À@ @d‡>ünúöëx` @~&`80 Ýña6|²lë²1ðDë¥û¯.Ý}‰˜:`b„ÎÅÃGÏÿý;xõ1H+3rG÷G ¨ãðW¿æÝ§ï>€&Ap-jül9S3#ž^îǯ?>~í²‡*’¬ÿþü¦ :t¾Æ–±Dw“ 9Y »¹Ý L@%ÅÅvöö‚ÀN,°ur–Pàèbü£ïX@‘œ8ð-OÂ;‡„ @ƒ`¬Ìÿ™˜÷ ˆ]nˆK DÏyÄ8˜1n¤-ÅèC‘2pÀi¶cŠÿG´^äå)ãâ… @6°Ì˜;w®¼‚†£ÿb´ù€˜Õ{& û [[:à€cœW>ÏÂ3Bf¹ûûúðô‰^ ˆäÄAùÒ# Ç€õú¶+ÎP’Ùe å rAØÿ?Œ¡•l“]–MçŒGTL±”° Ž&:çüöʼn0O:`¨Ä×èìãOED¯Ë:çèi¥ìÓø=´»XÑ‘ª=¸Ú£§‡Uå¢isÍX[ê”Tû¦.¥W¸«²cÄšëé©Lý/pœˆÌÄa¡,)ÂÉAÆ™`@×Z«H<ä%x&˜†¤°oi¡%KÌ™`Üœj’‚:òÏãåTå#¬ÉÙ‚\@ÚZUò…?ñg‚mæã"Ê¢?‘Æ?(ä=X@‘|`Üû÷ žçèi‚Á`;MPPPˆT-Drâàáá&ÕŽQ0À—/_IÕ@$W+?~ü ¬h @$'޵àµI£`$€"óÚQ0ä‡ÔÐhLœ €ÈYÏA wŒ‚Ah´ä8@&ŽQ€Ðhâ8@&ŽQ€OÓxÍÄ›ïIEND®B`‚pyftpdlib-release-1.5.4/docs/images/smartfile.png000066400000000000000000000242701327314554200220400ustar00rootroot00000000000000‰PNG  IHDR |såØsBIT|dˆ IDATxœíwxÕ¹ÿ¿S¶W­ÊJZuK²dÙÆ¸cŒ;`l:$ô„š˜Àå—äÞärSÈsS¹!!7¿› !¡\ˆbÅØT7l\ä&[–,«íª®VÛËÌ9÷c°vgVZY’}>Ïã?¼óÎ9g43ß9ç=ïyÀ`0iÀuãÞh&åW«íyÚr^ä²€*Å#¤­«%ÒÒïŽ 2ë†2Æ& €¨álŵ¦e3–eßZVgšeÍÕäL¢ád"Sâïøû;Þè_wd§ÿy_oü0(ÈXµ›qæa¢qŽÃñÐT/¾Áù`íöEZ¯Qs!”z=±®Mëùãžw¼GC²{´ÛÊcÝÆØÁ œ~ú¢¬û¾øïå¿)­5ODNõóÀqg´Š–šy¶ÅŽ|íùí‡ÃõÑÜ=šíeŒ˜hœ£pÄE_t>|ùšâï-¢i$e9Ë å•3-—5í l Xãl‡‰Æ¹‰°âK¿]~sÁÊáˆæ,½r¦åò#;ý2á8»a¢qŽÁ œaÉMù,½±à+™Œ,¢¹ò|ëeÍ{›Ã~©3“e3ÆL4Î!D-o]rcþÃËn.¸7Ó‚÷s˜í[ùTóŠöCáú€7Ñšé:csQË[1ïÇ‹oÌÜ$‡&«h²qagcx_À›86šu1Î÷ã–›zÚ¢[G~5Œñ³šÕ_)úóWçÞ(мb¯2èK7>ëùïoôÿZŠ“¾!‹ä qU/»|MñO˦šëÔ4ÃÛë{ê{M×t·F7¥{ Œñžœ¥ˆÎrñí…¿¹è:çm‚ Áèitþù{M_hÜîšÈ4””øû‡ëßxÞ`ª &ªx!u¯Cg Õsl«íì xÙPe¢ÃDã,DÔòÖe·übÑuÎÛ²`¸›ÃG_øyëÝ]-‘wP5u™F›wÞ‡ÜâÓ´TѤÇqF‹h.j^Òy$|Ðß—8šÆå0ÆL4Î2´z>kéÍùÿµèzç]¢–•ìÛ…¿ühÛwsxcºu‚x[Cø}"Q[qy†¨áRÖg¶kì¥u¦ÅL8&2L4Î"D gYzKÁÏ^ç¼SÓÓs4|ì¥_¶~ÅÓyw¸u™Æ;ÛˆLí¥u時zýŠÙ®±•Ô¶ïô3ᘈ0Ñ8KàΰòîÂÇ^ã¼M•`´„Ûþú³c·uµDÞiÝD¦ñöC¡÷d‰:ʧ›g)ͬ˜³4öI3,·Õ¼L8&L4ÎxžÓ¯ZãúË…W;oTãôìm‹xžzèèÕ}Ñm™j%ÚB1â˜t¾e¶R<‡Ñ*ZjæÛ®jÚØô1çèD‚‰ÆGÔp–•÷>~áÕÎÕÄNt6…þù»ÍWúºã»3ÝJ!·7„7H š]Zgž¡´Ô^«çu“çØ.okíó÷%š3ÝÆèÀDc£ÕóŽ‹owýzÁÕy·‚rc ;Þýןµ|Y©‡Á‹œ-¿Òr±«ÆrKn™i¹ áuáÁD'($¥:(i?z7%¦Ò)¦™¢&¹3–ã8Î`M3ÌËÝM‘þžø¥òc ŠVÏ;V|©à ®Ê»5Õ‹y)A¤7Ÿèxøðvÿ³©Ëòæ__ô‡9WþǤَ¥Óí‹ÊgدÑèø‚ž–Ð6"ÓˆR]”@r oNĨ±´Î“Çs\^¹išÙ¡­ëhð¿C¤Ôâ” ÑÙÞ󤿹J>£U´TœgYÞÖÜçïK4)_c,Pµèˆ1>àÎpÕ¿?7ïòœëÔ¬%9™–½ J6F»¦NÔ ÆT6•s—,»»â%AÃç©©W–hdóËÝß~í±ÎG¥Qô‰8òu¹_þQ勇fªšòg&ŽÏºâÞ¢§æ\–s%Ï«[ž~2’DSðEüR %4¡dWRg¿â+¯¨J!o[×óà?ëx$!1%{³]c^x]Þý`Ï縄ݔ €Î(䮼ÛõÛ9«s®Ž`Çqz%9A:[ë}›Ëâ9®dªuþ’ÛKŸÓ…5õS ²ýõ¾¾ù§Ž_ÆÂrTÉ>»@7]¹”½ÆØÀ|ã½Ip^vë·s/˹.ÆéÞ>XßÛýHÁŒö¶Fö:+L‹ÌYÚ”½Žã8G¡¡Âš«›îi l•âÄ«ÔJ yš#[#!Y,›jš#j’G®v6GŽØì{ŽR(ö|g&ã­žÏ^õÕ¢Çf_š}ÕHxö8ð"(äTv‰¨ÜÓu$¸;·Ì8_I8 «ÐPavèj=-Rœ (ÙSÉs4²-”5åÓ-sEñóÓ±„Pºë­þÿm=z[©<Æ™‡‰Æ8EÔpÖk¾^úü¬‹³W« WÂds›w¶¼‰%ÛXHîp |”WfZ`v( ‡£ÐPa/ÐÏj?àߨf:–$ÜM‘MAoU3­ NžŽ%„ÒæÝ=¯ÿ¡ó…¼Œ1‚‰Æ8DÔp–+î+~fÎʜ˹aú0NGktF‹Xvp³ïï”BÑÈžöýþ ªÍ+Lvm¶’½Ý©/Éræµîöý“T²§²»)ü¾çhÄ£3 Åö\mN÷±ˆ{Çýk×ý¶ý+‰a»µSXº¿q†ÎÀç¬^SôÄì•Û%I…$ù½ç»þ´áiÏ×€ÔÔOÛc*W}½êõÜRS•’-%”v4ø?ÞðøÑk1¢zß^à4¼À ”P"ËTbJoXOca0 …WÜWòĬ‹³/WJ¡7xžã]“MS©LmmCïÊ/§œ ÞÖ=¾·ó+ÍKÕ8GmyzWN±qAgƒÿC)NúÕ´‹R"S‰ÈP™9Œ1v0Ñ'ÌBÁê{‹Ÿ±Ì±*>Œdˆ"'–Ö™gÇ#Dãn o£DyvBŠ“¾ÎCm9ÅÆy–l]¾’½-O_dËÓŸç9ü(:A1câÂDc ˆœñÚo”<ÞÇ¥j"=ca9Ú},z¤a«oóî Þ·:‡"Î œNkàõJ¹,‘ʧYæKq¢ï8ÞªF8âÙã>ø(Ëe˜eÍÕ¹”ìíùúR{~¦ûP`s&„ƒã µ9u3æëmùúù¢†7ÆÂr¯š¶32 óiŒjçÛî¼å¡ŠÇ•¦U ¡¤­!tøÝçº~Ö´Ûÿšœ 'OqjrŠt3ç®Ê¹îªÜkõ¦Ôáà’ø[Ovþ|ëºÞŸ™*\€É®™¶ô®ò§ «-3ÔØ·îõm~ï/­_Š…¤agèDÎ:eIîwf¬,øšÁ"Z •£û6ö<³ëuÏ7ˆÄfYÎ$¬§1˜·*÷{åÓ,)÷!2%;Þìåù½ª·=ºœþ’“°_î<²3°ÎÝn+›j¾È`S lj‡,KÛÁÐ;PáãHDIOëÞÁ Uææ,m®’½Ý©/q–›–´ìø'‘©_Éþtx³Î\]ð˹W»ÐèøOWÞ "/:+L3qbîn½•n¹ŒáÃDcPw¡ý®¢É¦IÉŽ™’-ÿè}îÕß¶ÝI”V—Ò~w¬¾e_°¾z¶õ2UÂ1Õ<‡ã9gËÞà›PሔdàèǯÖX/3gis”ì-Ùºü‚*Ë%Çêß’Ê`'àÎ6÷×ïf¬Ì¿}¨ãÏñÙÅÆ©{ßî~*gƒ#‡‰Æ8 Û¥›\5ËzÑP¾Y¦dóË=O­ÿ³û›r_ê€7q´e_pOõë*%áàŽ/©1Í ޶†Ð{TE†."S[½ocn‰i%GW dovhórJ t lIDI¯’=/pÖy׺~_·,÷†TKê 'Ö¿Ùõ'J TÆÈ L4Æý±–ªÙÖUV‡æ” *I"òk»ÿ¸ñYσ k;N'àM4·í+›j^l²‰ÖT¶‚È euæÙÈjk} vV¥£Á¿Éž¯?ßæÔ+Ù[sõ®œã}máC‘€Ô$½½E,›w]ÑcSå\+©ý<¿äß»¾û!¨Z12q@"N|Í»ÛÍYb‰¨åL±ˆìMxÞ|Âýóm¯ö>,Å©o¸eûûMî¦psq­iÙ®±§²åŽ/®1/ň®óHx!ˆ+¶=Fz=íö|ý 5ÂaÉÑOµ]¦Õ e‰( ™Êƒ¨åm–]eqõŽ…7•ü¢dªíB^àSƪ™È{Þì~²«)øšR½ŒÌÁfOÆÑ"Vhô¼18 uHñÌ…RM6®ü·Êÿà,U~±ãQﯞÇ>x¡û»RB9$ ±jÉeÏ×Ùæªm“$i 3ÒÈAA䵎B}±Ö(ÔœKd"7mx{Ósm·ª "cdÖÓOPȉ鋆dO¦kùûM‡Ã‡&Ͱ\l´|>ßÉ"'ךgÈ:¶/ø6Ttý¥8ñ¶ï÷¿ë¬0/5giò”bE€ãª&»ÖnÍÑå™Úl!ÅRù“¡„Ò¦íooYÛ~G"¦ìad&çþþDSÓ.ÿ¶ªYÖÕF«hNe+ˆœP:Õ4Ko*Žì ¼ U!çdàèǯ8'™–YU8G‡ËÁ÷{_ÙöbÇíjªŒÌÃDcb¢±æêæØœúót&±8–)…âÖ”ÚŽì l­<ß²ÒdÓXRÙò<ÇM6NÕèø¢–½Á·UΪÛ÷ûßɯ4¯0;”ã8Òeÿ;=/íxŽ&cC’±‚‰ÆÃhÓTÍÿBÑæ\åz°ú‚ì›+ç:¾P<ÕvE_{¸9â—se@Ø/µ·5„ö•Ô˜Zš¬T¶<ÏñÅ“MÓ)`o?ÞLdª¸¬^Šo ?ÞS2Ív™¨ý, k$BÉáMý¯ôrÇÝL0Æ&^à ÝZ²¶j^ö¥:£hµ¼V£ô–]Aéy¶Õ=GCGB¾Ä¨Ð xGÛ…÷תãKjM3yËnomVrô&š-9Úé¹¥¦”‘®j%"5nñ¾¾íÅöÛ™`Œ=L4&¶<ý¢‹n)}h('£V/‹¦X—õ´„›B¾x#Ô GKç‘pcqi¡%+õt¬ rBÑdÓy‚Èe·¾¯8K!ÇÂR¸zžãNÅ³É ‘·z_ÿ襎{ØŠÙñ „kŠõÚŠ™Y—$;®5¦’:ëŠÞÖpk°?~@M™þþDSëþÐî²i¦åæ,-•­ rBñdÓ ^äò›ë¯AA˜äM¸ê¬×˜lÚ”‚” B(9øAßß·ÿ Æx‚ma0HÄdEg§Ñ®Í^ñ•ŠÇœ“Ì׫-·ëX䃧pôºÞ¶ˆGÉVÔòâ‚«ónÎué/R²GäÁh@êQÛŽÓÙ¿±çÅí/wÞÆ$ã &ˆž£¡ñ°¤(‹ÆvñW+˯4ß ¶l¯'öñS?h¾Ös4ܪd«ÕóúªÙVÅ¥ñŽã†74ÙÿNÏK¯s¯‘âdØÑ°ŒÑ‰Æ"’Žly¡ã?¥Q ï6Ú4ŽK¿6éù•æÀ©†öuĶ=ÿ“–›;C)÷QåyŽS“ŽPkì«Æ©¦î™Èû6ö¼¸ýïw3ÁŸ0ŸÆÄ‚öwF¶Ç£„ÏŸdž£A)jy]ñTÛ²¾¶ð± 7~*´BƒR{Gcø`a•q¾-gè,äñ(‰½û¼ç÷ƒ½‰#©ÊÊ)1^4íâ¼»”6~>,©áþW>~¥“õ0Æ1L4&¤·5´%–hA•ež áµ©Œ5zÁP4źÜ뎸ý}±ýP9«Òv0Tïª6.°åœš/C–ˆ¼÷=ï†mëz’{ñgœuyÁOsKMµj.JJÄÁ÷z_øøîû±ôWô2ÎL4&&r_[x[4(É…5օ»”Ì'±¤÷X¨Eí¬JÈ'µ5ï l5giª²œÚ|"ƒD‚RhÛº¾¿­ÿ³û~¥•·®Zëç¯.x ÕÖ‹' „’ïô¾ðñ«îQ³Kcla«\'6â¤9Y_[üå²_ªÙ¶1Lø7ü±å÷¡ÀSéT¢3𕋘”ZÔìgbËÓÍYõÿªÖYr”3—ƒ‚Ö¯ïzîãuž5²DT­¨eŒ-¬§1±!>Ot§¿7æ+™f[¡äœµ‚®l†ýâÞÖ'ЯW[‰,Qo4$·™*¥„Á"V¬øê¤W² ôÅJ+])¡tÏ]Oïüg×ט`L˜hL|ˆÏÝH„\5–‹1uCÐðÚÒéöå}m¡ŽO†*Ëx•U ¿`é]åkóÊLÕJÛIÊ‘ömìyaçkž52Ë&>¡`¢qv@ú;"ÛÉH~¥ù¥Eb¢†×–Lµ.ðD{ý=±½È€p8\†…Kï,&¯ÌT­ÔÃâ$¾oCϳ»^óÜ/Kê’ü0ÆL4ÎHgdG4 Åó+Íó…C+è\µ–‹ú;#î@_lD=‡Ë°pÑm¥O¨ÙïUJľ=ÿ»ûuÏ7¥aÉ€' L4Î.HGdGЛðÕZ+Åqht‚¾hŠué€'Ú;Ø«Ç0öQu-¹£ì/jC–ˆ´ïížgw¿îù:Œ‰ ³2àŽì ôÇü%Ól+9G5:A_2Ͷ¬¿=ÜãïíJ§"{¾þ‚‹¿Z±Öá2–ª±ßóz×S»_ïz@–ˆ¢C•1~a¢qvB|žèÎw´»t†}¥’p^[v~Ö%!oœ x¢; œ¡‹ÏŸd¾zÅšŠç¬9:§’ƒJv½æyr÷]÷™†Ó¾Ƹ‚‰ÆÙ ñuEwû<ÑW­u±¨M9*œX<ͶØYnº$”ÂáÁD‘OuRòŸ„‡ã 0Xç#§äb˜³ò4óF&"! ¸÷c°çu$bˆês÷!»dvJ)AóöB–ºÒj8-J§=“£<¥YhÀƒ¶½ß¥rzå/˜P>ãGÐYTl]H)º›×£¿ã¯Ãª 4ºTÌú­nØeœL<@Ç.ú×CŠuAéþÙó¯EAõjp|ÊÃ&ämEÛþÿ%¾¤6–ìÅ(ªûRÆÚöu¢mï£ Ä›ÔÆš»®šÛÀ Ÿ¥Ìôu†ûÐÏ¡ö™ç…|TÌzZãg»ÞI‘mA–<Ãnÿ ²‹o…sÒò—sO:þäÔ}/´Æ:Lšó\“/Qá¥çÑ ‡·<„¾ÖÇAi\Õyù•_FnÙœ”6„ûÛà>üpZm2gÍGÉyÿ ÁªhÛ¾ÿ‡ òðDÃh»å³¾¡ÚÞäX:"Ñ0Ú£xêW‡}þP”ÍB¾NùèÇèk},¥mVÁpÕÜžÑúO&Ðw îÆç‘ˆ&¼ŠÛહ3cu:á>ü<â‘䢑]t\µwò›ÑzžÆGA‰ºç]«/GaÍÐêO}&Û¼ŠÐÀºô~U÷ ¿rшË9AnI:þä3eÖ™ê0cåË(¬^™‘ ôf;¦­ø\µß8MFÊžç᪹oLë¹¥é9&‡Ýû\¬¹C#„`°k² “‹Š5w>4z'â‘ÌQ:^‚¯ë/CÓèþ£} ìΪa•Ý{ìô{$éñó.y¹eW¥,£þ­  »‡UÿÙÆð¥d&†Hc·ëVaÍÕļÏà´pÕÞ ^T/ŒÇÎMã[–|6' ¢qÛ÷1Ø}4©Éá‚Árþ0êNÒCH)v [f÷/ÉeÌž¤óB%ëbRôµ¿ "'Ÿ–ì9Q'èɘ²ª`/¸ýíÏœò»Á<¹¥Ã RKÿ¦‰º ØœÉ{R´¡mðum‚ÍY1¤ ÏóÈ+¿ ƒ=¯"(ZÆÙÅèi¨âºû!jË1àÙ€h`?âÑnP´Ö­õ£ÕÆÔˆ¢Õ·£¿c-pRäivÉa´9ÏHl¹3¡7å'=>àÙ "÷Âß»D¾¼0ôßÝæœAㄜyñ Æÿ—k¼·qêEÔUŽÊ¹÷¸!Ÿ¡Cpo×ýbáýãþ‘xdG„£pLö ¼5(¨¼öŒÕos®†¨ÕyŒR oç›oçû R4i|‰ÙQ ‹c |Ý™ñÿåïí;³œ‘QÇEƒÈ!ŠS§'0Ù `² ¯|)(ý‚^7ü½;àó|ˆ¾öõHÄôÌu±µœ“nÃÑߨò—Ã’]}Fêæ83œ‹“‡ýùŽwñCºaͺm¢VsÎ¥ðuoÌ`û”nL¿ö£S·ÑšQ[JÕûÛd©SÿÎècʪA^ù½ªã4äD¾®—@äOg°Ž‹F ï¢¡>-C8à8–l,Ù.¸j®>^^;zZÖÁÓø¢Á8côâ)_À±=?‘zPd°×f´O†¿· §_Çià(Z £½(cuôæ,,¹}¿j{J)ömøWô´üj[¥®£‰ÝY»ó—ióÑË]ô½~â¿Ç_¬HpüÝ»`´df±š%»fÇ×P:ý´ìþ#Ú÷Dޏ\_×~èͅЛŸ;&hMpM¾ƒ½ÛasNòüXØ‹ð`7² jSÔ’Þ—/»xD­!éñÁÞ ¤ÿÓÿ‡}›@¤{’ŠšÞT “m6B¾Miµ#“/ü&ªçÝ7ä1NÐAµ«k¢ÂqMòûx&Û1Þà¸SžÕO†#4Š–]?Ä€g_+â j¨˜u/\µÿLLïv|Ýͯ yŒçy¸jïĤ™ÿ Q7´ÏÀsähßÿÅËR!cIDATÌdž‡kUÊÝßñÞ)ÿ÷uïE<Å2oÁ[þÂL5 Zˆ:ËÿÔF"æV8¶:˜OãdÆ¿ê¤9äÛŠ}®AãÖGèo†œ¡Ø AÔÂUs tÆ‘û(¢óÐÓ ÒаÙQŒì’ÙC¾ÄR"†î¦gUçöPƒFW {Áä¤Ç£!¾m§üB4Ø‘ôžç‘[r%ÆË¬¥!ß>`ÔDƒ1ž!„œžäÔ.r<ÒŒ¶}߆ûðïaÏ¿¶¼K‘[v!´Æ\ˆZ³jGéé­…09* ~ë?!Ø ¯»9%³TŸC)Å€ûc„|» Ï`Uvñ¥)×ìÄÃ>p¼zó©k_ü}Ç`ÏŸ‘ô<{ADí$Hñ¦Œµu¸H‰<Ïu3Ò†Þö‘ˆ‡UŸèÛ5Š-RÇhO}ÇÐqði¢ÎgFä(Âþ'ÿ4ô¸ZŠ· ¯­}mÏáèN#ô–é°çÏ…-ïRä”΃Fg/hT_ /j ÑÔ”.”ÄÑuä8ŠÎOKÄ:þü½ÿåÚFçË*IgÿÉLŸÆqѵ.mÓÀqC¹ ‘ò~BAi¡õ ¼ Ž×CÐd¡xÊ÷Q>ëž3’³(ñ¡óÐZTÌZ£lK)´îýo9ùlˆÖ`«ö>pÜ™HA8î»ãg” 0 5Xs«`Í^v§ÑB–ºÑyè¥!EƒR wÃ_AIfSÅñ¼Ù%©³y;ö¡þ­9Š‹¥Šê~„š ‡Î:€rË®EëÞá‹F2ü½ëÑ{ìÖ$ß§%·l¬¹—ÀßûfÆëgLXN¬ríõšˆÜ¯l4 ¼/ ì¿Fë©‹ÆÂ¾ô[›ñúLY“aÊJÝ:ØûŽª°ëÎw$ °\ˆöY ò@:ÍTŒöƒ¿BnÙ¢¤½ QcDéôÿÀ¾L4Î$•s€ð ºÜ¶ÑÀt4|TåªU‡kª¼ "§ññïEÇÁï ïD}Ð[rÒ.H áA‚ý£“¥'jÆ¡¿sÖÔS~ú"yêéX²—¥RÈR>÷6¨évÇ£ öÀhK¾JÖš3 zsƒÃTÝÝ`ÿfô¶}€Âê¡{Ç{: `v,GЛ¹åú§Õ2JåN\rKg˜©Ê–ž¦Ÿ@Š© ƒÅâºám»‹\ð™hDü»ÐsìM”L»uX…¥‚Oãß 1|ÄмOÃÛ9:ÅŸŠˆœ²KRZÈñü½[U•&ÅáëÞ £-ùMÔ™l°d/CxpwZ-U¥Qtøä/‚Ö0toƒã”÷mx“ê¯ãÌÁó“]þøèøì ¥q´ÖÿÞŽÔõt!„ÀÛñ:ÒH†2¶$¿~­¡F1¹¯»²Ô®®&Á`—r˜¹³r%0JÓŸþMèkMÞ‹à8¹åKa´±ìñÉw$æ…aïÆ/¢¤î;(šzÃgä J@¥Ú<‡öý?…W—žžÈqHRòU¨”Œ|¥%%RÊ:Re³9ë j­)ÏïïH' Š¢¯m#$é)­´ú"ˆšBHñ–Ï“¥PêëRúW( ¡mÿ¯‘Sº¼&¹0Tß„¦>Dº3)²”úž‚Ð>øCý ˆ<º½"B>]Dá>œ¥2ˆ”P¼© !I?ôDެìÓø$¼ahQ4“`Êšq˜II¤xAïv9½\—ë…0X†Þ?…RŠ`ÿ&$bê¾âɵE°æ&£NÄüŸd)úüË!j aÉ^ŽzÖ‰þ ÅÓ¹nöük“&)o#0ÄFJ`__. .. toctree:: :maxdepth: 2 install tutorial api faqs benchmarks rfc-compliance adoptions Indices and tables ================== * :ref:`genindex` * :ref:`search` pyftpdlib-release-1.5.4/docs/install.rst000066400000000000000000000015001327314554200202660ustar00rootroot00000000000000Install ======= By using pip: .. code-block:: sh $ pip install pyftpdlib From sources: .. code-block:: sh $ git clone git@github.com:giampaolo/pyftpdlib.git $ cd pyftpdlib $ python setup.py install You might want to run tests to make sure pyftpdlib works: .. code-block:: sh $ make test $ make test-contrib Additional dependencies ----------------------- `PyOpenSSL `__, to support `FTPS `__: .. code-block:: sh $ pip install PyOpenSSL `pysendfile `__, if you're on UNIX, in order to `speedup uploads `__ (from server to client): .. code-block:: sh $ pip install pysendfile pyftpdlib-release-1.5.4/docs/make.bat000066400000000000000000000150631327314554200175040ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pyftpdlib.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pyftpdlib.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end pyftpdlib-release-1.5.4/docs/rfc-compliance.rst000066400000000000000000001522201327314554200215100ustar00rootroot00000000000000======================== pyftpdlib 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. RFC-959 - File Transfer Protocol ================================ The base specification of the current File Transfer Protocol. - Issued: October 1985 - Status: STANDARD - Obsoletes: `RFC-765 `__ - Updated by: `RFC-1123 `__, `RFC-2228 `__, `RFC-2640 `__, `RFC-2773 `__ - `Link `__ +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | *Command* | *Implemented* | *Milestone* | *Description* | *Notes* | +===========+===============+=============+==================================================+================================================================================================================================================================================================================+ | ABOR | YES | 0.1.0 | Abort data transfer. | | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | ACCT | NO | --- | Specify account information. | It will never be implemented (useless). | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | ALLO | YES | 0.1.0 | Ask for server to allocate enough storage space. | Treated as a NOOP (no operation). | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | APPE | YES | 0.1.0 | Append data to an existing file. | | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | CDUP | YES | 0.1.0 | Go to parent directory. | | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | CWD | YES | 0.1.0 | Change current working directory. | | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | DELE | YES | 0.1.0 | Delete file. | | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | HELP | YES | 0.1.0 | Show help. | Accept also arguments. | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | LIST | YES | 0.1.0 | List files. | Accept also bad arguments like "-ls", "-la", ... | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | MKD | YES | 0.1.0 | Create directory. | | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | MODE | YES | 0.1.0 | Set data transfer mode. | "STREAM" mode is supported, "Block" and "Compressed" aren't. | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | NLST | YES | 0.1.0 | List files in a compact form. | Globbing of wildcards is not supported (for example, ``NLST *.txt`` will not work) | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | NOOP | YES | 0.1.0 | NOOP (no operation), just do nothing. | | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | PASS | YES | 0.1.0 | Set user password. | | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | PASV | YES | 0.1.0 | Set server in passive connection mode. | | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | PORT | YES | 0.1.0 | Set server in active connection mode. | | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | PWD | YES | 0.1.0 | Get current working directory. | | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | QUIT | YES | 0.1.0 | Quit session. | If file transfer is in progress, the connection will remain open until it is finished. | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | REIN | YES | 0.1.0 | Reinitialize user's current session. | | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | REST | YES | 0.1.0 | Restart file position. | | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | RETR | YES | 0.1.0 | Retrieve a file (client's download). | | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | RMD | YES | 0.1.0 | Remove directory. | | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | RNFR | YES | 0.1.0 | File renaming (source) | | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | RNTO | YES | 0.1.0 | File renaming (destination) | | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | SITE | YES | 0.5.1 | Site specific server services. | No SITE commands aside from "SITE HELP" are implemented by default. The user willing to add support for a specific SITE command has to define a new ``ftp_SITE_CMD`` method in the ``FTPHandler`` subclass. | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | SMNT | NO | --- | Mount file-system structure. | Will never be implemented (too much system-dependent and almost never used). | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | STAT | YES | 0.1.0 | Server's status information / File LIST | | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | STOR | YES | 0.1.0 | Store a file (client's upload). | | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | STOU | YES | 0.1.0 | Store a file with a unique name. | | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | STRU | YES | 0.1.0 | Set file structure. | Supports only File type structure by doing a NOOP (no operation). Other structure types (Record and Page) are not implemented. | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | SYST | YES | 0.1.0 | Get system type. | Always return "UNIX Type: L8" because of the LIST output provided. | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | TYPE | YES | 0.1.0 | Set current type (Binary/ASCII). | Accept only Binary and ASII TYPEs. Other TYPEs such as EBCDIC are obsoleted, system-dependent and thus not implemented. | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | USER | YES | 0.1.0 | Set user. | A new USER command could be entered at any point in order to change the access control flushing any user, password, and account information already supplied and beginning the login sequence again. | +-----------+---------------+-------------+--------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ RFC-1123 - Requirements for Internet Hosts ========================================== Extends and clarifies some aspects of `RFC-959 `__. Introduces new response codes 554 and 555. - Issued: October 1989 - Status: STANDARD - `Link `__ +--------------------------------------+---------------+-------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------+ | *Feature* | *Implemented* | *Milestone* | *Description* | *Notes* | +======================================+===============+=============+====================================================================================================================================================================================================================================================+=======================================================================================+ | TYPE L 8 as synonym of TYPE I | YES | 0.2.0 | TYPE L 8 command should be treated as synonym of TYPE I ("IMAGE" or binary type). | | +--------------------------------------+---------------+-------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------+ | PASV is per-transfer | YES | 0.1.0 | PASV must be used for a unique transfer. | If PASV is issued twice data-channel is restarted. | +--------------------------------------+---------------+-------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------+ | Implied type for LIST and NLST | YES | 0.1.0 | The data returned by a LIST or NLST command SHOULD use an implied TYPE AN. | | +--------------------------------------+---------------+-------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------+ | STOU format output | YES | 0.2.0 | Defined the exact format output which STOU response must respect ("125/150 FILE filename"). | | +--------------------------------------+---------------+-------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------+ | 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. | | +--------------------------------------+---------------+-------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------+ | Idle timeout | YES | 0.5.0 | A Server-FTP process SHOULD have a configurable idle timeout of 5 minutes, which will terminate the process and close the control connection if the server is inactive (i.e., no command or data transfer in progress) for a long period of time. | | +--------------------------------------+---------------+-------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------+ | Concurrency of data and control | YES | 0.1.0 | Server-FTP should be able to process STAT or ABOR while a data transfer is in progress | Feature granted natively for ALL commands since we're in an asynchronous environment. | +--------------------------------------+---------------+-------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------+ | 554 response on wrong REST | YES | 0.2.0 | Return a 554 reply may for a command that follows a REST command. The reply indicates that the existing file at the Server-FTP cannot be repositioned as specified in the REST. | | +--------------------------------------+---------------+-------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------+ RFC-2228 - FTP Security Extensions ================================== 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. - Issued: October 1997 - Status: PROPOSED STANDARD - Updates: `RFC-959 `__ - `Link `__ +-----------+---------------+-------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | *Command* | *Implemented* | *Milestone* | *Description* | *Notes* | +===========+===============+=============+====================================+====================================================================================================================================================================================================================================+ | AUTH | NO | --- | Authentication/Security Mechanism. | Implemented as `demo script `__ by following the `RFC=4217 `__ guide line. | +-----------+---------------+-------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | CCC | NO | --- | Clear Command Channel. | | +-----------+---------------+-------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | CONF | NO | --- | Confidentiality 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 `__. | +-----------+---------------+-------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | PBSZ | NO | --- | Protection Buffer Size. | Implemented as `demo script `__ by following the `RFC-4217 `__ guide line as 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. | +-----------+---------------+-------------+------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ RFC-2389 - Feature negotiation mechanism for the File Transfer Protocol ======================================================================= Introduces the new FEAT and OPTS commands. - Issued: August 1998 - Status: PROPOSED STANDARD - `Link `__ +-----------+---------------+-------------+-----------------------------------------------------------------------------------------+---------------------------------------------------------+ | *Command* | *Implemented* | *Milestone* | *Description* | *Notes* | +===========+===============+=============+=========================================================================================+=========================================================+ | 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. | +-----------+---------------+-------------+-----------------------------------------------------------------------------------------+---------------------------------------------------------+ 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. - Issued: September 1998 - Status: PROPOSED STANDARD - `Link `__ +-----------+---------------+-------------+-----------------------------------------------+---------+ | *Command* | *Implemented* | *Milestone* | *Description* | *Notes* | +===========+===============+=============+===============================================+=========+ | EPRT | YES | 0.4.0 | Set active data connection over IPv4 or IPv6 | | +-----------+---------------+-------------+-----------------------------------------------+---------+ | EPSV | YES | 0.4.0 | Set passive data connection over IPv4 or IPv6 | | +-----------+---------------+-------------+-----------------------------------------------+---------+ 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". - Issued: May 1999 - Status: INFORMATIONAL - `Link `__ +--------------------------------------------+---------------+-------------+-------------------------------------------------------------------------------------------------------------------------------------------+---------------+ | *Feature* | *Implemented* | *Milestone* | *Description* | *Notes* | +============================================+===============+=============+===========================================================================================================================================+===============+ | FTP bounce protection | YES | 0.2.0 | Reject PORT if IP address specified in it does not match client IP address. Drop the incoming (PASV) data connection for the same reason. | Configurable. | +--------------------------------------------+---------------+-------------+-------------------------------------------------------------------------------------------------------------------------------------------+---------------+ | Restrict PASV/PORT to non privileged ports | YES | 0.2.0 | Reject connections to privileged ports. | Configurable. | +--------------------------------------------+---------------+-------------+-------------------------------------------------------------------------------------------------------------------------------------------+---------------+ | Brute force protection (1) | YES | 0.1.0 | Disconnect client after a certain number (3 or 5) of wrong authentications. | Configurable. | +--------------------------------------------+---------------+-------------+-------------------------------------------------------------------------------------------------------------------------------------------+---------------+ | Brute force protection (2) | YES | 0.5.0 | Impose a 5 second delay before replying to an invalid "PASS" command to diminish the efficiency of a brute force attack. | | +--------------------------------------------+---------------+-------------+-------------------------------------------------------------------------------------------------------------------------------------------+---------------+ | Per-source-IP limit | YES | 0.2.0 | Limit the total number of per-ip control connections to avoid parallel brute-force attack attempts. | Configurable. | +--------------------------------------------+---------------+-------------+-------------------------------------------------------------------------------------------------------------------------------------------+---------------+ | Do not reject wrong usernames | YES | 0.1.0 | Always return 331 to the USER command to prevent client from determining valid usernames on the server. | | +--------------------------------------------+---------------+-------------+-------------------------------------------------------------------------------------------------------------------------------------------+---------------+ | Port stealing protection | YES | 0.1.1 | Use random-assigned local ports for data connections. | | +--------------------------------------------+---------------+-------------+-------------------------------------------------------------------------------------------------------------------------------------------+---------------+ 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. - Issued: July 1999 - Status: PROPOSED STANDARD - Updates: `RFC-959 `__ - `Link `__ +----------------------+---------------+-------------+-------------------------------------------------------------------------------------------------------------------------------+---------+ | *Feature* | *Implemented* | *Milestone* | *Description* | *Notes* | +======================+===============+=============+===============================================================================================================================+=========+ | LANG command | NO | --- | Set current response's language. | | +----------------------+---------------+-------------+-------------------------------------------------------------------------------------------------------------------------------+---------+ | Support for UNICODE | YES | 1.0.0 | For support of global compatibility it is rencommended that clients and servers use UTF-8 encoding when exchanging pathnames. | | +----------------------+---------------+-------------+-------------------------------------------------------------------------------------------------------------------------------+---------+ RFC-3659 - Extensions to FTP ============================ Four new commands are added: "SIZE", "MDTM", "MLST", and "MLSD". The existing command "REST" is modified. - Issued: March 2007 - Status: PROPOSED STANDARD - Updates: `RFC-959 `__ - `Link `__ +------------------------------------+---------------+-------------+------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------+ | *Feature* | *Implemented* | *Milestone* | *Description* | *Notes* | +====================================+===============+=============+==========================================================================================================================================+=====================================================================================================================+ | MDTM command | YES | 0.1.0 | Get file's last modification time | | +------------------------------------+---------------+-------------+------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------+ | MLSD command | YES | 0.3.0 | Get directory list in a standardized form. | | +------------------------------------+---------------+-------------+------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------+ | MLST command | YES | 0.3.0 | Get file information in a standardized form. | | +------------------------------------+---------------+-------------+------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------+ | SIZE command | YES | 0.1.0 | Get file size. | In case of ASCII TYPE it does not perform the ASCII conversion to avoid DoS conditions (see FAQs for more details). | +------------------------------------+---------------+-------------+------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------+ | TVSF mechanism | YES | 0.1.0 | Provide a file system naming conventions modeled loosely upon those of the Unix file system supporting relative and absolute path names. | | +------------------------------------+---------------+-------------+------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------+ | Minimum required set of MLST facts | YES | 0.3.0 | If conceivably possible, support at least the type, perm, size, unique, and modify MLSX command facts. | | +------------------------------------+---------------+-------------+------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------+ | GMT should be used for timestamps | YES | 0.6.0 | All times reported by MDTM, LIST, MLSD and MLST commands must be in GMT times | Possibility to change time display between GMT and local time provided as "use_gmt_times" attribute | +------------------------------------+---------------+-------------+------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------+ 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. - Issued: October 2005 - Status: STANDARD - Updates: `RFC-959 `__, `RFC-2246 `__, `RFC-2228 `__ - `Link `__ +-----------+---------------+-------------+------------------------------------+---------------------------------------------+ | *Command* | *Implemented* | *Milestone* | *Description* | *Notes* | +===========+===============+=============+====================================+=============================================+ | AUTH | YES | --- | Authentication/Security Mechanism. | | +-----------+---------------+-------------+------------------------------------+---------------------------------------------+ | CCC | NO | --- | Clear Command Channel. | | +-----------+---------------+-------------+------------------------------------+---------------------------------------------+ | 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. | +-----------+---------------+-------------+------------------------------------+---------------------------------------------+ Unofficial commands =================== 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. | | +------------+---------------+-------------+-------------------+---------+ pyftpdlib-release-1.5.4/docs/tutorial.rst000066400000000000000000000440301327314554200204700ustar00rootroot00000000000000======== 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. 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 `__ .. code-block:: python import os from pyftpdlib.authorizers import DummyAuthorizer from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer def main(): # Instantiate a dummy authorizer for managing 'virtual' users authorizer = DummyAuthorizer() # Define a new user having full r/w permissions and a read-only # anonymous user authorizer.add_user('user', '12345', '.', perm='elradfmwMT') authorizer.add_anonymous(os.getcwd()) # Instantiate FTP handler class handler = FTPHandler handler.authorizer = authorizer # Define a customized banner (string returned when client connects) handler.banner = "pyftpdlib based ftpd ready." # Specify a masquerade address and the range of ports to use for # passive connections. Decomment in case you're behind a NAT. #handler.masquerade_address = '151.25.42.11' #handler.passive_ports = range(60000, 65535) # Instantiate FTP server class and listen on 0.0.0.0:2121 address = ('', 2121) server = FTPServer(address, handler) # set a limit for connections server.max_cons = 256 server.max_cons_per_ip = 5 # start ftp server server.serve_forever() if __name__ == '__main__': main() Logging management ================== pyftpdlib uses the `logging `__ 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: .. code-block:: python import logging from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer from pyftpdlib.authorizers import DummyAuthorizer authorizer = DummyAuthorizer() authorizer.add_user('user', '12345', '.', perm='elradfmwMT') handler = FTPHandler handler.authorizer = authorizer logging.basicConfig(filename='/var/log/pyftpd.log', level=logging.INFO) server = FTPServer(('', 2121), handler) server.serve_forever() 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: .. code-block:: python logging.basicConfig(level=logging.DEBUG) To enable DEBUG logging from command line use: .. code-block:: bash python -m pyftpdlib -D DEBUG logs look like this: :: [I 2017-11-07 12:03:44] >>> starting FTP server on 0.0.0.0:2121, pid=22991 <<< [I 2017-11-07 12:03:44] concurrency model: async [I 2017-11-07 12:03:44] masquerade (NAT) address: None [I 2017-11-07 12:03:44] passive ports: None [D 2017-11-07 12:03:44] poller: 'pyftpdlib.ioloop.Epoll' [D 2017-11-07 12:03:44] authorizer: 'pyftpdlib.authorizers.DummyAuthorizer' [D 2017-11-07 12:03:44] use sendfile(2): True [D 2017-11-07 12:03:44] handler: 'pyftpdlib.handlers.FTPHandler' [D 2017-11-07 12:03:44] max connections: 512 [D 2017-11-07 12:03:44] max connections per ip: unlimited [D 2017-11-07 12:03:44] timeout: 300 [D 2017-11-07 12:03:44] banner: 'pyftpdlib 1.5.4 ready.' [D 2017-11-07 12:03:44] max login attempts: 3 [I 2017-11-07 12:03:44] 127.0.0.1:37303-[] FTP session opened (connect) [D 2017-11-07 12:03:44] 127.0.0.1:37303-[] -> 220 pyftpdlib 1.0.0 ready. [D 2017-11-07 12:03:44] 127.0.0.1:37303-[] <- USER user [D 2017-11-07 12:03:44] 127.0.0.1:37303-[] -> 331 Username ok, send password. [D 2017-11-07 12:03:44] 127.0.0.1:37303-[user] <- PASS ****** [D 2017-11-07 12:03:44] 127.0.0.1:37303-[user] -> 230 Login successful. [I 2017-11-07 12:03:44] 127.0.0.1:37303-[user] USER 'user' logged in. [D 2017-11-07 12:03:44] 127.0.0.1:37303-[user] <- TYPE I [D 2017-11-07 12:03:44] 127.0.0.1:37303-[user] -> 200 Type set to: Binary. [D 2017-11-07 12:03:44] 127.0.0.1:37303-[user] <- PASV [D 2017-11-07 12:03:44] 127.0.0.1:37303-[user] -> 227 Entering passive mode (127,0,0,1,233,208). [D 2017-11-07 12:03:44] 127.0.0.1:37303-[user] <- RETR tmp-pyftpdlib [D 2017-11-07 12:03:44] 127.0.0.1:37303-[user] -> 125 Data connection already open. Transfer starting. [D 2017-11-07 12:03:44] 127.0.0.1:37303-[user] -> 226 Transfer complete. [I 2017-11-07 12:03:44] 127.0.0.1:37303-[user] RETR /home/giampaolo/IMG29312.JPG completed=1 bytes=1205012 seconds=0.003 [D 2017-11-07 12:03:44] 127.0.0.1:37303-[user] <- QUIT [D 2017-11-07 12:03:44] 127.0.0.1:37303-[user] -> 221 Goodbye. [I 2017-11-07 12:03:44] 127.0.0.1:37303-[user] FTP session closed (disconnect). Changing log line prefix ^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: python handler = FTPHandler handler.log_prefix = 'XXX [%(username)s]@%(remote_ip)s' server = FTPServer(('localhost', 2121), handler) server.serve_forever() Logs will now look like this: :: [I 13-02-01 19:12:26] XXX []@127.0.0.1 FTP session opened (connect) [I 13-02-01 19:12:26] XXX [user]@127.0.0.1 USER 'user' logged in. 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 storage passwords as one-way hashes by using md5 algorithm. `source code `__ .. code-block:: python import os import sys from hashlib import md5 from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer from pyftpdlib.authorizers import DummyAuthorizer, AuthenticationFailed class DummyMD5Authorizer(DummyAuthorizer): def validate_authentication(self, username, password, handler): if sys.version_info >= (3, 0): password = md5(password.encode('latin1')) hash = md5(password).hexdigest() try: if self.user_table[username]['pwd'] != hash: raise KeyError except KeyError: raise AuthenticationFailed def main(): # get a hash digest from a clear-text password hash = md5('12345').hexdigest() authorizer = DummyMD5Authorizer() authorizer.add_user('user', hash, os.getcwd(), perm='elradfmwMT') authorizer.add_anonymous(os.getcwd()) handler = FTPHandler handler.authorizer = authorizer server = FTPServer(('', 2121), handler) server.serve_forever() if __name__ == "__main__": main() 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. .. code-block:: python from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer from pyftpdlib.authorizers import UnixAuthorizer from pyftpdlib.filesystems import UnixFilesystem def main(): authorizer = UnixAuthorizer(rejected_users=["root"], require_valid_shell=True) handler = FTPHandler handler.authorizer = authorizer handler.abstracted_fs = UnixFilesystem server = FTPServer(('', 21), handler) server.serve_forever() if __name__ == "__main__": main() 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 `__ .. code-block:: python from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer from pyftpdlib.authorizers import WindowsAuthorizer def main(): authorizer = WindowsAuthorizer() # Use Guest user with empty password to handle anonymous sessions. # Guest user must be enabled first, empty password set and profile # directory specified. #authorizer = WindowsAuthorizer(anonymous_user="Guest", anonymous_password="") handler = FTPHandler handler.authorizer = authorizer server = FTPServer(('', 2121), handler) server.serve_forever() if __name__ == "__main__": main() Changing the concurrency model ============================== By nature pyftpdlib is asynchronous. This 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 supports 2 new classes which changes the default concurrency model by introducing multiple threads or processes. In technical terms this 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: Thread-based example: .. code-block:: python from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import ThreadedFTPServer # <- from pyftpdlib.authorizers import DummyAuthorizer def main(): authorizer = DummyAuthorizer() authorizer.add_user('user', '12345', '.') handler = FTPHandler handler.authorizer = authorizer server = ThreadedFTPServer(('', 2121), handler) server.serve_forever() if __name__ == "__main__": main() Multiple process example: .. code-block:: python from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import MultiprocessFTPServer # <- from pyftpdlib.authorizers import DummyAuthorizer def main(): authorizer = DummyAuthorizer() authorizer.add_user('user', '12345', '.') handler = FTPHandler handler.authorizer = authorizer server = MultiprocessFTPServer(('', 2121), handler) server.serve_forever() 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 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() FTPS (FTP over TLS/SSL) server ============================== 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 `__ .. code-block:: python """ An RFC-4217 asynchronous FTPS server supporting both SSL and TLS. Requires PyOpenSSL module (http://pypi.python.org/pypi/pyOpenSSL). """ from pyftpdlib.servers import FTPServer from pyftpdlib.authorizers import DummyAuthorizer from pyftpdlib.handlers import TLS_FTPHandler def main(): authorizer = DummyAuthorizer() authorizer.add_user('user', '12345', '.', perm='elradfmwMT') authorizer.add_anonymous('.') handler = TLS_FTPHandler handler.certfile = 'keycert.pem' handler.authorizer = authorizer # requires SSL for both control and data channel #handler.tls_control_required = True #handler.tls_data_required = True server = FTPServer(('', 21), handler) server.serve_forever() if __name__ == '__main__': main() Event callbacks =============== A small example which shows how to use callback methods via `FTPHandler `__ subclassing: .. code-block:: python from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer from pyftpdlib.authorizers import DummyAuthorizer class MyHandler(FTPHandler): def on_connect(self): print "%s:%s connected" % (self.remote_ip, self.remote_port) def on_disconnect(self): # do something when client disconnects pass def on_login(self, username): # do something when user login pass def on_logout(self, username): # do something when user logs out pass def on_file_sent(self, file): # do something when a file has been sent pass def on_file_received(self, file): # do something when a file has been received pass def on_incomplete_file_sent(self, file): # do something when a file is partially sent pass def on_incomplete_file_received(self, file): # remove partially uploaded files import os os.remove(file) def main(): authorizer = DummyAuthorizer() authorizer.add_user('user', '12345', homedir='.', perm='elradfmwMT') authorizer.add_anonymous(homedir='.') handler = MyHandler handler.authorizer = authorizer server = FTPServer(('', 2121), 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. Anonymous FTPd sharing current directory: .. code-block:: sh $ python -m pyftpdlib [I 13-04-09 17:55:18] >>> starting FTP server on 0.0.0.0:2121, pid=6412 <<< [I 13-04-09 17:55:18] poller: [I 13-04-09 17:55:18] masquerade (NAT) address: None [I 13-04-09 17:55:18] passive ports: None [I 13-04-09 17:55:18] use sendfile(2): True Anonymous FTPd with write permission: .. code-block:: sh $ python -m pyftpdlib -w Set a different address/port and home directory: .. code-block:: sh $ python -m pyftpdlib -i localhost -p 8021 -d /home/someone See ``python -m pyftpdlib -h`` for a complete list of options. pyftpdlib-release-1.5.4/make.bat000066400000000000000000000060621327314554200165530ustar00rootroot00000000000000@echo off rem ========================================================================== rem Shortcuts for various tasks, emulating UNIX "make" on Windows. rem It is primarly intended as a shortcut for installing pyftpdlib and running rem tests (just run "make.bat test"). rem By default C:\Python27\python.exe is used. rem To use another Python version run: rem set PYTHON=C:\Python24\python.exe & make.bat test rem ========================================================================== if "%PYTHON%" == "" ( set PYTHON=C:\Python27\python.exe ) if "%TSCRIPT%" == "" ( set TSCRIPT=pyftpdlib\test\runner.py ) if "%1" == "help" ( :help echo Run `make ^` where ^ is one of: echo clean clean build files echo install compile and install echo uninstall uninstall echo test run tests echo setup-dev-env install all deps goto :eof ) if "%1" == "clean" ( :clean for /r %%R in (__pycache__) do if exist %%R (rmdir /S /Q %%R) for /r %%R in (*.pyc) do if exist %%R (del /s %%R) for /r %%R in (*.pyd) do if exist %%R (del /s %%R) for /r %%R in (*.orig) do if exist %%R (del /s %%R) for /r %%R in (*.bak) do if exist %%R (del /s %%R) for /r %%R in (*.rej) do if exist %%R (del /s %%R) if exist pyftpdlib.egg-info (rmdir /S /Q pyftpdlib.egg-info) if exist build (rmdir /S /Q build) if exist dist (rmdir /S /Q dist) goto :eof ) if "%1" == "install" ( :install if %PYTHON%==C:\Python24\python.exe ( %PYTHON% setup.py build -c mingw32 install ) else if %PYTHON%==C:\Python25\python.exe ( %PYTHON% setup.py build -c mingw32 install ) else ( %PYTHON% setup.py build install ) goto :eof ) if "%1" == "uninstall" ( :uninstall rmdir /S /Q %PYTHON%\Lib\site-packages\pyftpdlib* goto :eof ) if "%1" == "test" ( :test call :install %PYTHON% %TSCRIPT% goto :eof ) if "%1" == "setup-dev-env" ( :setup-env if not exist get-pip.py ( @echo ------------------------------------------------ @echo downloading pip installer @echo ------------------------------------------------ C:\python27\python.exe -c "import urllib2; r = urllib2.urlopen('https://bootstrap.pypa.io/get-pip.py'); open('get-pip.py', 'wb').write(r.read())" ) @echo ------------------------------------------------ @echo installing pip for %PYTHON% @echo ------------------------------------------------ %PYTHON% get-pip.py @echo ------------------------------------------------ @echo upgrade pip for %PYTHON% @echo ------------------------------------------------ %PYTHON% -m pip install pip --upgrade @echo ------------------------------------------------ @echo installing deps @echo ------------------------------------------------ rem mandatory / for unittests %PYTHON% -m pip install unittest2 ipaddress mock wmi pypiwin32 pyopenssl --upgrade goto :eof ) goto :help :error echo last command returned an error; exiting exit /b %errorlevel% pyftpdlib-release-1.5.4/pyftpdlib/000077500000000000000000000000001327314554200171375ustar00rootroot00000000000000pyftpdlib-release-1.5.4/pyftpdlib/__init__.py000066400000000000000000000054351327314554200212570ustar00rootroot00000000000000# Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. """ pyftpdlib: RFC-959 asynchronous FTP server. pyftpdlib implements a fully functioning asynchronous FTP server as defined in RFC-959. A hierarchy of classes outlined below implement the backend functionality for the FTPd: [pyftpdlib.ftpservers.FTPServer] accepts connections and dispatches them to a handler [pyftpdlib.handlers.FTPHandler] a class representing the server-protocol-interpreter (server-PI, see RFC-959). Each time a new connection occurs FTPServer will create a new FTPHandler instance to handle the current PI session. [pyftpdlib.handlers.ActiveDTP] [pyftpdlib.handlers.PassiveDTP] base classes for active/passive-DTP backends. [pyftpdlib.handlers.DTPHandler] this class handles processing of data transfer operations (server-DTP, see RFC-959). [pyftpdlib.authorizers.DummyAuthorizer] an "authorizer" is a class handling FTPd authentications and permissions. It is used inside FTPHandler class to verify user passwords, to get user's home directory and to get permissions when a filesystem read/write occurs. "DummyAuthorizer" is the base authorizer class providing a platform independent interface for managing virtual users. [pyftpdlib.filesystems.AbstractedFS] class used to interact with the file system, providing a high level, cross-platform interface compatible with both Windows and UNIX style filesystems. Usage example: >>> from pyftpdlib.authorizers import DummyAuthorizer >>> from pyftpdlib.handlers import FTPHandler >>> from pyftpdlib.servers import FTPServer >>> >>> authorizer = DummyAuthorizer() >>> authorizer.add_user("user", "12345", "/home/giampaolo", perm="elradfmwMT") >>> authorizer.add_anonymous("/home/nobody") >>> >>> handler = FTPHandler >>> handler.authorizer = authorizer >>> >>> server = FTPServer(("127.0.0.1", 21), handler) >>> server.serve_forever() [I 13-02-19 10:55:42] >>> starting FTP server on 127.0.0.1:21 <<< [I 13-02-19 10:55:42] poller: [I 13-02-19 10:55:42] masquerade (NAT) address: None [I 13-02-19 10:55:42] passive ports: None [I 13-02-19 10:55:42] use sendfile(2): True [I 13-02-19 10:55:45] 127.0.0.1:34178-[] FTP session opened (connect) [I 13-02-19 10:55:48] 127.0.0.1:34178-[user] USER 'user' logged in. [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). """ __ver__ = '1.5.4' __author__ = "Giampaolo Rodola' " __web__ = 'https://github.com/giampaolo/pyftpdlib/' pyftpdlib-release-1.5.4/pyftpdlib/__main__.py000066400000000000000000000114171327314554200212350ustar00rootroot00000000000000# Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. """ Start a stand alone anonymous FTP server from the command line as in: $ python -m pyftpdlib """ import logging import optparse import os import sys from . import __ver__ from ._compat import getcwdu from .authorizers import DummyAuthorizer from .handlers import FTPHandler from .log import config_logging from .servers import FTPServer class CustomizedOptionFormatter(optparse.IndentedHelpFormatter): """Formats options shown in help in a prettier way.""" def format_option(self, option): result = [] opts = self.option_strings[option] result.append(' %s\n' % opts) if option.help: help_text = ' %s\n\n' % self.expand_default(option) result.append(help_text) return ''.join(result) def main(): """Start a stand alone anonymous FTP server.""" usage = "python -m pyftpdlib [options]" parser = optparse.OptionParser(usage=usage, description=main.__doc__, formatter=CustomizedOptionFormatter()) parser.add_option('-i', '--interface', default=None, metavar="ADDRESS", help="specify the interface to run on (default all " "interfaces)") parser.add_option('-p', '--port', type="int", default=2121, metavar="PORT", help="specify port number to run on (default 2121)") parser.add_option('-w', '--write', action="store_true", default=False, help="grants write access for logged in user " "(default read-only)") parser.add_option('-d', '--directory', default=getcwdu(), metavar="FOLDER", help="specify the directory to share (default current " "directory)") parser.add_option('-n', '--nat-address', default=None, metavar="ADDRESS", help="the NAT address to use for passive connections") parser.add_option('-r', '--range', default=None, metavar="FROM-TO", help="the range of TCP ports to use for passive " "connections (e.g. -r 8000-9000)") parser.add_option('-D', '--debug', action='store_true', help="enable DEBUG logging evel") parser.add_option('-v', '--version', action='store_true', help="print pyftpdlib version and exit") parser.add_option('-V', '--verbose', action='store_true', help="activate a more verbose logging") parser.add_option('-u', '--username', type=str, default=None, help="specify username to login with (anonymous login " "will be disabled and password required " "if supplied)") parser.add_option('-P', '--password', type=str, default=None, help="specify a password to login with (username " "required to be useful)") options, args = parser.parse_args() if options.version: sys.exit("pyftpdlib %s" % __ver__) if options.debug: config_logging(level=logging.DEBUG) passive_ports = None if options.range: try: start, stop = options.range.split('-') start = int(start) stop = int(stop) except ValueError: parser.error('invalid argument passed to -r option') else: passive_ports = list(range(start, stop + 1)) # On recent Windows versions, if address is not specified and IPv6 # is installed the socket will listen on IPv6 by default; in this # case we force IPv4 instead. if os.name in ('nt', 'ce') and not options.interface: options.interface = '0.0.0.0' authorizer = DummyAuthorizer() perm = options.write and "elradfmwMT" or "elr" if options.username: if not options.password: parser.error( "if username (-u) is supplied, password ('-P') is required") authorizer.add_user(options.username, options.password, options.directory, perm=perm) else: authorizer.add_anonymous(options.directory, perm=perm) handler = FTPHandler handler.authorizer = authorizer handler.masquerade_address = options.nat_address handler.passive_ports = passive_ports ftpd = FTPServer((options.interface, options.port), FTPHandler) # On Windows specify a timeout for the underlying select() so # that the server can be interrupted with CTRL + C. try: ftpd.serve_forever(timeout=2 if os.name == 'nt' else None) finally: ftpd.close_all() if __name__ == '__main__': main() pyftpdlib-release-1.5.4/pyftpdlib/_compat.py000066400000000000000000000013471327314554200211400ustar00rootroot00000000000000#!/usr/bin/env python """ Compatibility module similar to six which helps maintaining a single code base working with python from 2.6 to 3.x. """ import os import sys PY3 = sys.version_info[0] == 3 if PY3: def u(s): return s def b(s): return s.encode("latin-1") getcwdu = os.getcwd unicode = str xrange = range else: def u(s): return unicode(s) def b(s): return s getcwdu = os.getcwdu unicode = unicode xrange = xrange # removed in 3.0, reintroduced in 3.2 try: callable = callable except Exception: def callable(obj): for klass in type(obj).__mro__: if "__call__" in klass.__dict__: return True return False pyftpdlib-release-1.5.4/pyftpdlib/authorizers.py000066400000000000000000001046561327314554200221040ustar00rootroot00000000000000# Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. """An "authorizer" is a class handling authentications and permissions of the FTP server. It is used by pyftpdlib.handlers.FTPHandler class for: - verifying user password - getting user home directory - checking user permissions when a filesystem read/write event occurs - changing user when accessing the filesystem DummyAuthorizer is the main class which handles virtual users. UnixAuthorizer and WindowsAuthorizer are platform specific and interact with UNIX and Windows password database. """ import errno import os import sys import warnings from ._compat import PY3 from ._compat import unicode from ._compat import getcwdu __all__ = ['DummyAuthorizer', # 'BaseUnixAuthorizer', 'UnixAuthorizer', # 'BaseWindowsAuthorizer', 'WindowsAuthorizer', ] # =================================================================== # --- exceptions # =================================================================== class AuthorizerError(Exception): """Base class for authorizer exceptions.""" class AuthenticationFailed(Exception): """Exception raised when authentication fails for any reason.""" # =================================================================== # --- base class # =================================================================== class DummyAuthorizer(object): """Basic "dummy" authorizer class, suitable for subclassing to create your own custom authorizers. An "authorizer" is a class handling authentications and permissions of the FTP server. It is used inside FTPHandler class for verifying user's password, getting users home directory, checking user permissions when a file read/write event occurs and changing user before accessing the filesystem. DummyAuthorizer is the base authorizer, providing a platform independent interface for managing "virtual" FTP users. System dependent authorizers can by written by subclassing this base class and overriding appropriate methods as necessary. """ read_perms = "elr" write_perms = "adfmwMT" def __init__(self): self.user_table = {} def add_user(self, username, password, homedir, perm='elr', msg_login="Login successful.", msg_quit="Goodbye."): """Add a user to the virtual users table. AuthorizerError exceptions raised on error conditions such as invalid permissions, missing home directory or duplicate usernames. Optional perm argument is a string referencing the user's permissions explained below: Read permissions: - "e" = change directory (CWD command) - "l" = list files (LIST, NLST, STAT, MLSD, MLST, SIZE, MDTM commands) - "r" = retrieve file from the server (RETR command) Write permissions: - "a" = append data to an existing file (APPE command) - "d" = delete file or directory (DELE, RMD commands) - "f" = rename file or directory (RNFR, RNTO commands) - "m" = create directory (MKD command) - "w" = store a file to the server (STOR, STOU commands) - "M" = change file mode (SITE CHMOD command) - "T" = update file last modified time (MFMT command) Optional msg_login and msg_quit arguments can be specified to provide customized response strings when user log-in and quit. """ if self.has_user(username): raise ValueError('user %r already exists' % username) if not isinstance(homedir, unicode): homedir = homedir.decode('utf8') if not os.path.isdir(homedir): raise ValueError('no such directory: %r' % homedir) homedir = os.path.realpath(homedir) self._check_permissions(username, perm) dic = {'pwd': str(password), 'home': homedir, 'perm': perm, 'operms': {}, 'msg_login': str(msg_login), 'msg_quit': str(msg_quit) } self.user_table[username] = dic def add_anonymous(self, homedir, **kwargs): """Add an anonymous user to the virtual users table. AuthorizerError exception raised on error conditions such as invalid permissions, missing home directory, or duplicate anonymous users. The keyword arguments in kwargs are the same expected by add_user method: "perm", "msg_login" and "msg_quit". The optional "perm" keyword argument is a string defaulting to "elr" referencing "read-only" anonymous user's permissions. Using write permission values ("adfmwM") results in a RuntimeWarning. """ DummyAuthorizer.add_user(self, 'anonymous', '', homedir, **kwargs) def remove_user(self, username): """Remove a user from the virtual users table.""" del self.user_table[username] def override_perm(self, username, directory, perm, recursive=False): """Override permissions for a given directory.""" self._check_permissions(username, perm) if not os.path.isdir(directory): raise ValueError('no such directory: %r' % directory) directory = os.path.normcase(os.path.realpath(directory)) home = os.path.normcase(self.get_home_dir(username)) if directory == home: raise ValueError("can't override home directory permissions") if not self._issubpath(directory, home): raise ValueError("path escapes user home directory") self.user_table[username]['operms'][directory] = perm, recursive def validate_authentication(self, username, password, handler): """Raises AuthenticationFailed if supplied username and password don't match the stored credentials, else return None. """ msg = "Authentication failed." if not self.has_user(username): if username == 'anonymous': msg = "Anonymous access not allowed." raise AuthenticationFailed(msg) if username != 'anonymous': if self.user_table[username]['pwd'] != password: raise AuthenticationFailed(msg) def get_home_dir(self, username): """Return the user's home directory. Since this is called during authentication (PASS), AuthenticationFailed can be freely raised by subclasses in case the provided username no longer exists. """ return self.user_table[username]['home'] def impersonate_user(self, username, password): """Impersonate another user (noop). It is always called before accessing the filesystem. By default it does nothing. The subclass overriding this method is expected to provide a mechanism to change the current user. """ def terminate_impersonation(self, username): """Terminate impersonation (noop). It is always called after having accessed the filesystem. By default it does nothing. The subclass overriding this method is expected to provide a mechanism to switch back to the original user. """ def has_user(self, username): """Whether the username exists in the virtual users table.""" return username in self.user_table def has_perm(self, username, perm, path=None): """Whether the user has permission over path (an absolute pathname of a file or a directory). Expected perm argument is one of the following letters: "elradfmwMT". """ if path is None: return perm in self.user_table[username]['perm'] path = os.path.normcase(path) for dir in self.user_table[username]['operms'].keys(): operm, recursive = self.user_table[username]['operms'][dir] if self._issubpath(path, dir): if recursive: return perm in operm if (path == dir or os.path.dirname(path) == dir and not os.path.isdir(path)): return perm in operm return perm in self.user_table[username]['perm'] def get_perms(self, username): """Return current user permissions.""" return self.user_table[username]['perm'] def get_msg_login(self, username): """Return the user's login message.""" return self.user_table[username]['msg_login'] def get_msg_quit(self, username): """Return the user's quitting message.""" try: return self.user_table[username]['msg_quit'] except KeyError: return "Goodbye." def _check_permissions(self, username, perm): warned = 0 for p in perm: if p not in self.read_perms + self.write_perms: raise ValueError('no such permission %r' % p) if (username == 'anonymous' and p in self.write_perms and not warned): warnings.warn("write permissions assigned to anonymous user.", RuntimeWarning) warned = 1 def _issubpath(self, a, b): """Return True if a is a sub-path of b or if the paths are equal.""" p1 = a.rstrip(os.sep).split(os.sep) p2 = b.rstrip(os.sep).split(os.sep) return p1[:len(p2)] == p2 def replace_anonymous(callable): """A decorator to replace anonymous user string passed to authorizer methods as first argument with the actual user used to handle anonymous sessions. """ def wrapper(self, username, *args, **kwargs): if username == 'anonymous': username = self.anonymous_user or username return callable(self, username, *args, **kwargs) return wrapper # =================================================================== # --- platform specific authorizers # =================================================================== class _Base(object): """Methods common to both Unix and Windows authorizers. Not supposed to be used directly. """ msg_no_such_user = "Authentication failed." msg_wrong_password = "Authentication failed." msg_anon_not_allowed = "Anonymous access not allowed." msg_invalid_shell = "User %s doesn't have a valid shell." msg_rejected_user = "User %s is not allowed to login." def __init__(self): """Check for errors in the constructor.""" if self.rejected_users and self.allowed_users: raise AuthorizerError("rejected_users and allowed_users options " "are mutually exclusive") users = self._get_system_users() for user in (self.allowed_users or self.rejected_users): if user == 'anonymous': raise AuthorizerError('invalid username "anonymous"') if user not in users: raise AuthorizerError('unknown user %s' % user) if self.anonymous_user is not None: if not self.has_user(self.anonymous_user): raise AuthorizerError('no such user %s' % self.anonymous_user) home = self.get_home_dir(self.anonymous_user) if not os.path.isdir(home): raise AuthorizerError('no valid home set for user %s' % self.anonymous_user) def override_user(self, username, password=None, homedir=None, perm=None, msg_login=None, msg_quit=None): """Overrides the options specified in the class constructor for a specific user. """ if (not password and not homedir and not perm and not msg_login and not msg_quit): raise AuthorizerError( "at least one keyword argument must be specified") if self.allowed_users and username not in self.allowed_users: raise AuthorizerError('%s is not an allowed user' % username) if self.rejected_users and username in self.rejected_users: raise AuthorizerError('%s is not an allowed user' % username) if username == "anonymous" and password: raise AuthorizerError("can't assign password to anonymous user") if not self.has_user(username): raise AuthorizerError('no such user %s' % username) if homedir is not None and not isinstance(homedir, unicode): homedir = homedir.decode('utf8') if username in self._dummy_authorizer.user_table: # re-set parameters del self._dummy_authorizer.user_table[username] self._dummy_authorizer.add_user(username, password or "", homedir or getcwdu(), perm or "", msg_login or "", msg_quit or "") if homedir is None: self._dummy_authorizer.user_table[username]['home'] = "" def get_msg_login(self, username): return self._get_key(username, 'msg_login') or self.msg_login def get_msg_quit(self, username): return self._get_key(username, 'msg_quit') or self.msg_quit def get_perms(self, username): overridden_perms = self._get_key(username, 'perm') if overridden_perms: return overridden_perms if username == 'anonymous': return 'elr' return self.global_perm def has_perm(self, username, perm, path=None): return perm in self.get_perms(username) def _get_key(self, username, key): if self._dummy_authorizer.has_user(username): return self._dummy_authorizer.user_table[username][key] def _is_rejected_user(self, username): """Return True if the user has been black listed via allowed_users or rejected_users options. """ if self.allowed_users and username not in self.allowed_users: return True if self.rejected_users and username in self.rejected_users: return True return False # =================================================================== # --- UNIX # =================================================================== try: import crypt import pwd import spwd except ImportError: pass else: __all__.extend(['BaseUnixAuthorizer', 'UnixAuthorizer']) # the uid/gid the server runs under PROCESS_UID = os.getuid() PROCESS_GID = os.getgid() class BaseUnixAuthorizer(object): """An authorizer compatible with Unix user account and password database. This class should not be used directly unless for subclassing. Use higher-level UnixAuthorizer class instead. """ def __init__(self, anonymous_user=None): if os.geteuid() != 0 or not spwd.getspall(): raise AuthorizerError("super user privileges are required") self.anonymous_user = anonymous_user if self.anonymous_user is not None: try: pwd.getpwnam(self.anonymous_user).pw_dir except KeyError: raise AuthorizerError('no such user %s' % anonymous_user) # --- overridden / private API def validate_authentication(self, username, password, handler): """Authenticates against shadow password db; raises AuthenticationFailed in case of failed authentication. """ if username == "anonymous": if self.anonymous_user is None: raise AuthenticationFailed(self.msg_anon_not_allowed) else: try: pw1 = spwd.getspnam(username).sp_pwd pw2 = crypt.crypt(password, pw1) except KeyError: # no such username raise AuthenticationFailed(self.msg_no_such_user) else: if pw1 != pw2: raise AuthenticationFailed(self.msg_wrong_password) @replace_anonymous def impersonate_user(self, username, password): """Change process effective user/group ids to reflect logged in user. """ try: pwdstruct = pwd.getpwnam(username) except KeyError: raise AuthorizerError(self.msg_no_such_user) else: os.setegid(pwdstruct.pw_gid) os.seteuid(pwdstruct.pw_uid) def terminate_impersonation(self, username): """Revert process effective user/group IDs.""" os.setegid(PROCESS_GID) os.seteuid(PROCESS_UID) @replace_anonymous def has_user(self, username): """Return True if user exists on the Unix system. If the user has been black listed via allowed_users or rejected_users options always return False. """ return username in self._get_system_users() @replace_anonymous def get_home_dir(self, username): """Return user home directory.""" try: home = pwd.getpwnam(username).pw_dir except KeyError: raise AuthorizerError(self.msg_no_such_user) else: if not PY3: home = home.decode('utf8') return home @staticmethod def _get_system_users(): """Return all users defined on the UNIX system.""" # there should be no need to convert usernames to unicode # as UNIX does not allow chars outside of ASCII set return [entry.pw_name for entry in pwd.getpwall()] def get_msg_login(self, username): return "Login successful." def get_msg_quit(self, username): return "Goodbye." def get_perms(self, username): return "elradfmwMT" def has_perm(self, username, perm, path=None): return perm in self.get_perms(username) class UnixAuthorizer(_Base, BaseUnixAuthorizer): """A wrapper on top of BaseUnixAuthorizer providing options to specify what users should be allowed to login, per-user options, etc. Example usages: >>> from pyftpdlib.authorizers import UnixAuthorizer >>> # accept all except root >>> auth = UnixAuthorizer(rejected_users=["root"]) >>> >>> # accept some users only >>> auth = UnixAuthorizer(allowed_users=["matt", "jay"]) >>> >>> # accept everybody and don't care if they have not a valid shell >>> auth = UnixAuthorizer(require_valid_shell=False) >>> >>> # set specific options for a user >>> auth.override_user("matt", password="foo", perm="elr") """ # --- public API def __init__(self, global_perm="elradfmwMT", allowed_users=None, rejected_users=None, require_valid_shell=True, anonymous_user=None, msg_login="Login successful.", msg_quit="Goodbye."): """Parameters: - (string) global_perm: a series of letters referencing the users permissions; defaults to "elradfmwMT" which means full read and write access for everybody (except anonymous). - (list) allowed_users: a list of users which are accepted for authenticating against the FTP server; defaults to [] (no restrictions). - (list) rejected_users: a list of users which are not accepted for authenticating against the FTP server; defaults to [] (no restrictions). - (bool) require_valid_shell: Deny access for those users which do not have a valid shell binary listed in /etc/shells. If /etc/shells cannot be found this is a no-op. Anonymous user is not subject to this option, and is free to not have a valid shell defined. Defaults to True (a valid shell is required for login). - (string) anonymous_user: specify it if you intend to provide anonymous access. The value expected is a string representing the system user to use for managing anonymous sessions; defaults to None (anonymous access disabled). - (string) msg_login: the string sent when client logs in. - (string) msg_quit: the string sent when client quits. """ BaseUnixAuthorizer.__init__(self, anonymous_user) if allowed_users is None: allowed_users = [] if rejected_users is None: rejected_users = [] self.global_perm = global_perm self.allowed_users = allowed_users self.rejected_users = rejected_users self.anonymous_user = anonymous_user self.require_valid_shell = require_valid_shell self.msg_login = msg_login self.msg_quit = msg_quit self._dummy_authorizer = DummyAuthorizer() self._dummy_authorizer._check_permissions('', global_perm) _Base.__init__(self) if require_valid_shell: for username in self.allowed_users: if not self._has_valid_shell(username): raise AuthorizerError("user %s has not a valid shell" % username) def override_user(self, username, password=None, homedir=None, perm=None, msg_login=None, msg_quit=None): """Overrides the options specified in the class constructor for a specific user. """ if self.require_valid_shell and username != 'anonymous': if not self._has_valid_shell(username): raise AuthorizerError(self.msg_invalid_shell % username) _Base.override_user(self, username, password, homedir, perm, msg_login, msg_quit) # --- overridden / private API def validate_authentication(self, username, password, handler): if username == "anonymous": if self.anonymous_user is None: raise AuthenticationFailed(self.msg_anon_not_allowed) return if self._is_rejected_user(username): raise AuthenticationFailed(self.msg_rejected_user % username) overridden_password = self._get_key(username, 'pwd') if overridden_password: if overridden_password != password: raise AuthenticationFailed(self.msg_wrong_password) else: BaseUnixAuthorizer.validate_authentication(self, username, password, handler) if self.require_valid_shell and username != 'anonymous': if not self._has_valid_shell(username): raise AuthenticationFailed( self.msg_invalid_shell % username) @replace_anonymous def has_user(self, username): if self._is_rejected_user(username): return False return username in self._get_system_users() @replace_anonymous def get_home_dir(self, username): overridden_home = self._get_key(username, 'home') if overridden_home: return overridden_home return BaseUnixAuthorizer.get_home_dir(self, username) @staticmethod def _has_valid_shell(username): """Return True if the user has a valid shell binary listed in /etc/shells. If /etc/shells can't be found return True. """ try: file = open('/etc/shells', 'r') except IOError as err: if err.errno == errno.ENOENT: return True raise else: with file: try: shell = pwd.getpwnam(username).pw_shell except KeyError: # invalid user return False for line in file: if line.startswith('#'): continue line = line.strip() if line == shell: return True return False # =================================================================== # --- Windows # =================================================================== # Note: requires pywin32 extension try: import pywintypes import win32api import win32con import win32net import win32security except ImportError: pass else: if sys.version_info < (3, 0): import _winreg as winreg else: import winreg __all__.extend(['BaseWindowsAuthorizer', 'WindowsAuthorizer']) class BaseWindowsAuthorizer(object): """An authorizer compatible with Windows user account and password database. This class should not be used directly unless for subclassing. Use higher-level WinowsAuthorizer class instead. """ def __init__(self, anonymous_user=None, anonymous_password=None): # actually try to impersonate the user self.anonymous_user = anonymous_user self.anonymous_password = anonymous_password if self.anonymous_user is not None: self.impersonate_user(self.anonymous_user, self.anonymous_password) self.terminate_impersonation(None) def validate_authentication(self, username, password, handler): if username == "anonymous": if self.anonymous_user is None: raise AuthenticationFailed(self.msg_anon_not_allowed) return try: win32security.LogonUser(username, None, password, win32con.LOGON32_LOGON_INTERACTIVE, win32con.LOGON32_PROVIDER_DEFAULT) except pywintypes.error: raise AuthenticationFailed(self.msg_wrong_password) @replace_anonymous def impersonate_user(self, username, password): """Impersonate the security context of another user.""" handler = win32security.LogonUser( username, None, password, win32con.LOGON32_LOGON_INTERACTIVE, win32con.LOGON32_PROVIDER_DEFAULT) win32security.ImpersonateLoggedOnUser(handler) handler.Close() def terminate_impersonation(self, username): """Terminate the impersonation of another user.""" win32security.RevertToSelf() @replace_anonymous def has_user(self, username): return username in self._get_system_users() @replace_anonymous def get_home_dir(self, username): """Return the user's profile directory, the closest thing to a user home directory we have on Windows. """ try: sid = win32security.ConvertSidToStringSid( win32security.LookupAccountName(None, username)[0]) except pywintypes.error as err: raise AuthorizerError(err) path = r"SOFTWARE\Microsoft\Windows NT" \ r"\CurrentVersion\ProfileList" + "\\" + sid try: key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path) except WindowsError: raise AuthorizerError( "No profile directory defined for user %s" % username) value = winreg.QueryValueEx(key, "ProfileImagePath")[0] home = win32api.ExpandEnvironmentStrings(value) if not PY3 and not isinstance(home, unicode): home = home.decode('utf8') return home @classmethod def _get_system_users(cls): """Return all users defined on the Windows system.""" # XXX - Does Windows allow usernames with chars outside of # ASCII set? In that case we need to convert this to unicode. return [entry['name'] for entry in win32net.NetUserEnum(None, 0)[0]] def get_msg_login(self, username): return "Login successful." def get_msg_quit(self, username): return "Goodbye." def get_perms(self, username): return "elradfmwMT" def has_perm(self, username, perm, path=None): return perm in self.get_perms(username) class WindowsAuthorizer(_Base, BaseWindowsAuthorizer): """A wrapper on top of BaseWindowsAuthorizer providing options to specify what users should be allowed to login, per-user options, etc. Example usages: >>> from pyftpdlib.authorizers import WindowsAuthorizer >>> # accept all except Administrator >>> auth = WindowsAuthorizer(rejected_users=["Administrator"]) >>> >>> # accept some users only >>> auth = WindowsAuthorizer(allowed_users=["matt", "jay"]) >>> >>> # set specific options for a user >>> auth.override_user("matt", password="foo", perm="elr") """ # --- public API def __init__(self, global_perm="elradfmwMT", allowed_users=None, rejected_users=None, anonymous_user=None, anonymous_password=None, msg_login="Login successful.", msg_quit="Goodbye."): """Parameters: - (string) global_perm: a series of letters referencing the users permissions; defaults to "elradfmwMT" which means full read and write access for everybody (except anonymous). - (list) allowed_users: a list of users which are accepted for authenticating against the FTP server; defaults to [] (no restrictions). - (list) rejected_users: a list of users which are not accepted for authenticating against the FTP server; defaults to [] (no restrictions). - (string) anonymous_user: specify it if you intend to provide anonymous access. The value expected is a string representing the system user to use for managing anonymous sessions. As for IIS, it is recommended to use Guest account. The common practice is to first enable the Guest user, which is disabled by default and then assign an empty password. Defaults to None (anonymous access disabled). - (string) anonymous_password: the password of the user who has been chosen to manage the anonymous sessions. Defaults to None (empty password). - (string) msg_login: the string sent when client logs in. - (string) msg_quit: the string sent when client quits. """ if allowed_users is None: allowed_users = [] if rejected_users is None: rejected_users = [] self.global_perm = global_perm self.allowed_users = allowed_users self.rejected_users = rejected_users self.anonymous_user = anonymous_user self.anonymous_password = anonymous_password self.msg_login = msg_login self.msg_quit = msg_quit self._dummy_authorizer = DummyAuthorizer() self._dummy_authorizer._check_permissions('', global_perm) _Base.__init__(self) # actually try to impersonate the user if self.anonymous_user is not None: self.impersonate_user(self.anonymous_user, self.anonymous_password) self.terminate_impersonation(None) def override_user(self, username, password=None, homedir=None, perm=None, msg_login=None, msg_quit=None): """Overrides the options specified in the class constructor for a specific user. """ _Base.override_user(self, username, password, homedir, perm, msg_login, msg_quit) # --- overridden / private API def validate_authentication(self, username, password, handler): """Authenticates against Windows user database; return True on success. """ if username == "anonymous": if self.anonymous_user is None: raise AuthenticationFailed(self.msg_anon_not_allowed) return if self.allowed_users and username not in self.allowed_users: raise AuthenticationFailed(self.msg_rejected_user % username) if self.rejected_users and username in self.rejected_users: raise AuthenticationFailed(self.msg_rejected_user % username) overridden_password = self._get_key(username, 'pwd') if overridden_password: if overridden_password != password: raise AuthenticationFailed(self.msg_wrong_password) else: BaseWindowsAuthorizer.validate_authentication( self, username, password, handler) def impersonate_user(self, username, password): """Impersonate the security context of another user.""" if username == "anonymous": username = self.anonymous_user or "" password = self.anonymous_password or "" BaseWindowsAuthorizer.impersonate_user(self, username, password) @replace_anonymous def has_user(self, username): if self._is_rejected_user(username): return False return username in self._get_system_users() @replace_anonymous def get_home_dir(self, username): overridden_home = self._get_key(username, 'home') if overridden_home: home = overridden_home else: home = BaseWindowsAuthorizer.get_home_dir(self, username) if not PY3 and not isinstance(home, unicode): home = home.decode('utf8') return home pyftpdlib-release-1.5.4/pyftpdlib/filesystems.py000066400000000000000000000571021327314554200220650ustar00rootroot00000000000000# Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. import os import stat import tempfile import time try: from stat import filemode as _filemode # PY 3.3 except ImportError: from tarfile import filemode as _filemode try: import pwd import grp except ImportError: pwd = grp = None try: from os import scandir # py 3.5 except ImportError: try: from scandir import scandir # requires "pip install scandir" except ImportError: scandir = None from ._compat import PY3 from ._compat import u from ._compat import unicode __all__ = ['FilesystemError', 'AbstractedFS'] _months_map = {1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May', 6: 'Jun', 7: 'Jul', 8: 'Aug', 9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dec'} # =================================================================== # --- custom exceptions # =================================================================== class FilesystemError(Exception): """Custom class for filesystem-related exceptions. You can raise this from an AbstractedFS subclass in order to send a customized error string to the client. """ # =================================================================== # --- base class # =================================================================== class AbstractedFS(object): """A class used to interact with the file system, providing a cross-platform interface compatible with both Windows and UNIX style filesystems where all paths use "/" separator. AbstractedFS distinguishes between "real" filesystem paths and "virtual" ftp paths emulating a UNIX chroot jail where the user can not escape its home directory (example: real "/home/user" path will be seen as "/" by the client) It also provides some utility methods and wraps around all os.* calls involving operations against the filesystem like creating files or removing directories. FilesystemError exception can be raised from within any of the methods below in order to send a customized error string to the client. """ def __init__(self, root, cmd_channel): """ - (str) root: the user "real" home directory (e.g. '/home/user') - (instance) cmd_channel: the FTPHandler class instance """ assert isinstance(root, unicode) # Set initial current working directory. # By default initial cwd is set to "/" to emulate a chroot jail. # If a different behavior is desired (e.g. initial cwd = root, # to reflect the real filesystem) users overriding this class # are responsible to set _cwd attribute as necessary. self._cwd = u('/') self._root = root self.cmd_channel = cmd_channel @property def root(self): """The user home directory.""" return self._root @property def cwd(self): """The user current working directory.""" return self._cwd @root.setter def root(self, path): assert isinstance(path, unicode), path self._root = path @cwd.setter def cwd(self, path): assert isinstance(path, unicode), path self._cwd = path # --- Pathname / conversion utilities def ftpnorm(self, ftppath): """Normalize a "virtual" ftp pathname (typically the raw string coming from client) depending on the current working directory. Example (having "/foo" as current working directory): >>> ftpnorm('bar') '/foo/bar' Note: directory separators are system independent ("/"). Pathname returned is always absolutized. """ assert isinstance(ftppath, unicode), ftppath if os.path.isabs(ftppath): p = os.path.normpath(ftppath) else: p = os.path.normpath(os.path.join(self.cwd, ftppath)) # normalize string in a standard web-path notation having '/' # as separator. if os.sep == "\\": p = p.replace("\\", "/") # os.path.normpath supports UNC paths (e.g. "//a/b/c") but we # don't need them. In case we get an UNC path we collapse # redundant separators appearing at the beginning of the string while p[:2] == '//': p = p[1:] # Anti path traversal: don't trust user input, in the event # that self.cwd is not absolute, return "/" as a safety measure. # This is for extra protection, maybe not really necessary. if not os.path.isabs(p): p = u("/") return p def ftp2fs(self, ftppath): """Translate a "virtual" ftp pathname (typically the raw string coming from client) into equivalent absolute "real" filesystem pathname. Example (having "/home/user" as root directory): >>> ftp2fs("foo") '/home/user/foo' Note: directory separators are system dependent. """ assert isinstance(ftppath, unicode), ftppath # as far as I know, it should always be path traversal safe... if os.path.normpath(self.root) == os.sep: return os.path.normpath(self.ftpnorm(ftppath)) else: p = self.ftpnorm(ftppath)[1:] return os.path.normpath(os.path.join(self.root, p)) def fs2ftp(self, fspath): """Translate a "real" filesystem pathname into equivalent absolute "virtual" ftp pathname depending on the user's root directory. Example (having "/home/user" as root directory): >>> fs2ftp("/home/user/foo") '/foo' As for ftpnorm, directory separators are system independent ("/") and pathname returned is always absolutized. On invalid pathnames escaping from user's root directory (e.g. "/home" when root is "/home/user") always return "/". """ assert isinstance(fspath, unicode), fspath if os.path.isabs(fspath): p = os.path.normpath(fspath) else: p = os.path.normpath(os.path.join(self.root, fspath)) if not self.validpath(p): return u('/') p = p.replace(os.sep, "/") p = p[len(self.root):] if not p.startswith('/'): p = '/' + p return p def validpath(self, path): """Check whether the path belongs to user's home directory. Expected argument is a "real" filesystem pathname. If path is a symbolic link it is resolved to check its real destination. Pathnames escaping from user's root directory are considered not valid. """ assert isinstance(path, unicode), path root = self.realpath(self.root) path = self.realpath(path) if not root.endswith(os.sep): root = root + os.sep if not path.endswith(os.sep): path = path + os.sep if path[0:len(root)] == root: return True return False # --- Wrapper methods around open() and tempfile.mkstemp def open(self, filename, mode): """Open a file returning its handler.""" assert isinstance(filename, unicode), filename return open(filename, mode) def mkstemp(self, suffix='', prefix='', dir=None, mode='wb'): """A wrap around tempfile.mkstemp creating a file with a unique name. Unlike mkstemp it returns an object with a file-like interface. """ class FileWrapper: def __init__(self, fd, name): self.file = fd self.name = name def __getattr__(self, attr): return getattr(self.file, attr) text = 'b' not in mode # max number of tries to find out a unique file name tempfile.TMP_MAX = 50 fd, name = tempfile.mkstemp(suffix, prefix, dir, text=text) file = os.fdopen(fd, mode) return FileWrapper(file, name) # --- Wrapper methods around os.* calls def chdir(self, path): """Change the current directory.""" # note: process cwd will be reset by the caller assert isinstance(path, unicode), path os.chdir(path) self._cwd = self.fs2ftp(path) def mkdir(self, path): """Create the specified directory.""" assert isinstance(path, unicode), path os.mkdir(path) def listdir(self, path): """List the content of a directory.""" assert isinstance(path, unicode), path return os.listdir(path) def listdirinfo(self, path): """List the content of a directory.""" assert isinstance(path, unicode), path return os.listdir(path) def rmdir(self, path): """Remove the specified directory.""" assert isinstance(path, unicode), path os.rmdir(path) def remove(self, path): """Remove the specified file.""" assert isinstance(path, unicode), path os.remove(path) def rename(self, src, dst): """Rename the specified src file to the dst filename.""" assert isinstance(src, unicode), src assert isinstance(dst, unicode), dst os.rename(src, dst) def chmod(self, path, mode): """Change file/directory mode.""" assert isinstance(path, unicode), path if not hasattr(os, 'chmod'): raise NotImplementedError os.chmod(path, mode) def stat(self, path): """Perform a stat() system call on the given path.""" # on python 2 we might also get bytes from os.lisdir() # assert isinstance(path, unicode), path return os.stat(path) def utime(self, path, timeval): """Perform a utime() call on the given path""" # utime expects a int/float (atime, mtime) in seconds # thus, setting both access and modify time to timeval return os.utime(path, (timeval, timeval)) if hasattr(os, 'lstat'): def lstat(self, path): """Like stat but does not follow symbolic links.""" # on python 2 we might also get bytes from os.lisdir() # assert isinstance(path, unicode), path return os.lstat(path) else: lstat = stat if hasattr(os, 'readlink'): def readlink(self, path): """Return a string representing the path to which a symbolic link points. """ assert isinstance(path, unicode), path return os.readlink(path) # --- Wrapper methods around os.path.* calls def isfile(self, path): """Return True if path is a file.""" assert isinstance(path, unicode), path return os.path.isfile(path) def islink(self, path): """Return True if path is a symbolic link.""" assert isinstance(path, unicode), path return os.path.islink(path) def isdir(self, path): """Return True if path is a directory.""" assert isinstance(path, unicode), path return os.path.isdir(path) def getsize(self, path): """Return the size of the specified file in bytes.""" assert isinstance(path, unicode), path return os.path.getsize(path) def getmtime(self, path): """Return the last modified time as a number of seconds since the epoch.""" assert isinstance(path, unicode), path return os.path.getmtime(path) def realpath(self, path): """Return the canonical version of path eliminating any symbolic links encountered in the path (if they are supported by the operating system). """ assert isinstance(path, unicode), path return os.path.realpath(path) def lexists(self, path): """Return True if path refers to an existing path, including a broken or circular symbolic link. """ assert isinstance(path, unicode), path return os.path.lexists(path) if pwd is not None: def get_user_by_uid(self, uid): """Return the username associated with user id. If this can't be determined return raw uid instead. On Windows just return "owner". """ try: return pwd.getpwuid(uid).pw_name except KeyError: return uid else: def get_user_by_uid(self, uid): return "owner" if grp is not None: def get_group_by_gid(self, gid): """Return the groupname associated with group id. If this can't be determined return raw gid instead. On Windows just return "group". """ try: return grp.getgrgid(gid).gr_name except KeyError: return gid else: def get_group_by_gid(self, gid): return "group" # --- Listing utilities def format_list(self, basedir, listing, ignore_err=True): """Return an iterator object that yields the entries of given directory emulating the "/bin/ls -lA" UNIX command output. - (str) basedir: the absolute dirname. - (list) listing: the names of the entries in basedir - (bool) ignore_err: when False raise exception if os.lstat() call fails. On platforms which do not support the pwd and grp modules (such as Windows), ownership is printed as "owner" and "group" as a default, and number of hard links is always "1". On UNIX systems, the actual owner, group, and number of links are printed. This is how output appears to client: -rw-rw-rw- 1 owner group 7045120 Sep 02 3:47 music.mp3 drwxrwxrwx 1 owner group 0 Aug 31 18:50 e-books -rw-rw-rw- 1 owner group 380 Sep 02 3:40 module.py """ assert isinstance(basedir, unicode), basedir if self.cmd_channel.use_gmt_times: timefunc = time.gmtime else: timefunc = time.localtime SIX_MONTHS = 180 * 24 * 60 * 60 readlink = getattr(self, 'readlink', None) now = time.time() for basename in listing: if not PY3: try: file = os.path.join(basedir, basename) except UnicodeDecodeError: # (Python 2 only) might happen on filesystem not # supporting UTF8 meaning os.listdir() returned a list # of mixed bytes and unicode strings: # http://goo.gl/6DLHD # http://bugs.python.org/issue683592 file = os.path.join(bytes(basedir), bytes(basename)) if not isinstance(basename, unicode): basename = unicode(basename, 'utf8', 'ignore') else: file = os.path.join(basedir, basename) try: st = self.lstat(file) except (OSError, FilesystemError): if ignore_err: continue raise perms = _filemode(st.st_mode) # permissions nlinks = st.st_nlink # number of links to inode if not nlinks: # non-posix system, let's use a bogus value nlinks = 1 size = st.st_size # file size uname = self.get_user_by_uid(st.st_uid) gname = self.get_group_by_gid(st.st_gid) mtime = timefunc(st.st_mtime) # if modification time > 6 months shows "month year" # else "month hh:mm"; this matches proftpd format, see: # https://github.com/giampaolo/pyftpdlib/issues/187 if (now - st.st_mtime) > SIX_MONTHS: fmtstr = "%d %Y" else: fmtstr = "%d %H:%M" try: mtimestr = "%s %s" % (_months_map[mtime.tm_mon], time.strftime(fmtstr, mtime)) except ValueError: # It could be raised if last mtime happens to be too # old (prior to year 1900) in which case we return # the current time as last mtime. mtime = timefunc() mtimestr = "%s %s" % (_months_map[mtime.tm_mon], time.strftime("%d %H:%M", mtime)) # same as stat.S_ISLNK(st.st_mode) but slighlty faster islink = (st.st_mode & 61440) == stat.S_IFLNK if islink and readlink is not None: # if the file is a symlink, resolve it, e.g. # "symlink -> realfile" try: basename = basename + " -> " + readlink(file) except (OSError, FilesystemError): if not ignore_err: raise # formatting is matched with proftpd ls output line = "%s %3s %-8s %-8s %8s %s %s\r\n" % ( perms, nlinks, uname, gname, size, mtimestr, basename) yield line.encode('utf8', self.cmd_channel.unicode_errors) def format_mlsx(self, basedir, listing, perms, facts, ignore_err=True): """Return an iterator object that yields the entries of a given directory or of a single file in a form suitable with MLSD and MLST commands. Every entry includes a list of "facts" referring the listed element. See RFC-3659, chapter 7, to see what every single fact stands for. - (str) basedir: the absolute dirname. - (list) listing: the names of the entries in basedir - (str) perms: the string referencing the user permissions. - (str) facts: the list of "facts" to be returned. - (bool) ignore_err: when False raise exception if os.stat() call fails. Note that "facts" returned may change depending on the platform and on what user specified by using the OPTS command. This is how output could appear to the client issuing a MLSD request: type=file;size=156;perm=r;modify=20071029155301;unique=8012; music.mp3 type=dir;size=0;perm=el;modify=20071127230206;unique=801e33; ebooks type=file;size=211;perm=r;modify=20071103093626;unique=192; module.py """ assert isinstance(basedir, unicode), basedir if self.cmd_channel.use_gmt_times: timefunc = time.gmtime else: timefunc = time.localtime permdir = ''.join([x for x in perms if x not in 'arw']) permfile = ''.join([x for x in perms if x not in 'celmp']) if ('w' in perms) or ('a' in perms) or ('f' in perms): permdir += 'c' if 'd' in perms: permdir += 'p' show_type = 'type' in facts show_perm = 'perm' in facts show_size = 'size' in facts show_modify = 'modify' in facts show_create = 'create' in facts show_mode = 'unix.mode' in facts show_uid = 'unix.uid' in facts show_gid = 'unix.gid' in facts show_unique = 'unique' in facts for basename in listing: retfacts = dict() if not PY3: try: file = os.path.join(basedir, basename) except UnicodeDecodeError: # (Python 2 only) might happen on filesystem not # supporting UTF8 meaning os.listdir() returned a list # of mixed bytes and unicode strings: # http://goo.gl/6DLHD # http://bugs.python.org/issue683592 file = os.path.join(bytes(basedir), bytes(basename)) if not isinstance(basename, unicode): basename = unicode(basename, 'utf8', 'ignore') else: file = os.path.join(basedir, basename) # in order to properly implement 'unique' fact (RFC-3659, # chapter 7.5.2) we are supposed to follow symlinks, hence # use os.stat() instead of os.lstat() try: st = self.stat(file) except (OSError, FilesystemError): if ignore_err: continue raise # type + perm # same as stat.S_ISDIR(st.st_mode) but slightly faster isdir = (st.st_mode & 61440) == stat.S_IFDIR if isdir: if show_type: if basename == '.': retfacts['type'] = 'cdir' elif basename == '..': retfacts['type'] = 'pdir' else: retfacts['type'] = 'dir' if show_perm: retfacts['perm'] = permdir else: if show_type: retfacts['type'] = 'file' if show_perm: retfacts['perm'] = permfile if show_size: retfacts['size'] = st.st_size # file size # last modification time if show_modify: try: retfacts['modify'] = time.strftime("%Y%m%d%H%M%S", timefunc(st.st_mtime)) # it could be raised if last mtime happens to be too old # (prior to year 1900) except ValueError: pass if show_create: # on Windows we can provide also the creation time try: retfacts['create'] = time.strftime("%Y%m%d%H%M%S", timefunc(st.st_ctime)) except ValueError: pass # UNIX only if show_mode: retfacts['unix.mode'] = oct(st.st_mode & 511) if show_uid: retfacts['unix.uid'] = st.st_uid if show_gid: retfacts['unix.gid'] = st.st_gid # We provide unique fact (see RFC-3659, chapter 7.5.2) on # posix platforms only; we get it by mixing st_dev and # st_ino values which should be enough for granting an # uniqueness for the file listed. # The same approach is used by pure-ftpd. # Implementors who want to provide unique fact on other # platforms should use some platform-specific method (e.g. # on Windows NTFS filesystems MTF records could be used). if show_unique: retfacts['unique'] = "%xg%x" % (st.st_dev, st.st_ino) # facts can be in any order but we sort them by name factstring = "".join(["%s=%s;" % (x, retfacts[x]) for x in sorted(retfacts.keys())]) line = "%s %s\r\n" % (factstring, basename) yield line.encode('utf8', self.cmd_channel.unicode_errors) # =================================================================== # --- platform specific implementation # =================================================================== if os.name == 'posix': __all__.append('UnixFilesystem') class UnixFilesystem(AbstractedFS): """Represents the real UNIX filesystem. Differently from AbstractedFS the client will login into /home/ and will be able to escape its home directory and navigate the real filesystem. """ def __init__(self, root, cmd_channel): AbstractedFS.__init__(self, root, cmd_channel) # initial cwd was set to "/" to emulate a chroot jail self.cwd = root def ftp2fs(self, ftppath): return self.ftpnorm(ftppath) def fs2ftp(self, fspath): return fspath def validpath(self, path): # validpath was used to check symlinks escaping user home # directory; this is no longer necessary. return True pyftpdlib-release-1.5.4/pyftpdlib/handlers.py000066400000000000000000004307711327314554200213250ustar00rootroot00000000000000# Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. import asynchat import contextlib import errno import glob import logging import os import random import socket import sys import time import traceback import warnings from datetime import datetime try: import pwd import grp except ImportError: pwd = grp = None try: from OpenSSL import SSL # requires "pip install pyopenssl" except ImportError: SSL = None try: from collections import OrderedDict # python >= 2.7 except ImportError: OrderedDict = dict from . import __ver__ from ._compat import b from ._compat import getcwdu from ._compat import PY3 from ._compat import u from ._compat import unicode from ._compat import xrange from .authorizers import AuthenticationFailed from .authorizers import AuthorizerError from .authorizers import DummyAuthorizer from .filesystems import AbstractedFS from .filesystems import FilesystemError from .ioloop import _ERRNOS_DISCONNECTED from .ioloop import _ERRNOS_RETRY from .ioloop import Acceptor from .ioloop import AsyncChat from .ioloop import Connector from .ioloop import RetryError from .ioloop import timer from .log import debug from .log import logger def _import_sendfile(): # By default attempt to use os.sendfile introduced in Python 3.3: # http://bugs.python.org/issue10882 # ...otherwise fallback on using third-party pysendfile module: # https://github.com/giampaolo/pysendfile/ if os.name == 'posix': try: return os.sendfile # py >= 3.3 except AttributeError: try: import sendfile as sf # dirty hack to detect whether old 1.2.4 version is installed if hasattr(sf, 'has_sf_hdtr'): raise ImportError return sf.sendfile except ImportError: pass return None sendfile = _import_sendfile() proto_cmds = { 'ABOR': dict( perm=None, auth=True, arg=False, help='Syntax: ABOR (abort transfer).'), 'ALLO': dict( perm=None, auth=True, arg=True, help='Syntax: ALLO bytes (noop; allocate storage).'), 'APPE': dict( perm='a', auth=True, arg=True, help='Syntax: APPE file-name (append data to file).'), 'CDUP': dict( perm='e', auth=True, arg=False, help='Syntax: CDUP (go to parent directory).'), 'CWD': dict( perm='e', auth=True, arg=None, help='Syntax: CWD [ dir-name] (change working directory).'), 'DELE': dict( perm='d', auth=True, arg=True, help='Syntax: DELE file-name (delete file).'), 'EPRT': dict( perm=None, auth=True, arg=True, help='Syntax: EPRT |proto|ip|port| (extended active mode).'), 'EPSV': dict( perm=None, auth=True, arg=None, help='Syntax: EPSV [ proto/"ALL"] (extended passive mode).'), 'FEAT': dict( perm=None, auth=False, arg=False, help='Syntax: FEAT (list all new features supported).'), 'HELP': dict( perm=None, auth=False, arg=None, help='Syntax: HELP [ cmd] (show help).'), 'LIST': dict( perm='l', auth=True, arg=None, help='Syntax: LIST [ path] (list files).'), 'MDTM': dict( perm='l', auth=True, arg=True, help='Syntax: MDTM [ path] (file last modification time).'), 'MFMT': dict( perm='T', auth=True, arg=True, help='Syntax: MFMT timeval path (file update last ' 'modification time).'), 'MLSD': dict( perm='l', auth=True, arg=None, help='Syntax: MLSD [ path] (list directory).'), 'MLST': dict( perm='l', auth=True, arg=None, help='Syntax: MLST [ path] (show information about path).'), 'MODE': dict( perm=None, auth=True, arg=True, help='Syntax: MODE mode (noop; set data transfer mode).'), 'MKD': dict( perm='m', auth=True, arg=True, help='Syntax: MKD path (create directory).'), 'NLST': dict( perm='l', auth=True, arg=None, help='Syntax: NLST [ path] (list path in a compact form).'), 'NOOP': dict( perm=None, auth=False, arg=False, help='Syntax: NOOP (just do nothing).'), 'OPTS': dict( perm=None, auth=True, arg=True, help='Syntax: OPTS cmd [ option] (set option for command).'), 'PASS': dict( perm=None, auth=False, arg=None, help='Syntax: PASS [ password] (set user password).'), 'PASV': dict( perm=None, auth=True, arg=False, help='Syntax: PASV (open passive data connection).'), 'PORT': dict( perm=None, auth=True, arg=True, help='Syntax: PORT h,h,h,h,p,p (open active data connection).'), 'PWD': dict( perm=None, auth=True, arg=False, help='Syntax: PWD (get current working directory).'), 'QUIT': dict( perm=None, auth=False, arg=False, help='Syntax: QUIT (quit current session).'), 'REIN': dict( perm=None, auth=True, arg=False, help='Syntax: REIN (flush account).'), 'REST': dict( perm=None, auth=True, arg=True, help='Syntax: REST offset (set file offset).'), 'RETR': dict( perm='r', auth=True, arg=True, help='Syntax: RETR file-name (retrieve a file).'), 'RMD': dict( perm='d', auth=True, arg=True, help='Syntax: RMD dir-name (remove directory).'), 'RNFR': dict( perm='f', auth=True, arg=True, help='Syntax: RNFR file-name (rename (source name)).'), 'RNTO': dict( perm='f', auth=True, arg=True, help='Syntax: RNTO file-name (rename (destination name)).'), 'SITE': dict( perm=None, auth=False, arg=True, help='Syntax: SITE site-command (execute SITE command).'), 'SITE HELP': dict( perm=None, auth=False, arg=None, help='Syntax: SITE HELP [ cmd] (show SITE command help).'), 'SITE CHMOD': dict( perm='M', auth=True, arg=True, help='Syntax: SITE CHMOD mode path (change file mode).'), 'SIZE': dict( perm='l', auth=True, arg=True, help='Syntax: SIZE file-name (get file size).'), 'STAT': dict( perm='l', auth=False, arg=None, help='Syntax: STAT [ path name] (server stats [list files]).'), 'STOR': dict( perm='w', auth=True, arg=True, help='Syntax: STOR file-name (store a file).'), 'STOU': dict( perm='w', auth=True, arg=None, help='Syntax: STOU [ name] (store a file with a unique name).'), 'STRU': dict( perm=None, auth=True, arg=True, help='Syntax: STRU type (noop; set file structure).'), 'SYST': dict( perm=None, auth=False, arg=False, help='Syntax: SYST (get operating system type).'), 'TYPE': dict( perm=None, auth=True, arg=True, help='Syntax: TYPE [A | I] (set transfer type).'), 'USER': dict( perm=None, auth=False, arg=True, help='Syntax: USER user-name (set username).'), 'XCUP': dict( perm='e', auth=True, arg=False, help='Syntax: XCUP (obsolete; go to parent directory).'), 'XCWD': dict( perm='e', auth=True, arg=None, help='Syntax: XCWD [ dir-name] (obsolete; change directory).'), 'XMKD': dict( perm='m', auth=True, arg=True, help='Syntax: XMKD dir-name (obsolete; create directory).'), 'XPWD': dict( perm=None, auth=True, arg=False, help='Syntax: XPWD (obsolete; get current dir).'), 'XRMD': dict( perm='d', auth=True, arg=True, help='Syntax: XRMD dir-name (obsolete; remove directory).'), } if not hasattr(os, 'chmod'): del proto_cmds['SITE CHMOD'] def _strerror(err): if isinstance(err, EnvironmentError): try: return os.strerror(err.errno) except AttributeError: # not available on PythonCE if not hasattr(os, 'strerror'): return err.strerror raise else: return str(err) def _is_ssl_sock(sock): return SSL is not None and isinstance(sock, SSL.Connection) def _support_hybrid_ipv6(): """Return True if it is possible to use hybrid IPv6/IPv4 sockets on this platform. """ # Note: IPPROTO_IPV6 constant is broken on Windows, see: # http://bugs.python.org/issue6926 try: if not socket.has_ipv6: return False with contextlib.closing(socket.socket(socket.AF_INET6)) as sock: return not sock.getsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY) except (socket.error, AttributeError): return False SUPPORTS_HYBRID_IPV6 = _support_hybrid_ipv6() class _FileReadWriteError(OSError): """Exception raised when reading or writing a file during a transfer.""" class _GiveUpOnSendfile(Exception): """Exception raised in case use of sendfile() fails on first try, in which case send() will be used. """ # --- DTP classes class PassiveDTP(Acceptor): """Creates a socket listening on a local port, dispatching the resultant connection to DTPHandler. Used for handling PASV command. - (int) timeout: the timeout for a remote client to establish connection with the listening socket. Defaults to 30 seconds. - (int) backlog: the maximum number of queued connections passed to listen(). If a connection request arrives when the queue is full the client may raise ECONNRESET. Defaults to 5. """ timeout = 30 backlog = None def __init__(self, cmd_channel, extmode=False): """Initialize the passive data server. - (instance) cmd_channel: the command channel class instance. - (bool) extmode: wheter use extended passive mode response type. """ self.cmd_channel = cmd_channel self.log = cmd_channel.log self.log_exception = cmd_channel.log_exception Acceptor.__init__(self, ioloop=cmd_channel.ioloop) local_ip = self.cmd_channel.socket.getsockname()[0] if local_ip in self.cmd_channel.masquerade_address_map: masqueraded_ip = self.cmd_channel.masquerade_address_map[local_ip] elif self.cmd_channel.masquerade_address: masqueraded_ip = self.cmd_channel.masquerade_address else: masqueraded_ip = None if self.cmd_channel.server.socket.family != socket.AF_INET: # dual stack IPv4/IPv6 support af = self.bind_af_unspecified((local_ip, 0)) self.socket.close() else: af = self.cmd_channel.socket.family self.create_socket(af, socket.SOCK_STREAM) if self.cmd_channel.passive_ports is None: # By using 0 as port number value we let kernel choose a # free unprivileged random port. self.bind((local_ip, 0)) else: ports = list(self.cmd_channel.passive_ports) while ports: port = ports.pop(random.randint(0, len(ports) - 1)) self.set_reuse_addr() try: self.bind((local_ip, port)) except socket.error as err: if err.errno == errno.EADDRINUSE: # port already in use if ports: continue # If cannot use one of the ports in the configured # range we'll use a kernel-assigned port, and log # a message reporting the issue. # By using 0 as port number value we let kernel # choose a free unprivileged random port. else: self.bind((local_ip, 0)) self.cmd_channel.log( "Can't find a valid passive port in the " "configured range. A random kernel-assigned " "port will be used.", logfun=logger.warning ) else: raise else: break self.listen(self.backlog or self.cmd_channel.server.backlog) port = self.socket.getsockname()[1] if not extmode: ip = masqueraded_ip or local_ip if ip.startswith('::ffff:'): # In this scenario, the server has an IPv6 socket, but # the remote client is using IPv4 and its address is # represented as an IPv4-mapped IPv6 address which # looks like this ::ffff:151.12.5.65, see: # http://en.wikipedia.org/wiki/IPv6#IPv4-mapped_addresses # http://tools.ietf.org/html/rfc3493.html#section-3.7 # We truncate the first bytes to make it look like a # common IPv4 address. ip = ip[7:] # The format of 227 response in not standardized. # This is the most expected: resp = '227 Entering passive mode (%s,%d,%d).' % ( ip.replace('.', ','), port // 256, port % 256) self.cmd_channel.respond(resp) else: self.cmd_channel.respond('229 Entering extended passive mode ' '(|||%d|).' % port) if self.timeout: self.call_later(self.timeout, self.handle_timeout) # --- connection / overridden def handle_accepted(self, sock, addr): """Called when remote client initiates a connection.""" if not self.cmd_channel.connected: return self.close() # Check the origin of data connection. If not expressively # configured we drop the incoming data connection if remote # IP address does not match the client's IP address. if self.cmd_channel.remote_ip != addr[0]: if not self.cmd_channel.permit_foreign_addresses: try: sock.close() except socket.error: pass msg = '425 Rejected data connection from foreign address ' \ '%s:%s.' % (addr[0], addr[1]) self.cmd_channel.respond_w_warning(msg) # do not close listening socket: it couldn't be client's blame return else: # site-to-site FTP allowed msg = 'Established data connection with foreign address ' \ '%s:%s.' % (addr[0], addr[1]) self.cmd_channel.log(msg, logfun=logger.warning) # Immediately close the current channel (we accept only one # connection at time) and avoid running out of max connections # limit. self.close() # delegate such connection to DTP handler if self.cmd_channel.connected: handler = self.cmd_channel.dtp_handler(sock, self.cmd_channel) if handler.connected: self.cmd_channel.data_channel = handler self.cmd_channel._on_dtp_connection() def handle_timeout(self): if self.cmd_channel.connected: self.cmd_channel.respond("421 Passive data channel timed out.", logfun=logger.info) self.close() def handle_error(self): """Called to handle any uncaught exceptions.""" try: raise except Exception: logger.error(traceback.format_exc()) try: self.close() except Exception: logger.critical(traceback.format_exc()) def close(self): debug("call: close()", inst=self) Acceptor.close(self) class ActiveDTP(Connector): """Connects to remote client and dispatches the resulting connection to DTPHandler. Used for handling PORT command. - (int) timeout: the timeout for us to establish connection with the client's listening data socket. """ timeout = 30 def __init__(self, ip, port, cmd_channel): """Initialize the active data channel attemping to connect to remote data socket. - (str) ip: the remote IP address. - (int) port: the remote port. - (instance) cmd_channel: the command channel class instance. """ Connector.__init__(self, ioloop=cmd_channel.ioloop) self.cmd_channel = cmd_channel self.log = cmd_channel.log self.log_exception = cmd_channel.log_exception self._idler = None if self.timeout: self._idler = self.ioloop.call_later(self.timeout, self.handle_timeout, _errback=self.handle_error) if ip.count('.') == 4: self._cmd = "PORT" self._normalized_addr = "%s:%s" % (ip, port) else: self._cmd = "EPRT" self._normalized_addr = "[%s]:%s" % (ip, port) source_ip = self.cmd_channel.socket.getsockname()[0] # dual stack IPv4/IPv6 support try: self.connect_af_unspecified((ip, port), (source_ip, 0)) except (socket.gaierror, socket.error): self.handle_close() def readable(self): return False def handle_write(self): # overridden to prevent unhandled read/write event messages to # be printed by asyncore on Python < 2.6 pass def handle_connect(self): """Called when connection is established.""" self.del_channel() if self._idler is not None and not self._idler.cancelled: self._idler.cancel() if not self.cmd_channel.connected: return self.close() # fix for asyncore on python < 2.6, meaning we aren't # actually connected. # test_active_conn_error tests this condition err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) if err != 0: raise socket.error(err) # msg = 'Active data connection established.' self.cmd_channel.respond('200 ' + msg) self.cmd_channel.log_cmd(self._cmd, self._normalized_addr, 200, msg) # if not self.cmd_channel.connected: return self.close() # delegate such connection to DTP handler handler = self.cmd_channel.dtp_handler(self.socket, self.cmd_channel) self.cmd_channel.data_channel = handler self.cmd_channel._on_dtp_connection() def handle_timeout(self): if self.cmd_channel.connected: msg = "Active data channel timed out." self.cmd_channel.respond("421 " + msg, logfun=logger.info) self.cmd_channel.log_cmd( self._cmd, self._normalized_addr, 421, msg) self.close() def handle_close(self): # With the new IO loop, handle_close() gets called in case # the fd appears in the list of exceptional fds. # This means connect() failed. if not self._closed: self.close() if self.cmd_channel.connected: msg = "Can't connect to specified address." self.cmd_channel.respond("425 " + msg) self.cmd_channel.log_cmd( self._cmd, self._normalized_addr, 425, msg) def handle_error(self): """Called to handle any uncaught exceptions.""" try: raise except (socket.gaierror, socket.error): pass except Exception: self.log_exception(self) try: self.handle_close() except Exception: logger.critical(traceback.format_exc()) def close(self): debug("call: close()", inst=self) if not self._closed: Connector.close(self) if self._idler is not None and not self._idler.cancelled: self._idler.cancel() class DTPHandler(AsyncChat): """Class handling server-data-transfer-process (server-DTP, see RFC-959) managing data-transfer operations involving sending and receiving data. Class attributes: - (int) timeout: the timeout which roughly is the maximum time we permit data transfers to stall for with no progress. If the timeout triggers, the remote client will be kicked off (defaults 300). - (int) ac_in_buffer_size: incoming data buffer size (defaults 65536) - (int) ac_out_buffer_size: outgoing data buffer size (defaults 65536) """ timeout = 300 ac_in_buffer_size = 65536 ac_out_buffer_size = 65536 def __init__(self, sock, cmd_channel): """Initialize the command channel. - (instance) sock: the socket object instance of the newly established connection. - (instance) cmd_channel: the command channel class instance. """ self.cmd_channel = cmd_channel self.file_obj = None self.receive = False self.transfer_finished = False self.tot_bytes_sent = 0 self.tot_bytes_received = 0 self.cmd = None self.log = cmd_channel.log self.log_exception = cmd_channel.log_exception self._data_wrapper = None self._lastdata = 0 self._had_cr = False self._start_time = timer() self._resp = () self._offset = None self._filefd = None self._idler = None self._initialized = False try: AsyncChat.__init__(self, sock, ioloop=cmd_channel.ioloop) except socket.error as err: # if we get an exception here we want the dispatcher # instance to set socket attribute before closing, see: # https://github.com/giampaolo/pyftpdlib/issues/188 AsyncChat.__init__( self, socket.socket(), ioloop=cmd_channel.ioloop) # https://github.com/giampaolo/pyftpdlib/issues/143 self.close() if err.errno == errno.EINVAL: return self.handle_error() return # remove this instance from IOLoop's socket map if not self.connected: self.close() return if self.timeout: self._idler = self.ioloop.call_every(self.timeout, self.handle_timeout, _errback=self.handle_error) def __repr__(self): return '<%s(%s)>' % (self.__class__.__name__, self.cmd_channel.get_repr_info(as_str=True)) __str__ = __repr__ def use_sendfile(self): if not self.cmd_channel.use_sendfile: # as per server config return False if self.file_obj is None or not hasattr(self.file_obj, "fileno"): # directory listing or unusual file obj return False if self.cmd_channel._current_type != 'i': # text file transfer (need to transform file content on the fly) return False return True def push(self, data): self._initialized = True self.modify_ioloop_events(self.ioloop.WRITE) self._wanted_io_events = self.ioloop.WRITE AsyncChat.push(self, data) def push_with_producer(self, producer): self._initialized = True self.modify_ioloop_events(self.ioloop.WRITE) self._wanted_io_events = self.ioloop.WRITE if self.use_sendfile(): self._offset = producer.file.tell() self._filefd = self.file_obj.fileno() try: self.initiate_sendfile() except _GiveUpOnSendfile: pass else: self.initiate_send = self.initiate_sendfile return debug("starting transfer using send()", self) AsyncChat.push_with_producer(self, producer) def close_when_done(self): asynchat.async_chat.close_when_done(self) def initiate_send(self): asynchat.async_chat.initiate_send(self) def initiate_sendfile(self): """A wrapper around sendfile.""" try: sent = sendfile(self._fileno, self._filefd, self._offset, self.ac_out_buffer_size) except OSError as err: if err.errno in _ERRNOS_RETRY or err.errno == errno.EBUSY: return elif err.errno in _ERRNOS_DISCONNECTED: self.handle_close() else: if self.tot_bytes_sent == 0: logger.warning( "sendfile() failed; falling back on using plain send") raise _GiveUpOnSendfile else: raise else: if sent == 0: # this signals the channel that the transfer is completed self.discard_buffers() self.handle_close() else: self._offset += sent self.tot_bytes_sent += sent # --- utility methods def _posix_ascii_data_wrapper(self, chunk): """The data wrapper used for receiving data in ASCII mode on systems using a single line terminator, handling those cases where CRLF ('\r\n') gets delivered in two chunks. """ if self._had_cr: chunk = b'\r' + chunk if chunk.endswith(b'\r'): self._had_cr = True chunk = chunk[:-1] else: self._had_cr = False return chunk.replace(b'\r\n', b(os.linesep)) def enable_receiving(self, type, cmd): """Enable receiving of data over the channel. Depending on the TYPE currently in use it creates an appropriate wrapper for the incoming data. - (str) type: current transfer type, 'a' (ASCII) or 'i' (binary). """ self._initialized = True self.modify_ioloop_events(self.ioloop.READ) self._wanted_io_events = self.ioloop.READ self.cmd = cmd if type == 'a': if os.linesep == '\r\n': self._data_wrapper = None else: self._data_wrapper = self._posix_ascii_data_wrapper elif type == 'i': self._data_wrapper = None else: raise TypeError("unsupported type") self.receive = True def get_transmitted_bytes(self): """Return the number of transmitted bytes.""" return self.tot_bytes_sent + self.tot_bytes_received def get_elapsed_time(self): """Return the transfer elapsed time in seconds.""" return timer() - self._start_time def transfer_in_progress(self): """Return True if a transfer is in progress, else False.""" return self.get_transmitted_bytes() != 0 # --- connection def send(self, data): result = AsyncChat.send(self, data) self.tot_bytes_sent += result return result def refill_buffer(self): # pragma: no cover """Overridden as a fix around http://bugs.python.org/issue1740572 (when the producer is consumed, close() was called instead of handle_close()). """ while True: if len(self.producer_fifo): p = self.producer_fifo.first() # a 'None' in the producer fifo is a sentinel, # telling us to close the channel. if p is None: if not self.ac_out_buffer: self.producer_fifo.pop() # self.close() self.handle_close() return elif isinstance(p, str): self.producer_fifo.pop() self.ac_out_buffer += p return data = p.more() if data: self.ac_out_buffer = self.ac_out_buffer + data return else: self.producer_fifo.pop() else: return def handle_read(self): """Called when there is data waiting to be read.""" try: chunk = self.recv(self.ac_in_buffer_size) except RetryError: pass except socket.error: self.handle_error() else: self.tot_bytes_received += len(chunk) if not chunk: self.transfer_finished = True # self.close() # <-- asyncore.recv() already do that... return if self._data_wrapper is not None: chunk = self._data_wrapper(chunk) try: self.file_obj.write(chunk) except OSError as err: raise _FileReadWriteError(err) handle_read_event = handle_read # small speedup def readable(self): """Predicate for inclusion in the readable for select().""" # It the channel is not supposed to be receiving but yet it's # in the list of readable events, that means it has been # disconnected, in which case we explicitly close() it. # This is necessary as differently from FTPHandler this channel # is not supposed to be readable/writable at first, meaning the # upper IOLoop might end up calling readable() repeatedly, # hogging CPU resources. if not self.receive and not self._initialized: return self.close() return self.receive def writable(self): """Predicate for inclusion in the writable for select().""" return not self.receive and asynchat.async_chat.writable(self) def handle_timeout(self): """Called cyclically to check if data trasfer is stalling with no progress in which case the client is kicked off. """ if self.get_transmitted_bytes() > self._lastdata: self._lastdata = self.get_transmitted_bytes() else: msg = "Data connection timed out." self._resp = ("421 " + msg, logger.info) self.close() self.cmd_channel.close_when_done() def handle_error(self): """Called when an exception is raised and not otherwise handled.""" try: raise # an error could occur in case we fail reading / writing # from / to file (e.g. file system gets full) except _FileReadWriteError as err: error = _strerror(err.errno) except Exception: # some other exception occurred; we don't want to provide # confidential error messages self.log_exception(self) error = "Internal error" try: self._resp = ("426 %s; transfer aborted." % error, logger.warning) self.close() except Exception: logger.critical(traceback.format_exc()) def handle_close(self): """Called when the socket is closed.""" # If we used channel for receiving we assume that transfer is # finished when client closes the connection, if we used channel # for sending we have to check that all data has been sent # (responding with 226) or not (responding with 426). # In both cases handle_close() is automatically called by the # underlying asynchat module. if not self._closed: if self.receive: self.transfer_finished = True else: self.transfer_finished = len(self.producer_fifo) == 0 try: if self.transfer_finished: self._resp = ("226 Transfer complete.", logger.debug) else: tot_bytes = self.get_transmitted_bytes() self._resp = ("426 Transfer aborted; %d bytes transmitted." % tot_bytes, logger.debug) finally: self.close() def close(self): """Close the data channel, first attempting to close any remaining file handles.""" debug("call: close()", inst=self) if not self._closed: # RFC-959 says we must close the connection before replying AsyncChat.close(self) # Close file object before responding successfully to client if self.file_obj is not None and not self.file_obj.closed: self.file_obj.close() if self._resp: self.cmd_channel.respond(self._resp[0], logfun=self._resp[1]) if self._idler is not None and not self._idler.cancelled: self._idler.cancel() if self.file_obj is not None: filename = self.file_obj.name elapsed_time = round(self.get_elapsed_time(), 3) self.cmd_channel.log_transfer( cmd=self.cmd, filename=self.file_obj.name, receive=self.receive, completed=self.transfer_finished, elapsed=elapsed_time, bytes=self.get_transmitted_bytes()) if self.transfer_finished: if self.receive: self.cmd_channel.on_file_received(filename) else: self.cmd_channel.on_file_sent(filename) else: if self.receive: self.cmd_channel.on_incomplete_file_received(filename) else: self.cmd_channel.on_incomplete_file_sent(filename) self.cmd_channel._on_dtp_close() # dirty hack in order to turn AsyncChat into a new style class in # python 2.x so that we can use super() if PY3: class _AsyncChatNewStyle(AsyncChat): pass else: class _AsyncChatNewStyle(object, AsyncChat): def __init__(self, *args, **kwargs): super(object, self).__init__(*args, **kwargs) # bypass object class ThrottledDTPHandler(_AsyncChatNewStyle, DTPHandler): """A DTPHandler subclass which wraps sending and receiving in a data counter and temporarily "sleeps" the channel so that you burst to no more than x Kb/sec average. - (int) read_limit: the maximum number of bytes to read (receive) in one second (defaults to 0 == no limit). - (int) write_limit: the maximum number of bytes to write (send) in one second (defaults to 0 == no limit). - (bool) auto_sized_buffers: this option only applies when read and/or write limits are specified. When enabled it bumps down the data buffer sizes so that they are never greater than read and write limits which results in a less bursty and smoother throughput (default: True). """ read_limit = 0 write_limit = 0 auto_sized_buffers = True def __init__(self, sock, cmd_channel): super(ThrottledDTPHandler, self).__init__(sock, cmd_channel) self._timenext = 0 self._datacount = 0 self.sleeping = False self._throttler = None if self.auto_sized_buffers: if self.read_limit: while self.ac_in_buffer_size > self.read_limit: self.ac_in_buffer_size /= 2 if self.write_limit: while self.ac_out_buffer_size > self.write_limit: self.ac_out_buffer_size /= 2 self.ac_in_buffer_size = int(self.ac_in_buffer_size) self.ac_out_buffer_size = int(self.ac_out_buffer_size) def __repr__(self): return DTPHandler.__repr__(self) def use_sendfile(self): return False def recv(self, buffer_size): chunk = super(ThrottledDTPHandler, self).recv(buffer_size) if self.read_limit: self._throttle_bandwidth(len(chunk), self.read_limit) return chunk def send(self, data): num_sent = super(ThrottledDTPHandler, self).send(data) if self.write_limit: self._throttle_bandwidth(num_sent, self.write_limit) return num_sent def _cancel_throttler(self): if self._throttler is not None and not self._throttler.cancelled: self._throttler.cancel() def _throttle_bandwidth(self, len_chunk, max_speed): """A method which counts data transmitted so that you burst to no more than x Kb/sec average. """ self._datacount += len_chunk if self._datacount >= max_speed: self._datacount = 0 now = timer() sleepfor = (self._timenext - now) * 2 if sleepfor > 0: # we've passed bandwidth limits def unsleep(): if self.receive: event = self.ioloop.READ else: event = self.ioloop.WRITE self.add_channel(events=event) self.del_channel() self._cancel_throttler() self._throttler = self.ioloop.call_later( sleepfor, unsleep, _errback=self.handle_error) self._timenext = now + 1 def close(self): self._cancel_throttler() super(ThrottledDTPHandler, self).close() # --- producers class FileProducer(object): """Producer wrapper for file[-like] objects.""" buffer_size = 65536 def __init__(self, file, type): """Initialize the producer with a data_wrapper appropriate to TYPE. - (file) file: the file[-like] object. - (str) type: the current TYPE, 'a' (ASCII) or 'i' (binary). """ self.file = file self.type = type if type == 'a' and os.linesep != '\r\n': self._data_wrapper = lambda x: x.replace(b(os.linesep), b'\r\n') else: self._data_wrapper = None def more(self): """Attempt a chunk of data of size self.buffer_size.""" try: data = self.file.read(self.buffer_size) except OSError as err: raise _FileReadWriteError(err) else: if self._data_wrapper is not None: data = self._data_wrapper(data) return data class BufferedIteratorProducer(object): """Producer for iterator objects with buffer capabilities.""" # how many times iterator.next() will be called before # returning some data loops = 20 def __init__(self, iterator): self.iterator = iterator def more(self): """Attempt a chunk of data from iterator by calling its next() method different times. """ buffer = [] for x in xrange(self.loops): try: buffer.append(next(self.iterator)) except StopIteration: break return b''.join(buffer) # --- FTP class FTPHandler(AsyncChat): """Implements the FTP server Protocol Interpreter (see RFC-959), handling commands received from the client on the control channel. All relevant session information is stored in class attributes reproduced below and can be modified before instantiating this class. - (int) timeout: The timeout which is the maximum time a remote client may spend between FTP commands. If the timeout triggers, the remote client will be kicked off. Defaults to 300 seconds. - (str) banner: the string sent when client connects. - (int) max_login_attempts: the maximum number of wrong authentications before disconnecting the client (default 3). - (bool)permit_foreign_addresses: FTP site-to-site transfer feature: also referenced as "FXP" it permits for transferring a file between two remote FTP servers without the transfer going through the client's host (not recommended for security reasons as described in RFC-2577). Having this attribute set to False means that all data connections from/to remote IP addresses which do not match the client's IP address will be dropped (defualt False). - (bool) permit_privileged_ports: set to True if you want to permit active data connections (PORT) over privileged ports (not recommended, defaulting to False). - (str) masquerade_address: the "masqueraded" IP address to provide along PASV reply when pyftpdlib is running behind a NAT or other types of gateways. When configured pyftpdlib will hide its local address and instead use the public address of your NAT (default None). - (dict) masquerade_address_map: in case the server has multiple IP addresses which are all behind a NAT router, you may wish to specify individual masquerade_addresses for each of them. The map expects a dictionary containing private IP addresses as keys, and their corresponding public (masquerade) addresses as values. - (list) passive_ports: what ports the ftpd will use for its passive data transfers. Value expected is a list of integers (e.g. range(60000, 65535)). When configured pyftpdlib will no longer use kernel-assigned random ports (default None). - (bool) use_gmt_times: when True causes the server to report all ls and MDTM times in GMT and not local time (default True). - (bool) use_sendfile: when True uses sendfile() system call to send a file resulting in faster uploads (from server to client). Works on UNIX only and requires pysendfile module to be installed separately: https://github.com/giampaolo/pysendfile/ Automatically defaults to True if pysendfile module is installed. - (bool) tcp_no_delay: controls the use of the TCP_NODELAY socket option which disables the Nagle algorithm resulting in significantly better performances (default True on all systems where it is supported). - (str) unicode_errors: the error handler passed to ''.encode() and ''.decode(): http://docs.python.org/library/stdtypes.html#str.decode (detaults to 'replace'). - (str) log_prefix: the prefix string preceding any log line; all instance attributes can be used as arguments. All relevant instance attributes initialized when client connects are reproduced below. You may be interested in them in case you want to subclass the original FTPHandler. - (bool) authenticated: True if client authenticated himself. - (str) username: the name of the connected user (if any). - (int) attempted_logins: number of currently attempted logins. - (str) current_type: the current transfer type (default "a") - (int) af: the connection's address family (IPv4/IPv6) - (instance) server: the FTPServer class instance. - (instance) data_channel: the data channel instance (if any). """ # these are overridable defaults # default classes authorizer = DummyAuthorizer() active_dtp = ActiveDTP passive_dtp = PassiveDTP dtp_handler = DTPHandler abstracted_fs = AbstractedFS proto_cmds = proto_cmds # session attributes (explained in the docstring) timeout = 300 banner = "pyftpdlib %s ready." % __ver__ max_login_attempts = 3 permit_foreign_addresses = False permit_privileged_ports = False masquerade_address = None masquerade_address_map = {} passive_ports = None use_gmt_times = True use_sendfile = sendfile is not None tcp_no_delay = hasattr(socket, "TCP_NODELAY") unicode_errors = 'replace' log_prefix = '%(remote_ip)s:%(remote_port)s-[%(username)s]' auth_failed_timeout = 3 def __init__(self, conn, server, ioloop=None): """Initialize the command channel. - (instance) conn: the socket object instance of the newly established connection. - (instance) server: the ftp server class instance. """ # public session attributes self.server = server self.fs = None self.authenticated = False self.username = "" self.password = "" self.attempted_logins = 0 self.data_channel = None self.remote_ip = "" self.remote_port = "" self.started = time.time() # private session attributes self._last_response = "" self._current_type = 'a' self._restart_position = 0 self._quit_pending = False self._in_buffer = [] self._in_buffer_len = 0 self._epsvall = False self._dtp_acceptor = None self._dtp_connector = None self._in_dtp_queue = None self._out_dtp_queue = None self._extra_feats = [] self._current_facts = ['type', 'perm', 'size', 'modify'] self._rnfr = None self._idler = None self._log_debug = logging.getLogger('pyftpdlib').getEffectiveLevel() \ <= logging.DEBUG if os.name == 'posix': self._current_facts.append('unique') self._available_facts = self._current_facts[:] if pwd and grp: self._available_facts += ['unix.mode', 'unix.uid', 'unix.gid'] if os.name == 'nt': self._available_facts.append('create') try: AsyncChat.__init__(self, conn, ioloop=ioloop) except socket.error as err: # if we get an exception here we want the dispatcher # instance to set socket attribute before closing, see: # https://github.com/giampaolo/pyftpdlib/issues/188 AsyncChat.__init__(self, socket.socket(), ioloop=ioloop) self.close() debug("call: FTPHandler.__init__, err %r" % err, self) if err.errno == errno.EINVAL: # https://github.com/giampaolo/pyftpdlib/issues/143 return self.handle_error() return self.set_terminator(b"\r\n") # connection properties try: self.remote_ip, self.remote_port = self.socket.getpeername()[:2] except socket.error as err: debug("call: FTPHandler.__init__, err on getpeername() %r" % err, self) # A race condition may occur if the other end is closing # before we can get the peername, hence ENOTCONN (see issue # #100) while EINVAL can occur on OSX (see issue #143). self.connected = False if err.errno in (errno.ENOTCONN, errno.EINVAL): self.close() else: self.handle_error() return else: self.log("FTP session opened (connect)") # try to handle urgent data inline try: self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_OOBINLINE, 1) except socket.error as err: debug("call: FTPHandler.__init__, err on SO_OOBINLINE %r" % err, self) # disable Nagle algorithm for the control socket only, resulting # in significantly better performances if self.tcp_no_delay: try: self.socket.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) except socket.error as err: debug( "call: FTPHandler.__init__, err on TCP_NODELAY %r" % err, self) # remove this instance from IOLoop's socket_map if not self.connected: self.close() return if self.timeout: self._idler = self.ioloop.call_later( self.timeout, self.handle_timeout, _errback=self.handle_error) def get_repr_info(self, as_str=False, extra_info={}): info = OrderedDict() info['id'] = id(self) info['addr'] = "%s:%s" % (self.remote_ip, self.remote_port) if _is_ssl_sock(self.socket): info['ssl'] = True if self.username: info['user'] = self.username # If threads are involved sometimes "self" may be None (?!?). dc = getattr(self, 'data_channel', None) if dc is not None: if _is_ssl_sock(dc.socket): info['ssl-data'] = True if dc.file_obj: if self.data_channel.receive: info['sending-file'] = dc.file_obj if dc.use_sendfile(): info['use-sendfile(2)'] = True else: info['receiving-file'] = dc.file_obj info['bytes-trans'] = dc.get_transmitted_bytes() info.update(extra_info) if as_str: return ', '.join(['%s=%r' % (k, v) for (k, v) in info.items()]) return info def __repr__(self): return '<%s(%s)>' % (self.__class__.__name__, self.get_repr_info(True)) __str__ = __repr__ def handle(self): """Return a 220 'ready' response to the client over the command channel. """ self.on_connect() if not self._closed and not self._closing: if len(self.banner) <= 75: self.respond("220 %s" % str(self.banner)) else: self.push('220-%s\r\n' % str(self.banner)) self.respond('220 ') def handle_max_cons(self): """Called when limit for maximum number of connections is reached.""" msg = "421 Too many connections. Service temporarily unavailable." self.respond_w_warning(msg) # If self.push is used, data could not be sent immediately in # which case a new "loop" will occur exposing us to the risk of # accepting new connections. Since this could cause asyncore to # run out of fds in case we're using select() on Windows we # immediately close the channel by using close() instead of # close_when_done(). If data has not been sent yet client will # be silently disconnected. self.close() def handle_max_cons_per_ip(self): """Called when too many clients are connected from the same IP.""" msg = "421 Too many connections from the same IP address." self.respond_w_warning(msg) self.close_when_done() def handle_timeout(self): """Called when client does not send any command within the time specified in attribute.""" msg = "Control connection timed out." self.respond("421 " + msg, logfun=logger.info) self.close_when_done() # --- asyncore / asynchat overridden methods def readable(self): # Checking for self.connected seems to be necessary as per: # https://github.com/giampaolo/pyftpdlib/issues/188#c18 # In contrast to DTPHandler, here we are not interested in # attempting to receive any further data from a closed socket. return self.connected and AsyncChat.readable(self) def writable(self): return self.connected and AsyncChat.writable(self) def collect_incoming_data(self, data): """Read incoming data and append to the input buffer.""" self._in_buffer.append(data) self._in_buffer_len += len(data) # Flush buffer if it gets too long (possible DoS attacks). # RFC-959 specifies that a 500 response could be given in # such cases buflimit = 2048 if self._in_buffer_len > buflimit: self.respond_w_warning('500 Command too long.') self._in_buffer = [] self._in_buffer_len = 0 def decode(self, bytes): return bytes.decode('utf8', self.unicode_errors) def found_terminator(self): r"""Called when the incoming data stream matches the \r\n terminator. """ if self._idler is not None and not self._idler.cancelled: self._idler.reset() line = b''.join(self._in_buffer) try: line = self.decode(line) except UnicodeDecodeError: # By default we'll never get here as we replace errors # but user might want to override this behavior. # RFC-2640 doesn't mention what to do in this case so # we'll just return 501 (bad arg). return self.respond("501 Can't decode command.") self._in_buffer = [] self._in_buffer_len = 0 cmd = line.split(' ')[0].upper() arg = line[len(cmd) + 1:] try: self.pre_process_command(line, cmd, arg) except UnicodeEncodeError: self.respond("501 can't decode path (server filesystem encoding " "is %s)" % sys.getfilesystemencoding()) def pre_process_command(self, line, cmd, arg): kwargs = {} if cmd == "SITE" and arg: cmd = "SITE %s" % arg.split(' ')[0].upper() arg = line[len(cmd) + 1:] if cmd != 'PASS': self.logline("<- %s" % line) else: self.logline("<- %s %s" % (line.split(' ')[0], '*' * 6)) # Recognize those commands having a "special semantic". They # should be sent by following the RFC-959 procedure of sending # Telnet IP/Synch sequence (chr 242 and 255) as OOB data but # since many ftp clients don't do it correctly we check the # last 4 characters only. if cmd not in self.proto_cmds: if cmd[-4:] in ('ABOR', 'STAT', 'QUIT'): cmd = cmd[-4:] else: msg = 'Command "%s" not understood.' % cmd self.respond('500 ' + msg) if cmd: self.log_cmd(cmd, arg, 500, msg) return if not arg and self.proto_cmds[cmd]['arg'] == True: # NOQA msg = "Syntax error: command needs an argument." self.respond("501 " + msg) self.log_cmd(cmd, "", 501, msg) return if arg and self.proto_cmds[cmd]['arg'] == False: # NOQA msg = "Syntax error: command does not accept arguments." self.respond("501 " + msg) self.log_cmd(cmd, arg, 501, msg) return if not self.authenticated: if self.proto_cmds[cmd]['auth'] or (cmd == 'STAT' and arg): msg = "Log in with USER and PASS first." self.respond("530 " + msg) self.log_cmd(cmd, arg, 530, msg) else: # call the proper ftp_* method self.process_command(cmd, arg) return else: if (cmd == 'STAT') and not arg: self.ftp_STAT(u('')) return # for file-system related commands check whether real path # destination is valid if self.proto_cmds[cmd]['perm'] and (cmd != 'STOU'): if cmd in ('CWD', 'XCWD'): arg = self.fs.ftp2fs(arg or u('/')) elif cmd in ('CDUP', 'XCUP'): arg = self.fs.ftp2fs(u('..')) elif cmd == 'LIST': if arg.lower() in ('-a', '-l', '-al', '-la'): arg = self.fs.ftp2fs(self.fs.cwd) else: arg = self.fs.ftp2fs(arg or self.fs.cwd) elif cmd == 'STAT': if glob.has_magic(arg): msg = 'Globbing not supported.' self.respond('550 ' + msg) self.log_cmd(cmd, arg, 550, msg) return arg = self.fs.ftp2fs(arg or self.fs.cwd) elif cmd == 'SITE CHMOD': if ' ' not in arg: msg = "Syntax error: command needs two arguments." self.respond("501 " + msg) self.log_cmd(cmd, "", 501, msg) return else: mode, arg = arg.split(' ', 1) arg = self.fs.ftp2fs(arg) kwargs = dict(mode=mode) elif cmd == 'MFMT': if ' ' not in arg: msg = "Syntax error: command needs two arguments." self.respond("501 " + msg) self.log_cmd(cmd, "", 501, msg) return else: timeval, arg = arg.split(' ', 1) arg = self.fs.ftp2fs(arg) kwargs = dict(timeval=timeval) else: # LIST, NLST, MLSD, MLST arg = self.fs.ftp2fs(arg or self.fs.cwd) if not self.fs.validpath(arg): line = self.fs.fs2ftp(arg) msg = '"%s" points to a path which is outside ' \ "the user's root directory" % line self.respond("550 %s." % msg) self.log_cmd(cmd, arg, 550, msg) return # check permission perm = self.proto_cmds[cmd]['perm'] if perm is not None and cmd != 'STOU': if not self.authorizer.has_perm(self.username, perm, arg): msg = "Not enough privileges." self.respond("550 " + msg) self.log_cmd(cmd, arg, 550, msg) return # call the proper ftp_* method self.process_command(cmd, arg, **kwargs) def process_command(self, cmd, *args, **kwargs): """Process command by calling the corresponding ftp_* class method (e.g. for received command "MKD pathname", ftp_MKD() method is called with "pathname" as the argument). """ if self._closed: return self._last_response = "" method = getattr(self, 'ftp_' + cmd.replace(' ', '_')) method(*args, **kwargs) if self._last_response: code = int(self._last_response[:3]) resp = self._last_response[4:] self.log_cmd(cmd, args[0], code, resp) def handle_error(self): try: self.log_exception(self) self.close() except Exception: logger.critical(traceback.format_exc()) def handle_close(self): self.close() def close(self): """Close the current channel disconnecting the client.""" debug("call: close()", inst=self) if not self._closed: AsyncChat.close(self) self._shutdown_connecting_dtp() if self.data_channel is not None: self.data_channel.close() del self.data_channel if self._out_dtp_queue is not None: file = self._out_dtp_queue[2] if file is not None: file.close() if self._in_dtp_queue is not None: file = self._in_dtp_queue[0] if file is not None: file.close() del self._out_dtp_queue del self._in_dtp_queue if self._idler is not None and not self._idler.cancelled: self._idler.cancel() # remove client IP address from ip map if self.remote_ip in self.server.ip_map: self.server.ip_map.remove(self.remote_ip) if self.fs is not None: self.fs.cmd_channel = None self.fs = None self.log("FTP session closed (disconnect).") # Having self.remote_ip not set means that no connection # actually took place, hence we're not interested in # invoking the callback. if self.remote_ip: self.ioloop.call_later(0, self.on_disconnect, _errback=self.handle_error) def _shutdown_connecting_dtp(self): """Close any ActiveDTP or PassiveDTP instance waiting to establish a connection (passive or active). """ if self._dtp_acceptor is not None: self._dtp_acceptor.close() self._dtp_acceptor = None if self._dtp_connector is not None: self._dtp_connector.close() self._dtp_connector = None # --- public callbacks # Note: to run a time consuming task make sure to use a separate # process or thread (see FAQs). def on_connect(self): """Called when client connects, *before* sending the initial 220 reply. """ def on_disconnect(self): """Called when connection is closed.""" def on_login(self, username): """Called on user login.""" def on_login_failed(self, username, password): """Called on failed login attempt. At this point client might have already been disconnected if it failed too many times. """ def on_logout(self, username): """Called when user "cleanly" logs out due to QUIT or USER issued twice (re-login). This is not called if the connection is simply closed by client. """ def on_file_sent(self, file): """Called every time a file has been succesfully sent. "file" is the absolute name of the file just being sent. """ def on_file_received(self, file): """Called every time a file has been succesfully received. "file" is the absolute name of the file just being received. """ def on_incomplete_file_sent(self, file): """Called every time a file has not been entirely sent. (e.g. ABOR during transfer or client disconnected). "file" is the absolute name of that file. """ def on_incomplete_file_received(self, file): """Called every time a file has not been entirely received (e.g. ABOR during transfer or client disconnected). "file" is the absolute name of that file. """ # --- internal callbacks def _on_dtp_connection(self): """Called every time data channel connects, either active or passive. Incoming and outgoing queues are checked for pending data. If outbound data is pending, it is pushed into the data channel. If awaiting inbound data, the data channel is enabled for receiving. """ # Close accepting DTP only. By closing ActiveDTP DTPHandler # would receive a closed socket object. # self._shutdown_connecting_dtp() if self._dtp_acceptor is not None: self._dtp_acceptor.close() self._dtp_acceptor = None # stop the idle timer as long as the data transfer is not finished if self._idler is not None and not self._idler.cancelled: self._idler.cancel() # check for data to send if self._out_dtp_queue is not None: data, isproducer, file, cmd = self._out_dtp_queue self._out_dtp_queue = None self.data_channel.cmd = cmd if file: self.data_channel.file_obj = file try: if not isproducer: self.data_channel.push(data) else: self.data_channel.push_with_producer(data) if self.data_channel is not None: self.data_channel.close_when_done() except Exception: # dealing with this exception is up to DTP (see bug #84) self.data_channel.handle_error() # check for data to receive elif self._in_dtp_queue is not None: file, cmd = self._in_dtp_queue self.data_channel.file_obj = file self._in_dtp_queue = None self.data_channel.enable_receiving(self._current_type, cmd) def _on_dtp_close(self): """Called every time the data channel is closed.""" self.data_channel = None if self._quit_pending: self.close() elif self.timeout: # data transfer finished, restart the idle timer if self._idler is not None and not self._idler.cancelled: self._idler.cancel() self._idler = self.ioloop.call_later( self.timeout, self.handle_timeout, _errback=self.handle_error) # --- utility def push(self, s): asynchat.async_chat.push(self, s.encode('utf8')) def respond(self, resp, logfun=logger.debug): """Send a response to the client using the command channel.""" self._last_response = resp self.push(resp + '\r\n') if self._log_debug: self.logline('-> %s' % resp, logfun=logfun) else: self.log(resp[4:], logfun=logfun) def respond_w_warning(self, resp): self.respond(resp, logfun=logger.warning) def push_dtp_data(self, data, isproducer=False, file=None, cmd=None): """Pushes data into the data channel. It is usually called for those commands requiring some data to be sent over the data channel (e.g. RETR). If data channel does not exist yet, it queues the data to send later; data will then be pushed into data channel when _on_dtp_connection() will be called. - (str/classobj) data: the data to send which may be a string or a producer object). - (bool) isproducer: whether treat data as a producer. - (file) file: the file[-like] object to send (if any). """ if self.data_channel is not None: self.respond( "125 Data connection already open. Transfer starting.") if file: self.data_channel.file_obj = file try: if not isproducer: self.data_channel.push(data) else: self.data_channel.push_with_producer(data) if self.data_channel is not None: self.data_channel.cmd = cmd self.data_channel.close_when_done() except Exception: # dealing with this exception is up to DTP (see bug #84) self.data_channel.handle_error() else: self.respond( "150 File status okay. About to open data connection.") self._out_dtp_queue = (data, isproducer, file, cmd) def flush_account(self): """Flush account information by clearing attributes that need to be reset on a REIN or new USER command. """ self._shutdown_connecting_dtp() # if there's a transfer in progress RFC-959 states we are # supposed to let it finish if self.data_channel is not None: if not self.data_channel.transfer_in_progress(): self.data_channel.close() self.data_channel = None username = self.username if self.authenticated and username: self.on_logout(username) self.authenticated = False self.username = "" self.password = "" self.attempted_logins = 0 self._current_type = 'a' self._restart_position = 0 self._quit_pending = False self._in_dtp_queue = None self._rnfr = None self._out_dtp_queue = None def run_as_current_user(self, function, *args, **kwargs): """Execute a function impersonating the current logged-in user.""" self.authorizer.impersonate_user(self.username, self.password) try: return function(*args, **kwargs) finally: self.authorizer.terminate_impersonation(self.username) # --- logging wrappers # this is defined earlier # log_prefix = '%(remote_ip)s:%(remote_port)s-[%(username)s]' def log(self, msg, logfun=logger.info): """Log a message, including additional identifying session data.""" prefix = self.log_prefix % self.__dict__ logfun("%s %s" % (prefix, msg)) def logline(self, msg, logfun=logger.debug): """Log a line including additional indentifying session data. By default this is disabled unless logging level == DEBUG. """ if self._log_debug: prefix = self.log_prefix % self.__dict__ logfun("%s %s" % (prefix, msg)) def logerror(self, msg): """Log an error including additional indentifying session data.""" prefix = self.log_prefix % self.__dict__ logger.error("%s %s" % (prefix, msg)) def log_exception(self, instance): """Log an unhandled exception. 'instance' is the instance where the exception was generated. """ logger.exception("unhandled exception in instance %r", instance) # the list of commands which gets logged when logging level # is >= logging.INFO log_cmds_list = ["DELE", "RNFR", "RNTO", "MKD", "RMD", "CWD", "XMKD", "XRMD", "XCWD", "REIN", "SITE CHMOD", "MFMT"] def log_cmd(self, cmd, arg, respcode, respstr): """Log commands and responses in a standardized format. This is disabled in case the logging level is set to DEBUG. - (str) cmd: the command sent by client - (str) arg: the command argument sent by client. For filesystem commands such as DELE, MKD, etc. this is already represented as an absolute real filesystem path like "/home/user/file.ext". - (int) respcode: the response code as being sent by server. Response codes starting with 4xx or 5xx are returned if the command has been rejected for some reason. - (str) respstr: the response string as being sent by server. By default only DELE, RMD, RNTO, MKD, CWD, ABOR, REIN, SITE CHMOD commands are logged and the output is redirected to self.log method. Can be overridden to provide alternate formats or to log further commands. """ if not self._log_debug and cmd in self.log_cmds_list: line = '%s %s' % (' '.join([cmd, arg]).strip(), respcode) if str(respcode)[0] in ('4', '5'): line += ' %r' % respstr self.log(line) def log_transfer(self, cmd, filename, receive, completed, elapsed, bytes): """Log all file transfers in a standardized format. - (str) cmd: the original command who caused the tranfer. - (str) filename: the absolutized name of the file on disk. - (bool) receive: True if the transfer was used for client uploading (STOR, STOU, APPE), False otherwise (RETR). - (bool) completed: True if the file has been entirely sent, else False. - (float) elapsed: transfer elapsed time in seconds. - (int) bytes: number of bytes transmitted. """ line = '%s %s completed=%s bytes=%s seconds=%s' % \ (cmd, filename, completed and 1 or 0, bytes, elapsed) self.log(line) # --- connection def _make_eport(self, ip, port): """Establish an active data channel with remote client which issued a PORT or EPRT command. """ # FTP bounce attacks protection: according to RFC-2577 it's # recommended to reject PORT if IP address specified in it # does not match client IP address. remote_ip = self.remote_ip if remote_ip.startswith('::ffff:'): # In this scenario, the server has an IPv6 socket, but # the remote client is using IPv4 and its address is # represented as an IPv4-mapped IPv6 address which # looks like this ::ffff:151.12.5.65, see: # http://en.wikipedia.org/wiki/IPv6#IPv4-mapped_addresses # http://tools.ietf.org/html/rfc3493.html#section-3.7 # We truncate the first bytes to make it look like a # common IPv4 address. remote_ip = remote_ip[7:] if not self.permit_foreign_addresses and ip != remote_ip: msg = "501 Rejected data connection to foreign address %s:%s." \ % (ip, port) self.respond_w_warning(msg) return # ...another RFC-2577 recommendation is rejecting connections # to privileged ports (< 1024) for security reasons. if not self.permit_privileged_ports and port < 1024: msg = '501 PORT against the privileged port "%s" refused.' % port self.respond_w_warning(msg) return # close establishing DTP instances, if any self._shutdown_connecting_dtp() if self.data_channel is not None: self.data_channel.close() self.data_channel = None # make sure we are not hitting the max connections limit if not self.server._accept_new_cons(): msg = "425 Too many connections. Can't open data channel." self.respond_w_warning(msg) return # open data channel self._dtp_connector = self.active_dtp(ip, port, self) def _make_epasv(self, extmode=False): """Initialize a passive data channel with remote client which issued a PASV or EPSV command. If extmode argument is True we assume that client issued EPSV in which case extended passive mode will be used (see RFC-2428). """ # close establishing DTP instances, if any self._shutdown_connecting_dtp() # close established data connections, if any if self.data_channel is not None: self.data_channel.close() self.data_channel = None # make sure we are not hitting the max connections limit if not self.server._accept_new_cons(): msg = "425 Too many connections. Can't open data channel." self.respond_w_warning(msg) return # open data channel self._dtp_acceptor = self.passive_dtp(self, extmode) def ftp_PORT(self, line): """Start an active data channel by using IPv4.""" if self._epsvall: self.respond("501 PORT not allowed after EPSV ALL.") return # Parse PORT request for getting IP and PORT. # Request comes in as: # > h1,h2,h3,h4,p1,p2 # ...where the client's IP address is h1.h2.h3.h4 and the TCP # port number is (p1 * 256) + p2. try: addr = list(map(int, line.split(','))) if len(addr) != 6: raise ValueError for x in addr[:4]: if not 0 <= x <= 255: raise ValueError ip = '%d.%d.%d.%d' % tuple(addr[:4]) port = (addr[4] * 256) + addr[5] if not 0 <= port <= 65535: raise ValueError except (ValueError, OverflowError): self.respond("501 Invalid PORT format.") return self._make_eport(ip, port) def ftp_EPRT(self, line): """Start an active data channel by choosing the network protocol to use (IPv4/IPv6) as defined in RFC-2428. """ if self._epsvall: self.respond("501 EPRT not allowed after EPSV ALL.") return # Parse EPRT request for getting protocol, IP and PORT. # Request comes in as: # protoipport # ...where is an arbitrary delimiter character (usually "|") and # is the network protocol to use (1 for IPv4, 2 for IPv6). try: af, ip, port = line.split(line[0])[1:-1] port = int(port) if not 0 <= port <= 65535: raise ValueError except (ValueError, IndexError, OverflowError): self.respond("501 Invalid EPRT format.") return if af == "1": # test if AF_INET6 and IPV6_V6ONLY if (self.socket.family == socket.AF_INET6 and not SUPPORTS_HYBRID_IPV6): self.respond('522 Network protocol not supported (use 2).') else: try: octs = list(map(int, ip.split('.'))) if len(octs) != 4: raise ValueError for x in octs: if not 0 <= x <= 255: raise ValueError except (ValueError, OverflowError): self.respond("501 Invalid EPRT format.") else: self._make_eport(ip, port) elif af == "2": if self.socket.family == socket.AF_INET: self.respond('522 Network protocol not supported (use 1).') else: self._make_eport(ip, port) else: if self.socket.family == socket.AF_INET: self.respond('501 Unknown network protocol (use 1).') else: self.respond('501 Unknown network protocol (use 2).') def ftp_PASV(self, line): """Start a passive data channel by using IPv4.""" if self._epsvall: self.respond("501 PASV not allowed after EPSV ALL.") return self._make_epasv(extmode=False) def ftp_EPSV(self, line): """Start a passive data channel by using IPv4 or IPv6 as defined in RFC-2428. """ # RFC-2428 specifies that if an optional parameter is given, # we have to determine the address family from that otherwise # use the same address family used on the control connection. # In such a scenario a client may use IPv4 on the control channel # and choose to use IPv6 for the data channel. # But how could we use IPv6 on the data channel without knowing # which IPv6 address to use for binding the socket? # Unfortunately RFC-2428 does not provide satisfing information # on how to do that. The assumption is that we don't have any way # to know wich address to use, hence we just use the same address # family used on the control connection. if not line: self._make_epasv(extmode=True) # IPv4 elif line == "1": if self.socket.family != socket.AF_INET: self.respond('522 Network protocol not supported (use 2).') else: self._make_epasv(extmode=True) # IPv6 elif line == "2": if self.socket.family == socket.AF_INET: self.respond('522 Network protocol not supported (use 1).') else: self._make_epasv(extmode=True) elif line.lower() == 'all': self._epsvall = True self.respond( '220 Other commands other than EPSV are now disabled.') else: if self.socket.family == socket.AF_INET: self.respond('501 Unknown network protocol (use 1).') else: self.respond('501 Unknown network protocol (use 2).') def ftp_QUIT(self, line): """Quit the current session disconnecting the client.""" if self.authenticated: msg_quit = self.authorizer.get_msg_quit(self.username) else: msg_quit = "Goodbye." if len(msg_quit) <= 75: self.respond("221 %s" % msg_quit) else: self.push("221-%s\r\n" % msg_quit) self.respond("221 ") # From RFC-959: # If file transfer is in progress, the connection must remain # open for result response and the server will then close it. # We also stop responding to any further command. if self.data_channel: self._quit_pending = True self.del_channel() else: self._shutdown_connecting_dtp() self.close_when_done() if self.authenticated and self.username: self.on_logout(self.username) # --- data transferring def ftp_LIST(self, path): """Return a list of files in the specified directory to the client. On success return the directory path, else None. """ # - If no argument, fall back on cwd as default. # - Some older FTP clients erroneously issue /bin/ls-like LIST # formats in which case we fall back on cwd as default. try: isdir = self.fs.isdir(path) if isdir: listing = self.run_as_current_user(self.fs.listdir, path) if isinstance(listing, list): try: # RFC 959 recommends the listing to be sorted. listing.sort() except UnicodeDecodeError: # (Python 2 only) might happen on filesystem not # supporting UTF8 meaning os.listdir() returned a list # of mixed bytes and unicode strings: # http://goo.gl/6DLHD # http://bugs.python.org/issue683592 pass iterator = self.fs.format_list(path, listing) else: basedir, filename = os.path.split(path) self.fs.lstat(path) # raise exc in case of problems iterator = self.fs.format_list(basedir, [filename]) except (OSError, FilesystemError) as err: why = _strerror(err) self.respond('550 %s.' % why) else: producer = BufferedIteratorProducer(iterator) self.push_dtp_data(producer, isproducer=True, cmd="LIST") return path def ftp_NLST(self, path): """Return a list of files in the specified directory in a compact form to the client. On success return the directory path, else None. """ try: if self.fs.isdir(path): listing = list(self.run_as_current_user(self.fs.listdir, path)) else: # if path is a file we just list its name self.fs.lstat(path) # raise exc in case of problems listing = [os.path.basename(path)] except (OSError, FilesystemError) as err: self.respond('550 %s.' % _strerror(err)) else: data = '' if listing: try: listing.sort() except UnicodeDecodeError: # (Python 2 only) might happen on filesystem not # supporting UTF8 meaning os.listdir() returned a list # of mixed bytes and unicode strings: # http://goo.gl/6DLHD # http://bugs.python.org/issue683592 ls = [] for x in listing: if not isinstance(x, unicode): x = unicode(x, 'utf8') ls.append(x) listing = sorted(ls) data = '\r\n'.join(listing) + '\r\n' data = data.encode('utf8', self.unicode_errors) self.push_dtp_data(data, cmd="NLST") return path # --- MLST and MLSD commands # The MLST and MLSD commands are intended to standardize the file and # directory information returned by the server-FTP process. These # commands differ from the LIST command in that the format of the # replies is strictly defined although extensible. def ftp_MLST(self, path): """Return information about a pathname in a machine-processable form as defined in RFC-3659. On success return the path just listed, else None. """ line = self.fs.fs2ftp(path) basedir, basename = os.path.split(path) perms = self.authorizer.get_perms(self.username) try: iterator = self.run_as_current_user( self.fs.format_mlsx, basedir, [basename], perms, self._current_facts, ignore_err=False) data = b''.join(iterator) except (OSError, FilesystemError) as err: self.respond('550 %s.' % _strerror(err)) else: data = data.decode('utf8', self.unicode_errors) # since TVFS is supported (see RFC-3659 chapter 6), a fully # qualified pathname should be returned data = data.split(' ')[0] + ' %s\r\n' % line # response is expected on the command channel self.push('250-Listing "%s":\r\n' % line) # the fact set must be preceded by a space self.push(' ' + data) self.respond('250 End MLST.') return path def ftp_MLSD(self, path): """Return contents of a directory in a machine-processable form as defined in RFC-3659. On success return the path just listed, else None. """ # RFC-3659 requires 501 response code if path is not a directory if not self.fs.isdir(path): self.respond("501 No such directory.") return try: listing = self.run_as_current_user(self.fs.listdir, path) except (OSError, FilesystemError) as err: why = _strerror(err) self.respond('550 %s.' % why) else: perms = self.authorizer.get_perms(self.username) iterator = self.fs.format_mlsx(path, listing, perms, self._current_facts) producer = BufferedIteratorProducer(iterator) self.push_dtp_data(producer, isproducer=True, cmd="MLSD") return path def ftp_RETR(self, file): """Retrieve the specified file (transfer from the server to the client). On success return the file path else None. """ rest_pos = self._restart_position self._restart_position = 0 try: fd = self.run_as_current_user(self.fs.open, file, 'rb') except (EnvironmentError, FilesystemError) as err: why = _strerror(err) self.respond('550 %s.' % why) return try: if rest_pos: # Make sure that the requested offset is valid (within the # size of the file being resumed). # According to RFC-1123 a 554 reply may result in case that # the existing file cannot be repositioned as specified in # the REST. ok = 0 try: if rest_pos > self.fs.getsize(file): raise ValueError fd.seek(rest_pos) ok = 1 except ValueError: why = "Invalid REST parameter" except (EnvironmentError, FilesystemError) as err: why = _strerror(err) if not ok: fd.close() self.respond('554 %s' % why) return producer = FileProducer(fd, self._current_type) self.push_dtp_data(producer, isproducer=True, file=fd, cmd="RETR") return file except Exception: fd.close() raise def ftp_STOR(self, file, mode='w'): """Store a file (transfer from the client to the server). On success return the file path, else None. """ # A resume could occur in case of APPE or REST commands. # In that case we have to open file object in different ways: # STOR: mode = 'w' # APPE: mode = 'a' # REST: mode = 'r+' (to permit seeking on file object) if 'a' in mode: cmd = 'APPE' else: cmd = 'STOR' rest_pos = self._restart_position self._restart_position = 0 if rest_pos: mode = 'r+' try: fd = self.run_as_current_user(self.fs.open, file, mode + 'b') except (EnvironmentError, FilesystemError) as err: why = _strerror(err) self.respond('550 %s.' % why) return try: if rest_pos: # Make sure that the requested offset is valid (within the # size of the file being resumed). # According to RFC-1123 a 554 reply may result in case # that the existing file cannot be repositioned as # specified in the REST. ok = 0 try: if rest_pos > self.fs.getsize(file): raise ValueError fd.seek(rest_pos) ok = 1 except ValueError: why = "Invalid REST parameter" except (EnvironmentError, FilesystemError) as err: why = _strerror(err) if not ok: fd.close() self.respond('554 %s' % why) return if self.data_channel is not None: resp = "Data connection already open. Transfer starting." self.respond("125 " + resp) self.data_channel.file_obj = fd self.data_channel.enable_receiving(self._current_type, cmd) else: resp = "File status okay. About to open data connection." self.respond("150 " + resp) self._in_dtp_queue = (fd, cmd) return file except Exception: fd.close() raise def ftp_STOU(self, line): """Store a file on the server with a unique name. On success return the file path, else None. """ # Note 1: RFC-959 prohibited STOU parameters, but this # prohibition is obsolete. # Note 2: 250 response wanted by RFC-959 has been declared # incorrect in RFC-1123 that wants 125/150 instead. # Note 3: RFC-1123 also provided an exact output format # defined to be as follow: # > 125 FILE: pppp # ...where pppp represents the unique path name of the # file that will be written. # watch for STOU preceded by REST, which makes no sense. if self._restart_position: self.respond("450 Can't STOU while REST request is pending.") return if line: basedir, prefix = os.path.split(self.fs.ftp2fs(line)) prefix = prefix + '.' else: basedir = self.fs.ftp2fs(self.fs.cwd) prefix = 'ftpd.' try: fd = self.run_as_current_user(self.fs.mkstemp, prefix=prefix, dir=basedir) except (EnvironmentError, FilesystemError) as err: # likely, we hit the max number of retries to find out a # file with a unique name if getattr(err, "errno", -1) == errno.EEXIST: why = 'No usable unique file name found' # something else happened else: why = _strerror(err) self.respond("450 %s." % why) return try: if not self.authorizer.has_perm(self.username, 'w', fd.name): try: fd.close() self.run_as_current_user(self.fs.remove, fd.name) except (OSError, FilesystemError): pass self.respond("550 Not enough privileges.") return # now just acts like STOR except that restarting isn't allowed filename = os.path.basename(fd.name) if self.data_channel is not None: self.respond("125 FILE: %s" % filename) self.data_channel.file_obj = fd self.data_channel.enable_receiving(self._current_type, "STOU") else: self.respond("150 FILE: %s" % filename) self._in_dtp_queue = (fd, "STOU") return filename except Exception: fd.close() raise def ftp_APPE(self, file): """Append data to an existing file on the server. On success return the file path, else None. """ # watch for APPE preceded by REST, which makes no sense. if self._restart_position: self.respond("450 Can't APPE while REST request is pending.") else: return self.ftp_STOR(file, mode='a') def ftp_REST(self, line): """Restart a file transfer from a previous mark.""" if self._current_type == 'a': self.respond('501 Resuming transfers not allowed in ASCII mode.') return try: marker = int(line) if marker < 0: raise ValueError except (ValueError, OverflowError): self.respond("501 Invalid parameter.") else: self.respond("350 Restarting at position %s." % marker) self._restart_position = marker def ftp_ABOR(self, line): """Abort the current data transfer.""" # ABOR received while no data channel exists if (self._dtp_acceptor is None and self._dtp_connector is None and self.data_channel is None): self.respond("225 No transfer to abort.") return else: # a PASV or PORT was received but connection wasn't made yet if (self._dtp_acceptor is not None or self._dtp_connector is not None): self._shutdown_connecting_dtp() resp = "225 ABOR command successful; data channel closed." # If a data transfer is in progress the server must first # close the data connection, returning a 426 reply to # indicate that the transfer terminated abnormally, then it # must send a 226 reply, indicating that the abort command # was successfully processed. # If no data has been transmitted we just respond with 225 # indicating that no transfer was in progress. if self.data_channel is not None: if self.data_channel.transfer_in_progress(): self.data_channel.close() self.data_channel = None self.respond("426 Transfer aborted via ABOR.", logfun=logger.info) resp = "226 ABOR command successful." else: self.data_channel.close() self.data_channel = None resp = "225 ABOR command successful; data channel closed." self.respond(resp) # --- authentication def ftp_USER(self, line): """Set the username for the current session.""" # RFC-959 specifies a 530 response to the USER command if the # username is not valid. If the username is valid is required # ftpd returns a 331 response instead. In order to prevent a # malicious client from determining valid usernames on a server, # it is suggested by RFC-2577 that a server always return 331 to # the USER command and then reject the combination of username # and password for an invalid username when PASS is provided later. if not self.authenticated: self.respond('331 Username ok, send password.') else: # a new USER command could be entered at any point in order # to change the access control flushing any user, password, # and account information already supplied and beginning the # login sequence again. self.flush_account() msg = 'Previous account information was flushed' self.respond('331 %s, send password.' % msg, logfun=logger.info) self.username = line def handle_auth_failed(self, msg, password): def callback(username, password, msg): self.add_channel() if hasattr(self, '_closed') and not self._closed: self.attempted_logins += 1 if self.attempted_logins >= self.max_login_attempts: msg += " Disconnecting." self.respond("530 " + msg) self.close_when_done() else: self.respond("530 " + msg) self.log("USER '%s' failed login." % username) self.on_login_failed(username, password) self.del_channel() if not msg: if self.username == 'anonymous': msg = "Anonymous access not allowed." else: msg = "Authentication failed." else: # response string should be capitalized as per RFC-959 msg = msg.capitalize() self.ioloop.call_later(self.auth_failed_timeout, callback, self.username, password, msg, _errback=self.handle_error) self.username = "" def handle_auth_success(self, home, password, msg_login): if not isinstance(home, unicode): if PY3: raise TypeError('type(home) != text') else: warnings.warn( '%s.get_home_dir returned a non-unicode string; now ' 'casting to unicode' % ( self.authorizer.__class__.__name__), RuntimeWarning) home = home.decode('utf8') if len(msg_login) <= 75: self.respond('230 %s' % msg_login) else: self.push("230-%s\r\n" % msg_login) self.respond("230 ") self.log("USER '%s' logged in." % self.username) self.authenticated = True self.password = password self.attempted_logins = 0 self.fs = self.abstracted_fs(home, self) self.on_login(self.username) def ftp_PASS(self, line): """Check username's password against the authorizer.""" if self.authenticated: self.respond("503 User already authenticated.") return if not self.username: self.respond("503 Login with USER first.") return try: self.authorizer.validate_authentication(self.username, line, self) home = self.authorizer.get_home_dir(self.username) msg_login = self.authorizer.get_msg_login(self.username) except (AuthenticationFailed, AuthorizerError) as err: self.handle_auth_failed(str(err), line) else: self.handle_auth_success(home, line, msg_login) def ftp_REIN(self, line): """Reinitialize user's current session.""" # From RFC-959: # REIN command terminates a USER, flushing all I/O and account # information, except to allow any transfer in progress to be # completed. All parameters are reset to the default settings # and the control connection is left open. This is identical # to the state in which a user finds himself immediately after # the control connection is opened. self.flush_account() # Note: RFC-959 erroneously mention "220" as the correct response # code to be given in this case, but this is wrong... self.respond("230 Ready for new user.") # --- filesystem operations def ftp_PWD(self, line): """Return the name of the current working directory to the client.""" # The 257 response is supposed to include the directory # name and in case it contains embedded double-quotes # they must be doubled (see RFC-959, chapter 7, appendix 2). cwd = self.fs.cwd assert isinstance(cwd, unicode), cwd self.respond('257 "%s" is the current directory.' % cwd.replace('"', '""')) def ftp_CWD(self, path): """Change the current working directory. On success return the new directory path, else None. """ # Temporarily join the specified directory to see if we have # permissions to do so, then get back to original process's # current working directory. # Note that if for some reason os.getcwd() gets removed after # the process is started we'll get into troubles (os.getcwd() # will fail with ENOENT) but we can't do anything about that # except logging an error. init_cwd = getcwdu() try: self.run_as_current_user(self.fs.chdir, path) except (OSError, FilesystemError) as err: why = _strerror(err) self.respond('550 %s.' % why) else: cwd = self.fs.cwd assert isinstance(cwd, unicode), cwd self.respond('250 "%s" is the current directory.' % cwd) if getcwdu() != init_cwd: os.chdir(init_cwd) return path def ftp_CDUP(self, path): """Change into the parent directory. On success return the new directory, else None. """ # Note: RFC-959 says that code 200 is required but it also says # that CDUP uses the same codes as CWD. return self.ftp_CWD(path) def ftp_SIZE(self, path): """Return size of file in a format suitable for using with RESTart as defined in RFC-3659.""" # Implementation note: properly handling the SIZE command when # TYPE ASCII is used would require to scan the entire file to # perform the ASCII translation logic # (file.read().replace(os.linesep, '\r\n')) and then calculating # the len of such data which may be different than the actual # size of the file on the server. Considering that calculating # such result could be very resource-intensive and also dangerous # (DoS) we reject SIZE when the current TYPE is ASCII. # However, clients in general should not be resuming downloads # in ASCII mode. Resuming downloads in binary mode is the # recommended way as specified in RFC-3659. line = self.fs.fs2ftp(path) if self._current_type == 'a': why = "SIZE not allowed in ASCII mode" self.respond("550 %s." % why) return if not self.fs.isfile(self.fs.realpath(path)): why = "%s is not retrievable" % line self.respond("550 %s." % why) return try: size = self.run_as_current_user(self.fs.getsize, path) except (OSError, FilesystemError) as err: why = _strerror(err) self.respond('550 %s.' % why) else: self.respond("213 %s" % size) def ftp_MDTM(self, path): """Return last modification time of file to the client as an ISO 3307 style timestamp (YYYYMMDDHHMMSS) as defined in RFC-3659. On success return the file path, else None. """ line = self.fs.fs2ftp(path) if not self.fs.isfile(self.fs.realpath(path)): self.respond("550 %s is not retrievable" % line) return if self.use_gmt_times: timefunc = time.gmtime else: timefunc = time.localtime try: secs = self.run_as_current_user(self.fs.getmtime, path) lmt = time.strftime("%Y%m%d%H%M%S", timefunc(secs)) except (ValueError, OSError, FilesystemError) as err: if isinstance(err, ValueError): # It could happen if file's last modification time # happens to be too old (prior to year 1900) why = "Can't determine file's last modification time" else: why = _strerror(err) self.respond('550 %s.' % why) else: self.respond("213 %s" % lmt) return path def ftp_MFMT(self, path, timeval): """ Sets the last modification time of file to timeval 3307 style timestamp (YYYYMMDDHHMMSS) as defined in RFC-3659. On success return the modified time and file path, else None. """ # Note: the MFMT command is not a formal RFC command # but stated in the following MEMO: # https://tools.ietf.org/html/draft-somers-ftp-mfxx-04 # this is implemented to assist with file synchronization line = self.fs.fs2ftp(path) if len(timeval) != len("YYYYMMDDHHMMSS"): why = "Invalid time format; expected: YYYYMMDDHHMMSS" self.respond('550 %s.' % why) return if not self.fs.isfile(self.fs.realpath(path)): self.respond("550 %s is not retrievable" % line) return if self.use_gmt_times: timefunc = time.gmtime else: timefunc = time.localtime try: # convert timeval string to epoch seconds epoch = datetime.utcfromtimestamp(0) timeval_datetime_obj = datetime.strptime(timeval, '%Y%m%d%H%M%S') timeval_secs = (timeval_datetime_obj - epoch).total_seconds() except ValueError: why = "Invalid time format; expected: YYYYMMDDHHMMSS" self.respond('550 %s.' % why) return try: # Modify Time self.run_as_current_user(self.fs.utime, path, timeval_secs) # Fetch Time secs = self.run_as_current_user(self.fs.getmtime, path) lmt = time.strftime("%Y%m%d%H%M%S", timefunc(secs)) except (ValueError, OSError, FilesystemError) as err: if isinstance(err, ValueError): # It could happen if file's last modification time # happens to be too old (prior to year 1900) why = "Can't determine file's last modification time" else: why = _strerror(err) self.respond('550 %s.' % why) else: self.respond("213 Modify=%s; %s." % (lmt, line)) return (lmt, path) def ftp_MKD(self, path): """Create the specified directory. On success return the directory path, else None. """ line = self.fs.fs2ftp(path) try: self.run_as_current_user(self.fs.mkdir, path) except (OSError, FilesystemError) as err: why = _strerror(err) self.respond('550 %s.' % why) else: # The 257 response is supposed to include the directory # name and in case it contains embedded double-quotes # they must be doubled (see RFC-959, chapter 7, appendix 2). self.respond( '257 "%s" directory created.' % line.replace('"', '""')) return path def ftp_RMD(self, path): """Remove the specified directory. On success return the directory path, else None. """ if self.fs.realpath(path) == self.fs.realpath(self.fs.root): msg = "Can't remove root directory." self.respond("550 %s" % msg) return try: self.run_as_current_user(self.fs.rmdir, path) except (OSError, FilesystemError) as err: why = _strerror(err) self.respond('550 %s.' % why) else: self.respond("250 Directory removed.") def ftp_DELE(self, path): """Delete the specified file. On success return the file path, else None. """ try: self.run_as_current_user(self.fs.remove, path) except (OSError, FilesystemError) as err: why = _strerror(err) self.respond('550 %s.' % why) else: self.respond("250 File removed.") return path def ftp_RNFR(self, path): """Rename the specified (only the source name is specified here, see RNTO command)""" if not self.fs.lexists(path): self.respond("550 No such file or directory.") elif self.fs.realpath(path) == self.fs.realpath(self.fs.root): self.respond("550 Can't rename home directory.") else: self._rnfr = path self.respond("350 Ready for destination name.") def ftp_RNTO(self, path): """Rename file (destination name only, source is specified with RNFR). On success return a (source_path, destination_path) tuple. """ if not self._rnfr: self.respond("503 Bad sequence of commands: use RNFR first.") return src = self._rnfr self._rnfr = None try: self.run_as_current_user(self.fs.rename, src, path) except (OSError, FilesystemError) as err: why = _strerror(err) self.respond('550 %s.' % why) else: self.respond("250 Renaming ok.") return (src, path) # --- others def ftp_TYPE(self, line): """Set current type data type to binary/ascii""" type = line.upper().replace(' ', '') if type in ("A", "L7"): self.respond("200 Type set to: ASCII.") self._current_type = 'a' elif type in ("I", "L8"): self.respond("200 Type set to: Binary.") self._current_type = 'i' else: self.respond('504 Unsupported type "%s".' % line) def ftp_STRU(self, line): """Set file structure ("F" is the only one supported (noop)).""" stru = line.upper() if stru == 'F': self.respond('200 File transfer structure set to: F.') elif stru in ('P', 'R'): # R is required in minimum implementations by RFC-959, 5.1. # RFC-1123, 4.1.2.13, amends this to only apply to servers # whose file systems support record structures, but also # suggests that such a server "may still accept files with # STRU R, recording the byte stream literally". # Should we accept R but with no operational difference from # F? proftpd and wu-ftpd don't accept STRU R. We just do # the same. # # RFC-1123 recommends against implementing P. self.respond('504 Unimplemented STRU type.') else: self.respond('501 Unrecognized STRU type.') def ftp_MODE(self, line): """Set data transfer mode ("S" is the only one supported (noop)).""" mode = line.upper() if mode == 'S': self.respond('200 Transfer mode set to: S') elif mode in ('B', 'C'): self.respond('504 Unimplemented MODE type.') else: self.respond('501 Unrecognized MODE type.') def ftp_STAT(self, path): """Return statistics about current ftp session. If an argument is provided return directory listing over command channel. Implementation note: RFC-959 does not explicitly mention globbing but many FTP servers do support it as a measure of convenience for FTP clients and users. In order to search for and match the given globbing expression, the code has to search (possibly) many directories, examine each contained filename, and build a list of matching files in memory. Since this operation can be quite intensive, both CPU- and memory-wise, we do not support globbing. """ # return STATus information about ftpd if not path: s = [] s.append('Connected to: %s:%s' % self.socket.getsockname()[:2]) if self.authenticated: s.append('Logged in as: %s' % self.username) else: if not self.username: s.append("Waiting for username.") else: s.append("Waiting for password.") if self._current_type == 'a': type = 'ASCII' else: type = 'Binary' s.append("TYPE: %s; STRUcture: File; MODE: Stream" % type) if self._dtp_acceptor is not None: s.append('Passive data channel waiting for connection.') elif self.data_channel is not None: bytes_sent = self.data_channel.tot_bytes_sent bytes_recv = self.data_channel.tot_bytes_received elapsed_time = self.data_channel.get_elapsed_time() s.append('Data connection open:') s.append('Total bytes sent: %s' % bytes_sent) s.append('Total bytes received: %s' % bytes_recv) s.append('Transfer elapsed time: %s secs' % elapsed_time) else: s.append('Data connection closed.') self.push('211-FTP server status:\r\n') self.push(''.join([' %s\r\n' % item for item in s])) self.respond('211 End of status.') # return directory LISTing over the command channel else: line = self.fs.fs2ftp(path) try: isdir = self.fs.isdir(path) if isdir: listing = self.run_as_current_user(self.fs.listdir, path) if isinstance(listing, list): try: # RFC 959 recommends the listing to be sorted. listing.sort() except UnicodeDecodeError: # (Python 2 only) might happen on filesystem not # supporting UTF8 meaning os.listdir() returned a # list of mixed bytes and unicode strings: # http://goo.gl/6DLHD # http://bugs.python.org/issue683592 pass iterator = self.fs.format_list(path, listing) else: basedir, filename = os.path.split(path) self.fs.lstat(path) # raise exc in case of problems iterator = self.fs.format_list(basedir, [filename]) except (OSError, FilesystemError) as err: why = _strerror(err) self.respond('550 %s.' % why) else: self.push('213-Status of "%s":\r\n' % line) self.push_with_producer(BufferedIteratorProducer(iterator)) self.respond('213 End of status.') return path def ftp_FEAT(self, line): """List all new features supported as defined in RFC-2398.""" features = set(['UTF8', 'TVFS']) features.update([feat for feat in ('EPRT', 'EPSV', 'MDTM', 'MFMT', 'SIZE') if feat in self.proto_cmds]) features.update(self._extra_feats) if 'MLST' in self.proto_cmds or 'MLSD' in self.proto_cmds: facts = '' for fact in self._available_facts: if fact in self._current_facts: facts += fact + '*;' else: facts += fact + ';' features.add('MLST ' + facts) if 'REST' in self.proto_cmds: features.add('REST STREAM') features = sorted(features) self.push("211-Features supported:\r\n") self.push("".join([" %s\r\n" % x for x in features])) self.respond('211 End FEAT.') def ftp_OPTS(self, line): """Specify options for FTP commands as specified in RFC-2389.""" try: if line.count(' ') > 1: raise ValueError('Invalid number of arguments') if ' ' in line: cmd, arg = line.split(' ') if ';' not in arg: raise ValueError('Invalid argument') else: cmd, arg = line, '' # actually the only command able to accept options is MLST if cmd.upper() != 'MLST' or 'MLST' not in self.proto_cmds: raise ValueError('Unsupported command "%s"' % cmd) except ValueError as err: self.respond('501 %s.' % err) else: facts = [x.lower() for x in arg.split(';')] self._current_facts = \ [x for x in facts if x in self._available_facts] f = ''.join([x + ';' for x in self._current_facts]) self.respond('200 MLST OPTS ' + f) def ftp_NOOP(self, line): """Do nothing.""" self.respond("200 I successfully done nothin'.") def ftp_SYST(self, line): """Return system type (always returns UNIX type: L8).""" # This command is used to find out the type of operating system # at the server. The reply shall have as its first word one of # the system names listed in RFC-943. # Since that we always return a "/bin/ls -lA"-like output on # LIST we prefer to respond as if we would on Unix in any case. self.respond("215 UNIX Type: L8") def ftp_ALLO(self, line): """Allocate bytes for storage (noop).""" # not necessary (always respond with 202) self.respond("202 No storage allocation necessary.") def ftp_HELP(self, line): """Return help text to the client.""" if line: line = line.upper() if line in self.proto_cmds: self.respond("214 %s" % self.proto_cmds[line]['help']) else: self.respond("501 Unrecognized command.") else: # provide a compact list of recognized commands def formatted_help(): cmds = [] keys = sorted([x for x in self.proto_cmds.keys() if not x.startswith('SITE ')]) while keys: elems = tuple((keys[0:8])) cmds.append(' %-6s' * len(elems) % elems + '\r\n') del keys[0:8] return ''.join(cmds) self.push("214-The following commands are recognized:\r\n") self.push(formatted_help()) self.respond("214 Help command successful.") # --- site commands # The user willing to add support for a specific SITE command must # update self.proto_cmds dictionary and define a new ftp_SITE_%CMD% # method in the subclass. def ftp_SITE_CHMOD(self, path, mode): """Change file mode. On success return a (file_path, mode) tuple. """ # Note: although most UNIX servers implement it, SITE CHMOD is not # defined in any official RFC. try: assert len(mode) in (3, 4) for x in mode: assert 0 <= int(x) <= 7 mode = int(mode, 8) except (AssertionError, ValueError): self.respond("501 Invalid SITE CHMOD format.") else: try: self.run_as_current_user(self.fs.chmod, path, mode) except (OSError, FilesystemError) as err: why = _strerror(err) self.respond('550 %s.' % why) else: self.respond('200 SITE CHMOD successful.') return (path, mode) def ftp_SITE_HELP(self, line): """Return help text to the client for a given SITE command.""" if line: line = line.upper() if line in self.proto_cmds: self.respond("214 %s" % self.proto_cmds[line]['help']) else: self.respond("501 Unrecognized SITE command.") else: self.push("214-The following SITE commands are recognized:\r\n") site_cmds = [] for cmd in sorted(self.proto_cmds.keys()): if cmd.startswith('SITE '): site_cmds.append(' %s\r\n' % cmd[5:]) self.push(''.join(site_cmds)) self.respond("214 Help SITE command successful.") # --- support for deprecated cmds # RFC-1123 requires that the server treat XCUP, XCWD, XMKD, XPWD # and XRMD commands as synonyms for CDUP, CWD, MKD, LIST and RMD. # Such commands are obsoleted but some ftp clients (e.g. Windows # ftp.exe) still use them. def ftp_XCUP(self, line): "Change to the parent directory. Synonym for CDUP. Deprecated." return self.ftp_CDUP(line) def ftp_XCWD(self, line): "Change the current working directory. Synonym for CWD. Deprecated." return self.ftp_CWD(line) def ftp_XMKD(self, line): "Create the specified directory. Synonym for MKD. Deprecated." return self.ftp_MKD(line) def ftp_XPWD(self, line): "Return the current working directory. Synonym for PWD. Deprecated." return self.ftp_PWD(line) def ftp_XRMD(self, line): "Remove the specified directory. Synonym for RMD. Deprecated." return self.ftp_RMD(line) # =================================================================== # --- FTP over SSL # =================================================================== if SSL is not None: class SSLConnection(_AsyncChatNewStyle): """An AsyncChat subclass supporting TLS/SSL.""" _ssl_accepting = False _ssl_established = False _ssl_closing = False _ssl_requested = False def __init__(self, *args, **kwargs): super(SSLConnection, self).__init__(*args, **kwargs) self._error = False self._ssl_want_read = False self._ssl_want_write = False def readable(self): return self._ssl_want_read or \ super(SSLConnection, self).readable() def writable(self): return self._ssl_want_write or \ super(SSLConnection, self).writable() def secure_connection(self, ssl_context): """Secure the connection switching from plain-text to SSL/TLS. """ debug("securing SSL connection", self) self._ssl_requested = True try: self.socket = SSL.Connection(ssl_context, self.socket) except socket.error as err: # may happen in case the client connects/disconnects # very quickly debug( "call: secure_connection(); can't secure SSL connection " "%r; closing" % err, self) self.close() except ValueError: # may happen in case the client connects/disconnects # very quickly if self.socket.fileno() == -1: debug( "ValueError and fd == -1 on secure_connection()", self) return raise else: self.socket.set_accept_state() self._ssl_accepting = True @contextlib.contextmanager def _handle_ssl_want_rw(self): prev_row_pending = self._ssl_want_read or self._ssl_want_write try: yield except SSL.WantReadError: # we should never get here; it's just for extra safety self._ssl_want_read = True except SSL.WantWriteError: # we should never get here; it's just for extra safety self._ssl_want_write = True if self._ssl_want_read: self.modify_ioloop_events( self._wanted_io_events | self.ioloop.READ, logdebug=True) elif self._ssl_want_write: self.modify_ioloop_events( self._wanted_io_events | self.ioloop.WRITE, logdebug=True) else: if prev_row_pending: self.modify_ioloop_events(self._wanted_io_events) def _do_ssl_handshake(self): self._ssl_accepting = True self._ssl_want_read = False self._ssl_want_write = False try: self.socket.do_handshake() except SSL.WantReadError: self._ssl_want_read = True debug("call: _do_ssl_handshake, err: ssl-want-read", inst=self) except SSL.WantWriteError: self._ssl_want_write = True debug("call: _do_ssl_handshake, err: ssl-want-write", inst=self) except SSL.SysCallError as err: debug("call: _do_ssl_handshake, err: %r" % err, inst=self) retval, desc = err.args if (retval == -1 and desc == 'Unexpected EOF') or retval > 0: return self.handle_close() raise except SSL.Error as err: debug("call: _do_ssl_handshake, err: %r" % err, inst=self) return self.handle_failed_ssl_handshake() else: debug("SSL connection established", self) self._ssl_accepting = False self._ssl_established = True self.handle_ssl_established() def handle_ssl_established(self): """Called when SSL handshake has completed.""" pass def handle_ssl_shutdown(self): """Called when SSL shutdown() has completed.""" super(SSLConnection, self).close() def handle_failed_ssl_handshake(self): raise NotImplementedError("must be implemented in subclass") def handle_read_event(self): if not self._ssl_requested: super(SSLConnection, self).handle_read_event() else: with self._handle_ssl_want_rw(): self._ssl_want_read = False if self._ssl_accepting: self._do_ssl_handshake() elif self._ssl_closing: self._do_ssl_shutdown() else: super(SSLConnection, self).handle_read_event() def handle_write_event(self): if not self._ssl_requested: super(SSLConnection, self).handle_write_event() else: with self._handle_ssl_want_rw(): self._ssl_want_write = False if self._ssl_accepting: self._do_ssl_handshake() elif self._ssl_closing: self._do_ssl_shutdown() else: super(SSLConnection, self).handle_write_event() def handle_error(self): self._error = True try: raise except Exception: self.log_exception(self) # when facing an unhandled exception in here it's better # to rely on base class (FTPHandler or DTPHandler) # close() method as it does not imply SSL shutdown logic try: super(SSLConnection, self).close() except Exception: logger.critical(traceback.format_exc()) def send(self, data): if not isinstance(data, bytes): data = bytes(data) try: return super(SSLConnection, self).send(data) except SSL.WantReadError: debug("call: send(), err: ssl-want-read", inst=self) self._ssl_want_read = True return 0 except SSL.WantWriteError: debug("call: send(), err: ssl-want-write", inst=self) self._ssl_want_write = True return 0 except SSL.ZeroReturnError as err: debug( "call: send() -> shutdown(), err: zero-return", inst=self) super(SSLConnection, self).handle_close() return 0 except SSL.SysCallError as err: debug("call: send(), err: %r" % err, inst=self) errnum, errstr = err.args if errnum == errno.EWOULDBLOCK: return 0 elif (errnum in _ERRNOS_DISCONNECTED or errstr == 'Unexpected EOF'): super(SSLConnection, self).handle_close() return 0 else: raise def recv(self, buffer_size): try: return super(SSLConnection, self).recv(buffer_size) except SSL.WantReadError: debug("call: recv(), err: ssl-want-read", inst=self) self._ssl_want_read = True raise RetryError except SSL.WantWriteError: debug("call: recv(), err: ssl-want-write", inst=self) self._ssl_want_write = True raise RetryError except SSL.ZeroReturnError as err: debug("call: recv() -> shutdown(), err: zero-return", inst=self) super(SSLConnection, self).handle_close() return b'' except SSL.SysCallError as err: debug("call: recv(), err: %r" % err, inst=self) errnum, errstr = err.args if (errnum in _ERRNOS_DISCONNECTED or errstr == 'Unexpected EOF'): super(SSLConnection, self).handle_close() return b'' else: raise def _do_ssl_shutdown(self): """Executes a SSL_shutdown() call to revert the connection back to clear-text. twisted/internet/tcp.py code has been used as an example. """ self._ssl_closing = True if os.name == 'posix': # since SSL_shutdown() doesn't report errors, an empty # write call is done first, to try to detect if the # connection has gone away try: os.write(self.socket.fileno(), b'') except (OSError, socket.error) as err: debug( "call: _do_ssl_shutdown() -> os.write, err: %r" % err, inst=self) if err.errno in (errno.EINTR, errno.EWOULDBLOCK, errno.ENOBUFS): return elif err.errno in _ERRNOS_DISCONNECTED: return super(SSLConnection, self).close() else: raise # Ok, this a mess, but the underlying OpenSSL API simply # *SUCKS* and I really couldn't do any better. # # Here we just want to shutdown() the SSL layer and then # close() the connection so we're not interested in a # complete SSL shutdown() handshake, so let's pretend # we already received a "RECEIVED" shutdown notification # from the client. # Once the client received our "SENT" shutdown notification # then we close() the connection. # # Since it is not clear what errors to expect during the # entire procedure we catch them all and assume the # following: # - WantReadError and WantWriteError means "retry" # - ZeroReturnError, SysCallError[EOF], Error[] are all # aliases for disconnection try: laststate = self.socket.get_shutdown() self.socket.set_shutdown(laststate | SSL.RECEIVED_SHUTDOWN) done = self.socket.shutdown() if not (laststate & SSL.RECEIVED_SHUTDOWN): self.socket.set_shutdown(SSL.SENT_SHUTDOWN) except SSL.WantReadError: self._ssl_want_read = True debug("call: _do_ssl_shutdown, err: ssl-want-read", inst=self) except SSL.WantWriteError: self._ssl_want_write = True debug("call: _do_ssl_shutdown, err: ssl-want-write", inst=self) except SSL.ZeroReturnError as err: debug( "call: _do_ssl_shutdown() -> shutdown(), err: zero-return", inst=self) super(SSLConnection, self).close() except SSL.SysCallError as err: debug("call: _do_ssl_shutdown() -> shutdown(), err: %r" % err, inst=self) errnum, errstr = err.args if (errnum in _ERRNOS_DISCONNECTED or errstr == 'Unexpected EOF'): super(SSLConnection, self).close() else: raise except SSL.Error as err: debug("call: _do_ssl_shutdown() -> shutdown(), err: %r" % err, inst=self) # see: # https://github.com/giampaolo/pyftpdlib/issues/171 # https://bugs.launchpad.net/pyopenssl/+bug/785985 if err.args and not getattr(err, "errno", None): pass else: raise except socket.error as err: debug("call: _do_ssl_shutdown() -> shutdown(), err: %r" % err, inst=self) if err.errno in _ERRNOS_DISCONNECTED: super(SSLConnection, self).close() else: raise else: if done: debug("call: _do_ssl_shutdown(), shutdown completed", inst=self) self._ssl_established = False self._ssl_closing = False self.handle_ssl_shutdown() else: debug( "call: _do_ssl_shutdown(), shutdown not completed yet", inst=self) def close(self): if self._ssl_established and not self._error: self._do_ssl_shutdown() else: self._ssl_accepting = False self._ssl_established = False self._ssl_closing = False super(SSLConnection, self).close() class TLS_DTPHandler(SSLConnection, DTPHandler): """A DTPHandler subclass supporting TLS/SSL.""" def __init__(self, sock, cmd_channel): super(TLS_DTPHandler, self).__init__(sock, cmd_channel) if self.cmd_channel._prot: self.secure_connection(self.cmd_channel.ssl_context) def __repr__(self): return DTPHandler.__repr__(self) def use_sendfile(self): if isinstance(self.socket, SSL.Connection): return False else: return super(TLS_DTPHandler, self).use_sendfile() def handle_failed_ssl_handshake(self): # TLS/SSL handshake failure, probably client's fault which # used a SSL version different from server's. # RFC-4217, chapter 10.2 expects us to return 522 over the # command channel. self.cmd_channel.respond("522 SSL handshake failed.") self.cmd_channel.log_cmd("PROT", "P", 522, "SSL handshake failed.") self.close() class TLS_FTPHandler(SSLConnection, FTPHandler): """A FTPHandler subclass supporting TLS/SSL. Implements AUTH, PBSZ and PROT commands (RFC-2228 and RFC-4217). Configurable attributes: - (bool) tls_control_required: When True requires SSL/TLS to be established on the control channel, before logging in. This means the user will have to issue AUTH before USER/PASS (default False). - (bool) tls_data_required: When True requires SSL/TLS to be established on the data channel. This means the user will have to issue PROT before PASV or PORT (default False). SSL-specific options: - (string) certfile: the path to the file which contains a certificate to be used to identify the local side of the connection. This must always be specified, unless context is provided instead. - (string) keyfile: the path to the file containing the private RSA key; can be omitted if certfile already contains the private key (defaults: None). - (int) ssl_protocol: the desired SSL protocol version to use. This defaults to PROTOCOL_SSLv23 which will negotiate the highest protocol that both the server and your installation of OpenSSL support. - (int) ssl_options: specific OpenSSL options. These default to: SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3| SSL.OP_NO_COMPRESSION which are all considered insecure features. Can be set to None in order to improve compatibilty with older (insecure) FTP clients. - (instance) ssl_context: a SSL Context object previously configured; if specified all other parameters will be ignored. (default None). """ # configurable attributes tls_control_required = False tls_data_required = False certfile = None keyfile = None ssl_protocol = SSL.SSLv23_METHOD # - SSLv2 is easily broken and is considered harmful and dangerous # - SSLv3 has several problems and is now dangerous # - Disable compression to prevent CRIME attacks for OpenSSL 1.0+ # (see https://github.com/shazow/urllib3/pull/309) ssl_options = SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3 if hasattr(SSL, "OP_NO_COMPRESSION"): ssl_options |= SSL.OP_NO_COMPRESSION ssl_context = None # overridden attributes dtp_handler = TLS_DTPHandler proto_cmds = FTPHandler.proto_cmds.copy() proto_cmds.update({ 'AUTH': dict( perm=None, auth=False, arg=True, help='Syntax: AUTH TLS|SSL (set up secure control ' 'channel).'), 'PBSZ': dict( perm=None, auth=False, arg=True, help='Syntax: PBSZ 0 (negotiate TLS buffer).'), 'PROT': dict( perm=None, auth=False, arg=True, help='Syntax: PROT [C|P] (set up un/secure data ' 'channel).'), }) def __init__(self, conn, server, ioloop=None): super(TLS_FTPHandler, self).__init__(conn, server, ioloop) if not self.connected: return self._extra_feats = ['AUTH TLS', 'AUTH SSL', 'PBSZ', 'PROT'] self._pbsz = False self._prot = False self.ssl_context = self.get_ssl_context() def __repr__(self): return FTPHandler.__repr__(self) @classmethod def get_ssl_context(cls): if cls.ssl_context is None: if cls.certfile is None: raise ValueError("at least certfile must be specified") cls.ssl_context = SSL.Context(cls.ssl_protocol) if cls.ssl_protocol != SSL.SSLv2_METHOD: cls.ssl_context.set_options(SSL.OP_NO_SSLv2) else: warnings.warn("SSLv2 protocol is insecure", RuntimeWarning) cls.ssl_context.use_certificate_chain_file(cls.certfile) if not cls.keyfile: cls.keyfile = cls.certfile cls.ssl_context.use_privatekey_file(cls.keyfile) if cls.ssl_options: cls.ssl_context.set_options(cls.ssl_options) return cls.ssl_context # --- overridden methods def flush_account(self): FTPHandler.flush_account(self) self._pbsz = False self._prot = False def process_command(self, cmd, *args, **kwargs): if cmd in ('USER', 'PASS'): if self.tls_control_required and not self._ssl_established: msg = "SSL/TLS required on the control channel." self.respond("550 " + msg) self.log_cmd(cmd, args[0], 550, msg) return elif cmd in ('PASV', 'EPSV', 'PORT', 'EPRT'): if self.tls_data_required and not self._prot: msg = "SSL/TLS required on the data channel." self.respond("550 " + msg) self.log_cmd(cmd, args[0], 550, msg) return FTPHandler.process_command(self, cmd, *args, **kwargs) # --- new methods def handle_failed_ssl_handshake(self): # TLS/SSL handshake failure, probably client's fault which # used a SSL version different from server's. # We can't rely on the control connection anymore so we just # disconnect the client without sending any response. self.log("SSL handshake failed.") self.close() def ftp_AUTH(self, line): """Set up secure control channel.""" arg = line.upper() if isinstance(self.socket, SSL.Connection): self.respond("503 Already using TLS.") elif arg in ('TLS', 'TLS-C', 'SSL', 'TLS-P'): # From RFC-4217: "As the SSL/TLS protocols self-negotiate # their levels, there is no need to distinguish between SSL # and TLS in the application layer". self.respond('234 AUTH %s successful.' % arg) self.secure_connection(self.ssl_context) else: self.respond( "502 Unrecognized encryption type (use TLS or SSL).") def ftp_PBSZ(self, line): """Negotiate size of buffer for secure data transfer. For TLS/SSL the only valid value for the parameter is '0'. Any other value is accepted but ignored. """ if not isinstance(self.socket, SSL.Connection): self.respond( "503 PBSZ not allowed on insecure control connection.") else: self.respond('200 PBSZ=0 successful.') self._pbsz = True def ftp_PROT(self, line): """Setup un/secure data channel.""" arg = line.upper() if not isinstance(self.socket, SSL.Connection): self.respond( "503 PROT not allowed on insecure control connection.") elif not self._pbsz: self.respond( "503 You must issue the PBSZ command prior to PROT.") elif arg == 'C': self.respond('200 Protection set to Clear') self._prot = False elif arg == 'P': self.respond('200 Protection set to Private') self._prot = True elif arg in ('S', 'E'): self.respond('521 PROT %s unsupported (use C or P).' % arg) else: self.respond("502 Unrecognized PROT type (use C or P).") pyftpdlib-release-1.5.4/pyftpdlib/ioloop.py000066400000000000000000001076411327314554200210230ustar00rootroot00000000000000# Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. """ A specialized IO loop on top of asyncore adding support for epoll() on Linux and kqueue() and OSX/BSD, dramatically increasing performances offered by base asyncore module. poll() and select() loops are also reimplemented and are an order of magnitude faster as they support fd un/registration and modification. This module is not supposed to be used directly unless you want to include a new dispatcher which runs within the main FTP server loop, in which case: __________________________________________________________________ | | | | INSTEAD OF | ...USE: | |______________________|___________________________________________| | | | | asyncore.dispacher | Acceptor (for servers) | | asyncore.dispacher | Connector (for clients) | | asynchat.async_chat | AsyncChat (for a full duplex connection ) | | asyncore.loop | FTPServer.server_forever() | |______________________|___________________________________________| asyncore.dispatcher_with_send is not supported, same for "map" argument for asyncore.loop and asyncore.dispatcher and asynchat.async_chat constructors. Follows a server example: import socket from pyftpdlib.ioloop import IOLoop, Acceptor, AsyncChat class Handler(AsyncChat): def __init__(self, sock): AsyncChat.__init__(self, sock) self.push('200 hello\r\n') self.close_when_done() class Server(Acceptor): def __init__(self, host, port): Acceptor.__init__(self) self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.set_reuse_addr() self.bind((host, port)) self.listen(5) def handle_accepted(self, sock, addr): Handler(sock) server = Server('localhost', 8021) IOLoop.instance().loop() """ import asynchat import asyncore import errno import heapq import os import select import socket import sys import time import traceback try: import threading except ImportError: import dummy_threading as threading from ._compat import callable from .log import config_logging from .log import debug from .log import is_logging_configured from .log import logger timer = getattr(time, 'monotonic', time.time) _read = asyncore.read _write = asyncore.write # These errnos indicate that a connection has been abruptly terminated. _ERRNOS_DISCONNECTED = set(( errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN, errno.ECONNABORTED, errno.EPIPE, errno.EBADF, errno.ETIMEDOUT)) if hasattr(errno, "WSAECONNRESET"): _ERRNOS_DISCONNECTED.add(errno.WSAECONNRESET) if hasattr(errno, "WSAECONNABORTED"): _ERRNOS_DISCONNECTED.add(errno.WSAECONNABORTED) # These errnos indicate that a non-blocking operation must be retried # at a later time. _ERRNOS_RETRY = set((errno.EAGAIN, errno.EWOULDBLOCK)) if hasattr(errno, "WSAEWOULDBLOCK"): _ERRNOS_RETRY.add(errno.WSAEWOULDBLOCK) class RetryError(Exception): pass # =================================================================== # --- scheduler # =================================================================== class _Scheduler(object): """Run the scheduled functions due to expire soonest (if any).""" def __init__(self): # the heap used for the scheduled tasks self._tasks = [] self._cancellations = 0 def poll(self): """Run the scheduled functions due to expire soonest and return the timeout of the next one (if any, else None). """ now = timer() calls = [] while self._tasks: if now < self._tasks[0].timeout: break call = heapq.heappop(self._tasks) if call.cancelled: self._cancellations -= 1 else: calls.append(call) for call in calls: if call._repush: heapq.heappush(self._tasks, call) call._repush = False continue try: call.call() except Exception: logger.error(traceback.format_exc()) # remove cancelled tasks and re-heapify the queue if the # number of cancelled tasks is more than the half of the # entire queue if (self._cancellations > 512 and self._cancellations > (len(self._tasks) >> 1)): debug("re-heapifying %s cancelled tasks" % self._cancellations) self.reheapify() try: return max(0, self._tasks[0].timeout - now) except IndexError: pass def register(self, what): """Register a _CallLater instance.""" heapq.heappush(self._tasks, what) def unregister(self, what): """Unregister a _CallLater instance. The actual unregistration will happen at a later time though. """ self._cancellations += 1 def reheapify(self): """Get rid of cancelled calls and reinitialize the internal heap.""" self._cancellations = 0 self._tasks = [x for x in self._tasks if not x.cancelled] heapq.heapify(self._tasks) class _CallLater(object): """Container object which instance is returned by ioloop.call_later().""" __slots__ = ('_delay', '_target', '_args', '_kwargs', '_errback', '_sched', '_repush', 'timeout', 'cancelled') def __init__(self, seconds, target, *args, **kwargs): assert callable(target), "%s is not callable" % target assert sys.maxsize >= seconds >= 0, \ "%s is not greater than or equal to 0 seconds" % seconds self._delay = seconds self._target = target self._args = args self._kwargs = kwargs self._errback = kwargs.pop('_errback', None) self._sched = kwargs.pop('_scheduler') self._repush = False # seconds from the epoch at which to call the function if not seconds: self.timeout = 0 else: self.timeout = timer() + self._delay self.cancelled = False self._sched.register(self) def __lt__(self, other): return self.timeout < other.timeout def __le__(self, other): return self.timeout <= other.timeout def __repr__(self): if self._target is None: sig = object.__repr__(self) else: sig = repr(self._target) sig += ' args=%s, kwargs=%s, cancelled=%s, secs=%s' % ( self._args or '[]', self._kwargs or '{}', self.cancelled, self._delay) return '<%s>' % sig __str__ = __repr__ def _post_call(self, exc): if not self.cancelled: self.cancel() def call(self): """Call this scheduled function.""" assert not self.cancelled, "already cancelled" exc = None try: self._target(*self._args, **self._kwargs) except Exception as _: exc = _ if self._errback is not None: self._errback() else: raise finally: self._post_call(exc) def reset(self): """Reschedule this call resetting the current countdown.""" assert not self.cancelled, "already cancelled" self.timeout = timer() + self._delay self._repush = True def cancel(self): """Unschedule this call.""" if not self.cancelled: self.cancelled = True self._target = self._args = self._kwargs = self._errback = None self._sched.unregister(self) class _CallEvery(_CallLater): """Container object which instance is returned by IOLoop.call_every().""" def _post_call(self, exc): if not self.cancelled: if exc: self.cancel() else: self.timeout = timer() + self._delay self._sched.register(self) class _IOLoop(object): """Base class which will later be referred as IOLoop.""" READ = 1 WRITE = 2 _instance = None _lock = threading.Lock() _started_once = False def __init__(self): self.socket_map = {} self.sched = _Scheduler() def __enter__(self): return self def __exit__(self, *args): self.close() def __repr__(self): status = [self.__class__.__module__ + "." + self.__class__.__name__] status.append("(fds=%s, tasks=%s)" % ( len(self.socket_map), len(self.sched._tasks))) return '<%s at %#x>' % (' '.join(status), id(self)) __str__ = __repr__ @classmethod def instance(cls): """Return a global IOLoop instance.""" if cls._instance is None: with cls._lock: if cls._instance is None: cls._instance = cls() return cls._instance def register(self, fd, instance, events): """Register a fd, handled by instance for the given events.""" raise NotImplementedError('must be implemented in subclass') def unregister(self, fd): """Register fd.""" raise NotImplementedError('must be implemented in subclass') def modify(self, fd, events): """Changes the events assigned for fd.""" raise NotImplementedError('must be implemented in subclass') def poll(self, timeout): """Poll once. The subclass overriding this method is supposed to poll over the registered handlers and the scheduled functions and then return. """ raise NotImplementedError('must be implemented in subclass') def loop(self, timeout=None, blocking=True): """Start the asynchronous IO loop. - (float) timeout: the timeout passed to the underlying multiplex syscall (select(), epoll() etc.). - (bool) blocking: if True poll repeatedly, as long as there are registered handlers and/or scheduled functions. If False poll only once and return the timeout of the next scheduled call (if any, else None). """ if not _IOLoop._started_once: _IOLoop._started_once = True if not is_logging_configured(): # If we get to this point it means the user hasn't # configured logging. We want to log by default so # we configure logging ourselves so that it will # print to stderr. config_logging() if blocking: # localize variable access to minimize overhead poll = self.poll socket_map = self.socket_map sched_poll = self.sched.poll if timeout is not None: while socket_map: poll(timeout) sched_poll() else: soonest_timeout = None while socket_map: poll(soonest_timeout) soonest_timeout = sched_poll() else: sched = self.sched if self.socket_map: self.poll(timeout) if sched._tasks: return sched.poll() def call_later(self, seconds, target, *args, **kwargs): """Calls a function at a later time. It can be used to asynchronously schedule a call within the polling loop without blocking it. The instance returned is an object that can be used to cancel or reschedule the call. - (int) seconds: the number of seconds to wait - (obj) target: the callable object to call later - args: the arguments to call it with - kwargs: the keyword arguments to call it with; a special '_errback' parameter can be passed: it is a callable called in case target function raises an exception. """ kwargs['_scheduler'] = self.sched return _CallLater(seconds, target, *args, **kwargs) def call_every(self, seconds, target, *args, **kwargs): """Schedules the given callback to be called periodically.""" kwargs['_scheduler'] = self.sched return _CallEvery(seconds, target, *args, **kwargs) def close(self): """Closes the IOLoop, freeing any resources used.""" debug("closing IOLoop", self) self.__class__._instance = None # free connections instances = sorted(self.socket_map.values(), key=lambda x: x._fileno) for inst in instances: try: inst.close() except OSError as err: if err.errno != errno.EBADF: logger.error(traceback.format_exc()) except Exception: logger.error(traceback.format_exc()) self.socket_map.clear() # free scheduled functions for x in self.sched._tasks: try: if not x.cancelled: x.cancel() except Exception: logger.error(traceback.format_exc()) del self.sched._tasks[:] # =================================================================== # --- select() - POSIX / Windows # =================================================================== class Select(_IOLoop): """select()-based poller.""" def __init__(self): _IOLoop.__init__(self) self._r = [] self._w = [] def register(self, fd, instance, events): if fd not in self.socket_map: self.socket_map[fd] = instance if events & self.READ: self._r.append(fd) if events & self.WRITE: self._w.append(fd) def unregister(self, fd): try: del self.socket_map[fd] except KeyError: debug("call: unregister(); fd was no longer in socket_map", self) for l in (self._r, self._w): try: l.remove(fd) except ValueError: pass def modify(self, fd, events): inst = self.socket_map.get(fd) if inst is not None: self.unregister(fd) self.register(fd, inst, events) else: debug("call: modify(); fd was no longer in socket_map", self) def poll(self, timeout): try: r, w, e = select.select(self._r, self._w, [], timeout) except select.error as err: if getattr(err, "errno", None) == errno.EINTR: return raise smap_get = self.socket_map.get for fd in r: obj = smap_get(fd) if obj is None or not obj.readable(): continue _read(obj) for fd in w: obj = smap_get(fd) if obj is None or not obj.writable(): continue _write(obj) # =================================================================== # --- poll() / epoll() # =================================================================== class _BasePollEpoll(_IOLoop): """This is common to both poll() (UNIX), epoll() (Linux) and /dev/poll (Solaris) implementations which share almost the same interface. Not supposed to be used directly. """ def __init__(self): _IOLoop.__init__(self) self._poller = self._poller() def register(self, fd, instance, events): try: self._poller.register(fd, events) except EnvironmentError as err: if err.errno == errno.EEXIST: debug("call: register(); poller raised EEXIST; ignored", self) else: raise self.socket_map[fd] = instance def unregister(self, fd): try: del self.socket_map[fd] except KeyError: debug("call: unregister(); fd was no longer in socket_map", self) else: try: self._poller.unregister(fd) except EnvironmentError as err: if err.errno in (errno.ENOENT, errno.EBADF): debug("call: unregister(); poller returned %r; " "ignoring it" % err, self) else: raise def modify(self, fd, events): try: self._poller.modify(fd, events) except OSError as err: if err.errno == errno.ENOENT and fd in self.socket_map: # XXX - see: # https://github.com/giampaolo/pyftpdlib/issues/329 instance = self.socket_map[fd] self.register(fd, instance, events) else: raise def poll(self, timeout): try: events = self._poller.poll(timeout or -1) # -1 waits indefinitely except (IOError, select.error) as err: # for epoll() and poll() respectively if err.errno == errno.EINTR: return raise # localize variable access to minimize overhead smap_get = self.socket_map.get for fd, event in events: inst = smap_get(fd) if inst is None: continue if event & self._ERROR and not event & self.READ: inst.handle_close() else: if event & self.READ: if inst.readable(): _read(inst) if event & self.WRITE: if inst.writable(): _write(inst) # =================================================================== # --- poll() - POSIX # =================================================================== if hasattr(select, 'poll'): class Poll(_BasePollEpoll): """poll() based poller.""" READ = select.POLLIN WRITE = select.POLLOUT _ERROR = select.POLLERR | select.POLLHUP | select.POLLNVAL _poller = select.poll def modify(self, fd, events): inst = self.socket_map[fd] self.unregister(fd) self.register(fd, inst, events) def poll(self, timeout): # poll() timeout is expressed in milliseconds if timeout is not None: timeout = int(timeout * 1000) _BasePollEpoll.poll(self, timeout) # =================================================================== # --- /dev/poll - Solaris (introduced in python 3.3) # =================================================================== if hasattr(select, 'devpoll'): # pragma: no cover class DevPoll(_BasePollEpoll): """/dev/poll based poller (introduced in python 3.3).""" READ = select.POLLIN WRITE = select.POLLOUT _ERROR = select.POLLERR | select.POLLHUP | select.POLLNVAL _poller = select.devpoll # introduced in python 3.4 if hasattr(select.devpoll, 'fileno'): def fileno(self): """Return devpoll() fd.""" return self._poller.fileno() def modify(self, fd, events): inst = self.socket_map[fd] self.unregister(fd) self.register(fd, inst, events) def poll(self, timeout): # /dev/poll timeout is expressed in milliseconds if timeout is not None: timeout = int(timeout * 1000) _BasePollEpoll.poll(self, timeout) # introduced in python 3.4 if hasattr(select.devpoll, 'close'): def close(self): _IOLoop.close(self) self._poller.close() # =================================================================== # --- epoll() - Linux # =================================================================== if hasattr(select, 'epoll'): class Epoll(_BasePollEpoll): """epoll() based poller.""" READ = select.EPOLLIN WRITE = select.EPOLLOUT _ERROR = select.EPOLLERR | select.EPOLLHUP _poller = select.epoll def fileno(self): """Return epoll() fd.""" return self._poller.fileno() def close(self): _IOLoop.close(self) self._poller.close() # =================================================================== # --- kqueue() - BSD / OSX # =================================================================== if hasattr(select, 'kqueue'): # pragma: no cover class Kqueue(_IOLoop): """kqueue() based poller.""" def __init__(self): _IOLoop.__init__(self) self._kqueue = select.kqueue() self._active = {} def fileno(self): """Return kqueue() fd.""" return self._kqueue.fileno() def close(self): _IOLoop.close(self) self._kqueue.close() def register(self, fd, instance, events): self.socket_map[fd] = instance try: self._control(fd, events, select.KQ_EV_ADD) except EnvironmentError as err: if err.errno == errno.EEXIST: debug("call: register(); poller raised EEXIST; ignored", self) else: raise self._active[fd] = events def unregister(self, fd): try: del self.socket_map[fd] events = self._active.pop(fd) except KeyError: pass else: try: self._control(fd, events, select.KQ_EV_DELETE) except EnvironmentError as err: if err.errno in (errno.ENOENT, errno.EBADF): debug("call: unregister(); poller returned %r; " "ignoring it" % err, self) else: raise def modify(self, fd, events): instance = self.socket_map[fd] self.unregister(fd) self.register(fd, instance, events) def _control(self, fd, events, flags): kevents = [] if events & self.WRITE: kevents.append(select.kevent( fd, filter=select.KQ_FILTER_WRITE, flags=flags)) if events & self.READ or not kevents: # always read when there is not a write kevents.append(select.kevent( fd, filter=select.KQ_FILTER_READ, flags=flags)) # even though control() takes a list, it seems to return # EINVAL on Mac OS X (10.6) when there is more than one # event in the list for kevent in kevents: self._kqueue.control([kevent], 0) # localize variable access to minimize overhead def poll(self, timeout, _len=len, _READ=select.KQ_FILTER_READ, _WRITE=select.KQ_FILTER_WRITE, _EOF=select.KQ_EV_EOF, _ERROR=select.KQ_EV_ERROR): try: kevents = self._kqueue.control(None, _len(self.socket_map), timeout) except OSError as err: if err.errno == errno.EINTR: return raise for kevent in kevents: inst = self.socket_map.get(kevent.ident) if inst is None: continue if kevent.filter == _READ: if inst.readable(): _read(inst) if kevent.filter == _WRITE: if kevent.flags & _EOF: # If an asynchronous connection is refused, # kqueue returns a write event with the EOF # flag set. # Note that for read events, EOF may be returned # before all data has been consumed from the # socket buffer, so we only check for EOF on # write events. inst.handle_close() else: if inst.writable(): _write(inst) if kevent.flags & _ERROR: inst.handle_close() # =================================================================== # --- choose the better poller for this platform # =================================================================== if hasattr(select, 'epoll'): # epoll() - Linux IOLoop = Epoll elif hasattr(select, 'kqueue'): # kqueue() - BSD / OSX IOLoop = Kqueue elif hasattr(select, 'devpoll'): # /dev/poll - Solaris IOLoop = DevPoll elif hasattr(select, 'poll'): # poll() - POSIX IOLoop = Poll else: # select() - POSIX and Windows IOLoop = Select # =================================================================== # --- asyncore dispatchers # =================================================================== # these are overridden in order to register() and unregister() # file descriptors against the new pollers class AsyncChat(asynchat.async_chat): """Same as asynchat.async_chat, only working with the new IO poller and being more clever in avoid registering for read events when it shouldn't. """ def __init__(self, sock=None, ioloop=None): self.ioloop = ioloop or IOLoop.instance() self._wanted_io_events = self.ioloop.READ self._current_io_events = self.ioloop.READ self._closed = False self._closing = False self._fileno = sock.fileno() if sock else None self._tasks = [] asynchat.async_chat.__init__(self, sock) # --- IO loop related methods def add_channel(self, map=None, events=None): assert self._fileno, repr(self._fileno) events = events if events is not None else self.ioloop.READ self.ioloop.register(self._fileno, self, events) self._wanted_io_events = events self._current_io_events = events def del_channel(self, map=None): if self._fileno is not None: self.ioloop.unregister(self._fileno) def modify_ioloop_events(self, events, logdebug=False): if not self._closed: assert self._fileno, repr(self._fileno) if self._fileno not in self.ioloop.socket_map: debug( "call: modify_ioloop_events(), fd was no longer in " "socket_map, had to register() it again", inst=self) self.add_channel(events=events) else: if events != self._current_io_events: if logdebug: if events == self.ioloop.READ: ev = "R" elif events == self.ioloop.WRITE: ev = "W" elif events == self.ioloop.READ | self.ioloop.WRITE: ev = "RW" else: ev = events debug("call: IOLoop.modify(); setting %r IO events" % ( ev), self) self.ioloop.modify(self._fileno, events) self._current_io_events = events else: debug( "call: modify_ioloop_events(), handler had already been " "close()d, skipping modify()", inst=self) # --- utils def call_later(self, seconds, target, *args, **kwargs): """Same as self.ioloop.call_later but also cancel()s the scheduled function on close(). """ if '_errback' not in kwargs and hasattr(self, 'handle_error'): kwargs['_errback'] = self.handle_error callback = self.ioloop.call_later(seconds, target, *args, **kwargs) self._tasks.append(callback) return callback # --- overridden asynchat methods def connect(self, addr): self.modify_ioloop_events(self.ioloop.WRITE) asynchat.async_chat.connect(self, addr) def connect_af_unspecified(self, addr, source_address=None): """Same as connect() but guesses address family from addr. Return the address family just determined. """ assert self.socket is None host, port = addr err = "getaddrinfo() returned an empty list" info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE) for res in info: self.socket = None af, socktype, proto, canonname, sa = res try: self.create_socket(af, socktype) if source_address: if source_address[0].startswith('::ffff:'): # In this scenario, the server has an IPv6 socket, but # the remote client is using IPv4 and its address is # represented as an IPv4-mapped IPv6 address which # looks like this ::ffff:151.12.5.65, see: # http://en.wikipedia.org/wiki/IPv6\ # IPv4-mapped_addresses # http://tools.ietf.org/html/rfc3493.html#section-3.7 # We truncate the first bytes to make it look like a # common IPv4 address. source_address = (source_address[0][7:], source_address[1]) self.bind(source_address) self.connect((host, port)) except socket.error as _: err = _ if self.socket is not None: self.socket.close() self.del_channel() self.socket = None continue break if self.socket is None: self.del_channel() raise socket.error(err) return af # send() and recv() overridden as a fix around various bugs: # - http://bugs.python.org/issue1736101 # - https://github.com/giampaolo/pyftpdlib/issues/104 # - https://github.com/giampaolo/pyftpdlib/issues/109 def send(self, data): try: return self.socket.send(data) except socket.error as err: debug("call: send(), err: %s" % err, inst=self) if err.errno in _ERRNOS_RETRY: return 0 elif err.errno in _ERRNOS_DISCONNECTED: self.handle_close() return 0 else: raise def recv(self, buffer_size): try: data = self.socket.recv(buffer_size) except socket.error as err: debug("call: recv(), err: %s" % err, inst=self) if err.errno in _ERRNOS_DISCONNECTED: self.handle_close() return b'' elif err.errno in _ERRNOS_RETRY: raise RetryError else: raise else: if not data: # a closed connection is indicated by signaling # a read condition, and having recv() return 0. self.handle_close() return b'' else: return data def handle_read(self): try: asynchat.async_chat.handle_read(self) except RetryError: # This can be raised by (the overridden) recv(). pass def initiate_send(self): asynchat.async_chat.initiate_send(self) if not self._closed: # if there's still data to send we want to be ready # for writing, else we're only intereseted in reading if not self.producer_fifo: wanted = self.ioloop.READ else: # In FTPHandler, we also want to listen for user input # hence the READ. DTPHandler has its own initiate_send() # which will either READ or WRITE. wanted = self.ioloop.READ | self.ioloop.WRITE if self._wanted_io_events != wanted: self.ioloop.modify(self._fileno, wanted) self._wanted_io_events = wanted else: debug("call: initiate_send(); called with no connection", inst=self) def close_when_done(self): if len(self.producer_fifo) == 0: self.handle_close() else: self._closing = True asynchat.async_chat.close_when_done(self) def close(self): if not self._closed: self._closed = True try: asynchat.async_chat.close(self) finally: for fun in self._tasks: try: fun.cancel() except Exception: logger.error(traceback.format_exc()) self._tasks = [] self._closed = True self._closing = False self.connected = False class Connector(AsyncChat): """Same as base AsyncChat and supposed to be used for clients. """ def add_channel(self, map=None, events=None): AsyncChat.add_channel(self, map=map, events=self.ioloop.WRITE) class Acceptor(AsyncChat): """Same as base AsyncChat and supposed to be used to accept new connections. """ def add_channel(self, map=None, events=None): AsyncChat.add_channel(self, map=map, events=self.ioloop.READ) def bind_af_unspecified(self, addr): """Same as bind() but guesses address family from addr. Return the address family just determined. """ assert self.socket is None host, port = addr if host == "": # When using bind() "" is a symbolic name meaning all # available interfaces. People might not know we're # using getaddrinfo() internally, which uses None # instead of "", so we'll make the conversion for them. host = None err = "getaddrinfo() returned an empty list" info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE) for res in info: self.socket = None self.del_channel() af, socktype, proto, canonname, sa = res try: self.create_socket(af, socktype) self.set_reuse_addr() self.bind(sa) except socket.error as _: err = _ if self.socket is not None: self.socket.close() self.del_channel() self.socket = None continue break if self.socket is None: self.del_channel() raise socket.error(err) return af def listen(self, num): AsyncChat.listen(self, num) # XXX - this seems to be necessary, otherwise kqueue.control() # won't return listening fd events try: if isinstance(self.ioloop, Kqueue): self.ioloop.modify(self._fileno, self.ioloop.READ) except NameError: pass def handle_accept(self): try: sock, addr = self.accept() except TypeError: # sometimes accept() might return None, see: # https://github.com/giampaolo/pyftpdlib/issues/91 debug("call: handle_accept(); accept() returned None", self) return except socket.error as err: # ECONNABORTED might be thrown on *BSD, see: # https://github.com/giampaolo/pyftpdlib/issues/105 if err.errno != errno.ECONNABORTED: raise else: debug("call: handle_accept(); accept() returned ECONNABORTED", self) else: # sometimes addr == None instead of (ip, port) (see issue 104) if addr is not None: self.handle_accepted(sock, addr) def handle_accepted(self, sock, addr): sock.close() self.log_info('unhandled accepted event', 'warning') # overridden for convenience; avoid to reuse address on Windows if (os.name in ('nt', 'ce')) or (sys.platform == 'cygwin'): def set_reuse_addr(self): pass pyftpdlib-release-1.5.4/pyftpdlib/log.py000066400000000000000000000130641327314554200202760ustar00rootroot00000000000000# Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. """ Logging support for pyftpdlib, inspired from Tornado's (http://www.tornadoweb.org/). This is not supposed to be imported/used directly. Instead you should use logging.basicConfig before serve_forever(). """ import logging import sys import time try: import curses except ImportError: curses = None from ._compat import unicode # default logger logger = logging.getLogger('pyftpdlib') def _stderr_supports_color(): color = False if curses is not None and sys.stderr.isatty(): try: curses.setupterm() if curses.tigetnum("colors") > 0: color = True except Exception: pass return color # configurable options LEVEL = logging.INFO PREFIX = '[%(levelname)1.1s %(asctime)s]' COLOURED = _stderr_supports_color() TIME_FORMAT = "%Y-%m-%d %H:%M:%S" # taken and adapted from Tornado class LogFormatter(logging.Formatter): """Log formatter used in pyftpdlib. Key features of this formatter are: * Color support when logging to a terminal that supports it. * Timestamps on every log line. * Robust against str/bytes encoding problems. """ def __init__(self, *args, **kwargs): logging.Formatter.__init__(self, *args, **kwargs) self._coloured = COLOURED and _stderr_supports_color() if self._coloured: curses.setupterm() # The curses module has some str/bytes confusion in # python3. Until version 3.2.3, most methods return # bytes, but only accept strings. In addition, we want to # output these strings with the logging module, which # works with unicode strings. The explicit calls to # unicode() below are harmless in python2 but will do the # right conversion in python 3. fg_color = (curses.tigetstr("setaf") or curses.tigetstr("setf") or "") if (3, 0) < sys.version_info < (3, 2, 3): fg_color = unicode(fg_color, "ascii") self._colors = { # blues logging.DEBUG: unicode(curses.tparm(fg_color, 4), "ascii"), # green logging.INFO: unicode(curses.tparm(fg_color, 2), "ascii"), # yellow logging.WARNING: unicode(curses.tparm(fg_color, 3), "ascii"), # red logging.ERROR: unicode(curses.tparm(fg_color, 1), "ascii") } self._normal = unicode(curses.tigetstr("sgr0"), "ascii") def format(self, record): try: record.message = record.getMessage() except Exception as err: record.message = "Bad message (%r): %r" % (err, record.__dict__) record.asctime = time.strftime(TIME_FORMAT, self.converter(record.created)) prefix = PREFIX % record.__dict__ if self._coloured: prefix = (self._colors.get(record.levelno, self._normal) + prefix + self._normal) # Encoding notes: The logging module prefers to work with character # strings, but only enforces that log messages are instances of # basestring. In python 2, non-ascii bytestrings will make # their way through the logging framework until they blow up with # an unhelpful decoding error (with this formatter it happens # when we attach the prefix, but there are other opportunities for # exceptions further along in the framework). # # If a byte string makes it this far, convert it to unicode to # ensure it will make it out to the logs. Use repr() as a fallback # to ensure that all byte strings can be converted successfully, # but don't do it by default so we don't add extra quotes to ascii # bytestrings. This is a bit of a hacky place to do this, but # it's worth it since the encoding errors that would otherwise # result are so useless (and tornado is fond of using utf8-encoded # byte strings wherever possible). try: message = unicode(record.message) except UnicodeDecodeError: message = repr(record.message) formatted = prefix + " " + message if record.exc_info: if not record.exc_text: record.exc_text = self.formatException(record.exc_info) if record.exc_text: formatted = formatted.rstrip() + "\n" + record.exc_text return formatted.replace("\n", "\n ") def debug(s, inst=None): s = "[debug] " + s if inst is not None: s += " (%r)" % inst logger.debug(s) def is_logging_configured(): if logging.getLogger('pyftpdlib').handlers: return True if logging.root.handlers: return True return False # TODO: write tests def config_logging(level=LEVEL, prefix=PREFIX, other_loggers=None): # Little speed up if "%(process)d" not in prefix: logging.logProcesses = False if "%(processName)s" not in prefix: logging.logMultiprocessing = False if "%(thread)d" not in prefix and "%(threadName)s" not in prefix: logging.logThreads = False handler = logging.StreamHandler() handler.setFormatter(LogFormatter()) loggers = [logging.getLogger('pyftpdlib')] if other_loggers is not None: loggers.extend(other_loggers) for logger in loggers: logger.setLevel(level) logger.addHandler(handler) pyftpdlib-release-1.5.4/pyftpdlib/servers.py000066400000000000000000000471431327314554200212130ustar00rootroot00000000000000# Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. """ This module contains the main FTPServer class which listens on a host:port and dispatches the incoming connections to a handler. The concurrency is handled asynchronously by the main process thread, meaning the handler cannot block otherwise the whole server will hang. Other than that we have 2 subclasses changing the asynchronous concurrency model using multiple threads or processes. You might be interested in these in case your code contains blocking parts which cannot be adapted to the base async model or if the underlying filesystem is particularly slow, see: https://github.com/giampaolo/pyftpdlib/issues/197 https://github.com/giampaolo/pyftpdlib/issues/212 Two classes are provided: - ThreadingFTPServer - MultiprocessFTPServer ...spawning a new thread or process every time a client connects. The main thread will be async-based and be used only to accept new connections. Every time a new connection comes in that will be dispatched to a separate thread/process which internally will run its own IO loop. This way the handler handling that connections will be free to block without hanging the whole FTP server. """ import errno import os import select import signal import sys import threading import time import traceback from .ioloop import Acceptor from .ioloop import IOLoop from .log import config_logging from .log import debug from .log import is_logging_configured from .log import logger __all__ = ['FTPServer', 'ThreadedFTPServer'] _BSD = 'bsd' in sys.platform # =================================================================== # --- base class # =================================================================== class FTPServer(Acceptor): """Creates a socket listening on
, dispatching the requests to a (typically FTPHandler class). Depending on the type of address specified IPv4 or IPv6 connections (or both, depending from the underlying system) will be accepted. All relevant session information is stored in class attributes described below. - (int) max_cons: number of maximum simultaneous connections accepted (defaults to 512). Can be set to 0 for unlimited but it is recommended to always have a limit to avoid running out of file descriptors (DoS). - (int) max_cons_per_ip: number of maximum connections accepted for the same IP address (defaults to 0 == unlimited). """ max_cons = 512 max_cons_per_ip = 0 def __init__(self, address_or_socket, handler, ioloop=None, backlog=100): """Creates a socket listening on 'address' dispatching connections to a 'handler'. - (tuple) address_or_socket: the (host, port) pair on which the command channel will listen for incoming connections or an existent socket object. - (instance) handler: the handler class to use. - (instance) ioloop: a pyftpdlib.ioloop.IOLoop instance - (int) backlog: the maximum number of queued connections passed to listen(). If a connection request arrives when the queue is full the client may raise ECONNRESET. Defaults to 5. """ Acceptor.__init__(self, ioloop=ioloop) self.handler = handler self.backlog = backlog self.ip_map = [] # in case of FTPS class not properly configured we want errors # to be raised here rather than later, when client connects if hasattr(handler, 'get_ssl_context'): handler.get_ssl_context() if callable(getattr(address_or_socket, 'listen', None)): sock = address_or_socket sock.setblocking(0) self.set_socket(sock) else: self.bind_af_unspecified(address_or_socket) self.listen(backlog) def __enter__(self): return self def __exit__(self, *args): self.close_all() @property def address(self): return self.socket.getsockname()[:2] def _map_len(self): return len(self.ioloop.socket_map) def _accept_new_cons(self): """Return True if the server is willing to accept new connections.""" if not self.max_cons: return True else: return self._map_len() <= self.max_cons def _log_start(self): def get_fqname(obj): try: return obj.__module__ + "." + obj.__class__.__name__ except AttributeError: try: return obj.__module__ + "." + obj.__name__ except AttributeError: return str(obj) if not is_logging_configured(): # If we get to this point it means the user hasn't # configured any logger. We want logging to be on # by default (stderr). config_logging() if self.handler.passive_ports: pasv_ports = "%s->%s" % (self.handler.passive_ports[0], self.handler.passive_ports[-1]) else: pasv_ports = None addr = self.address if hasattr(self.handler, 'ssl_protocol'): proto = "FTP+SSL" else: proto = "FTP" logger.info(">>> starting %s server on %s:%s, pid=%i <<<" % (proto, addr[0], addr[1], os.getpid())) if ('ThreadedFTPServer' in __all__ and issubclass(self.__class__, ThreadedFTPServer)): logger.info("concurrency model: multi-thread") elif ('MultiprocessFTPServer' in __all__ and issubclass(self.__class__, MultiprocessFTPServer)): logger.info("concurrency model: multi-process") elif issubclass(self.__class__, FTPServer): logger.info("concurrency model: async") logger.info("masquerade (NAT) address: %s", self.handler.masquerade_address) logger.info("passive ports: %s", pasv_ports) logger.debug("poller: %r", get_fqname(self.ioloop)) logger.debug("authorizer: %r", get_fqname(self.handler.authorizer)) if os.name == 'posix': logger.debug("use sendfile(2): %s", self.handler.use_sendfile) logger.debug("handler: %r", get_fqname(self.handler)) logger.debug("max connections: %s", self.max_cons or "unlimited") logger.debug("max connections per ip: %s", self.max_cons_per_ip or "unlimited") logger.debug("timeout: %s", self.handler.timeout or "unlimited") logger.debug("banner: %r", self.handler.banner) logger.debug("max login attempts: %r", self.handler.max_login_attempts) if getattr(self.handler, 'certfile', None): logger.debug("SSL certfile: %r", self.handler.certfile) if getattr(self.handler, 'keyfile', None): logger.debug("SSL keyfile: %r", self.handler.keyfile) def serve_forever(self, timeout=None, blocking=True, handle_exit=True): """Start serving. - (float) timeout: the timeout passed to the underlying IO loop expressed in seconds. - (bool) 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 SystemExit exceptions (generally caused by SIGTERM / SIGINT signals) and gracefully exits after cleaning up resources. Also, logs server start and stop. """ if handle_exit: log = handle_exit and blocking if log: self._log_start() try: self.ioloop.loop(timeout, blocking) except (KeyboardInterrupt, SystemExit): logger.info("received interrupt signal") if blocking: if log: logger.info( ">>> shutting down FTP server (%s active socket " "fds) <<<", self._map_len()) self.close_all() else: self.ioloop.loop(timeout, blocking) def handle_accepted(self, sock, addr): """Called when remote client initiates a connection.""" handler = None ip = None try: handler = self.handler(sock, self, ioloop=self.ioloop) if not handler.connected: return ip = addr[0] self.ip_map.append(ip) # For performance and security reasons we should always set a # limit for the number of file descriptors that socket_map # should contain. When we're running out of such limit we'll # use the last available channel for sending a 421 response # to the client before disconnecting it. if not self._accept_new_cons(): handler.handle_max_cons() return # accept only a limited number of connections from the same # source address. if self.max_cons_per_ip: if self.ip_map.count(ip) > self.max_cons_per_ip: handler.handle_max_cons_per_ip() return try: handler.handle() except Exception: handler.handle_error() else: return handler except Exception: # This is supposed to be an application bug that should # be fixed. We do not want to tear down the server though # (DoS). We just log the exception, hoping that someone # will eventually file a bug. References: # - https://github.com/giampaolo/pyftpdlib/issues/143 # - https://github.com/giampaolo/pyftpdlib/issues/166 # - https://groups.google.com/forum/#!topic/pyftpdlib/h7pPybzAx14 logger.error(traceback.format_exc()) if handler is not None: handler.close() else: if ip is not None and ip in self.ip_map: self.ip_map.remove(ip) def handle_error(self): """Called to handle any uncaught exceptions.""" try: raise except Exception: logger.error(traceback.format_exc()) self.close() def close_all(self): """Stop serving and also disconnects all currently connected clients. """ return self.ioloop.close() # =================================================================== # --- extra implementations # =================================================================== class _SpawnerBase(FTPServer): """Base class shared by multiple threads/process dispatcher. Not supposed to be used. """ # How many seconds to wait when join()ing parent's threads # or processes. join_timeout = 5 # How often thread/process finished tasks should be cleaned up. refresh_interval = 5 _lock = None _exit = None def __init__(self, address_or_socket, handler, ioloop=None, backlog=100): FTPServer.__init__(self, address_or_socket, handler, ioloop=ioloop, backlog=backlog) self._active_tasks = [] self._active_tasks_idler = self.ioloop.call_every( self.refresh_interval, self._refresh_tasks, _errback=self.handle_error) def _start_task(self, *args, **kwargs): raise NotImplementedError('must be implemented in subclass') def _map_len(self): if len(self._active_tasks) >= self.max_cons: # Since refresh()ing is a potentially expensive operation # (O(N)) do it only if we're exceeding max connections # limit. Other than in here, tasks are refreshed every 10 # seconds anyway. self._refresh_tasks() return len(self._active_tasks) def _refresh_tasks(self): """join() terminated tasks and update internal _tasks list. This gets called every X secs. """ if self._active_tasks: logger.debug("refreshing tasks (%s join() potentials)" % len(self._active_tasks)) with self._lock: new = [] for t in self._active_tasks: if not t.is_alive(): self._join_task(t) else: new.append(t) self._active_tasks = new def _loop(self, handler): """Serve handler's IO loop in a separate thread or process.""" with IOLoop() as ioloop: handler.ioloop = ioloop try: handler.add_channel() except EnvironmentError as err: if err.errno == errno.EBADF: # we might get here in case the other end quickly # disconnected (see test_quick_connect()) debug("call: %s._loop(); add_channel() returned EBADF", self) return else: raise # Here we localize variable access to minimize overhead. poll = ioloop.poll sched_poll = ioloop.sched.poll poll_timeout = getattr(self, 'poll_timeout', None) soonest_timeout = poll_timeout while (ioloop.socket_map or ioloop.sched._tasks) and \ not self._exit.is_set(): try: if ioloop.socket_map: poll(timeout=soonest_timeout) if ioloop.sched._tasks: soonest_timeout = sched_poll() # Handle the case where socket_map is emty but some # cancelled scheduled calls are still around causing # this while loop to hog CPU resources. # In theory this should never happen as all the sched # functions are supposed to be cancel()ed on close() # but by using threads we can incur into # synchronization issues such as this one. # https://github.com/giampaolo/pyftpdlib/issues/245 if not ioloop.socket_map: # get rid of cancel()led calls ioloop.sched.reheapify() soonest_timeout = sched_poll() if soonest_timeout: time.sleep(min(soonest_timeout, 1)) else: soonest_timeout = None except (KeyboardInterrupt, SystemExit): # note: these two exceptions are raised in all sub # processes self._exit.set() except select.error as err: # on Windows we can get WSAENOTSOCK if the client # rapidly connect and disconnects if os.name == 'nt' and err[0] == 10038: for fd in list(ioloop.socket_map.keys()): try: select.select([fd], [], [], 0) except select.error: try: logger.info("discarding broken socket %r", ioloop.socket_map[fd]) del ioloop.socket_map[fd] except KeyError: # dict changed during iteration pass else: raise else: if poll_timeout: if (soonest_timeout is None or soonest_timeout > poll_timeout): soonest_timeout = poll_timeout def handle_accepted(self, sock, addr): handler = FTPServer.handle_accepted(self, sock, addr) if handler is not None: # unregister the handler from the main IOLoop used by the # main thread to accept connections self.ioloop.unregister(handler._fileno) t = self._start_task(target=self._loop, args=(handler, ), name='ftpd') t.name = repr(addr) t.start() # it is a different process so free resources here if hasattr(t, 'pid'): handler.close() with self._lock: # add the new task self._active_tasks.append(t) def _log_start(self): FTPServer._log_start(self) def serve_forever(self, timeout=1.0, blocking=True, handle_exit=True): self._exit.clear() if handle_exit: log = handle_exit and blocking if log: self._log_start() try: self.ioloop.loop(timeout, blocking) except (KeyboardInterrupt, SystemExit): pass if blocking: if log: logger.info( ">>> shutting down FTP server (%s active workers) <<<", self._map_len()) self.close_all() else: self.ioloop.loop(timeout, blocking) def _terminate_task(self, t): if hasattr(t, 'terminate'): logger.debug("terminate()ing task %r" % t) try: if not _BSD: t.terminate() else: # XXX - On FreeBSD using SIGTERM doesn't work # as the process hangs on kqueue.control() or # select.select(). Use SIGKILL instead. os.kill(t.pid, signal.SIGKILL) except OSError as err: if err.errno != errno.ESRCH: raise def _join_task(self, t): logger.debug("join()ing task %r" % t) t.join(self.join_timeout) if t.is_alive(): logger.warning("task %r remained alive after %r secs", t, self.join_timeout) def close_all(self): self._active_tasks_idler.cancel() # this must be set after getting active tasks as it causes # thread objects to get out of the list too soon self._exit.set() with self._lock: for t in self._active_tasks: self._terminate_task(t) for t in self._active_tasks: self._join_task(t) del self._active_tasks[:] FTPServer.close_all(self) class ThreadedFTPServer(_SpawnerBase): """A modified version of base FTPServer class which spawns a thread every time a new connection is established. """ # The timeout passed to thread's IOLoop.poll() call on every # loop. Necessary since threads ignore KeyboardInterrupt. poll_timeout = 1.0 _lock = threading.Lock() _exit = threading.Event() # compatibility with python <= 2.6 if not hasattr(_exit, 'is_set'): _exit.is_set = _exit.isSet def _start_task(self, *args, **kwargs): return threading.Thread(*args, **kwargs) if os.name == 'posix': import multiprocessing __all__ += ['MultiprocessFTPServer'] class MultiprocessFTPServer(_SpawnerBase): """A modified version of base FTPServer class which spawns a process every time a new connection is established. """ _lock = multiprocessing.Lock() _exit = multiprocessing.Event() def _start_task(self, *args, **kwargs): return multiprocessing.Process(*args, **kwargs) pyftpdlib-release-1.5.4/pyftpdlib/test/000077500000000000000000000000001327314554200201165ustar00rootroot00000000000000pyftpdlib-release-1.5.4/pyftpdlib/test/README000066400000000000000000000002211327314554200207710ustar00rootroot00000000000000RUNNNING TESTS ============== In order to run these tests cd into project root directory then run "make test" (this is also valid for Windows). pyftpdlib-release-1.5.4/pyftpdlib/test/__init__.py000066400000000000000000000250361327314554200222350ustar00rootroot00000000000000# Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. import contextlib import errno import functools import logging import multiprocessing import os import shutil import socket import sys import tempfile import threading import time try: from unittest import mock # py3 except ImportError: import mock # NOQA - requires "pip install mock" from pyftpdlib._compat import getcwdu from pyftpdlib._compat import u from pyftpdlib.authorizers import DummyAuthorizer from pyftpdlib.handlers import FTPHandler from pyftpdlib.ioloop import IOLoop from pyftpdlib.servers import FTPServer import psutil if sys.version_info < (2, 7): import unittest2 as unittest # pip install unittest2 else: import unittest if not hasattr(unittest.TestCase, "assertRaisesRegex"): unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp if os.name == 'posix': import sendfile else: sendfile = None # Attempt to use IP rather than hostname (test suite will run a lot faster) try: HOST = socket.gethostbyname('localhost') except socket.error: HOST = 'localhost' USER = 'user' PASSWD = '12345' HOME = getcwdu() TESTFN = 'tmp-pyftpdlib' TESTFN_UNICODE = TESTFN + '-unicode-' + '\xe2\x98\x83' TESTFN_UNICODE_2 = TESTFN_UNICODE + '-2' TIMEOUT = 2 BUFSIZE = 1024 INTERRUPTED_TRANSF_SIZE = 32768 NO_RETRIES = 5 OSX = sys.platform.startswith("darwin") POSIX = os.name == 'posix' WINDOWS = os.name == 'nt' TRAVIS = bool(os.environ.get('TRAVIS')) VERBOSITY = 1 if os.getenv('SILENT') else 2 class TestCase(unittest.TestCase): def __str__(self): return "%s.%s.%s" % ( self.__class__.__module__, self.__class__.__name__, self._testMethodName) # Hack that overrides default unittest.TestCase in order to print # a full path representation of the single unit tests being run. unittest.TestCase = TestCase def try_address(host, port=0, family=socket.AF_INET): """Try to bind a socket on the given host:port and return True if that has been possible.""" try: with contextlib.closing(socket.socket(family)) as sock: sock.bind((host, port)) except (socket.error, socket.gaierror): return False else: return True SUPPORTS_IPV4 = try_address('127.0.0.1') SUPPORTS_IPV6 = socket.has_ipv6 and try_address('::1', family=socket.AF_INET6) SUPPORTS_SENDFILE = hasattr(os, 'sendfile') or sendfile is not None def safe_remove(*files): "Convenience function for removing temporary test files" for file in files: try: os.remove(file) except OSError as err: if os.name == 'nt': return if err.errno != errno.ENOENT: raise def safe_rmdir(dir): "Convenience function for removing temporary test directories" try: os.rmdir(dir) except OSError as err: if os.name == 'nt': return if err.errno != errno.ENOENT: raise def safe_mkdir(dir): "Convenience function for creating a directory" try: os.mkdir(dir) except OSError as err: if err.errno != errno.EEXIST: raise def touch(name): """Create a file and return its name.""" with open(name, 'w') as f: return f.name def remove_test_files(): """Remove files and directores created during tests.""" for name in os.listdir(u('.')): if name.startswith(tempfile.template): if os.path.isdir(name): shutil.rmtree(name) else: safe_remove(name) def configure_logging(): """Set pyftpdlib logger to "WARNING" level.""" channel = logging.StreamHandler() logger = logging.getLogger('pyftpdlib') logger.setLevel(logging.WARNING) logger.addHandler(channel) def disable_log_warning(fun): """Temporarily set FTP server's logging level to ERROR.""" @functools.wraps(fun) def wrapper(self, *args, **kwargs): logger = logging.getLogger('pyftpdlib') level = logger.getEffectiveLevel() logger.setLevel(logging.ERROR) try: return fun(self, *args, **kwargs) finally: logger.setLevel(level) return wrapper def cleanup(): """Cleanup function executed on interpreter exit.""" remove_test_files() map = IOLoop.instance().socket_map for x in list(map.values()): try: sys.stderr.write("garbage: %s\n" % repr(x)) x.close() except Exception: pass map.clear() def retry_on_failure(ntimes=None): """Decorator to retry a test in case of failure.""" def decorator(fun): @functools.wraps(fun) def wrapper(*args, **kwargs): for x in range(ntimes or NO_RETRIES): try: return fun(*args, **kwargs) except AssertionError as _: err = _ raise err return wrapper return decorator def call_until(fun, expr, timeout=TIMEOUT): """Keep calling function for timeout secs and exit if eval() expression is True. """ stop_at = time.time() + timeout while time.time() < stop_at: ret = fun() if eval(expr): return ret time.sleep(0.001) raise RuntimeError('timed out (ret=%r)' % ret) def get_server_handler(): """Return the first FTPHandler instance running in the IOLoop.""" ioloop = IOLoop.instance() for fd in ioloop.socket_map: instance = ioloop.socket_map[fd] if isinstance(instance, FTPHandler): return instance raise RuntimeError("can't find any FTPHandler instance") # commented out as per bug http://bugs.python.org/issue10354 # tempfile.template = 'tmp-pyftpdlib' def setup_server(handler, server_class, addr=None): addr = (HOST, 0) if addr is None else addr authorizer = DummyAuthorizer() # full perms authorizer.add_user(USER, PASSWD, HOME, perm='elradfmwMT') authorizer.add_anonymous(HOME) handler.authorizer = authorizer handler.auth_failed_timeout = 0.001 # lower buffer sizes = more "loops" while transfering data # = less false positives handler.dtp_handler.ac_in_buffer_size = 4096 handler.dtp_handler.ac_out_buffer_size = 4096 server = server_class(addr, handler) return server def assert_free_resources(): ts = threading.enumerate() assert len(ts) == 1, ts p = psutil.Process() children = p.children() assert not children, children cons = p.connections('tcp') assert not cons, cons def reset_server_opts(): # Since all pyftpdlib configurable "options" are class attributes # we reset them at module.class level. import pyftpdlib.handlers import pyftpdlib.servers from pyftpdlib.handlers import _import_sendfile # Control handlers. tls_handler = getattr(pyftpdlib.handlers, "TLS_FTPHandler", pyftpdlib.handlers.FTPHandler) for klass in (pyftpdlib.handlers.FTPHandler, tls_handler): klass.auth_failed_timeout = 0.001 klass.authorizer = DummyAuthorizer() klass.banner = "pyftpdlib ready." klass.masquerade_address = None klass.masquerade_address_map = {} klass.max_login_attempts = 3 klass.passive_ports = None klass.permit_foreign_addresses = False klass.permit_privileged_ports = False klass.tcp_no_delay = hasattr(socket, 'TCP_NODELAY') klass.timeout = 300 klass.unicode_errors = "replace" klass.use_gmt_times = True klass.use_sendfile = _import_sendfile() is not None klass.ac_in_buffer_size = 4096 klass.ac_out_buffer_size = 4096 if klass.__name__ == 'TLS_FTPHandler': klass.tls_control_required = False klass.tls_data_required = False # Data handlers. tls_handler = getattr(pyftpdlib.handlers, "TLS_DTPHandler", pyftpdlib.handlers.DTPHandler) for klass in (pyftpdlib.handlers.DTPHandler, tls_handler): klass.timeout = 300 klass.ac_in_buffer_size = 4096 klass.ac_out_buffer_size = 4096 pyftpdlib.handlers.ThrottledDTPHandler.read_limit = 0 pyftpdlib.handlers.ThrottledDTPHandler.write_limit = 0 pyftpdlib.handlers.ThrottledDTPHandler.auto_sized_buffers = True # Acceptors. ls = [pyftpdlib.servers.FTPServer, pyftpdlib.servers.ThreadedFTPServer] if os.name == 'posix': ls.append(pyftpdlib.servers.MultiprocessFTPServer) for klass in ls: klass.max_cons = 0 klass.max_cons_per_ip = 0 class ThreadedTestFTPd(threading.Thread): """A threaded FTP server used for running tests. This is basically a modified version of the FTPServer class which wraps the polling loop into a thread. The instance returned can be start()ed and stop()ped. """ handler = FTPHandler server_class = FTPServer poll_interval = 0.001 if TRAVIS else 0.000001 # Makes the thread stop on interpreter exit. daemon = True def __init__(self, addr=None): super(ThreadedTestFTPd, self).__init__(name='test-ftpd') self.server = setup_server(self.handler, self.server_class, addr=addr) self.host, self.port = self.server.socket.getsockname()[:2] self.lock = threading.Lock() self._stop_flag = False self._event_stop = threading.Event() def run(self): try: while not self._stop_flag: with self.lock: self.server.serve_forever(timeout=self.poll_interval, blocking=False) finally: self._event_stop.set() def stop(self): self._stop_flag = True # signal the main loop to exit self._event_stop.wait() self.server.close_all() self.join() reset_server_opts() assert_free_resources() class MProcessTestFTPd(multiprocessing.Process): """Same as above but using a sub process instead.""" handler = FTPHandler server_class = FTPServer def __init__(self, addr=None): super(MProcessTestFTPd, self).__init__(name='test-ftpd') self.server = setup_server(self.handler, self.server_class, addr=addr) self.host, self.port = self.server.socket.getsockname()[:2] self._started = False def run(self): assert not self._started self._started = True self.server.serve_forever() def stop(self): self.server.close_all() self.terminate() self.join() reset_server_opts() assert_free_resources() pyftpdlib-release-1.5.4/pyftpdlib/test/keycert.pem000066400000000000000000000035601327314554200222730ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIICXwIBAAKBgQC8ddrhm+LutBvjYcQlnH21PPIseJ1JVG2HMmN2CmZk2YukO+9L opdJhTvbGfEj0DQs1IE8M+kTUyOmuKfVrFMKwtVeCJphrAnhoz7TYOuLBSqt7lVH fhi/VwovESJlaBOp+WMnfhcduPEYHYx/6cnVapIkZnLt30zu2um+DzA9jQIDAQAB AoGBAK0FZpaKj6WnJZN0RqhhK+ggtBWwBnc0U/ozgKz2j1s3fsShYeiGtW6CK5nU D1dZ5wzhbGThI7LiOXDvRucc9n7vUgi0alqPQ/PFodPxAN/eEYkmXQ7W2k7zwsDA IUK0KUhktQbLu8qF/m8qM86ba9y9/9YkXuQbZ3COl5ahTZrhAkEA301P08RKv3KM oXnGU2UHTuJ1MAD2hOrPxjD4/wxA/39EWG9bZczbJyggB4RHu0I3NOSFjAm3HQm0 ANOu5QK9owJBANgOeLfNNcF4pp+UikRFqxk5hULqRAWzVxVrWe85FlPm0VVmHbb/ loif7mqjU8o1jTd/LM7RD9f2usZyE2psaw8CQQCNLhkpX3KO5kKJmS9N7JMZSc4j oog58yeYO8BBqKKzpug0LXuQultYv2K4veaIO04iL9VLe5z9S/Q1jaCHBBuXAkEA z8gjGoi1AOp6PBBLZNsncCvcV/0aC+1se4HxTNo2+duKSDnbq+ljqOM+E7odU+Nq ewvIWOG//e8fssd0mq3HywJBAJ8l/c8GVmrpFTx8r/nZ2Pyyjt3dH1widooDXYSV q6Gbf41Llo5sYAtmxdndTLASuHKecacTgZVhy0FryZpLKrU= -----END RSA PRIVATE KEY----- -----BEGIN CERTIFICATE----- MIICpzCCAhCgAwIBAgIJAP+qStv1cIGNMA0GCSqGSIb3DQEBBQUAMIGJMQswCQYD VQQGEwJVUzERMA8GA1UECBMIRGVsYXdhcmUxEzARBgNVBAcTCldpbG1pbmd0b24x IzAhBgNVBAoTGlB5dGhvbiBTb2Z0d2FyZSBGb3VuZGF0aW9uMQwwCgYDVQQLEwNT U0wxHzAdBgNVBAMTFnNvbWVtYWNoaW5lLnB5dGhvbi5vcmcwHhcNMDcwODI3MTY1 NDUwWhcNMTMwMjE2MTY1NDUwWjCBiTELMAkGA1UEBhMCVVMxETAPBgNVBAgTCERl bGF3YXJlMRMwEQYDVQQHEwpXaWxtaW5ndG9uMSMwIQYDVQQKExpQeXRob24gU29m dHdhcmUgRm91bmRhdGlvbjEMMAoGA1UECxMDU1NMMR8wHQYDVQQDExZzb21lbWFj aGluZS5weXRob24ub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8ddrh m+LutBvjYcQlnH21PPIseJ1JVG2HMmN2CmZk2YukO+9LopdJhTvbGfEj0DQs1IE8 M+kTUyOmuKfVrFMKwtVeCJphrAnhoz7TYOuLBSqt7lVHfhi/VwovESJlaBOp+WMn fhcduPEYHYx/6cnVapIkZnLt30zu2um+DzA9jQIDAQABoxUwEzARBglghkgBhvhC AQEEBAMCBkAwDQYJKoZIhvcNAQEFBQADgYEAF4Q5BVqmCOLv1n8je/Jw9K669VXb 08hyGzQhkemEBYQd6fzQ9A/1ZzHkJKb1P6yreOLSEh4KcxYPyrLRC1ll8nr5OlCx CMhKkTnR6qBsdNV0XtdU2+N25hqW+Ma4ZeqsN/iiJVCGNOZGnvQuvCAGWF8+J/f/ iHkC6gGdBJhogs4= -----END CERTIFICATE----- pyftpdlib-release-1.5.4/pyftpdlib/test/runner.py000066400000000000000000000017651327314554200220120ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. import os import sys from pyftpdlib.test import configure_logging from pyftpdlib.test import remove_test_files from pyftpdlib.test import unittest from pyftpdlib.test import VERBOSITY HERE = os.path.abspath(os.path.dirname(__file__)) def main(): testmodules = [os.path.splitext(x)[0] for x in os.listdir(HERE) if x.endswith('.py') and x.startswith('test_')] configure_logging() remove_test_files() suite = unittest.TestSuite() for t in testmodules: # ...so that "make test" will print the full test paths t = "pyftpdlib.test.%s" % t suite.addTest(unittest.defaultTestLoader.loadTestsFromName(t)) result = unittest.TextTestRunner(verbosity=VERBOSITY).run(suite) return result.wasSuccessful() if __name__ == '__main__': if not main(): sys.exit(1) pyftpdlib-release-1.5.4/pyftpdlib/test/test_authorizers.py000066400000000000000000000567161327314554200241250ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. import os import random import string import sys import tempfile import warnings from pyftpdlib._compat import getcwdu from pyftpdlib._compat import unicode from pyftpdlib.authorizers import AuthenticationFailed from pyftpdlib.authorizers import AuthorizerError from pyftpdlib.authorizers import DummyAuthorizer from pyftpdlib.test import HOME from pyftpdlib.test import PASSWD from pyftpdlib.test import POSIX from pyftpdlib.test import TESTFN from pyftpdlib.test import touch from pyftpdlib.test import unittest from pyftpdlib.test import USER from pyftpdlib.test import VERBOSITY from pyftpdlib.test import WINDOWS if POSIX: import pwd try: from pyftpdlib.authorizers import UnixAuthorizer except ImportError: UnixAuthorizer = None else: UnixAuthorizer = None if WINDOWS: from pywintypes import error as Win32ExtError from pyftpdlib.authorizers import WindowsAuthorizer else: WindowsAuthorizer = None class TestDummyAuthorizer(unittest.TestCase): """Tests for DummyAuthorizer class.""" # temporarily change warnings to exceptions for the purposes of testing def setUp(self): self.tempdir = tempfile.mkdtemp(dir=HOME) self.subtempdir = tempfile.mkdtemp( dir=os.path.join(HOME, self.tempdir)) self.tempfile = touch(os.path.join(self.tempdir, TESTFN)) self.subtempfile = touch(os.path.join(self.subtempdir, TESTFN)) warnings.filterwarnings("error") def tearDown(self): os.remove(self.tempfile) os.remove(self.subtempfile) os.rmdir(self.subtempdir) os.rmdir(self.tempdir) warnings.resetwarnings() def test_common_methods(self): auth = DummyAuthorizer() # create user auth.add_user(USER, PASSWD, HOME) auth.add_anonymous(HOME) # check credentials auth.validate_authentication(USER, PASSWD, None) self.assertRaises(AuthenticationFailed, auth.validate_authentication, USER, 'wrongpwd', None) auth.validate_authentication('anonymous', 'foo', None) auth.validate_authentication('anonymous', '', None) # empty passwd # remove them auth.remove_user(USER) auth.remove_user('anonymous') # raise exc if user does not exists self.assertRaises(KeyError, auth.remove_user, USER) # raise exc if path does not exist self.assertRaisesRegex(ValueError, 'no such directory', auth.add_user, USER, PASSWD, '?:\\') self.assertRaisesRegex(ValueError, 'no such directory', auth.add_anonymous, '?:\\') # raise exc if user already exists auth.add_user(USER, PASSWD, HOME) auth.add_anonymous(HOME) self.assertRaisesRegex(ValueError, 'user %r already exists' % USER, auth.add_user, USER, PASSWD, HOME) self.assertRaisesRegex(ValueError, "user 'anonymous' already exists", auth.add_anonymous, HOME) auth.remove_user(USER) auth.remove_user('anonymous') # raise on wrong permission self.assertRaisesRegex(ValueError, "no such permission", auth.add_user, USER, PASSWD, HOME, perm='?') self.assertRaisesRegex(ValueError, "no such permission", auth.add_anonymous, HOME, perm='?') # expect warning on write permissions assigned to anonymous user for x in "adfmw": self.assertRaisesRegex( RuntimeWarning, "write permissions assigned to anonymous user.", auth.add_anonymous, HOME, perm=x) def test_override_perm_interface(self): auth = DummyAuthorizer() auth.add_user(USER, PASSWD, HOME, perm='elr') # raise exc if user does not exists self.assertRaises(KeyError, auth.override_perm, USER + 'w', HOME, 'elr') # raise exc if path does not exist or it's not a directory self.assertRaisesRegex(ValueError, 'no such directory', auth.override_perm, USER, '?:\\', 'elr') self.assertRaisesRegex(ValueError, 'no such directory', auth.override_perm, USER, self.tempfile, 'elr') # raise on wrong permission self.assertRaisesRegex(ValueError, "no such permission", auth.override_perm, USER, HOME, perm='?') # expect warning on write permissions assigned to anonymous user auth.add_anonymous(HOME) for p in "adfmw": self.assertRaisesRegex( RuntimeWarning, "write permissions assigned to anonymous user.", auth.override_perm, 'anonymous', HOME, p) # raise on attempt to override home directory permissions self.assertRaisesRegex(ValueError, "can't override home directory permissions", auth.override_perm, USER, HOME, perm='w') # raise on attempt to override a path escaping home directory if os.path.dirname(HOME) != HOME: self.assertRaisesRegex(ValueError, "path escapes user home directory", auth.override_perm, USER, os.path.dirname(HOME), perm='w') # try to re-set an overridden permission auth.override_perm(USER, self.tempdir, perm='w') auth.override_perm(USER, self.tempdir, perm='wr') def test_override_perm_recursive_paths(self): auth = DummyAuthorizer() auth.add_user(USER, PASSWD, HOME, perm='elr') self.assertEqual(auth.has_perm(USER, 'w', self.tempdir), False) auth.override_perm(USER, self.tempdir, perm='w', recursive=True) self.assertEqual(auth.has_perm(USER, 'w', HOME), False) self.assertEqual(auth.has_perm(USER, 'w', self.tempdir), True) self.assertEqual(auth.has_perm(USER, 'w', self.tempfile), True) self.assertEqual(auth.has_perm(USER, 'w', self.subtempdir), True) self.assertEqual(auth.has_perm(USER, 'w', self.subtempfile), True) self.assertEqual(auth.has_perm(USER, 'w', HOME + '@'), False) self.assertEqual(auth.has_perm(USER, 'w', self.tempdir + '@'), False) path = os.path.join(self.tempdir + '@', os.path.basename(self.tempfile)) self.assertEqual(auth.has_perm(USER, 'w', path), False) # test case-sensitiveness if (os.name in ('nt', 'ce')) or (sys.platform == 'cygwin'): self.assertTrue(auth.has_perm(USER, 'w', self.tempdir.upper())) def test_override_perm_not_recursive_paths(self): auth = DummyAuthorizer() auth.add_user(USER, PASSWD, HOME, perm='elr') self.assertEqual(auth.has_perm(USER, 'w', self.tempdir), False) auth.override_perm(USER, self.tempdir, perm='w') self.assertEqual(auth.has_perm(USER, 'w', HOME), False) self.assertEqual(auth.has_perm(USER, 'w', self.tempdir), True) self.assertEqual(auth.has_perm(USER, 'w', self.tempfile), True) self.assertEqual(auth.has_perm(USER, 'w', self.subtempdir), False) self.assertEqual(auth.has_perm(USER, 'w', self.subtempfile), False) self.assertEqual(auth.has_perm(USER, 'w', HOME + '@'), False) self.assertEqual(auth.has_perm(USER, 'w', self.tempdir + '@'), False) path = os.path.join(self.tempdir + '@', os.path.basename(self.tempfile)) self.assertEqual(auth.has_perm(USER, 'w', path), False) # test case-sensitiveness if (os.name in ('nt', 'ce')) or (sys.platform == 'cygwin'): self.assertEqual(auth.has_perm(USER, 'w', self.tempdir.upper()), True) class _SharedAuthorizerTests(object): """Tests valid for both UnixAuthorizer and WindowsAuthorizer for those parts which share the same API. """ authorizer_class = None # --- utils def get_users(self): return self.authorizer_class._get_system_users() def get_current_user(self): if POSIX: return pwd.getpwuid(os.getuid()).pw_name else: return os.environ['USERNAME'] def get_current_user_homedir(self): if POSIX: return pwd.getpwuid(os.getuid()).pw_dir else: return os.environ['USERPROFILE'] def get_nonexistent_user(self): # return a user which does not exist on the system users = self.get_users() letters = string.ascii_lowercase while True: user = ''.join([random.choice(letters) for i in range(10)]) if user not in users: return user def assertRaisesWithMsg(self, excClass, msg, callableObj, *args, **kwargs): try: callableObj(*args, **kwargs) except excClass as err: if str(err) == msg: return raise self.failureException("%s != %s" % (str(err), msg)) else: if hasattr(excClass, '__name__'): excName = excClass.__name__ else: excName = str(excClass) raise self.failureException("%s not raised" % excName) # --- /utils def test_get_home_dir(self): auth = self.authorizer_class() home = auth.get_home_dir(self.get_current_user()) self.assertTrue(isinstance(home, unicode)) nonexistent_user = self.get_nonexistent_user() self.assertTrue(os.path.isdir(home)) if auth.has_user('nobody'): home = auth.get_home_dir('nobody') self.assertRaises(AuthorizerError, auth.get_home_dir, nonexistent_user) def test_has_user(self): auth = self.authorizer_class() current_user = self.get_current_user() nonexistent_user = self.get_nonexistent_user() self.assertTrue(auth.has_user(current_user)) self.assertFalse(auth.has_user(nonexistent_user)) auth = self.authorizer_class(rejected_users=[current_user]) self.assertFalse(auth.has_user(current_user)) def test_validate_authentication(self): # can't test for actual success in case of valid authentication # here as we don't have the user password if self.authorizer_class.__name__ == 'UnixAuthorizer': auth = self.authorizer_class(require_valid_shell=False) else: auth = self.authorizer_class() current_user = self.get_current_user() nonexistent_user = self.get_nonexistent_user() self.assertRaises( AuthenticationFailed, auth.validate_authentication, current_user, 'wrongpasswd', None) self.assertRaises( AuthenticationFailed, auth.validate_authentication, nonexistent_user, 'bar', None) def test_impersonate_user(self): auth = self.authorizer_class() nonexistent_user = self.get_nonexistent_user() try: if self.authorizer_class.__name__ == 'UnixAuthorizer': auth.impersonate_user(self.get_current_user(), '') self.assertRaises( AuthorizerError, auth.impersonate_user, nonexistent_user, 'pwd') else: self.assertRaises( Win32ExtError, auth.impersonate_user, nonexistent_user, 'pwd') self.assertRaises( Win32ExtError, auth.impersonate_user, self.get_current_user(), '') finally: auth.terminate_impersonation('') def test_terminate_impersonation(self): auth = self.authorizer_class() auth.terminate_impersonation('') auth.terminate_impersonation('') def test_get_perms(self): auth = self.authorizer_class(global_perm='elr') self.assertTrue('r' in auth.get_perms(self.get_current_user())) self.assertFalse('w' in auth.get_perms(self.get_current_user())) def test_has_perm(self): auth = self.authorizer_class(global_perm='elr') self.assertTrue(auth.has_perm(self.get_current_user(), 'r')) self.assertFalse(auth.has_perm(self.get_current_user(), 'w')) def test_messages(self): auth = self.authorizer_class(msg_login="login", msg_quit="quit") self.assertTrue(auth.get_msg_login, "login") self.assertTrue(auth.get_msg_quit, "quit") def test_error_options(self): wrong_user = self.get_nonexistent_user() self.assertRaisesWithMsg( AuthorizerError, "rejected_users and allowed_users options are mutually exclusive", self.authorizer_class, allowed_users=['foo'], rejected_users=['bar']) self.assertRaisesWithMsg( AuthorizerError, 'invalid username "anonymous"', self.authorizer_class, allowed_users=['anonymous']) self.assertRaisesWithMsg( AuthorizerError, 'invalid username "anonymous"', self.authorizer_class, rejected_users=['anonymous']) self.assertRaisesWithMsg( AuthorizerError, 'unknown user %s' % wrong_user, self.authorizer_class, allowed_users=[wrong_user]) self.assertRaisesWithMsg(AuthorizerError, 'unknown user %s' % wrong_user, self.authorizer_class, rejected_users=[wrong_user]) def test_override_user_password(self): auth = self.authorizer_class() user = self.get_current_user() auth.override_user(user, password='foo') auth.validate_authentication(user, 'foo', None) self.assertRaises(AuthenticationFailed(auth.validate_authentication, user, 'bar', None)) # make sure other settings keep using default values self.assertEqual(auth.get_home_dir(user), self.get_current_user_homedir()) self.assertEqual(auth.get_perms(user), "elradfmwMT") self.assertEqual(auth.get_msg_login(user), "Login successful.") self.assertEqual(auth.get_msg_quit(user), "Goodbye.") def test_override_user_homedir(self): auth = self.authorizer_class() user = self.get_current_user() dir = os.path.dirname(getcwdu()) auth.override_user(user, homedir=dir) self.assertEqual(auth.get_home_dir(user), dir) # make sure other settings keep using default values # self.assertEqual(auth.get_home_dir(user), # self.get_current_user_homedir()) self.assertEqual(auth.get_perms(user), "elradfmwMT") self.assertEqual(auth.get_msg_login(user), "Login successful.") self.assertEqual(auth.get_msg_quit(user), "Goodbye.") def test_override_user_perm(self): auth = self.authorizer_class() user = self.get_current_user() auth.override_user(user, perm="elr") self.assertEqual(auth.get_perms(user), "elr") # make sure other settings keep using default values self.assertEqual(auth.get_home_dir(user), self.get_current_user_homedir()) # self.assertEqual(auth.get_perms(user), "elradfmwMT") self.assertEqual(auth.get_msg_login(user), "Login successful.") self.assertEqual(auth.get_msg_quit(user), "Goodbye.") def test_override_user_msg_login_quit(self): auth = self.authorizer_class() user = self.get_current_user() auth.override_user(user, msg_login="foo", msg_quit="bar") self.assertEqual(auth.get_msg_login(user), "foo") self.assertEqual(auth.get_msg_quit(user), "bar") # make sure other settings keep using default values self.assertEqual(auth.get_home_dir(user), self.get_current_user_homedir()) self.assertEqual(auth.get_perms(user), "elradfmwMT") # self.assertEqual(auth.get_msg_login(user), "Login successful.") # self.assertEqual(auth.get_msg_quit(user), "Goodbye.") def test_override_user_errors(self): if self.authorizer_class.__name__ == 'UnixAuthorizer': auth = self.authorizer_class(require_valid_shell=False) else: auth = self.authorizer_class() this_user = self.get_current_user() for x in self.get_users(): if x != this_user: another_user = x break nonexistent_user = self.get_nonexistent_user() self.assertRaisesWithMsg( AuthorizerError, "at least one keyword argument must be specified", auth.override_user, this_user) self.assertRaisesWithMsg(AuthorizerError, 'no such user %s' % nonexistent_user, auth.override_user, nonexistent_user, perm='r') if self.authorizer_class.__name__ == 'UnixAuthorizer': auth = self.authorizer_class(allowed_users=[this_user], require_valid_shell=False) else: auth = self.authorizer_class(allowed_users=[this_user]) auth.override_user(this_user, perm='r') self.assertRaisesWithMsg(AuthorizerError, '%s is not an allowed user' % another_user, auth.override_user, another_user, perm='r') if self.authorizer_class.__name__ == 'UnixAuthorizer': auth = self.authorizer_class(rejected_users=[this_user], require_valid_shell=False) else: auth = self.authorizer_class(rejected_users=[this_user]) auth.override_user(another_user, perm='r') self.assertRaisesWithMsg(AuthorizerError, '%s is not an allowed user' % this_user, auth.override_user, this_user, perm='r') self.assertRaisesWithMsg(AuthorizerError, "can't assign password to anonymous user", auth.override_user, "anonymous", password='foo') # ===================================================================== # --- UNIX authorizer # ===================================================================== @unittest.skipUnless(POSIX, "UNIX only") @unittest.skipUnless(UnixAuthorizer is not None, "UnixAuthorizer class not available") class TestUnixAuthorizer(_SharedAuthorizerTests, unittest.TestCase): """Unix authorizer specific tests.""" authorizer_class = UnixAuthorizer def setUp(self): try: UnixAuthorizer() except AuthorizerError: # not root self.skipTest("need root access") def test_get_perms_anonymous(self): auth = UnixAuthorizer( global_perm='elr', anonymous_user=self.get_current_user()) self.assertTrue('e' in auth.get_perms('anonymous')) self.assertFalse('w' in auth.get_perms('anonymous')) warnings.filterwarnings("ignore") auth.override_user('anonymous', perm='w') warnings.resetwarnings() self.assertTrue('w' in auth.get_perms('anonymous')) def test_has_perm_anonymous(self): auth = UnixAuthorizer( global_perm='elr', anonymous_user=self.get_current_user()) self.assertTrue(auth.has_perm(self.get_current_user(), 'r')) self.assertFalse(auth.has_perm(self.get_current_user(), 'w')) self.assertTrue(auth.has_perm('anonymous', 'e')) self.assertFalse(auth.has_perm('anonymous', 'w')) warnings.filterwarnings("ignore") auth.override_user('anonymous', perm='w') warnings.resetwarnings() self.assertTrue(auth.has_perm('anonymous', 'w')) def test_validate_authentication(self): # we can only test for invalid credentials auth = UnixAuthorizer(require_valid_shell=False) self.assertRaises(AuthenticationFailed, auth.validate_authentication, '?!foo', '?!foo', None) auth = UnixAuthorizer(require_valid_shell=True) self.assertRaises(AuthenticationFailed, auth.validate_authentication, '?!foo', '?!foo', None) def test_validate_authentication_anonymous(self): current_user = self.get_current_user() auth = UnixAuthorizer(anonymous_user=current_user, require_valid_shell=False) self.assertRaises(AuthenticationFailed, auth.validate_authentication, 'foo', 'passwd', None) self.assertRaises( AuthenticationFailed, auth.validate_authentication, current_user, 'passwd', None) auth.validate_authentication('anonymous', 'passwd', None) def test_require_valid_shell(self): def get_fake_shell_user(): for user in self.get_users(): shell = pwd.getpwnam(user).pw_shell # On linux fake shell is usually /bin/false, on # freebsd /usr/sbin/nologin; in case of other # UNIX variants test needs to be adjusted. if '/false' in shell or '/nologin' in shell: return user self.fail("no user found") user = get_fake_shell_user() self.assertRaisesWithMsg( AuthorizerError, "user %s has not a valid shell" % user, UnixAuthorizer, allowed_users=[user]) # commented as it first fails for invalid home # self.assertRaisesWithMsg( # ValueError, # "user %s has not a valid shell" % user, # UnixAuthorizer, anonymous_user=user) auth = UnixAuthorizer() self.assertTrue(auth._has_valid_shell(self.get_current_user())) self.assertFalse(auth._has_valid_shell(user)) self.assertRaisesWithMsg(AuthorizerError, "User %s doesn't have a valid shell." % user, auth.override_user, user, perm='r') def test_not_root(self): # UnixAuthorizer is supposed to work only as super user auth = self.authorizer_class() try: auth.impersonate_user('nobody', '') self.assertRaisesWithMsg(AuthorizerError, "super user privileges are required", UnixAuthorizer) finally: auth.terminate_impersonation('nobody') # ===================================================================== # --- Windows authorizer # ===================================================================== @unittest.skipUnless(WINDOWS, "Windows only") class TestWindowsAuthorizer(_SharedAuthorizerTests, unittest.TestCase): """Windows authorizer specific tests.""" authorizer_class = WindowsAuthorizer def test_wrong_anonymous_credentials(self): user = self.get_current_user() self.assertRaises(Win32ExtError, self.authorizer_class, anonymous_user=user, anonymous_password='$|1wrongpasswd') if __name__ == '__main__': unittest.main(verbosity=VERBOSITY) pyftpdlib-release-1.5.4/pyftpdlib/test/test_filesystems.py000066400000000000000000000177101327314554200241040ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. import os import tempfile from pyftpdlib._compat import getcwdu from pyftpdlib._compat import u from pyftpdlib.filesystems import AbstractedFS from pyftpdlib.test import HOME from pyftpdlib.test import POSIX from pyftpdlib.test import safe_remove from pyftpdlib.test import TESTFN from pyftpdlib.test import touch from pyftpdlib.test import unittest from pyftpdlib.test import VERBOSITY if POSIX: from pyftpdlib.filesystems import UnixFilesystem class TestAbstractedFS(unittest.TestCase): """Test for conversion utility methods of AbstractedFS class.""" def setUp(self): safe_remove(TESTFN) tearDown = setUp def test_ftpnorm(self): # Tests for ftpnorm method. ae = self.assertEqual fs = AbstractedFS(u('/'), None) fs._cwd = u('/') ae(fs.ftpnorm(u('')), u('/')) ae(fs.ftpnorm(u('/')), u('/')) ae(fs.ftpnorm(u('.')), u('/')) ae(fs.ftpnorm(u('..')), u('/')) ae(fs.ftpnorm(u('a')), u('/a')) ae(fs.ftpnorm(u('/a')), u('/a')) ae(fs.ftpnorm(u('/a/')), u('/a')) ae(fs.ftpnorm(u('a/..')), u('/')) ae(fs.ftpnorm(u('a/b')), '/a/b') ae(fs.ftpnorm(u('a/b/..')), u('/a')) ae(fs.ftpnorm(u('a/b/../..')), u('/')) fs._cwd = u('/sub') ae(fs.ftpnorm(u('')), u('/sub')) ae(fs.ftpnorm(u('/')), u('/')) ae(fs.ftpnorm(u('.')), u('/sub')) ae(fs.ftpnorm(u('..')), u('/')) ae(fs.ftpnorm(u('a')), u('/sub/a')) ae(fs.ftpnorm(u('a/')), u('/sub/a')) ae(fs.ftpnorm(u('a/..')), u('/sub')) ae(fs.ftpnorm(u('a/b')), u('/sub/a/b')) ae(fs.ftpnorm(u('a/b/')), u('/sub/a/b')) ae(fs.ftpnorm(u('a/b/..')), u('/sub/a')) ae(fs.ftpnorm(u('a/b/../..')), u('/sub')) ae(fs.ftpnorm(u('a/b/../../..')), u('/')) ae(fs.ftpnorm(u('//')), u('/')) # UNC paths must be collapsed def test_ftp2fs(self): # Tests for ftp2fs method. def join(x, y): return os.path.join(x, y.replace('/', os.sep)) ae = self.assertEqual fs = AbstractedFS(u('/'), None) def goforit(root): fs._root = root fs._cwd = u('/') ae(fs.ftp2fs(u('')), root) ae(fs.ftp2fs(u('/')), root) ae(fs.ftp2fs(u('.')), root) ae(fs.ftp2fs(u('..')), root) ae(fs.ftp2fs(u('a')), join(root, u('a'))) ae(fs.ftp2fs(u('/a')), join(root, u('a'))) ae(fs.ftp2fs(u('/a/')), join(root, u('a'))) ae(fs.ftp2fs(u('a/..')), root) ae(fs.ftp2fs(u('a/b')), join(root, u(r'a/b'))) ae(fs.ftp2fs(u('/a/b')), join(root, u(r'a/b'))) ae(fs.ftp2fs(u('/a/b/..')), join(root, u('a'))) ae(fs.ftp2fs(u('/a/b/../..')), root) fs._cwd = u('/sub') ae(fs.ftp2fs(u('')), join(root, u('sub'))) ae(fs.ftp2fs(u('/')), root) ae(fs.ftp2fs(u('.')), join(root, u('sub'))) ae(fs.ftp2fs(u('..')), root) ae(fs.ftp2fs(u('a')), join(root, u('sub/a'))) ae(fs.ftp2fs(u('a/')), join(root, u('sub/a'))) ae(fs.ftp2fs(u('a/..')), join(root, u('sub'))) ae(fs.ftp2fs(u('a/b')), join(root, 'sub/a/b')) ae(fs.ftp2fs(u('a/b/..')), join(root, u('sub/a'))) ae(fs.ftp2fs(u('a/b/../..')), join(root, u('sub'))) ae(fs.ftp2fs(u('a/b/../../..')), root) # UNC paths must be collapsed ae(fs.ftp2fs(u('//a')), join(root, u('a'))) if os.sep == '\\': goforit(u(r'C:\dir')) goforit(u('C:\\')) # on DOS-derived filesystems (e.g. Windows) this is the same # as specifying the current drive directory (e.g. 'C:\\') goforit(u('\\')) elif os.sep == '/': goforit(u('/home/user')) goforit(u('/')) else: # os.sep == ':'? Don't know... let's try it anyway goforit(getcwdu()) def test_fs2ftp(self): # Tests for fs2ftp method. def join(x, y): return os.path.join(x, y.replace('/', os.sep)) ae = self.assertEqual fs = AbstractedFS(u('/'), None) def goforit(root): fs._root = root ae(fs.fs2ftp(root), u('/')) ae(fs.fs2ftp(join(root, u('/'))), u('/')) ae(fs.fs2ftp(join(root, u('.'))), u('/')) # can't escape from root ae(fs.fs2ftp(join(root, u('..'))), u('/')) ae(fs.fs2ftp(join(root, u('a'))), u('/a')) ae(fs.fs2ftp(join(root, u('a/'))), u('/a')) ae(fs.fs2ftp(join(root, u('a/..'))), u('/')) ae(fs.fs2ftp(join(root, u('a/b'))), u('/a/b')) ae(fs.fs2ftp(join(root, u('a/b'))), u('/a/b')) ae(fs.fs2ftp(join(root, u('a/b/..'))), u('/a')) ae(fs.fs2ftp(join(root, u('/a/b/../..'))), u('/')) fs._cwd = u('/sub') ae(fs.fs2ftp(join(root, 'a/')), u('/a')) if os.sep == '\\': goforit(u(r'C:\dir')) goforit(u('C:\\')) # on DOS-derived filesystems (e.g. Windows) this is the same # as specifying the current drive directory (e.g. 'C:\\') goforit(u('\\')) fs._root = u(r'C:\dir') ae(fs.fs2ftp(u('C:\\')), u('/')) ae(fs.fs2ftp(u('D:\\')), u('/')) ae(fs.fs2ftp(u('D:\\dir')), u('/')) elif os.sep == '/': goforit(u('/')) if os.path.realpath('/__home/user') != '/__home/user': self.fail('Test skipped (symlinks not allowed).') goforit(u('/__home/user')) fs._root = u('/__home/user') ae(fs.fs2ftp(u('/__home')), u('/')) ae(fs.fs2ftp(u('/')), u('/')) ae(fs.fs2ftp(u('/__home/userx')), u('/')) else: # os.sep == ':'? Don't know... let's try it anyway goforit(getcwdu()) def test_validpath(self): # Tests for validpath method. fs = AbstractedFS(u('/'), None) fs._root = HOME self.assertTrue(fs.validpath(HOME)) self.assertTrue(fs.validpath(HOME + '/')) self.assertFalse(fs.validpath(HOME + 'bar')) if hasattr(os, 'symlink'): def test_validpath_validlink(self): # Test validpath by issuing a symlink pointing to a path # inside the root directory. fs = AbstractedFS(u('/'), None) fs._root = HOME TESTFN2 = TESTFN + '1' try: touch(TESTFN) os.symlink(TESTFN, TESTFN2) self.assertTrue(fs.validpath(u(TESTFN))) finally: safe_remove(TESTFN, TESTFN2) def test_validpath_external_symlink(self): # Test validpath by issuing a symlink pointing to a path # outside the root directory. fs = AbstractedFS(u('/'), None) fs._root = HOME # tempfile should create our file in /tmp directory # which should be outside the user root. If it is # not we just skip the test. with tempfile.NamedTemporaryFile() as file: try: if HOME == os.path.dirname(file.name): return os.symlink(file.name, TESTFN) self.assertFalse(fs.validpath(u(TESTFN))) finally: safe_remove(TESTFN) @unittest.skipUnless(POSIX, "UNIX only") class TestUnixFilesystem(unittest.TestCase): def test_case(self): root = getcwdu() fs = UnixFilesystem(root, None) self.assertEqual(fs.root, root) self.assertEqual(fs.cwd, root) cdup = os.path.dirname(root) self.assertEqual(fs.ftp2fs(u('..')), cdup) self.assertEqual(fs.fs2ftp(root), root) if __name__ == '__main__': unittest.main(verbosity=VERBOSITY) pyftpdlib-release-1.5.4/pyftpdlib/test/test_functional.py000066400000000000000000003234771327314554200237110ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. import contextlib import errno import ftplib import logging import os import random import re import select import shutil import socket import stat import sys import tempfile import time from pyftpdlib._compat import b from pyftpdlib._compat import PY3 from pyftpdlib._compat import u from pyftpdlib.filesystems import AbstractedFS from pyftpdlib.handlers import DTPHandler from pyftpdlib.handlers import FTPHandler from pyftpdlib.handlers import SUPPORTS_HYBRID_IPV6 from pyftpdlib.handlers import ThrottledDTPHandler from pyftpdlib.ioloop import IOLoop from pyftpdlib.servers import FTPServer from pyftpdlib.test import BUFSIZE from pyftpdlib.test import configure_logging from pyftpdlib.test import disable_log_warning from pyftpdlib.test import get_server_handler from pyftpdlib.test import HOME from pyftpdlib.test import HOST from pyftpdlib.test import INTERRUPTED_TRANSF_SIZE from pyftpdlib.test import mock from pyftpdlib.test import MProcessTestFTPd from pyftpdlib.test import OSX from pyftpdlib.test import PASSWD from pyftpdlib.test import POSIX from pyftpdlib.test import remove_test_files from pyftpdlib.test import retry_on_failure from pyftpdlib.test import safe_remove from pyftpdlib.test import safe_rmdir from pyftpdlib.test import SUPPORTS_IPV4 from pyftpdlib.test import SUPPORTS_IPV6 from pyftpdlib.test import SUPPORTS_SENDFILE from pyftpdlib.test import TESTFN from pyftpdlib.test import ThreadedTestFTPd from pyftpdlib.test import TIMEOUT from pyftpdlib.test import touch from pyftpdlib.test import TRAVIS from pyftpdlib.test import unittest from pyftpdlib.test import USER from pyftpdlib.test import VERBOSITY from pyftpdlib.test import WINDOWS try: from StringIO import StringIO as BytesIO except ImportError: from io import BytesIO import ssl if POSIX: import sendfile else: sendfile = None class TestFtpAuthentication(unittest.TestCase): "test: USER, PASS, REIN." server_class = MProcessTestFTPd client_class = ftplib.FTP def setUp(self): self.server = self.server_class() self.server.start() self.client = self.client_class(timeout=TIMEOUT) self.client.connect(self.server.host, self.server.port) self.file = open(TESTFN, 'w+b') self.dummyfile = BytesIO() def tearDown(self): self.client.close() self.server.stop() if not self.file.closed: self.file.close() if not self.dummyfile.closed: self.dummyfile.close() os.remove(TESTFN) def assert_auth_failed(self, user, passwd): self.assertRaisesRegex(ftplib.error_perm, '530 Authentication failed', self.client.login, user, passwd) def test_auth_ok(self): self.client.login(user=USER, passwd=PASSWD) def test_anon_auth(self): self.client.login(user='anonymous', passwd='anon@') self.client.login(user='anonymous', passwd='') # supposed to be case sensitive self.assert_auth_failed('AnoNymouS', 'foo') # empty passwords should be allowed self.client.sendcmd('user anonymous') self.client.sendcmd('pass ') self.client.sendcmd('user anonymous') self.client.sendcmd('pass') def test_auth_failed(self): self.assert_auth_failed(USER, 'wrong') self.assert_auth_failed('wrong', PASSWD) self.assert_auth_failed('wrong', 'wrong') def test_wrong_cmds_order(self): self.assertRaisesRegex(ftplib.error_perm, '503 Login with USER first', self.client.sendcmd, 'pass ' + PASSWD) self.client.login(user=USER, passwd=PASSWD) self.assertRaisesRegex(ftplib.error_perm, "503 User already authenticated.", self.client.sendcmd, 'pass ' + PASSWD) def test_max_auth(self): self.assert_auth_failed(USER, 'wrong') self.assert_auth_failed(USER, 'wrong') self.assert_auth_failed(USER, 'wrong') # If authentication fails for 3 times ftpd disconnects the # client. We can check if that happens by using self.client.sendcmd() # on the 'dead' socket object. If socket object is really # closed it should be raised a socket.error exception (Windows) # or a EOFError exception (Linux). self.client.sock.settimeout(.1) self.assertRaises((socket.error, EOFError), self.client.sendcmd, '') def test_rein(self): self.client.login(user=USER, passwd=PASSWD) self.client.sendcmd('rein') # user not authenticated, error response expected self.assertRaisesRegex(ftplib.error_perm, '530 Log in with USER and PASS first', self.client.sendcmd, 'pwd') # by logging-in again we should be able to execute a # file-system command self.client.login(user=USER, passwd=PASSWD) self.client.sendcmd('pwd') @retry_on_failure() def test_rein_during_transfer(self): # Test REIN while already authenticated and a transfer is # in progress. self.client.login(user=USER, passwd=PASSWD) data = b'abcde12345' * 1000000 self.file.write(data) self.file.close() conn = self.client.transfercmd('retr ' + TESTFN) with contextlib.closing(conn): rein_sent = False bytes_recv = 0 while True: chunk = conn.recv(BUFSIZE) if not chunk: break bytes_recv += len(chunk) self.dummyfile.write(chunk) if bytes_recv > INTERRUPTED_TRANSF_SIZE and not rein_sent: rein_sent = True # flush account, error response expected self.client.sendcmd('rein') self.assertRaisesRegex( ftplib.error_perm, '530 Log in with USER and PASS first', self.client.dir) # a 226 response is expected once tranfer finishes self.assertEqual(self.client.voidresp()[:3], '226') # account is still flushed, error response is still expected self.assertRaisesRegex(ftplib.error_perm, '530 Log in with USER and PASS first', self.client.sendcmd, 'size ' + TESTFN) # by logging-in again we should be able to execute a # filesystem command self.client.login(user=USER, passwd=PASSWD) self.client.sendcmd('pwd') self.dummyfile.seek(0) datafile = self.dummyfile.read() self.assertEqual(len(data), len(datafile)) self.assertEqual(hash(data), hash(datafile)) def test_user(self): # Test USER while already authenticated and no transfer # is in progress. self.client.login(user=USER, passwd=PASSWD) self.client.sendcmd('user ' + USER) # authentication flushed self.assertRaisesRegex(ftplib.error_perm, '530 Log in with USER and PASS first', self.client.sendcmd, 'pwd') self.client.sendcmd('pass ' + PASSWD) self.client.sendcmd('pwd') def test_user_during_transfer(self): # Test USER while already authenticated and a transfer is # in progress. self.client.login(user=USER, passwd=PASSWD) data = b'abcde12345' * 1000000 self.file.write(data) self.file.close() conn = self.client.transfercmd('retr ' + TESTFN) with contextlib.closing(conn): rein_sent = 0 bytes_recv = 0 while True: chunk = conn.recv(BUFSIZE) if not chunk: break bytes_recv += len(chunk) self.dummyfile.write(chunk) # stop transfer while it isn't finished yet if bytes_recv > INTERRUPTED_TRANSF_SIZE and not rein_sent: rein_sent = True # flush account, expect an error response self.client.sendcmd('user ' + USER) self.assertRaisesRegex( ftplib.error_perm, '530 Log in with USER and PASS first', self.client.dir) # a 226 response is expected once transfer finishes self.assertEqual(self.client.voidresp()[:3], '226') # account is still flushed, error response is still expected self.assertRaisesRegex(ftplib.error_perm, '530 Log in with USER and PASS first', self.client.sendcmd, 'pwd') # by logging-in again we should be able to execute a # filesystem command self.client.sendcmd('pass ' + PASSWD) self.client.sendcmd('pwd') self.dummyfile.seek(0) datafile = self.dummyfile.read() self.assertEqual(len(data), len(datafile)) self.assertEqual(hash(data), hash(datafile)) class TestFtpDummyCmds(unittest.TestCase): "test: TYPE, STRU, MODE, NOOP, SYST, ALLO, HELP, SITE HELP" server_class = MProcessTestFTPd client_class = ftplib.FTP def setUp(self): self.server = self.server_class() self.server.start() self.client = self.client_class(timeout=TIMEOUT) self.client.connect(self.server.host, self.server.port) self.client.login(USER, PASSWD) def tearDown(self): self.client.close() self.server.stop() def test_type(self): self.client.sendcmd('type a') self.client.sendcmd('type i') self.client.sendcmd('type l7') self.client.sendcmd('type l8') self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'type ?!?') def test_stru(self): self.client.sendcmd('stru f') self.client.sendcmd('stru F') self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'stru p') self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'stru r') self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'stru ?!?') def test_mode(self): self.client.sendcmd('mode s') self.client.sendcmd('mode S') self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'mode b') self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'mode c') self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'mode ?!?') def test_noop(self): self.client.sendcmd('noop') def test_syst(self): self.client.sendcmd('syst') def test_allo(self): self.client.sendcmd('allo x') def test_quit(self): self.client.sendcmd('quit') def test_help(self): self.client.sendcmd('help') cmd = random.choice(list(FTPHandler.proto_cmds.keys())) self.client.sendcmd('help %s' % cmd) self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'help ?!?') def test_site(self): self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'site') self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'site ?!?') self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'site foo bar') self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'sitefoo bar') def test_site_help(self): self.client.sendcmd('site help') self.client.sendcmd('site help help') self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'site help ?!?') def test_rest(self): # Test error conditions only; resumed data transfers are # tested later. self.client.sendcmd('type i') self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'rest') self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'rest str') self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'rest -1') self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'rest 10.1') # REST is not supposed to be allowed in ASCII mode self.client.sendcmd('type a') self.assertRaisesRegex(ftplib.error_perm, 'not allowed in ASCII mode', self.client.sendcmd, 'rest 10') def test_feat(self): resp = self.client.sendcmd('feat') self.assertTrue('UTF8' in resp) self.assertTrue('TVFS' in resp) def test_opts_feat(self): self.assertRaises( ftplib.error_perm, self.client.sendcmd, 'opts mlst bad_fact') self.assertRaises( ftplib.error_perm, self.client.sendcmd, 'opts mlst type ;') self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'opts not_mlst') # utility function which used for extracting the MLST "facts" # string from the FEAT response def mlst(): resp = self.client.sendcmd('feat') return re.search(r'^\s*MLST\s+(\S+)$', resp, re.MULTILINE).group(1) # we rely on "type", "perm", "size", and "modify" facts which # are those available on all platforms self.assertTrue('type*;perm*;size*;modify*;' in mlst()) self.assertEqual(self.client.sendcmd( 'opts mlst type;'), '200 MLST OPTS type;') self.assertEqual(self.client.sendcmd( 'opts mLSt TypE;'), '200 MLST OPTS type;') self.assertTrue('type*;perm;size;modify;' in mlst()) self.assertEqual(self.client.sendcmd('opts mlst'), '200 MLST OPTS ') self.assertTrue('*' not in mlst()) self.assertEqual( self.client.sendcmd('opts mlst fish;cakes;'), '200 MLST OPTS ') self.assertTrue('*' not in mlst()) self.assertEqual(self.client.sendcmd('opts mlst fish;cakes;type;'), '200 MLST OPTS type;') self.assertTrue('type*;perm;size;modify;' in mlst()) class TestFtpCmdsSemantic(unittest.TestCase): server_class = MProcessTestFTPd client_class = ftplib.FTP arg_cmds = \ ['allo', 'appe', 'dele', 'eprt', 'mdtm', 'mfmt', 'mode', 'mkd', 'opts', 'port', 'rest', 'retr', 'rmd', 'rnfr', 'rnto', 'site', 'size', 'stor', 'stru', 'type', 'user', 'xmkd', 'xrmd', 'site chmod'] def setUp(self): self.server = self.server_class() self.server.start() self.client = self.client_class(timeout=TIMEOUT) self.client.connect(self.server.host, self.server.port) self.client.login(USER, PASSWD) def tearDown(self): self.client.close() self.server.stop() def test_arg_cmds(self): # Test commands requiring an argument. expected = "501 Syntax error: command needs an argument." for cmd in self.arg_cmds: self.client.putcmd(cmd) resp = self.client.getmultiline() self.assertEqual(resp, expected) def test_no_arg_cmds(self): # Test commands accepting no arguments. expected = "501 Syntax error: command does not accept arguments." narg_cmds = ['abor', 'cdup', 'feat', 'noop', 'pasv', 'pwd', 'quit', 'rein', 'syst', 'xcup', 'xpwd'] for cmd in narg_cmds: self.client.putcmd(cmd + ' arg') resp = self.client.getmultiline() self.assertEqual(resp, expected) def test_auth_cmds(self): # Test those commands requiring client to be authenticated. expected = "530 Log in with USER and PASS first." self.client.sendcmd('rein') for cmd in self.server.handler.proto_cmds: cmd = cmd.lower() if cmd in ('feat', 'help', 'noop', 'user', 'pass', 'stat', 'syst', 'quit', 'site', 'site help', 'pbsz', 'auth', 'prot', 'ccc'): continue if cmd in self.arg_cmds: cmd = cmd + ' arg' self.client.putcmd(cmd) resp = self.client.getmultiline() self.assertEqual(resp, expected) def test_no_auth_cmds(self): # Test those commands that do not require client to be authenticated. self.client.sendcmd('rein') for cmd in ('feat', 'help', 'noop', 'stat', 'syst', 'site help'): self.client.sendcmd(cmd) # STAT provided with an argument is equal to LIST hence not allowed # if not authenticated self.assertRaisesRegex(ftplib.error_perm, '530 Log in with USER', self.client.sendcmd, 'stat /') self.client.sendcmd('quit') class TestFtpFsOperations(unittest.TestCase): "test: PWD, CWD, CDUP, SIZE, RNFR, RNTO, DELE, MKD, RMD, MDTM, STAT, MFMT" server_class = MProcessTestFTPd client_class = ftplib.FTP def setUp(self): self.server = self.server_class() self.server.start() self.client = self.client_class(timeout=TIMEOUT) self.client.connect(self.server.host, self.server.port) self.client.login(USER, PASSWD) self.tempfile = os.path.basename(touch(TESTFN)) self.tempdir = os.path.basename(tempfile.mkdtemp(dir=HOME)) def tearDown(self): self.client.close() self.server.stop() safe_remove(self.tempfile) if os.path.exists(self.tempdir): shutil.rmtree(self.tempdir) def test_cwd(self): self.client.cwd(self.tempdir) self.assertEqual(self.client.pwd(), '/' + self.tempdir) self.assertRaises(ftplib.error_perm, self.client.cwd, 'subtempdir') # cwd provided with no arguments is supposed to move us to the # root directory self.client.sendcmd('cwd') self.assertEqual(self.client.pwd(), u('/')) def test_pwd(self): self.assertEqual(self.client.pwd(), u('/')) self.client.cwd(self.tempdir) self.assertEqual(self.client.pwd(), '/' + self.tempdir) def test_cdup(self): subfolder = os.path.basename(tempfile.mkdtemp(dir=self.tempdir)) self.assertEqual(self.client.pwd(), u('/')) self.client.cwd(self.tempdir) self.assertEqual(self.client.pwd(), '/%s' % self.tempdir) self.client.cwd(subfolder) self.assertEqual(self.client.pwd(), '/%s/%s' % (self.tempdir, subfolder)) self.client.sendcmd('cdup') self.assertEqual(self.client.pwd(), '/%s' % self.tempdir) self.client.sendcmd('cdup') self.assertEqual(self.client.pwd(), u('/')) # make sure we can't escape from root directory self.client.sendcmd('cdup') self.assertEqual(self.client.pwd(), u('/')) def test_mkd(self): tempdir = os.path.basename(tempfile.mktemp(dir=HOME)) dirname = self.client.mkd(tempdir) # the 257 response is supposed to include the absolute dirname self.assertEqual(dirname, '/' + tempdir) # make sure we can't create directories which already exist # (probably not really necessary); # let's use a try/except statement to avoid leaving behind # orphaned temporary directory in the event of a test failure. try: self.client.mkd(tempdir) except ftplib.error_perm: os.rmdir(tempdir) # ok else: self.fail('ftplib.error_perm not raised.') def test_rmd(self): self.client.rmd(self.tempdir) self.assertRaises(ftplib.error_perm, self.client.rmd, self.tempfile) # make sure we can't remove the root directory self.assertRaisesRegex(ftplib.error_perm, "Can't remove root directory", self.client.rmd, u('/')) def test_dele(self): self.client.delete(self.tempfile) self.assertRaises(ftplib.error_perm, self.client.delete, self.tempdir) def test_rnfr_rnto(self): # rename file tempname = os.path.basename(tempfile.mktemp(dir=HOME)) self.client.rename(self.tempfile, tempname) self.client.rename(tempname, self.tempfile) # rename dir tempname = os.path.basename(tempfile.mktemp(dir=HOME)) self.client.rename(self.tempdir, tempname) self.client.rename(tempname, self.tempdir) # rnfr/rnto over non-existing paths bogus = os.path.basename(tempfile.mktemp(dir=HOME)) self.assertRaises(ftplib.error_perm, self.client.rename, bogus, '/x') self.assertRaises( ftplib.error_perm, self.client.rename, self.tempfile, u('/')) # rnto sent without first specifying the source self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'rnto ' + self.tempfile) # make sure we can't rename root directory self.assertRaisesRegex(ftplib.error_perm, "Can't rename home directory", self.client.rename, '/', '/x') def test_mdtm(self): self.client.sendcmd('mdtm ' + self.tempfile) bogus = os.path.basename(tempfile.mktemp(dir=HOME)) self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'mdtm ' + bogus) # make sure we can't use mdtm against directories try: self.client.sendcmd('mdtm ' + self.tempdir) except ftplib.error_perm as err: self.assertTrue("not retrievable" in str(err)) else: self.fail('Exception not raised') def test_mfmt(self): # making sure MFMT is able to modify the timestamp for the file test_timestamp = "20170921013410" self.client.sendcmd('mfmt ' + test_timestamp + ' ' + self.tempfile) resp_time = os.path.getmtime(self.tempfile) resp_time_str = time.strftime('%Y%m%d%H%M%S', time.gmtime(resp_time)) self.assertIn(test_timestamp, resp_time_str) def test_invalid_mfmt_timeval(self): # testing MFMT with invalid timeval argument test_timestamp_with_chars = "B017092101341A" test_timestamp_invalid_length = "20170921" try: self.client.sendcmd( 'mfmt ' + test_timestamp_with_chars + ' ' + self.tempfile) except ftplib.error_perm as err: self.assertIn('Invalid time format', str(err)) else: self.fail('Exception not raised') try: self.client.sendcmd( 'mfmt ' + test_timestamp_invalid_length + ' ' + self.tempfile) except ftplib.error_perm as err: self.assertIn('Invalid time format', str(err)) else: self.fail('Exception not raised') def test_missing_mfmt_timeval_arg(self): # testing missing timeval argument try: self.client.sendcmd('mfmt ' + self.tempfile) except ftplib.error_perm as err: self.assertIn('Syntax error', str(err)) else: self.fail('Exception not raised') def test_size(self): self.client.sendcmd('type a') self.assertRaises(ftplib.error_perm, self.client.size, self.tempfile) self.client.sendcmd('type i') self.client.size(self.tempfile) # make sure we can't use size against directories try: self.client.sendcmd('size ' + self.tempdir) except ftplib.error_perm as err: self.assertTrue("not retrievable" in str(err)) else: self.fail('Exception not raised') if not hasattr(os, 'chmod'): def test_site_chmod(self): self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'site chmod 777 ' + self.tempfile) else: def test_site_chmod(self): # not enough args self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'site chmod 777') # bad args self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'site chmod -177 ' + self.tempfile) self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'site chmod 778 ' + self.tempfile) self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'site chmod foo ' + self.tempfile) def getmode(): mode = oct(stat.S_IMODE(os.stat(self.tempfile).st_mode)) if PY3: mode = mode.replace('o', '') return mode # on Windows it is possible to set read-only flag only if WINDOWS: self.client.sendcmd('site chmod 777 ' + self.tempfile) self.assertEqual(getmode(), '0666') self.client.sendcmd('site chmod 444 ' + self.tempfile) self.assertEqual(getmode(), '0444') self.client.sendcmd('site chmod 666 ' + self.tempfile) self.assertEqual(getmode(), '0666') else: self.client.sendcmd('site chmod 777 ' + self.tempfile) self.assertEqual(getmode(), '0777') self.client.sendcmd('site chmod 755 ' + self.tempfile) self.assertEqual(getmode(), '0755') self.client.sendcmd('site chmod 555 ' + self.tempfile) self.assertEqual(getmode(), '0555') class TestFtpStoreData(unittest.TestCase): """Test STOR, STOU, APPE, REST, TYPE.""" server_class = MProcessTestFTPd client_class = ftplib.FTP use_sendfile = None def setUp(self): self.server = self.server_class() if self.use_sendfile is not None: self.server.handler.use_sendfile = self.use_sendfile self.server.start() self.client = self.client_class(timeout=TIMEOUT) self.client.connect(self.server.host, self.server.port) self.client.login(USER, PASSWD) self.dummy_recvfile = BytesIO() self.dummy_sendfile = BytesIO() def tearDown(self): self.client.close() self.server.stop() self.dummy_recvfile.close() self.dummy_sendfile.close() safe_remove(TESTFN) if self.use_sendfile is not None: from pyftpdlib.handlers import _import_sendfile self.server.handler.use_sendfile = _import_sendfile() is not None def test_stor(self): try: data = b'abcde12345' * 100000 self.dummy_sendfile.write(data) self.dummy_sendfile.seek(0) self.client.storbinary('stor ' + TESTFN, self.dummy_sendfile) self.client.retrbinary('retr ' + TESTFN, self.dummy_recvfile.write) self.dummy_recvfile.seek(0) datafile = self.dummy_recvfile.read() self.assertEqual(len(data), len(datafile)) self.assertEqual(hash(data), hash(datafile)) finally: # We do not use os.remove() because file could still be # locked by ftpd thread. If DELE through FTP fails try # os.remove() as last resort. if os.path.exists(TESTFN): try: self.client.delete(TESTFN) except (ftplib.Error, EOFError, socket.error): safe_remove(TESTFN) def test_stor_active(self): # Like test_stor but using PORT self.client.set_pasv(False) self.test_stor() def test_stor_ascii(self): # Test STOR in ASCII mode def store(cmd, fp, blocksize=8192): # like storbinary() except it sends "type a" instead of # "type i" before starting the transfer self.client.voidcmd('type a') with contextlib.closing(self.client.transfercmd(cmd)) as conn: while True: buf = fp.read(blocksize) if not buf: break conn.sendall(buf) return self.client.voidresp() try: data = b'abcde12345\r\n' * 100000 self.dummy_sendfile.write(data) self.dummy_sendfile.seek(0) store('stor ' + TESTFN, self.dummy_sendfile) self.client.retrbinary('retr ' + TESTFN, self.dummy_recvfile.write) expected = data.replace(b'\r\n', b(os.linesep)) self.dummy_recvfile.seek(0) datafile = self.dummy_recvfile.read() self.assertEqual(len(expected), len(datafile)) self.assertEqual(hash(expected), hash(datafile)) finally: # We do not use os.remove() because file could still be # locked by ftpd thread. If DELE through FTP fails try # os.remove() as last resort. if os.path.exists(TESTFN): try: self.client.delete(TESTFN) except (ftplib.Error, EOFError, socket.error): safe_remove(TESTFN) def test_stor_ascii_2(self): # Test that no extra extra carriage returns are added to the # file in ASCII mode in case CRLF gets truncated in two chunks # (issue 116) def store(cmd, fp, blocksize=8192): # like storbinary() except it sends "type a" instead of # "type i" before starting the transfer self.client.voidcmd('type a') with contextlib.closing(self.client.transfercmd(cmd)) as conn: while True: buf = fp.read(blocksize) if not buf: break conn.sendall(buf) return self.client.voidresp() old_buffer = DTPHandler.ac_in_buffer_size try: # set a small buffer so that CRLF gets delivered in two # separate chunks: "CRLF", " f", "oo", " CR", "LF", " b", "ar" DTPHandler.ac_in_buffer_size = 2 data = b'\r\n foo \r\n bar' self.dummy_sendfile.write(data) self.dummy_sendfile.seek(0) store('stor ' + TESTFN, self.dummy_sendfile) expected = data.replace(b'\r\n', b(os.linesep)) self.client.retrbinary('retr ' + TESTFN, self.dummy_recvfile.write) self.dummy_recvfile.seek(0) self.assertEqual(expected, self.dummy_recvfile.read()) finally: DTPHandler.ac_in_buffer_size = old_buffer # We do not use os.remove() because file could still be # locked by ftpd thread. If DELE through FTP fails try # os.remove() as last resort. if os.path.exists(TESTFN): try: self.client.delete(TESTFN) except (ftplib.Error, EOFError, socket.error): safe_remove(TESTFN) def test_stou(self): data = b'abcde12345' * 100000 self.dummy_sendfile.write(data) self.dummy_sendfile.seek(0) self.client.voidcmd('TYPE I') # filename comes in as "1xx FILE: " filename = self.client.sendcmd('stou').split('FILE: ')[1] try: with contextlib.closing(self.client.makeport()) as sock: conn, sockaddr = sock.accept() with contextlib.closing(conn): conn.settimeout(TIMEOUT) if hasattr(self.client_class, 'ssl_version'): conn = ssl.wrap_socket(conn) while True: buf = self.dummy_sendfile.read(8192) if not buf: break conn.sendall(buf) # transfer finished, a 226 response is expected self.assertEqual('226', self.client.voidresp()[:3]) self.client.retrbinary('retr ' + filename, self.dummy_recvfile.write) self.dummy_recvfile.seek(0) datafile = self.dummy_recvfile.read() self.assertEqual(len(data), len(datafile)) self.assertEqual(hash(data), hash(datafile)) finally: # We do not use os.remove() because file could still be # locked by ftpd thread. If DELE through FTP fails try # os.remove() as last resort. if os.path.exists(filename): try: self.client.delete(filename) except (ftplib.Error, EOFError, socket.error): safe_remove(filename) def test_stou_rest(self): # Watch for STOU preceded by REST, which makes no sense. self.client.sendcmd('type i') self.client.sendcmd('rest 10') self.assertRaisesRegex(ftplib.error_temp, "Can't STOU while REST", self.client.sendcmd, 'stou') def test_stou_orphaned_file(self): # Check that no orphaned file gets left behind when STOU fails. # Even if STOU fails the file is first created and then erased. # Since we can't know the name of the file the best way that # we have to test this case is comparing the content of the # directory before and after STOU has been issued. # Assuming that TESTFN is supposed to be a "reserved" file # name we shouldn't get false positives. safe_remove(TESTFN) # login as a limited user in order to make STOU fail self.client.login('anonymous', '@nopasswd') before = os.listdir(HOME) self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'stou ' + TESTFN) after = os.listdir(HOME) if before != after: for file in after: self.assertFalse(file.startswith(TESTFN)) def test_appe(self): try: data1 = b'abcde12345' * 100000 self.dummy_sendfile.write(data1) self.dummy_sendfile.seek(0) self.client.storbinary('stor ' + TESTFN, self.dummy_sendfile) data2 = b'fghil67890' * 100000 self.dummy_sendfile.write(data2) self.dummy_sendfile.seek(len(data1)) self.client.storbinary('appe ' + TESTFN, self.dummy_sendfile) self.client.retrbinary("retr " + TESTFN, self.dummy_recvfile.write) self.dummy_recvfile.seek(0) datafile = self.dummy_recvfile.read() self.assertEqual(len(data1 + data2), len(datafile)) self.assertEqual(hash(data1 + data2), hash(datafile)) finally: # We do not use os.remove() because file could still be # locked by ftpd thread. If DELE through FTP fails try # os.remove() as last resort. if os.path.exists(TESTFN): try: self.client.delete(TESTFN) except (ftplib.Error, EOFError, socket.error): safe_remove(TESTFN) def test_appe_rest(self): # Watch for APPE preceded by REST, which makes no sense. self.client.sendcmd('type i') self.client.sendcmd('rest 10') self.assertRaisesRegex(ftplib.error_temp, "Can't APPE while REST", self.client.sendcmd, 'appe x') def test_rest_on_stor(self): # Test STOR preceded by REST. data = b'abcde12345' * 100000 self.dummy_sendfile.write(data) self.dummy_sendfile.seek(0) self.client.voidcmd('TYPE I') with contextlib.closing( self.client.transfercmd('stor ' + TESTFN)) as conn: bytes_sent = 0 while True: chunk = self.dummy_sendfile.read(BUFSIZE) conn.sendall(chunk) bytes_sent += len(chunk) # stop transfer while it isn't finished yet if bytes_sent >= INTERRUPTED_TRANSF_SIZE or not chunk: break # transfer wasn't finished yet but server can't know this, # hence expect a 226 response self.assertEqual('226', self.client.voidresp()[:3]) # resuming transfer by using a marker value greater than the # file size stored on the server should result in an error # on stor file_size = self.client.size(TESTFN) self.assertEqual(file_size, bytes_sent) self.client.sendcmd('rest %s' % ((file_size + 1))) self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'stor ' + TESTFN) self.client.sendcmd('rest %s' % bytes_sent) self.client.storbinary('stor ' + TESTFN, self.dummy_sendfile) self.client.retrbinary('retr ' + TESTFN, self.dummy_recvfile.write) self.dummy_sendfile.seek(0) self.dummy_recvfile.seek(0) data_sendfile = self.dummy_sendfile.read() data_recvfile = self.dummy_recvfile.read() self.assertEqual(len(data_sendfile), len(data_recvfile)) self.assertEqual(len(data_sendfile), len(data_recvfile)) self.client.delete(TESTFN) def test_failing_rest_on_stor(self): # Test REST -> STOR against a non existing file. if os.path.exists(TESTFN): self.client.delete(TESTFN) self.client.sendcmd('type i') self.client.sendcmd('rest 10') self.assertRaises(ftplib.error_perm, self.client.storbinary, 'stor ' + TESTFN, lambda x: x) # if the first STOR failed because of REST, the REST marker # is supposed to be resetted to 0 self.dummy_sendfile.write(b'x' * 4096) self.dummy_sendfile.seek(0) self.client.storbinary('stor ' + TESTFN, self.dummy_sendfile) def test_quit_during_transfer(self): # RFC-959 states that if QUIT is sent while a transfer is in # progress, the connection must remain open for result response # and the server will then close it. with contextlib.closing( self.client.transfercmd('stor ' + TESTFN)) as conn: conn.sendall(b'abcde12345' * 50000) self.client.sendcmd('quit') conn.sendall(b'abcde12345' * 50000) # expect the response (transfer ok) self.assertEqual('226', self.client.voidresp()[:3]) # Make sure client has been disconnected. # socket.error (Windows) or EOFError (Linux) exception is supposed # to be raised in such a case. self.client.sock.settimeout(.1) self.assertRaises((socket.error, EOFError), self.client.sendcmd, 'noop') def test_stor_empty_file(self): self.client.storbinary('stor ' + TESTFN, self.dummy_sendfile) self.client.quit() with open(TESTFN) as f: self.assertEqual(f.read(), "") @unittest.skipUnless(POSIX, "POSIX only") @unittest.skipIf(sys.version_info < (3, 3) and sendfile is None, "pysendfile not installed") class TestFtpStoreDataNoSendfile(TestFtpStoreData): """Test STOR, STOU, APPE, REST, TYPE not using sendfile().""" use_sendfile = False class TestFtpRetrieveData(unittest.TestCase): "Test RETR, REST, TYPE" server_class = MProcessTestFTPd client_class = ftplib.FTP use_sendfile = None def setUp(self): self.server = self.server_class() if self.use_sendfile is not None: self.server.handler.use_sendfile = self.use_sendfile self.server.start() self.client = self.client_class(timeout=TIMEOUT) self.client.connect(self.server.host, self.server.port) self.client.login(USER, PASSWD) self.file = open(TESTFN, 'w+b') self.dummyfile = BytesIO() def tearDown(self): self.client.close() self.server.stop() if not self.file.closed: self.file.close() if not self.dummyfile.closed: self.dummyfile.close() safe_remove(TESTFN) if self.use_sendfile is not None: from pyftpdlib.handlers import _import_sendfile self.server.handler.use_sendfile = _import_sendfile() is not None def test_retr(self): data = b'abcde12345' * 100000 self.file.write(data) self.file.close() self.client.retrbinary("retr " + TESTFN, self.dummyfile.write) self.dummyfile.seek(0) datafile = self.dummyfile.read() self.assertEqual(len(data), len(datafile)) self.assertEqual(hash(data), hash(datafile)) # attempt to retrieve a file which doesn't exist bogus = os.path.basename(tempfile.mktemp(dir=HOME)) self.assertRaises(ftplib.error_perm, self.client.retrbinary, "retr " + bogus, lambda x: x) def test_retr_ascii(self): # Test RETR in ASCII mode. def retrieve(cmd, callback, blocksize=8192, rest=None): # like retrbinary but uses TYPE A instead self.client.voidcmd('type a') with contextlib.closing( self.client.transfercmd(cmd, rest)) as conn: conn.settimeout(TIMEOUT) while True: data = conn.recv(blocksize) if not data: break callback(data) return self.client.voidresp() data = (b'abcde12345' + b(os.linesep)) * 100000 self.file.write(data) self.file.close() retrieve("retr " + TESTFN, self.dummyfile.write) expected = data.replace(b(os.linesep), b'\r\n') self.dummyfile.seek(0) datafile = self.dummyfile.read() self.assertEqual(len(expected), len(datafile)) self.assertEqual(hash(expected), hash(datafile)) @retry_on_failure() def test_restore_on_retr(self): data = b'abcde12345' * 1000000 self.file.write(data) self.file.close() received_bytes = 0 self.client.voidcmd('TYPE I') with contextlib.closing( self.client.transfercmd('retr ' + TESTFN)) as conn: conn.settimeout(TIMEOUT) while True: chunk = conn.recv(BUFSIZE) if not chunk: break self.dummyfile.write(chunk) received_bytes += len(chunk) if received_bytes >= INTERRUPTED_TRANSF_SIZE: break # transfer wasn't finished yet so we expect a 426 response self.assertEqual(self.client.getline()[:3], "426") # resuming transfer by using a marker value greater than the # file size stored on the server should result in an error # on retr (RFC-1123) file_size = self.client.size(TESTFN) self.client.sendcmd('rest %s' % ((file_size + 1))) self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'retr ' + TESTFN) # test resume self.client.sendcmd('rest %s' % received_bytes) self.client.retrbinary("retr " + TESTFN, self.dummyfile.write) self.dummyfile.seek(0) datafile = self.dummyfile.read() self.assertEqual(len(data), len(datafile)) self.assertEqual(hash(data), hash(datafile)) def test_retr_empty_file(self): self.client.retrbinary("retr " + TESTFN, self.dummyfile.write) self.dummyfile.seek(0) self.assertEqual(self.dummyfile.read(), b"") @unittest.skipUnless(POSIX, "POSIX only") @unittest.skipIf(sys.version_info < (3, 3) and sendfile is None, "pysendfile not installed") class TestFtpRetrieveDataNoSendfile(TestFtpRetrieveData): """Test RETR, REST, TYPE by not using sendfile().""" use_sendfile = False class TestFtpListingCmds(unittest.TestCase): """Test LIST, NLST, argumented STAT.""" server_class = MProcessTestFTPd client_class = ftplib.FTP def setUp(self): self.server = self.server_class() self.server.start() self.client = self.client_class(timeout=TIMEOUT) self.client.connect(self.server.host, self.server.port) self.client.login(USER, PASSWD) touch(TESTFN) def tearDown(self): self.client.close() self.server.stop() os.remove(TESTFN) def _test_listing_cmds(self, cmd): """Tests common to LIST NLST and MLSD commands.""" # assume that no argument has the same meaning of "/" l1 = l2 = [] self.client.retrlines(cmd, l1.append) self.client.retrlines(cmd + ' /', l2.append) self.assertEqual(l1, l2) if cmd.lower() != 'mlsd': # if pathname is a file one line is expected x = [] self.client.retrlines('%s ' % cmd + TESTFN, x.append) self.assertEqual(len(x), 1) self.assertTrue(''.join(x).endswith(TESTFN)) # non-existent path, 550 response is expected bogus = os.path.basename(tempfile.mktemp(dir=HOME)) self.assertRaises(ftplib.error_perm, self.client.retrlines, '%s ' % cmd + bogus, lambda x: x) # for an empty directory we excpect that the data channel is # opened anyway and that no data is received x = [] tempdir = os.path.basename(tempfile.mkdtemp(dir=HOME)) try: self.client.retrlines('%s %s' % (cmd, tempdir), x.append) self.assertEqual(x, []) finally: safe_rmdir(tempdir) def test_nlst(self): # common tests self._test_listing_cmds('nlst') def test_list(self): # common tests self._test_listing_cmds('list') # known incorrect pathname arguments (e.g. old clients) are # expected to be treated as if pathname would be == '/' l1 = l2 = l3 = l4 = l5 = [] self.client.retrlines('list /', l1.append) self.client.retrlines('list -a', l2.append) self.client.retrlines('list -l', l3.append) self.client.retrlines('list -al', l4.append) self.client.retrlines('list -la', l5.append) tot = (l1, l2, l3, l4, l5) for x in range(len(tot) - 1): self.assertEqual(tot[x], tot[x + 1]) def test_mlst(self): # utility function for extracting the line of interest def mlstline(cmd): return self.client.voidcmd(cmd).split('\n')[1] # the fact set must be preceded by a space self.assertTrue(mlstline('mlst').startswith(' ')) # where TVFS is supported, a fully qualified pathname is expected self.assertTrue(mlstline('mlst ' + TESTFN).endswith('/' + TESTFN)) self.assertTrue(mlstline('mlst').endswith('/')) # assume that no argument has the same meaning of "/" self.assertEqual(mlstline('mlst'), mlstline('mlst /')) # non-existent path bogus = os.path.basename(tempfile.mktemp(dir=HOME)) self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'mlst ' + bogus) # test file/dir notations self.assertTrue('type=dir' in mlstline('mlst')) self.assertTrue('type=file' in mlstline('mlst ' + TESTFN)) # let's add some tests for OPTS command self.client.sendcmd('opts mlst type;') self.assertEqual(mlstline('mlst'), ' type=dir; /') # where no facts are present, two leading spaces before the # pathname are required (RFC-3659) self.client.sendcmd('opts mlst') self.assertEqual(mlstline('mlst'), ' /') def test_mlsd(self): # common tests self._test_listing_cmds('mlsd') dir = os.path.basename(tempfile.mkdtemp(dir=HOME)) self.addCleanup(safe_rmdir, dir) try: self.client.retrlines('mlsd ' + TESTFN, lambda x: x) except ftplib.error_perm as err: resp = str(err) # if path is a file a 501 response code is expected self.assertEqual(str(resp)[0:3], "501") else: self.fail("Exception not raised") def test_mlsd_all_facts(self): feat = self.client.sendcmd('feat') # all the facts facts = re.search(r'^\s*MLST\s+(\S+)$', feat, re.MULTILINE).group(1) facts = facts.replace("*;", ";") self.client.sendcmd('opts mlst ' + facts) resp = self.client.sendcmd('mlst') local = facts[:-1].split(";") returned = resp.split("\n")[1].strip()[:-3] returned = [x.split("=")[0] for x in returned.split(";")] self.assertEqual(sorted(local), sorted(returned)) self.assertTrue("type" in resp) self.assertTrue("size" in resp) self.assertTrue("perm" in resp) self.assertTrue("modify" in resp) if POSIX: self.assertTrue("unique" in resp) self.assertTrue("unix.mode" in resp) self.assertTrue("unix.uid" in resp) self.assertTrue("unix.gid" in resp) elif WINDOWS: self.assertTrue("create" in resp) def test_stat(self): # Test STAT provided with argument which is equal to LIST self.client.sendcmd('stat /') self.client.sendcmd('stat ' + TESTFN) self.client.putcmd('stat *') resp = self.client.getmultiline() self.assertEqual(resp, '550 Globbing not supported.') bogus = os.path.basename(tempfile.mktemp(dir=HOME)) self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'stat ' + bogus) def test_unforeseen_time_event(self): # Emulate a case where the file last modification time is prior # to year 1900. This most likely will never happen unless # someone specifically force the last modification time of a # file in some way. # To do so we temporarily override os.path.getmtime so that it # returns a negative value referring to a year prior to 1900. # It causes time.localtime/gmtime to raise a ValueError exception # which is supposed to be handled by server. _getmtime = AbstractedFS.getmtime try: AbstractedFS.getmtime = lambda x, y: -9000000000 self.client.sendcmd('stat /') # test AbstractedFS.format_list() self.client.sendcmd('mlst /') # test AbstractedFS.format_mlsx() # make sure client hasn't been disconnected self.client.sendcmd('noop') finally: AbstractedFS.getmtime = _getmtime class TestFtpAbort(unittest.TestCase): "test: ABOR" server_class = MProcessTestFTPd client_class = ftplib.FTP def setUp(self): self.server = self.server_class() self.server.start() self.client = self.client_class(timeout=TIMEOUT) self.client.connect(self.server.host, self.server.port) self.client.login(USER, PASSWD) def tearDown(self): self.client.close() self.server.stop() def test_abor_no_data(self): # Case 1: ABOR while no data channel is opened: respond with 225. resp = self.client.sendcmd('ABOR') self.assertEqual('225 No transfer to abort.', resp) self.client.retrlines('list', [].append) def test_abor_pasv(self): # Case 2: user sends a PASV, a data-channel socket is listening # but not connected, and ABOR is sent: close listening data # socket, respond with 225. self.client.makepasv() respcode = self.client.sendcmd('ABOR')[:3] self.assertEqual('225', respcode) self.client.retrlines('list', [].append) def test_abor_port(self): # Case 3: data channel opened with PASV or PORT, but ABOR sent # before a data transfer has been started: close data channel, # respond with 225 self.client.set_pasv(0) with contextlib.closing(self.client.makeport()): respcode = self.client.sendcmd('ABOR')[:3] self.assertEqual('225', respcode) self.client.retrlines('list', [].append) def test_abor_during_transfer(self): # Case 4: ABOR while a data transfer on DTP channel is in # progress: close data channel, respond with 426, respond # with 226. data = b'abcde12345' * 1000000 with open(TESTFN, 'w+b') as f: f.write(data) try: self.client.voidcmd('TYPE I') with contextlib.closing( self.client.transfercmd('retr ' + TESTFN)) as conn: bytes_recv = 0 while bytes_recv < 65536: chunk = conn.recv(BUFSIZE) bytes_recv += len(chunk) # stop transfer while it isn't finished yet self.client.putcmd('ABOR') # transfer isn't finished yet so ftpd should respond with 426 self.assertEqual(self.client.getline()[:3], "426") # transfer successfully aborted, so should now respond # with a 226 self.assertEqual('226', self.client.voidresp()[:3]) finally: # We do not use os.remove() because file could still be # locked by ftpd thread. If DELE through FTP fails try # os.remove() as last resort. try: self.client.delete(TESTFN) except (ftplib.Error, EOFError, socket.error): safe_remove(TESTFN) @unittest.skipUnless(hasattr(socket, 'MSG_OOB'), "MSG_OOB not available") @unittest.skipIf(sys.version_info < (2, 6), "does not work on python < 2.6") @unittest.skipIf(OSX, "does not work on OSX") def test_oob_abor(self): # Send ABOR by following the RFC-959 directives of sending # Telnet IP/Synch sequence as OOB data. # On some systems like FreeBSD this happened to be a problem # due to a different SO_OOBINLINE behavior. # On some platforms (e.g. Python CE) the test may fail # although the MSG_OOB constant is defined. self.client.sock.sendall(b(chr(244)), socket.MSG_OOB) self.client.sock.sendall(b(chr(255)), socket.MSG_OOB) self.client.sock.sendall(b'abor\r\n') self.assertEqual(self.client.getresp()[:3], '225') class TestThrottleBandwidth(unittest.TestCase): """Test ThrottledDTPHandler class.""" server_class = MProcessTestFTPd client_class = ftplib.FTP def setUp(self): class CustomDTPHandler(ThrottledDTPHandler): # overridden so that the "awake" callback is executed # immediately; this way we won't introduce any slowdown # and still test the code of interest def _throttle_bandwidth(self, *args, **kwargs): ThrottledDTPHandler._throttle_bandwidth(self, *args, **kwargs) if (self._throttler is not None and not self._throttler.cancelled): self._throttler.call() self._throttler = None self.server = self.server_class() self.server.handler.dtp_handler = CustomDTPHandler self.server.start() self.client = self.client_class(timeout=TIMEOUT) self.client.connect(self.server.host, self.server.port) self.client.login(USER, PASSWD) self.dummyfile = BytesIO() def tearDown(self): self.client.close() self.server.handler.dtp_handler.read_limit = 0 self.server.handler.dtp_handler.write_limit = 0 self.server.handler.dtp_handler = DTPHandler self.server.stop() if not self.dummyfile.closed: self.dummyfile.close() if os.path.exists(TESTFN): os.remove(TESTFN) def test_throttle_send(self): # This test doesn't test the actual speed accuracy, just # awakes all that code which implements the throttling. # with self.server.lock: self.server.handler.dtp_handler.write_limit = 32768 data = b'abcde12345' * 100000 with open(TESTFN, 'wb') as file: file.write(data) self.client.retrbinary("retr " + TESTFN, self.dummyfile.write) self.dummyfile.seek(0) datafile = self.dummyfile.read() self.assertEqual(len(data), len(datafile)) self.assertEqual(hash(data), hash(datafile)) def test_throttle_recv(self): # This test doesn't test the actual speed accuracy, just # awakes all that code which implements the throttling. # with self.server.lock: self.server.handler.dtp_handler.read_limit = 32768 data = b'abcde12345' * 100000 self.dummyfile.write(data) self.dummyfile.seek(0) self.client.storbinary("stor " + TESTFN, self.dummyfile) self.client.quit() # needed to fix occasional failures with open(TESTFN, 'rb') as file: file_data = file.read() self.assertEqual(len(data), len(file_data)) self.assertEqual(hash(data), hash(file_data)) @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") class TestTimeouts(unittest.TestCase): """Test idle-timeout capabilities of control and data channels. Some tests may fail on slow machines. """ server_class = MProcessTestFTPd client_class = ftplib.FTP def setUp(self): self.server = None self.client = None def _setUp(self, idle_timeout=300, data_timeout=300, pasv_timeout=30, port_timeout=30): self.server = self.server_class() self.server.handler.timeout = idle_timeout self.server.handler.dtp_handler.timeout = data_timeout self.server.handler.passive_dtp.timeout = pasv_timeout self.server.handler.active_dtp.timeout = port_timeout self.server.start() self.client = self.client_class(timeout=TIMEOUT) self.client.connect(self.server.host, self.server.port) self.client.login(USER, PASSWD) def tearDown(self): if self.client is not None and self.server is not None: self.client.close() self.server.handler.timeout = 300 self.server.handler.dtp_handler.timeout = 300 self.server.handler.passive_dtp.timeout = 30 self.server.handler.active_dtp.timeout = 30 self.server.stop() # Note: moved later. # def test_idle_timeout(self): # # Test control channel timeout. The client which does not send # # any command within the time specified in FTPHandler.timeout is # # supposed to be kicked off. # self._setUp(idle_timeout=0.1) # # fail if no msg is received within 1 second # self.client.sock.settimeout(1) # data = self.client.sock.recv(BUFSIZE) # self.assertEqual(data, b"421 Control connection timed out.\r\n") # # ensure client has been kicked off # self.assertRaises((socket.error, EOFError), self.client.sendcmd, # 'noop') def test_data_timeout(self): # Test data channel timeout. The client which does not send # or receive any data within the time specified in # DTPHandler.timeout is supposed to be kicked off. self._setUp(data_timeout=0.5 if TRAVIS else 0.1) addr = self.client.makepasv() with contextlib.closing(socket.socket()) as s: s.settimeout(TIMEOUT) s.connect(addr) # fail if no msg is received within 1 second self.client.sock.settimeout(1) data = self.client.sock.recv(BUFSIZE) self.assertEqual(data, b"421 Data connection timed out.\r\n") # ensure client has been kicked off self.assertRaises((socket.error, EOFError), self.client.sendcmd, 'noop') def test_data_timeout_not_reached(self): # Impose a timeout for the data channel, then keep sending data for a # time which is longer than that to make sure that the code checking # whether the transfer stalled for with no progress is executed. self._setUp(data_timeout=0.5 if TRAVIS else 0.1) with contextlib.closing( self.client.transfercmd('stor ' + TESTFN)) as sock: if hasattr(self.client_class, 'ssl_version'): sock = ssl.wrap_socket(sock) try: stop_at = time.time() + 0.2 while time.time() < stop_at: sock.send(b'x' * 1024) sock.close() self.client.voidresp() finally: if os.path.exists(TESTFN): self.client.delete(TESTFN) def test_idle_data_timeout1(self): # Tests that the control connection timeout is suspended while # the data channel is opened self._setUp(idle_timeout=0.5 if TRAVIS else 0.1, data_timeout=0.6 if TRAVIS else 0.2) addr = self.client.makepasv() with contextlib.closing(socket.socket()) as s: s.settimeout(TIMEOUT) s.connect(addr) # fail if no msg is received within 1 second self.client.sock.settimeout(1) data = self.client.sock.recv(BUFSIZE) self.assertEqual(data, b"421 Data connection timed out.\r\n") # ensure client has been kicked off self.assertRaises((socket.error, EOFError), self.client.sendcmd, 'noop') def test_idle_data_timeout2(self): # Tests that the control connection timeout is restarted after # data channel has been closed self._setUp(idle_timeout=0.5 if TRAVIS else 0.1, data_timeout=0.6 if TRAVIS else 0.2) addr = self.client.makepasv() with contextlib.closing(socket.socket()) as s: s.settimeout(TIMEOUT) s.connect(addr) # close data channel self.client.sendcmd('abor') self.client.sock.settimeout(1) data = self.client.sock.recv(BUFSIZE) self.assertEqual(data, b"421 Control connection timed out.\r\n") # ensure client has been kicked off self.assertRaises((socket.error, EOFError), self.client.sendcmd, 'noop') def test_pasv_timeout(self): # Test pasv data channel timeout. The client which does not # connect to the listening data socket within the time specified # in PassiveDTP.timeout is supposed to receive a 421 response. self._setUp(pasv_timeout=0.5 if TRAVIS else 0.1) self.client.makepasv() # fail if no msg is received within 1 second self.client.sock.settimeout(1) data = self.client.sock.recv(BUFSIZE) self.assertEqual(data, b"421 Passive data channel timed out.\r\n") # client is not expected to be kicked off self.client.sendcmd('noop') def test_disabled_idle_timeout(self): self._setUp(idle_timeout=0) self.client.sendcmd('noop') def test_disabled_data_timeout(self): self._setUp(data_timeout=0) addr = self.client.makepasv() with contextlib.closing(socket.socket()) as s: s.settimeout(TIMEOUT) s.connect(addr) def test_disabled_pasv_timeout(self): self._setUp(pasv_timeout=0) self.client.makepasv() # reset passive socket addr = self.client.makepasv() with contextlib.closing(socket.socket()) as s: s.settimeout(TIMEOUT) s.connect(addr) def test_disabled_port_timeout(self): self._setUp(port_timeout=0) with contextlib.closing(self.client.makeport()): with contextlib.closing(self.client.makeport()): pass class TestConfigurableOptions(unittest.TestCase): """Test those daemon options which are commonly modified by user.""" server_class = MProcessTestFTPd client_class = ftplib.FTP def setUp(self): touch(TESTFN) self.server = None self.client = None def connect(self): self.client = self.client_class(timeout=TIMEOUT) self.client.connect(self.server.host, self.server.port) self.client.login(USER, PASSWD) def tearDown(self): if self.client is not None: self.client.close() # set back options to their original value if self.server is not None: self.server.server.max_cons = 0 self.server.server.max_cons_per_ip = 0 self.server.handler.banner = "pyftpdlib ready." self.server.handler.max_login_attempts = 3 self.server.handler.auth_failed_timeout = 5 self.server.handler.masquerade_address = None self.server.handler.masquerade_address_map = {} self.server.handler.permit_privileged_ports = False self.server.handler.permit_foreign_addresses = False self.server.handler.passive_ports = None self.server.handler.use_gmt_times = True self.server.handler.tcp_no_delay = hasattr(socket, 'TCP_NODELAY') self.server.stop() os.remove(TESTFN) @disable_log_warning def test_max_connections(self): # Test FTPServer.max_cons attribute self.server = self.server_class() self.server.server.max_cons = 3 self.server.start() c1 = self.client_class() c2 = self.client_class() c3 = self.client_class() try: c1.connect(self.server.host, self.server.port) c2.connect(self.server.host, self.server.port) self.assertRaises(ftplib.error_temp, c3.connect, self.server.host, self.server.port) # with passive data channel established c2.quit() c1.login(USER, PASSWD) c1.makepasv() self.assertRaises(ftplib.error_temp, c2.connect, self.server.host, self.server.port) # with passive data socket waiting for connection c1.login(USER, PASSWD) c1.sendcmd('pasv') self.assertRaises(ftplib.error_temp, c2.connect, self.server.host, self.server.port) # with active data channel established c1.login(USER, PASSWD) with contextlib.closing(c1.makeport()): self.assertRaises( ftplib.error_temp, c2.connect, self.server.host, self.server.port) finally: for c in (c1, c2, c3): try: c.quit() except (socket.error, EOFError): # already disconnected c.close() @disable_log_warning def test_max_connections_per_ip(self): # Test FTPServer.max_cons_per_ip attribute self.server = self.server_class() self.server.server.max_cons_per_ip = 3 self.server.start() c1 = self.client_class() c2 = self.client_class() c3 = self.client_class() c4 = self.client_class() try: c1.connect(self.server.host, self.server.port) c2.connect(self.server.host, self.server.port) c3.connect(self.server.host, self.server.port) self.assertRaises(ftplib.error_temp, c4.connect, self.server.host, self.server.port) # Make sure client has been disconnected. # socket.error (Windows) or EOFError (Linux) exception is # supposed to be raised in such a case. self.assertRaises((socket.error, EOFError), c4.sendcmd, 'noop') finally: for c in (c1, c2, c3, c4): try: c.quit() except (socket.error, EOFError): # already disconnected c.close() def test_banner(self): # Test FTPHandler.banner attribute self.server = self.server_class() self.server.handler.banner = 'hello there' self.server.start() self.client = self.client_class(timeout=TIMEOUT) self.client.connect(self.server.host, self.server.port) self.assertEqual(self.client.getwelcome()[4:], 'hello there') def test_max_login_attempts(self): # Test FTPHandler.max_login_attempts attribute. self.server = self.server_class() self.server.handler.max_login_attempts = 1 self.server.handler.auth_failed_timeout = 0 self.server.start() self.connect() self.assertRaises(ftplib.error_perm, self.client.login, 'wrong', 'wrong') # socket.error (Windows) or EOFError (Linux) exceptions are # supposed to be raised when attempting to send/recv some data # using a disconnected socket self.assertRaises((socket.error, EOFError), self.client.sendcmd, 'noop') def test_masquerade_address(self): # Test FTPHandler.masquerade_address attribute self.server = self.server_class() self.server.handler.masquerade_address = "256.256.256.256" self.server.start() self.connect() host, port = self.client.makepasv() self.assertEqual(host, "256.256.256.256") def test_masquerade_address_map(self): # Test FTPHandler.masquerade_address_map attribute self.server = self.server_class() self.server.handler.masquerade_address_map = {self.server.host: "128.128.128.128"} self.server.start() self.connect() host, port = self.client.makepasv() self.assertEqual(host, "128.128.128.128") def test_passive_ports(self): # Test FTPHandler.passive_ports attribute self.server = self.server_class() _range = list(range(40000, 60000, 200)) self.server.handler.passive_ports = _range self.server.start() self.connect() self.assertTrue(self.client.makepasv()[1] in _range) self.assertTrue(self.client.makepasv()[1] in _range) self.assertTrue(self.client.makepasv()[1] in _range) self.assertTrue(self.client.makepasv()[1] in _range) @disable_log_warning def test_passive_ports_busy(self): # If the ports in the configured range are busy it is expected # that a kernel-assigned port gets chosen with contextlib.closing(socket.socket()) as s: s.settimeout(TIMEOUT) s.bind((HOST, 0)) port = s.getsockname()[1] self.server = self.server_class() self.server.handler.passive_ports = [port] self.server.start() self.connect() resulting_port = self.client.makepasv()[1] self.assertTrue(port != resulting_port) @retry_on_failure() def test_use_gmt_times(self): # use GMT time self.server = self.server_class() self.server.handler.use_gmt_times = True self.server.start() self.connect() gmt1 = self.client.sendcmd('mdtm ' + TESTFN) gmt2 = self.client.sendcmd('mlst ' + TESTFN) gmt3 = self.client.sendcmd('stat ' + TESTFN) # use local time self.tearDown() self.setUp() self.server = self.server_class() self.server.handler.use_gmt_times = False self.server.start() self.connect() loc1 = self.client.sendcmd('mdtm ' + TESTFN) loc2 = self.client.sendcmd('mlst ' + TESTFN) loc3 = self.client.sendcmd('stat ' + TESTFN) # if we're not in a GMT time zone times are supposed to be # different if time.timezone != 0: self.assertNotEqual(gmt1, loc1) self.assertNotEqual(gmt2, loc2) self.assertNotEqual(gmt3, loc3) # ...otherwise they should be the same else: self.assertEqual(gmt1, loc1) self.assertEqual(gmt2, loc2) self.assertEqual(gmt3, loc3) class TestCallbacks(unittest.TestCase): server_class = MProcessTestFTPd client_class = ftplib.FTP TESTFN_2 = TESTFN + "-2" def setUp(self): class Handler(FTPHandler): def write(self, text): with open(TESTFN, "at") as f: f.write(text) def on_connect(self): self.write("on_connect,") def on_disconnect(self): self.write("on_disconnect,") def on_login(self, username): self.write("on_login:%s," % username) def on_login_failed(self, username, password): self.write("on_login_failed:%s+%s," % (username, password)) def on_logout(self, username): self.write("on_logout:%s," % username) def on_file_sent(self, file): self.write("on_file_sent:%s," % os.path.basename(file)) def on_file_received(self, file): self.write("on_file_received:%s," % os.path.basename(file)) def on_incomplete_file_sent(self, file): self.write( "on_incomplete_file_sent:%s," % os.path.basename(file)) def on_incomplete_file_received(self, file): self.write( "on_incomplete_file_received:%s," % os.path.basename(file)) safe_remove(TESTFN) safe_remove(self.TESTFN_2) self.server = self.server_class() self.server.server.handler = Handler self.server.start() self.client = self.client_class(timeout=TIMEOUT) self.client.connect(self.server.host, self.server.port) def tearDown(self): self.client.close() self.server.stop() safe_remove(TESTFN) safe_remove(self.TESTFN_2) def read_file(self, text): stop_at = time.time() + 1 while time.time() <= stop_at: with open(TESTFN, "rt") as f: data = f.read() if data == text: return time.sleep(0.01) self.fail("data: %r; expected: %r" % (data, text)) def test_on_disconnect(self): self.client.login(USER, PASSWD) self.client.close() self.read_file('on_connect,on_login:%s,on_disconnect,' % USER) def test_on_logout_quit(self): self.client.login(USER, PASSWD) self.client.sendcmd('quit') self.read_file( 'on_connect,on_login:%s,on_logout:%s,on_disconnect,' % ( USER, USER)) def test_on_logout_rein(self): self.client.login(USER, PASSWD) self.client.sendcmd('rein') self.read_file( 'on_connect,on_login:%s,on_logout:%s,' % (USER, USER)) def test_on_logout_no_pass(self): # make sure on_logout() is not called if USER was provided # but not PASS self.client.sendcmd("user foo") self.read_file('on_connect,') def test_on_logout_user_issued_twice(self): # At this point user "user" is logged in. Re-login as anonymous, # then quit and expect queue == ["user", "anonymous"] self.client.login(USER, PASSWD) self.client.login("anonymous") self.read_file( 'on_connect,on_login:%s,on_logout:%s,on_login:anonymous,' % (USER, USER)) def test_on_login_failed(self): self.assertRaises( ftplib.error_perm, self.client.login, 'foo', 'bar?!?') self.read_file('on_connect,on_login_failed:foo+bar?!?,') def test_on_file_received(self): data = b'abcde12345' * 100000 dummyfile = BytesIO() dummyfile.write(data) dummyfile.seek(0) self.client.login(USER, PASSWD) self.client.storbinary('stor ' + self.TESTFN_2, dummyfile) self.read_file( 'on_connect,on_login:%s,on_file_received:%s,' % ( USER, self.TESTFN_2)) def test_on_file_sent(self): self.client.login(USER, PASSWD) data = b'abcde12345' * 100000 with open(self.TESTFN_2, 'wb') as f: f.write(data) self.client.retrbinary("retr " + self.TESTFN_2, lambda x: x) self.read_file( 'on_connect,on_login:%s,on_file_sent:%s,' % (USER, self.TESTFN_2)) def test_on_incomplete_file_received(self): self.client.login(USER, PASSWD) data = b'abcde12345' * 1000000 dummyfile = BytesIO() dummyfile.write(data) dummyfile.seek(0) with contextlib.closing( self.client.transfercmd('stor ' + self.TESTFN_2)) as conn: bytes_sent = 0 while True: chunk = dummyfile.read(BUFSIZE) conn.sendall(chunk) bytes_sent += len(chunk) # stop transfer while it isn't finished yet if bytes_sent >= INTERRUPTED_TRANSF_SIZE or not chunk: self.client.putcmd('abor') break self.assertRaises(ftplib.error_temp, self.client.getresp) # 426 self.read_file( 'on_connect,on_login:%s,on_incomplete_file_received:%s,' % (USER, self.TESTFN_2)) def test_on_incomplete_file_sent(self): self.client.login(USER, PASSWD) data = b'abcde12345' * 1000000 with open(self.TESTFN_2, 'wb') as f: f.write(data) bytes_recv = 0 with contextlib.closing(self.client.transfercmd( "retr " + self.TESTFN_2, None)) as conn: while True: chunk = conn.recv(BUFSIZE) bytes_recv += len(chunk) if bytes_recv >= INTERRUPTED_TRANSF_SIZE or not chunk: break self.assertEqual(self.client.getline()[:3], "426") self.read_file( 'on_connect,on_login:%s,on_incomplete_file_sent:%s,' % (USER, self.TESTFN_2)) class _TestNetworkProtocols(object): """Test PASV, EPSV, PORT and EPRT commands. Do not use this class directly, let TestIPv4Environment and TestIPv6Environment classes use it instead. """ def setUp(self): self.server = self.server_class((self.HOST, 0)) self.server.start() self.client = self.client_class(timeout=TIMEOUT) self.client.connect(self.server.host, self.server.port) self.client.login(USER, PASSWD) if self.client.af == socket.AF_INET: self.proto = "1" self.other_proto = "2" else: self.proto = "2" self.other_proto = "1" def tearDown(self): self.client.close() self.server.stop() def cmdresp(self, cmd): """Send a command and return response, also if the command failed.""" try: return self.client.sendcmd(cmd) except ftplib.Error as err: return str(err) @disable_log_warning def test_eprt(self): if not SUPPORTS_HYBRID_IPV6: # test wrong proto try: self.client.sendcmd('eprt |%s|%s|%s|' % ( self.other_proto, self.server.host, self.server.port)) except ftplib.error_perm as err: self.assertEqual(str(err)[0:3], "522") else: self.fail("Exception not raised") # test bad args msg = "501 Invalid EPRT format." # len('|') > 3 self.assertEqual(self.cmdresp('eprt ||||'), msg) # len('|') < 3 self.assertEqual(self.cmdresp('eprt ||'), msg) # port > 65535 self.assertEqual(self.cmdresp('eprt |%s|%s|65536|' % (self.proto, self.HOST)), msg) # port < 0 self.assertEqual(self.cmdresp('eprt |%s|%s|-1|' % (self.proto, self.HOST)), msg) # port < 1024 resp = self.cmdresp('eprt |%s|%s|222|' % (self.proto, self.HOST)) self.assertEqual(resp[:3], '501') self.assertIn('privileged port', resp) # proto > 2 _cmd = 'eprt |3|%s|%s|' % (self.server.host, self.server.port) self.assertRaises(ftplib.error_perm, self.client.sendcmd, _cmd) if self.proto == '1': # len(ip.octs) > 4 self.assertEqual(self.cmdresp('eprt |1|1.2.3.4.5|2048|'), msg) # ip.oct > 255 self.assertEqual(self.cmdresp('eprt |1|1.2.3.256|2048|'), msg) # bad proto resp = self.cmdresp('eprt |2|1.2.3.256|2048|') self.assertTrue("Network protocol not supported" in resp) # test connection with contextlib.closing(socket.socket(self.client.af)) as sock: sock.bind((self.client.sock.getsockname()[0], 0)) sock.listen(5) sock.settimeout(TIMEOUT) ip, port = sock.getsockname()[:2] self.client.sendcmd('eprt |%s|%s|%s|' % (self.proto, ip, port)) try: s = sock.accept() s[0].close() except socket.timeout: self.fail("Server didn't connect to passive socket") def test_epsv(self): # test wrong proto try: self.client.sendcmd('epsv ' + self.other_proto) except ftplib.error_perm as err: self.assertEqual(str(err)[0:3], "522") else: self.fail("Exception not raised") # proto > 2 self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'epsv 3') # test connection for cmd in ('EPSV', 'EPSV ' + self.proto): host, port = ftplib.parse229(self.client.sendcmd(cmd), self.client.sock.getpeername()) with contextlib.closing( socket.socket(self.client.af, socket.SOCK_STREAM)) as s: s.settimeout(TIMEOUT) s.connect((host, port)) self.client.sendcmd('abor') def test_epsv_all(self): self.client.sendcmd('epsv all') self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'pasv') self.assertRaises(ftplib.error_perm, self.client.sendport, self.HOST, 2000) self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'eprt |%s|%s|%s|' % (self.proto, self.HOST, 2000)) @unittest.skipUnless(SUPPORTS_IPV4, "IPv4 not supported") class TestIPv4Environment(_TestNetworkProtocols, unittest.TestCase): """Test PASV, EPSV, PORT and EPRT commands. Runs tests contained in _TestNetworkProtocols class by using IPv4 plus some additional specific tests. """ server_class = MProcessTestFTPd client_class = ftplib.FTP HOST = '127.0.0.1' @disable_log_warning def test_port_v4(self): # test connection with contextlib.closing(self.client.makeport()): self.client.sendcmd('abor') # test bad arguments ae = self.assertEqual msg = "501 Invalid PORT format." ae(self.cmdresp('port 127,0,0,1,1.1'), msg) # sep != ',' ae(self.cmdresp('port X,0,0,1,1,1'), msg) # value != int ae(self.cmdresp('port 127,0,0,1,1,1,1'), msg) # len(args) > 6 ae(self.cmdresp('port 127,0,0,1'), msg) # len(args) < 6 ae(self.cmdresp('port 256,0,0,1,1,1'), msg) # oct > 255 ae(self.cmdresp('port 127,0,0,1,256,1'), msg) # port > 65535 ae(self.cmdresp('port 127,0,0,1,-1,0'), msg) # port < 0 # port < 1024 resp = self.cmdresp('port %s,1,1' % self.HOST.replace('.', ',')) self.assertEqual(resp[:3], '501') self.assertIn('privileged port', resp) if "1.2.3.4" != self.HOST: resp = self.cmdresp('port 1,2,3,4,4,4') assert 'foreign address' in resp, resp @disable_log_warning def test_eprt_v4(self): resp = self.cmdresp('eprt |1|0.10.10.10|2222|') self.assertEqual(resp[:3], '501') self.assertIn('foreign address', resp) def test_pasv_v4(self): host, port = ftplib.parse227(self.client.sendcmd('pasv')) with contextlib.closing( socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: s.settimeout(TIMEOUT) s.connect((host, port)) @unittest.skipUnless(SUPPORTS_IPV6, "IPv6 not supported") class TestIPv6Environment(_TestNetworkProtocols, unittest.TestCase): """Test PASV, EPSV, PORT and EPRT commands. Runs tests contained in _TestNetworkProtocols class by using IPv6 plus some additional specific tests. """ server_class = MProcessTestFTPd client_class = ftplib.FTP HOST = '::1' def test_port_v6(self): # PORT is not supposed to work self.assertRaises(ftplib.error_perm, self.client.sendport, self.server.host, self.server.port) def test_pasv_v6(self): # PASV is still supposed to work to support clients using # IPv4 connecting to a server supporting both IPv4 and IPv6 self.client.makepasv() @disable_log_warning def test_eprt_v6(self): resp = self.cmdresp('eprt |2|::foo|2222|') self.assertEqual(resp[:3], '501') self.assertIn('foreign address', resp) @unittest.skipUnless(SUPPORTS_HYBRID_IPV6, "IPv4/6 dual stack not supported") class TestIPv6MixedEnvironment(unittest.TestCase): """By running the server by specifying "::" as IP address the server is supposed to listen on all interfaces, supporting both IPv4 and IPv6 by using a single socket. What we are going to do here is starting the server in this manner and try to connect by using an IPv4 client. """ server_class = MProcessTestFTPd client_class = ftplib.FTP HOST = "::" def setUp(self): self.server = self.server_class((self.HOST, 0)) self.server.start() self.client = None def tearDown(self): if self.client is not None: self.client.close() self.server.stop() def test_port_v4(self): def noop(x): return x self.client = self.client_class(timeout=TIMEOUT) self.client.connect('127.0.0.1', self.server.port) self.client.set_pasv(False) self.client.login(USER, PASSWD) self.client.retrlines('list', noop) def test_pasv_v4(self): def noop(x): return x self.client = self.client_class(timeout=TIMEOUT) self.client.connect('127.0.0.1', self.server.port) self.client.set_pasv(True) self.client.login(USER, PASSWD) self.client.retrlines('list', noop) # make sure pasv response doesn't return an IPv4-mapped address ip = self.client.makepasv()[0] self.assertFalse(ip.startswith("::ffff:")) def test_eprt_v4(self): self.client = self.client_class(timeout=TIMEOUT) self.client.connect('127.0.0.1', self.server.port) self.client.login(USER, PASSWD) # test connection with contextlib.closing( socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: sock.bind((self.client.sock.getsockname()[0], 0)) sock.listen(5) sock.settimeout(2) ip, port = sock.getsockname()[:2] self.client.sendcmd('eprt |1|%s|%s|' % (ip, port)) try: sock2, addr = sock.accept() sock2.close() except socket.timeout: self.fail("Server didn't connect to passive socket") def test_epsv_v4(self): def mlstline(cmd): return self.client.voidcmd(cmd).split('\n')[1] self.client = self.client_class(timeout=TIMEOUT) self.client.connect('127.0.0.1', self.server.port) self.client.login(USER, PASSWD) host, port = ftplib.parse229(self.client.sendcmd('EPSV'), self.client.sock.getpeername()) self.assertEqual('127.0.0.1', host) with contextlib.closing( socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: s.settimeout(TIMEOUT) s.connect((host, port)) self.assertTrue(mlstline('mlst /').endswith('/')) class TestCornerCases(unittest.TestCase): """Tests for any kind of strange situation for the server to be in, mainly referring to bugs signaled on the bug tracker. """ server_class = MProcessTestFTPd client_class = ftplib.FTP def setUp(self): self.server = self.server_class() self.server.start() self.client = self.client_class(timeout=TIMEOUT) self.client.connect(self.server.host, self.server.port) self.client.login(USER, PASSWD) def tearDown(self): self.client.close() self.server.stop() def test_port_race_condition(self): # Refers to bug #120, first sends PORT, then disconnects the # control channel before accept()ing the incoming data connection. # The original server behavior was to reply with "200 Active # data connection established" *after* the client had already # disconnected the control connection. with contextlib.closing(socket.socket(self.client.af)) as sock: sock.bind((self.client.sock.getsockname()[0], 0)) sock.listen(5) sock.settimeout(TIMEOUT) host, port = sock.getsockname()[:2] hbytes = host.split('.') pbytes = [repr(port // 256), repr(port % 256)] bytes = hbytes + pbytes cmd = 'PORT ' + ','.join(bytes) + '\r\n' self.client.sock.sendall(b(cmd)) self.client.getresp() s, addr = sock.accept() s.close() def test_quick_connect(self): # Clients that connected and disconnected quickly could cause # the server to crash, due to a failure to catch errors in the # initial part of the connection process. # Tracked in issues #91, #104 and #105. # See also https://bugs.launchpad.net/zodb/+bug/135108 import struct def connect(addr): with contextlib.closing(socket.socket()) as s: # Set SO_LINGER to 1,0 causes a connection reset (RST) to # be sent when close() is called, instead of the standard # FIN shutdown sequence. s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0)) s.settimeout(TIMEOUT) try: s.connect(addr) except socket.error: pass for x in range(10): connect((self.server.host, self.server.port)) for x in range(10): addr = self.client.makepasv() connect(addr) def test_error_on_callback(self): # test that the server do not crash in case an error occurs # while firing a scheduled function self.tearDown() server = FTPServer((HOST, 0), FTPHandler) logger = logging.getLogger('pyftpdlib') logger.disabled = True try: len1 = len(IOLoop.instance().socket_map) IOLoop.instance().call_later(0, lambda: 1 // 0) server.serve_forever(timeout=0.001, blocking=False) len2 = len(IOLoop.instance().socket_map) self.assertEqual(len1, len2) finally: logger.disabled = False server.close() 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 with contextlib.closing(socket.socket()) as sock: sock.bind((HOST, 0)) port = sock.getsockname()[1] self.client.sock.settimeout(.1) try: resp = self.client.sendport(HOST, port) except ftplib.error_temp as err: self.assertEqual(str(err)[:3], '425') except (socket.timeout, getattr(ssl, "SSLError", object())): pass else: self.assertNotEqual(str(resp)[:3], '200') def test_repr(self): # make sure the FTP/DTP handler classes have a sane repr() with contextlib.closing(self.client.makeport()): for inst in IOLoop.instance().socket_map.values(): repr(inst) str(inst) if hasattr(os, 'sendfile'): def test_sendfile(self): # make sure that on python >= 3.3 we're using os.sendfile # rather than third party pysendfile module from pyftpdlib.handlers import sendfile self.assertIs(sendfile, os.sendfile) if SUPPORTS_SENDFILE: def test_sendfile_enabled(self): self.assertEqual(FTPHandler.use_sendfile, True) if hasattr(select, 'epoll') or hasattr(select, 'kqueue'): def test_ioloop_fileno(self): fd = self.server.server.ioloop.fileno() self.assertTrue(isinstance(fd, int), fd) # # TODO: disabled as on certain platforms (OSX and Windows) # # produces failures with python3. Will have to get back to # # this and fix it. # @unittest.skipIf(OSX or WINDOWS, "fails on OSX or Windows") # class TestUnicodePathNames(unittest.TestCase): # """Test FTP commands and responses by using path names with non # ASCII characters. # """ # server_class = MProcessTestFTPd # client_class = ftplib.FTP # def setUp(self): # self.server = self.server_class() # self.server.start() # self.client = self.client_class(timeout=TIMEOUT) # self.client.encoding = 'utf8' # PY3 only # self.client.connect(self.server.host, self.server.port) # self.client.login(USER, PASSWD) # if PY3: # safe_mkdir(bytes(TESTFN_UNICODE, 'utf8')) # touch(bytes(TESTFN_UNICODE_2, 'utf8')) # self.utf8fs = TESTFN_UNICODE in os.listdir('.') # else: # warnings.filterwarnings("ignore") # safe_mkdir(TESTFN_UNICODE) # touch(TESTFN_UNICODE_2) # self.utf8fs = \ # unicode(TESTFN_UNICODE, 'utf8') in os.listdir(u('.')) # warnings.resetwarnings() # def tearDown(self): # self.client.close() # self.server.stop() # remove_test_files() # # --- fs operations # def test_cwd(self): # if self.utf8fs: # resp = self.client.cwd(TESTFN_UNICODE) # self.assertTrue(TESTFN_UNICODE in resp) # else: # self.assertRaises(ftplib.error_perm, self.client.cwd, # TESTFN_UNICODE) # def test_mkd(self): # if self.utf8fs: # os.rmdir(TESTFN_UNICODE) # dirname = self.client.mkd(TESTFN_UNICODE) # self.assertEqual(dirname, '/' + TESTFN_UNICODE) # self.assertTrue(os.path.isdir(TESTFN_UNICODE)) # else: # self.assertRaises(ftplib.error_perm, self.client.mkd, # TESTFN_UNICODE) # def test_rmdir(self): # if self.utf8fs: # self.client.rmd(TESTFN_UNICODE) # else: # self.assertRaises(ftplib.error_perm, self.client.rmd, # TESTFN_UNICODE) # def test_rnfr_rnto(self): # if self.utf8fs: # self.client.rename(TESTFN_UNICODE, TESTFN) # else: # self.assertRaises(ftplib.error_perm, self.client.rename, # TESTFN_UNICODE, TESTFN) # def test_size(self): # self.client.sendcmd('type i') # if self.utf8fs: # self.client.sendcmd('size ' + TESTFN_UNICODE_2) # else: # self.assertRaises(ftplib.error_perm, self.client.sendcmd, # 'size ' + TESTFN_UNICODE_2) # def test_mdtm(self): # if self.utf8fs: # self.client.sendcmd('mdtm ' + TESTFN_UNICODE_2) # else: # self.assertRaises(ftplib.error_perm, self.client.sendcmd, # 'mdtm ' + TESTFN_UNICODE_2) # def test_stou(self): # if self.utf8fs: # resp = self.client.sendcmd('stou ' + TESTFN_UNICODE) # self.assertTrue(TESTFN_UNICODE in resp) # else: # self.assertRaises(ftplib.error_perm, self.client.sendcmd, # 'stou ' + TESTFN_UNICODE) # if hasattr(os, 'chmod'): # def test_site_chmod(self): # if self.utf8fs: # self.client.sendcmd('site chmod 777 ' + TESTFN_UNICODE) # else: # self.assertRaises(ftplib.error_perm, self.client.sendcmd, # 'site chmod 777 ' + TESTFN_UNICODE) # # --- listing cmds # def _test_listing_cmds(self, cmd): # ls = [] # self.client.retrlines(cmd, ls.append) # ls = '\n'.join(ls) # if self.utf8fs: # self.assertTrue(TESTFN_UNICODE in ls) # else: # # Part of the filename which are not encodable are supposed # # to have been replaced. The file should be something like # # 'tmp-pyftpdlib-unicode-????'. In any case it is not # # referenceable (e.g. DELE 'tmp-pyftpdlib-unicode-????' # # won't work). # self.assertTrue('tmp-pyftpdlib-unicode' in ls) # def test_list(self): # self._test_listing_cmds('list') # def test_nlst(self): # self._test_listing_cmds('nlst') # def test_mlsd(self): # self._test_listing_cmds('mlsd') # def test_mlst(self): # # utility function for extracting the line of interest # def mlstline(cmd): # return self.client.voidcmd(cmd).split('\n')[1] # if self.utf8fs: # self.assertTrue('type=dir' in # mlstline('mlst ' + TESTFN_UNICODE)) # self.assertTrue('/' + TESTFN_UNICODE in # mlstline('mlst ' + TESTFN_UNICODE)) # self.assertTrue('type=file' in # mlstline('mlst ' + TESTFN_UNICODE_2)) # self.assertTrue('/' + TESTFN_UNICODE_2 in # mlstline('mlst ' + TESTFN_UNICODE_2)) # else: # self.assertRaises(ftplib.error_perm, # mlstline, 'mlst ' + TESTFN_UNICODE) # # --- file transfer # def test_stor(self): # if self.utf8fs: # data = b'abcde12345' * 500 # os.remove(TESTFN_UNICODE_2) # dummy = BytesIO() # dummy.write(data) # dummy.seek(0) # self.client.storbinary('stor ' + TESTFN_UNICODE_2, dummy) # dummy_recv = BytesIO() # self.client.retrbinary('retr ' + TESTFN_UNICODE_2, # dummy_recv.write) # dummy_recv.seek(0) # self.assertEqual(dummy_recv.read(), data) # else: # dummy = BytesIO() # self.assertRaises(ftplib.error_perm, self.client.storbinary, # 'stor ' + TESTFN_UNICODE_2, dummy) # def test_retr(self): # if self.utf8fs: # data = b'abcd1234' * 500 # with open(TESTFN_UNICODE_2, 'wb') as f: # f.write(data) # dummy = BytesIO() # self.client.retrbinary('retr ' + TESTFN_UNICODE_2, dummy.write) # dummy.seek(0) # self.assertEqual(dummy.read(), data) # else: # dummy = BytesIO() # self.assertRaises(ftplib.error_perm, self.client.retrbinary, # 'retr ' + TESTFN_UNICODE_2, dummy.write) class ThreadedFTPTests(unittest.TestCase): server_class = ThreadedTestFTPd client_class = ftplib.FTP def setUp(self): self.server = self.server_class() self.server.start() self.client = self.client_class(timeout=TIMEOUT) self.client.connect(self.server.host, self.server.port) self.client.login(USER, PASSWD) self.tempfile = os.path.basename(touch(TESTFN)) self.tempdir = os.path.basename(tempfile.mkdtemp(dir=HOME)) self.dummy_recvfile = BytesIO() self.dummy_sendfile = BytesIO() def tearDown(self): self.client.close() self.server.stop() self.dummy_recvfile.close() self.dummy_sendfile.close() safe_remove(TESTFN) def test_unforeseen_mdtm_event(self): # Emulate a case where the file last modification time is prior # to year 1900. This most likely will never happen unless # someone specifically force the last modification time of a # file in some way. # To do so we temporarily override os.path.getmtime so that it # returns a negative value referring to a year prior to 1900. # It causes time.localtime/gmtime to raise a ValueError exception # which is supposed to be handled by server. # On python 3 it seems that the trick of replacing the original # method with the lambda doesn't work. if not PY3: _getmtime = AbstractedFS.getmtime try: AbstractedFS.getmtime = lambda x, y: -9000000000 self.assertRaisesRegex( ftplib.error_perm, "550 Can't determine file's last modification time", self.client.sendcmd, 'mdtm ' + self.tempfile) # make sure client hasn't been disconnected self.client.sendcmd('noop') finally: AbstractedFS.getmtime = _getmtime def test_stou_max_tries(self): # Emulates case where the max number of tries to find out a # unique file name when processing STOU command gets hit. class TestFS(AbstractedFS): def mkstemp(self, *args, **kwargs): raise IOError(errno.EEXIST, "No usable temporary file name found") with self.server.lock: self.server.handler.abstracted_fs = TestFS try: self.client.quit() self.client.connect(self.server.host, self.server.port) self.client.login(USER, PASSWD) self.assertRaises(ftplib.error_temp, self.client.sendcmd, 'stou') finally: with self.server.lock: self.server.handler.abstracted_fs = AbstractedFS def test_idle_timeout(self): # Test control channel timeout. The client which does not send # any command within the time specified in FTPHandler.timeout is # supposed to be kicked off. with self.server.lock: self.server.handler.timeout = 0.1 try: self.client.quit() self.client.connect() self.client.login(USER, PASSWD) # fail if no msg is received within 1 second self.client.sock.settimeout(1) data = self.client.sock.recv(BUFSIZE) self.assertEqual(data, b"421 Control connection timed out.\r\n") # ensure client has been kicked off self.assertRaises((socket.error, EOFError), self.client.sendcmd, 'noop') finally: with self.server.lock: self.server.handler.timeout = 0.1 @unittest.skipUnless(hasattr(socket, 'TCP_NODELAY'), 'TCP_NODELAY not available') def test_tcp_no_delay(self): s = get_server_handler().socket self.assertTrue(s.getsockopt(socket.SOL_TCP, socket.TCP_NODELAY)) self.client.quit() with self.server.lock: self.server.handler.tcp_no_delay = False self.client.connect(self.server.host, self.server.port) self.client.sendcmd('noop') s = get_server_handler().socket self.assertFalse(s.getsockopt(socket.SOL_TCP, socket.TCP_NODELAY)) def test_permit_foreign_address_false(self): handler = get_server_handler() with self.server.lock: handler.permit_foreign_addresses = False handler.remote_ip = '9.9.9.9' with self.assertRaises(ftplib.error_perm) as cm: self.client.makeport() self.assertIn('foreign address', str(cm.exception)) def test_permit_foreign_address_true(self): handler = get_server_handler() with self.server.lock: handler.permit_foreign_addresses = True handler.remote_ip = '9.9.9.9' s = self.client.makeport() s.close() @disable_log_warning def test_permit_privileged_ports(self): # Test FTPHandler.permit_privileged_ports_active attribute # try to bind a socket on a privileged port sock = None for port in reversed(range(1, 1024)): try: socket.getservbyport(port) except socket.error: # not registered port; go on try: sock = socket.socket(self.client.af, socket.SOCK_STREAM) self.addCleanup(sock.close) sock.settimeout(TIMEOUT) sock.bind((HOST, port)) break except socket.error as err: if err.errno == errno.EACCES: # root privileges needed if sock is not None: sock.close() sock = None break sock.close() continue else: # registered port found; skip to the next one continue else: # no usable privileged port was found sock = None with self.server.lock: self.server.handler.permit_privileged_ports = False self.assertRaises(ftplib.error_perm, self.client.sendport, HOST, port) if sock: port = sock.getsockname()[1] with self.server.lock: self.server.handler.permit_privileged_ports = True sock.listen(5) sock.settimeout(TIMEOUT) self.client.sendport(HOST, port) s, addr = sock.accept() s.close() @unittest.skipUnless(POSIX, "POSIX only") @unittest.skipIf(sys.version_info < (3, 3) and sendfile is None, "pysendfile not installed") def test_sendfile_fails(self): # Makes sure that if sendfile() fails and no bytes were # transmitted yet the server falls back on using plain # send() data = b'abcde12345' * 100000 self.dummy_sendfile.write(data) self.dummy_sendfile.seek(0) self.client.storbinary('stor ' + TESTFN, self.dummy_sendfile) with mock.patch('pyftpdlib.handlers.sendfile', side_effect=OSError(errno.EINVAL)) as fun: try: self.client.retrbinary( 'retr ' + TESTFN, self.dummy_recvfile.write) assert fun.called self.dummy_recvfile.seek(0) datafile = self.dummy_recvfile.read() self.assertEqual(len(data), len(datafile)) self.assertEqual(hash(data), hash(datafile)) finally: # We do not use os.remove() because file could still be # locked by ftpd thread. If DELE through FTP fails try # os.remove() as last resort. if os.path.exists(TESTFN): try: self.client.delete(TESTFN) except (ftplib.Error, EOFError, socket.error): safe_remove(TESTFN) configure_logging() remove_test_files() if __name__ == '__main__': unittest.main(verbosity=VERBOSITY) pyftpdlib-release-1.5.4/pyftpdlib/test/test_functional_ssl.py000066400000000000000000000277351327314554200245700ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. import contextlib import ftplib import os import socket import sys import ssl import OpenSSL # requires "pip install pyopenssl" from pyftpdlib.handlers import TLS_FTPHandler from pyftpdlib.test import configure_logging from pyftpdlib.test import MProcessTestFTPd from pyftpdlib.test import OSX from pyftpdlib.test import PASSWD from pyftpdlib.test import remove_test_files from pyftpdlib.test import TIMEOUT from pyftpdlib.test import TRAVIS from pyftpdlib.test import unittest from pyftpdlib.test import USER from pyftpdlib.test import VERBOSITY from pyftpdlib.test.test_functional import TestConfigurableOptions from pyftpdlib.test.test_functional import TestCornerCases from pyftpdlib.test.test_functional import TestFtpAbort from pyftpdlib.test.test_functional import TestFtpAuthentication from pyftpdlib.test.test_functional import TestFtpCmdsSemantic from pyftpdlib.test.test_functional import TestFtpDummyCmds from pyftpdlib.test.test_functional import TestFtpFsOperations from pyftpdlib.test.test_functional import TestFtpListingCmds from pyftpdlib.test.test_functional import TestFtpRetrieveData from pyftpdlib.test.test_functional import TestFtpStoreData from pyftpdlib.test.test_functional import TestIPv4Environment from pyftpdlib.test.test_functional import TestIPv6Environment from pyftpdlib.test.test_functional import TestTimeouts FTPS_SUPPORT = hasattr(ftplib, 'FTP_TLS') if sys.version_info < (2, 7): FTPS_UNSUPPORT_REASON = "requires python 2.7+" else: FTPS_UNSUPPORT_REASON = "FTPS test skipped" CERTFILE = os.path.abspath(os.path.join(os.path.dirname(__file__), 'keycert.pem')) del OpenSSL # ===================================================================== # --- FTPS mixin tests # ===================================================================== # What we're going to do here is repeat the original functional tests # defined in test_functinal.py but by using FTPS. # we secure both control and data connections before running any test. # This is useful as we reuse the existent functional tests which are # supposed to work no matter if the underlying protocol is FTP or FTPS. if FTPS_SUPPORT: class FTPSClient(ftplib.FTP_TLS): """A modified version of ftplib.FTP_TLS class which implicitly secure the data connection after login(). """ def login(self, *args, **kwargs): ftplib.FTP_TLS.login(self, *args, **kwargs) self.prot_p() class FTPSServer(MProcessTestFTPd): """A threaded FTPS server used for functional testing.""" handler = TLS_FTPHandler handler.certfile = CERTFILE class TLSTestMixin: server_class = FTPSServer client_class = FTPSClient else: @unittest.skipIf(True, FTPS_UNSUPPORT_REASON) class TLSTestMixin: pass class TestFtpAuthenticationTLSMixin(TLSTestMixin, TestFtpAuthentication): pass class TestTFtpDummyCmdsTLSMixin(TLSTestMixin, TestFtpDummyCmds): pass class TestFtpCmdsSemanticTLSMixin(TLSTestMixin, TestFtpCmdsSemantic): pass class TestFtpFsOperationsTLSMixin(TLSTestMixin, TestFtpFsOperations): pass class TestFtpStoreDataTLSMixin(TLSTestMixin, TestFtpStoreData): @unittest.skipIf(1, "fails with SSL") def test_stou(self): pass # class TestSendFileTLSMixin(TLSTestMixin, TestSendfile): # def test_fallback(self): # self.client.prot_c() # super(TestSendFileTLSMixin, self).test_fallback() class TestFtpRetrieveDataTLSMixin(TLSTestMixin, TestFtpRetrieveData): @unittest.skipIf(os.name == 'nt', "may fail on windows") def test_restore_on_retr(self): super(TestFtpRetrieveDataTLSMixin, self).test_restore_on_retr() class TestFtpListingCmdsTLSMixin(TLSTestMixin, TestFtpListingCmds): # TODO: see https://travis-ci.org/giampaolo/pyftpdlib/jobs/87318445 # Fails with: # File "/opt/python/2.7.9/lib/python2.7/ftplib.py", line 735, in retrlines # conn.unwrap() # File "/opt/python/2.7.9/lib/python2.7/ssl.py", line 771, in unwrap # s = self._sslobj.shutdown() # error: [Errno 0] Error @unittest.skipIf(TRAVIS or os.name == 'nt', "may fail on travis/windows") def test_nlst(self): super(TestFtpListingCmdsTLSMixin, self).test_nlst() class TestFtpAbortTLSMixin(TLSTestMixin, TestFtpAbort): @unittest.skipIf(1, "fails with SSL") def test_oob_abor(self): pass class TestTimeoutsTLSMixin(TLSTestMixin, TestTimeouts): @unittest.skipIf(1, "fails with SSL") def test_data_timeout_not_reached(self): pass class TestConfigurableOptionsTLSMixin(TLSTestMixin, TestConfigurableOptions): pass class TestIPv4EnvironmentTLSMixin(TLSTestMixin, TestIPv4Environment): pass class TestIPv6EnvironmentTLSMixin(TLSTestMixin, TestIPv6Environment): pass class TestCornerCasesTLSMixin(TLSTestMixin, TestCornerCases): pass # ===================================================================== # dedicated FTPS tests # ===================================================================== @unittest.skipUnless(FTPS_SUPPORT, FTPS_UNSUPPORT_REASON) class TestFTPS(unittest.TestCase): """Specific tests fot TSL_FTPHandler class.""" def _setup(self, tls_control_required=False, tls_data_required=False, ssl_protocol=ssl.PROTOCOL_SSLv23, ): self.server = FTPSServer() self.server.handler.tls_control_required = tls_control_required self.server.handler.tls_data_required = tls_data_required self.server.handler.ssl_protocol = ssl_protocol self.server.start() self.client = ftplib.FTP_TLS(timeout=TIMEOUT) self.client.connect(self.server.host, self.server.port) def setUp(self): self.client = None self.server = None def tearDown(self): if self.client is not None: self.client.ssl_version = ssl.PROTOCOL_SSLv23 self.client.close() if self.server is not None: self.server.handler.ssl_protocol = ssl.PROTOCOL_SSLv23 self.server.handler.tls_control_required = False self.server.handler.tls_data_required = False self.server.stop() def assertRaisesWithMsg(self, excClass, msg, callableObj, *args, **kwargs): try: callableObj(*args, **kwargs) except excClass as err: if str(err) == msg: return raise self.failureException("%s != %s" % (str(err), msg)) else: if hasattr(excClass, '__name__'): excName = excClass.__name__ else: excName = str(excClass) raise self.failureException("%s not raised" % excName) def test_auth(self): # unsecured self._setup() self.client.login(secure=False) self.assertFalse(isinstance(self.client.sock, ssl.SSLSocket)) # secured self.client.login() self.assertTrue(isinstance(self.client.sock, ssl.SSLSocket)) # AUTH issued twice msg = '503 Already using TLS.' self.assertRaisesWithMsg(ftplib.error_perm, msg, self.client.sendcmd, 'auth tls') def test_pbsz(self): # unsecured self._setup() self.client.login(secure=False) msg = "503 PBSZ not allowed on insecure control connection." self.assertRaisesWithMsg(ftplib.error_perm, msg, self.client.sendcmd, 'pbsz 0') # secured self.client.login(secure=True) resp = self.client.sendcmd('pbsz 0') self.assertEqual(resp, "200 PBSZ=0 successful.") def test_prot(self): self._setup() self.client.login(secure=False) msg = "503 PROT not allowed on insecure control connection." self.assertRaisesWithMsg(ftplib.error_perm, msg, self.client.sendcmd, 'prot p') self.client.login(secure=True) # secured self.client.prot_p() sock = self.client.transfercmd('list') with contextlib.closing(sock): while True: if not sock.recv(1024): self.client.voidresp() break self.assertTrue(isinstance(sock, ssl.SSLSocket)) # unsecured self.client.prot_c() sock = self.client.transfercmd('list') with contextlib.closing(sock): while True: if not sock.recv(1024): self.client.voidresp() break self.assertFalse(isinstance(sock, ssl.SSLSocket)) def test_feat(self): self._setup() feat = self.client.sendcmd('feat') cmds = ['AUTH TLS', 'AUTH SSL', 'PBSZ', 'PROT'] for cmd in cmds: self.assertTrue(cmd in feat) def test_unforseen_ssl_shutdown(self): self._setup() self.client.login() try: sock = self.client.sock.unwrap() except socket.error as err: if err.errno == 0: return raise sock.settimeout(TIMEOUT) sock.sendall(b'noop') try: chunk = sock.recv(1024) except socket.error: pass else: self.assertEqual(chunk, b"") def test_tls_control_required(self): self._setup(tls_control_required=True) msg = "550 SSL/TLS required on the control channel." self.assertRaisesWithMsg(ftplib.error_perm, msg, self.client.sendcmd, "user " + USER) self.assertRaisesWithMsg(ftplib.error_perm, msg, self.client.sendcmd, "pass " + PASSWD) self.client.login(secure=True) def test_tls_data_required(self): self._setup(tls_data_required=True) self.client.login(secure=True) msg = "550 SSL/TLS required on the data channel." self.assertRaisesWithMsg(ftplib.error_perm, msg, self.client.retrlines, 'list', lambda x: x) self.client.prot_p() self.client.retrlines('list', lambda x: x) def try_protocol_combo(self, server_protocol, client_protocol): self._setup(ssl_protocol=server_protocol) self.client.ssl_version = client_protocol self.client.close() self.client.connect(self.server.host, self.server.port) try: self.client.login() except (ssl.SSLError, socket.error): self.client.close() else: self.client.quit() # def test_ssl_version(self): # protos = [ssl.PROTOCOL_SSLv3, ssl.PROTOCOL_SSLv23, # ssl.PROTOCOL_TLSv1] # if hasattr(ssl, "PROTOCOL_SSLv2"): # protos.append(ssl.PROTOCOL_SSLv2) # for proto in protos: # self.try_protocol_combo(ssl.PROTOCOL_SSLv2, proto) # for proto in protos: # self.try_protocol_combo(ssl.PROTOCOL_SSLv3, proto) # for proto in protos: # self.try_protocol_combo(ssl.PROTOCOL_SSLv23, proto) # for proto in protos: # self.try_protocol_combo(ssl.PROTOCOL_TLSv1, proto) if hasattr(ssl, "PROTOCOL_SSLv2"): def test_sslv2(self): self.client.ssl_version = ssl.PROTOCOL_SSLv2 self.client.close() if not OSX: with self.server.lock: self.client.connect(self.server.host, self.server.port) self.assertRaises(socket.error, self.client.login) else: with self.server.lock: with self.assertRaises(socket.error): self.client.connect(self.server.host, self.server.port, timeout=0.1) self.client.ssl_version = ssl.PROTOCOL_SSLv2 configure_logging() remove_test_files() if __name__ == '__main__': unittest.main(verbosity=VERBOSITY) pyftpdlib-release-1.5.4/pyftpdlib/test/test_ioloop.py000066400000000000000000000452201327314554200230330ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. import contextlib import errno import select import socket import sys import time from pyftpdlib._compat import PY3 from pyftpdlib.ioloop import Acceptor from pyftpdlib.ioloop import AsyncChat from pyftpdlib.ioloop import IOLoop from pyftpdlib.ioloop import RetryError from pyftpdlib.test import mock from pyftpdlib.test import POSIX from pyftpdlib.test import unittest from pyftpdlib.test import VERBOSITY import pyftpdlib.ioloop if hasattr(socket, 'socketpair'): socketpair = socket.socketpair else: def socketpair(family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0): with contextlib.closing(socket.socket(family, type, proto)) as l: l.bind(("localhost", 0)) l.listen(5) c = socket.socket(family, type, proto) try: c.connect(l.getsockname()) caddr = c.getsockname() while True: a, addr = l.accept() # check that we've got the correct client if addr == caddr: return c, a a.close() except OSError: c.close() raise # TODO: write more tests. class BaseIOLoopTestCase(object): ioloop_class = None def make_socketpair(self): rd, wr = socketpair() self.addCleanup(rd.close) self.addCleanup(wr.close) return rd, wr def test_register(self): s = self.ioloop_class() self.addCleanup(s.close) rd, wr = self.make_socketpair() handler = AsyncChat(rd) s.register(rd, handler, s.READ) s.register(wr, handler, s.WRITE) self.assertIn(rd, s.socket_map) self.assertIn(wr, s.socket_map) return (s, rd, wr) def test_unregister(self): s, rd, wr = self.test_register() s.unregister(rd) s.unregister(wr) self.assertNotIn(rd, s.socket_map) self.assertNotIn(wr, s.socket_map) def test_unregister_twice(self): s, rd, wr = self.test_register() s.unregister(rd) s.unregister(rd) s.unregister(wr) s.unregister(wr) def test_modify(self): s, rd, wr = self.test_register() s.modify(rd, s.WRITE) s.modify(wr, s.READ) def test_loop(self): # no timeout s, rd, wr = self.test_register() s.call_later(0, s.close) s.loop() # with timeout s, rd, wr = self.test_register() s.call_later(0, s.close) s.loop(timeout=0.001) def test_close(self): s, rd, wr = self.test_register() s.close() self.assertEqual(s.socket_map, {}) def test_close_w_handler_exc(self): # Simulate an exception when close()ing a socket handler. # Exception should be logged and ignored. class Handler(AsyncChat): def close(self): 1 / 0 s = self.ioloop_class() self.addCleanup(s.close) rd, wr = self.make_socketpair() handler = Handler(rd) s.register(rd, handler, s.READ) with mock.patch("pyftpdlib.ioloop.logger.error") as m: s.close() assert m.called self.assertIn('ZeroDivisionError', m.call_args[0][0]) def test_close_w_handler_ebadf_exc(self): # Simulate an exception when close()ing a socket handler. # Exception should be ignored (and not logged). class Handler(AsyncChat): def close(self): raise OSError(errno.EBADF, "") s = self.ioloop_class() self.addCleanup(s.close) rd, wr = self.make_socketpair() handler = Handler(rd) s.register(rd, handler, s.READ) with mock.patch("pyftpdlib.ioloop.logger.error") as m: s.close() assert not m.called def test_close_w_callback_exc(self): # Simulate an exception when close()ing the IO loop and a # scheduled callback raises an exception on cancel(). with mock.patch("pyftpdlib.ioloop.logger.error") as logerr: with mock.patch("pyftpdlib.ioloop._CallLater.cancel", side_effect=lambda: 1 / 0) as cancel: s = self.ioloop_class() self.addCleanup(s.close) s.call_later(1, lambda: 0) s.close() assert cancel.called assert logerr.called self.assertIn('ZeroDivisionError', logerr.call_args[0][0]) class DefaultIOLoopTestCase(unittest.TestCase, BaseIOLoopTestCase): ioloop_class = pyftpdlib.ioloop.IOLoop # =================================================================== # select() # =================================================================== class SelectIOLoopTestCase(unittest.TestCase, BaseIOLoopTestCase): ioloop_class = pyftpdlib.ioloop.Select def test_select_eintr(self): # EINTR is supposed to be ignored with mock.patch('pyftpdlib.ioloop.select.select', side_effect=select.error()) as m: m.side_effect.errno = errno.EINTR s, rd, wr = self.test_register() s.poll(0) # ...but just that with mock.patch('pyftpdlib.ioloop.select.select', side_effect=select.error()) as m: m.side_effect.errno = errno.EBADF s, rd, wr = self.test_register() self.assertRaises(select.error, s.poll, 0) # =================================================================== # poll() # =================================================================== @unittest.skipUnless(hasattr(pyftpdlib.ioloop, 'Poll'), "poll() not available on this platform") class PollIOLoopTestCase(unittest.TestCase, BaseIOLoopTestCase): ioloop_class = getattr(pyftpdlib.ioloop, "Poll", None) poller_mock = "pyftpdlib.ioloop.Poll._poller" @unittest.skipIf(sys.version_info[:2] == (3, 2), "") def test_eintr_on_poll(self): # EINTR is supposed to be ignored with mock.patch(self.poller_mock, return_vaue=mock.Mock()) as m: if not PY3: m.return_value.poll.side_effect = select.error m.return_value.poll.side_effect.errno = errno.EINTR else: m.return_value.poll.side_effect = OSError(errno.EINTR, "") s, rd, wr = self.test_register() s.poll(0) assert m.called # ...but just that with mock.patch(self.poller_mock, return_vaue=mock.Mock()) as m: if not PY3: m.return_value.poll.side_effect = select.error m.return_value.poll.side_effect.errno = errno.EBADF else: m.return_value.poll.side_effect = OSError(errno.EBADF, "") s, rd, wr = self.test_register() self.assertRaises(select.error, s.poll, 0) assert m.called def test_eexist_on_register(self): # EEXIST is supposed to be ignored with mock.patch(self.poller_mock, return_vaue=mock.Mock()) as m: m.return_value.register.side_effect = \ EnvironmentError(errno.EEXIST, "") s, rd, wr = self.test_register() # ...but just that with mock.patch(self.poller_mock, return_vaue=mock.Mock()) as m: m.return_value.register.side_effect = \ EnvironmentError(errno.EBADF, "") self.assertRaises(EnvironmentError, self.test_register) def test_enoent_ebadf_on_unregister(self): # ENOENT and EBADF are supposed to be ignored for errnum in (errno.EBADF, errno.ENOENT): with mock.patch(self.poller_mock, return_vaue=mock.Mock()) as m: m.return_value.unregister.side_effect = \ EnvironmentError(errnum, "") s, rd, wr = self.test_register() s.unregister(rd) # ...but just those with mock.patch(self.poller_mock, return_vaue=mock.Mock()) as m: m.return_value.unregister.side_effect = \ EnvironmentError(errno.EEXIST, "") s, rd, wr = self.test_register() self.assertRaises(EnvironmentError, s.unregister, rd) def test_enoent_on_modify(self): # ENOENT is supposed to be ignored with mock.patch(self.poller_mock, return_vaue=mock.Mock()) as m: m.return_value.modify.side_effect = \ OSError(errno.ENOENT, "") s, rd, wr = self.test_register() s.modify(rd, s.READ) # =================================================================== # epoll() # =================================================================== @unittest.skipUnless(hasattr(pyftpdlib.ioloop, 'Epoll'), "epoll() not available on this platform (Linux only)") class EpollIOLoopTestCase(PollIOLoopTestCase): ioloop_class = getattr(pyftpdlib.ioloop, "Epoll", None) poller_mock = "pyftpdlib.ioloop.Epoll._poller" # =================================================================== # /dev/poll # =================================================================== @unittest.skipUnless(hasattr(pyftpdlib.ioloop, 'DevPoll'), "/dev/poll not available on this platform (Solaris only)") class DevPollIOLoopTestCase(unittest.TestCase, BaseIOLoopTestCase): ioloop_class = getattr(pyftpdlib.ioloop, "DevPoll", None) # =================================================================== # kqueue # =================================================================== @unittest.skipUnless(hasattr(pyftpdlib.ioloop, 'Kqueue'), "/dev/poll not available on this platform (BSD only)") class KqueueIOLoopTestCase(unittest.TestCase, BaseIOLoopTestCase): ioloop_class = getattr(pyftpdlib.ioloop, "Kqueue", None) class TestCallLater(unittest.TestCase): """Tests for CallLater class.""" def setUp(self): self.ioloop = IOLoop.instance() for task in self.ioloop.sched._tasks: if not task.cancelled: task.cancel() del self.ioloop.sched._tasks[:] def scheduler(self, timeout=0.01, count=100): while self.ioloop.sched._tasks and count > 0: self.ioloop.sched.poll() count -= 1 time.sleep(timeout) def test_interface(self): def fun(): return 0 self.assertRaises(AssertionError, self.ioloop.call_later, -1, fun) x = self.ioloop.call_later(3, fun) self.assertEqual(x.cancelled, False) x.cancel() self.assertEqual(x.cancelled, True) self.assertRaises(AssertionError, x.call) self.assertRaises(AssertionError, x.reset) x.cancel() def test_order(self): def fun(x): l.append(x) l = [] for x in [0.05, 0.04, 0.03, 0.02, 0.01]: self.ioloop.call_later(x, fun, x) self.scheduler() self.assertEqual(l, [0.01, 0.02, 0.03, 0.04, 0.05]) # The test is reliable only on those systems where time.time() # provides time with a better precision than 1 second. if not str(time.time()).endswith('.0'): def test_reset(self): def fun(x): l.append(x) l = [] self.ioloop.call_later(0.01, fun, 0.01) self.ioloop.call_later(0.02, fun, 0.02) self.ioloop.call_later(0.03, fun, 0.03) x = self.ioloop.call_later(0.04, fun, 0.04) self.ioloop.call_later(0.05, fun, 0.05) time.sleep(0.1) x.reset() self.scheduler() self.assertEqual(l, [0.01, 0.02, 0.03, 0.05, 0.04]) def test_cancel(self): def fun(x): l.append(x) l = [] self.ioloop.call_later(0.01, fun, 0.01).cancel() self.ioloop.call_later(0.02, fun, 0.02) self.ioloop.call_later(0.03, fun, 0.03) self.ioloop.call_later(0.04, fun, 0.04) self.ioloop.call_later(0.05, fun, 0.05).cancel() self.scheduler() self.assertEqual(l, [0.02, 0.03, 0.04]) def test_errback(self): l = [] self.ioloop.call_later( 0.0, lambda: 1 // 0, _errback=lambda: l.append(True)) self.scheduler() self.assertEqual(l, [True]) def test__repr__(self): repr(self.ioloop.call_later(0.01, lambda: 0, 0.01)) def test__lt__(self): a = self.ioloop.call_later(0.01, lambda: 0, 0.01) b = self.ioloop.call_later(0.02, lambda: 0, 0.02) self.assertTrue(a < b) def test__le__(self): a = self.ioloop.call_later(0.01, lambda: 0, 0.01) b = self.ioloop.call_later(0.02, lambda: 0, 0.02) self.assertTrue(a <= b) class TestCallEvery(unittest.TestCase): """Tests for CallEvery class.""" def setUp(self): self.ioloop = IOLoop.instance() for task in self.ioloop.sched._tasks: if not task.cancelled: task.cancel() del self.ioloop.sched._tasks[:] def scheduler(self, timeout=0.003): stop_at = time.time() + timeout while time.time() < stop_at: self.ioloop.sched.poll() def test_interface(self): def fun(): return 0 self.assertRaises(AssertionError, self.ioloop.call_every, -1, fun) x = self.ioloop.call_every(3, fun) self.assertEqual(x.cancelled, False) x.cancel() self.assertEqual(x.cancelled, True) self.assertRaises(AssertionError, x.call) self.assertRaises(AssertionError, x.reset) x.cancel() def test_only_once(self): # make sure that callback is called only once per-loop def fun(): l1.append(None) l1 = [] self.ioloop.call_every(0, fun) self.ioloop.sched.poll() self.assertEqual(l1, [None]) def test_multi_0_timeout(self): # make sure a 0 timeout callback is called as many times # as the number of loops def fun(): l.append(None) l = [] self.ioloop.call_every(0, fun) for x in range(100): self.ioloop.sched.poll() self.assertEqual(len(l), 100) # run it on systems where time.time() has a higher precision if POSIX: def test_low_and_high_timeouts(self): # make sure a callback with a lower timeout is called more # frequently than another with a greater timeout def fun(): l1.append(None) l1 = [] self.ioloop.call_every(0.001, fun) self.scheduler() def fun(): l2.append(None) l2 = [] self.ioloop.call_every(0.005, fun) self.scheduler(timeout=0.01) self.assertTrue(len(l1) > len(l2)) def test_cancel(self): # make sure a cancelled callback doesn't get called anymore def fun(): l.append(None) l = [] call = self.ioloop.call_every(0.001, fun) self.scheduler() len_l = len(l) call.cancel() self.scheduler() self.assertEqual(len_l, len(l)) def test_errback(self): l = [] self.ioloop.call_every( 0.0, lambda: 1 // 0, _errback=lambda: l.append(True)) self.scheduler() self.assertTrue(l) class TestAsyncChat(unittest.TestCase): def get_connected_handler(self): s = socket.socket() self.addCleanup(s.close) ac = AsyncChat(sock=s) self.addCleanup(ac.close) return ac def test_send_retry(self): ac = self.get_connected_handler() for errnum in pyftpdlib.ioloop._ERRNOS_RETRY: with mock.patch("pyftpdlib.ioloop.socket.socket.send", side_effect=socket.error(errnum, "")) as m: self.assertEqual(ac.send(b"x"), 0) assert m.called def test_send_disconnect(self): ac = self.get_connected_handler() for errnum in pyftpdlib.ioloop._ERRNOS_DISCONNECTED: with mock.patch("pyftpdlib.ioloop.socket.socket.send", side_effect=socket.error(errnum, "")) as send: with mock.patch.object(ac, "handle_close") as handle_close: self.assertEqual(ac.send(b"x"), 0) assert send.called assert handle_close.called def test_recv_retry(self): ac = self.get_connected_handler() for errnum in pyftpdlib.ioloop._ERRNOS_RETRY: with mock.patch("pyftpdlib.ioloop.socket.socket.recv", side_effect=socket.error(errnum, "")) as m: self.assertRaises(RetryError, ac.recv, 1024) assert m.called def test_recv_disconnect(self): ac = self.get_connected_handler() for errnum in pyftpdlib.ioloop._ERRNOS_DISCONNECTED: with mock.patch("pyftpdlib.ioloop.socket.socket.recv", side_effect=socket.error(errnum, "")) as send: with mock.patch.object(ac, "handle_close") as handle_close: self.assertEqual(ac.recv(b"x"), b'') assert send.called assert handle_close.called def test_connect_af_unspecified_err(self): ac = AsyncChat() with mock.patch.object( ac, "connect", side_effect=socket.error(errno.EBADF, "")) as m: self.assertRaises(socket.error, ac.connect_af_unspecified, ("localhost", 0)) assert m.called self.assertIsNone(ac.socket) class TestAcceptor(unittest.TestCase): def test_bind_af_unspecified_err(self): ac = Acceptor() with mock.patch.object( ac, "bind", side_effect=socket.error(errno.EBADF, "")) as m: self.assertRaises(socket.error, ac.bind_af_unspecified, ("localhost", 0)) assert m.called self.assertIsNone(ac.socket) def test_handle_accept_econnacorted(self): # https://github.com/giampaolo/pyftpdlib/issues/105 ac = Acceptor() with mock.patch.object( ac, "accept", side_effect=socket.error(errno.ECONNABORTED, "")) as m: ac.handle_accept() assert m.called self.assertIsNone(ac.socket) def test_handle_accept_typeerror(self): # https://github.com/giampaolo/pyftpdlib/issues/91 ac = Acceptor() with mock.patch.object(ac, "accept", side_effect=TypeError) as m: ac.handle_accept() assert m.called self.assertIsNone(ac.socket) if __name__ == '__main__': unittest.main(verbosity=VERBOSITY) pyftpdlib-release-1.5.4/pyftpdlib/test/test_misc.py000066400000000000000000000105741327314554200224710ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. import logging import sys import warnings try: from StringIO import StringIO as BytesIO except ImportError: from io import BytesIO from pyftpdlib._compat import PY3 from pyftpdlib.servers import FTPServer from pyftpdlib.test import mock from pyftpdlib.test import safe_mkdir from pyftpdlib.test import safe_rmdir from pyftpdlib.test import TESTFN from pyftpdlib.test import unittest from pyftpdlib.test import VERBOSITY import pyftpdlib import pyftpdlib.__main__ class TestCommandLineParser(unittest.TestCase): """Test command line parser.""" SYSARGV = sys.argv STDERR = sys.stderr def setUp(self): class DummyFTPServer(FTPServer): """An overridden version of FTPServer class which forces serve_forever() to return immediately. """ def serve_forever(self, *args, **kwargs): return if PY3: import io self.devnull = io.StringIO() else: self.devnull = BytesIO() sys.argv = self.SYSARGV[:] sys.stderr = self.STDERR self.original_ftpserver_class = FTPServer pyftpdlib.__main__.FTPServer = DummyFTPServer def tearDown(self): self.devnull.close() sys.argv = self.SYSARGV[:] sys.stderr = self.STDERR pyftpdlib.servers.FTPServer = self.original_ftpserver_class safe_rmdir(TESTFN) def test_a_option(self): sys.argv += ["-i", "localhost", "-p", "0"] pyftpdlib.__main__.main() sys.argv = self.SYSARGV[:] # no argument sys.argv += ["-a"] sys.stderr = self.devnull self.assertRaises(SystemExit, pyftpdlib.__main__.main) def test_p_option(self): sys.argv += ["-p", "0"] pyftpdlib.__main__.main() # no argument sys.argv = self.SYSARGV[:] sys.argv += ["-p"] sys.stderr = self.devnull self.assertRaises(SystemExit, pyftpdlib.__main__.main) # invalid argument sys.argv += ["-p foo"] self.assertRaises(SystemExit, pyftpdlib.__main__.main) def test_w_option(self): sys.argv += ["-w", "-p", "0"] with warnings.catch_warnings(): warnings.filterwarnings("error") self.assertRaises(RuntimeWarning, pyftpdlib.__main__.main) # unexpected argument sys.argv = self.SYSARGV[:] sys.argv += ["-w foo"] sys.stderr = self.devnull self.assertRaises(SystemExit, pyftpdlib.__main__.main) def test_d_option(self): sys.argv += ["-d", TESTFN, "-p", "0"] safe_mkdir(TESTFN) pyftpdlib.__main__.main() # without argument sys.argv = self.SYSARGV[:] sys.argv += ["-d"] sys.stderr = self.devnull self.assertRaises(SystemExit, pyftpdlib.__main__.main) # no such directory sys.argv = self.SYSARGV[:] sys.argv += ["-d %s" % TESTFN] safe_rmdir(TESTFN) self.assertRaises(ValueError, pyftpdlib.__main__.main) def test_r_option(self): sys.argv += ["-r 60000-61000", "-p", "0"] pyftpdlib.__main__.main() # without arg sys.argv = self.SYSARGV[:] sys.argv += ["-r"] sys.stderr = self.devnull self.assertRaises(SystemExit, pyftpdlib.__main__.main) # wrong arg sys.argv = self.SYSARGV[:] sys.argv += ["-r yyy-zzz"] self.assertRaises(SystemExit, pyftpdlib.__main__.main) def test_v_option(self): sys.argv += ["-v"] self.assertRaises(SystemExit, pyftpdlib.__main__.main) # unexpected argument sys.argv = self.SYSARGV[:] sys.argv += ["-v foo"] sys.stderr = self.devnull self.assertRaises(SystemExit, pyftpdlib.__main__.main) def test_D_option(self): with mock.patch('pyftpdlib.__main__.config_logging') as fun: sys.argv += ["-D", "-p 0"] pyftpdlib.__main__.main() fun.assert_called_once_with(level=logging.DEBUG) # unexpected argument sys.argv = self.SYSARGV[:] sys.argv += ["-V foo"] sys.stderr = self.devnull self.assertRaises(SystemExit, pyftpdlib.__main__.main) if __name__ == '__main__': unittest.main(verbosity=VERBOSITY) pyftpdlib-release-1.5.4/pyftpdlib/test/test_servers.py000066400000000000000000000155461327314554200232330ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. import contextlib import ftplib import inspect import socket import sys from pyftpdlib import handlers from pyftpdlib import servers from pyftpdlib.test import configure_logging from pyftpdlib.test import HOST from pyftpdlib.test import PASSWD from pyftpdlib.test import remove_test_files from pyftpdlib.test import ThreadedTestFTPd from pyftpdlib.test import TIMEOUT from pyftpdlib.test import unittest from pyftpdlib.test import USER from pyftpdlib.test import VERBOSITY from pyftpdlib.test.test_functional import TestCornerCases from pyftpdlib.test.test_functional import TestFtpAbort from pyftpdlib.test.test_functional import TestFtpAuthentication from pyftpdlib.test.test_functional import TestFtpCmdsSemantic from pyftpdlib.test.test_functional import TestFtpDummyCmds from pyftpdlib.test.test_functional import TestFtpFsOperations from pyftpdlib.test.test_functional import TestFtpListingCmds from pyftpdlib.test.test_functional import TestFtpRetrieveData from pyftpdlib.test.test_functional import TestFtpStoreData from pyftpdlib.test.test_functional import TestIPv4Environment from pyftpdlib.test.test_functional import TestIPv6Environment MPROCESS_SUPPORT = hasattr(servers, 'MultiprocessFTPServer') class TestFTPServer(unittest.TestCase): """Tests for *FTPServer classes.""" server_class = ThreadedTestFTPd client_class = ftplib.FTP def setUp(self): self.server = None self.client = None def tearDown(self): if self.client is not None: self.client.close() if self.server is not None: self.server.stop() def test_sock_instead_of_addr(self): # pass a socket object instead of an address tuple to FTPServer # constructor with contextlib.closing(socket.socket()) as sock: sock.bind((HOST, 0)) sock.listen(5) ip, port = sock.getsockname()[:2] self.server = self.server_class(sock) self.server.start() self.client = self.client_class(timeout=TIMEOUT) self.client.connect(ip, port) self.client.login(USER, PASSWD) def test_ctx_mgr(self): with servers.FTPServer((HOST, 0), handlers.FTPHandler) as server: self.assertIsNotNone(server) # ===================================================================== # --- threaded FTP server mixin tests # ===================================================================== # What we're going to do here is repeat the original functional tests # defined in test_functinal.py but by using different concurrency # modules (multi thread and multi process instead of async. # This is useful as we reuse the existent functional tests which are # supposed to work no matter what the concurrency model is. class _TFTPd(ThreadedTestFTPd): server_class = servers.ThreadedFTPServer class ThreadFTPTestMixin: server_class = _TFTPd class TestFtpAuthenticationThreadMixin(ThreadFTPTestMixin, TestFtpAuthentication): pass class TestTFtpDummyCmdsThreadMixin(ThreadFTPTestMixin, TestFtpDummyCmds): pass class TestFtpCmdsSemanticThreadMixin(ThreadFTPTestMixin, TestFtpCmdsSemantic): pass class TestFtpFsOperationsThreadMixin(ThreadFTPTestMixin, TestFtpFsOperations): pass class TestFtpStoreDataThreadMixin(ThreadFTPTestMixin, TestFtpStoreData): pass class TestFtpRetrieveDataThreadMixin(ThreadFTPTestMixin, TestFtpRetrieveData): pass class TestFtpListingCmdsThreadMixin(ThreadFTPTestMixin, TestFtpListingCmds): pass class TestFtpAbortThreadMixin(ThreadFTPTestMixin, TestFtpAbort): pass # class TestTimeoutsThreadMixin(ThreadFTPTestMixin, TestTimeouts): # def test_data_timeout_not_reached(self): pass # class TestConfOptsThreadMixin(ThreadFTPTestMixin, TestConfigurableOptions): # pass # class TestCallbacksThreadMixin(ThreadFTPTestMixin, TestCallbacks): # pass class TestIPv4EnvironmentThreadMixin(ThreadFTPTestMixin, TestIPv4Environment): pass class TestIPv6EnvironmentThreadMixin(ThreadFTPTestMixin, TestIPv6Environment): pass class TestCornerCasesThreadMixin(ThreadFTPTestMixin, TestCornerCases): pass # class TestFTPServerThreadMixin(ThreadFTPTestMixin, TestFTPServer): # pass # ===================================================================== # --- multiprocess FTP server mixin tests # ===================================================================== if MPROCESS_SUPPORT: class MultiProcFTPd(ThreadedTestFTPd): server_class = servers.MultiprocessFTPServer class MProcFTPTestMixin: server_class = MultiProcFTPd else: @unittest.skipIf(True, "multiprocessing module not installed") class MProcFTPTestMixin: pass class TestFtpAuthenticationMProcMixin(MProcFTPTestMixin, TestFtpAuthentication): pass class TestTFtpDummyCmdsMProcMixin(MProcFTPTestMixin, TestFtpDummyCmds): pass class TestFtpCmdsSemanticMProcMixin(MProcFTPTestMixin, TestFtpCmdsSemantic): pass class TestFtpFsOperationsMProcMixin(MProcFTPTestMixin, TestFtpFsOperations): def test_unforeseen_mdtm_event(self): pass class TestFtpStoreDataMProcMixin(MProcFTPTestMixin, TestFtpStoreData): pass class TestFtpRetrieveDataMProcMixin(MProcFTPTestMixin, TestFtpRetrieveData): pass class TestFtpListingCmdsMProcMixin(MProcFTPTestMixin, TestFtpListingCmds): pass class TestFtpAbortMProcMixin(MProcFTPTestMixin, TestFtpAbort): pass # class TestTimeoutsMProcMixin(MProcFTPTestMixin, TestTimeouts): # def test_data_timeout_not_reached(self): pass # class TestConfiOptsMProcMixin(MProcFTPTestMixin, TestConfigurableOptions): # pass # class TestCallbacksMProcMixin(MProcFTPTestMixin, TestCallbacks): pass class TestIPv4EnvironmentMProcMixin(MProcFTPTestMixin, TestIPv4Environment): pass class TestIPv6EnvironmentMProcMixin(MProcFTPTestMixin, TestIPv6Environment): pass class TestCornerCasesMProcMixin(MProcFTPTestMixin, TestCornerCases): pass # class TestFTPServerMProcMixin(MProcFTPTestMixin, TestFTPServer): # pass configure_logging() remove_test_files() def main(): test_classes = set() for name, obj in inspect.getmembers(sys.modules[__name__]): if inspect.isclass(obj): if obj.__module__ == '__main__' and name.startswith('Test'): test_classes.add(obj) loader = unittest.TestLoader() suite = [] for test_class in test_classes: suite.append(loader.loadTestsFromTestCase(test_class)) runner = unittest.TextTestRunner(verbosity=VERBOSITY) result = runner.run(unittest.TestSuite(unittest.TestSuite(suite))) success = result.wasSuccessful() sys.exit(0 if success else 1) if __name__ == '__main__': main() pyftpdlib-release-1.5.4/scripts/000077500000000000000000000000001327314554200166315ustar00rootroot00000000000000pyftpdlib-release-1.5.4/scripts/ftpbench000077500000000000000000000445221327314554200203570ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (C) 2007-2016 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. """ FTP server benchmark script. In order to run this you must have a listening FTP server with a user with writing permissions configured. This is a stand-alone script which does not depend from pyftpdlib. It just requires python >= 2.6 (and optionally psutil to keep track of FTP server memory usage). Example usages: ftpbench -u USER -p PASSWORD ftpbench -u USER -p PASSWORD -H ftp.domain.com -P 21 # host / port ftpbench -u USER -p PASSWORD -b transfer ftpbench -u USER -p PASSWORD -b concurrence ftpbench -u USER -p PASSWORD -b all ftpbench -u USER -p PASSWORD -b concurrence -n 500 # 500 clients ftpbench -u USER -p PASSWORD -b concurrence -s 20M # file size ftpbench -u USER -p PASSWORD -b concurrence -p 3521 # memory usage """ # Some benchmarks (Linux 3.0.0, Intel core duo - 3.1 Ghz). # pyftpdlib 1.0.0: # # (starting with 6.7M of memory being used) # STOR (client -> server) 557.97 MB/sec 6.7M # RETR (server -> client) 1613.82 MB/sec 6.8M # 300 concurrent clients (connect, login) 1.20 secs 8.8M # STOR (1 file with 300 idle clients) 567.52 MB/sec 8.8M # RETR (1 file with 300 idle clients) 1561.41 MB/sec 8.8M # 300 concurrent clients (RETR 10.0M file) 3.26 secs 10.8M # 300 concurrent clients (STOR 10.0M file) 8.46 secs 12.6M # 300 concurrent clients (QUIT) 0.07 secs # # # proftpd 1.3.4a: # # (starting with 1.4M of memory being used) # STOR (client -> server) 554.67 MB/sec 3.2M # RETR (server -> client) 1517.12 MB/sec 3.2M # 300 concurrent clients (connect, login) 9.30 secs 568.6M # STOR (1 file with 300 idle clients) 484.11 MB/sec 570.6M # RETR (1 file with 300 idle clients) 1534.61 MB/sec 570.6M # 300 concurrent clients (RETR 10.0M file) 3.67 secs 568.6M # 300 concurrent clients (STOR 10.0M file) 11.21 secs 568.7M # 300 concurrent clients (QUIT) 0.43 secs # # # vsftpd 2.3.2 # # (starting with 352.0K of memory being used) # STOR (client -> server) 607.23 MB/sec 816.0K # RETR (server -> client) 1506.59 MB/sec 816.0K # 300 concurrent clients (connect, login) 18.91 secs 140.9M # STOR (1 file with 300 idle clients) 618.99 MB/sec 141.4M # RETR (1 file with 300 idle clients) 1402.48 MB/sec 141.4M # 300 concurrent clients (RETR 10.0M file) 3.64 secs 140.9M # 300 concurrent clients (STOR 10.0M file) 9.74 secs 140.9M # 300 concurrent clients (QUIT) 0.00 secs from __future__ import division, print_function import asynchat import asyncore import atexit import contextlib import ftplib import optparse import os import ssl import sys import time try: import resource except ImportError: resource = None try: import psutil except ImportError: psutil = None HOST = 'localhost' PORT = 21 USER = None PASSWORD = None TESTFN = "$testfile" BUFFER_LEN = 8192 SERVER_PROC = None TIMEOUT = None FILE_SIZE = "10M" SSL = False PY3 = sys.version_info >= (3, 0) server_memory = [] # python >= 2.7.9 SSLWantReadError = getattr(ssl, "SSLWantReadError", object()) SSLWantWriteError = getattr(ssl, "SSLWantWriteError", object()) # python <= 2.7.8 SSL_ERROR_WANT_READ = getattr(ssl, "SSL_ERROR_WANT_READ", object()) SSL_ERROR_WANT_WRITE = getattr(ssl, "SSL_ERROR_WANT_WRITE", object()) if not sys.stdout.isatty() or os.name != 'posix': def hilite(s, *args, **kwargs): return s else: # http://goo.gl/6V8Rm def hilite(string, ok=True, bold=False): """Return an highlighted version of 'string'.""" attr = [] if ok is None: # no color pass elif ok: # green attr.append('32') else: # red attr.append('31') if bold: attr.append('1') return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), string) def print_bench(what, value, unit=""): s = "%s %s %-8s" % (hilite("%-50s" % what, ok=None, bold=0), hilite("%8.2f" % value), unit) if server_memory: s += "%s" % hilite(server_memory.pop()) print(s.strip()) # http://goo.gl/zeJZl def bytes2human(n, format="%(value).1f%(symbol)s"): """ >>> bytes2human(10000) '9K' >>> bytes2human(100001221) '95M' """ symbols = ('B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') prefix = {} for i, s in enumerate(symbols[1:]): prefix[s] = 1 << (i + 1) * 10 for symbol in reversed(symbols[1:]): if n >= prefix[symbol]: value = float(n) / prefix[symbol] return format % locals() return format % dict(symbol=symbols[0], value=n) # http://goo.gl/zeJZl def human2bytes(s): """ >>> human2bytes('1M') 1048576 >>> human2bytes('1G') 1073741824 """ symbols = ('B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') letter = s[-1:].strip().upper() num = s[:-1] assert num.isdigit() and letter in symbols, s num = float(num) prefix = {symbols[0]: 1} for i, s in enumerate(symbols[1:]): prefix[s] = 1 << (i + 1) * 10 return int(num * prefix[letter]) def register_memory(): """Register an approximation of memory used by FTP server process and all of its children. """ # XXX How to get a reliable representation of memory being used is # not clear. (rss - shared) seems kind of ok but we might also use # the private working set via get_memory_maps().private*. def get_mem(proc): if os.name == 'posix': mem = proc.memory_info_ex() counter = mem.rss if 'shared' in mem._fields: counter -= mem.shared return counter else: # TODO figure out what to do on Windows return proc.get_memory_info().rss if SERVER_PROC is not None: mem = get_mem(SERVER_PROC) for child in SERVER_PROC.children(): mem += get_mem(child) server_memory.append(bytes2human(mem)) def timethis(what): """"Utility function for making simple benchmarks (calculates time calls). It can be used either as a context manager or as a decorator. """ @contextlib.contextmanager def benchmark(): timer = time.clock if sys.platform == "win32" else time.time start = timer() yield stop = timer() res = (stop - start) print_bench(what, res, "secs") if hasattr(what, "__call__"): def timed(*args, **kwargs): with benchmark(): return what(*args, **kwargs) return timed else: return benchmark() def connect(): """Connect to FTP server, login and return an ftplib.FTP instance.""" ftp_class = ftplib.FTP if not SSL else ftplib.FTP_TLS ftp = ftp_class(timeout=TIMEOUT) ftp.connect(HOST, PORT) ftp.login(USER, PASSWORD) if SSL: ftp.prot_p() # secure data connection return ftp def retr(ftp): """Same as ftplib's retrbinary() but discard the received data.""" ftp.voidcmd('TYPE I') with contextlib.closing(ftp.transfercmd("RETR " + TESTFN)) as conn: recv_bytes = 0 while True: data = conn.recv(BUFFER_LEN) if not data: break recv_bytes += len(data) ftp.voidresp() def stor(ftp=None): """Same as ftplib's storbinary() but just sends dummy data instead of reading it from a real file. """ if ftp is None: ftp = connect() quit = True else: quit = False ftp.voidcmd('TYPE I') with contextlib.closing(ftp.transfercmd("STOR " + TESTFN)) as conn: chunk = b'x' * BUFFER_LEN total_sent = 0 while True: sent = conn.send(chunk) total_sent += sent if total_sent >= FILE_SIZE: break ftp.voidresp() if quit: ftp.quit() return ftp def bytes_per_second(ftp, retr=True): """Return the number of bytes transmitted in 1 second.""" tot_bytes = 0 if retr: def request_file(): ftp.voidcmd('TYPE I') conn = ftp.transfercmd("retr " + TESTFN) return conn with contextlib.closing(request_file()) as conn: register_memory() stop_at = time.time() + 1.0 while stop_at > time.time(): chunk = conn.recv(BUFFER_LEN) if not chunk: a = time.time() while conn.recv(BUFFER_LEN): break conn.close() ftp.voidresp() conn = request_file() stop_at += time.time() - a tot_bytes += len(chunk) conn.close() try: ftp.voidresp() except (ftplib.error_temp, ftplib.error_perm): pass else: ftp.voidcmd('TYPE I') with contextlib.closing(ftp.transfercmd("STOR " + TESTFN)) as conn: register_memory() chunk = b'x' * BUFFER_LEN stop_at = time.time() + 1 while stop_at > time.time(): tot_bytes += conn.send(chunk) ftp.voidresp() return tot_bytes def cleanup(): ftp = connect() try: if TESTFN in ftp.nlst(): ftp.delete(TESTFN) except (ftplib.error_perm, ftplib.error_temp) as err: msg = "could not delete %r test file on cleanup: %r" % (TESTFN, err) print(hilite(msg, ok=False), file=sys.stderr) ftp.quit() def bench_stor(ftp=None, title="STOR (client -> server)"): if ftp is None: ftp = connect() tot_bytes = bytes_per_second(ftp, retr=False) print_bench(title, round(tot_bytes / 1024.0 / 1024.0, 2), "MB/sec") ftp.quit() def bench_retr(ftp=None, title="RETR (server -> client)"): if ftp is None: ftp = connect() tot_bytes = bytes_per_second(ftp, retr=True) print_bench(title, round(tot_bytes / 1024.0 / 1024.0, 2), "MB/sec") ftp.quit() def bench_multi(howmany): # The OS usually sets a limit of 1024 as the maximum number of # open file descriptors for the current process. # Let's set the highest number possible, just to be sure. if howmany > 500 and resource is not None: soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) resource.setrlimit(resource.RLIMIT_NOFILE, (hard, hard)) def bench_multi_connect(): with timethis("%i concurrent clients (connect, login)" % howmany): clients = [] for x in range(howmany): clients.append(connect()) register_memory() return clients def bench_multi_retr(clients): stor(clients[0]) with timethis("%s concurrent clients (RETR %s file)" % ( howmany, bytes2human(FILE_SIZE))): for ftp in clients: ftp.voidcmd('TYPE I') conn = ftp.transfercmd("RETR " + TESTFN) AsyncReader(conn) register_memory() asyncore.loop(use_poll=True) for ftp in clients: ftp.voidresp() def bench_multi_stor(clients): with timethis("%s concurrent clients (STOR %s file)" % ( howmany, bytes2human(FILE_SIZE))): for ftp in clients: ftp.voidcmd('TYPE I') conn = ftp.transfercmd("STOR " + TESTFN) AsyncWriter(conn, FILE_SIZE) register_memory() asyncore.loop(use_poll=True) for ftp in clients: ftp.voidresp() def bench_multi_quit(clients): for ftp in clients: AsyncQuit(ftp.sock) with timethis("%i concurrent clients (QUIT)" % howmany): asyncore.loop(use_poll=True) clients = bench_multi_connect() bench_stor(title="STOR (1 file with %s idle clients)" % len(clients)) bench_retr(title="RETR (1 file with %s idle clients)" % len(clients)) bench_multi_retr(clients) bench_multi_stor(clients) bench_multi_quit(clients) @contextlib.contextmanager def handle_ssl_want_rw_errs(): try: yield except (SSLWantReadError, SSLWantWriteError) as err: if DEBUG: print(err) except ssl.SSLError as err: if err.args[0] in (SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE): if DEBUG: print(err) else: raise class AsyncReader(asyncore.dispatcher): """Just read data from a connected socket, asynchronously.""" def __init__(self, sock): asyncore.dispatcher.__init__(self, sock) def handle_read(self): if SSL: with handle_ssl_want_rw_errs(): chunk = self.socket.recv(65536) else: chunk = self.socket.recv(65536) if not chunk: self.close() def handle_close(self): self.close() def handle_error(self): raise class AsyncWriter(asyncore.dispatcher): """Just write dummy data to a connected socket, asynchronously.""" def __init__(self, sock, size): asyncore.dispatcher.__init__(self, sock) self.size = size self.sent = 0 self.chunk = b'x' * BUFFER_LEN def handle_write(self): if SSL: with handle_ssl_want_rw_errs(): self.sent += asyncore.dispatcher.send(self, self.chunk) else: self.sent += asyncore.dispatcher.send(self, self.chunk) if self.sent >= self.size: self.handle_close() def handle_error(self): raise class AsyncQuit(asynchat.async_chat): def __init__(self, sock): asynchat.async_chat.__init__(self, sock) self.in_buffer = [] self.set_terminator(b'\r\n') self.push(b'QUIT\r\n') def collect_incoming_data(self, data): self.in_buffer.append(data) def found_terminator(self): self.handle_close() def handle_error(self): raise class OptFormatter(optparse.IndentedHelpFormatter): def format_epilog(self, s): return s.lstrip() def format_option(self, option): result = [] opts = self.option_strings[option] result.append(' %s\n' % opts) if option.help: help_text = ' %s\n\n' % self.expand_default(option) result.append(help_text) return ''.join(result) def main(): global HOST, PORT, USER, PASSWORD, SERVER_PROC, TIMEOUT, SSL, FILE_SIZE, \ DEBUG USAGE = "%s -u USERNAME -p PASSWORD [-H] [-P] [-b] [-n] [-s] [-k] " \ "[-t] [-d] [-S]" % (os.path.basename(__file__)) parser = optparse.OptionParser(usage=USAGE, epilog=__doc__[__doc__.find('Example'):], formatter=OptFormatter()) parser.add_option('-u', '--user', dest='user', help='username') parser.add_option('-p', '--pass', dest='password', help='password') parser.add_option('-H', '--host', dest='host', default=HOST, help='hostname') parser.add_option('-P', '--port', dest='port', default=PORT, help='port', type=int) parser.add_option('-b', '--benchmark', dest='benchmark', default='transfer', help="benchmark type ('transfer', 'download', 'upload', " "'concurrence', 'all')") parser.add_option('-n', '--clients', dest='clients', default=200, type="int", help="number of concurrent clients used by " "'concurrence' benchmark") parser.add_option('-s', '--filesize', dest='filesize', default="10M", help="file size used by 'concurrence' benchmark " "(e.g. '10M')") parser.add_option('-k', '--pid', dest='pid', default=None, type="int", help="the PID of the FTP server process, to track its " "memory usage") parser.add_option('-t', '--timeout', dest='timeout', default=TIMEOUT, type="int", help="the socket timeout") parser.add_option('-d', '--debug', action='store_true', dest='debug', help="whether to print debugging info") parser.add_option('-S', '--ssl', action='store_true', dest='ssl', help="whether to use FTPS") options, args = parser.parse_args() if not options.user or not options.password: sys.exit(USAGE) else: USER = options.user PASSWORD = options.password HOST = options.host PORT = options.port TIMEOUT = options.timeout SSL = bool(options.ssl) DEBUG = options.debug if SSL and sys.version_info < (2, 7): sys.exit("--ssl option requires python >= 2.7") try: FILE_SIZE = human2bytes(options.filesize) except (ValueError, AssertionError): parser.error("invalid file size %r" % options.filesize) if options.pid is not None: if psutil is None: raise ImportError("-p option requires psutil module") SERVER_PROC = psutil.Process(options.pid) # before starting make sure we have write permissions ftp = connect() conn = ftp.transfercmd("STOR " + TESTFN) conn.close() ftp.voidresp() ftp.delete(TESTFN) ftp.quit() atexit.register(cleanup) # start benchmark if SERVER_PROC is not None: register_memory() print("(starting with %s of memory being used)" % ( hilite(server_memory.pop()))) if options.benchmark == 'download': stor() bench_retr() elif options.benchmark == 'upload': bench_stor() elif options.benchmark == 'transfer': bench_stor() bench_retr() elif options.benchmark == 'concurrence': bench_multi(options.clients) elif options.benchmark == 'all': bench_stor() bench_retr() bench_multi(options.clients) else: sys.exit("invalid 'benchmark' parameter %r" % options.benchmark) if __name__ == '__main__': main() pyftpdlib-release-1.5.4/scripts/generate_manifest.py000066400000000000000000000014071327314554200226650ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (c) 2009 Giampaolo Rodola'. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """ Generate MANIFEST.in file. """ import os import subprocess IGNORED_EXTS = ('.png', '.jpg', '.jpeg') IGNORED_FILES = ('.travis.yml', 'appveyor.yml') def sh(cmd): return subprocess.check_output( cmd, shell=True, universal_newlines=True).strip() def main(): files = sh("git ls-files").split('\n') for file in files: if file.startswith('.ci/') or \ os.path.splitext(file)[1].lower() in IGNORED_EXTS or \ file in IGNORED_FILES: continue print("include " + file) if __name__ == '__main__': main() pyftpdlib-release-1.5.4/scripts/print_announce.py000077500000000000000000000045741327314554200222420ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (c) 2009 Giampaolo Rodola'. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """ Prints release announce based on HISTORY.rst file content. """ import os import re from pyftpdlib import __ver__ as PRJ_VERSION HERE = os.path.abspath(os.path.dirname(__file__)) HISTORY = os.path.abspath(os.path.join(HERE, '../HISTORY.rst')) 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_WHATSNEW = \ 'https://github.com/giampaolo/pyftpdlib/blob/master/HISTORY.rst' template = """\ Hello all, I'm glad to announce the release of {prj_name} {prj_version}: {prj_urlhome} About ===== Python FTP server library provides a high-level portable interface to easily \ write very efficient, scalable and asynchronous FTP servers with Python. What's new ========== {changes} Links ===== - Home page: {prj_urlhome} - Download: {prj_urldownload} - Documentation: {prj_urldoc} - What's new: {prj_urlwhatsnew} -- Giampaolo - http://grodola.blogspot.com """ def get_changes(): """Get the most recent changes for this release by parsing HISTORY.rst file. """ with open(HISTORY) as f: lines = f.readlines() block = [] # eliminate the part preceding the first block for i, line in enumerate(lines): line = lines.pop(0) if line.startswith('===='): break lines.pop(0) for i, line in enumerate(lines): line = lines.pop(0) line = line.rstrip() if re.match("^- \d+_: ", line): num, _, rest = line.partition(': ') num = ''.join([x for x in num if x.isdigit()]) line = "- #%s: %s" % (num, rest) if line.startswith('===='): break block.append(line) # eliminate bottom empty lines block.pop(-1) while not block[-1]: block.pop(-1) return "\n".join(block) def main(): changes = get_changes() print(template.format( prj_name=PRJ_NAME, prj_version=PRJ_VERSION, prj_urlhome=PRJ_URL_HOME, prj_urldownload=PRJ_URL_DOWNLOAD, prj_urldoc=PRJ_URL_DOC, prj_urlwhatsnew=PRJ_URL_WHATSNEW, changes=changes, )) if __name__ == '__main__': main() pyftpdlib-release-1.5.4/scripts/purge_installation.py000066400000000000000000000020221327314554200231020ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (c) 2009 Giampaolo Rodola'. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """ Purge pyftpdlib installation by removing pyftpdlib-related files and directories found in site-packages directories. This is needed mainly because sometimes "import pyftpdlib" imports a leftover installation from site-packages directory instead of the main working directory. """ import os import shutil import site PKGNAME = "pyftpdlib" def rmpath(path): if os.path.isdir(path): print("rmdir " + path) shutil.rmtree(path) else: print("rm " + path) os.remove(path) def main(): locations = [site.getusersitepackages()] locations.extend(site.getsitepackages()) for root in locations: if os.path.isdir(root): for name in os.listdir(root): if PKGNAME in name: abspath = os.path.join(root, name) rmpath(abspath) main() pyftpdlib-release-1.5.4/setup.py000066400000000000000000000117251327314554200166620ustar00rootroot00000000000000# Copyright (C) 2007 Giampaolo Rodola' . # Use of this source code is governed by MIT license that can be # found in the LICENSE file. """pyftpdlib installer. $ python setup.py install """ from __future__ import print_function import os import sys import textwrap try: from setuptools import setup except ImportError: from distutils.core import setup def get_version(): INIT = os.path.abspath(os.path.join(os.path.dirname(__file__), 'pyftpdlib', '__init__.py')) with open(INIT, 'r') as f: for line in f: if line.startswith('__ver__'): ret = eval(line.strip().split(' = ')[1]) assert ret.count('.') == 2, ret for num in ret.split('.'): assert num.isdigit(), ret return ret raise ValueError("couldn't find version string") def term_supports_colors(): try: import curses assert sys.stderr.isatty() curses.setupterm() assert curses.tigetnum("colors") > 0 except Exception: return False else: return True def hilite(s, ok=True, bold=False): """Return an highlighted version of 's'.""" if not term_supports_colors(): return s else: attr = [] if ok is None: # no color pass elif ok: attr.append('32') # green else: attr.append('31') # red if bold: attr.append('1') return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), s) if sys.version_info < (2, 6): sys.exit('python version not supported (< 2.6)') require_pysendfile = (os.name == 'posix' and sys.version_info < (3, 3)) extras_require = {'ssl': ["PyOpenSSL"]} if require_pysendfile: extras_require.update({'sendfile': ['pysendfile']}) VERSION = get_version() def main(): setup( name='pyftpdlib', version=get_version(), description='Very fast asynchronous FTP server library', long_description=open('README.rst').read(), license='MIT', platforms='Platform Independent', author="Giampaolo Rodola'", author_email='g.rodola@gmail.com', url='https://github.com/giampaolo/pyftpdlib/', packages=['pyftpdlib', 'pyftpdlib.test'], scripts=['scripts/ftpbench'], package_data={ "pyftpdlib.test": [ "README", 'keycert.pem', ], }, keywords=['ftp', 'ftps', 'server', 'ftpd', 'daemon', 'python', 'ssl', 'sendfile', 'asynchronous', 'nonblocking', 'eventdriven', 'rfc959', 'rfc1123', 'rfc2228', 'rfc2428', 'rfc2640', 'rfc3659'], extras_require=extras_require, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Topic :: Internet :: File Transfer Protocol (FTP)', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: System :: Filesystems', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', ], ) # suggest to install pysendfile if require_pysendfile: try: # os.sendfile() appeared in python 3.3 # http://bugs.python.org/issue10882 if not hasattr(os, 'sendfile'): # fallback on using third-party pysendfile module # https://github.com/giampaolo/pysendfile/ import sendfile if hasattr(sendfile, 'has_sf_hdtr'): # old 1.2.4 version raise ImportError except ImportError: msg = textwrap.dedent(""" 'pysendfile' third-party module is not installed. This is not essential but it considerably speeds up file transfers. You can install it with 'pip install pysendfile'. More at: https://github.com/giampaolo/pysendfile""") print(hilite(msg, ok=False), file=sys.stderr) try: from OpenSSL import SSL # NOQA except ImportError: msg = textwrap.dedent(""" 'pyopenssl' third-party module is not installed. This means FTPS support will be disabled. You can install it with: 'pip install pyopenssl'.""") print(hilite(msg, ok=False), file=sys.stderr) if __name__ == '__main__': main() pyftpdlib-release-1.5.4/tox.ini000066400000000000000000000014511327314554200164560ustar00rootroot00000000000000# Tox (http://tox.testrun.org/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. # To use it run "pip install tox" and then run "tox" from this # directory. [tox] envlist = py26, py27, py32, py33, py34, py35 [testenv] deps = flake8 pysendfile PyOpenSSL py26: ipaddress py26: mock==1.0.1 py26: unittest2 py27: ipaddress py27: mock py32: ipaddress py32: mock==1.0.1 py32: unittest2 py33: ipaddress setenv = PYTHONPATH = {toxinidir} TOX = 1 SILENT = 1 commands = python pyftpdlib/test/runner.py git ls-files | grep \\.py$ | xargs flake8 # suppress "WARNING: 'git' command found but not installed in testenv whitelist_externals = git usedevelop = True