1
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
2
# See LICENSE for details.
7
Maintainer: U{Andrew Bennetts<mailto:spiv@twistedmatrix.com>}
10
from __future__ import nested_scopes
13
from StringIO import StringIO
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
23
from twisted.protocols import ftp, loopback
25
class NonClosingStringIO(StringIO):
29
StringIOWithoutClosing = NonClosingStringIO
34
class Dummy(basic.LineReceiver):
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):
49
class _BufferingProtocol(protocol.Protocol):
50
def connectionMade(self):
52
self.d = defer.Deferred()
53
def dataReceived(self, data):
55
def connectionLost(self, reason):
59
class FTPServerTestCase(unittest.TestCase):
60
"""Simple tests for an FTP server with the default settings."""
64
self.directory = self.mktemp()
65
os.mkdir(self.directory)
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")
74
# Hook the server's buildProtocol to make the protocol instance
75
# accessible to tests.
76
buildProtocol = self.factory.buildProtocol
78
def _rememberProtocolInstance(addr):
79
protocol = buildProtocol(addr)
80
self.serverProtocol = protocol.wrappedProtocol
83
self.factory.buildProtocol = _rememberProtocolInstance
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):
91
d2.addCallback(gotClient)
92
return defer.gatherResults([d1, d2])
96
self.client.transport.loseConnection()
97
d = defer.maybeDeferred(self.port.stopListening)
98
d.addCallback(self.ebTearDown)
101
def ebTearDown(self, ignore):
102
del self.serverProtocol
103
# Clean up temporary directory
104
shutil.rmtree(self.directory)
106
def assertCommandResponse(self, command, expectedResponseLines,
108
"""Asserts that a sending an FTP command receives the expected
111
Returns a Deferred. Optionally accepts a deferred to chain its actions
114
if chainDeferred is None:
115
chainDeferred = defer.succeed(None)
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)
124
def assertCommandFailed(self, command, expectedResponse=None,
126
if chainDeferred is None:
127
chainDeferred = defer.succeed(None)
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)
139
def _anonymousLogin(self):
140
d = self.assertCommandResponse(
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.'],
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.
154
commandList = ['CDUP', 'CWD', 'LIST', 'MODE', 'PASV',
155
'PWD', 'RETR', 'STRU', 'SYST', 'TYPE']
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],))
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)
171
def testPASSBeforeUSER(self):
172
"""Issuing PASS before USER should give an error."""
173
return self.assertCommandFailed(
175
["503 Incorrect sequence of commands: "
176
"USER required before PASS"])
178
def testNoParamsForUSER(self):
179
"""Issuing USER without a username is a syntax error."""
180
return self.assertCommandFailed(
182
['500 Syntax error: USER requires an argument.'])
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(
189
['500 Syntax error: PASS requires an argument.'],
192
def testAnonymousLogin(self):
193
return self._anonymousLogin()
196
"""Issuing QUIT should return a 221 message."""
197
d = self._anonymousLogin()
198
return self.assertCommandResponse(
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)
211
# Same response code as allowAnonymous=True, but different text.
212
d = self.assertCommandResponse(
214
['331 Password required for anonymous.'])
216
# It will be denied. No-one can login.
217
d = self.assertCommandFailed(
218
'PASS test@twistedmatrix.com',
219
['530 Sorry, Authentication failed.'],
222
# It's not just saying that. You aren't logged in.
223
d = self.assertCommandFailed(
225
['530 Please login with USER and PASS.'],
229
def testUnknownCommand(self):
230
d = self._anonymousLogin()
231
return self.assertCommandFailed(
233
["502 Command 'GIBBERISH' not implemented"],
236
def testRETRBeforePORT(self):
237
d = self._anonymousLogin()
238
return self.assertCommandFailed(
240
["503 Incorrect sequence of commands: "
241
"PORT or PASV required before RETR"],
244
def testSTORBeforePORT(self):
245
d = self._anonymousLogin()
246
return self.assertCommandFailed(
248
["503 Incorrect sequence of commands: "
249
"PORT or PASV required before STOR"],
252
def testBadCommandArgs(self):
253
d = self._anonymousLogin()
254
self.assertCommandFailed(
256
["504 Not implemented for parameter 'z'."],
258
self.assertCommandFailed(
260
["504 Not implemented for parameter 'I'."],
264
def testDecodeHostPort(self):
265
self.assertEquals(ftp.decodeHostPort('25,234,129,22,100,23'),
266
('25.234.129.22', 25623))
269
badValue = list(nums)
271
s = ','.join(map(str, badValue))
272
self.assertRaises(ValueError, ftp.decodeHostPort, s)
276
yield defer.waitForDeferred(self._anonymousLogin())
278
# Issue a PASV command, and extract the host and port from the response
279
pasvCmd = defer.waitForDeferred(self.client.queueStringCommand('PASV'))
281
responseLines = pasvCmd.getResult()
282
host, port = ftp.decodeHostPort(responseLines[-1][4:])
284
# Make sure the server is listening on the port it claims to be
285
self.assertEqual(port, self.serverProtocol.dtpPort.getHost().port)
287
# Semi-reasonable way to force cleanup
288
self.serverProtocol.connectionLost(error.ConnectionDone())
289
testPASV = defer.deferredGenerator(testPASV)
292
d = self._anonymousLogin()
293
self.assertCommandResponse('SYST', ["215 UNIX Type: L8"],
297
class FTPServerPasvDataConnectionTestCase(FTPServerTestCase):
298
def _makeDataConnection(self, ignored=None):
299
# Establish a passive data connection (i.e. client connecting to
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)
308
def _download(self, command, chainDeferred=None):
309
if chainDeferred is None:
310
chainDeferred = defer.succeed(None)
312
chainDeferred.addCallback(self._makeDataConnection)
313
def queueCommand(downloader):
314
# wait for the command to return, and the download connection to be
316
d1 = self.client.queueStringCommand(command)
318
return defer.gatherResults([d1, d2])
319
chainDeferred.addCallback(queueCommand)
321
def downloadDone((ignored, downloader)):
322
return downloader.buffer
323
return chainDeferred.addCallback(downloadDone)
325
def testEmptyLIST(self):
327
d = self._anonymousLogin()
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)
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'))
341
d = self._anonymousLogin()
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)
349
# Download a names-only listing.
350
self._download('NLST ', chainDeferred=d)
351
def checkDownload(download):
352
filenames = download[:-2].split('\r\n')
354
self.assertEqual(['bar', 'foo'], filenames)
355
d.addCallback(checkDownload)
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)
364
# Change the current working directory to 'foo'.
366
return self.client.queueStringCommand('CWD foo')
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)
376
def testManyLargeDownloads(self):
378
d = self._anonymousLogin()
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)
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)
393
class FTPServerPortDataConnectionTestCase(FTPServerPasvDataConnectionTestCase):
396
return FTPServerPasvDataConnectionTestCase.setUp(self)
398
def _makeDataConnection(self, ignored=None):
399
# Establish an active data connection (i.e. server connecting to
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)
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)
415
l = [defer.maybeDeferred(port.stopListening) for port in self.dataPorts]
416
d = defer.maybeDeferred(
417
FTPServerPasvDataConnectionTestCase.tearDown, self)
419
return defer.DeferredList(l, fireOnOneErrback=True)
421
def testPORTCannotConnect(self):
423
d = self._anonymousLogin()
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)
434
d.addCallback(loggedIn)
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)
445
# -- Client Tests -----------------------------------------------------------
447
class PrintLines(protocol.Protocol):
448
"""Helper class used by FTPFileListingTests."""
450
def __init__(self, lines):
453
def connectionMade(self):
454
for line in self._lines:
455
self.transport.write(line + "\r\n")
456
self.transport.loseConnection()
459
class MyFTPFileListProtocol(ftp.FTPFileListProtocol):
462
ftp.FTPFileListProtocol.__init__(self)
464
def unknownLine(self, line):
465
self.other.append(line)
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))
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)
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'
495
def check(((file1, file2), (other,))):
496
self.failUnless(other == 'woohoo! \r', 'incorrect other line')
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')
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)
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)
527
# This example derived from bug description in issue 514.
528
fileList = ftp.FTPFileListProtocol()
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()
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')
542
d = loopback.loopbackAsync(PrintLine(), fileList)
543
return d.addCallback(check)
546
class FTPClientTests(unittest.TestCase):
548
# Clean up self.port, if any.
549
port = getattr(self, 'port', None)
551
return port.stopListening()
553
def testFailedRETR(self):
554
f = protocol.Factory()
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)',
561
'331 Please specify the password.',
562
# PASS twisted@twistedmatrix.com
563
'230 Login successful. Have fun.',
565
'200 Binary it is, then.',
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)
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)
582
def testErrbacksUponDisconnect(self):
583
ftpClient = ftp.FTPClient()
584
d = ftpClient.list('some path', Dummy())
590
from twisted.internet.main import CONNECTION_LOST
591
ftpClient.connectionLost(failure.Failure(CONNECTION_LOST))
592
self.failUnless(m, m)
596
class FTPClientTestCase(unittest.TestCase):
598
Test advanced FTP client commands.
602
Create a FTP client and connect it to fake transport.
604
self.client = ftp.FTPClient()
605
self.transport = proto_helpers.StringTransport()
606
self.client.makeConnection(self.transport)
611
Deliver disconnection notification to the client so that it can
612
perform any cleanup which may be required.
614
self.client.connectionLost(error.ConnectionLost())
617
def _testLogin(self):
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.')
635
Test the CDUP command.
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.
641
(XXX - This is a bad API)
644
self.assertEquals(res[0], '250 Requested File Action Completed OK')
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')
654
def test_failedCDUP(self):
656
Test L{ftp.FTPClient.cdup}'s handling of a failed CDUP command.
658
When the CDUP command fails, the returned Deferred should errback
659
with L{ftp.CommandFailed}.
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')
672
Test the PWD command.
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.
678
(XXX - This is a bad API)
681
self.assertEquals(ftp.parsePWDResponse(res[0]), "/bar/baz")
684
d = self.client.pwd().addCallback(cbPwd)
685
self.assertEquals(self.transport.value(), 'PWD\r\n')
686
self.client.lineReceived('257 "/bar/baz"')
690
def test_failedPWD(self):
692
Test a failure in PWD command.
694
When the PWD command fails, the returned Deferred should errback
695
with L{ftp.CommandFailed}.
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')
707
Test the CWD command.
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.
713
(XXX - This is a bad API)
716
self.assertEquals(res[0], '250 Requested File Action Completed OK')
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')
725
def test_failedCWD(self):
727
Test a failure in CWD command.
729
When the PWD command fails, the returned Deferred should errback
730
with L{ftp.CommandFailed}.
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')
740
def test_passiveRETR(self):
742
Test the RETR command in passive mode: get a file and verify its
745
L{ftp.FTPClient.retrieveFile} should return a Deferred which fires
746
with the protocol instance passed to it after the download has
749
(XXX - This API should be based on producers and consumers)
751
def cbRetr(res, proto):
752
self.assertEquals(proto.buffer, 'x' * 1000)
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("")))
764
self.client.connectFactory = cbConnect
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.')
781
Test the RETR command in non-passive mode.
783
Like L{test_passiveRETR} but in the configuration where the server
784
establishes the data connection to the client, rather than the other
787
self.client.passive = False
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("")))
796
def cbRetr(res, proto):
797
self.assertEquals(proto.buffer, 'x' * 1000)
799
self.client.generatePortCommand = generatePort
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.')
814
def test_failedRETR(self):
816
Try to RETR an unexisting file.
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.
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("")))
831
self.client.connectFactory = cbConnect
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')
846
def test_passiveSTOR(self):
848
Test the STOR command: send a file and verify its content.
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.
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
861
tr = proto_helpers.StringTransport()
863
self.client.lineReceived(
864
'150 File status okay; about to open data connection.')
865
sender.transport.write("x" * 1000)
867
sender.connectionLost(failure.Failure(error.ConnectionDone("")))
870
self.assertEquals(tr.value(), "x" * 1000)
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)
878
self.client.connectFactory = cbConnect
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])
893
def test_failedSTOR(self):
895
Test a failure in the STOR command.
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}.
901
tr = proto_helpers.StringTransport()
903
self.client.lineReceived(
904
'150 File status okay; about to open data connection.')
905
sender.transport.write("x" * 1000)
907
sender.connectionLost(failure.Failure(error.ConnectionDone("")))
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)
915
self.client.connectFactory = cbConnect
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])
933
Test the STOR command in non-passive mode.
935
Like L{test_passiveSTOR} but in the configuration where the server
936
establishes the data connection to the client, rather than the other
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)
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)
956
sender.connectionLost(failure.Failure(error.ConnectionDone("")))
957
self.client.lineReceived('226 Transfer Complete.')
960
self.assertEquals(tr.value(), "x" * 1000)
962
self.client.generatePortCommand = generatePort
964
d1, d2 = self.client.storeFile("spam")
965
d1.addCallback(cbStore)
966
d2.addCallback(cbFinish)
967
return defer.gatherResults([d1, d2])
970
def test_passiveLIST(self):
972
Test the LIST command.
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
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
989
def cbList(res, fileList):
990
fls = [f["filename"] for f in fileList.files]
991
expected = ["foo", "bar", "baz"]
994
self.assertEquals(fls, expected)
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.')
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',
1009
proto.dataReceived(i)
1010
proto.connectionLost(failure.Failure(error.ConnectionDone("")))
1012
self.client.connectFactory = cbConnect
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.')
1025
def test_LIST(self):
1027
Test the LIST command in non-passive mode.
1029
Like L{test_passiveLIST} but in the configuration where the server
1030
establishes the data connection to the client, rather than the other
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.')
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',
1045
portCmd.protocol.dataReceived(i)
1046
portCmd.protocol.connectionLost(
1047
failure.Failure(error.ConnectionDone("")))
1049
def cbList(res, fileList):
1050
fls = [f["filename"] for f in fileList.files]
1051
expected = ["foo", "bar", "baz"]
1054
self.assertEquals(fls, expected)
1056
self.client.generatePortCommand = generatePort
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.')
1070
def test_failedLIST(self):
1072
Test a failure in LIST command.
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.
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("")))
1087
self.client.connectFactory = cbConnect
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')
1101
def test_NLST(self):
1103
Test the NLST command in non-passive mode.
1105
L{ftp.FTPClient.nlst} should return a Deferred which fires with a
1106
list of filenames when the list command has completed.
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("")))
1120
def cbList(res, proto):
1121
fls = proto.buffer.splitlines()
1122
expected = ["foo", "bar", "baz"]
1125
self.assertEquals(fls, expected)
1127
self.client.generatePortCommand = generatePort
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.')
1140
def test_passiveNLST(self):
1142
Test the NLST command.
1144
Like L{test_passiveNLST} but in the configuration where the server
1145
establishes the data connection to the client, rather than the other
1148
def cbList(res, proto):
1149
fls = proto.buffer.splitlines()
1150
expected = ["foo", "bar", "baz"]
1153
self.assertEquals(fls, expected)
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("")))
1167
self.client.connectFactory = cbConnect
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.')
1180
def test_failedNLST(self):
1182
Test a failure in NLST command.
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.
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("")))
1198
self.client.connectFactory = cbConnect
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')
1212
def test_changeDirectory(self):
1214
Test the changeDirectory method.
1216
L{ftp.FTPClient.changeDirectory} should return a Deferred which fires
1217
with True if succeeded.
1220
self.assertEquals(res, True)
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')
1229
def test_failedChangeDirectory(self):
1231
Test a failure in the changeDirectory method.
1233
The behaviour here is the same as a failed CWD.
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')
1243
def test_strangeFailedChangeDirectory(self):
1245
Test a strange failure in changeDirectory method.
1247
L{ftp.FTPClient.changeDirectory} is stricter than CWD as it checks
1248
code 250 for success.
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 !')
1258
def test_getDirectory(self):
1260
Test the getDirectory method.
1262
L{ftp.FTPClient.getDirectory} should return a Deferred which fires with
1263
the current directory on the server. It wraps PWD command.
1266
self.assertEquals(res, "/bar/baz")
1269
d = self.client.getDirectory().addCallback(cbGet)
1270
self.assertEquals(self.transport.value(), 'PWD\r\n')
1271
self.client.lineReceived('257 "/bar/baz"')
1275
def test_failedGetDirectory(self):
1277
Test a failure in getDirectory method.
1279
The behaviour should be the same as PWD.
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')
1289
def test_anotherFailedGetDirectory(self):
1291
Test a different failure in getDirectory method.
1293
The response should be quoted to be parsed, so it returns an error
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')
1305
class DummyTransport:
1306
def write(self, bytes):
1309
class BufferingTransport:
1311
def write(self, bytes):
1312
self.buffer += bytes
1315
class FTPClientBasicTests(unittest.TestCase):
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)
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)
1330
def testMultilineResponse(self):
1331
ftpClient = ftp.FTPClientBasic()
1332
ftpClient.transport = DummyTransport()
1333
ftpClient.lineReceived('220 Imaginary FTP.')
1335
# Queue (and send) a dummy command, and set up a callback to capture the
1337
deferred = ftpClient.queueStringCommand('BLAH')
1339
deferred.addCallback(result.append)
1340
deferred.addErrback(self.fail)
1342
# Send the first line of a multiline response.
1343
ftpClient.lineReceived('210-First line.')
1344
self.failUnlessEqual([], result)
1346
# Send a second line, again prefixed with "nnn-".
1347
ftpClient.lineReceived('123-Second line.')
1348
self.failUnlessEqual([], result)
1350
# Send a plain line of text, no prefix.
1351
ftpClient.lineReceived('Just some text.')
1352
self.failUnlessEqual([], result)
1354
# Now send a short (less than 4 chars) line.
1355
ftpClient.lineReceived('Hi')
1356
self.failUnlessEqual([], result)
1358
# Now send an empty line.
1359
ftpClient.lineReceived('')
1360
self.failUnlessEqual([], result)
1362
# And a line with 3 digits in it, and nothing else.
1363
ftpClient.lineReceived('321')
1364
self.failUnlessEqual([], result)
1367
ftpClient.lineReceived('210 Done.')
1368
self.failUnlessEqual(
1375
'210 Done.'], result[0])
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.')
1384
# Queue a login with no password
1385
ftpClient.queueLogin('bob', None)
1386
self.failUnlessEqual('USER bob\r\n', ftpClient.transport.buffer)
1388
# Clear the test buffer, acknowledge the USER command.
1389
ftpClient.transport.buffer = ''
1390
ftpClient.lineReceived('200 Hello bob.')
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)
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.')
1403
# Queue a login with no password
1404
ftpClient.queueLogin('bob', 'secret')
1405
self.failUnlessEqual('USER bob\r\n', ftpClient.transport.buffer)
1407
# Clear the test buffer, acknowledge the USER command with a 230
1409
ftpClient.transport.buffer = ''
1410
ftpClient.lineReceived('230 Hello bob. No password needed.')
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)
1417
class PathHandling(unittest.TestCase):
1418
def testNormalizer(self):
1419
for inp, outp in [('a', ['a']),
1422
('a/b/c', ['a', 'b', 'c']),
1423
('/a/b/c', ['a', 'b', 'c']),
1426
self.assertEquals(ftp.toSegments([], inp), outp)
1428
for inp, outp in [('b', ['a', '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)
1438
for inp, outp in [('//', []),
1441
('a//b', ['a', 'b'])]:
1442
self.assertEquals(ftp.toSegments([], inp), outp)
1444
for inp, outp in [('//', []),
1446
('b//c', ['a', 'b', 'c'])]:
1447
self.assertEquals(ftp.toSegments(['a'], inp), outp)
1449
for inp, outp in [('..', []),
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)
1463
for inp in ['..', '../', 'a/../..', 'a/../../',
1464
'/..', '/../', '/a/../..', '/a/../../',
1466
self.assertRaises(ftp.InvalidPath, ftp.toSegments, [], inp)
1468
for inp in ['../..', '../../', '../a/../..']:
1469
self.assertRaises(ftp.InvalidPath, ftp.toSegments, ['x'], inp)