~hardware-certification/zope3/certify-staging-2.5

« back to all changes in this revision

Viewing changes to src/twisted/test/test_ftp.py

  • Committer: Marc Tardif
  • Date: 2008-04-26 19:03:34 UTC
  • Revision ID: cr3@lime-20080426190334-u16xo4llz56vliqf
Initial import.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
 
2
# See LICENSE for details.
 
3
 
 
4
 
 
5
"""FTP tests.
 
6
 
 
7
Maintainer: U{Andrew Bennetts<mailto:spiv@twistedmatrix.com>}
 
8
"""
 
9
 
 
10
from __future__ import nested_scopes
 
11
 
 
12
import os.path
 
13
from StringIO import StringIO
 
14
import shutil
 
15
 
 
16
from twisted.trial import unittest
 
17
from twisted.protocols import basic
 
18
from twisted.internet import reactor, protocol, defer, error
 
19
from twisted.cred import portal, checkers, credentials
 
20
from twisted.python import failure
 
21
from twisted.test import proto_helpers
 
22
 
 
23
from twisted.protocols import ftp, loopback
 
24
 
 
25
class NonClosingStringIO(StringIO):
 
26
    def close(self):
 
27
        pass
 
28
 
 
29
StringIOWithoutClosing = NonClosingStringIO
 
30
 
 
31
 
 
32
 
 
33
 
 
34
class Dummy(basic.LineReceiver):
 
35
    logname = None
 
36
    def __init__(self):
 
37
        self.lines = []
 
38
        self.rawData = []
 
39
    def connectionMade(self):
 
40
        self.f = self.factory   # to save typing in pdb :-)
 
41
    def lineReceived(self,line):
 
42
        self.lines.append(line)
 
43
    def rawDataReceived(self, data):
 
44
        self.rawData.append(data)
 
45
    def lineLengthExceeded(self, line):
 
46
        pass
 
47
 
 
48
 
 
49
class _BufferingProtocol(protocol.Protocol):
 
50
    def connectionMade(self):
 
51
        self.buffer = ''
 
52
        self.d = defer.Deferred()
 
53
    def dataReceived(self, data):
 
54
        self.buffer += data
 
55
    def connectionLost(self, reason):
 
56
        self.d.callback(self)
 
57
 
 
58
 
 
59
class FTPServerTestCase(unittest.TestCase):
 
60
    """Simple tests for an FTP server with the default settings."""
 
61
 
 
62
    def setUp(self):
 
63
        # Create a directory
 
64
        self.directory = self.mktemp()
 
65
        os.mkdir(self.directory)
 
66
 
 
67
        # Start the server
 
68
        p = portal.Portal(ftp.FTPRealm(self.directory))
 
69
        p.registerChecker(checkers.AllowAnonymousAccess(),
 
70
                          credentials.IAnonymous)
 
71
        self.factory = ftp.FTPFactory(portal=p)
 
72
        self.port = reactor.listenTCP(0, self.factory, interface="127.0.0.1")
 
73
 
 
74
        # Hook the server's buildProtocol to make the protocol instance
 
75
        # accessible to tests.
 
76
        buildProtocol = self.factory.buildProtocol
 
77
        d1 = defer.Deferred()
 
78
        def _rememberProtocolInstance(addr):
 
79
            protocol = buildProtocol(addr)
 
80
            self.serverProtocol = protocol.wrappedProtocol
 
81
            d1.callback(None)
 
82
            return protocol
 
83
        self.factory.buildProtocol = _rememberProtocolInstance
 
84
 
 
85
        # Connect a client to it
 
86
        portNum = self.port.getHost().port
 
87
        clientCreator = protocol.ClientCreator(reactor, ftp.FTPClientBasic)
 
88
        d2 = clientCreator.connectTCP("127.0.0.1", portNum)
 
89
        def gotClient(client):
 
90
            self.client = client
 
91
        d2.addCallback(gotClient)
 
92
        return defer.gatherResults([d1, d2])
 
93
 
 
94
    def tearDown(self):
 
95
        # Clean up sockets
 
96
        self.client.transport.loseConnection()
 
97
        d = defer.maybeDeferred(self.port.stopListening)
 
98
        d.addCallback(self.ebTearDown)
 
99
        return d
 
100
 
 
101
    def ebTearDown(self, ignore):
 
102
        del self.serverProtocol
 
103
        # Clean up temporary directory
 
104
        shutil.rmtree(self.directory)
 
105
 
 
106
    def assertCommandResponse(self, command, expectedResponseLines,
 
107
                              chainDeferred=None):
 
108
        """Asserts that a sending an FTP command receives the expected
 
109
        response.
 
110
 
 
111
        Returns a Deferred.  Optionally accepts a deferred to chain its actions
 
112
        to.
 
113
        """
 
114
        if chainDeferred is None:
 
115
            chainDeferred = defer.succeed(None)
 
116
 
 
117
        def queueCommand(ignored):
 
118
            d = self.client.queueStringCommand(command)
 
119
            def gotResponse(responseLines):
 
120
                self.assertEquals(expectedResponseLines, responseLines)
 
121
            return d.addCallback(gotResponse)
 
122
        return chainDeferred.addCallback(queueCommand)
 
123
 
 
124
    def assertCommandFailed(self, command, expectedResponse=None,
 
125
                            chainDeferred=None):
 
126
        if chainDeferred is None:
 
127
            chainDeferred = defer.succeed(None)
 
128
 
 
129
        def queueCommand(ignored):
 
130
            return self.client.queueStringCommand(command)
 
131
        chainDeferred.addCallback(queueCommand)
 
132
        self.assertFailure(chainDeferred, ftp.CommandFailed)
 
133
        def failed(exception):
 
134
            if expectedResponse is not None:
 
135
                self.failUnlessEqual(
 
136
                    expectedResponse, exception.args[0])
 
137
        return chainDeferred.addCallback(failed)
 
138
 
 
139
    def _anonymousLogin(self):
 
140
        d = self.assertCommandResponse(
 
141
            'USER anonymous',
 
142
            ['331 Guest login ok, type your email address as password.'])
 
143
        return self.assertCommandResponse(
 
144
            'PASS test@twistedmatrix.com',
 
145
            ['230 Anonymous login ok, access restrictions apply.'],
 
146
            chainDeferred=d)
 
147
 
 
148
 
 
149
class BasicFTPServerTestCase(FTPServerTestCase):
 
150
    def testNotLoggedInReply(self):
 
151
        """When not logged in, all commands other than USER and PASS should
 
152
        get NOT_LOGGED_IN errors.
 
153
        """
 
154
        commandList = ['CDUP', 'CWD', 'LIST', 'MODE', 'PASV',
 
155
                       'PWD', 'RETR', 'STRU', 'SYST', 'TYPE']
 
156
 
 
157
        # Issue commands, check responses
 
158
        def checkResponse(exception):
 
159
            failureResponseLines = exception.args[0]
 
160
            self.failUnless(failureResponseLines[-1].startswith("530"),
 
161
                            "Response didn't start with 530: %r"
 
162
                            % (failureResponseLines[-1],))
 
163
        deferreds = []
 
164
        for command in commandList:
 
165
            deferred = self.client.queueStringCommand(command)
 
166
            self.assertFailure(deferred, ftp.CommandFailed)
 
167
            deferred.addCallback(checkResponse)
 
168
            deferreds.append(deferred)
 
169
        return defer.DeferredList(deferreds, fireOnOneErrback=True)
 
170
 
 
171
    def testPASSBeforeUSER(self):
 
172
        """Issuing PASS before USER should give an error."""
 
173
        return self.assertCommandFailed(
 
174
            'PASS foo',
 
175
            ["503 Incorrect sequence of commands: "
 
176
             "USER required before PASS"])
 
177
 
 
178
    def testNoParamsForUSER(self):
 
179
        """Issuing USER without a username is a syntax error."""
 
180
        return self.assertCommandFailed(
 
181
            'USER',
 
182
            ['500 Syntax error: USER requires an argument.'])
 
183
 
 
184
    def testNoParamsForPASS(self):
 
185
        """Issuing PASS without a password is a syntax error."""
 
186
        d = self.client.queueStringCommand('USER foo')
 
187
        return self.assertCommandFailed(
 
188
            'PASS',
 
189
            ['500 Syntax error: PASS requires an argument.'],
 
190
            chainDeferred=d)
 
191
 
 
192
    def testAnonymousLogin(self):
 
193
        return self._anonymousLogin()
 
194
 
 
195
    def testQuit(self):
 
196
        """Issuing QUIT should return a 221 message."""
 
197
        d = self._anonymousLogin()
 
198
        return self.assertCommandResponse(
 
199
            'QUIT',
 
200
            ['221 Goodbye.'],
 
201
            chainDeferred=d)
 
202
 
 
203
    def testAnonymousLoginDenied(self):
 
204
        # Reconfigure the server to disallow anonymous access, and to have an
 
205
        # IUsernamePassword checker that always rejects.
 
206
        self.factory.allowAnonymous = False
 
207
        denyAlwaysChecker = checkers.InMemoryUsernamePasswordDatabaseDontUse()
 
208
        self.factory.portal.registerChecker(denyAlwaysChecker,
 
209
                                            credentials.IUsernamePassword)
 
210
 
 
211
        # Same response code as allowAnonymous=True, but different text.
 
212
        d = self.assertCommandResponse(
 
213
            'USER anonymous',
 
214
            ['331 Password required for anonymous.'])
 
215
 
 
216
        # It will be denied.  No-one can login.
 
217
        d = self.assertCommandFailed(
 
218
            'PASS test@twistedmatrix.com',
 
219
            ['530 Sorry, Authentication failed.'],
 
220
            chainDeferred=d)
 
221
 
 
222
        # It's not just saying that.  You aren't logged in.
 
223
        d = self.assertCommandFailed(
 
224
            'PWD',
 
225
            ['530 Please login with USER and PASS.'],
 
226
            chainDeferred=d)
 
227
        return d
 
228
 
 
229
    def testUnknownCommand(self):
 
230
        d = self._anonymousLogin()
 
231
        return self.assertCommandFailed(
 
232
            'GIBBERISH',
 
233
            ["502 Command 'GIBBERISH' not implemented"],
 
234
            chainDeferred=d)
 
235
 
 
236
    def testRETRBeforePORT(self):
 
237
        d = self._anonymousLogin()
 
238
        return self.assertCommandFailed(
 
239
            'RETR foo',
 
240
            ["503 Incorrect sequence of commands: "
 
241
             "PORT or PASV required before RETR"],
 
242
            chainDeferred=d)
 
243
 
 
244
    def testSTORBeforePORT(self):
 
245
        d = self._anonymousLogin()
 
246
        return self.assertCommandFailed(
 
247
            'STOR foo',
 
248
            ["503 Incorrect sequence of commands: "
 
249
             "PORT or PASV required before STOR"],
 
250
            chainDeferred=d)
 
251
 
 
252
    def testBadCommandArgs(self):
 
253
        d = self._anonymousLogin()
 
254
        self.assertCommandFailed(
 
255
            'MODE z',
 
256
            ["504 Not implemented for parameter 'z'."],
 
257
            chainDeferred=d)
 
258
        self.assertCommandFailed(
 
259
            'STRU I',
 
260
            ["504 Not implemented for parameter 'I'."],
 
261
            chainDeferred=d)
 
262
        return d
 
263
 
 
264
    def testDecodeHostPort(self):
 
265
        self.assertEquals(ftp.decodeHostPort('25,234,129,22,100,23'),
 
266
                ('25.234.129.22', 25623))
 
267
        nums = range(6)
 
268
        for i in range(6):
 
269
            badValue = list(nums)
 
270
            badValue[i] = 256
 
271
            s = ','.join(map(str, badValue))
 
272
            self.assertRaises(ValueError, ftp.decodeHostPort, s)
 
273
 
 
274
    def testPASV(self):
 
275
        # Login
 
276
        yield defer.waitForDeferred(self._anonymousLogin())
 
277
 
 
278
        # Issue a PASV command, and extract the host and port from the response
 
279
        pasvCmd = defer.waitForDeferred(self.client.queueStringCommand('PASV'))
 
280
        yield pasvCmd
 
281
        responseLines = pasvCmd.getResult()
 
282
        host, port = ftp.decodeHostPort(responseLines[-1][4:])
 
283
 
 
284
        # Make sure the server is listening on the port it claims to be
 
285
        self.assertEqual(port, self.serverProtocol.dtpPort.getHost().port)
 
286
 
 
287
        # Semi-reasonable way to force cleanup
 
288
        self.serverProtocol.connectionLost(error.ConnectionDone())
 
289
    testPASV = defer.deferredGenerator(testPASV)
 
290
 
 
291
    def testSYST(self):
 
292
        d = self._anonymousLogin()
 
293
        self.assertCommandResponse('SYST', ["215 UNIX Type: L8"],
 
294
                                   chainDeferred=d)
 
295
        return d
 
296
 
 
297
class FTPServerPasvDataConnectionTestCase(FTPServerTestCase):
 
298
    def _makeDataConnection(self, ignored=None):
 
299
        # Establish a passive data connection (i.e. client connecting to
 
300
        # server).
 
301
        d = self.client.queueStringCommand('PASV')
 
302
        def gotPASV(responseLines):
 
303
            host, port = ftp.decodeHostPort(responseLines[-1][4:])
 
304
            cc = protocol.ClientCreator(reactor, _BufferingProtocol)
 
305
            return cc.connectTCP('127.0.0.1', port)
 
306
        return d.addCallback(gotPASV)
 
307
 
 
308
    def _download(self, command, chainDeferred=None):
 
309
        if chainDeferred is None:
 
310
            chainDeferred = defer.succeed(None)
 
311
 
 
312
        chainDeferred.addCallback(self._makeDataConnection)
 
313
        def queueCommand(downloader):
 
314
            # wait for the command to return, and the download connection to be
 
315
            # closed.
 
316
            d1 = self.client.queueStringCommand(command)
 
317
            d2 = downloader.d
 
318
            return defer.gatherResults([d1, d2])
 
319
        chainDeferred.addCallback(queueCommand)
 
320
 
 
321
        def downloadDone((ignored, downloader)):
 
322
            return downloader.buffer
 
323
        return chainDeferred.addCallback(downloadDone)
 
324
 
 
325
    def testEmptyLIST(self):
 
326
        # Login
 
327
        d = self._anonymousLogin()
 
328
 
 
329
        # No files, so the file listing should be empty
 
330
        self._download('LIST', chainDeferred=d)
 
331
        def checkEmpty(result):
 
332
            self.assertEqual('', result)
 
333
        return d.addCallback(checkEmpty)
 
334
 
 
335
    def testTwoDirLIST(self):
 
336
        # Make some directories
 
337
        os.mkdir(os.path.join(self.directory, 'foo'))
 
338
        os.mkdir(os.path.join(self.directory, 'bar'))
 
339
 
 
340
        # Login
 
341
        d = self._anonymousLogin()
 
342
 
 
343
        # We expect 2 lines because there are two files.
 
344
        self._download('LIST', chainDeferred=d)
 
345
        def checkDownload(download):
 
346
            self.assertEqual(2, len(download[:-2].split('\r\n')))
 
347
        d.addCallback(checkDownload)
 
348
 
 
349
        # Download a names-only listing.
 
350
        self._download('NLST ', chainDeferred=d)
 
351
        def checkDownload(download):
 
352
            filenames = download[:-2].split('\r\n')
 
353
            filenames.sort()
 
354
            self.assertEqual(['bar', 'foo'], filenames)
 
355
        d.addCallback(checkDownload)
 
356
 
 
357
        # Download a listing of the 'foo' subdirectory.  'foo' has no files, so
 
358
        # the file listing should be empty.
 
359
        self._download('LIST foo', chainDeferred=d)
 
360
        def checkDownload(download):
 
361
            self.assertEqual('', download)
 
362
        d.addCallback(checkDownload)
 
363
 
 
364
        # Change the current working directory to 'foo'.
 
365
        def chdir(ignored):
 
366
            return self.client.queueStringCommand('CWD foo')
 
367
        d.addCallback(chdir)
 
368
 
 
369
        # Download a listing from within 'foo', and again it should be empty,
 
370
        # because LIST uses the working directory by default.
 
371
        self._download('LIST', chainDeferred=d)
 
372
        def checkDownload(download):
 
373
            self.assertEqual('', download)
 
374
        return d.addCallback(checkDownload)
 
375
 
 
376
    def testManyLargeDownloads(self):
 
377
        # Login
 
378
        d = self._anonymousLogin()
 
379
 
 
380
        # Download a range of different size files
 
381
        for size in range(100000, 110000, 500):
 
382
            fObj = file(os.path.join(self.directory, '%d.txt' % (size,)), 'wb')
 
383
            fObj.write('x' * size)
 
384
            fObj.close()
 
385
 
 
386
            self._download('RETR %d.txt' % (size,), chainDeferred=d)
 
387
            def checkDownload(download, size=size):
 
388
                self.assertEqual('x' * size, download)
 
389
            d.addCallback(checkDownload)
 
390
        return d
 
391
 
 
392
 
 
393
class FTPServerPortDataConnectionTestCase(FTPServerPasvDataConnectionTestCase):
 
394
    def setUp(self):
 
395
        self.dataPorts = []
 
396
        return FTPServerPasvDataConnectionTestCase.setUp(self)
 
397
 
 
398
    def _makeDataConnection(self, ignored=None):
 
399
        # Establish an active data connection (i.e. server connecting to
 
400
        # client).
 
401
        deferred = defer.Deferred()
 
402
        class DataFactory(protocol.ServerFactory):
 
403
            protocol = _BufferingProtocol
 
404
            def buildProtocol(self, addr):
 
405
                p = protocol.ServerFactory.buildProtocol(self, addr)
 
406
                reactor.callLater(0, deferred.callback, p)
 
407
                return p
 
408
        dataPort = reactor.listenTCP(0, DataFactory(), interface='127.0.0.1')
 
409
        self.dataPorts.append(dataPort)
 
410
        cmd = 'PORT ' + ftp.encodeHostPort('127.0.0.1', dataPort.getHost().port)
 
411
        self.client.queueStringCommand(cmd)
 
412
        return deferred
 
413
 
 
414
    def tearDown(self):
 
415
        l = [defer.maybeDeferred(port.stopListening) for port in self.dataPorts]
 
416
        d = defer.maybeDeferred(
 
417
            FTPServerPasvDataConnectionTestCase.tearDown, self)
 
418
        l.append(d)
 
419
        return defer.DeferredList(l, fireOnOneErrback=True)
 
420
 
 
421
    def testPORTCannotConnect(self):
 
422
        # Login
 
423
        d = self._anonymousLogin()
 
424
 
 
425
        # Listen on a port, and immediately stop listening as a way to find a
 
426
        # port number that is definitely closed.
 
427
        def loggedIn(ignored):
 
428
            port = reactor.listenTCP(0, protocol.Factory(),
 
429
                                     interface='127.0.0.1')
 
430
            portNum = port.getHost().port
 
431
            d = port.stopListening()
 
432
            d.addCallback(lambda _: portNum)
 
433
            return d
 
434
        d.addCallback(loggedIn)
 
435
 
 
436
        # Tell the server to connect to that port with a PORT command, and
 
437
        # verify that it fails with the right error.
 
438
        def gotPortNum(portNum):
 
439
            return self.assertCommandFailed(
 
440
                'PORT ' + ftp.encodeHostPort('127.0.0.1', portNum),
 
441
                ["425 Can't open data connection."])
 
442
        return d.addCallback(gotPortNum)
 
443
 
 
444
 
 
445
# -- Client Tests -----------------------------------------------------------
 
446
 
 
447
class PrintLines(protocol.Protocol):
 
448
    """Helper class used by FTPFileListingTests."""
 
449
 
 
450
    def __init__(self, lines):
 
451
        self._lines = lines
 
452
 
 
453
    def connectionMade(self):
 
454
        for line in self._lines:
 
455
            self.transport.write(line + "\r\n")
 
456
        self.transport.loseConnection()
 
457
 
 
458
 
 
459
class MyFTPFileListProtocol(ftp.FTPFileListProtocol):
 
460
    def __init__(self):
 
461
        self.other = []
 
462
        ftp.FTPFileListProtocol.__init__(self)
 
463
 
 
464
    def unknownLine(self, line):
 
465
        self.other.append(line)
 
466
 
 
467
 
 
468
class FTPFileListingTests(unittest.TestCase):
 
469
    def getFilesForLines(self, lines):
 
470
        fileList = MyFTPFileListProtocol()
 
471
        d = loopback.loopbackAsync(PrintLines(lines), fileList)
 
472
        d.addCallback(lambda _: (fileList.files, fileList.other))
 
473
        return d
 
474
 
 
475
    def testOneLine(self):
 
476
        # This example line taken from the docstring for FTPFileListProtocol
 
477
        line = '-rw-r--r--   1 root     other        531 Jan 29 03:26 README'
 
478
        def check(((file,), other)):
 
479
            self.failIf(other, 'unexpect unparsable lines: %s' % repr(other))
 
480
            self.failUnless(file['filetype'] == '-', 'misparsed fileitem')
 
481
            self.failUnless(file['perms'] == 'rw-r--r--', 'misparsed perms')
 
482
            self.failUnless(file['owner'] == 'root', 'misparsed fileitem')
 
483
            self.failUnless(file['group'] == 'other', 'misparsed fileitem')
 
484
            self.failUnless(file['size'] == 531, 'misparsed fileitem')
 
485
            self.failUnless(file['date'] == 'Jan 29 03:26', 'misparsed fileitem')
 
486
            self.failUnless(file['filename'] == 'README', 'misparsed fileitem')
 
487
            self.failUnless(file['nlinks'] == 1, 'misparsed nlinks')
 
488
            self.failIf(file['linktarget'], 'misparsed linktarget')
 
489
        return self.getFilesForLines([line]).addCallback(check)
 
490
 
 
491
    def testVariantLines(self):
 
492
        line1 = 'drw-r--r--   2 root     other        531 Jan  9  2003 A'
 
493
        line2 = 'lrw-r--r--   1 root     other          1 Jan 29 03:26 B -> A'
 
494
        line3 = 'woohoo! '
 
495
        def check(((file1, file2), (other,))):
 
496
            self.failUnless(other == 'woohoo! \r', 'incorrect other line')
 
497
            # file 1
 
498
            self.failUnless(file1['filetype'] == 'd', 'misparsed fileitem')
 
499
            self.failUnless(file1['perms'] == 'rw-r--r--', 'misparsed perms')
 
500
            self.failUnless(file1['owner'] == 'root', 'misparsed owner')
 
501
            self.failUnless(file1['group'] == 'other', 'misparsed group')
 
502
            self.failUnless(file1['size'] == 531, 'misparsed size')
 
503
            self.failUnless(file1['date'] == 'Jan  9  2003', 'misparsed date')
 
504
            self.failUnless(file1['filename'] == 'A', 'misparsed filename')
 
505
            self.failUnless(file1['nlinks'] == 2, 'misparsed nlinks')
 
506
            self.failIf(file1['linktarget'], 'misparsed linktarget')
 
507
            # file 2
 
508
            self.failUnless(file2['filetype'] == 'l', 'misparsed fileitem')
 
509
            self.failUnless(file2['perms'] == 'rw-r--r--', 'misparsed perms')
 
510
            self.failUnless(file2['owner'] == 'root', 'misparsed owner')
 
511
            self.failUnless(file2['group'] == 'other', 'misparsed group')
 
512
            self.failUnless(file2['size'] == 1, 'misparsed size')
 
513
            self.failUnless(file2['date'] == 'Jan 29 03:26', 'misparsed date')
 
514
            self.failUnless(file2['filename'] == 'B', 'misparsed filename')
 
515
            self.failUnless(file2['nlinks'] == 1, 'misparsed nlinks')
 
516
            self.failUnless(file2['linktarget'] == 'A', 'misparsed linktarget')
 
517
        return self.getFilesForLines([line1, line2, line3]).addCallback(check)
 
518
 
 
519
    def testUnknownLine(self):
 
520
        def check((files, others)):
 
521
            self.failIf(files, 'unexpected file entries')
 
522
            self.failUnless(others == ['ABC\r', 'not a file\r'],
 
523
                            'incorrect unparsable lines: %s' % repr(others))
 
524
        return self.getFilesForLines(['ABC', 'not a file']).addCallback(check)
 
525
 
 
526
    def testYear(self):
 
527
        # This example derived from bug description in issue 514.
 
528
        fileList = ftp.FTPFileListProtocol()
 
529
        exampleLine = (
 
530
            '-rw-r--r--   1 root     other        531 Jan 29 2003 README\n')
 
531
        class PrintLine(protocol.Protocol):
 
532
            def connectionMade(self):
 
533
                self.transport.write(exampleLine)
 
534
                self.transport.loseConnection()
 
535
 
 
536
        def check(ignored):
 
537
            file = fileList.files[0]
 
538
            self.failUnless(file['size'] == 531, 'misparsed fileitem')
 
539
            self.failUnless(file['date'] == 'Jan 29 2003', 'misparsed fileitem')
 
540
            self.failUnless(file['filename'] == 'README', 'misparsed fileitem')
 
541
 
 
542
        d = loopback.loopbackAsync(PrintLine(), fileList)
 
543
        return d.addCallback(check)
 
544
 
 
545
 
 
546
class FTPClientTests(unittest.TestCase):
 
547
    def tearDown(self):
 
548
        # Clean up self.port, if any.
 
549
        port = getattr(self, 'port', None)
 
550
        if port is not None:
 
551
            return port.stopListening()
 
552
 
 
553
    def testFailedRETR(self):
 
554
        f = protocol.Factory()
 
555
        f.noisy = 0
 
556
        self.port = reactor.listenTCP(0, f, interface="127.0.0.1")
 
557
        portNum = self.port.getHost().port
 
558
        # This test data derived from a bug report by ranty on #twisted
 
559
        responses = ['220 ready, dude (vsFTPd 1.0.0: beat me, break me)',
 
560
                     # USER anonymous
 
561
                     '331 Please specify the password.',
 
562
                     # PASS twisted@twistedmatrix.com
 
563
                     '230 Login successful. Have fun.',
 
564
                     # TYPE I
 
565
                     '200 Binary it is, then.',
 
566
                     # PASV
 
567
                     '227 Entering Passive Mode (127,0,0,1,%d,%d)' %
 
568
                     (portNum >> 8, portNum & 0xff),
 
569
                     # RETR /file/that/doesnt/exist
 
570
                     '550 Failed to open file.']
 
571
        f.buildProtocol = lambda addr: PrintLines(responses)
 
572
 
 
573
        client = ftp.FTPClient(passive=1)
 
574
        cc = protocol.ClientCreator(reactor, ftp.FTPClient, passive=1)
 
575
        d = cc.connectTCP('127.0.0.1', portNum)
 
576
        def gotClient(client):
 
577
            p = protocol.Protocol()
 
578
            return client.retrieveFile('/file/that/doesnt/exist', p)
 
579
        d.addCallback(gotClient)
 
580
        return self.assertFailure(d, ftp.CommandFailed)
 
581
 
 
582
    def testErrbacksUponDisconnect(self):
 
583
        ftpClient = ftp.FTPClient()
 
584
        d = ftpClient.list('some path', Dummy())
 
585
        m = []
 
586
        def _eb(failure):
 
587
            m.append(failure)
 
588
            return None
 
589
        d.addErrback(_eb)
 
590
        from twisted.internet.main import CONNECTION_LOST
 
591
        ftpClient.connectionLost(failure.Failure(CONNECTION_LOST))
 
592
        self.failUnless(m, m)
 
593
 
 
594
 
 
595
 
 
596
class FTPClientTestCase(unittest.TestCase):
 
597
    """
 
598
    Test advanced FTP client commands.
 
599
    """
 
600
    def setUp(self):
 
601
        """
 
602
        Create a FTP client and connect it to fake transport.
 
603
        """
 
604
        self.client = ftp.FTPClient()
 
605
        self.transport = proto_helpers.StringTransport()
 
606
        self.client.makeConnection(self.transport)
 
607
 
 
608
 
 
609
    def tearDown(self):
 
610
        """
 
611
        Deliver disconnection notification to the client so that it can
 
612
        perform any cleanup which may be required.
 
613
        """
 
614
        self.client.connectionLost(error.ConnectionLost())
 
615
 
 
616
 
 
617
    def _testLogin(self):
 
618
        """
 
619
        Test the login part.
 
620
        """
 
621
        self.assertEquals(self.transport.value(), '')
 
622
        self.client.lineReceived(
 
623
            '331 Guest login ok, type your email address as password.')
 
624
        self.assertEquals(self.transport.value(), 'USER anonymous\r\n')
 
625
        self.transport.clear()
 
626
        self.client.lineReceived(
 
627
            '230 Anonymous login ok, access restrictions apply.')
 
628
        self.assertEquals(self.transport.value(), 'TYPE I\r\n')
 
629
        self.transport.clear()
 
630
        self.client.lineReceived('200 Type set to I.')
 
631
 
 
632
 
 
633
    def test_CDUP(self):
 
634
        """
 
635
        Test the CDUP command.
 
636
 
 
637
        L{ftp.FTPClient.cdup} should return a Deferred which fires with a
 
638
        sequence of one element which is the string the server sent
 
639
        indicating that the command was executed successfully.
 
640
 
 
641
        (XXX - This is a bad API)
 
642
        """
 
643
        def cbCdup(res):
 
644
            self.assertEquals(res[0], '250 Requested File Action Completed OK')
 
645
 
 
646
        self._testLogin()
 
647
        d = self.client.cdup().addCallback(cbCdup)
 
648
        self.assertEquals(self.transport.value(), 'CDUP\r\n')
 
649
        self.transport.clear()
 
650
        self.client.lineReceived('250 Requested File Action Completed OK')
 
651
        return d
 
652
 
 
653
 
 
654
    def test_failedCDUP(self):
 
655
        """
 
656
        Test L{ftp.FTPClient.cdup}'s handling of a failed CDUP command.
 
657
 
 
658
        When the CDUP command fails, the returned Deferred should errback
 
659
        with L{ftp.CommandFailed}.
 
660
        """
 
661
        self._testLogin()
 
662
        d = self.client.cdup()
 
663
        self.assertFailure(d, ftp.CommandFailed)
 
664
        self.assertEquals(self.transport.value(), 'CDUP\r\n')
 
665
        self.transport.clear()
 
666
        self.client.lineReceived('550 ..: No such file or directory')
 
667
        return d
 
668
 
 
669
 
 
670
    def test_PWD(self):
 
671
        """
 
672
        Test the PWD command.
 
673
 
 
674
        L{ftp.FTPClient.pwd} should return a Deferred which fires with a
 
675
        sequence of one element which is a string representing the current
 
676
        working directory on the server.
 
677
 
 
678
        (XXX - This is a bad API)
 
679
        """
 
680
        def cbPwd(res):
 
681
            self.assertEquals(ftp.parsePWDResponse(res[0]), "/bar/baz")
 
682
 
 
683
        self._testLogin()
 
684
        d = self.client.pwd().addCallback(cbPwd)
 
685
        self.assertEquals(self.transport.value(), 'PWD\r\n')
 
686
        self.client.lineReceived('257 "/bar/baz"')
 
687
        return d
 
688
 
 
689
 
 
690
    def test_failedPWD(self):
 
691
        """
 
692
        Test a failure in PWD command.
 
693
 
 
694
        When the PWD command fails, the returned Deferred should errback
 
695
        with L{ftp.CommandFailed}.
 
696
        """
 
697
        self._testLogin()
 
698
        d = self.client.pwd()
 
699
        self.assertFailure(d, ftp.CommandFailed)
 
700
        self.assertEquals(self.transport.value(), 'PWD\r\n')
 
701
        self.client.lineReceived('550 /bar/baz: No such file or directory')
 
702
        return d
 
703
 
 
704
 
 
705
    def test_CWD(self):
 
706
        """
 
707
        Test the CWD command.
 
708
 
 
709
        L{ftp.FTPClient.cwd} should return a Deferred which fires with a
 
710
        sequence of one element which is the string the server sent
 
711
        indicating that the command was executed successfully.
 
712
 
 
713
        (XXX - This is a bad API)
 
714
        """
 
715
        def cbCwd(res):
 
716
            self.assertEquals(res[0], '250 Requested File Action Completed OK')
 
717
 
 
718
        self._testLogin()
 
719
        d = self.client.cwd("bar/foo").addCallback(cbCwd)
 
720
        self.assertEquals(self.transport.value(), 'CWD bar/foo\r\n')
 
721
        self.client.lineReceived('250 Requested File Action Completed OK')
 
722
        return d
 
723
 
 
724
 
 
725
    def test_failedCWD(self):
 
726
        """
 
727
        Test a failure in CWD command.
 
728
 
 
729
        When the PWD command fails, the returned Deferred should errback
 
730
        with L{ftp.CommandFailed}.
 
731
        """
 
732
        self._testLogin()
 
733
        d = self.client.cwd("bar/foo")
 
734
        self.assertFailure(d, ftp.CommandFailed)
 
735
        self.assertEquals(self.transport.value(), 'CWD bar/foo\r\n')
 
736
        self.client.lineReceived('550 bar/foo: No such file or directory')
 
737
        return d
 
738
 
 
739
 
 
740
    def test_passiveRETR(self):
 
741
        """
 
742
        Test the RETR command in passive mode: get a file and verify its
 
743
        content.
 
744
 
 
745
        L{ftp.FTPClient.retrieveFile} should return a Deferred which fires
 
746
        with the protocol instance passed to it after the download has
 
747
        completed.
 
748
 
 
749
        (XXX - This API should be based on producers and consumers)
 
750
        """
 
751
        def cbRetr(res, proto):
 
752
            self.assertEquals(proto.buffer, 'x' * 1000)
 
753
 
 
754
        def cbConnect(host, port, factory):
 
755
            self.assertEquals(host, '127.0.0.1')
 
756
            self.assertEquals(port, 12345)
 
757
            proto = factory.buildProtocol((host, port))
 
758
            proto.makeConnection(proto_helpers.StringTransport())
 
759
            self.client.lineReceived(
 
760
                '150 File status okay; about to open data connection.')
 
761
            proto.dataReceived("x" * 1000)
 
762
            proto.connectionLost(failure.Failure(error.ConnectionDone("")))
 
763
 
 
764
        self.client.connectFactory = cbConnect
 
765
        self._testLogin()
 
766
        proto = _BufferingProtocol()
 
767
        d = self.client.retrieveFile("spam", proto)
 
768
        d.addCallback(cbRetr, proto)
 
769
        self.assertEquals(self.transport.value(), 'PASV\r\n')
 
770
        self.transport.clear()
 
771
        self.client.lineReceived('227 Entering Passive Mode (%s).' %
 
772
            (ftp.encodeHostPort('127.0.0.1', 12345),))
 
773
        self.assertEquals(self.transport.value(), 'RETR spam\r\n')
 
774
        self.transport.clear()
 
775
        self.client.lineReceived('226 Transfer Complete.')
 
776
        return d
 
777
 
 
778
 
 
779
    def test_RETR(self):
 
780
        """
 
781
        Test the RETR command in non-passive mode.
 
782
 
 
783
        Like L{test_passiveRETR} but in the configuration where the server
 
784
        establishes the data connection to the client, rather than the other
 
785
        way around.
 
786
        """
 
787
        self.client.passive = False
 
788
 
 
789
        def generatePort(portCmd):
 
790
            portCmd.text = 'PORT %s' % (ftp.encodeHostPort('127.0.0.1', 9876),)
 
791
            portCmd.protocol.makeConnection(proto_helpers.StringTransport())
 
792
            portCmd.protocol.dataReceived("x" * 1000)
 
793
            portCmd.protocol.connectionLost(
 
794
                failure.Failure(error.ConnectionDone("")))
 
795
 
 
796
        def cbRetr(res, proto):
 
797
            self.assertEquals(proto.buffer, 'x' * 1000)
 
798
 
 
799
        self.client.generatePortCommand = generatePort
 
800
        self._testLogin()
 
801
        proto = _BufferingProtocol()
 
802
        d = self.client.retrieveFile("spam", proto)
 
803
        d.addCallback(cbRetr, proto)
 
804
        self.assertEquals(self.transport.value(), 'PORT %s\r\n' %
 
805
            (ftp.encodeHostPort('127.0.0.1', 9876),))
 
806
        self.transport.clear()
 
807
        self.client.lineReceived('200 PORT OK')
 
808
        self.assertEquals(self.transport.value(), 'RETR spam\r\n')
 
809
        self.transport.clear()
 
810
        self.client.lineReceived('226 Transfer Complete.')
 
811
        return d
 
812
 
 
813
 
 
814
    def test_failedRETR(self):
 
815
        """
 
816
        Try to RETR an unexisting file.
 
817
 
 
818
        L{ftp.FTPClient.retrieveFile} should return a Deferred which
 
819
        errbacks with L{ftp.CommandFailed} if the server indicates the file
 
820
        cannot be transferred for some reason.
 
821
        """
 
822
        def cbConnect(host, port, factory):
 
823
            self.assertEquals(host, '127.0.0.1')
 
824
            self.assertEquals(port, 12345)
 
825
            proto = factory.buildProtocol((host, port))
 
826
            proto.makeConnection(proto_helpers.StringTransport())
 
827
            self.client.lineReceived(
 
828
                '150 File status okay; about to open data connection.')
 
829
            proto.connectionLost(failure.Failure(error.ConnectionDone("")))
 
830
 
 
831
        self.client.connectFactory = cbConnect
 
832
        self._testLogin()
 
833
        proto = _BufferingProtocol()
 
834
        d = self.client.retrieveFile("spam", proto)
 
835
        self.assertFailure(d, ftp.CommandFailed)
 
836
        self.assertEquals(self.transport.value(), 'PASV\r\n')
 
837
        self.transport.clear()
 
838
        self.client.lineReceived('227 Entering Passive Mode (%s).' %
 
839
            (ftp.encodeHostPort('127.0.0.1', 12345),))
 
840
        self.assertEquals(self.transport.value(), 'RETR spam\r\n')
 
841
        self.transport.clear()
 
842
        self.client.lineReceived('550 spam: No such file or directory')
 
843
        return d
 
844
 
 
845
 
 
846
    def test_passiveSTOR(self):
 
847
        """
 
848
        Test the STOR command: send a file and verify its content.
 
849
 
 
850
        L{ftp.FTPClient.storeFile} should return a two-tuple of Deferreds.
 
851
        The first of which should fire with a protocol instance when the
 
852
        data connection has been established and is responsible for sending
 
853
        the contents of the file.  The second of which should fire when the
 
854
        upload has completed, the data connection has been closed, and the
 
855
        server has acknowledged receipt of the file.
 
856
 
 
857
        (XXX - storeFile should take a producer as an argument, instead, and
 
858
        only return a Deferred which fires when the upload has succeeded or
 
859
        failed).
 
860
        """
 
861
        tr = proto_helpers.StringTransport()
 
862
        def cbStore(sender):
 
863
            self.client.lineReceived(
 
864
                '150 File status okay; about to open data connection.')
 
865
            sender.transport.write("x" * 1000)
 
866
            sender.finish()
 
867
            sender.connectionLost(failure.Failure(error.ConnectionDone("")))
 
868
 
 
869
        def cbFinish(ign):
 
870
            self.assertEquals(tr.value(), "x" * 1000)
 
871
 
 
872
        def cbConnect(host, port, factory):
 
873
            self.assertEquals(host, '127.0.0.1')
 
874
            self.assertEquals(port, 12345)
 
875
            proto = factory.buildProtocol((host, port))
 
876
            proto.makeConnection(tr)
 
877
 
 
878
        self.client.connectFactory = cbConnect
 
879
        self._testLogin()
 
880
        d1, d2 = self.client.storeFile("spam")
 
881
        d1.addCallback(cbStore)
 
882
        d2.addCallback(cbFinish)
 
883
        self.assertEquals(self.transport.value(), 'PASV\r\n')
 
884
        self.transport.clear()
 
885
        self.client.lineReceived('227 Entering Passive Mode (%s).' %
 
886
            (ftp.encodeHostPort('127.0.0.1', 12345),))
 
887
        self.assertEquals(self.transport.value(), 'STOR spam\r\n')
 
888
        self.transport.clear()
 
889
        self.client.lineReceived('226 Transfer Complete.')
 
890
        return defer.gatherResults([d1, d2])
 
891
 
 
892
 
 
893
    def test_failedSTOR(self):
 
894
        """
 
895
        Test a failure in the STOR command.
 
896
 
 
897
        If the server does not acknowledge successful receipt of the
 
898
        uploaded file, the second Deferred returned by
 
899
        L{ftp.FTPClient.storeFile} should errback with L{ftp.CommandFailed}.
 
900
        """
 
901
        tr = proto_helpers.StringTransport()
 
902
        def cbStore(sender):
 
903
            self.client.lineReceived(
 
904
                '150 File status okay; about to open data connection.')
 
905
            sender.transport.write("x" * 1000)
 
906
            sender.finish()
 
907
            sender.connectionLost(failure.Failure(error.ConnectionDone("")))
 
908
 
 
909
        def cbConnect(host, port, factory):
 
910
            self.assertEquals(host, '127.0.0.1')
 
911
            self.assertEquals(port, 12345)
 
912
            proto = factory.buildProtocol((host, port))
 
913
            proto.makeConnection(tr)
 
914
 
 
915
        self.client.connectFactory = cbConnect
 
916
        self._testLogin()
 
917
        d1, d2 = self.client.storeFile("spam")
 
918
        d1.addCallback(cbStore)
 
919
        self.assertFailure(d2, ftp.CommandFailed)
 
920
        self.assertEquals(self.transport.value(), 'PASV\r\n')
 
921
        self.transport.clear()
 
922
        self.client.lineReceived('227 Entering Passive Mode (%s).' %
 
923
            (ftp.encodeHostPort('127.0.0.1', 12345),))
 
924
        self.assertEquals(self.transport.value(), 'STOR spam\r\n')
 
925
        self.transport.clear()
 
926
        self.client.lineReceived(
 
927
            '426 Transfer aborted.  Data connection closed.')
 
928
        return defer.gatherResults([d1, d2])
 
929
 
 
930
 
 
931
    def test_STOR(self):
 
932
        """
 
933
        Test the STOR command in non-passive mode.
 
934
 
 
935
        Like L{test_passiveSTOR} but in the configuration where the server
 
936
        establishes the data connection to the client, rather than the other
 
937
        way around.
 
938
        """
 
939
        tr = proto_helpers.StringTransport()
 
940
        self.client.passive = False
 
941
        def generatePort(portCmd):
 
942
            portCmd.text = 'PORT %s' % ftp.encodeHostPort('127.0.0.1', 9876)
 
943
            portCmd.protocol.makeConnection(tr)
 
944
 
 
945
        def cbStore(sender):
 
946
            self.assertEquals(self.transport.value(), 'PORT %s\r\n' %
 
947
                (ftp.encodeHostPort('127.0.0.1', 9876),))
 
948
            self.transport.clear()
 
949
            self.client.lineReceived('200 PORT OK')
 
950
            self.assertEquals(self.transport.value(), 'STOR spam\r\n')
 
951
            self.transport.clear()
 
952
            self.client.lineReceived(
 
953
                '150 File status okay; about to open data connection.')
 
954
            sender.transport.write("x" * 1000)
 
955
            sender.finish()
 
956
            sender.connectionLost(failure.Failure(error.ConnectionDone("")))
 
957
            self.client.lineReceived('226 Transfer Complete.')
 
958
 
 
959
        def cbFinish(ign):
 
960
            self.assertEquals(tr.value(), "x" * 1000)
 
961
 
 
962
        self.client.generatePortCommand = generatePort
 
963
        self._testLogin()
 
964
        d1, d2 = self.client.storeFile("spam")
 
965
        d1.addCallback(cbStore)
 
966
        d2.addCallback(cbFinish)
 
967
        return defer.gatherResults([d1, d2])
 
968
 
 
969
 
 
970
    def test_passiveLIST(self):
 
971
        """
 
972
        Test the LIST command.
 
973
 
 
974
        L{ftp.FTPClient.list} should return a Deferred which fires with a
 
975
        protocol instance which was passed to list after the command has
 
976
        succeeded.
 
977
 
 
978
        (XXX - This is a very unfortunate API; if my understanding is
 
979
        correct, the results are always at least line-oriented, so allowing
 
980
        a per-line parser function to be specified would make this simpler,
 
981
        but a default implementation should really be provided which knows
 
982
        how to deal with all the formats used in real servers, so
 
983
        application developers never have to care about this insanity.  It
 
984
        would also be nice to either get back a Deferred of a list of
 
985
        filenames or to be able to consume the files as they are received
 
986
        (which the current API does allow, but in a somewhat inconvenient
 
987
        fashion) -exarkun)
 
988
        """
 
989
        def cbList(res, fileList):
 
990
            fls = [f["filename"] for f in fileList.files]
 
991
            expected = ["foo", "bar", "baz"]
 
992
            expected.sort()
 
993
            fls.sort()
 
994
            self.assertEquals(fls, expected)
 
995
 
 
996
        def cbConnect(host, port, factory):
 
997
            self.assertEquals(host, '127.0.0.1')
 
998
            self.assertEquals(port, 12345)
 
999
            proto = factory.buildProtocol((host, port))
 
1000
            proto.makeConnection(proto_helpers.StringTransport())
 
1001
            self.client.lineReceived(
 
1002
                '150 File status okay; about to open data connection.')
 
1003
            sending = [
 
1004
                '-rw-r--r--    0 spam      egg      100 Oct 10 2006 foo\r\n',
 
1005
                '-rw-r--r--    3 spam      egg      100 Oct 10 2006 bar\r\n',
 
1006
                '-rw-r--r--    4 spam      egg      100 Oct 10 2006 baz\r\n',
 
1007
            ]
 
1008
            for i in sending:
 
1009
                proto.dataReceived(i)
 
1010
            proto.connectionLost(failure.Failure(error.ConnectionDone("")))
 
1011
 
 
1012
        self.client.connectFactory = cbConnect
 
1013
        self._testLogin()
 
1014
        fileList = ftp.FTPFileListProtocol()
 
1015
        d = self.client.list('foo/bar', fileList).addCallback(cbList, fileList)
 
1016
        self.assertEquals(self.transport.value(), 'PASV\r\n')
 
1017
        self.transport.clear()
 
1018
        self.client.lineReceived('227 Entering Passive Mode (%s).' %
 
1019
            (ftp.encodeHostPort('127.0.0.1', 12345),))
 
1020
        self.assertEquals(self.transport.value(), 'LIST foo/bar\r\n')
 
1021
        self.client.lineReceived('226 Transfer Complete.')
 
1022
        return d
 
1023
 
 
1024
 
 
1025
    def test_LIST(self):
 
1026
        """
 
1027
        Test the LIST command in non-passive mode.
 
1028
 
 
1029
        Like L{test_passiveLIST} but in the configuration where the server
 
1030
        establishes the data connection to the client, rather than the other
 
1031
        way around.
 
1032
        """
 
1033
        self.client.passive = False
 
1034
        def generatePort(portCmd):
 
1035
            portCmd.text = 'PORT %s' % (ftp.encodeHostPort('127.0.0.1', 9876),)
 
1036
            portCmd.protocol.makeConnection(proto_helpers.StringTransport())
 
1037
            self.client.lineReceived(
 
1038
                '150 File status okay; about to open data connection.')
 
1039
            sending = [
 
1040
                '-rw-r--r--    0 spam      egg      100 Oct 10 2006 foo\r\n',
 
1041
                '-rw-r--r--    3 spam      egg      100 Oct 10 2006 bar\r\n',
 
1042
                '-rw-r--r--    4 spam      egg      100 Oct 10 2006 baz\r\n',
 
1043
            ]
 
1044
            for i in sending:
 
1045
                portCmd.protocol.dataReceived(i)
 
1046
            portCmd.protocol.connectionLost(
 
1047
                failure.Failure(error.ConnectionDone("")))
 
1048
 
 
1049
        def cbList(res, fileList):
 
1050
            fls = [f["filename"] for f in fileList.files]
 
1051
            expected = ["foo", "bar", "baz"]
 
1052
            expected.sort()
 
1053
            fls.sort()
 
1054
            self.assertEquals(fls, expected)
 
1055
 
 
1056
        self.client.generatePortCommand = generatePort
 
1057
        self._testLogin()
 
1058
        fileList = ftp.FTPFileListProtocol()
 
1059
        d = self.client.list('foo/bar', fileList).addCallback(cbList, fileList)
 
1060
        self.assertEquals(self.transport.value(), 'PORT %s\r\n' %
 
1061
            (ftp.encodeHostPort('127.0.0.1', 9876),))
 
1062
        self.transport.clear()
 
1063
        self.client.lineReceived('200 PORT OK')
 
1064
        self.assertEquals(self.transport.value(), 'LIST foo/bar\r\n')
 
1065
        self.transport.clear()
 
1066
        self.client.lineReceived('226 Transfer Complete.')
 
1067
        return d
 
1068
 
 
1069
 
 
1070
    def test_failedLIST(self):
 
1071
        """
 
1072
        Test a failure in LIST command.
 
1073
 
 
1074
        L{ftp.FTPClient.list} should return a Deferred which fails with
 
1075
        L{ftp.CommandFailed} if the server indicates the indicated path is
 
1076
        invalid for some reason.
 
1077
        """
 
1078
        def cbConnect(host, port, factory):
 
1079
            self.assertEquals(host, '127.0.0.1')
 
1080
            self.assertEquals(port, 12345)
 
1081
            proto = factory.buildProtocol((host, port))
 
1082
            proto.makeConnection(proto_helpers.StringTransport())
 
1083
            self.client.lineReceived(
 
1084
                '150 File status okay; about to open data connection.')
 
1085
            proto.connectionLost(failure.Failure(error.ConnectionDone("")))
 
1086
 
 
1087
        self.client.connectFactory = cbConnect
 
1088
        self._testLogin()
 
1089
        fileList = ftp.FTPFileListProtocol()
 
1090
        d = self.client.list('foo/bar', fileList)
 
1091
        self.assertFailure(d, ftp.CommandFailed)
 
1092
        self.assertEquals(self.transport.value(), 'PASV\r\n')
 
1093
        self.transport.clear()
 
1094
        self.client.lineReceived('227 Entering Passive Mode (%s).' %
 
1095
            (ftp.encodeHostPort('127.0.0.1', 12345),))
 
1096
        self.assertEquals(self.transport.value(), 'LIST foo/bar\r\n')
 
1097
        self.client.lineReceived('550 foo/bar: No such file or directory')
 
1098
        return d
 
1099
 
 
1100
 
 
1101
    def test_NLST(self):
 
1102
        """
 
1103
        Test the NLST command in non-passive mode.
 
1104
 
 
1105
        L{ftp.FTPClient.nlst} should return a Deferred which fires with a
 
1106
        list of filenames when the list command has completed.
 
1107
        """
 
1108
        self.client.passive = False
 
1109
        def generatePort(portCmd):
 
1110
            portCmd.text = 'PORT %s' % (ftp.encodeHostPort('127.0.0.1', 9876),)
 
1111
            portCmd.protocol.makeConnection(proto_helpers.StringTransport())
 
1112
            self.client.lineReceived(
 
1113
                '150 File status okay; about to open data connection.')
 
1114
            portCmd.protocol.dataReceived('foo\r\n')
 
1115
            portCmd.protocol.dataReceived('bar\r\n')
 
1116
            portCmd.protocol.dataReceived('baz\r\n')
 
1117
            portCmd.protocol.connectionLost(
 
1118
                failure.Failure(error.ConnectionDone("")))
 
1119
 
 
1120
        def cbList(res, proto):
 
1121
            fls = proto.buffer.splitlines()
 
1122
            expected = ["foo", "bar", "baz"]
 
1123
            expected.sort()
 
1124
            fls.sort()
 
1125
            self.assertEquals(fls, expected)
 
1126
 
 
1127
        self.client.generatePortCommand = generatePort
 
1128
        self._testLogin()
 
1129
        lstproto = _BufferingProtocol()
 
1130
        d = self.client.nlst('foo/bar', lstproto).addCallback(cbList, lstproto)
 
1131
        self.assertEquals(self.transport.value(), 'PORT %s\r\n' %
 
1132
            (ftp.encodeHostPort('127.0.0.1', 9876),))
 
1133
        self.transport.clear()
 
1134
        self.client.lineReceived('200 PORT OK')
 
1135
        self.assertEquals(self.transport.value(), 'NLST foo/bar\r\n')
 
1136
        self.client.lineReceived('226 Transfer Complete.')
 
1137
        return d
 
1138
 
 
1139
 
 
1140
    def test_passiveNLST(self):
 
1141
        """
 
1142
        Test the NLST command.
 
1143
 
 
1144
        Like L{test_passiveNLST} but in the configuration where the server
 
1145
        establishes the data connection to the client, rather than the other
 
1146
        way around.
 
1147
        """
 
1148
        def cbList(res, proto):
 
1149
            fls = proto.buffer.splitlines()
 
1150
            expected = ["foo", "bar", "baz"]
 
1151
            expected.sort()
 
1152
            fls.sort()
 
1153
            self.assertEquals(fls, expected)
 
1154
 
 
1155
        def cbConnect(host, port, factory):
 
1156
            self.assertEquals(host, '127.0.0.1')
 
1157
            self.assertEquals(port, 12345)
 
1158
            proto = factory.buildProtocol((host, port))
 
1159
            proto.makeConnection(proto_helpers.StringTransport())
 
1160
            self.client.lineReceived(
 
1161
                '150 File status okay; about to open data connection.')
 
1162
            proto.dataReceived('foo\r\n')
 
1163
            proto.dataReceived('bar\r\n')
 
1164
            proto.dataReceived('baz\r\n')
 
1165
            proto.connectionLost(failure.Failure(error.ConnectionDone("")))
 
1166
 
 
1167
        self.client.connectFactory = cbConnect
 
1168
        self._testLogin()
 
1169
        lstproto = _BufferingProtocol()
 
1170
        d = self.client.nlst('foo/bar', lstproto).addCallback(cbList, lstproto)
 
1171
        self.assertEquals(self.transport.value(), 'PASV\r\n')
 
1172
        self.transport.clear()
 
1173
        self.client.lineReceived('227 Entering Passive Mode (%s).' %
 
1174
            (ftp.encodeHostPort('127.0.0.1', 12345),))
 
1175
        self.assertEquals(self.transport.value(), 'NLST foo/bar\r\n')
 
1176
        self.client.lineReceived('226 Transfer Complete.')
 
1177
        return d
 
1178
 
 
1179
 
 
1180
    def test_failedNLST(self):
 
1181
        """
 
1182
        Test a failure in NLST command.
 
1183
 
 
1184
        L{ftp.FTPClient.nlst} should return a Deferred which fails with
 
1185
        L{ftp.CommandFailed} if the server indicates the indicated path is
 
1186
        invalid for some reason.
 
1187
        """
 
1188
        tr = proto_helpers.StringTransport()
 
1189
        def cbConnect(host, port, factory):
 
1190
            self.assertEquals(host, '127.0.0.1')
 
1191
            self.assertEquals(port, 12345)
 
1192
            proto = factory.buildProtocol((host, port))
 
1193
            proto.makeConnection(tr)
 
1194
            self.client.lineReceived(
 
1195
                '150 File status okay; about to open data connection.')
 
1196
            proto.connectionLost(failure.Failure(error.ConnectionDone("")))
 
1197
 
 
1198
        self.client.connectFactory = cbConnect
 
1199
        self._testLogin()
 
1200
        lstproto = _BufferingProtocol()
 
1201
        d = self.client.nlst('foo/bar', lstproto)
 
1202
        self.assertFailure(d, ftp.CommandFailed)
 
1203
        self.assertEquals(self.transport.value(), 'PASV\r\n')
 
1204
        self.transport.clear()
 
1205
        self.client.lineReceived('227 Entering Passive Mode (%s).' %
 
1206
            (ftp.encodeHostPort('127.0.0.1', 12345),))
 
1207
        self.assertEquals(self.transport.value(), 'NLST foo/bar\r\n')
 
1208
        self.client.lineReceived('550 foo/bar: No such file or directory')
 
1209
        return d
 
1210
 
 
1211
 
 
1212
    def test_changeDirectory(self):
 
1213
        """
 
1214
        Test the changeDirectory method.
 
1215
 
 
1216
        L{ftp.FTPClient.changeDirectory} should return a Deferred which fires
 
1217
        with True if succeeded.
 
1218
        """
 
1219
        def cbCd(res):
 
1220
            self.assertEquals(res, True)
 
1221
 
 
1222
        self._testLogin()
 
1223
        d = self.client.changeDirectory("bar/foo").addCallback(cbCd)
 
1224
        self.assertEquals(self.transport.value(), 'CWD bar/foo\r\n')
 
1225
        self.client.lineReceived('250 Requested File Action Completed OK')
 
1226
        return d
 
1227
 
 
1228
 
 
1229
    def test_failedChangeDirectory(self):
 
1230
        """
 
1231
        Test a failure in the changeDirectory method.
 
1232
 
 
1233
        The behaviour here is the same as a failed CWD.
 
1234
        """
 
1235
        self._testLogin()
 
1236
        d = self.client.changeDirectory("bar/foo")
 
1237
        self.assertFailure(d, ftp.CommandFailed)
 
1238
        self.assertEquals(self.transport.value(), 'CWD bar/foo\r\n')
 
1239
        self.client.lineReceived('550 bar/foo: No such file or directory')
 
1240
        return d
 
1241
 
 
1242
 
 
1243
    def test_strangeFailedChangeDirectory(self):
 
1244
        """
 
1245
        Test a strange failure in changeDirectory method.
 
1246
 
 
1247
        L{ftp.FTPClient.changeDirectory} is stricter than CWD as it checks
 
1248
        code 250 for success.
 
1249
        """
 
1250
        self._testLogin()
 
1251
        d = self.client.changeDirectory("bar/foo")
 
1252
        self.assertFailure(d, ftp.CommandFailed)
 
1253
        self.assertEquals(self.transport.value(), 'CWD bar/foo\r\n')
 
1254
        self.client.lineReceived('252 I do what I want !')
 
1255
        return d
 
1256
 
 
1257
 
 
1258
    def test_getDirectory(self):
 
1259
        """
 
1260
        Test the getDirectory method.
 
1261
 
 
1262
        L{ftp.FTPClient.getDirectory} should return a Deferred which fires with
 
1263
        the current directory on the server. It wraps PWD command.
 
1264
        """
 
1265
        def cbGet(res):
 
1266
            self.assertEquals(res, "/bar/baz")
 
1267
 
 
1268
        self._testLogin()
 
1269
        d = self.client.getDirectory().addCallback(cbGet)
 
1270
        self.assertEquals(self.transport.value(), 'PWD\r\n')
 
1271
        self.client.lineReceived('257 "/bar/baz"')
 
1272
        return d
 
1273
 
 
1274
 
 
1275
    def test_failedGetDirectory(self):
 
1276
        """
 
1277
        Test a failure in getDirectory method.
 
1278
 
 
1279
        The behaviour should be the same as PWD.
 
1280
        """
 
1281
        self._testLogin()
 
1282
        d = self.client.getDirectory()
 
1283
        self.assertFailure(d, ftp.CommandFailed)
 
1284
        self.assertEquals(self.transport.value(), 'PWD\r\n')
 
1285
        self.client.lineReceived('550 /bar/baz: No such file or directory')
 
1286
        return d
 
1287
 
 
1288
 
 
1289
    def test_anotherFailedGetDirectory(self):
 
1290
        """
 
1291
        Test a different failure in getDirectory method.
 
1292
 
 
1293
        The response should be quoted to be parsed, so it returns an error
 
1294
        otherwise.
 
1295
        """
 
1296
        self._testLogin()
 
1297
        d = self.client.getDirectory()
 
1298
        self.assertFailure(d, ftp.CommandFailed)
 
1299
        self.assertEquals(self.transport.value(), 'PWD\r\n')
 
1300
        self.client.lineReceived('257 /bar/baz')
 
1301
        return d
 
1302
 
 
1303
 
 
1304
 
 
1305
class DummyTransport:
 
1306
    def write(self, bytes):
 
1307
        pass
 
1308
 
 
1309
class BufferingTransport:
 
1310
    buffer = ''
 
1311
    def write(self, bytes):
 
1312
        self.buffer += bytes
 
1313
 
 
1314
 
 
1315
class FTPClientBasicTests(unittest.TestCase):
 
1316
 
 
1317
    def testGreeting(self):
 
1318
        # The first response is captured as a greeting.
 
1319
        ftpClient = ftp.FTPClientBasic()
 
1320
        ftpClient.lineReceived('220 Imaginary FTP.')
 
1321
        self.failUnlessEqual(['220 Imaginary FTP.'], ftpClient.greeting)
 
1322
 
 
1323
    def testResponseWithNoMessage(self):
 
1324
        # Responses with no message are still valid, i.e. three digits followed
 
1325
        # by a space is complete response.
 
1326
        ftpClient = ftp.FTPClientBasic()
 
1327
        ftpClient.lineReceived('220 ')
 
1328
        self.failUnlessEqual(['220 '], ftpClient.greeting)
 
1329
 
 
1330
    def testMultilineResponse(self):
 
1331
        ftpClient = ftp.FTPClientBasic()
 
1332
        ftpClient.transport = DummyTransport()
 
1333
        ftpClient.lineReceived('220 Imaginary FTP.')
 
1334
 
 
1335
        # Queue (and send) a dummy command, and set up a callback to capture the
 
1336
        # result
 
1337
        deferred = ftpClient.queueStringCommand('BLAH')
 
1338
        result = []
 
1339
        deferred.addCallback(result.append)
 
1340
        deferred.addErrback(self.fail)
 
1341
 
 
1342
        # Send the first line of a multiline response.
 
1343
        ftpClient.lineReceived('210-First line.')
 
1344
        self.failUnlessEqual([], result)
 
1345
 
 
1346
        # Send a second line, again prefixed with "nnn-".
 
1347
        ftpClient.lineReceived('123-Second line.')
 
1348
        self.failUnlessEqual([], result)
 
1349
 
 
1350
        # Send a plain line of text, no prefix.
 
1351
        ftpClient.lineReceived('Just some text.')
 
1352
        self.failUnlessEqual([], result)
 
1353
 
 
1354
        # Now send a short (less than 4 chars) line.
 
1355
        ftpClient.lineReceived('Hi')
 
1356
        self.failUnlessEqual([], result)
 
1357
 
 
1358
        # Now send an empty line.
 
1359
        ftpClient.lineReceived('')
 
1360
        self.failUnlessEqual([], result)
 
1361
 
 
1362
        # And a line with 3 digits in it, and nothing else.
 
1363
        ftpClient.lineReceived('321')
 
1364
        self.failUnlessEqual([], result)
 
1365
 
 
1366
        # Now finish it.
 
1367
        ftpClient.lineReceived('210 Done.')
 
1368
        self.failUnlessEqual(
 
1369
            ['210-First line.',
 
1370
             '123-Second line.',
 
1371
             'Just some text.',
 
1372
             'Hi',
 
1373
             '',
 
1374
             '321',
 
1375
             '210 Done.'], result[0])
 
1376
 
 
1377
    def testNoPasswordGiven(self):
 
1378
        """Passing None as the password avoids sending the PASS command."""
 
1379
        # Create a client, and give it a greeting.
 
1380
        ftpClient = ftp.FTPClientBasic()
 
1381
        ftpClient.transport = BufferingTransport()
 
1382
        ftpClient.lineReceived('220 Welcome to Imaginary FTP.')
 
1383
 
 
1384
        # Queue a login with no password
 
1385
        ftpClient.queueLogin('bob', None)
 
1386
        self.failUnlessEqual('USER bob\r\n', ftpClient.transport.buffer)
 
1387
 
 
1388
        # Clear the test buffer, acknowledge the USER command.
 
1389
        ftpClient.transport.buffer = ''
 
1390
        ftpClient.lineReceived('200 Hello bob.')
 
1391
 
 
1392
        # The client shouldn't have sent anything more (i.e. it shouldn't have
 
1393
        # sent a PASS command).
 
1394
        self.failUnlessEqual('', ftpClient.transport.buffer)
 
1395
 
 
1396
    def testNoPasswordNeeded(self):
 
1397
        """Receiving a 230 response to USER prevents PASS from being sent."""
 
1398
        # Create a client, and give it a greeting.
 
1399
        ftpClient = ftp.FTPClientBasic()
 
1400
        ftpClient.transport = BufferingTransport()
 
1401
        ftpClient.lineReceived('220 Welcome to Imaginary FTP.')
 
1402
 
 
1403
        # Queue a login with no password
 
1404
        ftpClient.queueLogin('bob', 'secret')
 
1405
        self.failUnlessEqual('USER bob\r\n', ftpClient.transport.buffer)
 
1406
 
 
1407
        # Clear the test buffer, acknowledge the USER command with a 230
 
1408
        # response code.
 
1409
        ftpClient.transport.buffer = ''
 
1410
        ftpClient.lineReceived('230 Hello bob.  No password needed.')
 
1411
 
 
1412
        # The client shouldn't have sent anything more (i.e. it shouldn't have
 
1413
        # sent a PASS command).
 
1414
        self.failUnlessEqual('', ftpClient.transport.buffer)
 
1415
 
 
1416
 
 
1417
class PathHandling(unittest.TestCase):
 
1418
    def testNormalizer(self):
 
1419
        for inp, outp in [('a', ['a']),
 
1420
                          ('/a', ['a']),
 
1421
                          ('/', []),
 
1422
                          ('a/b/c', ['a', 'b', 'c']),
 
1423
                          ('/a/b/c', ['a', 'b', 'c']),
 
1424
                          ('/a/', ['a']),
 
1425
                          ('a/', ['a'])]:
 
1426
            self.assertEquals(ftp.toSegments([], inp), outp)
 
1427
 
 
1428
        for inp, outp in [('b', ['a', 'b']),
 
1429
                          ('b/', ['a', 'b']),
 
1430
                          ('/b', ['b']),
 
1431
                          ('/b/', ['b']),
 
1432
                          ('b/c', ['a', 'b', 'c']),
 
1433
                          ('b/c/', ['a', 'b', 'c']),
 
1434
                          ('/b/c', ['b', 'c']),
 
1435
                          ('/b/c/', ['b', 'c'])]:
 
1436
            self.assertEquals(ftp.toSegments(['a'], inp), outp)
 
1437
 
 
1438
        for inp, outp in [('//', []),
 
1439
                          ('//a', ['a']),
 
1440
                          ('a//', ['a']),
 
1441
                          ('a//b', ['a', 'b'])]:
 
1442
            self.assertEquals(ftp.toSegments([], inp), outp)
 
1443
 
 
1444
        for inp, outp in [('//', []),
 
1445
                          ('//b', ['b']),
 
1446
                          ('b//c', ['a', 'b', 'c'])]:
 
1447
            self.assertEquals(ftp.toSegments(['a'], inp), outp)
 
1448
 
 
1449
        for inp, outp in [('..', []),
 
1450
                          ('../', []),
 
1451
                          ('a/..', ['x']),
 
1452
                          ('/a/..', []),
 
1453
                          ('/a/b/..', ['a']),
 
1454
                          ('/a/b/../', ['a']),
 
1455
                          ('/a/b/../c', ['a', 'c']),
 
1456
                          ('/a/b/../c/', ['a', 'c']),
 
1457
                          ('/a/b/../../c', ['c']),
 
1458
                          ('/a/b/../../c/', ['c']),
 
1459
                          ('/a/b/../../c/..', []),
 
1460
                          ('/a/b/../../c/../', [])]:
 
1461
            self.assertEquals(ftp.toSegments(['x'], inp), outp)
 
1462
 
 
1463
        for inp in ['..', '../', 'a/../..', 'a/../../',
 
1464
                    '/..', '/../', '/a/../..', '/a/../../',
 
1465
                    '/a/b/../../..']:
 
1466
            self.assertRaises(ftp.InvalidPath, ftp.toSegments, [], inp)
 
1467
 
 
1468
        for inp in ['../..', '../../', '../a/../..']:
 
1469
            self.assertRaises(ftp.InvalidPath, ftp.toSegments, ['x'], inp)