Ticket #4180: 4180-list-checks-3.diff

File 4180-list-checks-3.diff, 11.7 KB (added by adiroiban, 4 years ago)
  • twisted/protocols/ftp.py

    diff --git a/twisted/protocols/ftp.py b/twisted/protocols/ftp.py
    index b035f04..444accd 100644
    a b from zope.interface import Interface, implements 
    2525
    2626# Twisted Imports
    2727from twisted import copyright
    28 from twisted.internet import reactor, interfaces, protocol, error, defer
     28from twisted.internet import defer, error, interfaces, protocol, reactor, task
    2929from twisted.protocols import basic, policies
    3030
    3131from twisted.python import log, failure, filepath
    REQ_FILE_ACTN_PENDING_FURTHER_INFO = "350" 
    7272
    7373SVC_NOT_AVAIL_CLOSING_CTRL_CNX          = "421.1"
    7474TOO_MANY_CONNECTIONS                    = "421.2"
    75 CANT_OPEN_DATA_CNX                      = "425"
     75CANT_OPEN_DATA_CNX                      = "425.1"
     76DTP_TIMEOUT                             = "425.2"
    7677CNX_CLOSED_TXFR_ABORTED                 = "426"
    7778REQ_ACTN_ABRTD_FILE_UNAVAIL             = "450"
    7879REQ_ACTN_ABRTD_LOCAL_ERR                = "451"
    RESPONSE = { 
    141142    SVC_NOT_AVAIL_CLOSING_CTRL_CNX:     '421 Service not available, closing control connection.',
    142143    TOO_MANY_CONNECTIONS:               '421 Too many users right now, try again in a few minutes.',
    143144    CANT_OPEN_DATA_CNX:                 "425 Can't open data connection.",
     145    DTP_TIMEOUT:                        '425 Data channel initialization timed out.',
    144146    CNX_CLOSED_TXFR_ABORTED:            '426 Transfer aborted.  Data connection closed.',
    145147
    146148    REQ_ACTN_ABRTD_FILE_UNAVAIL:        '450 Requested action aborted. File unavailable.',
    class FTP(object, basic.LineReceiver, policies.TimeoutMixin): 
    777779            (self.passivePortRange,))
    778780
    779781
     782    def _checkDataTransportStarted(self, command):
     783        """Checks that data transport is ready.
     784
     785        If data transport was not requested using PORT, PASV etc it raises
     786        L{BadCmdSequenceError}.
     787        """
     788        if self.dtpInstance is None:
     789            raise BadCmdSequenceError(
     790                'PORT or PASV required before %s' % (command,))
     791
    780792    def ftp_USER(self, username):
    781793        """
    782794        First part of login.  Get the username the peer wants to
    class FTP(object, basic.LineReceiver, policies.TimeoutMixin): 
    829841
    830842
    831843    def ftp_PASV(self):
    832         """Request for a passive connection
     844        """
     845        Request for a passive connection
    833846
    834847        from the rfc::
    835848
    class FTP(object, basic.LineReceiver, policies.TimeoutMixin): 
    876889
    877890
    878891    def ftp_LIST(self, path=''):
    879         """ This command causes a list to be sent from the server to the
    880         passive DTP.  If the pathname specifies a directory or other
     892        """
     893        This command causes a list to be sent from the server to the
     894        passive DTP.
     895
     896        If the pathname specifies a directory or other
    881897        group of files, the server should transfer a list of files
    882         in the specified directory.  If the pathname specifies a
    883         file then the server should send current information on the
    884         file.  A null argument implies the user's current working or
    885         default directory.
     898        in the specified directory.
     899        If the pathname specifies a file then the server should send current
     900        information on the file.
     901        A null argument implies the user's current working or default
     902        directory.
    886903        """
    887         # Uh, for now, do this retarded thing.
    888         if self.dtpInstance is None or not self.dtpInstance.isConnected:
    889             return defer.fail(BadCmdSequenceError('must send PORT or PASV before RETR'))
     904        self._checkDataTransportStarted('LIST')
    890905
    891906        # bug in konqueror
    892907        if path == "-a":
    class FTP(object, basic.LineReceiver, policies.TimeoutMixin): 
    908923            self.dtpInstance.transport.loseConnection()
    909924            return (TXFR_COMPLETE_OK,)
    910925
     926        def ebDTPTimeout(failure):
     927            failure.trap(defer.TimeoutError)
     928            self.reply(DTP_TIMEOUT)
     929
    911930        try:
    912931            segments = toSegments(self.workingDirectory, path)
    913932        except InvalidPath:
    class FTP(object, basic.LineReceiver, policies.TimeoutMixin): 
    917936            segments,
    918937            ('size', 'directory', 'permissions', 'hardlinks',
    919938             'modified', 'owner', 'group'))
     939        d.addCallback(self._cbWaitDTPConnectionWithTimeout)
    920940        d.addCallback(gotListing)
     941        d.addErrback(ebDTPTimeout)
    921942        return d
    922943
    923944
    class FTP(object, basic.LineReceiver, policies.TimeoutMixin): 
    936957        @return: a L{Deferred} which will be fired when the listing request
    937958            is finished.
    938959        """
    939         # XXX: why is this check different from ftp_RETR/ftp_STOR? See #4180
    940         if self.dtpInstance is None or not self.dtpInstance.isConnected:
    941             return defer.fail(
    942                 BadCmdSequenceError('must send PORT or PASV before RETR'))
    943 
     960        self._checkDataTransportStarted('NLST')
    944961        try:
    945962            segments = toSegments(self.workingDirectory, path)
    946963        except InvalidPath:
    class FTP(object, basic.LineReceiver, policies.TimeoutMixin): 
    9951012            '*' in segments[-1] or '?' in segments[-1] or
    9961013            ('[' in segments[-1] and ']' in segments[-1])):
    9971014            d = self.shell.list(segments[:-1])
     1015            d.addCallback(self._cbWaitDTPConnectionWithTimeout)
    9981016            d.addCallback(cbGlob)
    9991017        else:
    10001018            d = self.shell.list(segments)
     1019            d.addCallback(self._cbWaitDTPConnectionWithTimeout)
    10011020            d.addCallback(cbList)
    1002             # self.shell.list will generate an error if the path is invalid
    1003             d.addErrback(listErr)
     1021
     1022        # self.shell.list will generate an error if the path is invalid
     1023        d.addErrback(listErr)
    10041024        return d
    10051025
     1026    def _cbWaitDTPConnectionWithTimeout(self, result):
     1027        """
     1028        Helper callback that waits for DTP instance to be connected.
     1029
     1030        It will raise a C{PortConnectionError} if DTP instance is not
     1031        connected after the interval defined by self.factory.timeOut.
     1032        """
     1033        def ebDTPTimeout(failure):
     1034            """
     1035            Called at data transport port timeout.
     1036            """
     1037            failure.trap(defer.CancelledError)
     1038            return defer.fail(defer.TimeoutError("DTP connection timeout"))
     1039
     1040        def cbContinueCommand(ignore, timeoutCall):
     1041            if timeoutCall is not None and timeoutCall.active():
     1042                timeoutCall.cancel()
     1043            return result
     1044
     1045        def cbCallTimeout(ignore):
     1046            self.dtpFactory.deferred.cancel()
     1047
     1048        timeoutCall = self.factory.callLater(
     1049            self.factory.timeOut, cbCallTimeout, None)
     1050
     1051        self.dtpFactory.deferred.addCallback(cbContinueCommand, timeoutCall)
     1052        self.dtpFactory.deferred.addErrback(ebDTPTimeout)
     1053
     1054        return self.dtpFactory.deferred
    10061055
    10071056    def ftp_CWD(self, path):
    10081057        try:
    class FTP(object, basic.LineReceiver, policies.TimeoutMixin): 
    10381087        @rtype: L{Deferred}
    10391088        @return: a L{Deferred} which will be fired when the transfer is done.
    10401089        """
    1041         if self.dtpInstance is None:
    1042             raise BadCmdSequenceError('PORT or PASV required before RETR')
     1090        self._checkDataTransportStarted('RETR')
    10431091
    10441092        try:
    10451093            newsegs = toSegments(self.workingDirectory, path)
    class FTP(object, basic.LineReceiver, policies.TimeoutMixin): 
    11001148
    11011149
    11021150    def ftp_STOR(self, path):
    1103         if self.dtpInstance is None:
    1104             raise BadCmdSequenceError('PORT or PASV required before STOR')
     1151        """
     1152        This command causes the server-DTP to accept the data
     1153        transferred via the data connection and to store the data as
     1154        a file at the server site.  If the file specified in the
     1155        pathname exists at the server site, then its contents shall
     1156        be replaced by the data being transferred.  A new file is
     1157        created at the server site if the file specified in the
     1158        pathname does not already exist.
     1159
     1160        @type path: C{str}
     1161        @param path: The file path where the content should be stored.
     1162
     1163        @rtype: L{Deferred}
     1164        @return: a L{Deferred} which will be fired when the transfer
     1165            is finished.
     1166        """
     1167        self._checkDataTransportStarted('STOR')
    11051168
    11061169        try:
    11071170            newsegs = toSegments(self.workingDirectory, path)
    class FTP(object, basic.LineReceiver, policies.TimeoutMixin): 
    12891352
    12901353
    12911354    def cleanupDTP(self):
    1292         """call when DTP connection exits
     1355        """
     1356        Called when DTP connection exits.
    12931357        """
    12941358        log.msg('cleanupDTP', debug=True)
    12951359
    class FTPFactory(policies.LimitTotalConnectionsFactory): 
    13291393
    13301394    passivePortRange = xrange(0, 1)
    13311395
     1396    callLater = reactor.callLater
     1397
    13321398    def __init__(self, portal=None, userAnonymous='anonymous'):
    13331399        self.portal = portal
    13341400        self.userAnonymous = userAnonymous
  • twisted/test/test_ftp.py

    diff --git a/twisted/test/test_ftp.py b/twisted/test/test_ftp.py
    index 23ffcba..8789132 100644
    a b class BasicFTPServerTestCase(FTPServerTestCase): 
    463463        self.assertEqual(portRange, protocol.wrappedProtocol.passivePortRange)
    464464
    465465
     466    def _startDataConnection(self):
     467        """
     468        Prepare data transport protocol to look like it was created by
     469        a previous call to PASV or PORT
     470        """
     471        self.serverProtocol.dtpFactory = ftp.DTPFactory(self.serverProtocol)
     472        self.serverProtocol.dtpFactory.buildProtocol('ignore_address')
     473
     474        self.serverProtocol.dtpPort = self.serverProtocol.listenFactory(
     475            6000, self.serverProtocol.dtpFactory)
     476
     477        dtpTransport = proto_helpers.StringTransportWithDisconnection()
     478        dtpTransport.protocol = ftp.DTP()
     479        self.serverProtocol.dtpInstance.transport = dtpTransport
     480
     481
     482    def test_LISTWithoutDataChannel(self):
     483        """
     484        Calling LIST without prior setup of data connection will result in a
     485        incorrect sequence of commands error.
     486        """
     487        d = self._anonymousLogin()
     488        self.assertCommandFailed(
     489            'LIST .',
     490            ["503 Incorrect sequence of commands: "
     491             "PORT or PASV required before LIST"],
     492            chainDeferred=d)
     493        return d
     494
     495
     496    def test_LISTTimeout(self):
     497        """
     498        LIST will timeout if setting up the DTP instance will take to long.
     499        """
     500        # Set timeout to a very small value to not slow down tests.
     501        self.factory.timeOut = 0.01
     502
     503        d = self._anonymousLogin()
     504        self._startDataConnection()
     505
     506        self.assertCommandFailed(
     507            'LIST .',
     508            ["425 Data channel initialization timed out."],
     509            chainDeferred=d)
     510        return d
     511
     512
     513    def test_NLSTWithoutDataChannel(self):
     514        """
     515        Calling NLST without prior setup of data connection will result in a
     516        incorrect sequence of commands error.
     517        """
     518        d = self._anonymousLogin()
     519        self.assertCommandFailed(
     520            'NLST .',
     521            ["503 Incorrect sequence of commands: "
     522             "PORT or PASV required before NLST"],
     523            chainDeferred=d)
     524        return d
     525
     526
     527    def test_NLSTTimeout(self):
     528        """
     529        NLST will timeout if setting up the DTP instance will take to long,
     530        but NLST will not return any error.
     531        """
     532        self.factory.timeOut = 0.01
     533
     534        d = self._anonymousLogin()
     535        self._startDataConnection()
     536
     537        # Set timeout to a very small value to not slow down tests.
     538        self.assertCommandResponse(
     539            'NLST .',
     540            ["226 Transfer Complete."],
     541            chainDeferred=d)
     542        return d
     543
     544
    466545
    467546class FTPServerTestCaseAdvancedClient(FTPServerTestCase):
    468547    """