Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
  • 6 commits
  • 3 files changed
  • 0 comments
  • 1 contributor
3  .gitignore
... ...
@@ -0,0 +1,3 @@
  1
+*.pyc
  2
+_trial_temp/
  3
+twisted/plugins/dropin.cache
94  twisted/protocols/ftp.py
@@ -25,7 +25,7 @@
25 25
 
26 26
 # Twisted Imports
27 27
 from twisted import copyright
28  
-from twisted.internet import reactor, interfaces, protocol, error, defer
  28
+from twisted.internet import reactor, interfaces, protocol, task, error, defer
29 29
 from twisted.protocols import basic, policies
30 30
 
31 31
 from twisted.python import log, failure, filepath
@@ -72,7 +72,8 @@
72 72
 
73 73
 SVC_NOT_AVAIL_CLOSING_CTRL_CNX          = "421.1"
74 74
 TOO_MANY_CONNECTIONS                    = "421.2"
75  
-CANT_OPEN_DATA_CNX                      = "425"
  75
+CANT_OPEN_DATA_CNX                      = "425.1"
  76
+DTP_TIMEOUT                             = "425.2"
76 77
 CNX_CLOSED_TXFR_ABORTED                 = "426"
77 78
 REQ_ACTN_ABRTD_FILE_UNAVAIL             = "450"
78 79
 REQ_ACTN_ABRTD_LOCAL_ERR                = "451"
@@ -141,6 +142,7 @@
141 142
     SVC_NOT_AVAIL_CLOSING_CTRL_CNX:     '421 Service not available, closing control connection.',
142 143
     TOO_MANY_CONNECTIONS:               '421 Too many users right now, try again in a few minutes.',
143 144
     CANT_OPEN_DATA_CNX:                 "425 Can't open data connection.",
  145
+    DTP_TIMEOUT:                        '425 Data channel initialization timed out.',
144 146
     CNX_CLOSED_TXFR_ABORTED:            '426 Transfer aborted.  Data connection closed.',
145 147
 
146 148
     REQ_ACTN_ABRTD_FILE_UNAVAIL:        '450 Requested action aborted. File unavailable.',
@@ -648,6 +650,10 @@ class FTP(object, basic.LineReceiver, policies.TimeoutMixin):
648 650
     # States an FTP can be in
649 651
     UNAUTH, INAUTH, AUTHED, RENAMING = range(4)
650 652
 
  653
+    # Human readeable text listing command that can be used for setting up a
  654
+    # data connection channel.
  655
+    DATA_CONNECTION_COMMANDS = 'PORT or PASV'
  656
+
651 657
     # how long the DTP waits for a connection
652 658
     dtpTimeout = 10
653 659
 
@@ -777,6 +783,18 @@ def getDTPPort(self, factory):
777 783
             (self.passivePortRange,))
778 784
 
779 785
 
  786
+    def checkDataTransportStarted(self, command):
  787
+        '''Checks that data transport is ready.
  788
+
  789
+        If data transport was not requeted using PORT, PASV etc it raises
  790
+        L{BadCmdSequenceError}.
  791
+        '''
  792
+        if self.dtpInstance is None:
  793
+            raise BadCmdSequenceError(
  794
+                '%s required before %s' % (
  795
+                    self.DATA_CONNECTION_COMMANDS, command,))
  796
+
  797
+
780 798
     def ftp_USER(self, username):
781 799
         """
782 800
         First part of login.  Get the username the peer wants to
@@ -884,9 +902,7 @@ def ftp_LIST(self, path=''):
884 902
         file.  A null argument implies the user's current working or
885 903
         default directory.
886 904
         """
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'))
  905
+        self.checkDataTransportStarted('LIST')
890 906
 
891 907
         # bug in konqueror
892 908
         if path == "-a":
@@ -917,6 +933,7 @@ def gotListing(results):
917 933
             segments,
918 934
             ('size', 'directory', 'permissions', 'hardlinks',
919 935
              'modified', 'owner', 'group'))
  936
+        d.addCallback(self._cbWaitDTPConnectionWithTimeout)
920 937
         d.addCallback(gotListing)
921 938
         return d
922 939
 
@@ -936,11 +953,7 @@ def ftp_NLST(self, path):
936 953
         @return: a L{Deferred} which will be fired when the listing request
937 954
             is finished.
938 955
         """
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  
-
  956
+        self.checkDataTransportStarted('NLST')
944 957
         try:
945 958
             segments = toSegments(self.workingDirectory, path)
946 959
         except InvalidPath:
@@ -987,6 +1000,7 @@ def listErr(results):
987 1000
             @returns: A C{tuple} containing the status code for a successful
988 1001
                 transfer.
989 1002
             """
  1003
+            log.err(results)
990 1004
             self.dtpInstance.transport.loseConnection()
991 1005
             return (TXFR_COMPLETE_OK,)
992 1006
 
@@ -995,14 +1009,48 @@ def listErr(results):
995 1009
             '*' in segments[-1] or '?' in segments[-1] or
996 1010
             ('[' in segments[-1] and ']' in segments[-1])):
997 1011
             d = self.shell.list(segments[:-1])
  1012
+            d.addCallback(self._cbWaitDTPConnectionWithTimeout)
998 1013
             d.addCallback(cbGlob)
999 1014
         else:
1000 1015
             d = self.shell.list(segments)
  1016
+            d.addCallback(self._cbWaitDTPConnectionWithTimeout)
1001 1017
             d.addCallback(cbList)
1002  
-            # self.shell.list will generate an error if the path is invalid
1003  
-            d.addErrback(listErr)
  1018
+
  1019
+        # self.shell.list will generate an error if the path is invalid
  1020
+        d.addErrback(listErr)
1004 1021
         return d
1005 1022
 
  1023
+    def _cbWaitDTPConnectionWithTimeout(self, result):
  1024
+        """Helper callback that waits for DTP instance to be connected.
  1025
+
  1026
+        It will raise a {PortConnectionError} if DTP instance is not
  1027
+        connected after the interval defined by self.factory.timeOut.
  1028
+        """
  1029
+        def cancelTimeout():
  1030
+            if timout_call is not None and timout_call.active():
  1031
+                log.msg('cancelling DTP timeout', debug=True)
  1032
+                return timout_call.cancel()
  1033
+
  1034
+        def cbCheckDTPInstance(value):
  1035
+            check_result = None
  1036
+
  1037
+            if timout_call.called:
  1038
+                self.reply(DTP_TIMEOUT)
  1039
+                check_result = defer.fail(
  1040
+                    PortConnectionError(
  1041
+                        defer.TimeoutError("DTP connection timeout")))
  1042
+            elif self.dtpInstance and self.dtpInstance.isConnected:
  1043
+                cancelTimeout()
  1044
+                check_result = result
  1045
+            else:
  1046
+                check_result = task.deferLater(
  1047
+                    reactor, 0.1, cbCheckDTPInstance, None)
  1048
+            
  1049
+            return check_result
  1050
+
  1051
+        timout_call = reactor.callLater(
  1052
+            self.factory.timeOut, lambda ignore: ignore, None)
  1053
+        return cbCheckDTPInstance(None)
1006 1054
 
1007 1055
     def ftp_CWD(self, path):
1008 1056
         try:
@@ -1038,8 +1086,7 @@ def ftp_RETR(self, path):
1038 1086
         @rtype: L{Deferred}
1039 1087
         @return: a L{Deferred} which will be fired when the transfer is done.
1040 1088
         """
1041  
-        if self.dtpInstance is None:
1042  
-            raise BadCmdSequenceError('PORT or PASV required before RETR')
  1089
+        self.checkDataTransportStarted('RETR')
1043 1090
 
1044 1091
         try:
1045 1092
             newsegs = toSegments(self.workingDirectory, path)
@@ -1098,8 +1145,23 @@ def ebOpened(err):
1098 1145
 
1099 1146
 
1100 1147
     def ftp_STOR(self, path):
1101  
-        if self.dtpInstance is None:
1102  
-            raise BadCmdSequenceError('PORT or PASV required before STOR')
  1148
+        """
  1149
+        This command causes the server-DTP to accept the data
  1150
+        transferred via the data connection and to store the data as
  1151
+        a file at the server site.  If the file specified in the
  1152
+        pathname exists at the server site, then its contents shall
  1153
+        be replaced by the data being transferred.  A new file is
  1154
+        created at the server site if the file specified in the
  1155
+        pathname does not already exist.
  1156
+
  1157
+        @type path: C{str}
  1158
+        @param path: The file path where the content should be stored.
  1159
+
  1160
+        @rtype: L{Deferred}
  1161
+        @return: a L{Deferred} which will be fired when the transfer
  1162
+            is finished.
  1163
+        """
  1164
+        self.checkDataTransportStarted('STOR')
1103 1165
 
1104 1166
         try:
1105 1167
             newsegs = toSegments(self.workingDirectory, path)
94  twisted/test/test_ftp.py
@@ -399,6 +399,84 @@ def test_portRangeInheritedFromFactory(self):
399 399
         protocol = self.factory.buildProtocol(None)
400 400
         self.assertEqual(portRange, protocol.wrappedProtocol.passivePortRange)
401 401
 
  402
+    def _startDataConnection(self):
  403
+        '''Prepare data transport protocol to look like it was created by
  404
+        a previous call to PASV or PORT'''
  405
+        dtp_factory = ftp.DTPFactory(self.serverProtocol)
  406
+        dtp_factory.buildProtocol('ignore_address')
  407
+        dtp_transport = proto_helpers.StringTransportWithDisconnection()
  408
+        dtp_transport.protocol = ftp.DTP()
  409
+        self.serverProtocol.dtpInstance.transport = dtp_transport
  410
+
  411
+    def _checkTimeoutLog(self, result):
  412
+        '''Check the log after a timeout error.'''
  413
+        current_errors = self.flushLoggedErrors()
  414
+        self.assertEqual(1, len(current_errors))
  415
+        self.assertIsInstance(
  416
+            current_errors[0].value, ftp.PortConnectionError)
  417
+
  418
+    def test_LISTWithoutDataChannel(self):
  419
+        """
  420
+        Calling LIST without prior setup of data connection will result in a
  421
+        incorrect sequence of commands error.
  422
+        """
  423
+        d = self._anonymousLogin()
  424
+        self.assertCommandFailed(
  425
+            'LIST .',
  426
+            ["503 Incorrect sequence of commands: "
  427
+             "PORT or PASV required before LIST"],
  428
+            chainDeferred=d)
  429
+        return d
  430
+
  431
+    def test_LISTTimeout(self):
  432
+        """
  433
+        LIST will timeout if setting up the DTP instance will take to long.
  434
+        """
  435
+        d = self._anonymousLogin()
  436
+        self._startDataConnection()
  437
+
  438
+        # Set timeout to a very small value to not slow down tests.
  439
+        self.serverProtocol.factory.timeOut = 0.01
  440
+
  441
+        self.assertCommandFailed(
  442
+            'LIST .',
  443
+            ["425 Data channel initialization timed out."],
  444
+            chainDeferred=d)
  445
+
  446
+        d.addCallback(self._checkTimeoutLog)
  447
+        return d
  448
+
  449
+    def test_NLSTWithoutDataChannel(self):
  450
+        """
  451
+        Calling NLST without prior setup of data connection will result in a
  452
+        incorrect sequence of commands error.
  453
+        """
  454
+        d = self._anonymousLogin()
  455
+        self.assertCommandFailed(
  456
+            'NLST .',
  457
+            ["503 Incorrect sequence of commands: "
  458
+             "PORT or PASV required before NLST"],
  459
+            chainDeferred=d)
  460
+        return d
  461
+
  462
+    def test_NLSTTimeout(self):
  463
+        """
  464
+        NLST will timeout if setting up the DTP instance will take to long.
  465
+        """
  466
+        d = self._anonymousLogin()
  467
+
  468
+        self._startDataConnection()
  469
+
  470
+        # Set timeout to a very small value to not slow down tests.
  471
+        self.serverProtocol.factory.timeOut = 0.01
  472
+
  473
+        self.assertCommandFailed(
  474
+            'NLST .',
  475
+            ["425 Data channel initialization timed out."],
  476
+            chainDeferred=d)
  477
+
  478
+        d.addCallback(self._checkTimeoutLog)
  479
+        return d
402 480
 
403 481
 
404 482
 class FTPServerTestCaseAdvancedClient(FTPServerTestCase):
@@ -604,13 +682,23 @@ def test_NLSTNonexistent(self):
604 682
         """
605 683
         NLST on a non-existent file/directory returns nothing.
606 684
         """
  685
+        def checkDownload(download):
  686
+            self.assertEqual('', download)
  687
+
  688
+        def checkErrorLog(result):
  689
+            current_errors = self.flushLoggedErrors()
  690
+            self.assertEqual(1, len(current_errors))
  691
+            self.assertIsInstance(
  692
+                current_errors[0].value, ftp.FileNotFoundError)
  693
+
607 694
         # Login
608 695
         d = self._anonymousLogin()
609 696
 
610 697
         self._download('NLST nonexistent.txt', chainDeferred=d)
611  
-        def checkDownload(download):
612  
-            self.assertEqual('', download)
613  
-        return d.addCallback(checkDownload)
  698
+
  699
+        d.addCallback(checkDownload)
  700
+        d.addCallback(checkErrorLog)
  701
+        return d
614 702
 
615 703
 
616 704
     def test_NLSTOnPathToFile(self):

No commit comments for this range

Something went wrong with that request. Please try again.