1# Copyright (C) 2007 Giampaolo Rodola' <g.rodola@gmail.com>.
2# Use of this source code is governed by MIT license that can be
3# found in the LICENSE file.
4
5import asynchat
6import contextlib
7import errno
8import glob
9import logging
10import os
11import random
12import socket
13import sys
14import time
15import traceback
16import warnings
17from datetime import datetime
18
19try:
20    import pwd
21    import grp
22except ImportError:
23    pwd = grp = None
24
25try:
26    from OpenSSL import SSL  # requires "pip install pyopenssl"
27except ImportError:
28    SSL = None
29
30try:
31    from collections import OrderedDict  # python >= 2.7
32except ImportError:
33    OrderedDict = dict
34
35from . import __ver__
36from ._compat import b
37from ._compat import getcwdu
38from ._compat import PY3
39from ._compat import u
40from ._compat import unicode
41from ._compat import xrange
42from .authorizers import AuthenticationFailed
43from .authorizers import AuthorizerError
44from .authorizers import DummyAuthorizer
45from .filesystems import AbstractedFS
46from .filesystems import FilesystemError
47from .ioloop import _ERRNOS_DISCONNECTED
48from .ioloop import _ERRNOS_RETRY
49from .ioloop import Acceptor
50from .ioloop import AsyncChat
51from .ioloop import Connector
52from .ioloop import RetryError
53from .ioloop import timer
54from .log import debug
55from .log import logger
56
57
58CR_BYTE = ord('\r')
59
60
61def _import_sendfile():
62    # By default attempt to use os.sendfile introduced in Python 3.3:
63    # http://bugs.python.org/issue10882
64    # ...otherwise fallback on using third-party pysendfile module:
65    # https://github.com/giampaolo/pysendfile/
66    if os.name == 'posix':
67        try:
68            return os.sendfile  # py >= 3.3
69        except AttributeError:
70            try:
71                import sendfile as sf
72                # dirty hack to detect whether old 1.2.4 version is installed
73                if hasattr(sf, 'has_sf_hdtr'):
74                    raise ImportError
75                return sf.sendfile
76            except ImportError:
77                pass
78    return None
79
80
81sendfile = _import_sendfile()
82
83proto_cmds = {
84    'ABOR': dict(
85        perm=None, auth=True, arg=False,
86        help='Syntax: ABOR (abort transfer).'),
87    'ALLO': dict(
88        perm=None, auth=True, arg=True,
89        help='Syntax: ALLO <SP> bytes (noop; allocate storage).'),
90    'APPE': dict(
91        perm='a', auth=True, arg=True,
92        help='Syntax: APPE <SP> file-name (append data to file).'),
93    'CDUP': dict(
94        perm='e', auth=True, arg=False,
95        help='Syntax: CDUP (go to parent directory).'),
96    'CWD': dict(
97        perm='e', auth=True, arg=None,
98        help='Syntax: CWD [<SP> dir-name] (change working directory).'),
99    'DELE': dict(
100        perm='d', auth=True, arg=True,
101        help='Syntax: DELE <SP> file-name (delete file).'),
102    'EPRT': dict(
103        perm=None, auth=True, arg=True,
104        help='Syntax: EPRT <SP> |proto|ip|port| (extended active mode).'),
105    'EPSV': dict(
106        perm=None, auth=True, arg=None,
107        help='Syntax: EPSV [<SP> proto/"ALL"] (extended passive mode).'),
108    'FEAT': dict(
109        perm=None, auth=False, arg=False,
110        help='Syntax: FEAT (list all new features supported).'),
111    'HELP': dict(
112        perm=None, auth=False, arg=None,
113        help='Syntax: HELP [<SP> cmd] (show help).'),
114    'LIST': dict(
115        perm='l', auth=True, arg=None,
116        help='Syntax: LIST [<SP> path] (list files).'),
117    'MDTM': dict(
118        perm='l', auth=True, arg=True,
119        help='Syntax: MDTM [<SP> path] (file last modification time).'),
120    'MFMT': dict(
121        perm='T', auth=True, arg=True,
122        help='Syntax: MFMT <SP> timeval <SP> path (file update last '
123             'modification time).'),
124    'MLSD': dict(
125        perm='l', auth=True, arg=None,
126        help='Syntax: MLSD [<SP> path] (list directory).'),
127    'MLST': dict(
128        perm='l', auth=True, arg=None,
129        help='Syntax: MLST [<SP> path] (show information about path).'),
130    'MODE': dict(
131        perm=None, auth=True, arg=True,
132        help='Syntax: MODE <SP> mode (noop; set data transfer mode).'),
133    'MKD': dict(
134        perm='m', auth=True, arg=True,
135        help='Syntax: MKD <SP> path (create directory).'),
136    'NLST': dict(
137        perm='l', auth=True, arg=None,
138        help='Syntax: NLST [<SP> path] (list path in a compact form).'),
139    'NOOP': dict(
140        perm=None, auth=False, arg=False,
141        help='Syntax: NOOP (just do nothing).'),
142    'OPTS': dict(
143        perm=None, auth=True, arg=True,
144        help='Syntax: OPTS <SP> cmd [<SP> option] (set option for command).'),
145    'PASS': dict(
146        perm=None, auth=False, arg=None,
147        help='Syntax: PASS [<SP> password] (set user password).'),
148    'PASV': dict(
149        perm=None, auth=True, arg=False,
150        help='Syntax: PASV (open passive data connection).'),
151    'PORT': dict(
152        perm=None, auth=True, arg=True,
153        help='Syntax: PORT <sp> h,h,h,h,p,p (open active data connection).'),
154    'PWD': dict(
155        perm=None, auth=True, arg=False,
156        help='Syntax: PWD (get current working directory).'),
157    'QUIT': dict(
158        perm=None, auth=False, arg=False,
159        help='Syntax: QUIT (quit current session).'),
160    'REIN': dict(
161        perm=None, auth=True, arg=False,
162        help='Syntax: REIN (flush account).'),
163    'REST': dict(
164        perm=None, auth=True, arg=True,
165        help='Syntax: REST <SP> offset (set file offset).'),
166    'RETR': dict(
167        perm='r', auth=True, arg=True,
168        help='Syntax: RETR <SP> file-name (retrieve a file).'),
169    'RMD': dict(
170        perm='d', auth=True, arg=True,
171        help='Syntax: RMD <SP> dir-name (remove directory).'),
172    'RNFR': dict(
173        perm='f', auth=True, arg=True,
174        help='Syntax: RNFR <SP> file-name (rename (source name)).'),
175    'RNTO': dict(
176        perm='f', auth=True, arg=True,
177        help='Syntax: RNTO <SP> file-name (rename (destination name)).'),
178    'SITE': dict(
179        perm=None, auth=False, arg=True,
180        help='Syntax: SITE <SP> site-command (execute SITE command).'),
181    'SITE HELP': dict(
182        perm=None, auth=False, arg=None,
183        help='Syntax: SITE HELP [<SP> cmd] (show SITE command help).'),
184    'SITE CHMOD': dict(
185        perm='M', auth=True, arg=True,
186        help='Syntax: SITE CHMOD <SP> mode path (change file mode).'),
187    'SIZE': dict(
188        perm='l', auth=True, arg=True,
189        help='Syntax: SIZE <SP> file-name (get file size).'),
190    'STAT': dict(
191        perm='l', auth=False, arg=None,
192        help='Syntax: STAT [<SP> path name] (server stats [list files]).'),
193    'STOR': dict(
194        perm='w', auth=True, arg=True,
195        help='Syntax: STOR <SP> file-name (store a file).'),
196    'STOU': dict(
197        perm='w', auth=True, arg=None,
198        help='Syntax: STOU [<SP> name] (store a file with a unique name).'),
199    'STRU': dict(
200        perm=None, auth=True, arg=True,
201        help='Syntax: STRU <SP> type (noop; set file structure).'),
202    'SYST': dict(
203        perm=None, auth=False, arg=False,
204        help='Syntax: SYST (get operating system type).'),
205    'TYPE': dict(
206        perm=None, auth=True, arg=True,
207        help='Syntax: TYPE <SP> [A | I] (set transfer type).'),
208    'USER': dict(
209        perm=None, auth=False, arg=True,
210        help='Syntax: USER <SP> user-name (set username).'),
211    'XCUP': dict(
212        perm='e', auth=True, arg=False,
213        help='Syntax: XCUP (obsolete; go to parent directory).'),
214    'XCWD': dict(
215        perm='e', auth=True, arg=None,
216        help='Syntax: XCWD [<SP> dir-name] (obsolete; change directory).'),
217    'XMKD': dict(
218        perm='m', auth=True, arg=True,
219        help='Syntax: XMKD <SP> dir-name (obsolete; create directory).'),
220    'XPWD': dict(
221        perm=None, auth=True, arg=False,
222        help='Syntax: XPWD (obsolete; get current dir).'),
223    'XRMD': dict(
224        perm='d', auth=True, arg=True,
225        help='Syntax: XRMD <SP> dir-name (obsolete; remove directory).'),
226}
227
228if not hasattr(os, 'chmod'):
229    del proto_cmds['SITE CHMOD']
230
231
232def _strerror(err):
233    if isinstance(err, EnvironmentError):
234        try:
235            return os.strerror(err.errno)
236        except AttributeError:
237            # not available on PythonCE
238            if not hasattr(os, 'strerror'):
239                return err.strerror
240            raise
241    else:
242        return str(err)
243
244
245def _is_ssl_sock(sock):
246    return SSL is not None and isinstance(sock, SSL.Connection)
247
248
249def _support_hybrid_ipv6():
250    """Return True if it is possible to use hybrid IPv6/IPv4 sockets
251    on this platform.
252    """
253    # Note: IPPROTO_IPV6 constant is broken on Windows, see:
254    # http://bugs.python.org/issue6926
255    try:
256        if not socket.has_ipv6:
257            return False
258        with contextlib.closing(socket.socket(socket.AF_INET6)) as sock:
259            return not sock.getsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY)
260    except (socket.error, AttributeError):
261        return False
262
263
264SUPPORTS_HYBRID_IPV6 = _support_hybrid_ipv6()
265
266
267class _FileReadWriteError(OSError):
268    """Exception raised when reading or writing a file during a transfer."""
269
270
271class _GiveUpOnSendfile(Exception):
272    """Exception raised in case use of sendfile() fails on first try,
273    in which case send() will be used.
274    """
275
276
277# --- DTP classes
278
279class PassiveDTP(Acceptor):
280    """Creates a socket listening on a local port, dispatching the
281    resultant connection to DTPHandler. Used for handling PASV command.
282
283     - (int) timeout: the timeout for a remote client to establish
284       connection with the listening socket. Defaults to 30 seconds.
285
286     - (int) backlog: the maximum number of queued connections passed
287       to listen(). If a connection request arrives when the queue is
288       full the client may raise ECONNRESET. Defaults to 5.
289    """
290    timeout = 30
291    backlog = None
292
293    def __init__(self, cmd_channel, extmode=False):
294        """Initialize the passive data server.
295
296         - (instance) cmd_channel: the command channel class instance.
297         - (bool) extmode: wheter use extended passive mode response type.
298        """
299        self.cmd_channel = cmd_channel
300        self.log = cmd_channel.log
301        self.log_exception = cmd_channel.log_exception
302        Acceptor.__init__(self, ioloop=cmd_channel.ioloop)
303
304        local_ip = self.cmd_channel.socket.getsockname()[0]
305        if local_ip in self.cmd_channel.masquerade_address_map:
306            masqueraded_ip = self.cmd_channel.masquerade_address_map[local_ip]
307        elif self.cmd_channel.masquerade_address:
308            masqueraded_ip = self.cmd_channel.masquerade_address
309        else:
310            masqueraded_ip = None
311
312        if self.cmd_channel.server.socket.family != socket.AF_INET:
313            # dual stack IPv4/IPv6 support
314            af = self.bind_af_unspecified((local_ip, 0))
315            self.socket.close()
316        else:
317            af = self.cmd_channel.socket.family
318
319        self.create_socket(af, socket.SOCK_STREAM)
320
321        if self.cmd_channel.passive_ports is None:
322            # By using 0 as port number value we let kernel choose a
323            # free unprivileged random port.
324            self.bind((local_ip, 0))
325        else:
326            ports = list(self.cmd_channel.passive_ports)
327            while ports:
328                port = ports.pop(random.randint(0, len(ports) - 1))
329                self.set_reuse_addr()
330                try:
331                    self.bind((local_ip, port))
332                except socket.error as err:
333                    if err.errno == errno.EADDRINUSE:  # port already in use
334                        if ports:
335                            continue
336                        # If cannot use one of the ports in the configured
337                        # range we'll use a kernel-assigned port, and log
338                        # a message reporting the issue.
339                        # By using 0 as port number value we let kernel
340                        # choose a free unprivileged random port.
341                        else:
342                            self.bind((local_ip, 0))
343                            self.cmd_channel.log(
344                                "Can't find a valid passive port in the "
345                                "configured range. A random kernel-assigned "
346                                "port will be used.",
347                                logfun=logger.warning
348                            )
349                    else:
350                        raise
351                else:
352                    break
353        self.listen(self.backlog or self.cmd_channel.server.backlog)
354
355        port = self.socket.getsockname()[1]
356        if not extmode:
357            ip = masqueraded_ip or local_ip
358            if ip.startswith('::ffff:'):
359                # In this scenario, the server has an IPv6 socket, but
360                # the remote client is using IPv4 and its address is
361                # represented as an IPv4-mapped IPv6 address which
362                # looks like this ::ffff:151.12.5.65, see:
363                # http://en.wikipedia.org/wiki/IPv6#IPv4-mapped_addresses
364                # http://tools.ietf.org/html/rfc3493.html#section-3.7
365                # We truncate the first bytes to make it look like a
366                # common IPv4 address.
367                ip = ip[7:]
368            # The format of 227 response in not standardized.
369            # This is the most expected:
370            resp = '227 Entering passive mode (%s,%d,%d).' % (
371                ip.replace('.', ','), port // 256, port % 256)
372            self.cmd_channel.respond(resp)
373        else:
374            self.cmd_channel.respond('229 Entering extended passive mode '
375                                     '(|||%d|).' % port)
376        if self.timeout:
377            self.call_later(self.timeout, self.handle_timeout)
378
379    # --- connection / overridden
380
381    def handle_accepted(self, sock, addr):
382        """Called when remote client initiates a connection."""
383        if not self.cmd_channel.connected:
384            return self.close()
385
386        # Check the origin of data connection.  If not expressively
387        # configured we drop the incoming data connection if remote
388        # IP address does not match the client's IP address.
389        if self.cmd_channel.remote_ip != addr[0]:
390            if not self.cmd_channel.permit_foreign_addresses:
391                try:
392                    sock.close()
393                except socket.error:
394                    pass
395                msg = '425 Rejected data connection from foreign address ' \
396                      '%s:%s.' % (addr[0], addr[1])
397                self.cmd_channel.respond_w_warning(msg)
398                # do not close listening socket: it couldn't be client's blame
399                return
400            else:
401                # site-to-site FTP allowed
402                msg = 'Established data connection with foreign address ' \
403                      '%s:%s.' % (addr[0], addr[1])
404                self.cmd_channel.log(msg, logfun=logger.warning)
405        # Immediately close the current channel (we accept only one
406        # connection at time) and avoid running out of max connections
407        # limit.
408        self.close()
409        # delegate such connection to DTP handler
410        if self.cmd_channel.connected:
411            handler = self.cmd_channel.dtp_handler(sock, self.cmd_channel)
412            if handler.connected:
413                self.cmd_channel.data_channel = handler
414                self.cmd_channel._on_dtp_connection()
415
416    def handle_timeout(self):
417        if self.cmd_channel.connected:
418            self.cmd_channel.respond("421 Passive data channel timed out.",
419                                     logfun=logger.info)
420        self.close()
421
422    def handle_error(self):
423        """Called to handle any uncaught exceptions."""
424        try:
425            raise
426        except Exception:
427            logger.error(traceback.format_exc())
428        try:
429            self.close()
430        except Exception:
431            logger.critical(traceback.format_exc())
432
433    def close(self):
434        debug("call: close()", inst=self)
435        Acceptor.close(self)
436
437
438class ActiveDTP(Connector):
439    """Connects to remote client and dispatches the resulting connection
440    to DTPHandler. Used for handling PORT command.
441
442     - (int) timeout: the timeout for us to establish connection with
443       the client's listening data socket.
444    """
445    timeout = 30
446
447    def __init__(self, ip, port, cmd_channel):
448        """Initialize the active data channel attemping to connect
449        to remote data socket.
450
451         - (str) ip: the remote IP address.
452         - (int) port: the remote port.
453         - (instance) cmd_channel: the command channel class instance.
454        """
455        Connector.__init__(self, ioloop=cmd_channel.ioloop)
456        self.cmd_channel = cmd_channel
457        self.log = cmd_channel.log
458        self.log_exception = cmd_channel.log_exception
459        self._idler = None
460        if self.timeout:
461            self._idler = self.ioloop.call_later(self.timeout,
462                                                 self.handle_timeout,
463                                                 _errback=self.handle_error)
464
465        if ip.count('.') == 4:
466            self._cmd = "PORT"
467            self._normalized_addr = "%s:%s" % (ip, port)
468        else:
469            self._cmd = "EPRT"
470            self._normalized_addr = "[%s]:%s" % (ip, port)
471
472        source_ip = self.cmd_channel.socket.getsockname()[0]
473        # dual stack IPv4/IPv6 support
474        try:
475            self.connect_af_unspecified((ip, port), (source_ip, 0))
476        except (socket.gaierror, socket.error):
477            self.handle_close()
478
479    def readable(self):
480        return False
481
482    def handle_write(self):
483        # overridden to prevent unhandled read/write event messages to
484        # be printed by asyncore on Python < 2.6
485        pass
486
487    def handle_connect(self):
488        """Called when connection is established."""
489        self.del_channel()
490        if self._idler is not None and not self._idler.cancelled:
491            self._idler.cancel()
492        if not self.cmd_channel.connected:
493            return self.close()
494        # fix for asyncore on python < 2.6, meaning we aren't
495        # actually connected.
496        # test_active_conn_error tests this condition
497        err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
498        if err != 0:
499            raise socket.error(err)
500        #
501        msg = 'Active data connection established.'
502        self.cmd_channel.respond('200 ' + msg)
503        self.cmd_channel.log_cmd(self._cmd, self._normalized_addr, 200, msg)
504        #
505        if not self.cmd_channel.connected:
506            return self.close()
507        # delegate such connection to DTP handler
508        handler = self.cmd_channel.dtp_handler(self.socket, self.cmd_channel)
509        self.cmd_channel.data_channel = handler
510        self.cmd_channel._on_dtp_connection()
511
512    def handle_timeout(self):
513        if self.cmd_channel.connected:
514            msg = "Active data channel timed out."
515            self.cmd_channel.respond("421 " + msg, logfun=logger.info)
516            self.cmd_channel.log_cmd(
517                self._cmd, self._normalized_addr, 421, msg)
518        self.close()
519
520    def handle_close(self):
521        # With the new IO loop, handle_close() gets called in case
522        # the fd appears in the list of exceptional fds.
523        # This means connect() failed.
524        if not self._closed:
525            self.close()
526            if self.cmd_channel.connected:
527                msg = "Can't connect to specified address."
528                self.cmd_channel.respond("425 " + msg)
529                self.cmd_channel.log_cmd(
530                    self._cmd, self._normalized_addr, 425, msg)
531
532    def handle_error(self):
533        """Called to handle any uncaught exceptions."""
534        try:
535            raise
536        except (socket.gaierror, socket.error):
537            pass
538        except Exception:
539            self.log_exception(self)
540        try:
541            self.handle_close()
542        except Exception:
543            logger.critical(traceback.format_exc())
544
545    def close(self):
546        debug("call: close()", inst=self)
547        if not self._closed:
548            Connector.close(self)
549            if self._idler is not None and not self._idler.cancelled:
550                self._idler.cancel()
551
552
553class DTPHandler(AsyncChat):
554    """Class handling server-data-transfer-process (server-DTP, see
555    RFC-959) managing data-transfer operations involving sending
556    and receiving data.
557
558    Class attributes:
559
560     - (int) timeout: the timeout which roughly is the maximum time we
561       permit data transfers to stall for with no progress. If the
562       timeout triggers, the remote client will be kicked off
563       (defaults 300).
564
565     - (int) ac_in_buffer_size: incoming data buffer size (defaults 65536)
566
567     - (int) ac_out_buffer_size: outgoing data buffer size (defaults 65536)
568    """
569
570    timeout = 300
571    ac_in_buffer_size = 65536
572    ac_out_buffer_size = 65536
573
574    def __init__(self, sock, cmd_channel):
575        """Initialize the command channel.
576
577         - (instance) sock: the socket object instance of the newly
578            established connection.
579         - (instance) cmd_channel: the command channel class instance.
580        """
581        self.cmd_channel = cmd_channel
582        self.file_obj = None
583        self.receive = False
584        self.transfer_finished = False
585        self.tot_bytes_sent = 0
586        self.tot_bytes_received = 0
587        self.cmd = None
588        self.log = cmd_channel.log
589        self.log_exception = cmd_channel.log_exception
590        self._data_wrapper = None
591        self._lastdata = 0
592        self._had_cr = False
593        self._start_time = timer()
594        self._resp = ()
595        self._offset = None
596        self._filefd = None
597        self._idler = None
598        self._initialized = False
599        try:
600            AsyncChat.__init__(self, sock, ioloop=cmd_channel.ioloop)
601        except socket.error as err:
602            # if we get an exception here we want the dispatcher
603            # instance to set socket attribute before closing, see:
604            # https://github.com/giampaolo/pyftpdlib/issues/188
605            AsyncChat.__init__(
606                self, socket.socket(), ioloop=cmd_channel.ioloop)
607            # https://github.com/giampaolo/pyftpdlib/issues/143
608            self.close()
609            if err.errno == errno.EINVAL:
610                return
611            self.handle_error()
612            return
613
614        # remove this instance from IOLoop's socket map
615        if not self.connected:
616            self.close()
617            return
618        if self.timeout:
619            self._idler = self.ioloop.call_every(self.timeout,
620                                                 self.handle_timeout,
621                                                 _errback=self.handle_error)
622
623    def __repr__(self):
624        return '<%s(%s)>' % (self.__class__.__name__,
625                             self.cmd_channel.get_repr_info(as_str=True))
626
627    __str__ = __repr__
628
629    def use_sendfile(self):
630        if not self.cmd_channel.use_sendfile:
631            # as per server config
632            return False
633        if self.file_obj is None or not hasattr(self.file_obj, "fileno"):
634            # directory listing or unusual file obj
635            return False
636        if self.cmd_channel._current_type != 'i':
637            # text file transfer (need to transform file content on the fly)
638            return False
639        return True
640
641    def push(self, data):
642        self._initialized = True
643        self.modify_ioloop_events(self.ioloop.WRITE)
644        self._wanted_io_events = self.ioloop.WRITE
645        AsyncChat.push(self, data)
646
647    def push_with_producer(self, producer):
648        self._initialized = True
649        self.modify_ioloop_events(self.ioloop.WRITE)
650        self._wanted_io_events = self.ioloop.WRITE
651        if self.use_sendfile():
652            self._offset = producer.file.tell()
653            self._filefd = self.file_obj.fileno()
654            try:
655                self.initiate_sendfile()
656            except _GiveUpOnSendfile:
657                pass
658            else:
659                self.initiate_send = self.initiate_sendfile
660                return
661        debug("starting transfer using send()", self)
662        AsyncChat.push_with_producer(self, producer)
663
664    def close_when_done(self):
665        asynchat.async_chat.close_when_done(self)
666
667    def initiate_send(self):
668        asynchat.async_chat.initiate_send(self)
669
670    def initiate_sendfile(self):
671        """A wrapper around sendfile."""
672        try:
673            sent = sendfile(self._fileno, self._filefd, self._offset,
674                            self.ac_out_buffer_size)
675        except OSError as err:
676            if err.errno in _ERRNOS_RETRY or err.errno == errno.EBUSY:
677                return
678            elif err.errno in _ERRNOS_DISCONNECTED:
679                self.handle_close()
680            else:
681                if self.tot_bytes_sent == 0:
682                    logger.warning(
683                        "sendfile() failed; falling back on using plain send")
684                    raise _GiveUpOnSendfile
685                else:
686                    raise
687        else:
688            if sent == 0:
689                # this signals the channel that the transfer is completed
690                self.discard_buffers()
691                self.handle_close()
692            else:
693                self._offset += sent
694                self.tot_bytes_sent += sent
695
696    # --- utility methods
697
698    def _posix_ascii_data_wrapper(self, chunk):
699        """The data wrapper used for receiving data in ASCII mode on
700        systems using a single line terminator, handling those cases
701        where CRLF ('\r\n') gets delivered in two chunks.
702        """
703        if self._had_cr:
704            chunk = b'\r' + chunk
705
706        if chunk.endswith(b'\r'):
707            self._had_cr = True
708            chunk = chunk[:-1]
709        else:
710            self._had_cr = False
711
712        return chunk.replace(b'\r\n', b(os.linesep))
713
714    def enable_receiving(self, type, cmd):
715        """Enable receiving of data over the channel. Depending on the
716        TYPE currently in use it creates an appropriate wrapper for the
717        incoming data.
718
719         - (str) type: current transfer type, 'a' (ASCII) or 'i' (binary).
720        """
721        self._initialized = True
722        self.modify_ioloop_events(self.ioloop.READ)
723        self._wanted_io_events = self.ioloop.READ
724        self.cmd = cmd
725        if type == 'a':
726            if os.linesep == '\r\n':
727                self._data_wrapper = None
728            else:
729                self._data_wrapper = self._posix_ascii_data_wrapper
730        elif type == 'i':
731            self._data_wrapper = None
732        else:
733            raise TypeError("unsupported type")
734        self.receive = True
735
736    def get_transmitted_bytes(self):
737        """Return the number of transmitted bytes."""
738        return self.tot_bytes_sent + self.tot_bytes_received
739
740    def get_elapsed_time(self):
741        """Return the transfer elapsed time in seconds."""
742        return timer() - self._start_time
743
744    def transfer_in_progress(self):
745        """Return True if a transfer is in progress, else False."""
746        return self.get_transmitted_bytes() != 0
747
748    # --- connection
749
750    def send(self, data):
751        result = AsyncChat.send(self, data)
752        self.tot_bytes_sent += result
753        return result
754
755    def refill_buffer(self):  # pragma: no cover
756        """Overridden as a fix around http://bugs.python.org/issue1740572
757        (when the producer is consumed, close() was called instead of
758        handle_close()).
759        """
760        while True:
761            if len(self.producer_fifo):
762                p = self.producer_fifo.first()
763                # a 'None' in the producer fifo is a sentinel,
764                # telling us to close the channel.
765                if p is None:
766                    if not self.ac_out_buffer:
767                        self.producer_fifo.pop()
768                        # self.close()
769                        self.handle_close()
770                    return
771                elif isinstance(p, str):
772                    self.producer_fifo.pop()
773                    self.ac_out_buffer += p
774                    return
775                data = p.more()
776                if data:
777                    self.ac_out_buffer = self.ac_out_buffer + data
778                    return
779                else:
780                    self.producer_fifo.pop()
781            else:
782                return
783
784    def handle_read(self):
785        """Called when there is data waiting to be read."""
786        try:
787            chunk = self.recv(self.ac_in_buffer_size)
788        except RetryError:
789            pass
790        except socket.error:
791            self.handle_error()
792        else:
793            self.tot_bytes_received += len(chunk)
794            if not chunk:
795                self.transfer_finished = True
796                # self.close()  # <-- asyncore.recv() already do that...
797                return
798            if self._data_wrapper is not None:
799                chunk = self._data_wrapper(chunk)
800            try:
801                self.file_obj.write(chunk)
802            except OSError as err:
803                raise _FileReadWriteError(err)
804
805    handle_read_event = handle_read  # small speedup
806
807    def readable(self):
808        """Predicate for inclusion in the readable for select()."""
809        # It the channel is not supposed to be receiving but yet it's
810        # in the list of readable events, that means it has been
811        # disconnected, in which case we explicitly close() it.
812        # This is necessary as differently from FTPHandler this channel
813        # is not supposed to be readable/writable at first, meaning the
814        # upper IOLoop might end up calling readable() repeatedly,
815        # hogging CPU resources.
816        if not self.receive and not self._initialized:
817            return self.close()
818        return self.receive
819
820    def writable(self):
821        """Predicate for inclusion in the writable for select()."""
822        return not self.receive and asynchat.async_chat.writable(self)
823
824    def handle_timeout(self):
825        """Called cyclically to check if data trasfer is stalling with
826        no progress in which case the client is kicked off.
827        """
828        if self.get_transmitted_bytes() > self._lastdata:
829            self._lastdata = self.get_transmitted_bytes()
830        else:
831            msg = "Data connection timed out."
832            self._resp = ("421 " + msg, logger.info)
833            self.close()
834            self.cmd_channel.close_when_done()
835
836    def handle_error(self):
837        """Called when an exception is raised and not otherwise handled."""
838        try:
839            raise
840        # an error could occur in case we fail reading / writing
841        # from / to file (e.g. file system gets full)
842        except _FileReadWriteError as err:
843            error = _strerror(err.errno)
844        except Exception:
845            # some other exception occurred;  we don't want to provide
846            # confidential error messages
847            self.log_exception(self)
848            error = "Internal error"
849        try:
850            self._resp = ("426 %s; transfer aborted." % error, logger.warning)
851            self.close()
852        except Exception:
853            logger.critical(traceback.format_exc())
854
855    def handle_close(self):
856        """Called when the socket is closed."""
857        # If we used channel for receiving we assume that transfer is
858        # finished when client closes the connection, if we used channel
859        # for sending we have to check that all data has been sent
860        # (responding with 226) or not (responding with 426).
861        # In both cases handle_close() is automatically called by the
862        # underlying asynchat module.
863        if not self._closed:
864            if self.receive:
865                self.transfer_finished = True
866            else:
867                self.transfer_finished = len(self.producer_fifo) == 0
868            try:
869                if self.transfer_finished:
870                    self._resp = ("226 Transfer complete.", logger.debug)
871                else:
872                    tot_bytes = self.get_transmitted_bytes()
873                    self._resp = ("426 Transfer aborted; %d bytes transmitted."
874                                  % tot_bytes, logger.debug)
875            finally:
876                self.close()
877
878    def close(self):
879        """Close the data channel, first attempting to close any remaining
880        file handles."""
881        debug("call: close()", inst=self)
882        if not self._closed:
883            # RFC-959 says we must close the connection before replying
884            AsyncChat.close(self)
885
886            # Close file object before responding successfully to client
887            if self.file_obj is not None and not self.file_obj.closed:
888                self.file_obj.close()
889
890            if self._resp:
891                self.cmd_channel.respond(self._resp[0], logfun=self._resp[1])
892
893            if self._idler is not None and not self._idler.cancelled:
894                self._idler.cancel()
895            if self.file_obj is not None:
896                filename = self.file_obj.name
897                elapsed_time = round(self.get_elapsed_time(), 3)
898                self.cmd_channel.log_transfer(
899                    cmd=self.cmd,
900                    filename=self.file_obj.name,
901                    receive=self.receive,
902                    completed=self.transfer_finished,
903                    elapsed=elapsed_time,
904                    bytes=self.get_transmitted_bytes())
905                if self.transfer_finished:
906                    if self.receive:
907                        self.cmd_channel.on_file_received(filename)
908                    else:
909                        self.cmd_channel.on_file_sent(filename)
910                else:
911                    if self.receive:
912                        self.cmd_channel.on_incomplete_file_received(filename)
913                    else:
914                        self.cmd_channel.on_incomplete_file_sent(filename)
915            self.cmd_channel._on_dtp_close()
916
917
918# dirty hack in order to turn AsyncChat into a new style class in
919# python 2.x so that we can use super()
920if PY3:
921    class _AsyncChatNewStyle(AsyncChat):
922        pass
923else:
924    class _AsyncChatNewStyle(object, AsyncChat):
925
926        def __init__(self, *args, **kwargs):
927            super(object, self).__init__(*args, **kwargs)  # bypass object
928
929
930class ThrottledDTPHandler(_AsyncChatNewStyle, DTPHandler):
931    """A DTPHandler subclass which wraps sending and receiving in a data
932    counter and temporarily "sleeps" the channel so that you burst to no
933    more than x Kb/sec average.
934
935     - (int) read_limit: the maximum number of bytes to read (receive)
936       in one second (defaults to 0 == no limit).
937
938     - (int) write_limit: the maximum number of bytes to write (send)
939       in one second (defaults to 0 == no limit).
940
941     - (bool) auto_sized_buffers: this option only applies when read
942       and/or write limits are specified. When enabled it bumps down
943       the data buffer sizes so that they are never greater than read
944       and write limits which results in a less bursty and smoother
945       throughput (default: True).
946    """
947    read_limit = 0
948    write_limit = 0
949    auto_sized_buffers = True
950
951    def __init__(self, sock, cmd_channel):
952        super(ThrottledDTPHandler, self).__init__(sock, cmd_channel)
953        self._timenext = 0
954        self._datacount = 0
955        self.sleeping = False
956        self._throttler = None
957        if self.auto_sized_buffers:
958            if self.read_limit:
959                while self.ac_in_buffer_size > self.read_limit:
960                    self.ac_in_buffer_size /= 2
961            if self.write_limit:
962                while self.ac_out_buffer_size > self.write_limit:
963                    self.ac_out_buffer_size /= 2
964        self.ac_in_buffer_size = int(self.ac_in_buffer_size)
965        self.ac_out_buffer_size = int(self.ac_out_buffer_size)
966
967    def __repr__(self):
968        return DTPHandler.__repr__(self)
969
970    def use_sendfile(self):
971        return False
972
973    def recv(self, buffer_size):
974        chunk = super(ThrottledDTPHandler, self).recv(buffer_size)
975        if self.read_limit:
976            self._throttle_bandwidth(len(chunk), self.read_limit)
977        return chunk
978
979    def send(self, data):
980        num_sent = super(ThrottledDTPHandler, self).send(data)
981        if self.write_limit:
982            self._throttle_bandwidth(num_sent, self.write_limit)
983        return num_sent
984
985    def _cancel_throttler(self):
986        if self._throttler is not None and not self._throttler.cancelled:
987            self._throttler.cancel()
988
989    def _throttle_bandwidth(self, len_chunk, max_speed):
990        """A method which counts data transmitted so that you burst to
991        no more than x Kb/sec average.
992        """
993        self._datacount += len_chunk
994        if self._datacount >= max_speed:
995            self._datacount = 0
996            now = timer()
997            sleepfor = (self._timenext - now) * 2
998            if sleepfor > 0:
999                # we've passed bandwidth limits
1000                def unsleep():
1001                    if self.receive:
1002                        event = self.ioloop.READ
1003                    else:
1004                        event = self.ioloop.WRITE
1005                    self.add_channel(events=event)
1006
1007                self.del_channel()
1008                self._cancel_throttler()
1009                self._throttler = self.ioloop.call_later(
1010                    sleepfor, unsleep, _errback=self.handle_error)
1011            self._timenext = now + 1
1012
1013    def close(self):
1014        self._cancel_throttler()
1015        super(ThrottledDTPHandler, self).close()
1016
1017
1018# --- producers
1019
1020
1021class FileProducer(object):
1022    """Producer wrapper for file[-like] objects."""
1023
1024    buffer_size = 65536
1025
1026    def __init__(self, file, type):
1027        """Initialize the producer with a data_wrapper appropriate to TYPE.
1028
1029         - (file) file: the file[-like] object.
1030         - (str) type: the current TYPE, 'a' (ASCII) or 'i' (binary).
1031        """
1032        self.file = file
1033        self.type = type
1034        self._prev_chunk_endswith_cr = False
1035        if type == 'a' and os.linesep != '\r\n':
1036            self._data_wrapper = self._posix_ascii_data_wrapper
1037        else:
1038            self._data_wrapper = None
1039
1040    def _posix_ascii_data_wrapper(self, chunk):
1041        """The data wrapper used for sending data in ASCII mode on
1042        systems using a single line terminator, handling those cases
1043        where CRLF ('\r\n') gets delivered in two chunks.
1044        """
1045        chunk = bytearray(chunk)
1046        pos = 0
1047        if self._prev_chunk_endswith_cr and chunk.startswith(b'\n'):
1048            pos += 1
1049        while True:
1050            pos = chunk.find(b'\n', pos)
1051            if pos == -1:
1052                break
1053            if chunk[pos - 1] != CR_BYTE:
1054                chunk.insert(pos, CR_BYTE)
1055                pos += 1
1056            pos += 1
1057        self._prev_chunk_endswith_cr = chunk.endswith(b'\r')
1058        return chunk
1059
1060    def more(self):
1061        """Attempt a chunk of data of size self.buffer_size."""
1062        try:
1063            data = self.file.read(self.buffer_size)
1064        except OSError as err:
1065            raise _FileReadWriteError(err)
1066        else:
1067            if self._data_wrapper is not None:
1068                data = self._data_wrapper(data)
1069            return data
1070
1071
1072class BufferedIteratorProducer(object):
1073    """Producer for iterator objects with buffer capabilities."""
1074    # how many times iterator.next() will be called before
1075    # returning some data
1076    loops = 20
1077
1078    def __init__(self, iterator):
1079        self.iterator = iterator
1080
1081    def more(self):
1082        """Attempt a chunk of data from iterator by calling
1083        its next() method different times.
1084        """
1085        buffer = []
1086        for x in xrange(self.loops):
1087            try:
1088                buffer.append(next(self.iterator))
1089            except StopIteration:
1090                break
1091        return b''.join(buffer)
1092
1093
1094# --- FTP
1095
1096class FTPHandler(AsyncChat):
1097    """Implements the FTP server Protocol Interpreter (see RFC-959),
1098    handling commands received from the client on the control channel.
1099
1100    All relevant session information is stored in class attributes
1101    reproduced below and can be modified before instantiating this
1102    class.
1103
1104     - (int) timeout:
1105       The timeout which is the maximum time a remote client may spend
1106       between FTP commands. If the timeout triggers, the remote client
1107       will be kicked off.  Defaults to 300 seconds.
1108
1109     - (str) banner: the string sent when client connects.
1110
1111     - (int) max_login_attempts:
1112        the maximum number of wrong authentications before disconnecting
1113        the client (default 3).
1114
1115     - (bool)permit_foreign_addresses:
1116        FTP site-to-site transfer feature: also referenced as "FXP" it
1117        permits for transferring a file between two remote FTP servers
1118        without the transfer going through the client's host (not
1119        recommended for security reasons as described in RFC-2577).
1120        Having this attribute set to False means that all data
1121        connections from/to remote IP addresses which do not match the
1122        client's IP address will be dropped (defualt False).
1123
1124     - (bool) permit_privileged_ports:
1125        set to True if you want to permit active data connections (PORT)
1126        over privileged ports (not recommended, defaulting to False).
1127
1128     - (str) masquerade_address:
1129        the "masqueraded" IP address to provide along PASV reply when
1130        pyftpdlib is running behind a NAT or other types of gateways.
1131        When configured pyftpdlib will hide its local address and
1132        instead use the public address of your NAT (default None).
1133
1134     - (dict) masquerade_address_map:
1135        in case the server has multiple IP addresses which are all
1136        behind a NAT router, you may wish to specify individual
1137        masquerade_addresses for each of them. The map expects a
1138        dictionary containing private IP addresses as keys, and their
1139        corresponding public (masquerade) addresses as values.
1140
1141     - (list) passive_ports:
1142        what ports the ftpd will use for its passive data transfers.
1143        Value expected is a list of integers (e.g. range(60000, 65535)).
1144        When configured pyftpdlib will no longer use kernel-assigned
1145        random ports (default None).
1146
1147     - (bool) use_gmt_times:
1148        when True causes the server to report all ls and MDTM times in
1149        GMT and not local time (default True).
1150
1151     - (bool) use_sendfile: when True uses sendfile() system call to
1152        send a file resulting in faster uploads (from server to client).
1153        Works on UNIX only and requires pysendfile module to be
1154        installed separately:
1155        https://github.com/giampaolo/pysendfile/
1156        Automatically defaults to True if pysendfile module is
1157        installed.
1158
1159     - (bool) tcp_no_delay: controls the use of the TCP_NODELAY socket
1160        option which disables the Nagle algorithm resulting in
1161        significantly better performances (default True on all systems
1162        where it is supported).
1163
1164     - (str) unicode_errors:
1165       the error handler passed to ''.encode() and ''.decode():
1166       http://docs.python.org/library/stdtypes.html#str.decode
1167       (detaults to 'replace').
1168
1169     - (str) log_prefix:
1170       the prefix string preceding any log line; all instance
1171       attributes can be used as arguments.
1172
1173
1174    All relevant instance attributes initialized when client connects
1175    are reproduced below.  You may be interested in them in case you
1176    want to subclass the original FTPHandler.
1177
1178     - (bool) authenticated: True if client authenticated himself.
1179     - (str) username: the name of the connected user (if any).
1180     - (int) attempted_logins: number of currently attempted logins.
1181     - (str) current_type: the current transfer type (default "a")
1182     - (int) af: the connection's address family (IPv4/IPv6)
1183     - (instance) server: the FTPServer class instance.
1184     - (instance) data_channel: the data channel instance (if any).
1185    """
1186    # these are overridable defaults
1187
1188    # default classes
1189    authorizer = DummyAuthorizer()
1190    active_dtp = ActiveDTP
1191    passive_dtp = PassiveDTP
1192    dtp_handler = DTPHandler
1193    abstracted_fs = AbstractedFS
1194    proto_cmds = proto_cmds
1195
1196    # session attributes (explained in the docstring)
1197    timeout = 300
1198    banner = "pyftpdlib %s ready." % __ver__
1199    max_login_attempts = 3
1200    permit_foreign_addresses = False
1201    permit_privileged_ports = False
1202    masquerade_address = None
1203    masquerade_address_map = {}
1204    passive_ports = None
1205    use_gmt_times = True
1206    use_sendfile = sendfile is not None
1207    tcp_no_delay = hasattr(socket, "TCP_NODELAY")
1208    unicode_errors = 'replace'
1209    log_prefix = '%(remote_ip)s:%(remote_port)s-[%(username)s]'
1210    auth_failed_timeout = 3
1211
1212    def __init__(self, conn, server, ioloop=None):
1213        """Initialize the command channel.
1214
1215         - (instance) conn: the socket object instance of the newly
1216            established connection.
1217         - (instance) server: the ftp server class instance.
1218        """
1219        # public session attributes
1220        self.server = server
1221        self.fs = None
1222        self.authenticated = False
1223        self.username = ""
1224        self.password = ""
1225        self.attempted_logins = 0
1226        self.data_channel = None
1227        self.remote_ip = ""
1228        self.remote_port = ""
1229        self.started = time.time()
1230
1231        # private session attributes
1232        self._last_response = ""
1233        self._current_type = 'a'
1234        self._restart_position = 0
1235        self._quit_pending = False
1236        self._in_buffer = []
1237        self._in_buffer_len = 0
1238        self._epsvall = False
1239        self._dtp_acceptor = None
1240        self._dtp_connector = None
1241        self._in_dtp_queue = None
1242        self._out_dtp_queue = None
1243        self._extra_feats = []
1244        self._current_facts = ['type', 'perm', 'size', 'modify']
1245        self._rnfr = None
1246        self._idler = None
1247        self._log_debug = logging.getLogger('pyftpdlib').getEffectiveLevel() \
1248            <= logging.DEBUG
1249
1250        if os.name == 'posix':
1251            self._current_facts.append('unique')
1252        self._available_facts = self._current_facts[:]
1253        if pwd and grp:
1254            self._available_facts += ['unix.mode', 'unix.uid', 'unix.gid']
1255        if os.name == 'nt':
1256            self._available_facts.append('create')
1257
1258        try:
1259            AsyncChat.__init__(self, conn, ioloop=ioloop)
1260        except socket.error as err:
1261            # if we get an exception here we want the dispatcher
1262            # instance to set socket attribute before closing, see:
1263            # https://github.com/giampaolo/pyftpdlib/issues/188
1264            AsyncChat.__init__(self, socket.socket(), ioloop=ioloop)
1265            self.close()
1266            debug("call: FTPHandler.__init__, err %r" % err, self)
1267            if err.errno == errno.EINVAL:
1268                # https://github.com/giampaolo/pyftpdlib/issues/143
1269                return
1270            self.handle_error()
1271            return
1272        self.set_terminator(b"\r\n")
1273
1274        # connection properties
1275        try:
1276            self.remote_ip, self.remote_port = self.socket.getpeername()[:2]
1277        except socket.error as err:
1278            debug("call: FTPHandler.__init__, err on getpeername() %r" % err,
1279                  self)
1280            # A race condition  may occur if the other end is closing
1281            # before we can get the peername, hence ENOTCONN (see issue
1282            # #100) while EINVAL can occur on OSX (see issue #143).
1283            self.connected = False
1284            if err.errno in (errno.ENOTCONN, errno.EINVAL):
1285                self.close()
1286            else:
1287                self.handle_error()
1288            return
1289        else:
1290            self.log("FTP session opened (connect)")
1291
1292        # try to handle urgent data inline
1293        try:
1294            self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_OOBINLINE, 1)
1295        except socket.error as err:
1296            debug("call: FTPHandler.__init__, err on SO_OOBINLINE %r" % err,
1297                  self)
1298
1299        # disable Nagle algorithm for the control socket only, resulting
1300        # in significantly better performances
1301        if self.tcp_no_delay:
1302            try:
1303                self.socket.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1)
1304            except socket.error as err:
1305                debug(
1306                    "call: FTPHandler.__init__, err on TCP_NODELAY %r" % err,
1307                    self)
1308
1309        # remove this instance from IOLoop's socket_map
1310        if not self.connected:
1311            self.close()
1312            return
1313
1314        if self.timeout:
1315            self._idler = self.ioloop.call_later(
1316                self.timeout, self.handle_timeout, _errback=self.handle_error)
1317
1318    def get_repr_info(self, as_str=False, extra_info={}):
1319        info = OrderedDict()
1320        info['id'] = id(self)
1321        info['addr'] = "%s:%s" % (self.remote_ip, self.remote_port)
1322        if _is_ssl_sock(self.socket):
1323            info['ssl'] = True
1324        if self.username:
1325            info['user'] = self.username
1326        # If threads are involved sometimes "self" may be None (?!?).
1327        dc = getattr(self, 'data_channel', None)
1328        if dc is not None:
1329            if _is_ssl_sock(dc.socket):
1330                info['ssl-data'] = True
1331            if dc.file_obj:
1332                if self.data_channel.receive:
1333                    info['sending-file'] = dc.file_obj
1334                    if dc.use_sendfile():
1335                        info['use-sendfile(2)'] = True
1336                else:
1337                    info['receiving-file'] = dc.file_obj
1338                info['bytes-trans'] = dc.get_transmitted_bytes()
1339        info.update(extra_info)
1340        if as_str:
1341            return ', '.join(['%s=%r' % (k, v) for (k, v) in info.items()])
1342        return info
1343
1344    def __repr__(self):
1345        return '<%s(%s)>' % (self.__class__.__name__, self.get_repr_info(True))
1346
1347    __str__ = __repr__
1348
1349    def handle(self):
1350        """Return a 220 'ready' response to the client over the command
1351        channel.
1352        """
1353        self.on_connect()
1354        if not self._closed and not self._closing:
1355            if len(self.banner) <= 75:
1356                self.respond("220 %s" % str(self.banner))
1357            else:
1358                self.push('220-%s\r\n' % str(self.banner))
1359                self.respond('220 ')
1360
1361    def handle_max_cons(self):
1362        """Called when limit for maximum number of connections is reached."""
1363        msg = "421 Too many connections. Service temporarily unavailable."
1364        self.respond_w_warning(msg)
1365        # If self.push is used, data could not be sent immediately in
1366        # which case a new "loop" will occur exposing us to the risk of
1367        # accepting new connections.  Since this could cause asyncore to
1368        # run out of fds in case we're using select() on Windows  we
1369        # immediately close the channel by using close() instead of
1370        # close_when_done(). If data has not been sent yet client will
1371        # be silently disconnected.
1372        self.close()
1373
1374    def handle_max_cons_per_ip(self):
1375        """Called when too many clients are connected from the same IP."""
1376        msg = "421 Too many connections from the same IP address."
1377        self.respond_w_warning(msg)
1378        self.close_when_done()
1379
1380    def handle_timeout(self):
1381        """Called when client does not send any command within the time
1382        specified in <timeout> attribute."""
1383        msg = "Control connection timed out."
1384        self.respond("421 " + msg, logfun=logger.info)
1385        self.close_when_done()
1386
1387    # --- asyncore / asynchat overridden methods
1388
1389    def readable(self):
1390        # Checking for self.connected seems to be necessary as per:
1391        # https://github.com/giampaolo/pyftpdlib/issues/188#c18
1392        # In contrast to DTPHandler, here we are not interested in
1393        # attempting to receive any further data from a closed socket.
1394        return self.connected and AsyncChat.readable(self)
1395
1396    def writable(self):
1397        return self.connected and AsyncChat.writable(self)
1398
1399    def collect_incoming_data(self, data):
1400        """Read incoming data and append to the input buffer."""
1401        self._in_buffer.append(data)
1402        self._in_buffer_len += len(data)
1403        # Flush buffer if it gets too long (possible DoS attacks).
1404        # RFC-959 specifies that a 500 response could be given in
1405        # such cases
1406        buflimit = 2048
1407        if self._in_buffer_len > buflimit:
1408            self.respond_w_warning('500 Command too long.')
1409            self._in_buffer = []
1410            self._in_buffer_len = 0
1411
1412    def decode(self, bytes):
1413        return bytes.decode('utf8', self.unicode_errors)
1414
1415    def found_terminator(self):
1416        r"""Called when the incoming data stream matches the \r\n
1417        terminator.
1418        """
1419        if self._idler is not None and not self._idler.cancelled:
1420            self._idler.reset()
1421
1422        line = b''.join(self._in_buffer)
1423        try:
1424            line = self.decode(line)
1425        except UnicodeDecodeError:
1426            # By default we'll never get here as we replace errors
1427            # but user might want to override this behavior.
1428            # RFC-2640 doesn't mention what to do in this case so
1429            # we'll just return 501 (bad arg).
1430            return self.respond("501 Can't decode command.")
1431
1432        self._in_buffer = []
1433        self._in_buffer_len = 0
1434
1435        cmd = line.split(' ')[0].upper()
1436        arg = line[len(cmd) + 1:]
1437        try:
1438            self.pre_process_command(line, cmd, arg)
1439        except UnicodeEncodeError:
1440            self.respond("501 can't decode path (server filesystem encoding "
1441                         "is %s)" % sys.getfilesystemencoding())
1442
1443    def pre_process_command(self, line, cmd, arg):
1444        kwargs = {}
1445        if cmd == "SITE" and arg:
1446            cmd = "SITE %s" % arg.split(' ')[0].upper()
1447            arg = line[len(cmd) + 1:]
1448
1449        if cmd != 'PASS':
1450            self.logline("<- %s" % line)
1451        else:
1452            self.logline("<- %s %s" % (line.split(' ')[0], '*' * 6))
1453
1454        # Recognize those commands having a "special semantic". They
1455        # should be sent by following the RFC-959 procedure of sending
1456        # Telnet IP/Synch sequence (chr 242 and 255) as OOB data but
1457        # since many ftp clients don't do it correctly we check the
1458        # last 4 characters only.
1459        if cmd not in self.proto_cmds:
1460            if cmd[-4:] in ('ABOR', 'STAT', 'QUIT'):
1461                cmd = cmd[-4:]
1462            else:
1463                msg = 'Command "%s" not understood.' % cmd
1464                self.respond('500 ' + msg)
1465                if cmd:
1466                    self.log_cmd(cmd, arg, 500, msg)
1467                return
1468
1469        if not arg and self.proto_cmds[cmd]['arg'] == True:  # NOQA
1470            msg = "Syntax error: command needs an argument."
1471            self.respond("501 " + msg)
1472            self.log_cmd(cmd, "", 501, msg)
1473            return
1474        if arg and self.proto_cmds[cmd]['arg'] == False:  # NOQA
1475            msg = "Syntax error: command does not accept arguments."
1476            self.respond("501 " + msg)
1477            self.log_cmd(cmd, arg, 501, msg)
1478            return
1479
1480        if not self.authenticated:
1481            if self.proto_cmds[cmd]['auth'] or (cmd == 'STAT' and arg):
1482                msg = "Log in with USER and PASS first."
1483                self.respond("530 " + msg)
1484                self.log_cmd(cmd, arg, 530, msg)
1485            else:
1486                # call the proper ftp_* method
1487                self.process_command(cmd, arg)
1488                return
1489        else:
1490            if (cmd == 'STAT') and not arg:
1491                self.ftp_STAT(u(''))
1492                return
1493
1494            # for file-system related commands check whether real path
1495            # destination is valid
1496            if self.proto_cmds[cmd]['perm'] and (cmd != 'STOU'):
1497                if cmd in ('CWD', 'XCWD'):
1498                    arg = self.fs.ftp2fs(arg or u('/'))
1499                elif cmd in ('CDUP', 'XCUP'):
1500                    arg = self.fs.ftp2fs(u('..'))
1501                elif cmd == 'LIST':
1502                    if arg.lower() in ('-a', '-l', '-al', '-la'):
1503                        arg = self.fs.ftp2fs(self.fs.cwd)
1504                    else:
1505                        arg = self.fs.ftp2fs(arg or self.fs.cwd)
1506                elif cmd == 'STAT':
1507                    if glob.has_magic(arg):
1508                        msg = 'Globbing not supported.'
1509                        self.respond('550 ' + msg)
1510                        self.log_cmd(cmd, arg, 550, msg)
1511                        return
1512                    arg = self.fs.ftp2fs(arg or self.fs.cwd)
1513                elif cmd == 'SITE CHMOD':
1514                    if ' ' not in arg:
1515                        msg = "Syntax error: command needs two arguments."
1516                        self.respond("501 " + msg)
1517                        self.log_cmd(cmd, "", 501, msg)
1518                        return
1519                    else:
1520                        mode, arg = arg.split(' ', 1)
1521                        arg = self.fs.ftp2fs(arg)
1522                        kwargs = dict(mode=mode)
1523                elif cmd == 'MFMT':
1524                    if ' ' not in arg:
1525                        msg = "Syntax error: command needs two arguments."
1526                        self.respond("501 " + msg)
1527                        self.log_cmd(cmd, "", 501, msg)
1528                        return
1529                    else:
1530                        timeval, arg = arg.split(' ', 1)
1531                        arg = self.fs.ftp2fs(arg)
1532                        kwargs = dict(timeval=timeval)
1533
1534                else:  # LIST, NLST, MLSD, MLST
1535                    arg = self.fs.ftp2fs(arg or self.fs.cwd)
1536
1537                if not self.fs.validpath(arg):
1538                    line = self.fs.fs2ftp(arg)
1539                    msg = '"%s" points to a path which is outside ' \
1540                          "the user's root directory" % line
1541                    self.respond("550 %s." % msg)
1542                    self.log_cmd(cmd, arg, 550, msg)
1543                    return
1544
1545            # check permission
1546            perm = self.proto_cmds[cmd]['perm']
1547            if perm is not None and cmd != 'STOU':
1548                if not self.authorizer.has_perm(self.username, perm, arg):
1549                    msg = "Not enough privileges."
1550                    self.respond("550 " + msg)
1551                    self.log_cmd(cmd, arg, 550, msg)
1552                    return
1553
1554            # call the proper ftp_* method
1555            self.process_command(cmd, arg, **kwargs)
1556
1557    def process_command(self, cmd, *args, **kwargs):
1558        """Process command by calling the corresponding ftp_* class
1559        method (e.g. for received command "MKD pathname", ftp_MKD()
1560        method is called with "pathname" as the argument).
1561        """
1562        if self._closed:
1563            return
1564        self._last_response = ""
1565        method = getattr(self, 'ftp_' + cmd.replace(' ', '_'))
1566        method(*args, **kwargs)
1567        if self._last_response:
1568            code = int(self._last_response[:3])
1569            resp = self._last_response[4:]
1570            self.log_cmd(cmd, args[0], code, resp)
1571
1572    def handle_error(self):
1573        try:
1574            self.log_exception(self)
1575            self.close()
1576        except Exception:
1577            logger.critical(traceback.format_exc())
1578
1579    def handle_close(self):
1580        self.close()
1581
1582    def close(self):
1583        """Close the current channel disconnecting the client."""
1584        debug("call: close()", inst=self)
1585        if not self._closed:
1586            AsyncChat.close(self)
1587
1588            self._shutdown_connecting_dtp()
1589
1590            if self.data_channel is not None:
1591                self.data_channel.close()
1592                del self.data_channel
1593
1594            if self._out_dtp_queue is not None:
1595                file = self._out_dtp_queue[2]
1596                if file is not None:
1597                    file.close()
1598            if self._in_dtp_queue is not None:
1599                file = self._in_dtp_queue[0]
1600                if file is not None:
1601                    file.close()
1602
1603            del self._out_dtp_queue
1604            del self._in_dtp_queue
1605
1606            if self._idler is not None and not self._idler.cancelled:
1607                self._idler.cancel()
1608
1609            # remove client IP address from ip map
1610            if self.remote_ip in self.server.ip_map:
1611                self.server.ip_map.remove(self.remote_ip)
1612
1613            if self.fs is not None:
1614                self.fs.cmd_channel = None
1615                self.fs = None
1616            self.log("FTP session closed (disconnect).")
1617            # Having self.remote_ip not set means that no connection
1618            # actually took place, hence we're not interested in
1619            # invoking the callback.
1620            if self.remote_ip:
1621                self.ioloop.call_later(0, self.on_disconnect,
1622                                       _errback=self.handle_error)
1623
1624    def _shutdown_connecting_dtp(self):
1625        """Close any ActiveDTP or PassiveDTP instance waiting to
1626        establish a connection (passive or active).
1627        """
1628        if self._dtp_acceptor is not None:
1629            self._dtp_acceptor.close()
1630            self._dtp_acceptor = None
1631        if self._dtp_connector is not None:
1632            self._dtp_connector.close()
1633            self._dtp_connector = None
1634
1635    # --- public callbacks
1636    # Note: to run a time consuming task make sure to use a separate
1637    # process or thread (see FAQs).
1638
1639    def on_connect(self):
1640        """Called when client connects, *before* sending the initial
1641        220 reply.
1642        """
1643
1644    def on_disconnect(self):
1645        """Called when connection is closed."""
1646
1647    def on_login(self, username):
1648        """Called on user login."""
1649
1650    def on_login_failed(self, username, password):
1651        """Called on failed login attempt.
1652        At this point client might have already been disconnected if it
1653        failed too many times.
1654        """
1655
1656    def on_logout(self, username):
1657        """Called when user "cleanly" logs out due to QUIT or USER
1658        issued twice (re-login). This is not called if the connection
1659        is simply closed by client.
1660        """
1661
1662    def on_file_sent(self, file):
1663        """Called every time a file has been succesfully sent.
1664        "file" is the absolute name of the file just being sent.
1665        """
1666
1667    def on_file_received(self, file):
1668        """Called every time a file has been succesfully received.
1669        "file" is the absolute name of the file just being received.
1670        """
1671
1672    def on_incomplete_file_sent(self, file):
1673        """Called every time a file has not been entirely sent.
1674        (e.g. ABOR during transfer or client disconnected).
1675        "file" is the absolute name of that file.
1676        """
1677
1678    def on_incomplete_file_received(self, file):
1679        """Called every time a file has not been entirely received
1680        (e.g. ABOR during transfer or client disconnected).
1681        "file" is the absolute name of that file.
1682        """
1683
1684    # --- internal callbacks
1685
1686    def _on_dtp_connection(self):
1687        """Called every time data channel connects, either active or
1688        passive.
1689
1690        Incoming and outgoing queues are checked for pending data.
1691        If outbound data is pending, it is pushed into the data channel.
1692        If awaiting inbound data, the data channel is enabled for
1693        receiving.
1694        """
1695        # Close accepting DTP only. By closing ActiveDTP DTPHandler
1696        # would receive a closed socket object.
1697        # self._shutdown_connecting_dtp()
1698        if self._dtp_acceptor is not None:
1699            self._dtp_acceptor.close()
1700            self._dtp_acceptor = None
1701
1702        # stop the idle timer as long as the data transfer is not finished
1703        if self._idler is not None and not self._idler.cancelled:
1704            self._idler.cancel()
1705
1706        # check for data to send
1707        if self._out_dtp_queue is not None:
1708            data, isproducer, file, cmd = self._out_dtp_queue
1709            self._out_dtp_queue = None
1710            self.data_channel.cmd = cmd
1711            if file:
1712                self.data_channel.file_obj = file
1713            try:
1714                if not isproducer:
1715                    self.data_channel.push(data)
1716                else:
1717                    self.data_channel.push_with_producer(data)
1718                if self.data_channel is not None:
1719                    self.data_channel.close_when_done()
1720            except Exception:
1721                # dealing with this exception is up to DTP (see bug #84)
1722                self.data_channel.handle_error()
1723
1724        # check for data to receive
1725        elif self._in_dtp_queue is not None:
1726            file, cmd = self._in_dtp_queue
1727            self.data_channel.file_obj = file
1728            self._in_dtp_queue = None
1729            self.data_channel.enable_receiving(self._current_type, cmd)
1730
1731    def _on_dtp_close(self):
1732        """Called every time the data channel is closed."""
1733        self.data_channel = None
1734        if self._quit_pending:
1735            self.close()
1736        elif self.timeout:
1737            # data transfer finished, restart the idle timer
1738            if self._idler is not None and not self._idler.cancelled:
1739                self._idler.cancel()
1740            self._idler = self.ioloop.call_later(
1741                self.timeout, self.handle_timeout, _errback=self.handle_error)
1742
1743    # --- utility
1744
1745    def push(self, s):
1746        asynchat.async_chat.push(self, s.encode('utf8'))
1747
1748    def respond(self, resp, logfun=logger.debug):
1749        """Send a response to the client using the command channel."""
1750        self._last_response = resp
1751        self.push(resp + '\r\n')
1752        if self._log_debug:
1753            self.logline('-> %s' % resp, logfun=logfun)
1754        else:
1755            self.log(resp[4:], logfun=logfun)
1756
1757    def respond_w_warning(self, resp):
1758        self.respond(resp, logfun=logger.warning)
1759
1760    def push_dtp_data(self, data, isproducer=False, file=None, cmd=None):
1761        """Pushes data into the data channel.
1762
1763        It is usually called for those commands requiring some data to
1764        be sent over the data channel (e.g. RETR).
1765        If data channel does not exist yet, it queues the data to send
1766        later; data will then be pushed into data channel when
1767        _on_dtp_connection() will be called.
1768
1769         - (str/classobj) data: the data to send which may be a string
1770            or a producer object).
1771         - (bool) isproducer: whether treat data as a producer.
1772         - (file) file: the file[-like] object to send (if any).
1773        """
1774        if self.data_channel is not None:
1775            self.respond(
1776                "125 Data connection already open. Transfer starting.")
1777            if file:
1778                self.data_channel.file_obj = file
1779            try:
1780                if not isproducer:
1781                    self.data_channel.push(data)
1782                else:
1783                    self.data_channel.push_with_producer(data)
1784                if self.data_channel is not None:
1785                    self.data_channel.cmd = cmd
1786                    self.data_channel.close_when_done()
1787            except Exception:
1788                # dealing with this exception is up to DTP (see bug #84)
1789                self.data_channel.handle_error()
1790        else:
1791            self.respond(
1792                "150 File status okay. About to open data connection.")
1793            self._out_dtp_queue = (data, isproducer, file, cmd)
1794
1795    def flush_account(self):
1796        """Flush account information by clearing attributes that need
1797        to be reset on a REIN or new USER command.
1798        """
1799        self._shutdown_connecting_dtp()
1800        # if there's a transfer in progress RFC-959 states we are
1801        # supposed to let it finish
1802        if self.data_channel is not None:
1803            if not self.data_channel.transfer_in_progress():
1804                self.data_channel.close()
1805                self.data_channel = None
1806
1807        username = self.username
1808        if self.authenticated and username:
1809            self.on_logout(username)
1810        self.authenticated = False
1811        self.username = ""
1812        self.password = ""
1813        self.attempted_logins = 0
1814        self._current_type = 'a'
1815        self._restart_position = 0
1816        self._quit_pending = False
1817        self._in_dtp_queue = None
1818        self._rnfr = None
1819        self._out_dtp_queue = None
1820
1821    def run_as_current_user(self, function, *args, **kwargs):
1822        """Execute a function impersonating the current logged-in user."""
1823        self.authorizer.impersonate_user(self.username, self.password)
1824        try:
1825            return function(*args, **kwargs)
1826        finally:
1827            self.authorizer.terminate_impersonation(self.username)
1828
1829    # --- logging wrappers
1830
1831    # this is defined earlier
1832    # log_prefix = '%(remote_ip)s:%(remote_port)s-[%(username)s]'
1833
1834    def log(self, msg, logfun=logger.info):
1835        """Log a message, including additional identifying session data."""
1836        prefix = self.log_prefix % self.__dict__
1837        logfun("%s %s" % (prefix, msg))
1838
1839    def logline(self, msg, logfun=logger.debug):
1840        """Log a line including additional indentifying session data.
1841        By default this is disabled unless logging level == DEBUG.
1842        """
1843        if self._log_debug:
1844            prefix = self.log_prefix % self.__dict__
1845            logfun("%s %s" % (prefix, msg))
1846
1847    def logerror(self, msg):
1848        """Log an error including additional indentifying session data."""
1849        prefix = self.log_prefix % self.__dict__
1850        logger.error("%s %s" % (prefix, msg))
1851
1852    def log_exception(self, instance):
1853        """Log an unhandled exception. 'instance' is the instance
1854        where the exception was generated.
1855        """
1856        logger.exception("unhandled exception in instance %r", instance)
1857
1858    # the list of commands which gets logged when logging level
1859    # is >= logging.INFO
1860    log_cmds_list = ["DELE", "RNFR", "RNTO", "MKD", "RMD", "CWD",
1861                     "XMKD", "XRMD", "XCWD",
1862                     "REIN", "SITE CHMOD", "MFMT"]
1863
1864    def log_cmd(self, cmd, arg, respcode, respstr):
1865        """Log commands and responses in a standardized format.
1866        This is disabled in case the logging level is set to DEBUG.
1867
1868         - (str) cmd:
1869            the command sent by client
1870
1871         - (str) arg:
1872            the command argument sent by client.
1873            For filesystem commands such as DELE, MKD, etc. this is
1874            already represented as an absolute real filesystem path
1875            like "/home/user/file.ext".
1876
1877         - (int) respcode:
1878            the response code as being sent by server. Response codes
1879            starting with 4xx or 5xx are returned if the command has
1880            been rejected for some reason.
1881
1882         - (str) respstr:
1883            the response string as being sent by server.
1884
1885        By default only DELE, RMD, RNTO, MKD, CWD, ABOR, REIN, SITE CHMOD
1886        commands are logged and the output is redirected to self.log
1887        method.
1888
1889        Can be overridden to provide alternate formats or to log
1890        further commands.
1891        """
1892        if not self._log_debug and cmd in self.log_cmds_list:
1893            line = '%s %s' % (' '.join([cmd, arg]).strip(), respcode)
1894            if str(respcode)[0] in ('4', '5'):
1895                line += ' %r' % respstr
1896            self.log(line)
1897
1898    def log_transfer(self, cmd, filename, receive, completed, elapsed, bytes):
1899        """Log all file transfers in a standardized format.
1900
1901         - (str) cmd:
1902            the original command who caused the tranfer.
1903
1904         - (str) filename:
1905            the absolutized name of the file on disk.
1906
1907         - (bool) receive:
1908            True if the transfer was used for client uploading (STOR,
1909            STOU, APPE), False otherwise (RETR).
1910
1911         - (bool) completed:
1912            True if the file has been entirely sent, else False.
1913
1914         - (float) elapsed:
1915            transfer elapsed time in seconds.
1916
1917         - (int) bytes:
1918            number of bytes transmitted.
1919        """
1920        line = '%s %s completed=%s bytes=%s seconds=%s' % \
1921            (cmd, filename, completed and 1 or 0, bytes, elapsed)
1922        self.log(line)
1923
1924    # --- connection
1925    def _make_eport(self, ip, port):
1926        """Establish an active data channel with remote client which
1927        issued a PORT or EPRT command.
1928        """
1929        # FTP bounce attacks protection: according to RFC-2577 it's
1930        # recommended to reject PORT if IP address specified in it
1931        # does not match client IP address.
1932        remote_ip = self.remote_ip
1933        if remote_ip.startswith('::ffff:'):
1934            # In this scenario, the server has an IPv6 socket, but
1935            # the remote client is using IPv4 and its address is
1936            # represented as an IPv4-mapped IPv6 address which
1937            # looks like this ::ffff:151.12.5.65, see:
1938            # http://en.wikipedia.org/wiki/IPv6#IPv4-mapped_addresses
1939            # http://tools.ietf.org/html/rfc3493.html#section-3.7
1940            # We truncate the first bytes to make it look like a
1941            # common IPv4 address.
1942            remote_ip = remote_ip[7:]
1943        if not self.permit_foreign_addresses and ip != remote_ip:
1944            msg = "501 Rejected data connection to foreign address %s:%s." \
1945                % (ip, port)
1946            self.respond_w_warning(msg)
1947            return
1948
1949        # ...another RFC-2577 recommendation is rejecting connections
1950        # to privileged ports (< 1024) for security reasons.
1951        if not self.permit_privileged_ports and port < 1024:
1952            msg = '501 PORT against the privileged port "%s" refused.' % port
1953            self.respond_w_warning(msg)
1954            return
1955
1956        # close establishing DTP instances, if any
1957        self._shutdown_connecting_dtp()
1958
1959        if self.data_channel is not None:
1960            self.data_channel.close()
1961            self.data_channel = None
1962
1963        # make sure we are not hitting the max connections limit
1964        if not self.server._accept_new_cons():
1965            msg = "425 Too many connections. Can't open data channel."
1966            self.respond_w_warning(msg)
1967            return
1968
1969        # open data channel
1970        self._dtp_connector = self.active_dtp(ip, port, self)
1971
1972    def _make_epasv(self, extmode=False):
1973        """Initialize a passive data channel with remote client which
1974        issued a PASV or EPSV command.
1975        If extmode argument is True we assume that client issued EPSV in
1976        which case extended passive mode will be used (see RFC-2428).
1977        """
1978        # close establishing DTP instances, if any
1979        self._shutdown_connecting_dtp()
1980
1981        # close established data connections, if any
1982        if self.data_channel is not None:
1983            self.data_channel.close()
1984            self.data_channel = None
1985
1986        # make sure we are not hitting the max connections limit
1987        if not self.server._accept_new_cons():
1988            msg = "425 Too many connections. Can't open data channel."
1989            self.respond_w_warning(msg)
1990            return
1991
1992        # open data channel
1993        self._dtp_acceptor = self.passive_dtp(self, extmode)
1994
1995    def ftp_PORT(self, line):
1996        """Start an active data channel by using IPv4."""
1997        if self._epsvall:
1998            self.respond("501 PORT not allowed after EPSV ALL.")
1999            return
2000        # Parse PORT request for getting IP and PORT.
2001        # Request comes in as:
2002        # > h1,h2,h3,h4,p1,p2
2003        # ...where the client's IP address is h1.h2.h3.h4 and the TCP
2004        # port number is (p1 * 256) + p2.
2005        try:
2006            addr = list(map(int, line.split(',')))
2007            if len(addr) != 6:
2008                raise ValueError
2009            for x in addr[:4]:
2010                if not 0 <= x <= 255:
2011                    raise ValueError
2012            ip = '%d.%d.%d.%d' % tuple(addr[:4])
2013            port = (addr[4] * 256) + addr[5]
2014            if not 0 <= port <= 65535:
2015                raise ValueError
2016        except (ValueError, OverflowError):
2017            self.respond("501 Invalid PORT format.")
2018            return
2019        self._make_eport(ip, port)
2020
2021    def ftp_EPRT(self, line):
2022        """Start an active data channel by choosing the network protocol
2023        to use (IPv4/IPv6) as defined in RFC-2428.
2024        """
2025        if self._epsvall:
2026            self.respond("501 EPRT not allowed after EPSV ALL.")
2027            return
2028        # Parse EPRT request for getting protocol, IP and PORT.
2029        # Request comes in as:
2030        # <d>proto<d>ip<d>port<d>
2031        # ...where <d> is an arbitrary delimiter character (usually "|") and
2032        # <proto> is the network protocol to use (1 for IPv4, 2 for IPv6).
2033        try:
2034            af, ip, port = line.split(line[0])[1:-1]
2035            port = int(port)
2036            if not 0 <= port <= 65535:
2037                raise ValueError
2038        except (ValueError, IndexError, OverflowError):
2039            self.respond("501 Invalid EPRT format.")
2040            return
2041
2042        if af == "1":
2043            # test if AF_INET6 and IPV6_V6ONLY
2044            if (self.socket.family == socket.AF_INET6 and not
2045                    SUPPORTS_HYBRID_IPV6):
2046                self.respond('522 Network protocol not supported (use 2).')
2047            else:
2048                try:
2049                    octs = list(map(int, ip.split('.')))
2050                    if len(octs) != 4:
2051                        raise ValueError
2052                    for x in octs:
2053                        if not 0 <= x <= 255:
2054                            raise ValueError
2055                except (ValueError, OverflowError):
2056                    self.respond("501 Invalid EPRT format.")
2057                else:
2058                    self._make_eport(ip, port)
2059        elif af == "2":
2060            if self.socket.family == socket.AF_INET:
2061                self.respond('522 Network protocol not supported (use 1).')
2062            else:
2063                self._make_eport(ip, port)
2064        else:
2065            if self.socket.family == socket.AF_INET:
2066                self.respond('501 Unknown network protocol (use 1).')
2067            else:
2068                self.respond('501 Unknown network protocol (use 2).')
2069
2070    def ftp_PASV(self, line):
2071        """Start a passive data channel by using IPv4."""
2072        if self._epsvall:
2073            self.respond("501 PASV not allowed after EPSV ALL.")
2074            return
2075        self._make_epasv(extmode=False)
2076
2077    def ftp_EPSV(self, line):
2078        """Start a passive data channel by using IPv4 or IPv6 as defined
2079        in RFC-2428.
2080        """
2081        # RFC-2428 specifies that if an optional parameter is given,
2082        # we have to determine the address family from that otherwise
2083        # use the same address family used on the control connection.
2084        # In such a scenario a client may use IPv4 on the control channel
2085        # and choose to use IPv6 for the data channel.
2086        # But how could we use IPv6 on the data channel without knowing
2087        # which IPv6 address to use for binding the socket?
2088        # Unfortunately RFC-2428 does not provide satisfing information
2089        # on how to do that.  The assumption is that we don't have any way
2090        # to know wich address to use, hence we just use the same address
2091        # family used on the control connection.
2092        if not line:
2093            self._make_epasv(extmode=True)
2094        # IPv4
2095        elif line == "1":
2096            if self.socket.family != socket.AF_INET:
2097                self.respond('522 Network protocol not supported (use 2).')
2098            else:
2099                self._make_epasv(extmode=True)
2100        # IPv6
2101        elif line == "2":
2102            if self.socket.family == socket.AF_INET:
2103                self.respond('522 Network protocol not supported (use 1).')
2104            else:
2105                self._make_epasv(extmode=True)
2106        elif line.lower() == 'all':
2107            self._epsvall = True
2108            self.respond(
2109                '220 Other commands other than EPSV are now disabled.')
2110        else:
2111            if self.socket.family == socket.AF_INET:
2112                self.respond('501 Unknown network protocol (use 1).')
2113            else:
2114                self.respond('501 Unknown network protocol (use 2).')
2115
2116    def ftp_QUIT(self, line):
2117        """Quit the current session disconnecting the client."""
2118        if self.authenticated:
2119            msg_quit = self.authorizer.get_msg_quit(self.username)
2120        else:
2121            msg_quit = "Goodbye."
2122        if len(msg_quit) <= 75:
2123            self.respond("221 %s" % msg_quit)
2124        else:
2125            self.push("221-%s\r\n" % msg_quit)
2126            self.respond("221 ")
2127
2128        # From RFC-959:
2129        # If file transfer is in progress, the connection must remain
2130        # open for result response and the server will then close it.
2131        # We also stop responding to any further command.
2132        if self.data_channel:
2133            self._quit_pending = True
2134            self.del_channel()
2135        else:
2136            self._shutdown_connecting_dtp()
2137            self.close_when_done()
2138        if self.authenticated and self.username:
2139            self.on_logout(self.username)
2140
2141        # --- data transferring
2142
2143    def ftp_LIST(self, path):
2144        """Return a list of files in the specified directory to the
2145        client.
2146        On success return the directory path, else None.
2147        """
2148        # - If no argument, fall back on cwd as default.
2149        # - Some older FTP clients erroneously issue /bin/ls-like LIST
2150        #   formats in which case we fall back on cwd as default.
2151        try:
2152            isdir = self.fs.isdir(path)
2153            if isdir:
2154                listing = self.run_as_current_user(self.fs.listdir, path)
2155                if isinstance(listing, list):
2156                    try:
2157                        # RFC 959 recommends the listing to be sorted.
2158                        listing.sort()
2159                    except UnicodeDecodeError:
2160                        # (Python 2 only) might happen on filesystem not
2161                        # supporting UTF8 meaning os.listdir() returned a list
2162                        # of mixed bytes and unicode strings:
2163                        # http://goo.gl/6DLHD
2164                        # http://bugs.python.org/issue683592
2165                        pass
2166                iterator = self.fs.format_list(path, listing)
2167            else:
2168                basedir, filename = os.path.split(path)
2169                self.fs.lstat(path)  # raise exc in case of problems
2170                iterator = self.fs.format_list(basedir, [filename])
2171        except (OSError, FilesystemError) as err:
2172            why = _strerror(err)
2173            self.respond('550 %s.' % why)
2174        else:
2175            producer = BufferedIteratorProducer(iterator)
2176            self.push_dtp_data(producer, isproducer=True, cmd="LIST")
2177            return path
2178
2179    def ftp_NLST(self, path):
2180        """Return a list of files in the specified directory in a
2181        compact form to the client.
2182        On success return the directory path, else None.
2183        """
2184        try:
2185            if self.fs.isdir(path):
2186                listing = list(self.run_as_current_user(self.fs.listdir, path))
2187            else:
2188                # if path is a file we just list its name
2189                self.fs.lstat(path)  # raise exc in case of problems
2190                listing = [os.path.basename(path)]
2191        except (OSError, FilesystemError) as err:
2192            self.respond('550 %s.' % _strerror(err))
2193        else:
2194            data = ''
2195            if listing:
2196                try:
2197                    listing.sort()
2198                except UnicodeDecodeError:
2199                    # (Python 2 only) might happen on filesystem not
2200                    # supporting UTF8 meaning os.listdir() returned a list
2201                    # of mixed bytes and unicode strings:
2202                    # http://goo.gl/6DLHD
2203                    # http://bugs.python.org/issue683592
2204                    ls = []
2205                    for x in listing:
2206                        if not isinstance(x, unicode):
2207                            x = unicode(x, 'utf8')
2208                        ls.append(x)
2209                    listing = sorted(ls)
2210                data = '\r\n'.join(listing) + '\r\n'
2211            data = data.encode('utf8', self.unicode_errors)
2212            self.push_dtp_data(data, cmd="NLST")
2213            return path
2214
2215        # --- MLST and MLSD commands
2216
2217    # The MLST and MLSD commands are intended to standardize the file and
2218    # directory information returned by the server-FTP process.  These
2219    # commands differ from the LIST command in that the format of the
2220    # replies is strictly defined although extensible.
2221
2222    def ftp_MLST(self, path):
2223        """Return information about a pathname in a machine-processable
2224        form as defined in RFC-3659.
2225        On success return the path just listed, else None.
2226        """
2227        line = self.fs.fs2ftp(path)
2228        basedir, basename = os.path.split(path)
2229        perms = self.authorizer.get_perms(self.username)
2230        try:
2231            iterator = self.run_as_current_user(
2232                self.fs.format_mlsx, basedir, [basename], perms,
2233                self._current_facts, ignore_err=False)
2234            data = b''.join(iterator)
2235        except (OSError, FilesystemError) as err:
2236            self.respond('550 %s.' % _strerror(err))
2237        else:
2238            data = data.decode('utf8', self.unicode_errors)
2239            # since TVFS is supported (see RFC-3659 chapter 6), a fully
2240            # qualified pathname should be returned
2241            data = data.split(' ')[0] + ' %s\r\n' % line
2242            # response is expected on the command channel
2243            self.push('250-Listing "%s":\r\n' % line)
2244            # the fact set must be preceded by a space
2245            self.push(' ' + data)
2246            self.respond('250 End MLST.')
2247            return path
2248
2249    def ftp_MLSD(self, path):
2250        """Return contents of a directory in a machine-processable form
2251        as defined in RFC-3659.
2252        On success return the path just listed, else None.
2253        """
2254        # RFC-3659 requires 501 response code if path is not a directory
2255        if not self.fs.isdir(path):
2256            self.respond("501 No such directory.")
2257            return
2258        try:
2259            listing = self.run_as_current_user(self.fs.listdir, path)
2260        except (OSError, FilesystemError) as err:
2261            why = _strerror(err)
2262            self.respond('550 %s.' % why)
2263        else:
2264            perms = self.authorizer.get_perms(self.username)
2265            iterator = self.fs.format_mlsx(path, listing, perms,
2266                                           self._current_facts)
2267            producer = BufferedIteratorProducer(iterator)
2268            self.push_dtp_data(producer, isproducer=True, cmd="MLSD")
2269            return path
2270
2271    def ftp_RETR(self, file):
2272        """Retrieve the specified file (transfer from the server to the
2273        client).  On success return the file path else None.
2274        """
2275        rest_pos = self._restart_position
2276        self._restart_position = 0
2277        try:
2278            fd = self.run_as_current_user(self.fs.open, file, 'rb')
2279        except (EnvironmentError, FilesystemError) as err:
2280            why = _strerror(err)
2281            self.respond('550 %s.' % why)
2282            return
2283
2284        try:
2285            if rest_pos:
2286                # Make sure that the requested offset is valid (within the
2287                # size of the file being resumed).
2288                # According to RFC-1123 a 554 reply may result in case that
2289                # the existing file cannot be repositioned as specified in
2290                # the REST.
2291                ok = 0
2292                try:
2293                    if rest_pos > self.fs.getsize(file):
2294                        raise ValueError
2295                    fd.seek(rest_pos)
2296                    ok = 1
2297                except ValueError:
2298                    why = "Invalid REST parameter"
2299                except (EnvironmentError, FilesystemError) as err:
2300                    why = _strerror(err)
2301                if not ok:
2302                    fd.close()
2303                    self.respond('554 %s' % why)
2304                    return
2305            producer = FileProducer(fd, self._current_type)
2306            self.push_dtp_data(producer, isproducer=True, file=fd, cmd="RETR")
2307            return file
2308        except Exception:
2309            fd.close()
2310            raise
2311
2312    def ftp_STOR(self, file, mode='w'):
2313        """Store a file (transfer from the client to the server).
2314        On success return the file path, else None.
2315        """
2316        # A resume could occur in case of APPE or REST commands.
2317        # In that case we have to open file object in different ways:
2318        # STOR: mode = 'w'
2319        # APPE: mode = 'a'
2320        # REST: mode = 'r+' (to permit seeking on file object)
2321        if 'a' in mode:
2322            cmd = 'APPE'
2323        else:
2324            cmd = 'STOR'
2325        rest_pos = self._restart_position
2326        self._restart_position = 0
2327        if rest_pos:
2328            mode = 'r+'
2329        try:
2330            fd = self.run_as_current_user(self.fs.open, file, mode + 'b')
2331        except (EnvironmentError, FilesystemError) as err:
2332            why = _strerror(err)
2333            self.respond('550 %s.' % why)
2334            return
2335
2336        try:
2337            if rest_pos:
2338                # Make sure that the requested offset is valid (within the
2339                # size of the file being resumed).
2340                # According to RFC-1123 a 554 reply may result in case
2341                # that the existing file cannot be repositioned as
2342                # specified in the REST.
2343                ok = 0
2344                try:
2345                    if rest_pos > self.fs.getsize(file):
2346                        raise ValueError
2347                    fd.seek(rest_pos)
2348                    ok = 1
2349                except ValueError:
2350                    why = "Invalid REST parameter"
2351                except (EnvironmentError, FilesystemError) as err:
2352                    why = _strerror(err)
2353                if not ok:
2354                    fd.close()
2355                    self.respond('554 %s' % why)
2356                    return
2357
2358            if self.data_channel is not None:
2359                resp = "Data connection already open. Transfer starting."
2360                self.respond("125 " + resp)
2361                self.data_channel.file_obj = fd
2362                self.data_channel.enable_receiving(self._current_type, cmd)
2363            else:
2364                resp = "File status okay. About to open data connection."
2365                self.respond("150 " + resp)
2366                self._in_dtp_queue = (fd, cmd)
2367            return file
2368        except Exception:
2369            fd.close()
2370            raise
2371
2372    def ftp_STOU(self, line):
2373        """Store a file on the server with a unique name.
2374        On success return the file path, else None.
2375        """
2376        # Note 1: RFC-959 prohibited STOU parameters, but this
2377        # prohibition is obsolete.
2378        # Note 2: 250 response wanted by RFC-959 has been declared
2379        # incorrect in RFC-1123 that wants 125/150 instead.
2380        # Note 3: RFC-1123 also provided an exact output format
2381        # defined to be as follow:
2382        # > 125 FILE: pppp
2383        # ...where pppp represents the unique path name of the
2384        # file that will be written.
2385
2386        # watch for STOU preceded by REST, which makes no sense.
2387        if self._restart_position:
2388            self.respond("450 Can't STOU while REST request is pending.")
2389            return
2390
2391        if line:
2392            basedir, prefix = os.path.split(self.fs.ftp2fs(line))
2393            prefix = prefix + '.'
2394        else:
2395            basedir = self.fs.ftp2fs(self.fs.cwd)
2396            prefix = 'ftpd.'
2397        try:
2398            fd = self.run_as_current_user(self.fs.mkstemp, prefix=prefix,
2399                                          dir=basedir)
2400        except (EnvironmentError, FilesystemError) as err:
2401            # likely, we hit the max number of retries to find out a
2402            # file with a unique name
2403            if getattr(err, "errno", -1) == errno.EEXIST:
2404                why = 'No usable unique file name found'
2405            # something else happened
2406            else:
2407                why = _strerror(err)
2408            self.respond("450 %s." % why)
2409            return
2410
2411        try:
2412            if not self.authorizer.has_perm(self.username, 'w', fd.name):
2413                try:
2414                    fd.close()
2415                    self.run_as_current_user(self.fs.remove, fd.name)
2416                except (OSError, FilesystemError):
2417                    pass
2418                self.respond("550 Not enough privileges.")
2419                return
2420
2421            # now just acts like STOR except that restarting isn't allowed
2422            filename = os.path.basename(fd.name)
2423            if self.data_channel is not None:
2424                self.respond("125 FILE: %s" % filename)
2425                self.data_channel.file_obj = fd
2426                self.data_channel.enable_receiving(self._current_type, "STOU")
2427            else:
2428                self.respond("150 FILE: %s" % filename)
2429                self._in_dtp_queue = (fd, "STOU")
2430            return filename
2431        except Exception:
2432            fd.close()
2433            raise
2434
2435    def ftp_APPE(self, file):
2436        """Append data to an existing file on the server.
2437        On success return the file path, else None.
2438        """
2439        # watch for APPE preceded by REST, which makes no sense.
2440        if self._restart_position:
2441            self.respond("450 Can't APPE while REST request is pending.")
2442        else:
2443            return self.ftp_STOR(file, mode='a')
2444
2445    def ftp_REST(self, line):
2446        """Restart a file transfer from a previous mark."""
2447        if self._current_type == 'a':
2448            self.respond('501 Resuming transfers not allowed in ASCII mode.')
2449            return
2450        try:
2451            marker = int(line)
2452            if marker < 0:
2453                raise ValueError
2454        except (ValueError, OverflowError):
2455            self.respond("501 Invalid parameter.")
2456        else:
2457            self.respond("350 Restarting at position %s." % marker)
2458            self._restart_position = marker
2459
2460    def ftp_ABOR(self, line):
2461        """Abort the current data transfer."""
2462        # ABOR received while no data channel exists
2463        if (self._dtp_acceptor is None and
2464                self._dtp_connector is None and
2465                self.data_channel is None):
2466            self.respond("225 No transfer to abort.")
2467            return
2468        else:
2469            # a PASV or PORT was received but connection wasn't made yet
2470            if (self._dtp_acceptor is not None or
2471                    self._dtp_connector is not None):
2472                self._shutdown_connecting_dtp()
2473                resp = "225 ABOR command successful; data channel closed."
2474
2475            # If a data transfer is in progress the server must first
2476            # close the data connection, returning a 426 reply to
2477            # indicate that the transfer terminated abnormally, then it
2478            # must send a 226 reply, indicating that the abort command
2479            # was successfully processed.
2480            # If no data has been transmitted we just respond with 225
2481            # indicating that no transfer was in progress.
2482            if self.data_channel is not None:
2483                if self.data_channel.transfer_in_progress():
2484                    self.data_channel.close()
2485                    self.data_channel = None
2486                    self.respond("426 Transfer aborted via ABOR.",
2487                                 logfun=logger.info)
2488                    resp = "226 ABOR command successful."
2489                else:
2490                    self.data_channel.close()
2491                    self.data_channel = None
2492                    resp = "225 ABOR command successful; data channel closed."
2493        self.respond(resp)
2494
2495        # --- authentication
2496    def ftp_USER(self, line):
2497        """Set the username for the current session."""
2498        # RFC-959 specifies a 530 response to the USER command if the
2499        # username is not valid.  If the username is valid is required
2500        # ftpd returns a 331 response instead.  In order to prevent a
2501        # malicious client from determining valid usernames on a server,
2502        # it is suggested by RFC-2577 that a server always return 331 to
2503        # the USER command and then reject the combination of username
2504        # and password for an invalid username when PASS is provided later.
2505        if not self.authenticated:
2506            self.respond('331 Username ok, send password.')
2507        else:
2508            # a new USER command could be entered at any point in order
2509            # to change the access control flushing any user, password,
2510            # and account information already supplied and beginning the
2511            # login sequence again.
2512            self.flush_account()
2513            msg = 'Previous account information was flushed'
2514            self.respond('331 %s, send password.' % msg, logfun=logger.info)
2515        self.username = line
2516
2517    def handle_auth_failed(self, msg, password):
2518        def callback(username, password, msg):
2519            self.add_channel()
2520            if hasattr(self, '_closed') and not self._closed:
2521                self.attempted_logins += 1
2522                if self.attempted_logins >= self.max_login_attempts:
2523                    msg += " Disconnecting."
2524                    self.respond("530 " + msg)
2525                    self.close_when_done()
2526                else:
2527                    self.respond("530 " + msg)
2528                self.log("USER '%s' failed login." % username)
2529            self.on_login_failed(username, password)
2530
2531        self.del_channel()
2532        if not msg:
2533            if self.username == 'anonymous':
2534                msg = "Anonymous access not allowed."
2535            else:
2536                msg = "Authentication failed."
2537        else:
2538            # response string should be capitalized as per RFC-959
2539            msg = msg.capitalize()
2540        self.ioloop.call_later(self.auth_failed_timeout, callback,
2541                               self.username, password, msg,
2542                               _errback=self.handle_error)
2543        self.username = ""
2544
2545    def handle_auth_success(self, home, password, msg_login):
2546        if not isinstance(home, unicode):
2547            if PY3:
2548                raise TypeError('type(home) != text')
2549            else:
2550                warnings.warn(
2551                    '%s.get_home_dir returned a non-unicode string; now '
2552                    'casting to unicode' % (
2553                        self.authorizer.__class__.__name__),
2554                    RuntimeWarning)
2555                home = home.decode('utf8')
2556
2557        if len(msg_login) <= 75:
2558            self.respond('230 %s' % msg_login)
2559        else:
2560            self.push("230-%s\r\n" % msg_login)
2561            self.respond("230 ")
2562        self.log("USER '%s' logged in." % self.username)
2563        self.authenticated = True
2564        self.password = password
2565        self.attempted_logins = 0
2566
2567        self.fs = self.abstracted_fs(home, self)
2568        self.on_login(self.username)
2569
2570    def ftp_PASS(self, line):
2571        """Check username's password against the authorizer."""
2572        if self.authenticated:
2573            self.respond("503 User already authenticated.")
2574            return
2575        if not self.username:
2576            self.respond("503 Login with USER first.")
2577            return
2578
2579        try:
2580            self.authorizer.validate_authentication(self.username, line, self)
2581            home = self.authorizer.get_home_dir(self.username)
2582            msg_login = self.authorizer.get_msg_login(self.username)
2583        except (AuthenticationFailed, AuthorizerError) as err:
2584            self.handle_auth_failed(str(err), line)
2585        else:
2586            self.handle_auth_success(home, line, msg_login)
2587
2588    def ftp_REIN(self, line):
2589        """Reinitialize user's current session."""
2590        # From RFC-959:
2591        # REIN command terminates a USER, flushing all I/O and account
2592        # information, except to allow any transfer in progress to be
2593        # completed.  All parameters are reset to the default settings
2594        # and the control connection is left open.  This is identical
2595        # to the state in which a user finds himself immediately after
2596        # the control connection is opened.
2597        self.flush_account()
2598        # Note: RFC-959 erroneously mention "220" as the correct response
2599        # code to be given in this case, but this is wrong...
2600        self.respond("230 Ready for new user.")
2601
2602        # --- filesystem operations
2603    def ftp_PWD(self, line):
2604        """Return the name of the current working directory to the client."""
2605        # The 257 response is supposed to include the directory
2606        # name and in case it contains embedded double-quotes
2607        # they must be doubled (see RFC-959, chapter 7, appendix 2).
2608        cwd = self.fs.cwd
2609        assert isinstance(cwd, unicode), cwd
2610        self.respond('257 "%s" is the current directory.'
2611                     % cwd.replace('"', '""'))
2612
2613    def ftp_CWD(self, path):
2614        """Change the current working directory.
2615        On success return the new directory path, else None.
2616        """
2617        # Temporarily join the specified directory to see if we have
2618        # permissions to do so, then get back to original process's
2619        # current working directory.
2620        # Note that if for some reason os.getcwd() gets removed after
2621        # the process is started we'll get into troubles (os.getcwd()
2622        # will fail with ENOENT) but we can't do anything about that
2623        # except logging an error.
2624        init_cwd = getcwdu()
2625        try:
2626            self.run_as_current_user(self.fs.chdir, path)
2627        except (OSError, FilesystemError) as err:
2628            why = _strerror(err)
2629            self.respond('550 %s.' % why)
2630        else:
2631            cwd = self.fs.cwd
2632            assert isinstance(cwd, unicode), cwd
2633            self.respond('250 "%s" is the current directory.' % cwd)
2634            if getcwdu() != init_cwd:
2635                os.chdir(init_cwd)
2636            return path
2637
2638    def ftp_CDUP(self, path):
2639        """Change into the parent directory.
2640        On success return the new directory, else None.
2641        """
2642        # Note: RFC-959 says that code 200 is required but it also says
2643        # that CDUP uses the same codes as CWD.
2644        return self.ftp_CWD(path)
2645
2646    def ftp_SIZE(self, path):
2647        """Return size of file in a format suitable for using with
2648        RESTart as defined in RFC-3659."""
2649
2650        # Implementation note: properly handling the SIZE command when
2651        # TYPE ASCII is used would require to scan the entire file to
2652        # perform the ASCII translation logic
2653        # (file.read().replace(os.linesep, '\r\n')) and then calculating
2654        # the len of such data which may be different than the actual
2655        # size of the file on the server.  Considering that calculating
2656        # such result could be very resource-intensive and also dangerous
2657        # (DoS) we reject SIZE when the current TYPE is ASCII.
2658        # However, clients in general should not be resuming downloads
2659        # in ASCII mode.  Resuming downloads in binary mode is the
2660        # recommended way as specified in RFC-3659.
2661
2662        line = self.fs.fs2ftp(path)
2663        if self._current_type == 'a':
2664            why = "SIZE not allowed in ASCII mode"
2665            self.respond("550 %s." % why)
2666            return
2667        if not self.fs.isfile(self.fs.realpath(path)):
2668            why = "%s is not retrievable" % line
2669            self.respond("550 %s." % why)
2670            return
2671        try:
2672            size = self.run_as_current_user(self.fs.getsize, path)
2673        except (OSError, FilesystemError) as err:
2674            why = _strerror(err)
2675            self.respond('550 %s.' % why)
2676        else:
2677            self.respond("213 %s" % size)
2678
2679    def ftp_MDTM(self, path):
2680        """Return last modification time of file to the client as an ISO
2681        3307 style timestamp (YYYYMMDDHHMMSS) as defined in RFC-3659.
2682        On success return the file path, else None.
2683        """
2684        line = self.fs.fs2ftp(path)
2685        if not self.fs.isfile(self.fs.realpath(path)):
2686            self.respond("550 %s is not retrievable" % line)
2687            return
2688        if self.use_gmt_times:
2689            timefunc = time.gmtime
2690        else:
2691            timefunc = time.localtime
2692        try:
2693            secs = self.run_as_current_user(self.fs.getmtime, path)
2694            lmt = time.strftime("%Y%m%d%H%M%S", timefunc(secs))
2695        except (ValueError, OSError, FilesystemError) as err:
2696            if isinstance(err, ValueError):
2697                # It could happen if file's last modification time
2698                # happens to be too old (prior to year 1900)
2699                why = "Can't determine file's last modification time"
2700            else:
2701                why = _strerror(err)
2702            self.respond('550 %s.' % why)
2703        else:
2704            self.respond("213 %s" % lmt)
2705            return path
2706
2707    def ftp_MFMT(self, path, timeval):
2708        """ Sets the last modification time of file to timeval
2709        3307 style timestamp (YYYYMMDDHHMMSS) as defined in RFC-3659.
2710        On success return the modified time and file path, else None.
2711        """
2712        # Note: the MFMT command is not a formal RFC command
2713        # but stated in the following MEMO:
2714        # https://tools.ietf.org/html/draft-somers-ftp-mfxx-04
2715        # this is implemented to assist with file synchronization
2716
2717        line = self.fs.fs2ftp(path)
2718
2719        if len(timeval) != len("YYYYMMDDHHMMSS"):
2720            why = "Invalid time format; expected: YYYYMMDDHHMMSS"
2721            self.respond('550 %s.' % why)
2722            return
2723        if not self.fs.isfile(self.fs.realpath(path)):
2724            self.respond("550 %s is not retrievable" % line)
2725            return
2726        if self.use_gmt_times:
2727            timefunc = time.gmtime
2728        else:
2729            timefunc = time.localtime
2730        try:
2731            # convert timeval string to epoch seconds
2732            epoch = datetime.utcfromtimestamp(0)
2733            timeval_datetime_obj = datetime.strptime(timeval, '%Y%m%d%H%M%S')
2734            timeval_secs = (timeval_datetime_obj - epoch).total_seconds()
2735        except ValueError:
2736            why = "Invalid time format; expected: YYYYMMDDHHMMSS"
2737            self.respond('550 %s.' % why)
2738            return
2739        try:
2740            # Modify Time
2741            self.run_as_current_user(self.fs.utime, path, timeval_secs)
2742            # Fetch Time
2743            secs = self.run_as_current_user(self.fs.getmtime, path)
2744            lmt = time.strftime("%Y%m%d%H%M%S", timefunc(secs))
2745        except (ValueError, OSError, FilesystemError) as err:
2746            if isinstance(err, ValueError):
2747                # It could happen if file's last modification time
2748                # happens to be too old (prior to year 1900)
2749                why = "Can't determine file's last modification time"
2750            else:
2751                why = _strerror(err)
2752            self.respond('550 %s.' % why)
2753        else:
2754            self.respond("213 Modify=%s; %s." % (lmt, line))
2755            return (lmt, path)
2756
2757    def ftp_MKD(self, path):
2758        """Create the specified directory.
2759        On success return the directory path, else None.
2760        """
2761        line = self.fs.fs2ftp(path)
2762        try:
2763            self.run_as_current_user(self.fs.mkdir, path)
2764        except (OSError, FilesystemError) as err:
2765            why = _strerror(err)
2766            self.respond('550 %s.' % why)
2767        else:
2768            # The 257 response is supposed to include the directory
2769            # name and in case it contains embedded double-quotes
2770            # they must be doubled (see RFC-959, chapter 7, appendix 2).
2771            self.respond(
2772                '257 "%s" directory created.' % line.replace('"', '""'))
2773            return path
2774
2775    def ftp_RMD(self, path):
2776        """Remove the specified directory.
2777        On success return the directory path, else None.
2778        """
2779        if self.fs.realpath(path) == self.fs.realpath(self.fs.root):
2780            msg = "Can't remove root directory."
2781            self.respond("550 %s" % msg)
2782            return
2783        try:
2784            self.run_as_current_user(self.fs.rmdir, path)
2785        except (OSError, FilesystemError) as err:
2786            why = _strerror(err)
2787            self.respond('550 %s.' % why)
2788        else:
2789            self.respond("250 Directory removed.")
2790
2791    def ftp_DELE(self, path):
2792        """Delete the specified file.
2793        On success return the file path, else None.
2794        """
2795        try:
2796            self.run_as_current_user(self.fs.remove, path)
2797        except (OSError, FilesystemError) as err:
2798            why = _strerror(err)
2799            self.respond('550 %s.' % why)
2800        else:
2801            self.respond("250 File removed.")
2802            return path
2803
2804    def ftp_RNFR(self, path):
2805        """Rename the specified (only the source name is specified
2806        here, see RNTO command)"""
2807        if not self.fs.lexists(path):
2808            self.respond("550 No such file or directory.")
2809        elif self.fs.realpath(path) == self.fs.realpath(self.fs.root):
2810            self.respond("550 Can't rename home directory.")
2811        else:
2812            self._rnfr = path
2813            self.respond("350 Ready for destination name.")
2814
2815    def ftp_RNTO(self, path):
2816        """Rename file (destination name only, source is specified with
2817        RNFR).
2818        On success return a (source_path, destination_path) tuple.
2819        """
2820        if not self._rnfr:
2821            self.respond("503 Bad sequence of commands: use RNFR first.")
2822            return
2823        src = self._rnfr
2824        self._rnfr = None
2825        try:
2826            self.run_as_current_user(self.fs.rename, src, path)
2827        except (OSError, FilesystemError) as err:
2828            why = _strerror(err)
2829            self.respond('550 %s.' % why)
2830        else:
2831            self.respond("250 Renaming ok.")
2832            return (src, path)
2833
2834        # --- others
2835    def ftp_TYPE(self, line):
2836        """Set current type data type to binary/ascii"""
2837        type = line.upper().replace(' ', '')
2838        if type in ("A", "L7"):
2839            self.respond("200 Type set to: ASCII.")
2840            self._current_type = 'a'
2841        elif type in ("I", "L8"):
2842            self.respond("200 Type set to: Binary.")
2843            self._current_type = 'i'
2844        else:
2845            self.respond('504 Unsupported type "%s".' % line)
2846
2847    def ftp_STRU(self, line):
2848        """Set file structure ("F" is the only one supported (noop))."""
2849        stru = line.upper()
2850        if stru == 'F':
2851            self.respond('200 File transfer structure set to: F.')
2852        elif stru in ('P', 'R'):
2853            # R is required in minimum implementations by RFC-959, 5.1.
2854            # RFC-1123, 4.1.2.13, amends this to only apply to servers
2855            # whose file systems support record structures, but also
2856            # suggests that such a server "may still accept files with
2857            # STRU R, recording the byte stream literally".
2858            # Should we accept R but with no operational difference from
2859            # F? proftpd and wu-ftpd don't accept STRU R. We just do
2860            # the same.
2861            #
2862            # RFC-1123 recommends against implementing P.
2863            self.respond('504 Unimplemented STRU type.')
2864        else:
2865            self.respond('501 Unrecognized STRU type.')
2866
2867    def ftp_MODE(self, line):
2868        """Set data transfer mode ("S" is the only one supported (noop))."""
2869        mode = line.upper()
2870        if mode == 'S':
2871            self.respond('200 Transfer mode set to: S')
2872        elif mode in ('B', 'C'):
2873            self.respond('504 Unimplemented MODE type.')
2874        else:
2875            self.respond('501 Unrecognized MODE type.')
2876
2877    def ftp_STAT(self, path):
2878        """Return statistics about current ftp session. If an argument
2879        is provided return directory listing over command channel.
2880
2881        Implementation note:
2882
2883        RFC-959 does not explicitly mention globbing but many FTP
2884        servers do support it as a measure of convenience for FTP
2885        clients and users.
2886
2887        In order to search for and match the given globbing expression,
2888        the code has to search (possibly) many directories, examine
2889        each contained filename, and build a list of matching files in
2890        memory.  Since this operation can be quite intensive, both CPU-
2891        and memory-wise, we do not support globbing.
2892        """
2893        # return STATus information about ftpd
2894        if not path:
2895            s = []
2896            s.append('Connected to: %s:%s' % self.socket.getsockname()[:2])
2897            if self.authenticated:
2898                s.append('Logged in as: %s' % self.username)
2899            else:
2900                if not self.username:
2901                    s.append("Waiting for username.")
2902                else:
2903                    s.append("Waiting for password.")
2904            if self._current_type == 'a':
2905                type = 'ASCII'
2906            else:
2907                type = 'Binary'
2908            s.append("TYPE: %s; STRUcture: File; MODE: Stream" % type)
2909            if self._dtp_acceptor is not None:
2910                s.append('Passive data channel waiting for connection.')
2911            elif self.data_channel is not None:
2912                bytes_sent = self.data_channel.tot_bytes_sent
2913                bytes_recv = self.data_channel.tot_bytes_received
2914                elapsed_time = self.data_channel.get_elapsed_time()
2915                s.append('Data connection open:')
2916                s.append('Total bytes sent: %s' % bytes_sent)
2917                s.append('Total bytes received: %s' % bytes_recv)
2918                s.append('Transfer elapsed time: %s secs' % elapsed_time)
2919            else:
2920                s.append('Data connection closed.')
2921
2922            self.push('211-FTP server status:\r\n')
2923            self.push(''.join([' %s\r\n' % item for item in s]))
2924            self.respond('211 End of status.')
2925        # return directory LISTing over the command channel
2926        else:
2927            line = self.fs.fs2ftp(path)
2928            try:
2929                isdir = self.fs.isdir(path)
2930                if isdir:
2931                    listing = self.run_as_current_user(self.fs.listdir, path)
2932                    if isinstance(listing, list):
2933                        try:
2934                            # RFC 959 recommends the listing to be sorted.
2935                            listing.sort()
2936                        except UnicodeDecodeError:
2937                            # (Python 2 only) might happen on filesystem not
2938                            # supporting UTF8 meaning os.listdir() returned a
2939                            # list of mixed bytes and unicode strings:
2940                            # http://goo.gl/6DLHD
2941                            # http://bugs.python.org/issue683592
2942                            pass
2943                    iterator = self.fs.format_list(path, listing)
2944                else:
2945                    basedir, filename = os.path.split(path)
2946                    self.fs.lstat(path)  # raise exc in case of problems
2947                    iterator = self.fs.format_list(basedir, [filename])
2948            except (OSError, FilesystemError) as err:
2949                why = _strerror(err)
2950                self.respond('550 %s.' % why)
2951            else:
2952                self.push('213-Status of "%s":\r\n' % line)
2953                self.push_with_producer(BufferedIteratorProducer(iterator))
2954                self.respond('213 End of status.')
2955                return path
2956
2957    def ftp_FEAT(self, line):
2958        """List all new features supported as defined in RFC-2398."""
2959        features = set(['UTF8', 'TVFS'])
2960        features.update([feat for feat in
2961                         ('EPRT', 'EPSV', 'MDTM', 'MFMT', 'SIZE')
2962                         if feat in self.proto_cmds])
2963        features.update(self._extra_feats)
2964        if 'MLST' in self.proto_cmds or 'MLSD' in self.proto_cmds:
2965            facts = ''
2966            for fact in self._available_facts:
2967                if fact in self._current_facts:
2968                    facts += fact + '*;'
2969                else:
2970                    facts += fact + ';'
2971            features.add('MLST ' + facts)
2972        if 'REST' in self.proto_cmds:
2973            features.add('REST STREAM')
2974        features = sorted(features)
2975        self.push("211-Features supported:\r\n")
2976        self.push("".join([" %s\r\n" % x for x in features]))
2977        self.respond('211 End FEAT.')
2978
2979    def ftp_OPTS(self, line):
2980        """Specify options for FTP commands as specified in RFC-2389."""
2981        try:
2982            if line.count(' ') > 1:
2983                raise ValueError('Invalid number of arguments')
2984            if ' ' in line:
2985                cmd, arg = line.split(' ')
2986                if ';' not in arg:
2987                    raise ValueError('Invalid argument')
2988            else:
2989                cmd, arg = line, ''
2990            # actually the only command able to accept options is MLST
2991            if cmd.upper() != 'MLST' or 'MLST' not in self.proto_cmds:
2992                raise ValueError('Unsupported command "%s"' % cmd)
2993        except ValueError as err:
2994            self.respond('501 %s.' % err)
2995        else:
2996            facts = [x.lower() for x in arg.split(';')]
2997            self._current_facts = \
2998                [x for x in facts if x in self._available_facts]
2999            f = ''.join([x + ';' for x in self._current_facts])
3000            self.respond('200 MLST OPTS ' + f)
3001
3002    def ftp_NOOP(self, line):
3003        """Do nothing."""
3004        self.respond("200 I successfully done nothin'.")
3005
3006    def ftp_SYST(self, line):
3007        """Return system type (always returns UNIX type: L8)."""
3008        # This command is used to find out the type of operating system
3009        # at the server.  The reply shall have as its first word one of
3010        # the system names listed in RFC-943.
3011        # Since that we always return a "/bin/ls -lA"-like output on
3012        # LIST we  prefer to respond as if we would on Unix in any case.
3013        self.respond("215 UNIX Type: L8")
3014
3015    def ftp_ALLO(self, line):
3016        """Allocate bytes for storage (noop)."""
3017        # not necessary (always respond with 202)
3018        self.respond("202 No storage allocation necessary.")
3019
3020    def ftp_HELP(self, line):
3021        """Return help text to the client."""
3022        if line:
3023            line = line.upper()
3024            if line in self.proto_cmds:
3025                self.respond("214 %s" % self.proto_cmds[line]['help'])
3026            else:
3027                self.respond("501 Unrecognized command.")
3028        else:
3029            # provide a compact list of recognized commands
3030            def formatted_help():
3031                cmds = []
3032                keys = sorted([x for x in self.proto_cmds.keys()
3033                               if not x.startswith('SITE ')])
3034                while keys:
3035                    elems = tuple((keys[0:8]))
3036                    cmds.append(' %-6s' * len(elems) % elems + '\r\n')
3037                    del keys[0:8]
3038                return ''.join(cmds)
3039
3040            self.push("214-The following commands are recognized:\r\n")
3041            self.push(formatted_help())
3042            self.respond("214 Help command successful.")
3043
3044        # --- site commands
3045
3046    # The user willing to add support for a specific SITE command must
3047    # update self.proto_cmds dictionary and define a new ftp_SITE_%CMD%
3048    # method in the subclass.
3049
3050    def ftp_SITE_CHMOD(self, path, mode):
3051        """Change file mode.
3052        On success return a (file_path, mode) tuple.
3053        """
3054        # Note: although most UNIX servers implement it, SITE CHMOD is not
3055        # defined in any official RFC.
3056        try:
3057            assert len(mode) in (3, 4)
3058            for x in mode:
3059                assert 0 <= int(x) <= 7
3060            mode = int(mode, 8)
3061        except (AssertionError, ValueError):
3062            self.respond("501 Invalid SITE CHMOD format.")
3063        else:
3064            try:
3065                self.run_as_current_user(self.fs.chmod, path, mode)
3066            except (OSError, FilesystemError) as err:
3067                why = _strerror(err)
3068                self.respond('550 %s.' % why)
3069            else:
3070                self.respond('200 SITE CHMOD successful.')
3071                return (path, mode)
3072
3073    def ftp_SITE_HELP(self, line):
3074        """Return help text to the client for a given SITE command."""
3075        if line:
3076            line = line.upper()
3077            if line in self.proto_cmds:
3078                self.respond("214 %s" % self.proto_cmds[line]['help'])
3079            else:
3080                self.respond("501 Unrecognized SITE command.")
3081        else:
3082            self.push("214-The following SITE commands are recognized:\r\n")
3083            site_cmds = []
3084            for cmd in sorted(self.proto_cmds.keys()):
3085                if cmd.startswith('SITE '):
3086                    site_cmds.append(' %s\r\n' % cmd[5:])
3087            self.push(''.join(site_cmds))
3088            self.respond("214 Help SITE command successful.")
3089
3090        # --- support for deprecated cmds
3091
3092    # RFC-1123 requires that the server treat XCUP, XCWD, XMKD, XPWD
3093    # and XRMD commands as synonyms for CDUP, CWD, MKD, LIST and RMD.
3094    # Such commands are obsoleted but some ftp clients (e.g. Windows
3095    # ftp.exe) still use them.
3096
3097    def ftp_XCUP(self, line):
3098        "Change to the parent directory. Synonym for CDUP. Deprecated."
3099        return self.ftp_CDUP(line)
3100
3101    def ftp_XCWD(self, line):
3102        "Change the current working directory. Synonym for CWD. Deprecated."
3103        return self.ftp_CWD(line)
3104
3105    def ftp_XMKD(self, line):
3106        "Create the specified directory. Synonym for MKD. Deprecated."
3107        return self.ftp_MKD(line)
3108
3109    def ftp_XPWD(self, line):
3110        "Return the current working directory. Synonym for PWD. Deprecated."
3111        return self.ftp_PWD(line)
3112
3113    def ftp_XRMD(self, line):
3114        "Remove the specified directory. Synonym for RMD. Deprecated."
3115        return self.ftp_RMD(line)
3116
3117
3118# ===================================================================
3119# --- FTP over SSL
3120# ===================================================================
3121
3122
3123if SSL is not None:
3124
3125    class SSLConnection(_AsyncChatNewStyle):
3126        """An AsyncChat subclass supporting TLS/SSL."""
3127
3128        _ssl_accepting = False
3129        _ssl_established = False
3130        _ssl_closing = False
3131        _ssl_requested = False
3132
3133        def __init__(self, *args, **kwargs):
3134            super(SSLConnection, self).__init__(*args, **kwargs)
3135            self._error = False
3136            self._ssl_want_read = False
3137            self._ssl_want_write = False
3138
3139        def readable(self):
3140            return self._ssl_want_read or \
3141                super(SSLConnection, self).readable()
3142
3143        def writable(self):
3144            return self._ssl_want_write or \
3145                super(SSLConnection, self).writable()
3146
3147        def secure_connection(self, ssl_context):
3148            """Secure the connection switching from plain-text to
3149            SSL/TLS.
3150            """
3151            debug("securing SSL connection", self)
3152            self._ssl_requested = True
3153            try:
3154                self.socket = SSL.Connection(ssl_context, self.socket)
3155            except socket.error as err:
3156                # may happen in case the client connects/disconnects
3157                # very quickly
3158                debug(
3159                    "call: secure_connection(); can't secure SSL connection "
3160                    "%r; closing" % err, self)
3161                self.close()
3162            except ValueError:
3163                # may happen in case the client connects/disconnects
3164                # very quickly
3165                if self.socket.fileno() == -1:
3166                    debug(
3167                        "ValueError and fd == -1 on secure_connection()", self)
3168                    return
3169                raise
3170            else:
3171                self.socket.set_accept_state()
3172                self._ssl_accepting = True
3173
3174        @contextlib.contextmanager
3175        def _handle_ssl_want_rw(self):
3176            prev_row_pending = self._ssl_want_read or self._ssl_want_write
3177            try:
3178                yield
3179            except SSL.WantReadError:
3180                # we should never get here; it's just for extra safety
3181                self._ssl_want_read = True
3182            except SSL.WantWriteError:
3183                # we should never get here; it's just for extra safety
3184                self._ssl_want_write = True
3185
3186            if self._ssl_want_read:
3187                self.modify_ioloop_events(
3188                    self._wanted_io_events | self.ioloop.READ, logdebug=True)
3189            elif self._ssl_want_write:
3190                self.modify_ioloop_events(
3191                    self._wanted_io_events | self.ioloop.WRITE, logdebug=True)
3192            else:
3193                if prev_row_pending:
3194                    self.modify_ioloop_events(self._wanted_io_events)
3195
3196        def _do_ssl_handshake(self):
3197            self._ssl_accepting = True
3198            self._ssl_want_read = False
3199            self._ssl_want_write = False
3200            try:
3201                self.socket.do_handshake()
3202            except SSL.WantReadError:
3203                self._ssl_want_read = True
3204                debug("call: _do_ssl_handshake, err: ssl-want-read", inst=self)
3205            except SSL.WantWriteError:
3206                self._ssl_want_write = True
3207                debug("call: _do_ssl_handshake, err: ssl-want-write",
3208                      inst=self)
3209            except SSL.SysCallError as err:
3210                debug("call: _do_ssl_handshake, err: %r" % err, inst=self)
3211                retval, desc = err.args
3212                if (retval == -1 and desc == 'Unexpected EOF') or retval > 0:
3213                    return self.handle_close()
3214                raise
3215            except SSL.Error as err:
3216                debug("call: _do_ssl_handshake, err: %r" % err, inst=self)
3217                return self.handle_failed_ssl_handshake()
3218            else:
3219                debug("SSL connection established", self)
3220                self._ssl_accepting = False
3221                self._ssl_established = True
3222                self.handle_ssl_established()
3223
3224        def handle_ssl_established(self):
3225            """Called when SSL handshake has completed."""
3226            pass
3227
3228        def handle_ssl_shutdown(self):
3229            """Called when SSL shutdown() has completed."""
3230            super(SSLConnection, self).close()
3231
3232        def handle_failed_ssl_handshake(self):
3233            raise NotImplementedError("must be implemented in subclass")
3234
3235        def handle_read_event(self):
3236            if not self._ssl_requested:
3237                super(SSLConnection, self).handle_read_event()
3238            else:
3239                with self._handle_ssl_want_rw():
3240                    self._ssl_want_read = False
3241                    if self._ssl_accepting:
3242                        self._do_ssl_handshake()
3243                    elif self._ssl_closing:
3244                        self._do_ssl_shutdown()
3245                    else:
3246                        super(SSLConnection, self).handle_read_event()
3247
3248        def handle_write_event(self):
3249            if not self._ssl_requested:
3250                super(SSLConnection, self).handle_write_event()
3251            else:
3252                with self._handle_ssl_want_rw():
3253                    self._ssl_want_write = False
3254                    if self._ssl_accepting:
3255                        self._do_ssl_handshake()
3256                    elif self._ssl_closing:
3257                        self._do_ssl_shutdown()
3258                    else:
3259                        super(SSLConnection, self).handle_write_event()
3260
3261        def handle_error(self):
3262            self._error = True
3263            try:
3264                raise
3265            except Exception:
3266                self.log_exception(self)
3267            # when facing an unhandled exception in here it's better
3268            # to rely on base class (FTPHandler or DTPHandler)
3269            # close() method as it does not imply SSL shutdown logic
3270            try:
3271                super(SSLConnection, self).close()
3272            except Exception:
3273                logger.critical(traceback.format_exc())
3274
3275        def send(self, data):
3276            if not isinstance(data, bytes):
3277                data = bytes(data)
3278            try:
3279                return super(SSLConnection, self).send(data)
3280            except SSL.WantReadError:
3281                debug("call: send(), err: ssl-want-read", inst=self)
3282                self._ssl_want_read = True
3283                return 0
3284            except SSL.WantWriteError:
3285                debug("call: send(), err: ssl-want-write", inst=self)
3286                self._ssl_want_write = True
3287                return 0
3288            except SSL.ZeroReturnError as err:
3289                debug(
3290                    "call: send() -> shutdown(), err: zero-return", inst=self)
3291                super(SSLConnection, self).handle_close()
3292                return 0
3293            except SSL.SysCallError as err:
3294                debug("call: send(), err: %r" % err, inst=self)
3295                errnum, errstr = err.args
3296                if errnum == errno.EWOULDBLOCK:
3297                    return 0
3298                elif (errnum in _ERRNOS_DISCONNECTED or
3299                        errstr == 'Unexpected EOF'):
3300                    super(SSLConnection, self).handle_close()
3301                    return 0
3302                else:
3303                    raise
3304
3305        def recv(self, buffer_size):
3306            try:
3307                return super(SSLConnection, self).recv(buffer_size)
3308            except SSL.WantReadError:
3309                debug("call: recv(), err: ssl-want-read", inst=self)
3310                self._ssl_want_read = True
3311                raise RetryError
3312            except SSL.WantWriteError:
3313                debug("call: recv(), err: ssl-want-write", inst=self)
3314                self._ssl_want_write = True
3315                raise RetryError
3316            except SSL.ZeroReturnError as err:
3317                debug("call: recv() -> shutdown(), err: zero-return",
3318                      inst=self)
3319                super(SSLConnection, self).handle_close()
3320                return b''
3321            except SSL.SysCallError as err:
3322                debug("call: recv(), err: %r" % err, inst=self)
3323                errnum, errstr = err.args
3324                if (errnum in _ERRNOS_DISCONNECTED or
3325                        errstr == 'Unexpected EOF'):
3326                    super(SSLConnection, self).handle_close()
3327                    return b''
3328                else:
3329                    raise
3330
3331        def _do_ssl_shutdown(self):
3332            """Executes a SSL_shutdown() call to revert the connection
3333            back to clear-text.
3334            twisted/internet/tcp.py code has been used as an example.
3335            """
3336            self._ssl_closing = True
3337            if os.name == 'posix':
3338                # since SSL_shutdown() doesn't report errors, an empty
3339                # write call is done first, to try to detect if the
3340                # connection has gone away
3341                try:
3342                    os.write(self.socket.fileno(), b'')
3343                except (OSError, socket.error) as err:
3344                    debug(
3345                        "call: _do_ssl_shutdown() -> os.write, err: %r" % err,
3346                        inst=self)
3347                    if err.errno in (errno.EINTR, errno.EWOULDBLOCK,
3348                                     errno.ENOBUFS):
3349                        return
3350                    elif err.errno in _ERRNOS_DISCONNECTED:
3351                        return super(SSLConnection, self).close()
3352                    else:
3353                        raise
3354            # Ok, this a mess, but the underlying OpenSSL API simply
3355            # *SUCKS* and I really couldn't do any better.
3356            #
3357            # Here we just want to shutdown() the SSL layer and then
3358            # close() the connection so we're not interested in a
3359            # complete SSL shutdown() handshake, so let's pretend
3360            # we already received a "RECEIVED" shutdown notification
3361            # from the client.
3362            # Once the client received our "SENT" shutdown notification
3363            # then we close() the connection.
3364            #
3365            # Since it is not clear what errors to expect during the
3366            # entire procedure we catch them all and assume the
3367            # following:
3368            # - WantReadError and WantWriteError means "retry"
3369            # - ZeroReturnError, SysCallError[EOF], Error[] are all
3370            #   aliases for disconnection
3371            try:
3372                laststate = self.socket.get_shutdown()
3373                self.socket.set_shutdown(laststate | SSL.RECEIVED_SHUTDOWN)
3374                done = self.socket.shutdown()
3375                if not (laststate & SSL.RECEIVED_SHUTDOWN):
3376                    self.socket.set_shutdown(SSL.SENT_SHUTDOWN)
3377            except SSL.WantReadError:
3378                self._ssl_want_read = True
3379                debug("call: _do_ssl_shutdown, err: ssl-want-read", inst=self)
3380            except SSL.WantWriteError:
3381                self._ssl_want_write = True
3382                debug("call: _do_ssl_shutdown, err: ssl-want-write", inst=self)
3383            except SSL.ZeroReturnError as err:
3384                debug(
3385                    "call: _do_ssl_shutdown() -> shutdown(), err: zero-return",
3386                    inst=self)
3387                super(SSLConnection, self).close()
3388            except SSL.SysCallError as err:
3389                debug("call: _do_ssl_shutdown() -> shutdown(), err: %r" % err,
3390                      inst=self)
3391                errnum, errstr = err.args
3392                if (errnum in _ERRNOS_DISCONNECTED or
3393                        errstr == 'Unexpected EOF'):
3394                    super(SSLConnection, self).close()
3395                else:
3396                    raise
3397            except SSL.Error as err:
3398                debug("call: _do_ssl_shutdown() -> shutdown(), err: %r" % err,
3399                      inst=self)
3400                # see:
3401                # https://github.com/giampaolo/pyftpdlib/issues/171
3402                # https://bugs.launchpad.net/pyopenssl/+bug/785985
3403                if err.args and not getattr(err, "errno", None):
3404                    pass
3405                else:
3406                    raise
3407            except socket.error as err:
3408                debug("call: _do_ssl_shutdown() -> shutdown(), err: %r" % err,
3409                      inst=self)
3410                if err.errno in _ERRNOS_DISCONNECTED:
3411                    super(SSLConnection, self).close()
3412                else:
3413                    raise
3414            else:
3415                if done:
3416                    debug("call: _do_ssl_shutdown(), shutdown completed",
3417                          inst=self)
3418                    self._ssl_established = False
3419                    self._ssl_closing = False
3420                    self.handle_ssl_shutdown()
3421                else:
3422                    debug(
3423                        "call: _do_ssl_shutdown(), shutdown not completed yet",
3424                        inst=self)
3425
3426        def close(self):
3427            if self._ssl_established and not self._error:
3428                self._do_ssl_shutdown()
3429            else:
3430                self._ssl_accepting = False
3431                self._ssl_established = False
3432                self._ssl_closing = False
3433                super(SSLConnection, self).close()
3434
3435    class TLS_DTPHandler(SSLConnection, DTPHandler):
3436        """A DTPHandler subclass supporting TLS/SSL."""
3437
3438        def __init__(self, sock, cmd_channel):
3439            super(TLS_DTPHandler, self).__init__(sock, cmd_channel)
3440            if self.cmd_channel._prot:
3441                self.secure_connection(self.cmd_channel.ssl_context)
3442
3443        def __repr__(self):
3444            return DTPHandler.__repr__(self)
3445
3446        def use_sendfile(self):
3447            if isinstance(self.socket, SSL.Connection):
3448                return False
3449            else:
3450                return super(TLS_DTPHandler, self).use_sendfile()
3451
3452        def handle_failed_ssl_handshake(self):
3453            # TLS/SSL handshake failure, probably client's fault which
3454            # used a SSL version different from server's.
3455            # RFC-4217, chapter 10.2 expects us to return 522 over the
3456            # command channel.
3457            self.cmd_channel.respond("522 SSL handshake failed.")
3458            self.cmd_channel.log_cmd("PROT", "P", 522, "SSL handshake failed.")
3459            self.close()
3460
3461    class TLS_FTPHandler(SSLConnection, FTPHandler):
3462        """A FTPHandler subclass supporting TLS/SSL.
3463        Implements AUTH, PBSZ and PROT commands (RFC-2228 and RFC-4217).
3464
3465        Configurable attributes:
3466
3467         - (bool) tls_control_required:
3468            When True requires SSL/TLS to be established on the control
3469            channel, before logging in.  This means the user will have
3470            to issue AUTH before USER/PASS (default False).
3471
3472         - (bool) tls_data_required:
3473            When True requires SSL/TLS to be established on the data
3474            channel.  This means the user will have to issue PROT
3475            before PASV or PORT (default False).
3476
3477        SSL-specific options:
3478
3479         - (string) certfile:
3480            the path to the file which contains a certificate to be
3481            used to identify the local side of the connection.
3482            This  must always be specified, unless context is provided
3483            instead.
3484
3485         - (string) keyfile:
3486            the path to the file containing the private RSA key;
3487            can be omitted if certfile already contains the private
3488            key (defaults: None).
3489
3490         - (int) ssl_protocol:
3491            the desired SSL protocol version to use. This defaults to
3492            PROTOCOL_SSLv23 which will negotiate the highest protocol
3493            that both the server and your installation of OpenSSL
3494            support.
3495
3496         - (int) ssl_options:
3497            specific OpenSSL options. These default to:
3498            SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3| SSL.OP_NO_COMPRESSION
3499            which are all considered insecure features.
3500            Can be set to None in order to improve compatibilty with
3501            older (insecure) FTP clients.
3502
3503          - (instance) ssl_context:
3504            a SSL Context object previously configured; if specified
3505            all other parameters will be ignored.
3506            (default None).
3507        """
3508
3509        # configurable attributes
3510        tls_control_required = False
3511        tls_data_required = False
3512        certfile = None
3513        keyfile = None
3514        ssl_protocol = SSL.SSLv23_METHOD
3515        # - SSLv2 is easily broken and is considered harmful and dangerous
3516        # - SSLv3 has several problems and is now dangerous
3517        # - Disable compression to prevent CRIME attacks for OpenSSL 1.0+
3518        #   (see https://github.com/shazow/urllib3/pull/309)
3519        ssl_options = SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3
3520        if hasattr(SSL, "OP_NO_COMPRESSION"):
3521            ssl_options |= SSL.OP_NO_COMPRESSION
3522        ssl_context = None
3523
3524        # overridden attributes
3525        dtp_handler = TLS_DTPHandler
3526        proto_cmds = FTPHandler.proto_cmds.copy()
3527        proto_cmds.update({
3528            'AUTH': dict(
3529                perm=None, auth=False, arg=True,
3530                help='Syntax: AUTH <SP> TLS|SSL (set up secure control '
3531                     'channel).'),
3532            'PBSZ': dict(
3533                perm=None, auth=False, arg=True,
3534                help='Syntax: PBSZ <SP> 0 (negotiate TLS buffer).'),
3535            'PROT': dict(
3536                perm=None, auth=False, arg=True,
3537                help='Syntax: PROT <SP> [C|P] (set up un/secure data '
3538                     'channel).'),
3539        })
3540
3541        def __init__(self, conn, server, ioloop=None):
3542            super(TLS_FTPHandler, self).__init__(conn, server, ioloop)
3543            if not self.connected:
3544                return
3545            self._extra_feats = ['AUTH TLS', 'AUTH SSL', 'PBSZ', 'PROT']
3546            self._pbsz = False
3547            self._prot = False
3548            self.ssl_context = self.get_ssl_context()
3549
3550        def __repr__(self):
3551            return FTPHandler.__repr__(self)
3552
3553        @classmethod
3554        def get_ssl_context(cls):
3555            if cls.ssl_context is None:
3556                if cls.certfile is None:
3557                    raise ValueError("at least certfile must be specified")
3558                cls.ssl_context = SSL.Context(cls.ssl_protocol)
3559                if cls.ssl_protocol != SSL.SSLv2_METHOD:
3560                    cls.ssl_context.set_options(SSL.OP_NO_SSLv2)
3561                else:
3562                    warnings.warn("SSLv2 protocol is insecure", RuntimeWarning)
3563                cls.ssl_context.use_certificate_chain_file(cls.certfile)
3564                if not cls.keyfile:
3565                    cls.keyfile = cls.certfile
3566                cls.ssl_context.use_privatekey_file(cls.keyfile)
3567                if cls.ssl_options:
3568                    cls.ssl_context.set_options(cls.ssl_options)
3569            return cls.ssl_context
3570
3571        # --- overridden methods
3572
3573        def flush_account(self):
3574            FTPHandler.flush_account(self)
3575            self._pbsz = False
3576            self._prot = False
3577
3578        def process_command(self, cmd, *args, **kwargs):
3579            if cmd in ('USER', 'PASS'):
3580                if self.tls_control_required and not self._ssl_established:
3581                    msg = "SSL/TLS required on the control channel."
3582                    self.respond("550 " + msg)
3583                    self.log_cmd(cmd, args[0], 550, msg)
3584                    return
3585            elif cmd in ('PASV', 'EPSV', 'PORT', 'EPRT'):
3586                if self.tls_data_required and not self._prot:
3587                    msg = "SSL/TLS required on the data channel."
3588                    self.respond("550 " + msg)
3589                    self.log_cmd(cmd, args[0], 550, msg)
3590                    return
3591            FTPHandler.process_command(self, cmd, *args, **kwargs)
3592
3593        # --- new methods
3594
3595        def handle_failed_ssl_handshake(self):
3596            # TLS/SSL handshake failure, probably client's fault which
3597            # used a SSL version different from server's.
3598            # We can't rely on the control connection anymore so we just
3599            # disconnect the client without sending any response.
3600            self.log("SSL handshake failed.")
3601            self.close()
3602
3603        def ftp_AUTH(self, line):
3604            """Set up secure control channel."""
3605            arg = line.upper()
3606            if isinstance(self.socket, SSL.Connection):
3607                self.respond("503 Already using TLS.")
3608            elif arg in ('TLS', 'TLS-C', 'SSL', 'TLS-P'):
3609                # From RFC-4217: "As the SSL/TLS protocols self-negotiate
3610                # their levels, there is no need to distinguish between SSL
3611                # and TLS in the application layer".
3612                self.respond('234 AUTH %s successful.' % arg)
3613                self.secure_connection(self.ssl_context)
3614            else:
3615                self.respond(
3616                    "502 Unrecognized encryption type (use TLS or SSL).")
3617
3618        def ftp_PBSZ(self, line):
3619            """Negotiate size of buffer for secure data transfer.
3620            For TLS/SSL the only valid value for the parameter is '0'.
3621            Any other value is accepted but ignored.
3622            """
3623            if not isinstance(self.socket, SSL.Connection):
3624                self.respond(
3625                    "503 PBSZ not allowed on insecure control connection.")
3626            else:
3627                self.respond('200 PBSZ=0 successful.')
3628                self._pbsz = True
3629
3630        def ftp_PROT(self, line):
3631            """Setup un/secure data channel."""
3632            arg = line.upper()
3633            if not isinstance(self.socket, SSL.Connection):
3634                self.respond(
3635                    "503 PROT not allowed on insecure control connection.")
3636            elif not self._pbsz:
3637                self.respond(
3638                    "503 You must issue the PBSZ command prior to PROT.")
3639            elif arg == 'C':
3640                self.respond('200 Protection set to Clear')
3641                self._prot = False
3642            elif arg == 'P':
3643                self.respond('200 Protection set to Private')
3644                self._prot = True
3645            elif arg in ('S', 'E'):
3646                self.respond('521 PROT %s unsupported (use C or P).' % arg)
3647            else:
3648                self.respond("502 Unrecognized PROT type (use C or P).")
3649