pyftpdlib-1.2.0/0000775000175000017500000000000012135246616015461 5ustar giampaologiampaolo00000000000000pyftpdlib-1.2.0/pyftpdlib/0000775000175000017500000000000012135246616017456 5ustar giampaologiampaolo00000000000000pyftpdlib-1.2.0/pyftpdlib/__main__.py0000664000175000017500000001140112130037216021532 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: __main__.py 1206 2013-04-06 15:26:00Z g.rodola $ # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ====================================================================== """ Start a stand alone anonymous FTP server from the command line as in: $ python -m pyftpdlib """ import optparse import sys import os from pyftpdlib import __ver__ from pyftpdlib.authorizers import DummyAuthorizer from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer from pyftpdlib._compat import getcwdu 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.ftpserver [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 21)") parser.add_option('-w', '--write', action="store_true", default=False, help="grants write access for the anonymous user " "(default read-only)") parser.add_option('-d', '--directory', default=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('-v', '--version', action='store_true', help="print pyftpdlib version and exit") options, args = parser.parse_args() if options.version: sys.exit("pyftpdlib %s" % __ver__) passive_ports = None if options.range: try: start, stop = options.range.split('-') start = int(start) stop = int(stop) except ValueError: parser.error('invalid argument passed to -r option') else: passive_ports = 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 "elradfmwM" or "elr" authorizer.add_anonymous(options.directory, perm=perm) handler = FTPHandler handler.authorizer = authorizer handler.masquerade_address = options.nat_address handler.passive_ports = passive_ports ftpd = FTPServer((options.interface, options.port), FTPHandler) try: ftpd.serve_forever() finally: ftpd.close_all() if __name__ == '__main__': main() pyftpdlib-1.2.0/pyftpdlib/servers.py0000664000175000017500000004756212134253126021530 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: servers.py 1219 2013-04-19 14:35:41Z g.rodola $ # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ====================================================================== """ This module 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://code.google.com/p/pyftpdlib/issues/detail?id=197 https://code.google.com/p/pyftpdlib/issues/detail?id=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 os import socket import traceback import sys import errno import select import logging import signal import time from pyftpdlib.log import logger from pyftpdlib.ioloop import Acceptor, IOLoop __all__ = ['FTPServer'] _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=5): """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 isinstance(address_or_socket, socket.socket): sock = address_or_socket sock.setblocking(0) self.set_socket(sock) if hasattr(sock, 'family'): self._af = sock.family else: # python 2.4 ip, port = self.socket.getsockname()[:2] self._af = socket.getaddrinfo(ip, port, socket.AF_UNSPEC, socket.SOCK_STREAM)[0][0] else: self._af = self.bind_af_unspecified(address_or_socket) self.listen(backlog) @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): if not logging.getLogger().handlers: # If we get to this point it means the user hasn't # configured logger. We want to log by default so # we configure logging ourselves so that it will # print to stderr. from pyftpdlib.ioloop import _config_logging _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 logger.info(">>> starting FTP server on %s:%s, pid=%i <<<" % (addr[0], addr[1], os.getpid())) logger.info("poller: %r", self.ioloop.__class__) logger.info("masquerade (NAT) address: %s", self.handler.masquerade_address) logger.info("passive ports: %s", pasv_ports) if os.name == 'posix': logger.info("use sendfile(2): %s", self.handler.use_sendfile) 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 (default 1.0). - (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 == True 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 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: 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: # - http://code.google.com/p/pyftpdlib/issues/detail?id=143 # - http://code.google.com/p/pyftpdlib/issues/detail?id=166 # - https://groups.google.com/forum/#!topic/pyftpdlib/h7pPybzAx14 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 _lock = None _exit = None def __init__(self, address, handler, ioloop=None): FTPServer.__init__(self, address, handler, ioloop) self._active_tasks = [] def _start_task(self, *args, **kwargs): raise NotImplementedError('must be implemented in subclass') def _current_task(self): raise NotImplementedError('must be implemented in subclass') def _map_len(self): raise NotImplementedError('must be implemented in subclass') def _loop(self, handler): """Serve handler's IO loop in a separate thread or process.""" ioloop = IOLoop() try: handler.ioloop = ioloop try: handler.add_channel() except EnvironmentError: err = sys.exc_info()[1] if err.errno == errno.EBADF: # we might get here in case the other end quickly # disconnected (see test_quick_connect()) 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://code.google.com/p/pyftpdlib/issues/detail?id=245 if not ioloop.socket_map: ioloop.sched.reheapify() # get rid of cancel()led calls 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: # on Windows we can get WSAENOTSOCK if the client # rapidly connect and disconnects err = sys.exc_info()[1] if os.name == 'nt' and err.args[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 finally: try: self._active_tasks.remove(self._current_task()) except ValueError: pass ioloop.close() 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,)) t.name = repr(addr) t.start() self._lock.acquire() try: self._active_tasks.append(t) finally: self._lock.release() def _log_start(self): FTPServer._log_start(self) logger.info("dispatcher: %r", self.__class__) def serve_forever(self, timeout=None, blocking=True, handle_exit=True): self._exit.clear() if handle_exit: log = handle_exit and blocking == True 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 close_all(self): tasks = self._active_tasks[:] # this must be set after getting active tasks as it causes # thread objects to get out of the list too soon self._exit.set() if tasks and hasattr(tasks[0], 'terminate'): # we're dealing with subprocesses for t in tasks: 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: err = sys.exc_info()[1] if err.errno != errno.ESRCH: raise self._wait_for_tasks(tasks) del self._active_tasks[:] FTPServer.close_all(self) def _wait_for_tasks(self, tasks): """Wait for threads or subprocesses to terminate.""" warn = logger.warning for t in tasks: t.join(self.join_timeout) if t.is_alive(): # Thread or process is still alive. If it's a process # attempt to send SIGKILL as last resort. # Set timeout to None so that we will exit immediately # in case also other threads/processes are hanging. self.join_timeout = None if hasattr(t, 'terminate'): msg = "could not terminate process %r" % t if not _BSD: warn(msg + "; sending SIGKILL as last resort") try: os.kill(t.pid, signal.SIGKILL) except OSError: err = sys.exc_info()[1] if err.errno != errno.ESRCH: raise else: warn(msg) else: warn("thread %r didn't terminate; ignoring it", t) try: import threading except ImportError: pass else: __all__ += ['ThreadedFTPServer'] # compatibility with python <= 2.6 if not hasattr(threading.Thread, 'is_alive'): threading.Thread.is_alive = threading.Thread.isAlive 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) def _current_task(self): return threading.currentThread() def _map_len(self): return threading.activeCount() if os.name == 'posix': try: import multiprocessing except ImportError: pass else: __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) def _current_task(self): return multiprocessing.current_process() def _map_len(self): return len(multiprocessing.active_children()) pyftpdlib-1.2.0/pyftpdlib/handlers.py0000664000175000017500000040377712134121172021636 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: handlers.py 1218 2013-04-19 01:48:39Z g.rodola $ # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ====================================================================== import asynchat import time import sys import os import errno import socket import traceback import glob import random import warnings import logging try: import pwd import grp except ImportError: pwd = grp = None from pyftpdlib import __ver__ from pyftpdlib.log import logger from pyftpdlib.filesystems import FilesystemError, AbstractedFS from pyftpdlib._compat import PY3, b, u, getcwdu, unicode, xrange, next from pyftpdlib.ioloop import AsyncChat, Connector, Acceptor, timer, _DISCONNECTED from pyftpdlib.authorizers import (DummyAuthorizer, AuthenticationFailed, AuthorizerError) 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: # http://code.google.com/p/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 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).'), '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 h1,h2,h3,h4,p1,p2 (open active data connection).'), 'PWD' : dict(perm=None, auth=True, arg=False, help='Syntax: PWD (get current working directory).'), 'QUIT' : dict(perm=None, auth=False, arg=False, help='Syntax: QUIT (quit current session).'), 'REIN' : dict(perm=None, auth=True, arg=False, help='Syntax: REIN (flush account).'), 'REST' : dict(perm=None, auth=True, arg=True, help='Syntax: REST 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 [ site-command] (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 [ file-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 _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 sock = None try: try: if not socket.has_ipv6: return False sock = socket.socket(socket.AF_INET6) return not sock.getsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY) except (socket.error, AttributeError): return False finally: if sock is not None: sock.close() SUPPORTS_HYBRID_IPV6 = _support_hybrid_ipv6() class _FileReadWriteError(OSError): """Exception raised when reading or writing a file during a transfer.""" # --- 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 self._closed = False self._idler = None 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._af != socket.AF_INET: # dual stack IPv4/IPv6 support af = self.bind_af_unspecified((local_ip, 0)) self.socket.close() else: af = self.cmd_channel._af 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: err = sys.exc_info()[1] if err.args[0] == errno.EADDRINUSE: # port already in use if ports: continue # If cannot use one of the ports in the configured # range we'll use a kernel-assigned port, and log # a message reporting the issue. # By using 0 as port number value we let kernel # choose a free unprivileged random port. else: self.bind((local_ip, 0)) self.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: self.cmd_channel.respond('227 Entering passive mode (%s,%d,%d).' % ( ip.replace('.', ','), port // 256, port % 256)) else: self.cmd_channel.respond('229 Entering extended passive mode ' '(|||%d|).' % port) if self.timeout: self._idler = self.ioloop.call_later(self.timeout, self.handle_timeout, _errback=self.handle_error) # --- 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=logging.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): if not self._closed: self._closed = True Acceptor.close(self) if self._idler is not None and not self._idler.cancelled: self._idler.cancel() 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._closed = False 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): if not self._closed: self._closed = True if self.socket is not None: 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._closed = False 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: err = sys.exc_info()[1] # if we get an exception here we want the dispatcher # instance to set socket attribute before closing, see: # http://code.google.com/p/pyftpdlib/issues/detail?id=188 AsyncChat.__init__(self, socket.socket(), ioloop=cmd_channel.ioloop) # http://code.google.com/p/pyftpdlib/issues/detail?id=143 self.close() if err.args[0] == errno.EINVAL: return self.handle_error() return # remove this instance from 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): try: addr = "%s:%s" % self.socket.getpeername()[:2] except socket.error: addr = None status = [self.__class__.__module__+ "." + self.__class__.__name__] status.append("(addr=%s, user=%r, receive=%r, file=%r)" \ % (addr, self.cmd_channel.username or '', self.receive, getattr(self.file_obj, 'name', ''))) return '<%s at %#x>' % (' '.join(status), id(self)) __str__ = __repr__ def _use_sendfile(self, producer): return self.cmd_channel.use_sendfile \ and isinstance(producer, FileProducer) \ and producer.type == 'i' def push(self, data): self._initialized = True self.ioloop.modify(self._fileno, self.ioloop.WRITE) AsyncChat.push(self, data) def push_with_producer(self, producer): self._initialized = True self.ioloop.modify(self._fileno, self.ioloop.WRITE) if self._use_sendfile(producer): self._offset = producer.file.tell() self._filefd = self.file_obj.fileno() self.initiate_sendfile() self.initiate_send = self.initiate_sendfile else: 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: err = sys.exc_info()[1] if err.errno in (errno.EAGAIN, errno.EWOULDBLOCK, errno.EBUSY): return elif err.errno in _DISCONNECTED: self.handle_close() else: raise else: if sent == 0: # this signals the channel that the transfer is completed self.discard_buffers() self.handle_close() else: self._offset += sent self.tot_bytes_sent += sent # --- utility methods def _posix_ascii_data_wrapper(self, chunk): """The data wrapper used for receiving data in ASCII mode on systems using a single line terminator, handling those cases where CRLF ('\r\n') gets delivered in two chunks. """ if self._had_cr: chunk = 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.ioloop.modify(self._fileno, 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): """Overridden as a fix around http://bugs.python.org/issue1740572 (when the producer is consumed, close() was called instead of handle_close()). """ while 1: if len(self.producer_fifo): p = self.producer_fifo.first() # a 'None' in the producer fifo is a sentinel, # telling us to close the channel. if p is None: if not self.ac_out_buffer: self.producer_fifo.pop() #self.close() self.handle_close() return elif isinstance(p, str): self.producer_fifo.pop() self.ac_out_buffer += p return data = p.more() if data: self.ac_out_buffer = self.ac_out_buffer + data return else: self.producer_fifo.pop() else: return def handle_read(self): """Called when there is data waiting to be read.""" try: chunk = self.recv(self.ac_in_buffer_size) except socket.error: self.handle_error() else: self.tot_bytes_received += len(chunk) if not chunk: self.transfer_finished = True #self.close() # <-- asyncore.recv() already do that... return if self._data_wrapper is not None: chunk = self._data_wrapper(chunk) try: self.file_obj.write(chunk) except OSError: err = sys.exc_info()[1] 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: err = sys.exc_info()[1] error = _strerror(err.args[0]) 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.""" if not self._closed: self._closed = True # RFC-959 says we must close the connection before replying AsyncChat.close(self) if self._resp: self.cmd_channel.respond(self._resp[0], logfun=self._resp[1]) if self.file_obj is not None and not self.file_obj.closed: self.file_obj.close() if self._idler is not None and not self._idler.cancelled: self._idler.cancel() if self.file_obj is not None: filename = self.file_obj.name elapsed_time = round(self.get_elapsed_time(), 3) self.cmd_channel.log_transfer(cmd=self.cmd, filename=self.file_obj.name, receive=self.receive, completed=self.transfer_finished, elapsed=elapsed_time, bytes=self.get_transmitted_bytes()) if self.transfer_finished: if self.receive: self.cmd_channel.on_file_received(filename) else: self.cmd_channel.on_file_sent(filename) else: if self.receive: self.cmd_channel.on_incomplete_file_received(filename) else: self.cmd_channel.on_incomplete_file_sent(filename) self.cmd_channel._on_dtp_close() # 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 _use_sendfile(self, producer): 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: err = sys.exc_info()[1] 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: http://code.google.com/p/pysendfile/ Automatically defaults to True if pysendfile module is installed. - (bool) tcp_no_delay: controls the use of the TCP_NODELAY socket option which disables the Nagle algorithm resulting in significantly better performances (default True on all systems where it is supported). - (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]' 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 = "" # private session attributes self._last_response = "" self._current_type = 'a' self._restart_position = 0 self._quit_pending = False self._af = -1 self._in_buffer = [] self._in_buffer_len = 0 self._epsvall = False self._dtp_acceptor = None self._dtp_connector = None self._in_dtp_queue = None self._out_dtp_queue = None self._closed = False self._extra_feats = [] self._current_facts = ['type', 'perm', 'size', 'modify'] self._rnfr = None 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: err = sys.exc_info()[1] # if we get an exception here we want the dispatcher # instance to set socket attribute before closing, see: # http://code.google.com/p/pyftpdlib/issues/detail?id=188 AsyncChat.__init__(self, socket.socket(), ioloop=ioloop) self.close() if err.args[0] == errno.EINVAL: # http://code.google.com/p/pyftpdlib/issues/detail?id=143 return self.handle_error() return self.set_terminator(b("\r\n")) # connection properties try: self.remote_ip, self.remote_port = self.socket.getpeername()[:2] except socket.error: err = sys.exc_info()[1] # A race condition may occur if the other end is closing # before we can get the peername, hence ENOTCONN (see issue # #100) while EINVAL can occur on OSX (see issue #143). self.connected = False if err.args[0] in (errno.ENOTCONN, errno.EINVAL): self.close() else: self.handle_error() return else: self.log("FTP session opened (connect)") if hasattr(self.socket, 'family'): self._af = self.socket.family else: # python < 2.5 ip, port = self.socket.getsockname()[:2] self._af = socket.getaddrinfo(ip, port, socket.AF_UNSPEC, socket.SOCK_STREAM)[0][0] # try to handle urgent data inline try: self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_OOBINLINE, 1) except socket.error: pass # disable Nagle algorithm for the control socket only, resulting # in significantly better performances if self.tcp_no_delay: try: self.socket.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) except socket.error: pass # remove this instance from 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 __repr__(self): status = [self.__class__.__module__+ "." + self.__class__.__name__] status.append("(addr=%s:%s, user=%r)" % (self.remote_ip, self.remote_port, self.username or '')) return '<%s at %#x>' % (' '.join(status), id(self)) __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: # http://code.google.com/p/pyftpdlib/issues/detail?id=188#c18 # In contrast to DTPHandler, here we are not interested in # attempting to receive any further data from a closed socket. return 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 not cmd in self.proto_cmds: if cmd[-4:] in ('ABOR', 'STAT', 'QUIT'): cmd = cmd[-4:] else: msg = 'Command "%s" not understood.' % cmd self.respond('500 ' + msg) if cmd: self.log_cmd(cmd, arg, 500, msg) return if not arg and self.proto_cmds[cmd]['arg'] == True: msg = "Syntax error: command needs an argument." self.respond("501 " + msg) self.log_cmd(cmd, "", 501, msg) return if arg and self.proto_cmds[cmd]['arg'] == False: msg = "Syntax error: command does not accept arguments." self.respond("501 " + msg) self.log_cmd(cmd, arg, 501, msg) return if not self.authenticated: if self.proto_cmds[cmd]['auth'] or (cmd == 'STAT' and arg): msg = "Log in with USER and PASS first." self.respond("530 " + msg) self.log_cmd(cmd, arg, 530, msg) else: # call the proper ftp_* method self.process_command(cmd, arg) return else: if (cmd == 'STAT') and not arg: self.ftp_STAT(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) 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.""" if not self._closed: self._closed = True self._closing = False self.connected = False 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: # 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: # 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"] 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._af == 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._af == socket.AF_INET: self.respond('522 Network protocol not supported (use 1).') else: self._make_eport(ip, port) else: if self._af == socket.AF_INET: self.respond('501 Unknown network protocol (use 1).') else: self.respond('501 Unknown network protocol (use 2).') def ftp_PASV(self, line): """Start a passive data channel by using IPv4.""" if self._epsvall: self.respond("501 PASV not allowed after EPSV ALL.") return self._make_epasv(extmode=False) def ftp_EPSV(self, line): """Start a passive data channel by using IPv4 or IPv6 as defined in RFC-2428. """ # RFC-2428 specifies that if an optional parameter is given, # we have to determine the address family from that otherwise # use the same address family used on the control connection. # In such a scenario a client may use IPv4 on the control channel # and choose to use IPv6 for the data channel. # But how could we use IPv6 on the data channel without knowing # which IPv6 address to use for binding the socket? # Unfortunately RFC-2428 does not provide satisfing information # on how to do that. The assumption is that we don't have any way # to know wich address to use, hence we just use the same address # family used on the control connection. if not line: self._make_epasv(extmode=True) # IPv4 elif line == "1": if self._af != socket.AF_INET: self.respond('522 Network protocol not supported (use 2).') else: self._make_epasv(extmode=True) # IPv6 elif line == "2": if self._af == socket.AF_INET: self.respond('522 Network protocol not supported (use 1).') else: self._make_epasv(extmode=True) elif line.lower() == 'all': self._epsvall = True self.respond('220 Other commands other than EPSV are now disabled.') else: if self._af == socket.AF_INET: self.respond('501 Unknown network protocol (use 1).') else: self.respond('501 Unknown network protocol (use 2).') def ftp_QUIT(self, line): """Quit the current session disconnecting the client.""" if self.authenticated: msg_quit = self.authorizer.get_msg_quit(self.username) else: msg_quit = "Goodbye." if len(msg_quit) <= 75: self.respond("221 %s" % msg_quit) else: self.push("221-%s\r\n" % msg_quit) self.respond("221 ") # From RFC-959: # If file transfer is in progress, the connection must remain # open for result response and the server will then close it. # We also stop responding to any further command. if self.data_channel: self._quit_pending = True self.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: iterator = self.run_as_current_user(self.fs.get_list_dir, path) except (OSError, FilesystemError): err = sys.exc_info()[1] 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 = 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): err = sys.exc_info()[1] 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): err = sys.exc_info()[1] 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): err = sys.exc_info()[1] 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): err = sys.exc_info()[1] why = _strerror(err) self.respond('550 %s.' % why) return if rest_pos: # Make sure that the requested offset is valid (within the # size of the file being resumed). # According to RFC-1123 a 554 reply may result in case that # the existing file cannot be repositioned as specified in # the REST. ok = 0 try: if rest_pos > self.fs.getsize(file): raise ValueError fd.seek(rest_pos) ok = 1 except ValueError: why = "Invalid REST parameter" except (EnvironmentError, FilesystemError): err = sys.exc_info()[1] 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 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): err = sys.exc_info()[1] why = _strerror(err) self.respond('550 %s.' %why) return if rest_pos: # Make sure that the requested offset is valid (within the # size of the file being resumed). # According to RFC-1123 a 554 reply may result in case # that the existing file cannot be repositioned as # specified in the REST. ok = 0 try: if rest_pos > self.fs.getsize(file): raise ValueError fd.seek(rest_pos) ok = 1 except ValueError: why = "Invalid REST parameter" except (EnvironmentError, FilesystemError): err = sys.exc_info()[1] 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 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): err = sys.exc_info()[1] # 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 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 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=logging.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=logging.info) self.username = line _auth_failed_timeout = 5 def ftp_PASS(self, line): """Check username's password against the authorizer.""" if self.authenticated: self.respond("503 User already authenticated.") return if not self.username: self.respond("503 Login with USER first.") return 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): def auth_failed(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) msg = str(sys.exc_info()[1]) 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.del_channel() self.ioloop.call_later(self._auth_failed_timeout, auth_failed, self.username, line, msg, _errback=self.handle_error) self.username = "" else: if not isinstance(home, unicode): if PY3: raise ValueError('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 = line self.attempted_logins = 0 self.fs = self.abstracted_fs(home, self) self.on_login(self.username) def ftp_REIN(self, line): """Reinitialize user's current session.""" # From RFC-959: # REIN command terminates a USER, flushing all I/O and account # information, except to allow any transfer in progress to be # completed. All parameters are reset to the default settings # and the control connection is left open. This is identical # to the state in which a user finds himself immediately after # the control connection is opened. self.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): err = sys.exc_info()[1] 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): err = sys.exc_info()[1] 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): err = sys.exc_info()[1] 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_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): err = sys.exc_info()[1] 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): err = sys.exc_info()[1] 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): err = sys.exc_info()[1] 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): err = sys.exc_info()[1] 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: iterator = self.run_as_current_user(self.fs.get_list_dir, path) except (OSError, FilesystemError): err = sys.exc_info()[1] 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', '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: err = sys.exc_info()[1] self.respond('501 %s.' % err) else: facts = [x.lower() for x in arg.split(';')] self._current_facts = [x for x in facts if x in self._available_facts] f = ''.join([x + ';' for x in self._current_facts]) self.respond('200 MLST OPTS ' + f) def ftp_NOOP(self, line): """Do nothing.""" self.respond("200 I successfully done nothin'.") def ftp_SYST(self, line): """Return system type (always returns UNIX type: L8).""" # This command is used to find out the type of operating system # at the server. The reply shall have as its first word one of # the system names listed in RFC-943. # Since that we always return a "/bin/ls -lA"-like output on # LIST we prefer to respond as if we would on Unix in any case. self.respond("215 UNIX Type: L8") def ftp_ALLO(self, line): """Allocate bytes for storage (noop).""" # not necessary (always respond with 202) self.respond("202 No storage allocation necessary.") def ftp_HELP(self, line): """Return help text to the client.""" if line: line = line.upper() if line in self.proto_cmds: self.respond("214 %s" % self.proto_cmds[line]['help']) else: self.respond("501 Unrecognized command.") else: # provide a compact list of recognized commands def formatted_help(): cmds = [] keys = [x for x in self.proto_cmds.keys() if not x.startswith('SITE ')] keys.sort() while keys: elems = tuple((keys[0:8])) cmds.append(' %-6s' * len(elems) % elems + '\r\n') del keys[0:8] return ''.join(cmds) self.push("214-The following commands are recognized:\r\n") self.push(formatted_help()) self.respond("214 Help command successful.") # --- site commands # The user willing to add support for a specific SITE command must # update self.proto_cmds dictionary and define a new ftp_SITE_%CMD% # method in the subclass. def ftp_SITE_CHMOD(self, path, mode): """Change file mode. 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): err = sys.exc_info()[1] 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 # =================================================================== # requires PyOpenSSL - http://pypi.python.org/pypi/pyOpenSSL try: from OpenSSL import SSL except ImportError: pass else: _ssl_proto_cmds = proto_cmds.copy() _ssl_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).'), }) class SSLConnection(_AsyncChatNewStyle): """An AsyncChat subclass supporting TLS/SSL.""" _ssl_accepting = False _ssl_established = False _ssl_closing = False def __init__(self, *args, **kwargs): super(SSLConnection, self).__init__(*args, **kwargs) self._error = False def secure_connection(self, ssl_context): """Secure the connection switching from plain-text to SSL/TLS. """ try: self.socket = SSL.Connection(ssl_context, self.socket) except socket.error: self.close() except ValueError: # may happen in case the client connects/disconnects # very quickly if self.socket.fileno() == -1: return raise else: self.socket.set_accept_state() self._ssl_accepting = True def _do_ssl_handshake(self): self._ssl_accepting = True try: self.socket.do_handshake() except (SSL.WantReadError, SSL.WantWriteError): return except SSL.SysCallError: err = sys.exc_info()[1] retval, desc = err.args if (retval == -1 and desc == 'Unexpected EOF') or retval > 0: return self.handle_close() raise except SSL.Error: return self.handle_failed_ssl_handshake() else: self._ssl_accepting = False self._ssl_established = True self.handle_ssl_established() def handle_ssl_established(self): """Called when SSL handshake has completed.""" pass def handle_ssl_shutdown(self): """Called when SSL shutdown() has completed.""" super(SSLConnection, self).close() def handle_failed_ssl_handshake(self): raise NotImplementedError("must be implemented in subclass") def handle_read_event(self): if self._ssl_accepting: self._do_ssl_handshake() elif self._ssl_closing: self._do_ssl_shutdown() else: super(SSLConnection, self).handle_read_event() def handle_write_event(self): if self._ssl_accepting: self._do_ssl_handshake() elif self._ssl_closing: self._do_ssl_shutdown() else: super(SSLConnection, self).handle_write_event() def handle_error(self): self._error = True try: raise except (KeyboardInterrupt, SystemExit): raise except: self.log_exception(self) # when facing an unhandled exception in here it's better # to rely on base class (FTPHandler or DTPHandler) # close() method as it does not imply SSL shutdown logic try: super(SSLConnection, self).close() except Exception: logger.critical(traceback.format_exc()) def send(self, data): try: return super(SSLConnection, self).send(data) except (SSL.WantReadError, SSL.WantWriteError): return 0 except SSL.ZeroReturnError: super(SSLConnection, self).handle_close() return 0 except SSL.SysCallError: err = sys.exc_info()[1] errnum, errstr = err.args if errnum == errno.EWOULDBLOCK: return 0 elif errnum in _DISCONNECTED or errstr == 'Unexpected EOF': super(SSLConnection, self).handle_close() return 0 else: raise def recv(self, buffer_size): try: return super(SSLConnection, self).recv(buffer_size) except (SSL.WantReadError, SSL.WantWriteError): return b('') except SSL.ZeroReturnError: super(SSLConnection, self).handle_close() return b('') except SSL.SysCallError: err = sys.exc_info()[1] errnum, errstr = err.args if errnum in _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 # 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): err = sys.exc_info()[1] if err.args[0] in (errno.EINTR, errno.EWOULDBLOCK, errno.ENOBUFS): return elif err.args[0] in _DISCONNECTED: return super(SSLConnection, self).close() else: raise # Ok, this a mess, but the underlying OpenSSL API simply # *SUCKS* and I really couldn't do any better. # # Here we just want to shutdown() the SSL layer and then # close() the connection so we're not interested in a # complete SSL shutdown() handshake, so let's pretend # we already received a "RECEIVED" shutdown notification # from the client. # Once the client received our "SENT" shutdown notification # then we close() the connection. # # Since it is not clear what errors to expect during the # entire procedure we catch them all and assume the # following: # - WantReadError and WantWriteError means "retry" # - ZeroReturnError, SysCallError[EOF], Error[] are all # aliases for disconnection try: laststate = self.socket.get_shutdown() self.socket.set_shutdown(laststate | SSL.RECEIVED_SHUTDOWN) done = self.socket.shutdown() if not (laststate & SSL.RECEIVED_SHUTDOWN): self.socket.set_shutdown(SSL.SENT_SHUTDOWN) except (SSL.WantReadError, SSL.WantWriteError): pass except SSL.ZeroReturnError: super(SSLConnection, self).close() except SSL.SysCallError: err = sys.exc_info()[1] errnum, errstr = err.args if errnum in _DISCONNECTED or errstr == 'Unexpected EOF': super(SSLConnection, self).close() else: raise except SSL.Error: # see: # http://code.google.com/p/pyftpdlib/issues/detail?id=171 # https://bugs.launchpad.net/pyopenssl/+bug/785985 err = sys.exc_info()[1] if err.args and not err.args[0]: pass else: raise except socket.error: err = sys.exc_info()[1] if err.args[0] in _DISCONNECTED: super(SSLConnection, self).close() else: raise else: if done: self._ssl_established = False self._ssl_closing = False self.handle_ssl_shutdown() def close(self): if self._ssl_established and not self._error: self._do_ssl_shutdown() else: self._ssl_accepting = False self._ssl_established = False self._ssl_closing = False super(SSLConnection, self).close() class TLS_DTPHandler(SSLConnection, DTPHandler): """A 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 _use_sendfile(self, producer): return False def handle_failed_ssl_handshake(self): # TLS/SSL handshake failure, probably client's fault which # used a SSL version different from server's. # RFC-4217, chapter 10.2 expects us to return 522 over the # command channel. self.cmd_channel.respond("522 SSL handshake failed.") self.cmd_channel.log_cmd("PROT", "P", 522, "SSL handshake failed.") self.close() class TLS_FTPHandler(SSLConnection, FTPHandler): """A 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) protocol: specifies which version of the SSL protocol to use when establishing SSL/TLS sessions; clients can then only connect using the configured protocol (defaults to SSLv23, allowing SSLv3 and TLSv1 protocols). Possible values: * SSL.SSLv2_METHOD - allow only SSLv2 * SSL.SSLv3_METHOD - allow only SSLv3 * SSL.SSLv23_METHOD - allow both SSLv3 and TLSv1 * SSL.TLSv1_METHOD - allow only TLSv1 - (instance) context: a SSL Context object previously configured; if specified all other parameters will be ignored. (default None). """ # configurable attributes tls_control_required = False tls_data_required = False certfile = None keyfile = None ssl_protocol = SSL.SSLv23_METHOD ssl_context = None # overridden attributes proto_cmds = _ssl_proto_cmds dtp_handler = TLS_DTPHandler 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() @classmethod def get_ssl_context(cls): if cls.ssl_context is None: if cls.certfile is None: raise ValueError("at least certfile must be specified") cls.ssl_context = SSL.Context(cls.ssl_protocol) if cls.ssl_protocol != SSL.SSLv2_METHOD: cls.ssl_context.set_options(SSL.OP_NO_SSLv2) else: warnings.warn("SSLv2 protocol is insecure", RuntimeWarning) cls.ssl_context.use_certificate_file(cls.certfile) if not cls.keyfile: cls.keyfile = cls.certfile cls.ssl_context.use_privatekey_file(cls.keyfile) 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-1.2.0/pyftpdlib/_compat.py0000664000175000017500000000400112110647263021441 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: _compat.py 1117 2012-11-02 19:45:33Z g.rodola $ """ Compatibility module similar to six which helps maintaining a single code base working with python from 2.4 to 3.x. """ import sys import os PY3 = sys.version_info[0] == 3 if PY3: import builtins def u(s): return s def b(s): return s.encode("latin-1") print_ = getattr(builtins, "print") getcwdu = os.getcwd unicode = str xrange = range else: def u(s): return unicode(s) def b(s): return s def print_(s): sys.stdout.write(s + '\n') sys.stdout.flush() getcwdu = os.getcwdu unicode = unicode xrange = xrange # introduced in 2.6 if hasattr(sys, 'maxsize'): MAXSIZE = sys.maxsize else: class X(object): def __len__(self): return 1 << 31 try: len(X()) except OverflowError: MAXSIZE = int((1 << 31) - 1) # 32-bit else: MAXSIZE = int((1 << 63) - 1) # 64-bit del X # 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 # introduced in 2.6 _default = object() try: next = next except NameError: def next(iterable, default=_default): if default == _default: return iterable.next() else: try: return iterable.next() except StopIteration: return default # dirty hack to support property.setter on python < 2.6 property = property if not hasattr(property, "setter"): class property(property): def setter(self, value): cls_ns = sys._getframe(1).f_locals for k, v in cls_ns.iteritems(): if v == self: name = k break cls_ns[name] = property(self.fget, value, self.fdel, self.__doc__) return cls_ns[name] pyftpdlib-1.2.0/pyftpdlib/ioloop.py0000664000175000017500000007454112134252353021336 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: ioloop.py 1217 2013-04-18 18:21:44Z g.rodola $ # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ====================================================================== """ A 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 asyncore import asynchat import errno import select import os import sys import traceback import time import heapq import socket import logging try: import threading except ImportError: import dummy_threading as threading from pyftpdlib._compat import MAXSIZE, callable, b from pyftpdlib.log import logger, _config_logging timer = getattr(time, 'monotonic', time.time) _read = asyncore.read _write = asyncore.write # =================================================================== # --- 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): 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 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: try: self._target(*self._args, **self._kwargs) except Exception: exc = sys.exc_info()[1] 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.""" assert not self.cancelled, "already 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() @classmethod def instance(cls): """Return a global IOLoop instance.""" if cls._instance is None: cls._lock.acquire() try: if cls._instance is None: cls._instance = cls() finally: cls._lock.release() 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 logging.getLogger().handlers: # 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 tasks = self.sched._tasks 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.""" 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: err = sys.exc_info()[1] if err.args[0] != 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: pass 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) def poll(self, timeout): try: r, w, e = select.select(self._r, self._w, [], timeout) except select.error: err = sys.exc_info()[1] if err.args[0] == 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/epoll implementations which almost share 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): self._poller.register(fd, events) self.socket_map[fd] = instance def unregister(self, fd): try: del self.socket_map[fd] except KeyError: pass else: self._poller.unregister(fd) def modify(self, fd, events): self._poller.modify(fd, events) def poll(self, timeout): try: events = self._poller.poll(timeout or -1) # -1 waits indefinitely except (IOError, select.error): # for epoll() and poll() respectively err = sys.exc_info()[1] if err.args[0] == 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 # select.poll() on py < 2.6 has no 'modify' method if not hasattr(select.poll(), 'modify'): 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) # =================================================================== # --- 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'): 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._poller.fileno() def close(self): _IOLoop.close(self) self._kqueue.close() def register(self, fd, instance, events): self.socket_map[fd] = instance self._control(fd, events, select.KQ_EV_ADD) 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 OSError: err = sys.exc_info()[1] if err.errno != errno.EBADF: 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: err = sys.exc_info()[1] if err.args[0] == 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 only IOLoop = Epoll elif hasattr(select, 'kqueue'): # kqueue() - BSD / OSX IOLoop = Kqueue 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 _DISCONNECTED = frozenset((errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN, errno.ECONNABORTED, errno.EPIPE, errno.EBADF)) class Acceptor(asyncore.dispatcher): """Same as base asyncore.dispatcher and supposed to be used to accept new connections. """ def __init__(self, ioloop=None): self.ioloop = ioloop or IOLoop.instance() self._fileno = None # py < 2.6 asyncore.dispatcher.__init__(self) 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: err = sys.exc_info()[1] 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 add_channel(self, map=None): self.ioloop.register(self._fileno, self, self.ioloop.READ) def del_channel(self, map=None): self.ioloop.unregister(self._fileno) def listen(self, num): asyncore.dispatcher.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 issue 91) return except socket.error: err = sys.exc_info()[1] # ECONNABORTED might be thrown on *BSD (see issue 105) if err.args[0] != errno.ECONNABORTED: raise 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 class Connector(Acceptor): """Same as base asyncore.dispatcher and supposed to be used for clients. """ 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: err = sys.exc_info()[1] 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 add_channel(self, map=None): self.ioloop.register(self._fileno, self, self.ioloop.WRITE) 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, ioloop=None): self.ioloop = ioloop or IOLoop.instance() self._current_io_events = self.ioloop.READ self._closed = False self._closing = False asynchat.async_chat.__init__(self, sock) def add_channel(self, map=None, events=None): self.ioloop.register(self._fileno, self, events or self.ioloop.READ) def del_channel(self, map=None): self.ioloop.unregister(self._fileno) # send() and recv() overridden as a fix around various bugs: # - http://bugs.python.org/issue1736101 # - http://code.google.com/p/pyftpdlib/issues/detail?id=104 # - http://code.google.com/p/pyftpdlib/issues/detail?id=109 def send(self, data): try: return self.socket.send(data) except socket.error: why = sys.exc_info()[1] if why.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN): return 0 elif why.args[0] in _DISCONNECTED: self.handle_close() return 0 else: raise def recv(self, buffer_size): try: data = self.socket.recv(buffer_size) 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 except socket.error: why = sys.exc_info()[1] if why.args[0] in _DISCONNECTED: self.handle_close() return b('') else: raise 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: wanted = self.ioloop.READ | self.ioloop.WRITE if self._current_io_events != wanted: self.ioloop.modify(self._fileno, wanted) self._current_io_events = wanted 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) pyftpdlib-1.2.0/pyftpdlib/filesystems.py0000664000175000017500000006214412110650104022366 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: filesystems.py 1171 2013-02-19 10:13:09Z g.rodola $ # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ====================================================================== import os import time import tempfile import stat 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 from pyftpdlib._compat import PY3, u, unicode, property __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 = not 'b' in mode # max number of tries to find out a unique file name tempfile.TMP_MAX = 50 fd, name = tempfile.mkstemp(suffix, prefix, dir, text=text) file = os.fdopen(fd, mode) return FileWrapper(file, name) # --- Wrapper methods around os.* calls def chdir(self, path): """Change the current directory.""" # 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 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 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) if not hasattr(os, 'lstat'): 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) 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 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 if pwd is None: get_user_by_uid = lambda x, y: "owner" if grp is None: get_group_by_gid = lambda x, y: "group" # --- Listing utilities def get_list_dir(self, path): """"Return an iterator object that yields a directory listing in a form suitable for LIST command. """ assert isinstance(path, unicode), path if self.isdir(path): listing = self.listdir(path) 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 pass return self.format_list(path, listing) # if path is a file or a symlink we return information about it else: basedir, filename = os.path.split(path) self.lstat(path) # raise exc in case of problems return self.format_list(basedir, [filename]) def format_list(self, basedir, listing, ignore_err=True): """Return an iterator object that yields the entries of given directory emulating the "/bin/ls -lA" UNIX command output. - (str) basedir: the absolute dirname. - (list) listing: the names of the entries in basedir - (bool) ignore_err: when False raise exception if os.lstat() call fails. On platforms which do not support the pwd and grp modules (such as Windows), ownership is printed as "owner" and "group" as a default, and number of hard links is always "1". On UNIX systems, the actual owner, group, and number of links are printed. This is how output appears to client: -rw-rw-rw- 1 owner group 7045120 Sep 02 3:47 music.mp3 drwxrwxrwx 1 owner group 0 Aug 31 18:50 e-books -rw-rw-rw- 1 owner group 380 Sep 02 3:40 module.py """ assert isinstance(basedir, unicode), basedir if listing: assert isinstance(listing[0], unicode) 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') 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: # http://code.google.com/p/pyftpdlib/issues/detail?id=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=801cd2; music.mp3 type=dir;size=0;perm=el;modify=20071127230206;unique=801e33; ebooks type=file;size=211;perm=r;modify=20071103093626;unique=801e32; module.py """ assert isinstance(basedir, unicode), basedir if listing: assert isinstance(listing[0], unicode) 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') 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-1.2.0/pyftpdlib/authorizers.py0000664000175000017500000010733512110650104022400 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: authorizers.py 1171 2013-02-19 10:13:09Z g.rodola $ # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ====================================================================== """An "authorizer" is a class handling authentications and permissions of the FTP server. It is used by pyftpdlib.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 os import warnings import errno import sys from pyftpdlib._compat import PY3, unicode, 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 = "adfmwM" def __init__(self): self.user_table = {} def add_user(self, username, password, homedir, perm='elr', msg_login="Login successful.", msg_quit="Goodbye."): """Add a user to the virtual users table. AuthorizerError exceptions raised on error conditions such as invalid permissions, missing home directory or duplicate usernames. Optional perm argument is a string referencing the user's permissions explained below: Read permissions: - "e" = change directory (CWD command) - "l" = list files (LIST, NLST, STAT, MLSD, MLST, SIZE, MDTM commands) - "r" = retrieve file from the server (RETR command) Write permissions: - "a" = append data to an existing file (APPE command) - "d" = delete file or directory (DELE, RMD commands) - "f" = rename file or directory (RNFR, RNTO commands) - "m" = create directory (MKD command) - "w" = store a file to the server (STOR, STOU commands) - "M" = change file mode (SITE CHMOD command) Optional msg_login and msg_quit arguments can be specified to provide customized response strings when user log-in and quit. """ if self.has_user(username): raise ValueError('user %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: "elradfmwM". """ if path is None: return perm in self.user_table[username]['perm'] path = os.path.normcase(path) for dir in self.user_table[username]['operms'].keys(): operm, recursive = self.user_table[username]['operms'][dir] if self._issubpath(path, dir): if recursive: return perm in operm if (path == dir) or (os.path.dirname(path) == dir \ and not os.path.isdir(path)): return perm in operm return perm in self.user_table[username]['perm'] def get_perms(self, username): """Return current user permissions.""" return self.user_table[username]['perm'] def get_msg_login(self, username): """Return the user's login message.""" return self.user_table[username]['msg_login'] def get_msg_quit(self, username): """Return the user's quitting message.""" return self.user_table[username]['msg_quit'] def _check_permissions(self, username, perm): warned = 0 for p in perm: if p not in self.read_perms + self.write_perms: raise ValueError('no such permission %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 # =================================================================== # Note: requires python >= 2.5 try: import pwd, spwd, crypt except ImportError: pass else: __all__.extend(['BaseUnixAuthorizer', 'UnixAuthorizer']) # the uid/gid the server runs under PROCESS_UID = os.getuid() PROCESS_GID = os.getgid() class BaseUnixAuthorizer(object): """An authorizer compatible with Unix user account and password database. This class should not be used directly unless for subclassing. Use higher-level UnixAuthorizer class instead. """ def __init__(self, anonymous_user=None): if os.geteuid() != 0 or not spwd.getspall(): raise AuthorizerError("super user privileges are required") self.anonymous_user = anonymous_user if self.anonymous_user is not None: 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 "elradfmw" def has_perm(self, username, perm, path=None): return perm in self.get_perms(username) class UnixAuthorizer(_Base, BaseUnixAuthorizer): """A wrapper on top of BaseUnixAuthorizer providing options to specify what users should be allowed to login, per-user options, etc. Example usages: >>> from pyftpdlib.contrib.authorizers import UnixAuthorizer >>> # accept all except root >>> auth = UnixAuthorizer(rejected_users=["root"]) >>> >>> # accept some users only >>> auth = UnixAuthorizer(allowed_users=["matt", "jay"]) >>> >>> # accept everybody and don't care if they have not a valid shell >>> auth = UnixAuthorizer(require_valid_shell=False) >>> >>> # set specific options for a user >>> auth.override_user("matt", password="foo", perm="elr") """ # --- public API def __init__(self, global_perm="elradfmw", allowed_users=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 "elradfmw" which means full read and write access for everybody (except anonymous). - (list) allowed_users: a list of users which are accepted for authenticating against the FTP server; defaults to [] (no restrictions). - (list) rejected_users: a list of users which are not accepted for authenticating against the FTP server; defaults to [] (no restrictions). - (bool) require_valid_shell: Deny access for those users which do not have a valid shell binary listed in /etc/shells. If /etc/shells cannot be found this is a no-op. Anonymous user is not subject to this option, and is free to not have a valid shell defined. Defaults to True (a valid shell is required for login). - (string) anonymous_user: specify it if you intend to provide anonymous access. The value expected is a string representing the system user to use for managing anonymous sessions; defaults to None (anonymous access disabled). - (string) msg_login: the string sent when client logs in. - (string) msg_quit: the string sent when client quits. """ BaseUnixAuthorizer.__init__(self, anonymous_user) 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. """ file = None try: try: file = open('/etc/shells', 'r') except IOError: err = sys.exc_info()[1] if err.errno == errno.ENOENT: return True raise else: try: shell = pwd.getpwnam(username).pw_shell except KeyError: # invalid user return False for line in file: if line.startswith('#'): continue line = line.strip() if line == shell: return True return False finally: if file is not None: file.close() # =================================================================== # --- Windows # =================================================================== try: import _winreg as winreg except ImportError: try: import winreg # PY3 except ImportError: pass # Note: requires pywin32 extension try: import win32security, win32net, pywintypes, win32con, win32api except ImportError: pass else: __all__.extend(['BaseWindowsAuthorizer', 'WindowsAuthorizer']) class BaseWindowsAuthorizer(object): """An authorizer compatible with Windows user account and password database. This class should not be used directly unless for subclassing. Use higher-level WinowsAuthorizer class instead. """ def __init__(self, anonymous_user=None, anonymous_password=None): # actually try to impersonate the user self.anonymous_user = anonymous_user self.anonymous_password = anonymous_password if self.anonymous_user is not None: self.impersonate_user(self.anonymous_user, self.anonymous_password) self.terminate_impersonation(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: err = sys.exc_info()[1] raise AuthorizerError(err) path = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" + \ "\\" + sid try: key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path) except WindowsError: raise AuthorizerError("No profile directory defined for user %s" % username) value = winreg.QueryValueEx(key, "ProfileImagePath")[0] 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 "elradfmw" def has_perm(self, username, perm, path=None): return perm in self.get_perms(username) class WindowsAuthorizer(_Base, BaseWindowsAuthorizer): """A wrapper on top of BaseWindowsAuthorizer providing options to specify what users should be allowed to login, per-user options, etc. Example usages: >>> from pyftpdlib.contrib.authorizers import WindowsAuthorizer >>> # accept all except Administrator >>> auth = UnixAuthorizer(rejected_users=["Administrator"]) >>> >>> # accept some users only >>> auth = UnixAuthorizer(allowed_users=["matt", "jay"]) >>> >>> # set specific options for a user >>> auth.override_user("matt", password="foo", perm="elr") """ # --- public API def __init__(self, global_perm="elradfmw", allowed_users=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 "elradfmw" which means full read and write access for everybody (except anonymous). - (list) allowed_users: a list of users which are accepted for authenticating against the FTP server; defaults to [] (no restrictions). - (list) rejected_users: a list of users which are not accepted for authenticating against the FTP server; defaults to [] (no restrictions). - (string) anonymous_user: specify it if you intend to provide anonymous access. The value expected is a string representing the system user to use for managing anonymous sessions. As for IIS, it is recommended to use Guest account. The common practice is to first enable the Guest user, which is disabled by default and then assign an empty password. Defaults to None (anonymous access disabled). - (string) anonymous_password: the password of the user who has been chosen to manage the anonymous sessions. Defaults to None (empty password). - (string) msg_login: the string sent when client logs in. - (string) msg_quit: the string sent when client quits. """ 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-1.2.0/pyftpdlib/log.py0000664000175000017500000001364712110650104020604 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: log.py 1171 2013-02-19 10:13:09Z g.rodola $ # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # """ 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 pyftpdlib._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 = { logging.DEBUG: unicode(curses.tparm(fg_color, 4), "ascii"), # blue logging.INFO: unicode(curses.tparm(fg_color, 2), "ascii"), # green logging.WARNING: unicode(curses.tparm(fg_color, 3), "ascii"), # yellow logging.ERROR: unicode(curses.tparm(fg_color, 1), "ascii") # red } self._normal = unicode(curses.tigetstr("sgr0"), "ascii") def format(self, record): try: record.message = record.getMessage() except Exception: err = sys.exc_info()[1] 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 _config_logging(): channel = logging.StreamHandler() channel.setFormatter(LogFormatter()) logger = logging.getLogger() logger.setLevel(LEVEL) logger.addHandler(channel) pyftpdlib-1.2.0/pyftpdlib/__init__.py0000664000175000017500000001074312135246566021600 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: __init__.py 1220 2013-04-22 14:47:16Z g.rodola $ # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ====================================================================== """ pyftpdlib: RFC-959 asynchronous FTP server. pyftpdlib implements a fully functioning asynchronous FTP server as defined in RFC-959. A hierarchy of classes outlined below implement the backend functionality for the FTPd: [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="elradfmw") >>> 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). """ import logging __ver__ = '1.2.0' __date__ = '2013-04-22' __author__ = "Giampaolo Rodola' " __web__ = 'http://code.google.com/p/pyftpdlib/' def _depwarn(msg): """ Force DeprecationWarning to be temporarily shown (it's been disabled by default starting from python 2.7 / 3.2), then re-set the default behavior. """ import warnings orig_filters = warnings.filters[:] try: #warnings.simplefilter('default') warnings.resetwarnings() warnings.warn(msg, category=DeprecationWarning, stacklevel=2) finally: warnings.filters = orig_filters pyftpdlib-1.2.0/pyftpdlib/ftpserver.py0000664000175000017500000000613412110650104022034 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: ftpserver.py 1171 2013-02-19 10:13:09Z g.rodola $ # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ====================================================================== """ Note: this module is here only for backward compatibility. The new import system which is supposed to be used is: from pyftpdlib.handlers import FTPHandler, TLS_FTPHandler, ... from pyftpdlib.authorizers import DummyAuthorizer, UnixAuthorizer, ... from pyftpdlib.servers import FTPServer, ... """ from pyftpdlib.log import logger from pyftpdlib.handlers import * from pyftpdlib.authorizers import * from pyftpdlib.servers import * from pyftpdlib import _depwarn, __ver__ __all__ = ['proto_cmds', 'Error', 'log', 'logline', 'logerror', 'DummyAuthorizer', 'AuthorizerError', 'FTPHandler', 'FTPServer', 'PassiveDTP', 'ActiveDTP', 'DTPHandler', 'ThrottledDTPHandler', 'FileProducer', 'BufferedIteratorProducer', 'AbstractedFS'] _depwarn("pyftpdlib.ftpserver module is deprecated") class CallLater(object): def __init__(self, *args, **kwargs): _depwarn("CallLater is deprecated; use " "pyftpdlib.ioloop.IOLoop.instance().call_later() instead") from pyftpdlib.ioloop import IOLoop IOLoop.instance().call_later(*args, **kwargs) class CallEvery(object): def __init__(self, *args, **kwargs): _depwarn("CallEvery is deprecated; use " "pyftpdlib.ioloop.IOLoop.instance().call_every() instead") from pyftpdlib.ioloop import IOLoop IOLoop.instance().call_every(*args, **kwargs) def log(msg): _depwarn("pyftpdlib.ftpserver.log() is deprecated") logger.info(msg) def logline(msg): _depwarn("pyftpdlib.ftpserver.logline() is deprecated") logger.debug(msg) def logerror(msg): _depwarn("pyftpdlib.ftpserver.logline() is deprecated") logger.error(msg) if __name__ == '__main__': from pyftpdlib import main main() pyftpdlib-1.2.0/pyftpdlib/contrib/0000775000175000017500000000000012135246616021116 5ustar giampaologiampaolo00000000000000pyftpdlib-1.2.0/pyftpdlib/contrib/handlers.py0000664000175000017500000000317312110650104023254 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: handlers.py 1171 2013-02-19 10:13:09Z g.rodola $ # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ====================================================================== from pyftpdlib import _depwarn _depwarn("pyftpdlib.contrib.handlers module is deprecated; " "use pyftpdlib.handlers instead") try: from pyftpdlib.handlers import SSLConnection, TLS_FTPHandler, TLS_DTPHandler except ImportError: pass pyftpdlib-1.2.0/pyftpdlib/contrib/filesystems.py0000664000175000017500000000325112110650104024020 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: filesystems.py 1171 2013-02-19 10:13:09Z g.rodola $ # pyftpdlib is released under the MIT license, reproduced below: # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ====================================================================== from pyftpdlib import _depwarn _depwarn("pyftpdlib.contrib.filesystem module is deprecated; " "use pyftpdlib.filesystems instead") try: from pyftpdlib.filesystems import UnixFilesystem except ImportError: pass pyftpdlib-1.2.0/pyftpdlib/contrib/authorizers.py0000664000175000017500000000346012110650104024032 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: authorizers.py 1171 2013-02-19 10:13:09Z g.rodola $ # pyftpdlib is released under the MIT license, reproduced below: # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ====================================================================== from pyftpdlib import _depwarn _depwarn("pyftpdlib.contrib.authorizers module is deprecated; " "use pyftpdlib.authorizers instead") try: from pyftpdlib.authorizers import BaseUnixAuthorizer, UnixAuthorizer except ImportError: pass try: from pyftpdlib.authorizers import BaseWindowsAuthorizer, WindowsAuthorizer except ImportError: pass pyftpdlib-1.2.0/pyftpdlib/contrib/__init__.py0000664000175000017500000000012612110647263023222 0ustar giampaologiampaolo00000000000000from pyftpdlib import _depwarn _depwarn("pyftpdlib.contrib namespace is deprecated") pyftpdlib-1.2.0/setup.cfg0000664000175000017500000000007312135246616017302 0ustar giampaologiampaolo00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 pyftpdlib-1.2.0/setup.py0000664000175000017500000001264312131031514017162 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: setup.py 1208 2013-04-09 15:27:02Z g.rodola $ # # pyftpdlib is released under the MIT license, reproduced below: # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ====================================================================== """pyftpdlib installer. To install pyftpdlib just open a command shell and run: > python setup.py install """ import os import sys try: from setuptools import setup except ImportError: from distutils.core import setup def get_version(): INIT = os.path.abspath(os.path.join(os.path.dirname(__file__), 'pyftpdlib', '__init__.py')) f = open(INIT, 'r') try: 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") finally: f.close() name = 'pyftpdlib' version = get_version() download_url = "http://pyftpdlib.googlecode.com/files/" + name + "-" + \ version + ".tar.gz" if sys.version_info < (2, 4): sys.exit('python version not supported (min 2.4)') setup( name=name, version=version, description='High-level asynchronous FTP server library', long_description="Python FTP server library provides an high-level portable " "interface to easily write asynchronous FTP servers with " "Python.", license='License :: OSI Approved :: MIT License', platforms='Platform Independent', author="Giampaolo Rodola'", author_email='g.rodola@gmail.com', url='http://code.google.com/p/pyftpdlib/', download_url=download_url, packages=['pyftpdlib', 'pyftpdlib/contrib'], keywords=['ftp', 'ftps', 'server', 'ftpd', 'daemon', 'python', 'ssl', 'sendfile', 'asynchronous', 'nonblocking', 'eventdriven', 'rfc959', 'rfc1123', 'rfc2228', 'rfc2428', 'rfc2640', 'rfc3659'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Topic :: Internet :: File Transfer Protocol (FTP)', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: System :: Filesystems', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.4', 'Programming Language :: Python :: 2.5', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.0', 'Programming Language :: Python :: 3.1', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', ], ) # suggest to install pysendfile if os.name == 'posix' and sys.version_info >= (2, 5): 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 # http://code.google.com/p/pysendfile/ import sendfile if hasattr(sendfile, 'has_sf_hdtr'): # old 1.2.4 version raise ImportError except ImportError: 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 msg = "\nYou might want to install pysendfile module to speedup " \ "transfers:\nhttp://code.google.com/p/pysendfile/\n" if term_supports_colors(): msg = '\x1b[1m%s\x1b[0m' % msg sys.stderr.write(msg) pyftpdlib-1.2.0/PKG-INFO0000664000175000017500000000323212135246616016556 0ustar giampaologiampaolo00000000000000Metadata-Version: 1.1 Name: pyftpdlib Version: 1.2.0 Summary: High-level asynchronous FTP server library Home-page: http://code.google.com/p/pyftpdlib/ Author: Giampaolo Rodola' Author-email: g.rodola@gmail.com License: License :: OSI Approved :: MIT License Download-URL: http://pyftpdlib.googlecode.com/files/pyftpdlib-1.2.0.tar.gz Description: Python FTP server library provides an high-level portable interface to easily write asynchronous FTP servers with Python. Keywords: ftp,ftps,server,ftpd,daemon,python,ssl,sendfile,asynchronous,nonblocking,eventdriven,rfc959,rfc1123,rfc2228,rfc2428,rfc2640,rfc3659 Platform: Platform Independent Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Internet :: File Transfer Protocol (FTP) Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: System :: Filesystems Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.4 Classifier: Programming Language :: Python :: 2.5 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.0 Classifier: Programming Language :: Python :: 3.1 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 pyftpdlib-1.2.0/HISTORY0000664000175000017500000007516412135246504016556 0ustar giampaologiampaolo00000000000000Bug tracker at http://code.google.com/p/pyftpdlib/issues/list History ======= 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. * Issues 212: provided two new classes: - pyftpdlib.servers.ThreadedFTPServer - 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 | NOW ---------------------------------------------------------------------------- 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 | NOW ------------------------------------------------------------------------------------------- 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. * 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: - __init__(conn, server) + __init__(conn, server, ioloop=None) * FTPServer.server_forever() signature has changed: - serve_forever(timeout=1.0, use_poll=False, count=None) + serve_forever(timeout=1.0, blocking=True, handle_exit=True) * FTPServer.close_all() signature has changed: - close_all(ignore_all=False) + 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-1.2.0/demo/0000775000175000017500000000000012135246616016405 5ustar giampaologiampaolo00000000000000pyftpdlib-1.2.0/demo/keycert.pem0000664000175000017500000000356011723477263020570 0ustar giampaologiampaolo00000000000000-----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-1.2.0/demo/md5_ftpd.py0000775000175000017500000000510212117430365020456 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: md5_ftpd.py 1174 2013-02-19 11:25:49Z g.rodola $ # pyftpdlib is released under the MIT license, reproduced below: # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ====================================================================== """A basic ftpd storing passwords as hash digests (platform independent). """ import os try: from hashlib import md5 except ImportError: # backward compatibility with Python < 2.5 from md5 import new as md5 from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer from pyftpdlib.authorizers import DummyAuthorizer, AuthenticationFailed from pyftpdlib._compat import b class DummyMD5Authorizer(DummyAuthorizer): def validate_authentication(self, username, password, handler): hash = md5(b(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='elradfmw') authorizer.add_anonymous(os.getcwd()) handler = FTPHandler handler.authorizer = authorizer server = FTPServer(('', 2121), handler) server.serve_forever() if __name__ == "__main__": main() pyftpdlib-1.2.0/demo/tls_ftpd.py0000775000175000017500000000450212110660503020566 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: tls_ftpd.py 1174 2013-02-19 11:25:49Z g.rodola $ # pyftpdlib is released under the MIT license, reproduced below: # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ====================================================================== """An RFC-4217 asynchronous FTPS server supporting both SSL and TLS. Requires PyOpenSSL module (http://pypi.python.org/pypi/pyOpenSSL). """ import os from pyftpdlib.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='elradfmw') 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-1.2.0/demo/throttled_ftpd.py0000775000175000017500000000450112110660503021774 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: throttled_ftpd.py 1174 2013-02-19 11:25:49Z g.rodola $ # pyftpdlib is released under the MIT license, reproduced below: # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ====================================================================== """An FTP server which uses the ThrottledDTPHandler class for limiting the speed of downloads and uploads. """ import os from pyftpdlib.authorizers import DummyAuthorizer from pyftpdlib.handlers import FTPHandler, ThrottledDTPHandler from pyftpdlib.servers import FTPServer def main(): authorizer = DummyAuthorizer() authorizer.add_user('user', '12345', os.getcwd(), perm='elradfmw') 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-1.2.0/demo/multi_proc_ftp.py0000775000175000017500000000406312117430365022007 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: multi_proc_ftp.py 1174 2013-02-19 11:25:49Z g.rodola $ # pyftpdlib is released under the MIT license, reproduced below: # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ====================================================================== """ A FTP server 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.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() pyftpdlib-1.2.0/demo/winnt_ftpd.py0000775000175000017500000000441612110660503021127 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: winnt_ftpd.py 1174 2013-02-19 11:25:49Z g.rodola $ # pyftpdlib is released under the MIT license, reproduced below: # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ====================================================================== """A ftpd using local Windows NT account database to authenticate users (users must already exist). It also provides a mechanism to (temporarily) impersonate the system users every time they are going to perform filesystem operations. """ from pyftpdlib.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 ftpd = FTPServer(('', 21), handler) ftpd.serve_forever() if __name__ == "__main__": main() pyftpdlib-1.2.0/demo/unix_daemon.py0000775000175000017500000001560412127326365021277 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: unix_daemon.py 1174 2013-02-19 11:25:49Z g.rodola $ # pyftpdlib is released under the MIT license, reproduced below: # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ====================================================================== """A basic unix daemon using the python-daemon library: http://pypi.python.org/pypi/python-daemon Example usages: $ python unix_daemon.py start $ python unix_daemon.py stop $ python unix_daemon.py status $ python unix_daemon.py # foreground (no daemon) $ python unix_daemon.py --logfile /var/log/ftpd.log start $ python unix_daemon.py --pidfile /var/run/ftpd.pid start This is just a proof of concept which demonstrates how to daemonize the FTP server. You might want to use this as an example and provide the necessary customizations. Parts you might want to customize are: - UMASK, WORKDIR, HOST, PORT constants - get_server() function (to define users and customize FTP handler) Authors: - Ben Timby - btimby gmail.com - Giampaolo Rodola' - g.rodola gmail.com """ from __future__ import with_statement import os import errno import sys import time import optparse import signal import atexit from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer from pyftpdlib.authorizers import UnixAuthorizer from pyftpdlib.filesystems import UnixFilesystem # overridable options HOST = "" PORT = 21 PID_FILE = "/var/run/pyftpdlib.pid" LOG_FILE = "/var/log/pyftpdlib.log" WORKDIR = os.getcwd() UMASK = 0 def print_(s): sys.stdout.write(s + '\n') sys.stdout.flush() def pid_exists(pid): """Return True if a process with the given PID is currently running.""" try: os.kill(pid, 0) except OSError: err = sys.exc_info()[1] 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: err = sys.exc_info()[1] 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: e = sys.exc_info()[1] if e.errno == errno.ESRCH: print_("\nstopped (pid %s)" % pid) return else: raise i += 1 if i == 25: sig = signal.SIGKILL elif i == 50: sys.exit("\ncould not kill daemon (pid %s)" % pid) time.sleep(0.1) def status(): """Print daemon status and exit.""" pid = get_pid() if not pid or not pid_exists(pid): print_("daemon not running") else: print_("daemon running with pid %s" % pid) sys.exit(0) def get_server(): """Return a pre-configured FTP server instance.""" 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()) f = open(PID_FILE,'w') f.write("%s\n" % pid) f.close() 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-1.2.0/demo/unix_ftpd.py0000775000175000017500000000413612110660503020752 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: unix_ftpd.py 1174 2013-02-19 11:25:49Z g.rodola $ # pyftpdlib is released under the MIT license, reproduced below: # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ====================================================================== """A FTPd using local UNIX account database to authenticate users. It temporarily impersonate the system users every time they are going to perform a filesystem operations. """ from pyftpdlib.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() pyftpdlib-1.2.0/demo/multi_thread_ftp.py0000775000175000017500000000400712117430365022311 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: multi_thread_ftp.py 1174 2013-02-19 11:25:49Z g.rodola $ # pyftpdlib is released under the MIT license, reproduced below: # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ====================================================================== """ A FTP server which handles every connection in a separate thread. Useful if your handler class contains blocking calls or your filesystem is too slow. """ 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() pyftpdlib-1.2.0/demo/anti_flood_ftpd.py0000775000175000017500000000732312117430365022116 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: anti_flood_ftpd.py 1174 2013-02-19 11:25:49Z g.rodola $ # pyftpdlib is released under the MIT license, reproduced below: # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ====================================================================== """ A FTP server banning clients in case of commands flood. If client sends more than 300 requests per-second it will be disconnected and won't be able to re-connect for 1 hour. """ from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer from pyftpdlib.authorizers import DummyAuthorizer 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='elradfmw') authorizer.add_anonymous('.') handler = AntiFloodHandler handler.authorizer = authorizer server = FTPServer(('', 2121), handler) server.serve_forever(timeout=1) if __name__ == '__main__': main() pyftpdlib-1.2.0/demo/basic_ftpd.py0000775000175000017500000000542112122616050021047 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: basic_ftpd.py 1174 2013-02-19 11:25:49Z g.rodola $ # pyftpdlib is released under the MIT license, reproduced below: # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ====================================================================== """A basic FTP server which uses a DummyAuthorizer for managing 'virtual users', setting a limit for incoming connections. """ import os from pyftpdlib.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='elradfmwM') 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-1.2.0/LICENSE0000664000175000017500000000221511724204624016462 0ustar giampaologiampaolo00000000000000====================================================================== Copyright (C) 2007-2012 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-1.2.0/test/0000775000175000017500000000000012135246616016440 5ustar giampaologiampaolo00000000000000pyftpdlib-1.2.0/test/keycert.pem0000664000175000017500000000356011723477263020623 0ustar giampaologiampaolo00000000000000-----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-1.2.0/test/bench.py0000664000175000017500000004224312127324357020076 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: bench.py 1202 2013-04-04 16:23:09Z g.rodola $ # # pyftpdlib is released under the MIT license, reproduced below: # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ====================================================================== """ FTP server benchmark script. In order to run this you must have a listening FTP server with a user with writing permissions configured. Example usages: python bench.py -u USER -p PASSWORD python bench.py -u USER -p PASSWORD -H ftp.domain.com -P 21 # host / port python bench.py -u USER -p PASSWORD -b transfer python bench.py -u USER -p PASSWORD -b concurrence python bench.py -u USER -p PASSWORD -b all python bench.py -u USER -p PASSWORD -b concurrence -n 500 # 500 clients python bench.py -u USER -p PASSWORD -b concurrence -s 20M # file size python bench.py -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 with_statement, division import ftplib import sys import os import atexit import time import optparse import contextlib import asyncore import asynchat 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 def print_(s): sys.stdout.write(s + '\n') sys.stdout.flush() # 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) if not sys.stdout.isatty() or os.name != 'posix': def hilite(s, *args, **kwargs): return s server_memory = [] 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.get_ext_memory_info() 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.get_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 = ftplib.FTP() ftp.connect(HOST, PORT) ftp.login(USER, PASSWORD) return ftp def retr(ftp): """Same as ftplib's retrbinary() but discard the received data.""" ftp.voidcmd('TYPE I') conn = ftp.transfercmd("RETR " + TESTFN) recv_bytes = 0 while 1: data = conn.recv(BUFFER_LEN) if not data: break recv_bytes += len(data) conn.close() ftp.voidresp() def stor(ftp, size): """Same as ftplib's storbinary() but just sends dummy data instead of reading it from a real file. """ ftp.voidcmd('TYPE I') conn = ftp.transfercmd("STOR " + TESTFN) chunk = 'x' * BUFFER_LEN total_sent = 0 while 1: sent = conn.send(chunk) total_sent += sent if total_sent >= size: break conn.close() ftp.voidresp() def bytes_per_second(ftp, retr=True): """Return the number of bytes transmitted in 1 second.""" bytes = 0 if retr: def request_file(): ftp.voidcmd('TYPE I') conn = ftp.transfercmd("retr " + TESTFN) return conn conn = request_file() 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 bytes += len(chunk) conn.close() try: ftp.voidresp() except (ftplib.error_temp, ftplib.error_perm): pass else: ftp.voidcmd('TYPE I') conn = ftp.transfercmd("STOR " + TESTFN) register_memory() chunk = 'x' * BUFFER_LEN stop_at = time.time() + 1 while stop_at > time.time(): bytes += conn.send(chunk) conn.close() ftp.voidresp() ftp.quit() return bytes def cleanup(): ftp = connect() try: ftp.delete(TESTFN) except (ftplib.error_perm, ftplib.error_temp): pass ftp.quit() class AsyncReader(asyncore.dispatcher): """Just read data from a connected socket, asynchronously.""" def __init__(self, sock): asyncore.dispatcher.__init__(self, sock) def writable(self): return False def handle_read(self): chunk = self.socket.recv(BUFFER_LEN) if not chunk: self.close() def handle_close(self): self.close() def handle_error(self): raise class AsyncWriter(asynchat.async_chat): """Just write dummy data to a connected socket, asynchronously.""" class ChunkProducer: def __init__(self, size): self.size = size self.sent = 0 self.chunk = 'x' * BUFFER_LEN def more(self): if self.sent >= self.size: return '' self.sent += len(self.chunk) return self.chunk def __init__(self, sock, size): asynchat.async_chat.__init__(self, sock) self.push_with_producer(self.ChunkProducer(size)) self.close_when_done() 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('\r\n') self.push('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 USAGE = "%s -u USERNAME -p PASSWORD [-H] [-P] [-b] [-n] [-s] [-k]" % __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') parser.add_option('-b', '--benchmark', dest='benchmark', default='transfer', help="benchmark type ('transfer', '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 server process to bench memory usage") 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 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) def bench_stor(title="STOR (client -> server)"): bytes = bytes_per_second(connect(), retr=False) print_bench(title, round(bytes / 1024.0 / 1024.0, 2), "MB/sec") def bench_retr(title="RETR (server -> client)"): bytes = bytes_per_second(connect(), retr=True) print_bench(title, round(bytes / 1024.0 / 1024.0, 2), "MB/sec") def bench_multi(): howmany = options.clients # 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], FILE_SIZE) 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, 1024 * 1024 * 5) 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("STOR (1 file with %s idle clients)" % len(clients)) bench_retr("RETR (1 file with %s idle clients)" % len(clients)) bench_multi_retr(clients) bench_multi_stor(clients) bench_multi_quit(clients) # 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 == 'transfer': bench_stor() bench_retr() elif options.benchmark == 'concurrence': bench_multi() elif options.benchmark == 'all': bench_stor() bench_retr() bench_multi() else: sys.exit("invalid 'benchmark' parameter %r" % options.benchmark) if __name__ == '__main__': main() pyftpdlib-1.2.0/test/test_contrib.py0000664000175000017500000010416312134253126021507 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: test_contrib.py 1219 2013-04-19 14:35:41Z g.rodola $ # pyftpdlib is released under the MIT license, reproduced below: # ======================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ======================================================================== """ Tests for pyftpdlib.contrib namespace: handlers.py, authorizers.py, filesystems.py and servers.py modules. """ import ftplib import unittest import os import random import string import warnings try: import pwd except ImportError: pwd = None try: import ssl except ImportError: ssl = None try: from pywintypes import error as Win32ExtError except ImportError: pass from pyftpdlib.authorizers import AuthenticationFailed, AuthorizerError from pyftpdlib import authorizers from pyftpdlib import handlers from pyftpdlib import filesystems from pyftpdlib import servers from pyftpdlib._compat import b, getcwdu, unicode from test_ftpd import * FTPS_SUPPORT = hasattr(ftplib, 'FTP_TLS') and hasattr(handlers, 'TLS_FTPHandler') CERTFILE = os.path.abspath(os.path.join(os.path.dirname(__file__), 'keycert.pem')) MPROCESS_SUPPORT = hasattr(servers, 'MultiprocessFTPServer') # ===================================================================== # --- Mixin tests # ===================================================================== # What we're going to do here is repeat the original functional tests # defined in test_ftpd.py but by using different FTP server # configurations. # # In case of FTPS we secure both control and data connections before # running any test. # Same story for ThreadedFTPServer which will be used instead of # base FTPServer class. # # 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, # or if the concurrency module used is asynchronous or based on # multiple threads or processes (fork). # ===================================================================== # --- FTPS mixin tests # ===================================================================== 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(FTPd): """A threaded FTPS server used for functional testing.""" handler = handlers.TLS_FTPHandler handler.certfile = CERTFILE class TLSTestMixin: server_class = FTPSServer client_class = FTPSClient else: 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): pass class TestFtpRetrieveDataTLSMixin(TLSTestMixin, TestFtpRetrieveData): pass class TestFtpListingCmdsTLSMixin(TLSTestMixin, TestFtpListingCmds): pass class TestFtpAbortTLSMixin(TLSTestMixin, TestFtpAbort): def test_oob_abor(self): pass class TestTimeoutsTLSMixin(TLSTestMixin, TestTimeouts): def test_data_timeout_not_reached(self): pass class TestConfigurableOptionsTLSMixin(TLSTestMixin, TestConfigurableOptions): pass class TestCallbacksTLSMixin(TLSTestMixin, TestCallbacks): def test_on_file_received(self): pass def test_on_file_sent(self): pass def test_on_incomplete_file_received(self): pass def test_on_incomplete_file_sent(self): pass def test_on_connect(self): pass def test_on_disconnect(self): pass def test_on_login(self): pass def test_on_login_failed(self): pass def test_on_logout_quit(self): pass def test_on_logout_rein(self): pass def test_on_logout_user_issued_twice(self): pass class TestIPv4EnvironmentTLSMixin(TLSTestMixin, TestIPv4Environment): pass class TestIPv6EnvironmentTLSMixin(TLSTestMixin, TestIPv6Environment): pass class TestCornerCasesTLSMixin(TLSTestMixin, TestCornerCases): pass # ===================================================================== # --- threaded FTP server mixin tests # ===================================================================== class TFTPd(FTPd): 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 TestConfigurableOptionsThreadMixin(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(FTPd): server_class = servers.MultiprocessFTPServer class MProcFTPTestMixin: server_class = MultiProcFTPd else: 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 TestConfigurableOptionsMProcMixin(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 # ===================================================================== # dedicated FTPs tests # ===================================================================== class TestFTPS(unittest.TestCase): """Specific tests fot TSL_FTPHandler class.""" def setUp(self): self.server = FTPSServer() self.server.start() self.client = ftplib.FTP_TLS() self.client.connect(self.server.host, self.server.port) self.client.sock.settimeout(2) def tearDown(self): self.client.ssl_version = ssl.PROTOCOL_SSLv23 self.server.handler.ssl_version = ssl.PROTOCOL_SSLv23 self.server.handler.tls_control_required = False self.server.handler.tls_data_required = False self.client.close() self.server.stop() def assertRaisesWithMsg(self, excClass, msg, callableObj, *args, **kwargs): try: callableObj(*args, **kwargs) except excClass: why = sys.exc_info()[1] if str(why) == msg: return raise self.failureException("%s != %s" % (str(why), 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.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.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.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') try: sock.settimeout(TIMEOUT) while 1: if not sock.recv(1024): self.client.voidresp() break self.assertTrue(isinstance(sock, ssl.SSLSocket)) # unsecured self.client.prot_c() finally: sock.close() sock = self.client.transfercmd('list') try: sock.settimeout(TIMEOUT) while 1: if not sock.recv(1024): self.client.voidresp() break self.assertFalse(isinstance(sock, ssl.SSLSocket)) finally: sock.close() def test_feat(self): 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.client.login() try: sock = self.client.sock.unwrap() except socket.error: err = sys.exc_info()[1] 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.server.handler.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.server.handler.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.server.handler.ssl_version = 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() self.client.connect(self.server.host, self.server.port) self.assertRaises(socket.error, self.client.login) self.client.ssl_version = ssl.PROTOCOL_SSLv2 # ===================================================================== # --- authorizer # ===================================================================== class SharedAuthorizerTests(unittest.TestCase): """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 os.name == 'posix': return pwd.getpwuid(os.getuid()).pw_name else: return os.environ['USERNAME'] def get_current_user_homedir(self): if os.name == '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 1: 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: why = sys.exc_info()[1] if str(why) == msg: return raise self.failureException("%s != %s" % (str(why), 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), "elradfmw") 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), "elradfmw") 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), "elradfmw") 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), "elradfmw") #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 # ===================================================================== class TestUnixAuthorizer(SharedAuthorizerTests): """Unix authorizer specific tests.""" authorizer_class = getattr(authorizers, "UnixAuthorizer", None) def test_get_perms_anonymous(self): auth = authorizers.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 = authorizers.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 = authorizers.UnixAuthorizer(require_valid_shell=False) self.assertRaises(AuthenticationFailed, auth.validate_authentication, '?!foo', '?!foo', None) auth = authorizers.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 = authorizers.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, authorizers.UnixAuthorizer, allowed_users=[user]) # commented as it first fails for invalid home #self.assertRaisesWithMsg(ValueError, # "user %s has not a valid shell" % user, # authorizers.UnixAuthorizer, anonymous_user=user) auth = authorizers.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", authorizers.UnixAuthorizer) finally: auth.terminate_impersonation('nobody') # ===================================================================== # --- Windows authorizer # ===================================================================== class TestWindowsAuthorizer(SharedAuthorizerTests): """Windows authorizer specific tests.""" authorizer_class = getattr(authorizers, "WindowsAuthorizer", None) def test_wrong_anonymous_credentials(self): user = self.get_current_user() self.assertRaises(Win32ExtError, self.authorizer_class, anonymous_user=user, anonymous_password='$|1wrongpasswd') # ===================================================================== # --- UNIX filesystem # ===================================================================== if os.name == 'posix': class TestUnixFilesystem(unittest.TestCase): def test_case(self): root = getcwdu() fs = filesystems.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) # ===================================================================== # --- main # ===================================================================== def test_main(): test_suite = unittest.TestSuite() tests = [] # FTPS tests if FTPS_SUPPORT: ftps_tests = [TestFTPS, TestFtpAuthenticationTLSMixin, TestTFtpDummyCmdsTLSMixin, TestFtpCmdsSemanticTLSMixin, TestFtpFsOperationsTLSMixin, TestFtpStoreDataTLSMixin, TestFtpRetrieveDataTLSMixin, TestFtpListingCmdsTLSMixin, TestFtpAbortTLSMixin, TestTimeoutsTLSMixin, TestConfigurableOptionsTLSMixin, TestCallbacksTLSMixin, TestCornerCasesTLSMixin, ] if SUPPORTS_IPV4: ftps_tests.append(TestIPv4EnvironmentTLSMixin) if SUPPORTS_IPV6: ftps_tests.append(TestIPv6EnvironmentTLSMixin) tests += ftps_tests else: if sys.version_info < (2, 7): warn("FTPS tests skipped (requires python 2.7)") elif ssl is None: warn("FTPS tests skipped (requires ssl module)") elif not hasattr(handlers, 'TLS_FTPHandler'): warn("FTPS tests skipped (requires PyOpenSSL module)") else: warn("FTPS tests skipped") # threaded FTP server tests ftp_thread_tests = [ TestFtpAuthenticationThreadMixin, TestTFtpDummyCmdsThreadMixin, TestFtpCmdsSemanticThreadMixin, TestFtpFsOperationsThreadMixin, TestFtpStoreDataThreadMixin, TestFtpRetrieveDataThreadMixin, TestFtpListingCmdsThreadMixin, TestFtpAbortThreadMixin, #TestTimeoutsThreadMixin, #TestConfigurableOptionsThreadMixin, TestCallbacksThreadMixin, TestCornerCasesThreadMixin, TestFTPServerThreadMixin, ] tests += ftp_thread_tests # multi process FTP server tests if MPROCESS_SUPPORT: ftp_mproc_tests = [ TestFtpAuthenticationMProcMixin, TestTFtpDummyCmdsMProcMixin, TestFtpCmdsSemanticMProcMixin, TestFtpFsOperationsMProcMixin, TestFtpStoreDataMProcMixin, TestFtpRetrieveDataMProcMixin, TestFtpListingCmdsMProcMixin, TestFtpAbortMProcMixin, #TestTimeoutsMProcMixin, #TestConfigurableOptionsMProcMixin, #TestCallbacksMProcMixin, TestCornerCasesMProcMixin, TestFTPServerMProcMixin, ] tests += ftp_mproc_tests # POSIX tests if os.name == 'posix': tests.append(TestUnixFilesystem) if hasattr(authorizers, "UnixAuthorizer"): try: authorizers.UnixAuthorizer() except AuthorizerError: # not root warn("UnixAuthorizer tests skipped (root privileges are " \ "required)") else: tests.append(TestUnixAuthorizer) else: try: import spwd except ImportError: warn("UnixAuthorizer tests skipped (spwd module is missing") else: warn("UnixAuthorizer tests skipped") # Windows tests elif os.name in ('nt', 'ce'): if hasattr(authorizers, "WindowsAuthorizer"): tests.append(TestWindowsAuthorizer) else: try: import win32api except ImportError: warn("WindowsAuthorizer tests skipped (pywin32 extension " \ "is required)") else: warn("WindowsAuthorizer tests skipped") for test in tests: test_suite.addTest(unittest.makeSuite(test)) try: result = unittest.TextTestRunner(verbosity=2).run(test_suite) finally: cleanup() return result if __name__ == '__main__': sys.exit(not test_main().wasSuccessful()) pyftpdlib-1.2.0/test/test_ftpd.py0000664000175000017500000041660412134253126021012 0ustar giampaologiampaolo00000000000000#!/usr/bin/env python # $Id: test_ftpd.py 1219 2013-04-19 14:35:41Z g.rodola $ # ====================================================================== # Copyright (C) 2007-2013 Giampaolo Rodola' # # All Rights Reserved # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # ====================================================================== import threading import unittest import socket import os import shutil import time import re import tempfile import ftplib import random import warnings import sys import errno import atexit import stat import logging import select try: from StringIO import StringIO as BytesIO except ImportError: from io import BytesIO try: import ssl except ImportError: ssl = None try: import sendfile except ImportError: sendfile = None import pyftpdlib.__main__ from pyftpdlib.ioloop import IOLoop from pyftpdlib.handlers import (FTPHandler, DTPHandler, ThrottledDTPHandler, SUPPORTS_HYBRID_IPV6) from pyftpdlib.servers import FTPServer from pyftpdlib.filesystems import AbstractedFS from pyftpdlib.authorizers import DummyAuthorizer, AuthenticationFailed from pyftpdlib._compat import PY3, u, b, getcwdu, callable # 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 = 196608 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: sock = socket.socket(family) sock.bind((host, port)) except (socket.error, socket.gaierror): return False else: sock.close() 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: err = sys.exc_info()[1] if err.errno != errno.ENOENT: raise def safe_rmdir(dir): "Convenience function for removing temporary test directories" try: os.rmdir(dir) except OSError: err = sys.exc_info()[1] if err.errno != errno.ENOENT: raise def safe_mkdir(dir): "Convenience function for creating a directory" try: os.mkdir(dir) except OSError: err = sys.exc_info()[1] if err.errno != errno.EEXIST: raise def touch(name): """Create a file and return its name.""" f = open(name, 'w') try: return f.name finally: f.close() 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: os.remove(name) def warn(msg): """Add warning message to be executed on exit.""" atexit.register(warnings.warn, str(msg) + " - tests have been skipped", RuntimeWarning) def disable_log_warning(inst): """Temporarily set FTP server's logging level to ERROR.""" def wrapper(self, *args, **kwargs): logger = logging.getLogger('pyftpdlib') level = logger.getEffectiveLevel() logger.setLevel(logging.ERROR) try: return callable(self, *args, **kwargs) finally: logger.setLevel(level) return wrapper def skip_other_tests(): """Decorator which skips all tests except the decorated one. http://mail.python.org/pipermail/python-ideas/2010-August/007992.html """ from unittest import TextTestRunner as _TextTestRunner class CustomTestRunner(_TextTestRunner): def run(self, test): if test._tests: for t1 in test._tests: for t2 in t1._tests: if t2._testMethodName == self._special_name: return _TextTestRunner.run(self, t2) raise RuntimeError("couldn't isolate test") def outer(fun, *args, **kwargs): # monkey patch unittest module unittest.TextTestRunner = CustomTestRunner if hasattr(unittest, 'runner'): unittest.runner.TextTestRunner = CustomTestRunner CustomTestRunner._special_name = fun.__name__ def inner(self): return fun(self, *args, **kwargs) return inner return outer 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: pass map.clear() # commented out as per bug http://bugs.python.org/issue10354 #tempfile.template = 'tmp-pyftpdlib' class FTPd(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 used to start(), stop() and eventually re-start() the server. """ handler = FTPHandler server_class = FTPServer def __init__(self, addr=None): threading.Thread.__init__(self) self.__serving = False self.__stopped = False self.__lock = threading.Lock() self.__flag = threading.Event() if addr is None: addr = (HOST, 0) authorizer = DummyAuthorizer() authorizer.add_user(USER, PASSWD, HOME, perm='elradfmwM') # full perms authorizer.add_anonymous(HOME) self.handler.authorizer = authorizer # lowering buffer sizes = more cycles to transfer data # = less false positive test failures self.handler.dtp_handler.ac_in_buffer_size = 32768 self.handler.dtp_handler.ac_out_buffer_size = 32768 self.server = self.server_class(addr, self.handler) self.host, self.port = self.server.socket.getsockname()[:2] def __repr__(self): status = [self.__class__.__module__ + "." + self.__class__.__name__] if self.__serving: status.append('active') else: status.append('inactive') status.append('%s:%s' % self.server.socket.getsockname()[:2]) return '<%s at %#x>' % (' '.join(status), id(self)) @property def running(self): return self.__serving def start(self, timeout=0.001): """Start serving until an explicit stop() request. Polls for shutdown every 'timeout' seconds. """ if self.__serving: raise RuntimeError("Server already started") if self.__stopped: # ensure the server can be started again FTPd.__init__(self, self.server.socket.getsockname(), self.handler) self.__timeout = timeout threading.Thread.start(self) self.__flag.wait() def run(self): self.__serving = True self.__flag.set() while self.__serving: self.__lock.acquire() self.server.serve_forever(timeout=self.__timeout, blocking=False) self.__lock.release() self.server.close_all() def stop(self): """Stop serving (also disconnecting all currently connected clients) by telling the serve_forever() loop to stop and waits until it does. """ if not self.__serving: raise RuntimeError("Server not started yet") self.__serving = False self.__stopped = True self.join() class TestCase(unittest.TestCase): # compatibility with python < 3.1 if not hasattr(unittest.TestCase, 'assertIn'): def assertIn(self, member, container, msg=None): if member not in container: self.fail(msg or '%r not found in %r' % (member, container)) def assertNotIn(self, member, container, msg=None): if member in container: self.fail(msg or '%r not found in %r' % (member, container)) # compatibility with python < 2.7 / 3.2 if not hasattr(unittest.TestCase, 'assertRaisesRegex'): def assertRaisesRegex(self, expected_exception, expected_regexp, callable_obj, *args, **kwargs): try: callable_obj(*args, **kwargs) except expected_exception: why = sys.exc_info()[1] msg = str(why) if isinstance(expected_regexp, basestring): expected_regexp = re.compile(expected_regexp) if not expected_regexp.search(msg): raise self.failureException('"%s" does not match "%s"' % (expected_regexp.pattern, msg)) else: if hasattr(expected_exception, '__name__'): exc_name = expected_exception.__name__ else: exc_name = str(expected_exception) raise self.failureException("%s not raised" % exc_name) class TestAbstractedFS(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. ae = self.assertEqual fs = AbstractedFS(u('/'), None) join = lambda x, y: os.path.join(x, y.replace('/', os.sep)) 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) ae(fs.ftp2fs(u('//a')), join(root, u('a'))) # UNC paths must be collapsed 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_ftp2fs(self): # Tests for ftp2fs method. ae = self.assertEqual fs = AbstractedFS(u('/'), None) join = lambda x, y: os.path.join(x, y.replace('/', os.sep)) 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, r'a/b')) ae(fs.ftp2fs(u('/a/b')), join(root, 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) ae(fs.ftp2fs(u('//a')), join(root, u('a'))) # UNC paths must be collapsed 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. ae = self.assertEqual fs = AbstractedFS(u('/'), None) join = lambda x, y: os.path.join(x, y.replace('/', os.sep)) 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('/')) ae(fs.fs2ftp(join(root, u('..'))), u('/')) # can't escape from root 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. file = tempfile.NamedTemporaryFile() try: if HOME == os.path.dirname(file.name): return os.symlink(file.name, TESTFN) self.assertFalse(fs.validpath(u(TESTFN))) finally: safe_remove(TESTFN) file.close() class TestDummyAuthorizer(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.assertEqual(auth.has_perm(USER, 'w', self.tempdir.upper()), True) 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 TestCallLater(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): fun = lambda: 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) self.assertRaises(AssertionError, x.cancel) def test_order(self): l = [] fun = lambda x: l.append(x) 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): l = [] fun = lambda x: l.append(x) 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): l = [] fun = lambda x: l.append(x) 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]) class TestCallEvery(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): fun = lambda: 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) self.assertRaises(AssertionError, x.cancel) def test_only_once(self): # make sure that callback is called only once per-loop l1 = [] fun = lambda: l1.append(None) 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 l = [] fun = lambda: l.append(None) 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 os.name == '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 l1 = [] fun = lambda: l1.append(None) self.ioloop.call_every(0.001, fun) self.scheduler() l2 = [] fun = lambda: l2.append(None) 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 l = [] fun = lambda: l.append(None) 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 TestFtpAuthentication(TestCase): "test: USER, PASS, REIN." server_class = FTPd client_class = ftplib.FTP def setUp(self): self.server = self.server_class() self.server.handler._auth_failed_timeout = 0.001 self.server.start() self.client = self.client_class() self.client.connect(self.server.host, self.server.port) self.client.sock.settimeout(TIMEOUT) self.file = open(TESTFN, 'w+b') self.dummyfile = BytesIO() def tearDown(self): self.server.handler._auth_failed_timeout = 5 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') 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) conn.settimeout(TIMEOUT) rein_sent = False bytes_recv = 0 while 1: 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) self.assertEqual(hash(data), hash (self.dummyfile.read())) conn.close() 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) conn.settimeout(TIMEOUT) rein_sent = 0 bytes_recv = 0 while 1: 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) self.assertEqual(hash(data), hash (self.dummyfile.read())) conn.close() class TestFtpDummyCmds(TestCase): "test: TYPE, STRU, MODE, NOOP, SYST, ALLO, HELP, SITE HELP" server_class = FTPd client_class = ftplib.FTP def setUp(self): self.server = self.server_class() self.server.start() self.client = self.client_class() self.client.connect(self.server.host, self.server.port) self.client.sock.settimeout(TIMEOUT) 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(TestCase): server_class = FTPd client_class = ftplib.FTP arg_cmds = ['allo','appe','dele','eprt','mdtm','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() self.client.connect(self.server.host, self.server.port) self.client.sock.settimeout(TIMEOUT) 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." for cmd in ('abor','cdup','feat','noop','pasv','pwd','quit','rein', 'syst','xcup','xpwd'): 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(TestCase): "test: PWD, CWD, CDUP, SIZE, RNFR, RNTO, DELE, MKD, RMD, MDTM, STAT" server_class = FTPd client_class = ftplib.FTP def setUp(self): self.server = self.server_class() self.server.start() self.client = self.client_class() self.client.connect(self.server.host, self.server.port) self.client.sock.settimeout(TIMEOUT) 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: err = sys.exc_info()[1] self.assertTrue("not retrievable" in str(err)) else: self.fail('Exception not raised') 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_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: err = sys.exc_info()[1] 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 os.name == 'nt': 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(TestCase): """Test STOR, STOU, APPE, REST, TYPE.""" server_class = FTPd client_class = ftplib.FTP def setUp(self): self.server = self.server_class() self.server.start() self.client = self.client_class() self.client.connect(self.server.host, self.server.port) self.client.sock.settimeout(TIMEOUT) 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) 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) self.assertEqual(hash(data), hash (self.dummy_recvfile.read())) 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') conn = self.client.transfercmd(cmd) conn.settimeout(TIMEOUT) while 1: buf = fp.read(blocksize) if not buf: break conn.sendall(buf) conn.close() 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) self.assertEqual(hash(expected), hash(self.dummy_recvfile.read())) 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') conn = self.client.transfercmd(cmd) conn.settimeout(TIMEOUT) while 1: buf = fp.read(blocksize) if not buf: break conn.sendall(buf) conn.close() 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: sock = self.client.makeport() sock.settimeout(TIMEOUT) conn, sockaddr = sock.accept() conn.settimeout(TIMEOUT) if hasattr(self.client_class, 'ssl_version'): conn = ssl.wrap_socket(conn) while 1: buf = self.dummy_sendfile.read(8192) if not buf: break conn.sendall(buf) sock.close() conn.close() # 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) self.assertEqual(hash(data), hash (self.dummy_recvfile.read())) 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) self.assertEqual(hash(data1 + data2), hash (self.dummy_recvfile.read())) 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') conn = self.client.transfercmd('stor ' + TESTFN) conn.settimeout(TIMEOUT) bytes_sent = 0 while 1: 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 conn.close() # 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) self.assertEqual(hash(self.dummy_sendfile.read()), hash(self.dummy_recvfile.read()) ) 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. conn = self.client.transfercmd('stor ' + TESTFN) conn.settimeout(TIMEOUT) conn.sendall(b('abcde12345') * 50000) self.client.sendcmd('quit') conn.sendall(b('abcde12345') * 50000) conn.close() # 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() f = open(TESTFN) self.assertEqual(f.read(), "") f.close() if SUPPORTS_SENDFILE: class TestFtpStoreDataNoSendfile(TestFtpStoreData): """Test STOR, STOU, APPE, REST, TYPE not using sendfile().""" def setUp(self): TestFtpStoreData.setUp(self) self.server.handler.use_sendfile = False def tearDown(self): TestFtpStoreData.tearDown(self) self.server.handler.use_sendfile = True class TestFtpRetrieveData(TestCase): "Test RETR, REST, TYPE" server_class = FTPd client_class = ftplib.FTP def setUp(self): self.server = self.server_class() self.server.start() self.client = self.client_class() self.client.connect(self.server.host, self.server.port) self.client.sock.settimeout(TIMEOUT) 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) 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) self.assertEqual(hash(data), hash(self.dummyfile.read())) # 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') conn = self.client.transfercmd(cmd, rest) conn.settimeout(TIMEOUT) while 1: data = conn.recv(blocksize) if not data: break callback(data) conn.close() 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) self.assertEqual(hash(expected), hash(self.dummyfile.read())) 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') conn = self.client.transfercmd('retr ' + TESTFN) conn.settimeout(TIMEOUT) while 1: chunk = conn.recv(BUFSIZE) if not chunk: break self.dummyfile.write(chunk) received_bytes += len(chunk) if received_bytes >= INTERRUPTED_TRANSF_SIZE: break conn.close() # 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) self.assertEqual(hash(data), hash (self.dummyfile.read())) def test_retr_empty_file(self): self.client.retrbinary("retr " + TESTFN, self.dummyfile.write) self.dummyfile.seek(0) self.assertEqual(self.dummyfile.read(), b("")) if SUPPORTS_SENDFILE: class TestFtpRetrieveDataNoSendfile(TestFtpRetrieveData): """Test RETR, REST, TYPE by not using sendfile().""" def setUp(self): TestFtpRetrieveData.setUp(self) self.server.handler.use_sendfile = False def tearDown(self): TestFtpRetrieveData.tearDown(self) self.server.handler.use_sendfile = True class TestFtpListingCmds(TestCase): """Test LIST, NLST, argumented STAT.""" server_class = FTPd client_class = ftplib.FTP def setUp(self): self.server = self.server_class() self.server.start() self.client = self.client_class() self.client.connect(self.server.host, self.server.port) self.client.sock.settimeout(TIMEOUT) 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 mlstline = lambda cmd: 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)) try: try: self.client.retrlines('mlsd ' + TESTFN, lambda x: x) except ftplib.error_perm: resp = sys.exc_info()[1] # if path is a file a 501 response code is expected self.assertEqual(str(resp)[0:3], "501") else: self.fail("Exception not raised") finally: safe_rmdir(dir) 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 os.name == '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 os.name == 'nt': 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(TestCase): "test: ABOR" server_class = FTPd client_class = ftplib.FTP def setUp(self): self.server = self.server_class() self.server.start() self.client = self.client_class() self.client.connect(self.server.host, self.server.port) self.client.sock.settimeout(TIMEOUT) 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) sock = self.client.makeport() sock.settimeout(TIMEOUT) respcode = self.client.sendcmd('ABOR')[:3] sock.close() 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 f = open(TESTFN, 'w+b') f.write(data) f.close() conn = None try: self.client.voidcmd('TYPE I') conn = self.client.transfercmd('retr ' + TESTFN) conn.settimeout(TIMEOUT) 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) if conn is not None: conn.close() if hasattr(socket, 'MSG_OOB'): 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.client.sock.settimeout(TIMEOUT) self.assertEqual(self.client.getresp()[:3], '225') class TestThrottleBandwidth(unittest.TestCase): """Test ThrottledDTPHandler class.""" server_class = FTPd 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() self.client.connect(self.server.host, self.server.port) self.client.sock.settimeout(2) 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. self.server.handler.dtp_handler.write_limit = 32768 data = b('abcde12345') * 100000 file = open(TESTFN, 'wb') file.write(data) file.close() self.client.retrbinary("retr " + TESTFN, self.dummyfile.write) self.dummyfile.seek(0) self.assertEqual(hash(data), hash(self.dummyfile.read())) def test_throttle_recv(self): # This test doesn't test the actual speed accuracy, just # awakes all that code which implements the throttling. 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 file = open(TESTFN, 'rb') file_data = file.read() file.close() self.assertEqual(hash(data), hash(file_data)) class TestTimeouts(TestCase): """Test idle-timeout capabilities of control and data channels. Some tests may fail on slow machines. """ server_class = FTPd 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() self.client.connect(self.server.host, self.server.port) self.client.sock.settimeout(TIMEOUT) 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() 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.1) addr = self.client.makepasv() s = socket.socket() 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') s.close() 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.1) sock = self.client.transfercmd('stor ' + TESTFN) sock.settimeout(TIMEOUT) 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.1, data_timeout=0.2) addr = self.client.makepasv() s = socket.socket() 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') s.close() 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.1, data_timeout=0.2) addr = self.client.makepasv() s = socket.socket() 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') s.close() 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.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() s = socket.socket() s.settimeout(TIMEOUT) s.connect(addr) s.close() def test_disabled_pasv_timeout(self): self._setUp(pasv_timeout=0) self.client.makepasv() # reset passive socket addr = self.client.makepasv() s = socket.socket() s.settimeout(TIMEOUT) s.connect(addr) s.close() def test_disabled_port_timeout(self): self._setUp(port_timeout=0) s1 = self.client.makeport() s2 = self.client.makeport() s1.close() s2.close() class TestConfigurableOptions(TestCase): """Test those daemon options which are commonly modified by user.""" server_class = FTPd client_class = ftplib.FTP def setUp(self): touch(TESTFN) self.server = self.server_class() self.server.start() self.client = self.client_class() self.client.connect(self.server.host, self.server.port) self.client.sock.settimeout(TIMEOUT) self.client.login(USER, PASSWD) def tearDown(self): os.remove(TESTFN) # set back options to their original value 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.passive_ports = None self.server.handler.use_gmt_times = True self.server.handler.tcp_no_delay = hasattr(socket, 'TCP_NODELAY') self.server.stop() @disable_log_warning def test_max_connections(self): # Test FTPServer.max_cons attribute self.server.server.max_cons = 3 self.client.quit() 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) sock = c1.makeport() sock.settimeout(TIMEOUT) self.assertRaises(ftplib.error_temp, c2.connect, self.server.host, self.server.port) sock.close() 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.server.max_cons_per_ip = 3 self.client.quit() 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.handler.banner = 'hello there' self.client.close() self.client = self.client_class() self.client.connect(self.server.host, self.server.port) self.client.sock.settimeout(TIMEOUT) self.assertEqual(self.client.getwelcome()[4:], 'hello there') def test_max_login_attempts(self): # Test FTPHandler.max_login_attempts attribute. self.server.handler.max_login_attempts = 1 self.server.handler._auth_failed_timeout = 0 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 host, port = self.client.makepasv() self.assertEqual(host, self.server.host) self.server.handler.masquerade_address = "256.256.256.256" host, port = self.client.makepasv() self.assertEqual(host, "256.256.256.256") def test_masquerade_address_map(self): # Test FTPHandler.masquerade_address_map attribute host, port = self.client.makepasv() self.assertEqual(host, self.server.host) self.server.handler.masquerade_address_map = {self.server.host : "128.128.128.128"} host, port = self.client.makepasv() self.assertEqual(host, "128.128.128.128") def test_passive_ports(self): # Test FTPHandler.passive_ports attribute _range = list(range(40000, 60000, 200)) self.server.handler.passive_ports = _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) 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 s = socket.socket() s.bind((HOST, 0)) s.settimeout(TIMEOUT) port = s.getsockname()[1] self.server.handler.passive_ports = [port] resulting_port = self.client.makepasv()[1] self.assertTrue(port != resulting_port) 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) sock.bind((HOST, port)) sock.settimeout(TIMEOUT) break except socket.error: err = sys.exc_info()[1] if err.args[0] == 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 try: self.server.handler.permit_privileged_ports = False self.assertRaises(ftplib.error_perm, self.client.sendport, HOST, port) if sock: port = sock.getsockname()[1] self.server.handler.permit_privileged_ports = True sock.listen(5) sock.settimeout(TIMEOUT) self.client.sendport(HOST, port) s, addr = sock.accept() s.close() finally: if sock is not None: sock.close() def test_use_gmt_times(self): # use GMT time self.server.handler.use_gmt_times = True gmt1 = self.client.sendcmd('mdtm ' + TESTFN) gmt2 = self.client.sendcmd('mlst ' + TESTFN) gmt3 = self.client.sendcmd('stat ' + TESTFN) # use local time self.server.handler.use_gmt_times = False self.client.quit() self.client.connect(self.server.host, self.server.port) self.client.sock.settimeout(TIMEOUT) self.client.login(USER, PASSWD) 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) if hasattr(socket, 'TCP_NODELAY'): def test_tcp_no_delay(self): def get_handler_socket(): # return the server's handler socket object ioloop = IOLoop.instance() for fd in ioloop.socket_map: instance = ioloop.socket_map[fd] if isinstance(instance, FTPHandler): break return instance.socket s = get_handler_socket() self.assertTrue(s.getsockopt(socket.SOL_TCP, socket.TCP_NODELAY)) self.client.quit() self.server.handler.tcp_no_delay = False self.client.connect(self.server.host, self.server.port) self.client.sendcmd('noop') s = get_handler_socket() self.assertFalse(s.getsockopt(socket.SOL_TCP, socket.TCP_NODELAY)) class TestCallbacks(TestCase): """Test FTPHandler class callback methods.""" server_class = FTPd client_class = ftplib.FTP def setUp(self): self.client = None self.server = None self._tearDown = True def _setUp(self, handler, connect=True, login=True): FTPd.handler = handler self.server = self.server_class() self.server.start() self.client = self.client_class() if connect: self.client.connect(self.server.host, self.server.port) self.client.sock.settimeout(TIMEOUT) if login: self.client.login(USER, PASSWD) self.file = open(TESTFN, 'w+b') self.dummyfile = BytesIO() self._tearDown = False def tearDown(self): if not self._tearDown: FTPd.handler = FTPHandler self._tearDown = True if self.client is not None: self.client.close() if self.server is not None: self.server.stop() if not self.file.closed: self.file.close() if not self.dummyfile.closed: self.dummyfile.close() os.remove(TESTFN) def test_on_file_sent(self): _file = [] class TestHandler(FTPHandler): def on_file_sent(self, file): _file.append(file) def on_file_received(self, file): raise Exception def on_incomplete_file_sent(self, file): raise Exception def on_incomplete_file_received(self, file): raise Exception self._setUp(TestHandler) data = b('abcde12345') * 100000 self.file.write(data) self.file.close() self.client.retrbinary("retr " + TESTFN, lambda x: x) # shut down the server to avoid race conditions self.tearDown() self.assertEqual(_file, [os.path.abspath(TESTFN)]) def test_on_file_received(self): _file = [] class TestHandler(FTPHandler): def on_file_sent(self, file): raise Exception def on_file_received(self, file): _file.append(file) def on_incomplete_file_sent(self, file): raise Exception def on_incomplete_file_received(self, file): raise Exception self._setUp(TestHandler) data = b('abcde12345') * 100000 self.dummyfile.write(data) self.dummyfile.seek(0) self.client.storbinary('stor ' + TESTFN, self.dummyfile) # shut down the server to avoid race conditions self.tearDown() self.assertEqual(_file, [os.path.abspath(TESTFN)]) def test_on_incomplete_file_sent(self): _file = [] class TestHandler(FTPHandler): def on_file_sent(self, file): raise Exception def on_file_received(self, file): raise Exception def on_incomplete_file_sent(self, file): _file.append(file) def on_incomplete_file_received(self, file): raise Exception self._setUp(TestHandler) data = b('abcde12345') * 100000 self.file.write(data) self.file.close() bytes_recv = 0 conn = self.client.transfercmd("retr " + TESTFN, None) conn.settimeout(TIMEOUT) while 1: chunk = conn.recv(BUFSIZE) bytes_recv += len(chunk) if bytes_recv >= INTERRUPTED_TRANSF_SIZE or not chunk: break conn.close() self.assertEqual(self.client.getline()[:3], "426") # shut down the server to avoid race conditions self.tearDown() self.assertEqual(_file, [os.path.abspath(TESTFN)]) def test_on_incomplete_file_received(self): _file = [] class TestHandler(FTPHandler): def on_file_sent(self, file): raise Exception def on_file_received(self, file): raise Exception def on_incomplete_file_sent(self, file): raise Exception def on_incomplete_file_received(self, file): _file.append(file) self._setUp(TestHandler) data = b('abcde12345') * 100000 self.dummyfile.write(data) self.dummyfile.seek(0) conn = self.client.transfercmd('stor ' + TESTFN) conn.settimeout(TIMEOUT) bytes_sent = 0 while 1: chunk = self.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 conn.close() self.assertRaises(ftplib.error_temp, self.client.getresp) # 426 # shut down the server to avoid race conditions self.tearDown() self.assertEqual(_file, [os.path.abspath(TESTFN)]) def test_on_connect(self): flag = [] class TestHandler(FTPHandler): def on_connect(self): flag.append(None) self._setUp(TestHandler, connect=False) self.client.connect(self.server.host, self.server.port) self.client.sock.settimeout(TIMEOUT) self.client.sendcmd('noop') self.assertTrue(flag) def test_on_disconnect(self): flag = [] class TestHandler(FTPHandler): def on_disconnect(self): flag.append(None) self._setUp(TestHandler, connect=False) self.client.connect(self.server.host, self.server.port) self.client.sock.settimeout(TIMEOUT) self.assertFalse(flag) self.client.sendcmd('quit') try: self.client.sendcmd('noop') except (socket.error, EOFError): pass else: self.fail('still connected') self.tearDown() self.assertTrue(flag) def test_on_login(self): user = [] class TestHandler(FTPHandler): _auth_failed_timeout = 0 def on_login(self, username): user.append(username) def on_login_failed(self, username, password): raise Exception self._setUp(TestHandler) # shut down the server to avoid race conditions self.tearDown() self.assertEqual(user, [USER]) def test_on_login_failed(self): pair = [] class TestHandler(FTPHandler): _auth_failed_timeout = 0 def on_login(self, username): raise Exception def on_login_failed(self, username, password): pair.append((username, password)) self._setUp(TestHandler, login=False) self.assertRaises(ftplib.error_perm, self.client.login, 'foo', 'bar') # shut down the server to avoid race conditions self.tearDown() self.assertEqual(pair, [('foo', 'bar')]) def test_on_login_failed(self): pair = [] class TestHandler(FTPHandler): _auth_failed_timeout = 0 def on_login(self, username): raise Exception def on_login_failed(self, username, password): pair.append((username, password)) self._setUp(TestHandler, login=False) self.assertRaises(ftplib.error_perm, self.client.login, 'foo', 'bar') # shut down the server to avoid race conditions self.tearDown() self.assertEqual(pair, [('foo', 'bar')]) def test_on_logout_quit(self): user = [] class TestHandler(FTPHandler): def on_logout(self, username): user.append(username) self._setUp(TestHandler) self.client.quit() # shut down the server to avoid race conditions self.tearDown() self.assertEqual(user, [USER]) def test_on_logout_rein(self): user = [] class TestHandler(FTPHandler): def on_logout(self, username): user.append(username) self._setUp(TestHandler) self.client.sendcmd('rein') # shut down the server to avoid race conditions self.tearDown() self.assertEqual(user, [USER]) def test_on_logout_user_issued_twice(self): users = [] class TestHandler(FTPHandler): def on_logout(self, username): users.append(username) self._setUp(TestHandler) # At this point user "user" is logged in. Re-login as anonymous, # then quit and expect queue == ["user", "anonymous"] self.client.login("anonymous") self.client.quit() # shut down the server to avoid race conditions self.tearDown() self.assertEqual(users, [USER, 'anonymous']) def test_on_logout_no_pass(self): # make sure on_logout() is not called if USER was provided # but not PASS users = [] class TestHandler(FTPHandler): def on_logout(self, username): users.append(username) self._setUp(TestHandler, login=False) self.client.sendcmd("user foo") self.client.quit() # shut down the server to avoid race conditions self.tearDown() self.assertEqual(users, []) class TestFTPServer(TestCase): """Tests for *FTPServer classes.""" server_class = FTPd 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 sock = socket.socket() 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() self.client.connect(ip, port) self.client.sock.settimeout(TIMEOUT) self.client.login(USER, PASSWD) class _TestNetworkProtocols(TestCase): """Test PASV, EPSV, PORT and EPRT commands. Do not use this class directly, let TestIPv4Environment and TestIPv6Environment classes use it instead. """ server_class = FTPd client_class = ftplib.FTP HOST = HOST def setUp(self): self.server = self.server_class((self.HOST, 0)) self.server.start() self.client = self.client_class() self.client.connect(self.server.host, self.server.port) self.client.sock.settimeout(TIMEOUT) 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: err = sys.exc_info()[1] 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: err = sys.exc_info()[1] 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 sock = socket.socket(self.client.af) 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: try: s = sock.accept() s[0].close() except socket.timeout: self.fail("Server didn't connect to passive socket") finally: sock.close() def test_epsv(self): # test wrong proto try: self.client.sendcmd('epsv ' + self.other_proto) except ftplib.error_perm: err = sys.exc_info()[1] 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()) s = socket.socket(self.client.af, socket.SOCK_STREAM) s.settimeout(TIMEOUT) try: s.connect((host, port)) self.client.sendcmd('abor') finally: s.close() 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)) class TestIPv4Environment(_TestNetworkProtocols): """Test PASV, EPSV, PORT and EPRT commands. Runs tests contained in _TestNetworkProtocols class by using IPv4 plus some additional specific tests. """ server_class = FTPd client_class = ftplib.FTP HOST = '127.0.0.1' @disable_log_warning def test_port_v4(self): # test connection sock = self.client.makeport() sock.settimeout(TIMEOUT) self.client.sendcmd('abor') sock.close() # 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 resp = self.cmdresp('port %s,1,1' % self.HOST.replace('.',',')) # port < 1024 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')) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(TIMEOUT) try: s.connect((host, port)) finally: s.close() class TestIPv6Environment(_TestNetworkProtocols): """Test PASV, EPSV, PORT and EPRT commands. Runs tests contained in _TestNetworkProtocols class by using IPv6 plus some additional specific tests. """ server_class = FTPd 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) class TestIPv6MixedEnvironment(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 = FTPd 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): noop = lambda x: x self.client = self.client_class() self.client.connect('127.0.0.1', self.server.port) self.client.set_pasv(False) self.client.sock.settimeout(TIMEOUT) self.client.login(USER, PASSWD) self.client.retrlines('list', noop) def test_pasv_v4(self): noop = lambda x: x self.client = self.client_class() self.client.connect('127.0.0.1', self.server.port) self.client.set_pasv(True) self.client.sock.settimeout(TIMEOUT) 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() self.client.connect('127.0.0.1', self.server.port) self.client.sock.settimeout(2) self.client.login(USER, PASSWD) # test connection sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 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)) sock2 = None try: try: sock2, addr = sock.accept() except socket.timeout: self.fail("Server didn't connect to passive socket") finally: sock.close() if sock2 is not None: sock2.close() def test_epsv_v4(self): mlstline = lambda cmd: self.client.voidcmd(cmd).split('\n')[1] self.client = self.client_class() self.client.connect('127.0.0.1', self.server.port) self.client.sock.settimeout(TIMEOUT) self.client.login(USER, PASSWD) host, port = ftplib.parse229(self.client.sendcmd('EPSV'), self.client.sock.getpeername()) self.assertEqual('127.0.0.1', host) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(TIMEOUT) s.connect((host, port)) self.assertTrue(mlstline('mlst /').endswith('/')) s.close() class TestCornerCases(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 = FTPd client_class = ftplib.FTP def setUp(self): self.server = self.server_class() self.server.start() self.client = self.client_class() self.client.connect(self.server.host, self.server.port) self.client.sock.settimeout(TIMEOUT) self.client.login(USER, PASSWD) def tearDown(self): self.client.close() if self.server.running: 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. sock = socket.socket(self.client.af) 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.quit() s, addr = sock.accept() s.close() sock.close() 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") 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: self.server.handler.abstracted_fs = AbstractedFS 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): s = socket.socket() # 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 s.close() 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 sock = socket.socket() sock.bind((HOST, 0)) port = sock.getsockname()[1] self.client.sock.settimeout(.1) try: resp = self.client.sendport(HOST, port) except ftplib.error_temp: err = sys.exc_info()[1] self.assertEqual(str(err)[:3], '425') except (socket.timeout, getattr(ssl, "SSLError", object())): pass else: self.assertNotEqual(str(resp)[:3], '200') sock.close() def test_repr(self): # make sure the FTP/DTP handler classes have a sane repr() sock = self.client.makeport() for inst in IOLoop.instance().socket_map.values(): repr(inst) str(inst) sock.close() 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. class TestUnicodePathNames(TestCase): """Test FTP commands and responses by using path names with non ASCII characters. """ server_class = FTPd client_class = ftplib.FTP def setUp(self): self.server = self.server_class() self.server.start() self.client = self.client_class() self.client.encoding = 'utf8' # PY3 only self.client.connect(self.server.host, self.server.port) self.client.sock.settimeout(TIMEOUT) 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 mlstline = lambda cmd: 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 f = open(TESTFN_UNICODE_2, 'wb') f.write(data) f.close() 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 TestCommandLineParser(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"] warnings.filterwarnings("error") try: self.assertRaises(RuntimeWarning, pyftpdlib.__main__.main) finally: warnings.resetwarnings() # 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) logging.basicConfig(level=logging.WARNING) remove_test_files() def test_main(tests=None): test_suite = unittest.TestSuite() if tests is None: tests = [ TestAbstractedFS, TestDummyAuthorizer, TestCallLater, TestCallEvery, TestFtpAuthentication, TestFtpDummyCmds, TestFtpCmdsSemantic, TestFtpFsOperations, TestFtpStoreData, TestFtpRetrieveData, TestFtpListingCmds, TestFtpAbort, TestThrottleBandwidth, TestTimeouts, TestConfigurableOptions, TestCallbacks, TestFTPServer, TestCornerCases, #TestUnicodePathNames, # TODO: fix errors and re-enable TestCommandLineParser, ] if SUPPORTS_IPV4: tests.append(TestIPv4Environment) else: warn("IPv4 stack not available") if SUPPORTS_IPV6: tests.append(TestIPv6Environment) else: warn("IPv6 stack not available") if SUPPORTS_HYBRID_IPV6: tests.append(TestIPv6MixedEnvironment) else: warn("IPv4/6 dual stack not available") if SUPPORTS_SENDFILE: tests.append(TestFtpRetrieveDataNoSendfile) tests.append(TestFtpStoreDataNoSendfile) else: if os.name == 'posix': warn("sendfile() not available") for test in tests: test_suite.addTest(unittest.makeSuite(test)) try: result = unittest.TextTestRunner(verbosity=2).run(test_suite) finally: cleanup() return result if __name__ == '__main__': sys.exit(not test_main().wasSuccessful()) pyftpdlib-1.2.0/CREDITS0000664000175000017500000000721612117370650016503 0ustar giampaologiampaolo00000000000000 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 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 pyftpdlib-1.2.0/MANIFEST.in0000664000175000017500000000053512110662101017202 0ustar giampaologiampaolo00000000000000# Tells "python setup.py sdist" what files to include in the tarball. # Note: make sure everything is in place with "svn list -R". include CREDITS include HISTORY include INSTALL include LICENSE include MANIFEST.in include README include setup.py recursive-include demo *.py *.pem recursive-include pyftpdlib *.py recursive-include test *.py *.pem pyftpdlib-1.2.0/README0000664000175000017500000000066011724204624016337 0ustar giampaologiampaolo00000000000000 About ===== Python FTP server library provides a high-level portable interface to easily write asynchronous FTP servers with Python. pyftpdlib is currently the most complete RFC-959 FTP server implementation available for Python programming language. It is used in projects like Google Chromium and Bazaar and included in Linux Fedora and FreeBSD package repositories. Learn more by visiting: http://code.google.com/p/pyftpdlib/ pyftpdlib-1.2.0/INSTALL0000664000175000017500000000004611724204624016506 0ustar giampaologiampaolo00000000000000Install ======= See doc/install.html