Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

test_ftp.py 127KB

1 year ago
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829
  1. # Copyright (c) Twisted Matrix Laboratories.
  2. # See LICENSE for details.
  3. """
  4. FTP tests.
  5. """
  6. import errno
  7. import getpass
  8. import os
  9. import random
  10. import string
  11. from io import BytesIO
  12. from zope.interface import implementer
  13. from zope.interface.verify import verifyClass
  14. from twisted.cred import checkers, credentials, portal
  15. from twisted.cred.error import UnauthorizedLogin
  16. from twisted.cred.portal import IRealm
  17. from twisted.internet import defer, error, protocol, reactor, task
  18. from twisted.internet.interfaces import IConsumer
  19. from twisted.protocols import basic, ftp, loopback
  20. from twisted.python import failure, filepath, runtime
  21. from twisted.test import proto_helpers
  22. from twisted.trial.unittest import TestCase
  23. if not runtime.platform.isWindows():
  24. nonPOSIXSkip = None
  25. else:
  26. nonPOSIXSkip = "Cannot run on Windows"
  27. class Dummy(basic.LineReceiver):
  28. logname = None
  29. def __init__(self):
  30. self.lines = []
  31. self.rawData = []
  32. def connectionMade(self):
  33. self.f = self.factory # to save typing in pdb :-)
  34. def lineReceived(self, line):
  35. self.lines.append(line)
  36. def rawDataReceived(self, data):
  37. self.rawData.append(data)
  38. def lineLengthExceeded(self, line):
  39. pass
  40. class _BufferingProtocol(protocol.Protocol):
  41. def connectionMade(self):
  42. self.buffer = b""
  43. self.d = defer.Deferred()
  44. def dataReceived(self, data):
  45. self.buffer += data
  46. def connectionLost(self, reason):
  47. self.d.callback(self)
  48. def passivemode_msg(protocol, host="127.0.0.1", port=12345):
  49. """
  50. Construct a passive mode message with the correct encoding
  51. @param protocol: the FTP protocol from which to base the encoding
  52. @param host: the hostname
  53. @param port: the port
  54. @return: the passive mode message
  55. """
  56. msg = f"227 Entering Passive Mode ({ftp.encodeHostPort(host, port)})."
  57. return msg.encode(protocol._encoding)
  58. class FTPServerTestCase(TestCase):
  59. """
  60. Simple tests for an FTP server with the default settings.
  61. @ivar clientFactory: class used as ftp client.
  62. """
  63. clientFactory = ftp.FTPClientBasic
  64. userAnonymous = "anonymous"
  65. def setUp(self):
  66. # Keep a list of the protocols created so we can make sure they all
  67. # disconnect before the tests end.
  68. protocols = []
  69. # Create a directory
  70. self.directory = self.mktemp()
  71. os.mkdir(self.directory)
  72. self.dirPath = filepath.FilePath(self.directory)
  73. # Start the server
  74. p = portal.Portal(
  75. ftp.FTPRealm(
  76. anonymousRoot=self.directory,
  77. userHome=self.directory,
  78. )
  79. )
  80. p.registerChecker(checkers.AllowAnonymousAccess(), credentials.IAnonymous)
  81. users_checker = checkers.InMemoryUsernamePasswordDatabaseDontUse()
  82. self.username = "test-user"
  83. self.password = "test-password"
  84. users_checker.addUser(self.username, self.password)
  85. p.registerChecker(users_checker, credentials.IUsernamePassword)
  86. self.factory = ftp.FTPFactory(portal=p, userAnonymous=self.userAnonymous)
  87. self.port = port = reactor.listenTCP(0, self.factory, interface="127.0.0.1")
  88. self.addCleanup(port.stopListening)
  89. # Hook the server's buildProtocol to make the protocol instance
  90. # accessible to tests.
  91. buildProtocol = self.factory.buildProtocol
  92. d1 = defer.Deferred()
  93. def _rememberProtocolInstance(addr):
  94. # Done hooking this.
  95. del self.factory.buildProtocol
  96. protocol = buildProtocol(addr)
  97. self.serverProtocol = protocol.wrappedProtocol
  98. def cleanupServer():
  99. if self.serverProtocol.transport is not None:
  100. self.serverProtocol.transport.loseConnection()
  101. self.addCleanup(cleanupServer)
  102. d1.callback(None)
  103. protocols.append(protocol)
  104. return protocol
  105. self.factory.buildProtocol = _rememberProtocolInstance
  106. # Connect a client to it
  107. portNum = port.getHost().port
  108. clientCreator = protocol.ClientCreator(reactor, self.clientFactory)
  109. d2 = clientCreator.connectTCP("127.0.0.1", portNum)
  110. def gotClient(client):
  111. self.client = client
  112. self.addCleanup(self.client.transport.loseConnection)
  113. protocols.append(self.client)
  114. d2.addCallback(gotClient)
  115. self.addCleanup(proto_helpers.waitUntilAllDisconnected, reactor, protocols)
  116. return defer.gatherResults([d1, d2])
  117. def assertCommandResponse(self, command, expectedResponseLines, chainDeferred=None):
  118. """
  119. Asserts that a sending an FTP command receives the expected
  120. response.
  121. Returns a Deferred. Optionally accepts a deferred to chain its actions
  122. to.
  123. """
  124. if chainDeferred is None:
  125. chainDeferred = defer.succeed(None)
  126. def queueCommand(ignored):
  127. d = self.client.queueStringCommand(command)
  128. def gotResponse(responseLines):
  129. self.assertEqual(expectedResponseLines, responseLines)
  130. return d.addCallback(gotResponse)
  131. return chainDeferred.addCallback(queueCommand)
  132. def assertCommandFailed(self, command, expectedResponse=None, chainDeferred=None):
  133. if chainDeferred is None:
  134. chainDeferred = defer.succeed(None)
  135. def queueCommand(ignored):
  136. return self.client.queueStringCommand(command)
  137. chainDeferred.addCallback(queueCommand)
  138. self.assertFailure(chainDeferred, ftp.CommandFailed)
  139. def failed(exception):
  140. if expectedResponse is not None:
  141. self.assertEqual(expectedResponse, exception.args[0])
  142. return chainDeferred.addCallback(failed)
  143. def _anonymousLogin(self):
  144. d = self.assertCommandResponse(
  145. "USER anonymous",
  146. ["331 Guest login ok, type your email address as password."],
  147. )
  148. return self.assertCommandResponse(
  149. "PASS test@twistedmatrix.com",
  150. ["230 Anonymous login ok, access restrictions apply."],
  151. chainDeferred=d,
  152. )
  153. def _userLogin(self):
  154. """
  155. Authenticates the FTP client using the test account.
  156. @return: L{Deferred} of command response
  157. """
  158. d = self.assertCommandResponse(
  159. "USER %s" % (self.username),
  160. ["331 Password required for %s." % (self.username)],
  161. )
  162. return self.assertCommandResponse(
  163. "PASS %s" % (self.password),
  164. ["230 User logged in, proceed"],
  165. chainDeferred=d,
  166. )
  167. class FTPAnonymousTests(FTPServerTestCase):
  168. """
  169. Simple tests for an FTP server with different anonymous username.
  170. The new anonymous username used in this test case is "guest"
  171. """
  172. userAnonymous = "guest"
  173. def test_anonymousLogin(self):
  174. """
  175. Tests whether the changing of the anonymous username is working or not.
  176. The FTP server should not comply about the need of password for the
  177. username 'guest', letting it login as anonymous asking just an email
  178. address as password.
  179. """
  180. d = self.assertCommandResponse(
  181. "USER guest", ["331 Guest login ok, type your email address as password."]
  182. )
  183. return self.assertCommandResponse(
  184. "PASS test@twistedmatrix.com",
  185. ["230 Anonymous login ok, access restrictions apply."],
  186. chainDeferred=d,
  187. )
  188. class BasicFTPServerTests(FTPServerTestCase):
  189. """
  190. Basic functionality of FTP server.
  191. """
  192. def test_tooManyConnections(self):
  193. """
  194. When the connection limit is reached, the server should send an
  195. appropriate response
  196. """
  197. self.factory.connectionLimit = 1
  198. cc = protocol.ClientCreator(reactor, _BufferingProtocol)
  199. d = cc.connectTCP("127.0.0.1", self.port.getHost().port)
  200. @d.addCallback
  201. def gotClient(proto):
  202. return proto.d
  203. @d.addCallback
  204. def onConnectionLost(proto):
  205. self.assertEqual(
  206. b"421 Too many users right now, try again in a few minutes." b"\r\n",
  207. proto.buffer,
  208. )
  209. return d
  210. def test_NotLoggedInReply(self):
  211. """
  212. When not logged in, most commands other than USER and PASS should
  213. get NOT_LOGGED_IN errors, but some can be called before USER and PASS.
  214. """
  215. loginRequiredCommandList = [
  216. "CDUP",
  217. "CWD",
  218. "LIST",
  219. "MODE",
  220. "PASV",
  221. "PWD",
  222. "RETR",
  223. "STRU",
  224. "SYST",
  225. "TYPE",
  226. ]
  227. loginNotRequiredCommandList = ["FEAT"]
  228. # Issue commands, check responses
  229. def checkFailResponse(exception, command):
  230. failureResponseLines = exception.args[0]
  231. self.assertTrue(
  232. failureResponseLines[-1].startswith("530"),
  233. "%s - Response didn't start with 530: %r"
  234. % (
  235. command,
  236. failureResponseLines[-1],
  237. ),
  238. )
  239. def checkPassResponse(result, command):
  240. result = result[0]
  241. self.assertFalse(
  242. result.startswith("530"),
  243. "%s - Response start with 530: %r"
  244. % (
  245. command,
  246. result,
  247. ),
  248. )
  249. deferreds = []
  250. for command in loginRequiredCommandList:
  251. deferred = self.client.queueStringCommand(command)
  252. self.assertFailure(deferred, ftp.CommandFailed)
  253. deferred.addCallback(checkFailResponse, command)
  254. deferreds.append(deferred)
  255. for command in loginNotRequiredCommandList:
  256. deferred = self.client.queueStringCommand(command)
  257. deferred.addCallback(checkPassResponse, command)
  258. deferreds.append(deferred)
  259. return defer.DeferredList(deferreds, fireOnOneErrback=True)
  260. def test_PASSBeforeUSER(self):
  261. """
  262. Issuing PASS before USER should give an error.
  263. """
  264. return self.assertCommandFailed(
  265. "PASS foo",
  266. ["503 Incorrect sequence of commands: " "USER required before PASS"],
  267. )
  268. def test_NoParamsForUSER(self):
  269. """
  270. Issuing USER without a username is a syntax error.
  271. """
  272. return self.assertCommandFailed(
  273. "USER", ["500 Syntax error: USER requires an argument."]
  274. )
  275. def test_NoParamsForPASS(self):
  276. """
  277. Issuing PASS without a password is a syntax error.
  278. """
  279. d = self.client.queueStringCommand("USER foo")
  280. return self.assertCommandFailed(
  281. "PASS", ["500 Syntax error: PASS requires an argument."], chainDeferred=d
  282. )
  283. def test_loginError(self):
  284. """
  285. Unexpected exceptions from the login handler are caught
  286. """
  287. def _fake_loginhandler(*args, **kwargs):
  288. return defer.fail(AssertionError("test exception"))
  289. self.serverProtocol.portal.login = _fake_loginhandler
  290. d = self.client.queueStringCommand("USER foo")
  291. self.assertCommandFailed(
  292. "PASS bar",
  293. ["550 Requested action not taken: internal server error"],
  294. chainDeferred=d,
  295. )
  296. @d.addCallback
  297. def checkLogs(result):
  298. logs = self.flushLoggedErrors()
  299. self.assertEqual(1, len(logs))
  300. self.assertIsInstance(logs[0].value, AssertionError)
  301. return d
  302. def test_AnonymousLogin(self):
  303. """
  304. Login with userid 'anonymous'
  305. """
  306. return self._anonymousLogin()
  307. def test_Quit(self):
  308. """
  309. Issuing QUIT should return a 221 message.
  310. @return: L{Deferred} of command response
  311. """
  312. d = self._anonymousLogin()
  313. return self.assertCommandResponse("QUIT", ["221 Goodbye."], chainDeferred=d)
  314. def test_AnonymousLoginDenied(self):
  315. """
  316. Reconfigure the server to disallow anonymous access, and to have an
  317. IUsernamePassword checker that always rejects.
  318. @return: L{Deferred} of command response
  319. """
  320. self.factory.allowAnonymous = False
  321. denyAlwaysChecker = checkers.InMemoryUsernamePasswordDatabaseDontUse()
  322. self.factory.portal.registerChecker(
  323. denyAlwaysChecker, credentials.IUsernamePassword
  324. )
  325. # Same response code as allowAnonymous=True, but different text.
  326. d = self.assertCommandResponse(
  327. "USER anonymous", ["331 Password required for anonymous."]
  328. )
  329. # It will be denied. No-one can login.
  330. d = self.assertCommandFailed(
  331. "PASS test@twistedmatrix.com",
  332. ["530 Sorry, Authentication failed."],
  333. chainDeferred=d,
  334. )
  335. # It's not just saying that. You aren't logged in.
  336. d = self.assertCommandFailed(
  337. "PWD", ["530 Please login with USER and PASS."], chainDeferred=d
  338. )
  339. return d
  340. def test_anonymousWriteDenied(self):
  341. """
  342. When an anonymous user attempts to edit the server-side filesystem, they
  343. will receive a 550 error with a descriptive message.
  344. @return: L{Deferred} of command response
  345. """
  346. d = self._anonymousLogin()
  347. return self.assertCommandFailed(
  348. "MKD newdir",
  349. ["550 Anonymous users are forbidden to change the filesystem"],
  350. chainDeferred=d,
  351. )
  352. def test_UnknownCommand(self):
  353. """
  354. Send an invalid command.
  355. @return: L{Deferred} of command response
  356. """
  357. d = self._anonymousLogin()
  358. return self.assertCommandFailed(
  359. "GIBBERISH", ["502 Command 'GIBBERISH' not implemented"], chainDeferred=d
  360. )
  361. def test_RETRBeforePORT(self):
  362. """
  363. Send RETR before sending PORT.
  364. @return: L{Deferred} of command response
  365. """
  366. d = self._anonymousLogin()
  367. return self.assertCommandFailed(
  368. "RETR foo",
  369. [
  370. "503 Incorrect sequence of commands: "
  371. "PORT or PASV required before RETR"
  372. ],
  373. chainDeferred=d,
  374. )
  375. def test_STORBeforePORT(self):
  376. """
  377. Send STOR before sending PORT.
  378. @return: L{Deferred} of command response
  379. """
  380. d = self._anonymousLogin()
  381. return self.assertCommandFailed(
  382. "STOR foo",
  383. [
  384. "503 Incorrect sequence of commands: "
  385. "PORT or PASV required before STOR"
  386. ],
  387. chainDeferred=d,
  388. )
  389. def test_BadCommandArgs(self):
  390. """
  391. Send command with bad arguments.
  392. @return: L{Deferred} of command response
  393. """
  394. d = self._anonymousLogin()
  395. self.assertCommandFailed(
  396. "MODE z", ["504 Not implemented for parameter 'z'."], chainDeferred=d
  397. )
  398. self.assertCommandFailed(
  399. "STRU I", ["504 Not implemented for parameter 'I'."], chainDeferred=d
  400. )
  401. return d
  402. def test_DecodeHostPort(self):
  403. """
  404. Decode a host port.
  405. """
  406. self.assertEqual(
  407. ftp.decodeHostPort("25,234,129,22,100,23"), ("25.234.129.22", 25623)
  408. )
  409. nums = range(6)
  410. for i in range(6):
  411. badValue = list(nums)
  412. badValue[i] = 256
  413. s = ",".join(map(str, badValue))
  414. self.assertRaises(ValueError, ftp.decodeHostPort, s)
  415. def test_PASV(self):
  416. """
  417. When the client sends the command C{PASV}, the server responds with a
  418. host and port, and is listening on that port.
  419. """
  420. # Login
  421. d = self._anonymousLogin()
  422. # Issue a PASV command
  423. d.addCallback(lambda _: self.client.queueStringCommand("PASV"))
  424. def cb(responseLines):
  425. """
  426. Extract the host and port from the resonse, and
  427. verify the server is listening of the port it claims to be.
  428. """
  429. host, port = ftp.decodeHostPort(responseLines[-1][4:])
  430. self.assertEqual(port, self.serverProtocol.dtpPort.getHost().port)
  431. d.addCallback(cb)
  432. # Semi-reasonable way to force cleanup
  433. d.addCallback(lambda _: self.serverProtocol.transport.loseConnection())
  434. return d
  435. def test_SYST(self):
  436. """
  437. SYST command will always return UNIX Type: L8
  438. """
  439. d = self._anonymousLogin()
  440. self.assertCommandResponse("SYST", ["215 UNIX Type: L8"], chainDeferred=d)
  441. return d
  442. def test_RNFRandRNTO(self):
  443. """
  444. Sending the RNFR command followed by RNTO, with valid filenames, will
  445. perform a successful rename operation.
  446. """
  447. # Create user home folder with a 'foo' file.
  448. self.dirPath.child(self.username).createDirectory()
  449. self.dirPath.child(self.username).child("foo").touch()
  450. d = self._userLogin()
  451. self.assertCommandResponse(
  452. "RNFR foo",
  453. ["350 Requested file action pending further information."],
  454. chainDeferred=d,
  455. )
  456. self.assertCommandResponse(
  457. "RNTO bar", ["250 Requested File Action Completed OK"], chainDeferred=d
  458. )
  459. def check_rename(result):
  460. self.assertTrue(self.dirPath.child(self.username).child("bar").exists())
  461. return result
  462. d.addCallback(check_rename)
  463. return d
  464. def test_RNFRwithoutRNTO(self):
  465. """
  466. Sending the RNFR command followed by any command other than RNTO
  467. should return an error informing users that RNFR should be followed
  468. by RNTO.
  469. """
  470. d = self._anonymousLogin()
  471. self.assertCommandResponse(
  472. "RNFR foo",
  473. ["350 Requested file action pending further information."],
  474. chainDeferred=d,
  475. )
  476. self.assertCommandFailed(
  477. "OTHER don-tcare",
  478. ["503 Incorrect sequence of commands: RNTO required after RNFR"],
  479. chainDeferred=d,
  480. )
  481. return d
  482. def test_portRangeForwardError(self):
  483. """
  484. Exceptions other than L{error.CannotListenError} which are raised by
  485. C{listenFactory} should be raised to the caller of L{FTP.getDTPPort}.
  486. """
  487. def listenFactory(portNumber, factory):
  488. raise RuntimeError()
  489. self.serverProtocol.listenFactory = listenFactory
  490. self.assertRaises(
  491. RuntimeError, self.serverProtocol.getDTPPort, protocol.Factory()
  492. )
  493. def test_portRange(self):
  494. """
  495. L{FTP.passivePortRange} should determine the ports which
  496. L{FTP.getDTPPort} attempts to bind. If no port from that iterator can
  497. be bound, L{error.CannotListenError} should be raised, otherwise the
  498. first successful result from L{FTP.listenFactory} should be returned.
  499. """
  500. def listenFactory(portNumber, factory):
  501. if portNumber in (22032, 22033, 22034):
  502. raise error.CannotListenError("localhost", portNumber, "error")
  503. return portNumber
  504. self.serverProtocol.listenFactory = listenFactory
  505. port = self.serverProtocol.getDTPPort(protocol.Factory())
  506. self.assertEqual(port, 0)
  507. self.serverProtocol.passivePortRange = range(22032, 65536)
  508. port = self.serverProtocol.getDTPPort(protocol.Factory())
  509. self.assertEqual(port, 22035)
  510. self.serverProtocol.passivePortRange = range(22032, 22035)
  511. self.assertRaises(
  512. error.CannotListenError, self.serverProtocol.getDTPPort, protocol.Factory()
  513. )
  514. def test_portRangeInheritedFromFactory(self):
  515. """
  516. The L{FTP} instances created by L{ftp.FTPFactory.buildProtocol} have
  517. their C{passivePortRange} attribute set to the same object the
  518. factory's C{passivePortRange} attribute is set to.
  519. """
  520. portRange = range(2017, 2031)
  521. self.factory.passivePortRange = portRange
  522. protocol = self.factory.buildProtocol(None)
  523. self.assertEqual(portRange, protocol.wrappedProtocol.passivePortRange)
  524. def test_FEAT(self):
  525. """
  526. When the server receives 'FEAT', it should report the list of supported
  527. features. (Additionally, ensure that the server reports various
  528. particular features that are supported by all Twisted FTP servers.)
  529. """
  530. d = self.client.queueStringCommand("FEAT")
  531. def gotResponse(responseLines):
  532. self.assertEqual("211-Features:", responseLines[0])
  533. self.assertIn(" MDTM", responseLines)
  534. self.assertIn(" PASV", responseLines)
  535. self.assertIn(" TYPE A;I", responseLines)
  536. self.assertIn(" SIZE", responseLines)
  537. self.assertEqual("211 End", responseLines[-1])
  538. return d.addCallback(gotResponse)
  539. def test_OPTS(self):
  540. """
  541. When the server receives 'OPTS something', it should report
  542. that the FTP server does not support the option called 'something'.
  543. """
  544. d = self._anonymousLogin()
  545. self.assertCommandFailed(
  546. "OPTS something",
  547. ["502 Option 'something' not implemented."],
  548. chainDeferred=d,
  549. )
  550. return d
  551. def test_STORreturnsErrorFromOpen(self):
  552. """
  553. Any FTP error raised inside STOR while opening the file is returned
  554. to the client.
  555. """
  556. # We create a folder inside user's home folder and then
  557. # we try to write a file with the same name.
  558. # This will trigger an FTPCmdError.
  559. self.dirPath.child(self.username).createDirectory()
  560. self.dirPath.child(self.username).child("folder").createDirectory()
  561. d = self._userLogin()
  562. def sendPASV(result):
  563. """
  564. Send the PASV command required before port.
  565. """
  566. return self.client.queueStringCommand("PASV")
  567. def mockDTPInstance(result):
  568. """
  569. Fake an incoming connection and create a mock DTPInstance so
  570. that PORT command will start processing the request.
  571. """
  572. self.serverProtocol.dtpFactory.deferred.callback(None)
  573. self.serverProtocol.dtpInstance = object()
  574. return result
  575. d.addCallback(sendPASV)
  576. d.addCallback(mockDTPInstance)
  577. self.assertCommandFailed(
  578. "STOR folder",
  579. ["550 folder: is a directory"],
  580. chainDeferred=d,
  581. )
  582. return d
  583. def test_STORunknownErrorBecomesFileNotFound(self):
  584. """
  585. Any non FTP error raised inside STOR while opening the file is
  586. converted into FileNotFound error and returned to the client together
  587. with the path.
  588. The unknown error is logged.
  589. """
  590. d = self._userLogin()
  591. def failingOpenForWriting(ignore):
  592. """
  593. Override openForWriting.
  594. @param ignore: ignored, used for callback
  595. @return: an error
  596. """
  597. return defer.fail(AssertionError())
  598. def sendPASV(result):
  599. """
  600. Send the PASV command required before port.
  601. @param result: parameter used in L{Deferred}
  602. """
  603. return self.client.queueStringCommand("PASV")
  604. def mockDTPInstance(result):
  605. """
  606. Fake an incoming connection and create a mock DTPInstance so
  607. that PORT command will start processing the request.
  608. @param result: parameter used in L{Deferred}
  609. """
  610. self.serverProtocol.dtpFactory.deferred.callback(None)
  611. self.serverProtocol.dtpInstance = object()
  612. self.serverProtocol.shell.openForWriting = failingOpenForWriting
  613. return result
  614. def checkLogs(result):
  615. """
  616. Check that unknown errors are logged.
  617. @param result: parameter used in L{Deferred}
  618. """
  619. logs = self.flushLoggedErrors()
  620. self.assertEqual(1, len(logs))
  621. self.assertIsInstance(logs[0].value, AssertionError)
  622. d.addCallback(sendPASV)
  623. d.addCallback(mockDTPInstance)
  624. self.assertCommandFailed(
  625. "STOR something",
  626. ["550 something: No such file or directory."],
  627. chainDeferred=d,
  628. )
  629. d.addCallback(checkLogs)
  630. return d
  631. class FTPServerAdvancedClientTests(FTPServerTestCase):
  632. """
  633. Test FTP server with the L{ftp.FTPClient} class.
  634. """
  635. clientFactory = ftp.FTPClient
  636. def test_anonymousSTOR(self):
  637. """
  638. Try to make an STOR as anonymous, and check that we got a permission
  639. denied error.
  640. """
  641. def eb(res):
  642. res.trap(ftp.CommandFailed)
  643. self.assertEqual(res.value.args[0][0], "550 foo: Permission denied.")
  644. d1, d2 = self.client.storeFile("foo")
  645. d2.addErrback(eb)
  646. return defer.gatherResults([d1, d2])
  647. def test_STORtransferErrorIsReturned(self):
  648. """
  649. Any FTP error raised by STOR while transferring the file is returned
  650. to the client.
  651. """
  652. # Make a failing file writer.
  653. class FailingFileWriter(ftp._FileWriter):
  654. def receive(self):
  655. return defer.fail(ftp.IsADirectoryError("failing_file"))
  656. def failingSTOR(a, b):
  657. return defer.succeed(FailingFileWriter(None))
  658. # Monkey patch the shell so it returns a file writer that will
  659. # fail during transfer.
  660. self.patch(ftp.FTPAnonymousShell, "openForWriting", failingSTOR)
  661. def eb(res):
  662. res.trap(ftp.CommandFailed)
  663. logs = self.flushLoggedErrors()
  664. self.assertEqual(1, len(logs))
  665. self.assertIsInstance(logs[0].value, ftp.IsADirectoryError)
  666. self.assertEqual(res.value.args[0][0], "550 failing_file: is a directory")
  667. d1, d2 = self.client.storeFile("failing_file")
  668. d2.addErrback(eb)
  669. return defer.gatherResults([d1, d2])
  670. def test_STORunknownTransferErrorBecomesAbort(self):
  671. """
  672. Any non FTP error raised by STOR while transferring the file is
  673. converted into a critical error and transfer is closed.
  674. The unknown error is logged.
  675. """
  676. class FailingFileWriter(ftp._FileWriter):
  677. def receive(self):
  678. return defer.fail(AssertionError())
  679. def failingSTOR(a, b):
  680. return defer.succeed(FailingFileWriter(None))
  681. # Monkey patch the shell so it returns a file writer that will
  682. # fail during transfer.
  683. self.patch(ftp.FTPAnonymousShell, "openForWriting", failingSTOR)
  684. def eb(res):
  685. res.trap(ftp.CommandFailed)
  686. logs = self.flushLoggedErrors()
  687. self.assertEqual(1, len(logs))
  688. self.assertIsInstance(logs[0].value, AssertionError)
  689. self.assertEqual(
  690. res.value.args[0][0], "426 Transfer aborted. Data connection closed."
  691. )
  692. d1, d2 = self.client.storeFile("failing_file")
  693. d2.addErrback(eb)
  694. return defer.gatherResults([d1, d2])
  695. def test_RETRreadError(self):
  696. """
  697. Any errors during reading a file inside a RETR should be returned to
  698. the client.
  699. """
  700. # Make a failing file reading.
  701. class FailingFileReader(ftp._FileReader):
  702. def send(self, consumer):
  703. return defer.fail(ftp.IsADirectoryError("blah"))
  704. def failingRETR(a, b):
  705. return defer.succeed(FailingFileReader(None))
  706. # Monkey patch the shell so it returns a file reader that will
  707. # fail.
  708. self.patch(ftp.FTPAnonymousShell, "openForReading", failingRETR)
  709. def check_response(failure):
  710. self.flushLoggedErrors()
  711. failure.trap(ftp.CommandFailed)
  712. self.assertEqual(
  713. failure.value.args[0][0],
  714. "125 Data connection already open, starting transfer",
  715. )
  716. self.assertEqual(failure.value.args[0][1], "550 blah: is a directory")
  717. proto = _BufferingProtocol()
  718. d = self.client.retrieveFile("failing_file", proto)
  719. d.addErrback(check_response)
  720. return d
  721. class FTPServerPasvDataConnectionTests(FTPServerTestCase):
  722. """
  723. PASV data connection.
  724. """
  725. def _makeDataConnection(self, ignored=None):
  726. """
  727. Establish a passive data connection (i.e. client connecting to
  728. server).
  729. @param ignored: ignored
  730. @return: L{Deferred.addCallback}
  731. """
  732. d = self.client.queueStringCommand("PASV")
  733. def gotPASV(responseLines):
  734. host, port = ftp.decodeHostPort(responseLines[-1][4:])
  735. cc = protocol.ClientCreator(reactor, _BufferingProtocol)
  736. return cc.connectTCP("127.0.0.1", port)
  737. return d.addCallback(gotPASV)
  738. def _download(self, command, chainDeferred=None):
  739. """
  740. Download file.
  741. @param command: command to run
  742. @param chainDeferred: L{Deferred} used to queue commands.
  743. @return: L{Deferred} of command response
  744. """
  745. if chainDeferred is None:
  746. chainDeferred = defer.succeed(None)
  747. chainDeferred.addCallback(self._makeDataConnection)
  748. def queueCommand(downloader):
  749. # Wait for the command to return, and the download connection to be
  750. # closed.
  751. d1 = self.client.queueStringCommand(command)
  752. d2 = downloader.d
  753. return defer.gatherResults([d1, d2])
  754. chainDeferred.addCallback(queueCommand)
  755. def downloadDone(result):
  756. (ignored, downloader) = result
  757. return downloader.buffer
  758. return chainDeferred.addCallback(downloadDone)
  759. def test_LISTEmpty(self):
  760. """
  761. When listing empty folders, LIST returns an empty response.
  762. """
  763. d = self._anonymousLogin()
  764. # No files, so the file listing should be empty
  765. self._download("LIST", chainDeferred=d)
  766. def checkEmpty(result):
  767. self.assertEqual(b"", result)
  768. return d.addCallback(checkEmpty)
  769. def test_LISTWithBinLsFlags(self):
  770. """
  771. LIST ignores requests for folder with names like '-al' and will list
  772. the content of current folder.
  773. """
  774. os.mkdir(os.path.join(self.directory, "foo"))
  775. os.mkdir(os.path.join(self.directory, "bar"))
  776. # Login
  777. d = self._anonymousLogin()
  778. self._download("LIST -aL", chainDeferred=d)
  779. def checkDownload(download):
  780. names = []
  781. for line in download.splitlines():
  782. names.append(line.split(b" ")[-1])
  783. self.assertEqual(2, len(names))
  784. self.assertIn(b"foo", names)
  785. self.assertIn(b"bar", names)
  786. return d.addCallback(checkDownload)
  787. def test_LISTWithContent(self):
  788. """
  789. LIST returns all folder's members, each member listed on a separate
  790. line and with name and other details.
  791. """
  792. os.mkdir(os.path.join(self.directory, "foo"))
  793. os.mkdir(os.path.join(self.directory, "bar"))
  794. # Login
  795. d = self._anonymousLogin()
  796. # We expect 2 lines because there are two files.
  797. self._download("LIST", chainDeferred=d)
  798. def checkDownload(download):
  799. self.assertEqual(2, len(download[:-2].split(b"\r\n")))
  800. d.addCallback(checkDownload)
  801. # Download a names-only listing.
  802. self._download("NLST ", chainDeferred=d)
  803. def checkDownload(download):
  804. filenames = download[:-2].split(b"\r\n")
  805. filenames.sort()
  806. self.assertEqual([b"bar", b"foo"], filenames)
  807. d.addCallback(checkDownload)
  808. # Download a listing of the 'foo' subdirectory. 'foo' has no files, so
  809. # the file listing should be empty.
  810. self._download("LIST foo", chainDeferred=d)
  811. def checkDownload(download):
  812. self.assertEqual(b"", download)
  813. d.addCallback(checkDownload)
  814. # Change the current working directory to 'foo'.
  815. def chdir(ignored):
  816. return self.client.queueStringCommand("CWD foo")
  817. d.addCallback(chdir)
  818. # Download a listing from within 'foo', and again it should be empty,
  819. # because LIST uses the working directory by default.
  820. self._download("LIST", chainDeferred=d)
  821. def checkDownload(download):
  822. self.assertEqual(b"", download)
  823. return d.addCallback(checkDownload)
  824. def _listTestHelper(self, command, listOutput, expectedOutput):
  825. """
  826. Exercise handling by the implementation of I{LIST} or I{NLST} of certain
  827. return values and types from an L{IFTPShell.list} implementation.
  828. This will issue C{command} and assert that if the L{IFTPShell.list}
  829. implementation includes C{listOutput} as one of the file entries then
  830. the result given to the client is matches C{expectedOutput}.
  831. @param command: Either C{b"LIST"} or C{b"NLST"}
  832. @type command: L{bytes}
  833. @param listOutput: A value suitable to be used as an element of the list
  834. returned by L{IFTPShell.list}. Vary the values and types of the
  835. contents to exercise different code paths in the server's handling
  836. of this result.
  837. @param expectedOutput: A line of output to expect as a result of
  838. C{listOutput} being transformed into a response to the command
  839. issued.
  840. @type expectedOutput: L{bytes}
  841. @return: A L{Deferred} which fires when the test is done, either with an
  842. L{Failure} if the test failed or with a function object if it
  843. succeeds. The function object is the function which implements
  844. L{IFTPShell.list} (and is useful to make assertions about what
  845. warnings might have been emitted).
  846. @rtype: L{Deferred}
  847. """
  848. # Login
  849. d = self._anonymousLogin()
  850. def patchedList(segments, keys=()):
  851. return defer.succeed([listOutput])
  852. def loggedIn(result):
  853. self.serverProtocol.shell.list = patchedList
  854. return result
  855. d.addCallback(loggedIn)
  856. self._download(f"{command} something", chainDeferred=d)
  857. def checkDownload(download):
  858. self.assertEqual(expectedOutput, download)
  859. return patchedList
  860. return d.addCallback(checkDownload)
  861. def test_LISTUnicode(self):
  862. """
  863. Unicode filenames returned from L{IFTPShell.list} are encoded using
  864. UTF-8 before being sent with the response.
  865. """
  866. return self._listTestHelper(
  867. "LIST",
  868. (
  869. "my resum\xe9",
  870. (0, 1, filepath.Permissions(0o777), 0, 0, "user", "group"),
  871. ),
  872. b"drwxrwxrwx 0 user group "
  873. b"0 Jan 01 1970 my resum\xc3\xa9\r\n",
  874. )
  875. def test_LISTNonASCIIBytes(self):
  876. """
  877. When LIST receive a filename as byte string from L{IFTPShell.list}
  878. it will just pass the data to lower level without any change.
  879. @return: L{_listTestHelper}
  880. """
  881. return self._listTestHelper(
  882. "LIST",
  883. (
  884. b"my resum\xc3\xa9",
  885. (0, 1, filepath.Permissions(0o777), 0, 0, "user", "group"),
  886. ),
  887. b"drwxrwxrwx 0 user group "
  888. b"0 Jan 01 1970 my resum\xc3\xa9\r\n",
  889. )
  890. def test_ManyLargeDownloads(self):
  891. """
  892. Download many large files.
  893. @return: L{Deferred}
  894. """
  895. # Login
  896. d = self._anonymousLogin()
  897. # Download a range of different size files
  898. for size in range(100000, 110000, 500):
  899. with open(os.path.join(self.directory, "%d.txt" % (size,)), "wb") as fObj:
  900. fObj.write(b"x" * size)
  901. self._download("RETR %d.txt" % (size,), chainDeferred=d)
  902. def checkDownload(download, size=size):
  903. self.assertEqual(size, len(download))
  904. d.addCallback(checkDownload)
  905. return d
  906. def test_downloadFolder(self):
  907. """
  908. When RETR is called for a folder, it will fail complaining that
  909. the path is a folder.
  910. """
  911. # Make a directory in the current working directory
  912. self.dirPath.child("foo").createDirectory()
  913. # Login
  914. d = self._anonymousLogin()
  915. d.addCallback(self._makeDataConnection)
  916. def retrFolder(downloader):
  917. downloader.transport.loseConnection()
  918. deferred = self.client.queueStringCommand("RETR foo")
  919. return deferred
  920. d.addCallback(retrFolder)
  921. def failOnSuccess(result):
  922. raise AssertionError("Downloading a folder should not succeed.")
  923. d.addCallback(failOnSuccess)
  924. def checkError(failure):
  925. failure.trap(ftp.CommandFailed)
  926. self.assertEqual(["550 foo: is a directory"], failure.value.args[0])
  927. current_errors = self.flushLoggedErrors()
  928. self.assertEqual(
  929. 0,
  930. len(current_errors),
  931. "No errors should be logged while downloading a folder.",
  932. )
  933. d.addErrback(checkError)
  934. return d
  935. def test_NLSTEmpty(self):
  936. """
  937. NLST with no argument returns the directory listing for the current
  938. working directory.
  939. """
  940. # Login
  941. d = self._anonymousLogin()
  942. # Touch a file in the current working directory
  943. self.dirPath.child("test.txt").touch()
  944. # Make a directory in the current working directory
  945. self.dirPath.child("foo").createDirectory()
  946. self._download("NLST ", chainDeferred=d)
  947. def checkDownload(download):
  948. filenames = download[:-2].split(b"\r\n")
  949. filenames.sort()
  950. self.assertEqual([b"foo", b"test.txt"], filenames)
  951. return d.addCallback(checkDownload)
  952. def test_NLSTNonexistent(self):
  953. """
  954. NLST on a non-existent file/directory returns nothing.
  955. """
  956. # Login
  957. d = self._anonymousLogin()
  958. self._download("NLST nonexistent.txt", chainDeferred=d)
  959. def checkDownload(download):
  960. self.assertEqual(b"", download)
  961. return d.addCallback(checkDownload)
  962. def test_NLSTUnicode(self):
  963. """
  964. NLST will receive Unicode filenames for IFTPShell.list, and will
  965. encode them using UTF-8.
  966. """
  967. return self._listTestHelper(
  968. "NLST",
  969. (
  970. "my resum\xe9",
  971. (0, 1, filepath.Permissions(0o777), 0, 0, "user", "group"),
  972. ),
  973. b"my resum\xc3\xa9\r\n",
  974. )
  975. def test_NLSTNonASCIIBytes(self):
  976. """
  977. NLST will just pass the non-Unicode data to lower level.
  978. """
  979. return self._listTestHelper(
  980. "NLST",
  981. (
  982. b"my resum\xc3\xa9",
  983. (0, 1, filepath.Permissions(0o777), 0, 0, "user", "group"),
  984. ),
  985. b"my resum\xc3\xa9\r\n",
  986. )
  987. def test_NLSTOnPathToFile(self):
  988. """
  989. NLST on an existent file returns only the path to that file.
  990. """
  991. # Login
  992. d = self._anonymousLogin()
  993. # Touch a file in the current working directory
  994. self.dirPath.child("test.txt").touch()
  995. self._download("NLST test.txt", chainDeferred=d)
  996. def checkDownload(download):
  997. filenames = download[:-2].split(b"\r\n")
  998. self.assertEqual([b"test.txt"], filenames)
  999. return d.addCallback(checkDownload)
  1000. class FTPServerPortDataConnectionTests(FTPServerPasvDataConnectionTests):
  1001. def setUp(self):
  1002. self.dataPorts = []
  1003. return FTPServerPasvDataConnectionTests.setUp(self)
  1004. def _makeDataConnection(self, ignored=None):
  1005. # Establish an active data connection (i.e. server connecting to
  1006. # client).
  1007. deferred = defer.Deferred()
  1008. class DataFactory(protocol.ServerFactory):
  1009. protocol = _BufferingProtocol
  1010. def buildProtocol(self, addr):
  1011. p = protocol.ServerFactory.buildProtocol(self, addr)
  1012. reactor.callLater(0, deferred.callback, p)
  1013. return p
  1014. dataPort = reactor.listenTCP(0, DataFactory(), interface="127.0.0.1")
  1015. self.dataPorts.append(dataPort)
  1016. cmd = "PORT " + ftp.encodeHostPort("127.0.0.1", dataPort.getHost().port)
  1017. self.client.queueStringCommand(cmd)
  1018. return deferred
  1019. def tearDown(self):
  1020. """
  1021. Tear down the connection.
  1022. @return: L{defer.DeferredList}
  1023. """
  1024. l = [defer.maybeDeferred(port.stopListening) for port in self.dataPorts]
  1025. d = defer.maybeDeferred(FTPServerPasvDataConnectionTests.tearDown, self)
  1026. l.append(d)
  1027. return defer.DeferredList(l, fireOnOneErrback=True)
  1028. def test_PORTCannotConnect(self):
  1029. """
  1030. Listen on a port, and immediately stop listening as a way to find a
  1031. port number that is definitely closed.
  1032. """
  1033. # Login
  1034. d = self._anonymousLogin()
  1035. def loggedIn(ignored):
  1036. port = reactor.listenTCP(0, protocol.Factory(), interface="127.0.0.1")
  1037. portNum = port.getHost().port
  1038. d = port.stopListening()
  1039. d.addCallback(lambda _: portNum)
  1040. return d
  1041. d.addCallback(loggedIn)
  1042. # Tell the server to connect to that port with a PORT command, and
  1043. # verify that it fails with the right error.
  1044. def gotPortNum(portNum):
  1045. return self.assertCommandFailed(
  1046. "PORT " + ftp.encodeHostPort("127.0.0.1", portNum),
  1047. ["425 Can't open data connection."],
  1048. )
  1049. return d.addCallback(gotPortNum)
  1050. def test_nlstGlobbing(self):
  1051. """
  1052. When Unix shell globbing is used with NLST only files matching the
  1053. pattern will be returned.
  1054. """
  1055. self.dirPath.child("test.txt").touch()
  1056. self.dirPath.child("ceva.txt").touch()
  1057. self.dirPath.child("no.match").touch()
  1058. d = self._anonymousLogin()
  1059. self._download("NLST *.txt", chainDeferred=d)
  1060. def checkDownload(download):
  1061. filenames = download[:-2].split(b"\r\n")
  1062. filenames.sort()
  1063. self.assertEqual([b"ceva.txt", b"test.txt"], filenames)
  1064. return d.addCallback(checkDownload)
  1065. class DTPFactoryTests(TestCase):
  1066. """
  1067. Tests for L{ftp.DTPFactory}.
  1068. """
  1069. def setUp(self):
  1070. """
  1071. Create a fake protocol interpreter and a L{ftp.DTPFactory} instance to
  1072. test.
  1073. """
  1074. self.reactor = task.Clock()
  1075. class ProtocolInterpreter:
  1076. dtpInstance = None
  1077. self.protocolInterpreter = ProtocolInterpreter()
  1078. self.factory = ftp.DTPFactory(self.protocolInterpreter, None, self.reactor)
  1079. def test_setTimeout(self):
  1080. """
  1081. L{ftp.DTPFactory.setTimeout} uses the reactor passed to its initializer
  1082. to set up a timed event to time out the DTP setup after the specified
  1083. number of seconds.
  1084. """
  1085. # Make sure the factory's deferred fails with the right exception, and
  1086. # make it so we can tell exactly when it fires.
  1087. finished = []
  1088. d = self.assertFailure(self.factory.deferred, ftp.PortConnectionError)
  1089. d.addCallback(finished.append)
  1090. self.factory.setTimeout(6)
  1091. # Advance the clock almost to the timeout
  1092. self.reactor.advance(5)
  1093. # Nothing should have happened yet.
  1094. self.assertFalse(finished)
  1095. # Advance it to the configured timeout.
  1096. self.reactor.advance(1)
  1097. # Now the Deferred should have failed with TimeoutError.
  1098. self.assertTrue(finished)
  1099. # There should also be no calls left in the reactor.
  1100. self.assertFalse(self.reactor.calls)
  1101. def test_buildProtocolOnce(self):
  1102. """
  1103. A L{ftp.DTPFactory} instance's C{buildProtocol} method can be used once
  1104. to create a L{ftp.DTP} instance.
  1105. """
  1106. protocol = self.factory.buildProtocol(None)
  1107. self.assertIsInstance(protocol, ftp.DTP)
  1108. # A subsequent call returns None.
  1109. self.assertIsNone(self.factory.buildProtocol(None))
  1110. def test_timeoutAfterConnection(self):
  1111. """
  1112. If a timeout has been set up using L{ftp.DTPFactory.setTimeout}, it is
  1113. cancelled by L{ftp.DTPFactory.buildProtocol}.
  1114. """
  1115. self.factory.setTimeout(10)
  1116. self.factory.buildProtocol(None)
  1117. # Make sure the call is no longer active.
  1118. self.assertFalse(self.reactor.calls)
  1119. def test_connectionAfterTimeout(self):
  1120. """
  1121. If L{ftp.DTPFactory.buildProtocol} is called after the timeout
  1122. specified by L{ftp.DTPFactory.setTimeout} has elapsed, L{None} is
  1123. returned.
  1124. """
  1125. # Handle the error so it doesn't get logged.
  1126. d = self.assertFailure(self.factory.deferred, ftp.PortConnectionError)
  1127. # Set up the timeout and then cause it to elapse so the Deferred does
  1128. # fail.
  1129. self.factory.setTimeout(10)
  1130. self.reactor.advance(10)
  1131. # Try to get a protocol - we should not be able to.
  1132. self.assertIsNone(self.factory.buildProtocol(None))
  1133. # Make sure the Deferred is doing the right thing.
  1134. return d
  1135. def test_timeoutAfterConnectionFailed(self):
  1136. """
  1137. L{ftp.DTPFactory.deferred} fails with L{PortConnectionError} when
  1138. L{ftp.DTPFactory.clientConnectionFailed} is called. If the timeout
  1139. specified with L{ftp.DTPFactory.setTimeout} expires after that, nothing
  1140. additional happens.
  1141. """
  1142. finished = []
  1143. d = self.assertFailure(self.factory.deferred, ftp.PortConnectionError)
  1144. d.addCallback(finished.append)
  1145. self.factory.setTimeout(10)
  1146. self.assertFalse(finished)
  1147. self.factory.clientConnectionFailed(None, None)
  1148. self.assertTrue(finished)
  1149. self.reactor.advance(10)
  1150. return d
  1151. def test_connectionFailedAfterTimeout(self):
  1152. """
  1153. If L{ftp.DTPFactory.clientConnectionFailed} is called after the timeout
  1154. specified by L{ftp.DTPFactory.setTimeout} has elapsed, nothing beyond
  1155. the normal timeout before happens.
  1156. """
  1157. # Handle the error so it doesn't get logged.
  1158. d = self.assertFailure(self.factory.deferred, ftp.PortConnectionError)
  1159. # Set up the timeout and then cause it to elapse so the Deferred does
  1160. # fail.
  1161. self.factory.setTimeout(10)
  1162. self.reactor.advance(10)
  1163. # Now fail the connection attempt. This should do nothing. In
  1164. # particular, it should not raise an exception.
  1165. self.factory.clientConnectionFailed(None, defer.TimeoutError("foo"))
  1166. # Give the Deferred to trial so it can make sure it did what we
  1167. # expected.
  1168. return d
  1169. class DTPTests(TestCase):
  1170. """
  1171. Tests for L{ftp.DTP}.
  1172. The DTP instances in these tests are generated using
  1173. DTPFactory.buildProtocol()
  1174. """
  1175. def setUp(self):
  1176. """
  1177. Create a fake protocol interpreter, a L{ftp.DTPFactory} instance,
  1178. and dummy transport to help with tests.
  1179. """
  1180. self.reactor = task.Clock()
  1181. class ProtocolInterpreter:
  1182. dtpInstance = None
  1183. self.protocolInterpreter = ProtocolInterpreter()
  1184. self.factory = ftp.DTPFactory(self.protocolInterpreter, None, self.reactor)
  1185. self.transport = proto_helpers.StringTransportWithDisconnection()
  1186. def test_sendLineNewline(self):
  1187. """
  1188. L{ftp.DTP.sendLine} writes the line passed to it plus a line delimiter
  1189. to its transport.
  1190. """
  1191. dtpInstance = self.factory.buildProtocol(None)
  1192. dtpInstance.makeConnection(self.transport)
  1193. lineContent = b"line content"
  1194. dtpInstance.sendLine(lineContent)
  1195. dataSent = self.transport.value()
  1196. self.assertEqual(lineContent + b"\r\n", dataSent)
  1197. # -- Client Tests -----------------------------------------------------------
  1198. class PrintLines(protocol.Protocol):
  1199. """
  1200. Helper class used by FTPFileListingTests.
  1201. """
  1202. def __init__(self, lines):
  1203. self._lines = lines
  1204. def connectionMade(self):
  1205. for line in self._lines:
  1206. self.transport.write(line.encode("latin-1") + b"\r\n")
  1207. self.transport.loseConnection()
  1208. class MyFTPFileListProtocol(ftp.FTPFileListProtocol):
  1209. def __init__(self):
  1210. self.other = []
  1211. ftp.FTPFileListProtocol.__init__(self)
  1212. def unknownLine(self, line):
  1213. self.other.append(line)
  1214. class FTPFileListingTests(TestCase):
  1215. def getFilesForLines(self, lines):
  1216. fileList = MyFTPFileListProtocol()
  1217. d = loopback.loopbackAsync(PrintLines(lines), fileList)
  1218. d.addCallback(lambda _: (fileList.files, fileList.other))
  1219. return d
  1220. def test_OneLine(self):
  1221. """
  1222. This example line taken from the docstring for FTPFileListProtocol
  1223. @return: L{Deferred} of command response
  1224. """
  1225. line = "-rw-r--r-- 1 root other 531 Jan 29 03:26 README"
  1226. def check(fileOther):
  1227. ((file,), other) = fileOther
  1228. self.assertFalse(other, f"unexpect unparsable lines: {repr(other)}")
  1229. self.assertTrue(file["filetype"] == "-", "misparsed fileitem")
  1230. self.assertTrue(file["perms"] == "rw-r--r--", "misparsed perms")
  1231. self.assertTrue(file["owner"] == "root", "misparsed fileitem")
  1232. self.assertTrue(file["group"] == "other", "misparsed fileitem")
  1233. self.assertTrue(file["size"] == 531, "misparsed fileitem")
  1234. self.assertTrue(file["date"] == "Jan 29 03:26", "misparsed fileitem")
  1235. self.assertTrue(file["filename"] == "README", "misparsed fileitem")
  1236. self.assertTrue(file["nlinks"] == 1, "misparsed nlinks")
  1237. self.assertFalse(file["linktarget"], "misparsed linktarget")
  1238. return self.getFilesForLines([line]).addCallback(check)
  1239. def test_VariantLines(self):
  1240. """
  1241. Variant lines.
  1242. """
  1243. line1 = "drw-r--r-- 2 root other 531 Jan 9 2003 A"
  1244. line2 = "lrw-r--r-- 1 root other 1 Jan 29 03:26 B -> A"
  1245. line3 = "woohoo! "
  1246. def check(result):
  1247. ((file1, file2), (other,)) = result
  1248. self.assertTrue(other == "woohoo! \r", "incorrect other line")
  1249. # file 1
  1250. self.assertTrue(file1["filetype"] == "d", "misparsed fileitem")
  1251. self.assertTrue(file1["perms"] == "rw-r--r--", "misparsed perms")
  1252. self.assertTrue(file1["owner"] == "root", "misparsed owner")
  1253. self.assertTrue(file1["group"] == "other", "misparsed group")
  1254. self.assertTrue(file1["size"] == 531, "misparsed size")
  1255. self.assertTrue(file1["date"] == "Jan 9 2003", "misparsed date")
  1256. self.assertTrue(file1["filename"] == "A", "misparsed filename")
  1257. self.assertTrue(file1["nlinks"] == 2, "misparsed nlinks")
  1258. self.assertFalse(file1["linktarget"], "misparsed linktarget")
  1259. # file 2
  1260. self.assertTrue(file2["filetype"] == "l", "misparsed fileitem")
  1261. self.assertTrue(file2["perms"] == "rw-r--r--", "misparsed perms")
  1262. self.assertTrue(file2["owner"] == "root", "misparsed owner")
  1263. self.assertTrue(file2["group"] == "other", "misparsed group")
  1264. self.assertTrue(file2["size"] == 1, "misparsed size")
  1265. self.assertTrue(file2["date"] == "Jan 29 03:26", "misparsed date")
  1266. self.assertTrue(file2["filename"] == "B", "misparsed filename")
  1267. self.assertTrue(file2["nlinks"] == 1, "misparsed nlinks")
  1268. self.assertTrue(file2["linktarget"] == "A", "misparsed linktarget")
  1269. return self.getFilesForLines([line1, line2, line3]).addCallback(check)
  1270. def test_UnknownLine(self):
  1271. """
  1272. Unknown lines.
  1273. """
  1274. def check(result):
  1275. (files, others) = result
  1276. self.assertFalse(files, "unexpected file entries")
  1277. self.assertTrue(
  1278. others == ["ABC\r", "not a file\r"],
  1279. "incorrect unparsable lines: %s" % repr(others),
  1280. )
  1281. return self.getFilesForLines(["ABC", "not a file"]).addCallback(check)
  1282. def test_filenameWithUnescapedSpace(self):
  1283. """
  1284. Will parse filenames and linktargets containing unescaped
  1285. space characters.
  1286. """
  1287. line1 = "drw-r--r-- 2 root other 531 Jan 9 2003 A B"
  1288. line2 = (
  1289. "lrw-r--r-- 1 root other 1 Jan 29 03:26 " "B A -> D C/A B"
  1290. )
  1291. def check(result):
  1292. (files, others) = result
  1293. self.assertEqual([], others, "unexpected others entries")
  1294. self.assertEqual("A B", files[0]["filename"], "misparsed filename")
  1295. self.assertEqual("B A", files[1]["filename"], "misparsed filename")
  1296. self.assertEqual("D C/A B", files[1]["linktarget"], "misparsed linktarget")
  1297. return self.getFilesForLines([line1, line2]).addCallback(check)
  1298. def test_filenameWithEscapedSpace(self):
  1299. """
  1300. Will parse filenames and linktargets containing escaped
  1301. space characters.
  1302. """
  1303. line1 = r"drw-r--r-- 2 root other 531 Jan 9 2003 A\ B"
  1304. line2 = (
  1305. "lrw-r--r-- 1 root other 1 Jan 29 03:26 " r"B A -> D\ C/A B"
  1306. )
  1307. def check(result):
  1308. (files, others) = result
  1309. self.assertEqual([], others, "unexpected others entries")
  1310. self.assertEqual("A B", files[0]["filename"], "misparsed filename")
  1311. self.assertEqual("B A", files[1]["filename"], "misparsed filename")
  1312. self.assertEqual("D C/A B", files[1]["linktarget"], "misparsed linktarget")
  1313. return self.getFilesForLines([line1, line2]).addCallback(check)
  1314. def test_Year(self):
  1315. """
  1316. This example derived from bug description in issue 514.
  1317. @return: L{Deferred} of command response
  1318. """
  1319. fileList = ftp.FTPFileListProtocol()
  1320. exampleLine = b"-rw-r--r-- 1 root other 531 Jan 29 2003 README\n"
  1321. class PrintLine(protocol.Protocol):
  1322. def connectionMade(self):
  1323. self.transport.write(exampleLine)
  1324. self.transport.loseConnection()
  1325. def check(ignored):
  1326. file = fileList.files[0]
  1327. self.assertTrue(file["size"] == 531, "misparsed fileitem")
  1328. self.assertTrue(file["date"] == "Jan 29 2003", "misparsed fileitem")
  1329. self.assertTrue(file["filename"] == "README", "misparsed fileitem")
  1330. d = loopback.loopbackAsync(PrintLine(), fileList)
  1331. return d.addCallback(check)
  1332. class FTPClientFailedRETRAndErrbacksUponDisconnectTests(TestCase):
  1333. """
  1334. FTP client fails and RETR fails and disconnects.
  1335. """
  1336. def test_FailedRETR(self):
  1337. """
  1338. RETR fails.
  1339. """
  1340. f = protocol.Factory()
  1341. f.noisy = 0
  1342. port = reactor.listenTCP(0, f, interface="127.0.0.1")
  1343. self.addCleanup(port.stopListening)
  1344. portNum = port.getHost().port
  1345. # This test data derived from a bug report by ranty on #twisted
  1346. responses = [
  1347. "220 ready, dude (vsFTPd 1.0.0: beat me, break me)",
  1348. # USER anonymous
  1349. "331 Please specify the password.",
  1350. # PASS twisted@twistedmatrix.com
  1351. "230 Login successful. Have fun.",
  1352. # TYPE I
  1353. "200 Binary it is, then.",
  1354. # PASV
  1355. "227 Entering Passive Mode (127,0,0,1,%d,%d)"
  1356. % (portNum >> 8, portNum & 0xFF),
  1357. # RETR /file/that/doesnt/exist
  1358. "550 Failed to open file.",
  1359. ]
  1360. f.buildProtocol = lambda addr: PrintLines(responses)
  1361. cc = protocol.ClientCreator(reactor, ftp.FTPClient, passive=1)
  1362. d = cc.connectTCP("127.0.0.1", portNum)
  1363. def gotClient(client):
  1364. p = protocol.Protocol()
  1365. return client.retrieveFile("/file/that/doesnt/exist", p)
  1366. d.addCallback(gotClient)
  1367. return self.assertFailure(d, ftp.CommandFailed)
  1368. def test_errbacksUponDisconnect(self):
  1369. """
  1370. Test the ftp command errbacks when a connection lost happens during
  1371. the operation.
  1372. """
  1373. ftpClient = ftp.FTPClient()
  1374. tr = proto_helpers.StringTransportWithDisconnection()
  1375. ftpClient.makeConnection(tr)
  1376. tr.protocol = ftpClient
  1377. d = ftpClient.list("some path", Dummy())
  1378. m = []
  1379. def _eb(failure):
  1380. m.append(failure)
  1381. return None
  1382. d.addErrback(_eb)
  1383. from twisted.internet.main import CONNECTION_LOST
  1384. ftpClient.connectionLost(failure.Failure(CONNECTION_LOST))
  1385. self.assertTrue(m, m)
  1386. return d
  1387. class FTPClientTests(TestCase):
  1388. """
  1389. Test advanced FTP client commands.
  1390. """
  1391. def setUp(self):
  1392. """
  1393. Create a FTP client and connect it to fake transport.
  1394. """
  1395. self.client = ftp.FTPClient()
  1396. self.transport = proto_helpers.StringTransportWithDisconnection()
  1397. self.client.makeConnection(self.transport)
  1398. self.transport.protocol = self.client
  1399. def tearDown(self):
  1400. """
  1401. Deliver disconnection notification to the client so that it can
  1402. perform any cleanup which may be required.
  1403. """
  1404. self.client.connectionLost(error.ConnectionLost())
  1405. def _testLogin(self):
  1406. """
  1407. Test the login part.
  1408. """
  1409. self.assertEqual(self.transport.value(), b"")
  1410. self.client.lineReceived(
  1411. b"331 Guest login ok, type your email address as password."
  1412. )
  1413. self.assertEqual(self.transport.value(), b"USER anonymous\r\n")
  1414. self.transport.clear()
  1415. self.client.lineReceived(b"230 Anonymous login ok, access restrictions apply.")
  1416. self.assertEqual(self.transport.value(), b"TYPE I\r\n")
  1417. self.transport.clear()
  1418. self.client.lineReceived(b"200 Type set to I.")
  1419. def test_sendLine(self):
  1420. """
  1421. Test encoding behaviour of sendLine
  1422. """
  1423. self.assertEqual(self.transport.value(), b"")
  1424. self.client.sendLine(None)
  1425. self.assertEqual(self.transport.value(), b"")
  1426. self.client.sendLine("")
  1427. self.assertEqual(self.transport.value(), b"\r\n")
  1428. self.transport.clear()
  1429. self.client.sendLine("\xe9")
  1430. self.assertEqual(self.transport.value(), b"\xe9\r\n")
  1431. def test_CDUP(self):
  1432. """
  1433. Test the CDUP command.
  1434. L{ftp.FTPClient.cdup} should return a Deferred which fires with a
  1435. sequence of one element which is the string the server sent
  1436. indicating that the command was executed successfully.
  1437. (XXX - This is a bad API)
  1438. """
  1439. def cbCdup(res):
  1440. self.assertEqual(res[0], "250 Requested File Action Completed OK")
  1441. self._testLogin()
  1442. d = self.client.cdup().addCallback(cbCdup)
  1443. self.assertEqual(self.transport.value(), b"CDUP\r\n")
  1444. self.transport.clear()
  1445. self.client.lineReceived(b"250 Requested File Action Completed OK")
  1446. return d
  1447. def test_failedCDUP(self):
  1448. """
  1449. Test L{ftp.FTPClient.cdup}'s handling of a failed CDUP command.
  1450. When the CDUP command fails, the returned Deferred should errback
  1451. with L{ftp.CommandFailed}.
  1452. """
  1453. self._testLogin()
  1454. d = self.client.cdup()
  1455. self.assertFailure(d, ftp.CommandFailed)
  1456. self.assertEqual(self.transport.value(), b"CDUP\r\n")
  1457. self.transport.clear()
  1458. self.client.lineReceived(b"550 ..: No such file or directory")
  1459. return d
  1460. def test_PWD(self):
  1461. """
  1462. Test the PWD command.
  1463. L{ftp.FTPClient.pwd} should return a Deferred which fires with a
  1464. sequence of one element which is a string representing the current
  1465. working directory on the server.
  1466. (XXX - This is a bad API)
  1467. """
  1468. def cbPwd(res):
  1469. self.assertEqual(ftp.parsePWDResponse(res[0]), "/bar/baz")
  1470. self._testLogin()
  1471. d = self.client.pwd().addCallback(cbPwd)
  1472. self.assertEqual(self.transport.value(), b"PWD\r\n")
  1473. self.client.lineReceived(b'257 "/bar/baz"')
  1474. return d
  1475. def test_failedPWD(self):
  1476. """
  1477. Test a failure in PWD command.
  1478. When the PWD command fails, the returned Deferred should errback
  1479. with L{ftp.CommandFailed}.
  1480. """
  1481. self._testLogin()
  1482. d = self.client.pwd()
  1483. self.assertFailure(d, ftp.CommandFailed)
  1484. self.assertEqual(self.transport.value(), b"PWD\r\n")
  1485. self.client.lineReceived(b"550 /bar/baz: No such file or directory")
  1486. return d
  1487. def test_CWD(self):
  1488. """
  1489. Test the CWD command.
  1490. L{ftp.FTPClient.cwd} should return a Deferred which fires with a
  1491. sequence of one element which is the string the server sent
  1492. indicating that the command was executed successfully.
  1493. (XXX - This is a bad API)
  1494. """
  1495. def cbCwd(res):
  1496. self.assertEqual(res[0], "250 Requested File Action Completed OK")
  1497. self._testLogin()
  1498. d = self.client.cwd("bar/foo").addCallback(cbCwd)
  1499. self.assertEqual(self.transport.value(), b"CWD bar/foo\r\n")
  1500. self.client.lineReceived(b"250 Requested File Action Completed OK")
  1501. return d
  1502. def test_failedCWD(self):
  1503. """
  1504. Test a failure in CWD command.
  1505. When the PWD command fails, the returned Deferred should errback
  1506. with L{ftp.CommandFailed}.
  1507. """
  1508. self._testLogin()
  1509. d = self.client.cwd("bar/foo")
  1510. self.assertFailure(d, ftp.CommandFailed)
  1511. self.assertEqual(self.transport.value(), b"CWD bar/foo\r\n")
  1512. self.client.lineReceived(b"550 bar/foo: No such file or directory")
  1513. return d
  1514. def test_passiveRETR(self):
  1515. """
  1516. Test the RETR command in passive mode: get a file and verify its
  1517. content.
  1518. L{ftp.FTPClient.retrieveFile} should return a Deferred which fires
  1519. with the protocol instance passed to it after the download has
  1520. completed.
  1521. (XXX - This API should be based on producers and consumers)
  1522. """
  1523. def cbRetr(res, proto):
  1524. self.assertEqual(proto.buffer, b"x" * 1000)
  1525. def cbConnect(host, port, factory):
  1526. self.assertEqual(host, "127.0.0.1")
  1527. self.assertEqual(port, 12345)
  1528. proto = factory.buildProtocol((host, port))
  1529. proto.makeConnection(proto_helpers.StringTransport())
  1530. self.client.lineReceived(
  1531. b"150 File status okay; about to open data connection."
  1532. )
  1533. proto.dataReceived(b"x" * 1000)
  1534. proto.connectionLost(failure.Failure(error.ConnectionDone("")))
  1535. self.client.connectFactory = cbConnect
  1536. self._testLogin()
  1537. proto = _BufferingProtocol()
  1538. d = self.client.retrieveFile("spam", proto)
  1539. d.addCallback(cbRetr, proto)
  1540. self.assertEqual(self.transport.value(), b"PASV\r\n")
  1541. self.transport.clear()
  1542. self.client.lineReceived(passivemode_msg(self.client))
  1543. self.assertEqual(self.transport.value(), b"RETR spam\r\n")
  1544. self.transport.clear()
  1545. self.client.lineReceived(b"226 Transfer Complete.")
  1546. return d
  1547. def test_RETR(self):
  1548. """
  1549. Test the RETR command in non-passive mode.
  1550. Like L{test_passiveRETR} but in the configuration where the server
  1551. establishes the data connection to the client, rather than the other
  1552. way around.
  1553. """
  1554. self.client.passive = False
  1555. def generatePort(portCmd):
  1556. portCmd.text = "PORT {}".format(ftp.encodeHostPort("127.0.0.1", 9876))
  1557. portCmd.protocol.makeConnection(proto_helpers.StringTransport())
  1558. portCmd.protocol.dataReceived(b"x" * 1000)
  1559. portCmd.protocol.connectionLost(failure.Failure(error.ConnectionDone("")))
  1560. def cbRetr(res, proto):
  1561. self.assertEqual(proto.buffer, b"x" * 1000)
  1562. self.client.generatePortCommand = generatePort
  1563. self._testLogin()
  1564. proto = _BufferingProtocol()
  1565. d = self.client.retrieveFile("spam", proto)
  1566. d.addCallback(cbRetr, proto)
  1567. self.assertEqual(
  1568. self.transport.value(),
  1569. ("PORT {}\r\n".format(ftp.encodeHostPort("127.0.0.1", 9876))).encode(
  1570. self.client._encoding
  1571. ),
  1572. )
  1573. self.transport.clear()
  1574. self.client.lineReceived(b"200 PORT OK")
  1575. self.assertEqual(self.transport.value(), b"RETR spam\r\n")
  1576. self.transport.clear()
  1577. self.client.lineReceived(b"226 Transfer Complete.")
  1578. return d
  1579. def test_failedRETR(self):
  1580. """
  1581. Try to RETR an unexisting file.
  1582. L{ftp.FTPClient.retrieveFile} should return a Deferred which
  1583. errbacks with L{ftp.CommandFailed} if the server indicates the file
  1584. cannot be transferred for some reason.
  1585. """
  1586. def cbConnect(host, port, factory):
  1587. self.assertEqual(host, "127.0.0.1")
  1588. self.assertEqual(port, 12345)
  1589. proto = factory.buildProtocol((host, port))
  1590. proto.makeConnection(proto_helpers.StringTransport())
  1591. self.client.lineReceived(
  1592. b"150 File status okay; about to open data connection."
  1593. )
  1594. proto.connectionLost(failure.Failure(error.ConnectionDone("")))
  1595. self.client.connectFactory = cbConnect
  1596. self._testLogin()
  1597. proto = _BufferingProtocol()
  1598. d = self.client.retrieveFile("spam", proto)
  1599. self.assertFailure(d, ftp.CommandFailed)
  1600. self.assertEqual(self.transport.value(), b"PASV\r\n")
  1601. self.transport.clear()
  1602. self.client.lineReceived(passivemode_msg(self.client))
  1603. self.assertEqual(self.transport.value(), b"RETR spam\r\n")
  1604. self.transport.clear()
  1605. self.client.lineReceived(b"550 spam: No such file or directory")
  1606. return d
  1607. def test_lostRETR(self):
  1608. """
  1609. Try a RETR, but disconnect during the transfer.
  1610. L{ftp.FTPClient.retrieveFile} should return a Deferred which
  1611. errbacks with L{ftp.ConnectionLost)
  1612. """
  1613. self.client.passive = False
  1614. l = []
  1615. def generatePort(portCmd):
  1616. portCmd.text = "PORT {}".format(ftp.encodeHostPort("127.0.0.1", 9876))
  1617. tr = proto_helpers.StringTransportWithDisconnection()
  1618. portCmd.protocol.makeConnection(tr)
  1619. tr.protocol = portCmd.protocol
  1620. portCmd.protocol.dataReceived(b"x" * 500)
  1621. l.append(tr)
  1622. self.client.generatePortCommand = generatePort
  1623. self._testLogin()
  1624. proto = _BufferingProtocol()
  1625. d = self.client.retrieveFile("spam", proto)
  1626. self.assertEqual(
  1627. self.transport.value(),
  1628. ("PORT {}\r\n".format(ftp.encodeHostPort("127.0.0.1", 9876))).encode(
  1629. self.client._encoding
  1630. ),
  1631. )
  1632. self.transport.clear()
  1633. self.client.lineReceived(b"200 PORT OK")
  1634. self.assertEqual(self.transport.value(), b"RETR spam\r\n")
  1635. self.assertTrue(l)
  1636. l[0].loseConnection()
  1637. self.transport.loseConnection()
  1638. self.assertFailure(d, ftp.ConnectionLost)
  1639. return d
  1640. def test_passiveSTOR(self):
  1641. """
  1642. Test the STOR command: send a file and verify its content.
  1643. L{ftp.FTPClient.storeFile} should return a two-tuple of Deferreds.
  1644. The first of which should fire with a protocol instance when the
  1645. data connection has been established and is responsible for sending
  1646. the contents of the file. The second of which should fire when the
  1647. upload has completed, the data connection has been closed, and the
  1648. server has acknowledged receipt of the file.
  1649. (XXX - storeFile should take a producer as an argument, instead, and
  1650. only return a Deferred which fires when the upload has succeeded or
  1651. failed).
  1652. """
  1653. tr = proto_helpers.StringTransport()
  1654. def cbStore(sender):
  1655. self.client.lineReceived(
  1656. b"150 File status okay; about to open data connection."
  1657. )
  1658. sender.transport.write(b"x" * 1000)
  1659. sender.finish()
  1660. sender.connectionLost(failure.Failure(error.ConnectionDone("")))
  1661. def cbFinish(ign):
  1662. self.assertEqual(tr.value(), b"x" * 1000)
  1663. def cbConnect(host, port, factory):
  1664. self.assertEqual(host, "127.0.0.1")
  1665. self.assertEqual(port, 12345)
  1666. proto = factory.buildProtocol((host, port))
  1667. proto.makeConnection(tr)
  1668. self.client.connectFactory = cbConnect
  1669. self._testLogin()
  1670. d1, d2 = self.client.storeFile("spam")
  1671. d1.addCallback(cbStore)
  1672. d2.addCallback(cbFinish)
  1673. self.assertEqual(self.transport.value(), b"PASV\r\n")
  1674. self.transport.clear()
  1675. self.client.lineReceived(passivemode_msg(self.client))
  1676. self.assertEqual(self.transport.value(), b"STOR spam\r\n")
  1677. self.transport.clear()
  1678. self.client.lineReceived(b"226 Transfer Complete.")
  1679. return defer.gatherResults([d1, d2])
  1680. def test_failedSTOR(self):
  1681. """
  1682. Test a failure in the STOR command.
  1683. If the server does not acknowledge successful receipt of the
  1684. uploaded file, the second Deferred returned by
  1685. L{ftp.FTPClient.storeFile} should errback with L{ftp.CommandFailed}.
  1686. """
  1687. tr = proto_helpers.StringTransport()
  1688. def cbStore(sender):
  1689. self.client.lineReceived(
  1690. b"150 File status okay; about to open data connection."
  1691. )
  1692. sender.transport.write(b"x" * 1000)
  1693. sender.finish()
  1694. sender.connectionLost(failure.Failure(error.ConnectionDone("")))
  1695. def cbConnect(host, port, factory):
  1696. self.assertEqual(host, "127.0.0.1")
  1697. self.assertEqual(port, 12345)
  1698. proto = factory.buildProtocol((host, port))
  1699. proto.makeConnection(tr)
  1700. self.client.connectFactory = cbConnect
  1701. self._testLogin()
  1702. d1, d2 = self.client.storeFile("spam")
  1703. d1.addCallback(cbStore)
  1704. self.assertFailure(d2, ftp.CommandFailed)
  1705. self.assertEqual(self.transport.value(), b"PASV\r\n")
  1706. self.transport.clear()
  1707. self.client.lineReceived(passivemode_msg(self.client))
  1708. self.assertEqual(self.transport.value(), b"STOR spam\r\n")
  1709. self.transport.clear()
  1710. self.client.lineReceived(b"426 Transfer aborted. Data connection closed.")
  1711. return defer.gatherResults([d1, d2])
  1712. def test_STOR(self):
  1713. """
  1714. Test the STOR command in non-passive mode.
  1715. Like L{test_passiveSTOR} but in the configuration where the server
  1716. establishes the data connection to the client, rather than the other
  1717. way around.
  1718. """
  1719. tr = proto_helpers.StringTransport()
  1720. self.client.passive = False
  1721. def generatePort(portCmd):
  1722. portCmd.text = "PORT " + ftp.encodeHostPort("127.0.0.1", 9876)
  1723. portCmd.protocol.makeConnection(tr)
  1724. def cbStore(sender):
  1725. self.assertEqual(
  1726. self.transport.value(),
  1727. ("PORT {}\r\n".format(ftp.encodeHostPort("127.0.0.1", 9876))).encode(
  1728. self.client._encoding
  1729. ),
  1730. )
  1731. self.transport.clear()
  1732. self.client.lineReceived(b"200 PORT OK")
  1733. self.assertEqual(self.transport.value(), b"STOR spam\r\n")
  1734. self.transport.clear()
  1735. self.client.lineReceived(
  1736. b"150 File status okay; about to open data connection."
  1737. )
  1738. sender.transport.write(b"x" * 1000)
  1739. sender.finish()
  1740. sender.connectionLost(failure.Failure(error.ConnectionDone("")))
  1741. self.client.lineReceived(b"226 Transfer Complete.")
  1742. def cbFinish(ign):
  1743. self.assertEqual(tr.value(), b"x" * 1000)
  1744. self.client.generatePortCommand = generatePort
  1745. self._testLogin()
  1746. d1, d2 = self.client.storeFile("spam")
  1747. d1.addCallback(cbStore)
  1748. d2.addCallback(cbFinish)
  1749. return defer.gatherResults([d1, d2])
  1750. def test_passiveLIST(self):
  1751. """
  1752. Test the LIST command.
  1753. L{ftp.FTPClient.list} should return a Deferred which fires with a
  1754. protocol instance which was passed to list after the command has
  1755. succeeded.
  1756. (XXX - This is a very unfortunate API; if my understanding is
  1757. correct, the results are always at least line-oriented, so allowing
  1758. a per-line parser function to be specified would make this simpler,
  1759. but a default implementation should really be provided which knows
  1760. how to deal with all the formats used in real servers, so
  1761. application developers never have to care about this insanity. It
  1762. would also be nice to either get back a Deferred of a list of
  1763. filenames or to be able to consume the files as they are received
  1764. (which the current API does allow, but in a somewhat inconvenient
  1765. fashion) -exarkun)
  1766. """
  1767. def cbList(res, fileList):
  1768. fls = [f["filename"] for f in fileList.files]
  1769. expected = ["foo", "bar", "baz"]
  1770. expected.sort()
  1771. fls.sort()
  1772. self.assertEqual(fls, expected)
  1773. def cbConnect(host, port, factory):
  1774. self.assertEqual(host, "127.0.0.1")
  1775. self.assertEqual(port, 12345)
  1776. proto = factory.buildProtocol((host, port))
  1777. proto.makeConnection(proto_helpers.StringTransport())
  1778. self.client.lineReceived(
  1779. b"150 File status okay; about to open data connection."
  1780. )
  1781. sending = [
  1782. b"-rw-r--r-- 0 spam egg 100 Oct 10 2006 foo\r\n",
  1783. b"-rw-r--r-- 3 spam egg 100 Oct 10 2006 bar\r\n",
  1784. b"-rw-r--r-- 4 spam egg 100 Oct 10 2006 baz\r\n",
  1785. ]
  1786. for i in sending:
  1787. proto.dataReceived(i)
  1788. proto.connectionLost(failure.Failure(error.ConnectionDone("")))
  1789. self.client.connectFactory = cbConnect
  1790. self._testLogin()
  1791. fileList = ftp.FTPFileListProtocol()
  1792. d = self.client.list("foo/bar", fileList).addCallback(cbList, fileList)
  1793. self.assertEqual(self.transport.value(), b"PASV\r\n")
  1794. self.transport.clear()
  1795. self.client.lineReceived(passivemode_msg(self.client))
  1796. self.assertEqual(self.transport.value(), b"LIST foo/bar\r\n")
  1797. self.client.lineReceived(b"226 Transfer Complete.")
  1798. return d
  1799. def test_LIST(self):
  1800. """
  1801. Test the LIST command in non-passive mode.
  1802. Like L{test_passiveLIST} but in the configuration where the server
  1803. establishes the data connection to the client, rather than the other
  1804. way around.
  1805. """
  1806. self.client.passive = False
  1807. def generatePort(portCmd):
  1808. portCmd.text = "PORT {}".format(ftp.encodeHostPort("127.0.0.1", 9876))
  1809. portCmd.protocol.makeConnection(proto_helpers.StringTransport())
  1810. self.client.lineReceived(
  1811. b"150 File status okay; about to open data connection."
  1812. )
  1813. sending = [
  1814. b"-rw-r--r-- 0 spam egg 100 Oct 10 2006 foo\r\n",
  1815. b"-rw-r--r-- 3 spam egg 100 Oct 10 2006 bar\r\n",
  1816. b"-rw-r--r-- 4 spam egg 100 Oct 10 2006 baz\r\n",
  1817. ]
  1818. for i in sending:
  1819. portCmd.protocol.dataReceived(i)
  1820. portCmd.protocol.connectionLost(failure.Failure(error.ConnectionDone("")))
  1821. def cbList(res, fileList):
  1822. fls = [f["filename"] for f in fileList.files]
  1823. expected = ["foo", "bar", "baz"]
  1824. expected.sort()
  1825. fls.sort()
  1826. self.assertEqual(fls, expected)
  1827. self.client.generatePortCommand = generatePort
  1828. self._testLogin()
  1829. fileList = ftp.FTPFileListProtocol()
  1830. d = self.client.list("foo/bar", fileList).addCallback(cbList, fileList)
  1831. self.assertEqual(
  1832. self.transport.value(),
  1833. ("PORT {}\r\n".format(ftp.encodeHostPort("127.0.0.1", 9876))).encode(
  1834. self.client._encoding
  1835. ),
  1836. )
  1837. self.transport.clear()
  1838. self.client.lineReceived(b"200 PORT OK")
  1839. self.assertEqual(self.transport.value(), b"LIST foo/bar\r\n")
  1840. self.transport.clear()
  1841. self.client.lineReceived(b"226 Transfer Complete.")
  1842. return d
  1843. def test_failedLIST(self):
  1844. """
  1845. Test a failure in LIST command.
  1846. L{ftp.FTPClient.list} should return a Deferred which fails with
  1847. L{ftp.CommandFailed} if the server indicates the indicated path is
  1848. invalid for some reason.
  1849. """
  1850. def cbConnect(host, port, factory):
  1851. self.assertEqual(host, "127.0.0.1")
  1852. self.assertEqual(port, 12345)
  1853. proto = factory.buildProtocol((host, port))
  1854. proto.makeConnection(proto_helpers.StringTransport())
  1855. self.client.lineReceived(
  1856. b"150 File status okay; about to open data connection."
  1857. )
  1858. proto.connectionLost(failure.Failure(error.ConnectionDone("")))
  1859. self.client.connectFactory = cbConnect
  1860. self._testLogin()
  1861. fileList = ftp.FTPFileListProtocol()
  1862. d = self.client.list("foo/bar", fileList)
  1863. self.assertFailure(d, ftp.CommandFailed)
  1864. self.assertEqual(self.transport.value(), b"PASV\r\n")
  1865. self.transport.clear()
  1866. self.client.lineReceived(passivemode_msg(self.client))
  1867. self.assertEqual(self.transport.value(), b"LIST foo/bar\r\n")
  1868. self.client.lineReceived(b"550 foo/bar: No such file or directory")
  1869. return d
  1870. def test_NLST(self):
  1871. """
  1872. Test the NLST command in non-passive mode.
  1873. L{ftp.FTPClient.nlst} should return a Deferred which fires with a
  1874. list of filenames when the list command has completed.
  1875. """
  1876. self.client.passive = False
  1877. def generatePort(portCmd):
  1878. portCmd.text = "PORT {}".format(ftp.encodeHostPort("127.0.0.1", 9876))
  1879. portCmd.protocol.makeConnection(proto_helpers.StringTransport())
  1880. self.client.lineReceived(
  1881. b"150 File status okay; about to open data connection."
  1882. )
  1883. portCmd.protocol.dataReceived(b"foo\r\n")
  1884. portCmd.protocol.dataReceived(b"bar\r\n")
  1885. portCmd.protocol.dataReceived(b"baz\r\n")
  1886. portCmd.protocol.connectionLost(failure.Failure(error.ConnectionDone("")))
  1887. def cbList(res, proto):
  1888. fls = proto.buffer.decode(self.client._encoding).splitlines()
  1889. expected = ["foo", "bar", "baz"]
  1890. expected.sort()
  1891. fls.sort()
  1892. self.assertEqual(fls, expected)
  1893. self.client.generatePortCommand = generatePort
  1894. self._testLogin()
  1895. lstproto = _BufferingProtocol()
  1896. d = self.client.nlst("foo/bar", lstproto).addCallback(cbList, lstproto)
  1897. self.assertEqual(
  1898. self.transport.value(),
  1899. ("PORT {}\r\n".format(ftp.encodeHostPort("127.0.0.1", 9876))).encode(
  1900. self.client._encoding
  1901. ),
  1902. )
  1903. self.transport.clear()
  1904. self.client.lineReceived(b"200 PORT OK")
  1905. self.assertEqual(self.transport.value(), b"NLST foo/bar\r\n")
  1906. self.client.lineReceived(b"226 Transfer Complete.")
  1907. return d
  1908. def test_passiveNLST(self):
  1909. """
  1910. Test the NLST command.
  1911. Like L{test_passiveNLST} but in the configuration where the server
  1912. establishes the data connection to the client, rather than the other
  1913. way around.
  1914. """
  1915. def cbList(res, proto):
  1916. fls = proto.buffer.splitlines()
  1917. expected = [b"foo", b"bar", b"baz"]
  1918. expected.sort()
  1919. fls.sort()
  1920. self.assertEqual(fls, expected)
  1921. def cbConnect(host, port, factory):
  1922. self.assertEqual(host, "127.0.0.1")
  1923. self.assertEqual(port, 12345)
  1924. proto = factory.buildProtocol((host, port))
  1925. proto.makeConnection(proto_helpers.StringTransport())
  1926. self.client.lineReceived(
  1927. b"150 File status okay; about to open data connection."
  1928. )
  1929. proto.dataReceived(b"foo\r\n")
  1930. proto.dataReceived(b"bar\r\n")
  1931. proto.dataReceived(b"baz\r\n")
  1932. proto.connectionLost(failure.Failure(error.ConnectionDone("")))
  1933. self.client.connectFactory = cbConnect
  1934. self._testLogin()
  1935. lstproto = _BufferingProtocol()
  1936. d = self.client.nlst("foo/bar", lstproto).addCallback(cbList, lstproto)
  1937. self.assertEqual(self.transport.value(), b"PASV\r\n")
  1938. self.transport.clear()
  1939. self.client.lineReceived(passivemode_msg(self.client))
  1940. self.assertEqual(self.transport.value(), b"NLST foo/bar\r\n")
  1941. self.client.lineReceived(b"226 Transfer Complete.")
  1942. return d
  1943. def test_failedNLST(self):
  1944. """
  1945. Test a failure in NLST command.
  1946. L{ftp.FTPClient.nlst} should return a Deferred which fails with
  1947. L{ftp.CommandFailed} if the server indicates the indicated path is
  1948. invalid for some reason.
  1949. """
  1950. tr = proto_helpers.StringTransport()
  1951. def cbConnect(host, port, factory):
  1952. self.assertEqual(host, "127.0.0.1")
  1953. self.assertEqual(port, 12345)
  1954. proto = factory.buildProtocol((host, port))
  1955. proto.makeConnection(tr)
  1956. self.client.lineReceived(
  1957. b"150 File status okay; about to open data connection."
  1958. )
  1959. proto.connectionLost(failure.Failure(error.ConnectionDone("")))
  1960. self.client.connectFactory = cbConnect
  1961. self._testLogin()
  1962. lstproto = _BufferingProtocol()
  1963. d = self.client.nlst("foo/bar", lstproto)
  1964. self.assertFailure(d, ftp.CommandFailed)
  1965. self.assertEqual(self.transport.value(), b"PASV\r\n")
  1966. self.transport.clear()
  1967. self.client.lineReceived(passivemode_msg(self.client))
  1968. self.assertEqual(self.transport.value(), b"NLST foo/bar\r\n")
  1969. self.client.lineReceived(b"550 foo/bar: No such file or directory")
  1970. return d
  1971. def test_renameFromTo(self):
  1972. """
  1973. L{ftp.FTPClient.rename} issues I{RNTO} and I{RNFR} commands and returns
  1974. a L{Deferred} which fires when a file has successfully been renamed.
  1975. """
  1976. self._testLogin()
  1977. d = self.client.rename("/spam", "/ham")
  1978. self.assertEqual(self.transport.value(), b"RNFR /spam\r\n")
  1979. self.transport.clear()
  1980. fromResponse = "350 Requested file action pending further information.\r\n"
  1981. self.client.lineReceived(fromResponse.encode(self.client._encoding))
  1982. self.assertEqual(self.transport.value(), b"RNTO /ham\r\n")
  1983. toResponse = "250 Requested File Action Completed OK"
  1984. self.client.lineReceived(toResponse.encode(self.client._encoding))
  1985. d.addCallback(self.assertEqual, ([fromResponse], [toResponse]))
  1986. return d
  1987. def test_renameFromToEscapesPaths(self):
  1988. """
  1989. L{ftp.FTPClient.rename} issues I{RNTO} and I{RNFR} commands with paths
  1990. escaped according to U{http://cr.yp.to/ftp/filesystem.html}.
  1991. """
  1992. self._testLogin()
  1993. fromFile = "/foo/ba\nr/baz"
  1994. toFile = "/qu\nux"
  1995. self.client.rename(fromFile, toFile)
  1996. self.client.lineReceived(b"350 ")
  1997. self.client.lineReceived(b"250 ")
  1998. self.assertEqual(
  1999. self.transport.value(), b"RNFR /foo/ba\x00r/baz\r\n" b"RNTO /qu\x00ux\r\n"
  2000. )
  2001. def test_renameFromToFailingOnFirstError(self):
  2002. """
  2003. The L{Deferred} returned by L{ftp.FTPClient.rename} is errbacked with
  2004. L{CommandFailed} if the I{RNFR} command receives an error response code
  2005. (for example, because the file does not exist).
  2006. """
  2007. self._testLogin()
  2008. d = self.client.rename("/spam", "/ham")
  2009. self.assertEqual(self.transport.value(), b"RNFR /spam\r\n")
  2010. self.transport.clear()
  2011. self.client.lineReceived(b"550 Requested file unavailable.\r\n")
  2012. # The RNTO should not execute since the RNFR failed.
  2013. self.assertEqual(self.transport.value(), b"")
  2014. return self.assertFailure(d, ftp.CommandFailed)
  2015. def test_renameFromToFailingOnRenameTo(self):
  2016. """
  2017. The L{Deferred} returned by L{ftp.FTPClient.rename} is errbacked with
  2018. L{CommandFailed} if the I{RNTO} command receives an error response code
  2019. (for example, because the destination directory does not exist).
  2020. """
  2021. self._testLogin()
  2022. d = self.client.rename("/spam", "/ham")
  2023. self.assertEqual(self.transport.value(), b"RNFR /spam\r\n")
  2024. self.transport.clear()
  2025. self.client.lineReceived(
  2026. b"350 Requested file action pending further information.\r\n"
  2027. )
  2028. self.assertEqual(self.transport.value(), b"RNTO /ham\r\n")
  2029. self.client.lineReceived(b"550 Requested file unavailable.\r\n")
  2030. return self.assertFailure(d, ftp.CommandFailed)
  2031. def test_makeDirectory(self):
  2032. """
  2033. L{ftp.FTPClient.makeDirectory} issues a I{MKD} command and returns a
  2034. L{Deferred} which is called back with the server's response if the
  2035. directory is created.
  2036. """
  2037. self._testLogin()
  2038. d = self.client.makeDirectory("/spam")
  2039. self.assertEqual(self.transport.value(), b"MKD /spam\r\n")
  2040. self.client.lineReceived(b'257 "/spam" created.')
  2041. return d.addCallback(self.assertEqual, ['257 "/spam" created.'])
  2042. def test_makeDirectoryPathEscape(self):
  2043. """
  2044. L{ftp.FTPClient.makeDirectory} escapes the path name it sends according
  2045. to U{http://cr.yp.to/ftp/filesystem.html}.
  2046. """
  2047. self._testLogin()
  2048. d = self.client.makeDirectory("/sp\nam")
  2049. self.assertEqual(self.transport.value(), b"MKD /sp\x00am\r\n")
  2050. # This is necessary to make the Deferred fire. The Deferred needs
  2051. # to fire so that tearDown doesn't cause it to errback and fail this
  2052. # or (more likely) a later test.
  2053. self.client.lineReceived(b"257 win")
  2054. return d
  2055. def test_failedMakeDirectory(self):
  2056. """
  2057. L{ftp.FTPClient.makeDirectory} returns a L{Deferred} which is errbacked
  2058. with L{CommandFailed} if the server returns an error response code.
  2059. """
  2060. self._testLogin()
  2061. d = self.client.makeDirectory("/spam")
  2062. self.assertEqual(self.transport.value(), b"MKD /spam\r\n")
  2063. self.client.lineReceived(b"550 PERMISSION DENIED")
  2064. return self.assertFailure(d, ftp.CommandFailed)
  2065. def test_getDirectory(self):
  2066. """
  2067. Test the getDirectory method.
  2068. L{ftp.FTPClient.getDirectory} should return a Deferred which fires with
  2069. the current directory on the server. It wraps PWD command.
  2070. """
  2071. def cbGet(res):
  2072. self.assertEqual(res, "/bar/baz")
  2073. self._testLogin()
  2074. d = self.client.getDirectory().addCallback(cbGet)
  2075. self.assertEqual(self.transport.value(), b"PWD\r\n")
  2076. self.client.lineReceived(b'257 "/bar/baz"')
  2077. return d
  2078. def test_failedGetDirectory(self):
  2079. """
  2080. Test a failure in getDirectory method.
  2081. The behaviour should be the same as PWD.
  2082. """
  2083. self._testLogin()
  2084. d = self.client.getDirectory()
  2085. self.assertFailure(d, ftp.CommandFailed)
  2086. self.assertEqual(self.transport.value(), b"PWD\r\n")
  2087. self.client.lineReceived(b"550 /bar/baz: No such file or directory")
  2088. return d
  2089. def test_anotherFailedGetDirectory(self):
  2090. """
  2091. Test a different failure in getDirectory method.
  2092. The response should be quoted to be parsed, so it returns an error
  2093. otherwise.
  2094. """
  2095. self._testLogin()
  2096. d = self.client.getDirectory()
  2097. self.assertFailure(d, ftp.CommandFailed)
  2098. self.assertEqual(self.transport.value(), b"PWD\r\n")
  2099. self.client.lineReceived(b"257 /bar/baz")
  2100. return d
  2101. def test_removeFile(self):
  2102. """
  2103. L{ftp.FTPClient.removeFile} sends a I{DELE} command to the server for
  2104. the indicated file and returns a Deferred which fires after the server
  2105. sends a 250 response code.
  2106. """
  2107. self._testLogin()
  2108. d = self.client.removeFile("/tmp/test")
  2109. self.assertEqual(self.transport.value(), b"DELE /tmp/test\r\n")
  2110. response = "250 Requested file action okay, completed."
  2111. self.client.lineReceived(response.encode(self.client._encoding))
  2112. return d.addCallback(self.assertEqual, [response])
  2113. def test_failedRemoveFile(self):
  2114. """
  2115. If the server returns a response code other than 250 in response to a
  2116. I{DELE} sent by L{ftp.FTPClient.removeFile}, the L{Deferred} returned
  2117. by C{removeFile} is errbacked with a L{Failure} wrapping a
  2118. L{CommandFailed}.
  2119. """
  2120. self._testLogin()
  2121. d = self.client.removeFile("/tmp/test")
  2122. self.assertEqual(self.transport.value(), b"DELE /tmp/test\r\n")
  2123. response = "501 Syntax error in parameters or arguments."
  2124. self.client.lineReceived(response.encode(self.client._encoding))
  2125. d = self.assertFailure(d, ftp.CommandFailed)
  2126. d.addCallback(lambda exc: self.assertEqual(exc.args, ([response],)))
  2127. return d
  2128. def test_unparsableRemoveFileResponse(self):
  2129. """
  2130. If the server returns a response line which cannot be parsed, the
  2131. L{Deferred} returned by L{ftp.FTPClient.removeFile} is errbacked with a
  2132. L{BadResponse} containing the response.
  2133. """
  2134. self._testLogin()
  2135. d = self.client.removeFile("/tmp/test")
  2136. response = "765 blah blah blah"
  2137. self.client.lineReceived(response.encode(self.client._encoding))
  2138. d = self.assertFailure(d, ftp.BadResponse)
  2139. d.addCallback(lambda exc: self.assertEqual(exc.args, ([response],)))
  2140. return d
  2141. def test_multilineRemoveFileResponse(self):
  2142. """
  2143. If the server returns multiple response lines, the L{Deferred} returned
  2144. by L{ftp.FTPClient.removeFile} is still fired with a true value if the
  2145. ultimate response code is 250.
  2146. """
  2147. self._testLogin()
  2148. d = self.client.removeFile("/tmp/test")
  2149. self.client.lineReceived(b"250-perhaps a progress report")
  2150. self.client.lineReceived(b"250 okay")
  2151. return d.addCallback(self.assertTrue)
  2152. def test_removeDirectory(self):
  2153. """
  2154. L{ftp.FTPClient.removeDirectory} sends a I{RMD} command to the server
  2155. for the indicated directory and returns a Deferred which fires after
  2156. the server sends a 250 response code.
  2157. """
  2158. self._testLogin()
  2159. d = self.client.removeDirectory("/tmp/test")
  2160. self.assertEqual(self.transport.value(), b"RMD /tmp/test\r\n")
  2161. response = "250 Requested file action okay, completed."
  2162. self.client.lineReceived(response.encode(self.client._encoding))
  2163. return d.addCallback(self.assertEqual, [response])
  2164. def test_failedRemoveDirectory(self):
  2165. """
  2166. If the server returns a response code other than 250 in response to a
  2167. I{RMD} sent by L{ftp.FTPClient.removeDirectory}, the L{Deferred}
  2168. returned by C{removeDirectory} is errbacked with a L{Failure} wrapping
  2169. a L{CommandFailed}.
  2170. """
  2171. self._testLogin()
  2172. d = self.client.removeDirectory("/tmp/test")
  2173. self.assertEqual(self.transport.value(), b"RMD /tmp/test\r\n")
  2174. response = "501 Syntax error in parameters or arguments."
  2175. self.client.lineReceived(response.encode(self.client._encoding))
  2176. d = self.assertFailure(d, ftp.CommandFailed)
  2177. d.addCallback(lambda exc: self.assertEqual(exc.args, ([response],)))
  2178. return d
  2179. def test_unparsableRemoveDirectoryResponse(self):
  2180. """
  2181. If the server returns a response line which cannot be parsed, the
  2182. L{Deferred} returned by L{ftp.FTPClient.removeDirectory} is errbacked
  2183. with a L{BadResponse} containing the response.
  2184. """
  2185. self._testLogin()
  2186. d = self.client.removeDirectory("/tmp/test")
  2187. response = "765 blah blah blah"
  2188. self.client.lineReceived(response.encode(self.client._encoding))
  2189. d = self.assertFailure(d, ftp.BadResponse)
  2190. d.addCallback(lambda exc: self.assertEqual(exc.args, ([response],)))
  2191. return d
  2192. def test_multilineRemoveDirectoryResponse(self):
  2193. """
  2194. If the server returns multiple response lines, the L{Deferred} returned
  2195. by L{ftp.FTPClient.removeDirectory} is still fired with a true value
  2196. if the ultimate response code is 250.
  2197. """
  2198. self._testLogin()
  2199. d = self.client.removeDirectory("/tmp/test")
  2200. self.client.lineReceived(b"250-perhaps a progress report")
  2201. self.client.lineReceived(b"250 okay")
  2202. return d.addCallback(self.assertTrue)
  2203. class FTPClientBasicTests(TestCase):
  2204. """
  2205. FTP client
  2206. """
  2207. def test_greeting(self):
  2208. """
  2209. The first response is captured as a greeting.
  2210. """
  2211. ftpClient = ftp.FTPClientBasic()
  2212. ftpClient.lineReceived(b"220 Imaginary FTP.")
  2213. self.assertEqual(["220 Imaginary FTP."], ftpClient.greeting)
  2214. def test_responseWithNoMessage(self):
  2215. """
  2216. Responses with no message are still valid, i.e. three digits
  2217. followed by a space is complete response.
  2218. """
  2219. ftpClient = ftp.FTPClientBasic()
  2220. ftpClient.lineReceived(b"220 ")
  2221. self.assertEqual(["220 "], ftpClient.greeting)
  2222. def test_MultilineResponse(self):
  2223. """
  2224. Multiline response
  2225. """
  2226. ftpClient = ftp.FTPClientBasic()
  2227. ftpClient.transport = proto_helpers.StringTransport()
  2228. ftpClient.lineReceived(b"220 Imaginary FTP.")
  2229. # Queue (and send) a dummy command, and set up a callback
  2230. # to capture the result
  2231. deferred = ftpClient.queueStringCommand("BLAH")
  2232. result = []
  2233. deferred.addCallback(result.append)
  2234. deferred.addErrback(self.fail)
  2235. # Send the first line of a multiline response.
  2236. ftpClient.lineReceived(b"210-First line.")
  2237. self.assertEqual([], result)
  2238. # Send a second line, again prefixed with "nnn-".
  2239. ftpClient.lineReceived(b"123-Second line.")
  2240. self.assertEqual([], result)
  2241. # Send a plain line of text, no prefix.
  2242. ftpClient.lineReceived(b"Just some text.")
  2243. self.assertEqual([], result)
  2244. # Now send a short (less than 4 chars) line.
  2245. ftpClient.lineReceived(b"Hi")
  2246. self.assertEqual([], result)
  2247. # Now send an empty line.
  2248. ftpClient.lineReceived(b"")
  2249. self.assertEqual([], result)
  2250. # And a line with 3 digits in it, and nothing else.
  2251. ftpClient.lineReceived(b"321")
  2252. self.assertEqual([], result)
  2253. # Now finish it.
  2254. ftpClient.lineReceived(b"210 Done.")
  2255. self.assertEqual(
  2256. [
  2257. "210-First line.",
  2258. "123-Second line.",
  2259. "Just some text.",
  2260. "Hi",
  2261. "",
  2262. "321",
  2263. "210 Done.",
  2264. ],
  2265. result[0],
  2266. )
  2267. def test_noPasswordGiven(self):
  2268. """
  2269. Passing None as the password avoids sending the PASS command.
  2270. """
  2271. # Create a client, and give it a greeting.
  2272. ftpClient = ftp.FTPClientBasic()
  2273. ftpClient.transport = proto_helpers.StringTransport()
  2274. ftpClient.lineReceived(b"220 Welcome to Imaginary FTP.")
  2275. # Queue a login with no password
  2276. ftpClient.queueLogin("bob", None)
  2277. self.assertEqual(b"USER bob\r\n", ftpClient.transport.value())
  2278. # Clear the test buffer, acknowledge the USER command.
  2279. ftpClient.transport.clear()
  2280. ftpClient.lineReceived(b"200 Hello bob.")
  2281. # The client shouldn't have sent anything more (i.e. it shouldn't have
  2282. # sent a PASS command).
  2283. self.assertEqual(b"", ftpClient.transport.value())
  2284. def test_noPasswordNeeded(self):
  2285. """
  2286. Receiving a 230 response to USER prevents PASS from being sent.
  2287. """
  2288. # Create a client, and give it a greeting.
  2289. ftpClient = ftp.FTPClientBasic()
  2290. ftpClient.transport = proto_helpers.StringTransport()
  2291. ftpClient.lineReceived(b"220 Welcome to Imaginary FTP.")
  2292. # Queue a login with no password
  2293. ftpClient.queueLogin("bob", "secret")
  2294. self.assertEqual(b"USER bob\r\n", ftpClient.transport.value())
  2295. # Clear the test buffer, acknowledge the USER command with a 230
  2296. # response code.
  2297. ftpClient.transport.clear()
  2298. ftpClient.lineReceived(b"230 Hello bob. No password needed.")
  2299. # The client shouldn't have sent anything more (i.e. it shouldn't have
  2300. # sent a PASS command).
  2301. self.assertEqual(b"", ftpClient.transport.value())
  2302. class PathHandlingTests(TestCase):
  2303. """
  2304. Handling paths.
  2305. """
  2306. def test_Normalizer(self):
  2307. """
  2308. Normalize paths.
  2309. """
  2310. for inp, outp in [
  2311. ("a", ["a"]),
  2312. ("/a", ["a"]),
  2313. ("/", []),
  2314. ("a/b/c", ["a", "b", "c"]),
  2315. ("/a/b/c", ["a", "b", "c"]),
  2316. ("/a/", ["a"]),
  2317. ("a/", ["a"]),
  2318. ]:
  2319. self.assertEqual(ftp.toSegments([], inp), outp)
  2320. for inp, outp in [
  2321. ("b", ["a", "b"]),
  2322. ("b/", ["a", "b"]),
  2323. ("/b", ["b"]),
  2324. ("/b/", ["b"]),
  2325. ("b/c", ["a", "b", "c"]),
  2326. ("b/c/", ["a", "b", "c"]),
  2327. ("/b/c", ["b", "c"]),
  2328. ("/b/c/", ["b", "c"]),
  2329. ]:
  2330. self.assertEqual(ftp.toSegments(["a"], inp), outp)
  2331. for inp, outp in [
  2332. ("//", []),
  2333. ("//a", ["a"]),
  2334. ("a//", ["a"]),
  2335. ("a//b", ["a", "b"]),
  2336. ]:
  2337. self.assertEqual(ftp.toSegments([], inp), outp)
  2338. for inp, outp in [("//", []), ("//b", ["b"]), ("b//c", ["a", "b", "c"])]:
  2339. self.assertEqual(ftp.toSegments(["a"], inp), outp)
  2340. for inp, outp in [
  2341. ("..", []),
  2342. ("../", []),
  2343. ("a/..", ["x"]),
  2344. ("/a/..", []),
  2345. ("/a/b/..", ["a"]),
  2346. ("/a/b/../", ["a"]),
  2347. ("/a/b/../c", ["a", "c"]),
  2348. ("/a/b/../c/", ["a", "c"]),
  2349. ("/a/b/../../c", ["c"]),
  2350. ("/a/b/../../c/", ["c"]),
  2351. ("/a/b/../../c/..", []),
  2352. ("/a/b/../../c/../", []),
  2353. ]:
  2354. self.assertEqual(ftp.toSegments(["x"], inp), outp)
  2355. for inp in [
  2356. "..",
  2357. "../",
  2358. "a/../..",
  2359. "a/../../",
  2360. "/..",
  2361. "/../",
  2362. "/a/../..",
  2363. "/a/../../",
  2364. "/a/b/../../..",
  2365. ]:
  2366. self.assertRaises(ftp.InvalidPath, ftp.toSegments, [], inp)
  2367. for inp in ["../..", "../../", "../a/../.."]:
  2368. self.assertRaises(ftp.InvalidPath, ftp.toSegments, ["x"], inp)
  2369. class IsGlobbingExpressionTests(TestCase):
  2370. """
  2371. Tests for _isGlobbingExpression utility function.
  2372. """
  2373. def test_isGlobbingExpressionEmptySegments(self):
  2374. """
  2375. _isGlobbingExpression will return False for None, or empty
  2376. segments.
  2377. """
  2378. self.assertFalse(ftp._isGlobbingExpression())
  2379. self.assertFalse(ftp._isGlobbingExpression([]))
  2380. self.assertFalse(ftp._isGlobbingExpression(None))
  2381. def test_isGlobbingExpressionNoGlob(self):
  2382. """
  2383. _isGlobbingExpression will return False for plain segments.
  2384. Also, it only checks the last segment part (filename) and will not
  2385. check the path name.
  2386. """
  2387. self.assertFalse(ftp._isGlobbingExpression(["ignore", "expr"]))
  2388. self.assertFalse(ftp._isGlobbingExpression(["*.txt", "expr"]))
  2389. def test_isGlobbingExpressionGlob(self):
  2390. """
  2391. _isGlobbingExpression will return True for segments which contains
  2392. globbing characters in the last segment part (filename).
  2393. """
  2394. self.assertTrue(ftp._isGlobbingExpression(["ignore", "*.txt"]))
  2395. self.assertTrue(ftp._isGlobbingExpression(["ignore", "[a-b].txt"]))
  2396. self.assertTrue(ftp._isGlobbingExpression(["ignore", "fil?.txt"]))
  2397. class BaseFTPRealmTests(TestCase):
  2398. """
  2399. Tests for L{ftp.BaseFTPRealm}, a base class to help define L{IFTPShell}
  2400. realms with different user home directory policies.
  2401. """
  2402. def test_interface(self):
  2403. """
  2404. L{ftp.BaseFTPRealm} implements L{IRealm}.
  2405. """
  2406. self.assertTrue(verifyClass(IRealm, ftp.BaseFTPRealm))
  2407. def test_getHomeDirectory(self):
  2408. """
  2409. L{ftp.BaseFTPRealm} calls its C{getHomeDirectory} method with the
  2410. avatarId being requested to determine the home directory for that
  2411. avatar.
  2412. """
  2413. result = filepath.FilePath(self.mktemp())
  2414. avatars = []
  2415. class TestRealm(ftp.BaseFTPRealm):
  2416. def getHomeDirectory(self, avatarId):
  2417. avatars.append(avatarId)
  2418. return result
  2419. realm = TestRealm(self.mktemp())
  2420. iface, avatar, logout = realm.requestAvatar(
  2421. "alice@example.com", None, ftp.IFTPShell
  2422. )
  2423. self.assertIsInstance(avatar, ftp.FTPShell)
  2424. self.assertEqual(avatar.filesystemRoot, result)
  2425. def test_anonymous(self):
  2426. """
  2427. L{ftp.BaseFTPRealm} returns an L{ftp.FTPAnonymousShell} instance for
  2428. anonymous avatar requests.
  2429. """
  2430. anonymous = self.mktemp()
  2431. realm = ftp.BaseFTPRealm(anonymous)
  2432. iface, avatar, logout = realm.requestAvatar(
  2433. checkers.ANONYMOUS, None, ftp.IFTPShell
  2434. )
  2435. self.assertIsInstance(avatar, ftp.FTPAnonymousShell)
  2436. self.assertEqual(avatar.filesystemRoot, filepath.FilePath(anonymous))
  2437. def test_notImplemented(self):
  2438. """
  2439. L{ftp.BaseFTPRealm.getHomeDirectory} should be overridden by a subclass
  2440. and raises L{NotImplementedError} if it is not.
  2441. """
  2442. realm = ftp.BaseFTPRealm(self.mktemp())
  2443. self.assertRaises(NotImplementedError, realm.getHomeDirectory, object())
  2444. class FTPRealmTests(TestCase):
  2445. """
  2446. Tests for L{ftp.FTPRealm}.
  2447. """
  2448. def test_getHomeDirectory(self):
  2449. """
  2450. L{ftp.FTPRealm} accepts an extra directory to its initializer and treats
  2451. the avatarId passed to L{ftp.FTPRealm.getHomeDirectory} as a single path
  2452. segment to construct a child of that directory.
  2453. """
  2454. base = "/path/to/home"
  2455. realm = ftp.FTPRealm(self.mktemp(), base)
  2456. home = realm.getHomeDirectory("alice@example.com")
  2457. self.assertEqual(filepath.FilePath(base).child("alice@example.com"), home)
  2458. def test_defaultHomeDirectory(self):
  2459. """
  2460. If no extra directory is passed to L{ftp.FTPRealm}, it uses C{"/home"}
  2461. as the base directory containing all user home directories.
  2462. """
  2463. realm = ftp.FTPRealm(self.mktemp())
  2464. home = realm.getHomeDirectory("alice@example.com")
  2465. self.assertEqual(filepath.FilePath("/home/alice@example.com"), home)
  2466. class SystemFTPRealmTests(TestCase):
  2467. """
  2468. Tests for L{ftp.SystemFTPRealm}.
  2469. """
  2470. skip = nonPOSIXSkip
  2471. def test_getHomeDirectory(self):
  2472. """
  2473. L{ftp.SystemFTPRealm.getHomeDirectory} treats the avatarId passed to it
  2474. as a username in the underlying platform and returns that account's home
  2475. directory.
  2476. """
  2477. # Try to pick a username that will have a home directory.
  2478. user = getpass.getuser()
  2479. # Try to find their home directory in a different way than used by the
  2480. # implementation. Maybe this is silly and can only introduce spurious
  2481. # failures due to system-specific configurations.
  2482. import pwd
  2483. expected = pwd.getpwnam(user).pw_dir
  2484. realm = ftp.SystemFTPRealm(self.mktemp())
  2485. home = realm.getHomeDirectory(user)
  2486. self.assertEqual(home, filepath.FilePath(expected))
  2487. def test_noSuchUser(self):
  2488. """
  2489. L{ftp.SystemFTPRealm.getHomeDirectory} raises L{UnauthorizedLogin} when
  2490. passed a username which has no corresponding home directory in the
  2491. system's accounts database.
  2492. """
  2493. # Add a prefix in case starting with a digit is a problem
  2494. user = random.choice(string.ascii_letters) + "".join(
  2495. random.choice(string.ascii_letters + string.digits) for _ in range(4)
  2496. )
  2497. realm = ftp.SystemFTPRealm(self.mktemp())
  2498. self.assertRaises(UnauthorizedLogin, realm.getHomeDirectory, user)
  2499. class ErrnoToFailureTests(TestCase):
  2500. """
  2501. Tests for L{ftp.errnoToFailure} errno checking.
  2502. """
  2503. def test_notFound(self):
  2504. """
  2505. C{errno.ENOENT} should be translated to L{ftp.FileNotFoundError}.
  2506. """
  2507. d = ftp.errnoToFailure(errno.ENOENT, "foo")
  2508. return self.assertFailure(d, ftp.FileNotFoundError)
  2509. def test_permissionDenied(self):
  2510. """
  2511. C{errno.EPERM} should be translated to L{ftp.PermissionDeniedError}.
  2512. """
  2513. d = ftp.errnoToFailure(errno.EPERM, "foo")
  2514. return self.assertFailure(d, ftp.PermissionDeniedError)
  2515. def test_accessDenied(self):
  2516. """
  2517. C{errno.EACCES} should be translated to L{ftp.PermissionDeniedError}.
  2518. """
  2519. d = ftp.errnoToFailure(errno.EACCES, "foo")
  2520. return self.assertFailure(d, ftp.PermissionDeniedError)
  2521. def test_notDirectory(self):
  2522. """
  2523. C{errno.ENOTDIR} should be translated to L{ftp.IsNotADirectoryError}.
  2524. """
  2525. d = ftp.errnoToFailure(errno.ENOTDIR, "foo")
  2526. return self.assertFailure(d, ftp.IsNotADirectoryError)
  2527. def test_fileExists(self):
  2528. """
  2529. C{errno.EEXIST} should be translated to L{ftp.FileExistsError}.
  2530. """
  2531. d = ftp.errnoToFailure(errno.EEXIST, "foo")
  2532. return self.assertFailure(d, ftp.FileExistsError)
  2533. def test_isDirectory(self):
  2534. """
  2535. C{errno.EISDIR} should be translated to L{ftp.IsADirectoryError}.
  2536. """
  2537. d = ftp.errnoToFailure(errno.EISDIR, "foo")
  2538. return self.assertFailure(d, ftp.IsADirectoryError)
  2539. def test_passThrough(self):
  2540. """
  2541. If an unknown errno is passed to L{ftp.errnoToFailure}, it should let
  2542. the originating exception pass through.
  2543. """
  2544. try:
  2545. raise RuntimeError("bar")
  2546. except BaseException:
  2547. d = ftp.errnoToFailure(-1, "foo")
  2548. return self.assertFailure(d, RuntimeError)
  2549. class AnonymousFTPShellTests(TestCase):
  2550. """
  2551. Test anonymous shell properties.
  2552. """
  2553. def test_anonymousWrite(self):
  2554. """
  2555. Check that L{ftp.FTPAnonymousShell} returns an error when trying to
  2556. open it in write mode.
  2557. """
  2558. shell = ftp.FTPAnonymousShell("")
  2559. d = shell.openForWriting(("foo",))
  2560. self.assertFailure(d, ftp.PermissionDeniedError)
  2561. return d
  2562. class IFTPShellTestsMixin:
  2563. """
  2564. Generic tests for the C{IFTPShell} interface.
  2565. """
  2566. def directoryExists(self, path):
  2567. """
  2568. Test if the directory exists at C{path}.
  2569. @param path: the relative path to check.
  2570. @type path: C{str}.
  2571. @return: C{True} if C{path} exists and is a directory, C{False} if
  2572. it's not the case
  2573. @rtype: C{bool}
  2574. """
  2575. raise NotImplementedError()
  2576. def createDirectory(self, path):
  2577. """
  2578. Create a directory in C{path}.
  2579. @param path: the relative path of the directory to create, with one
  2580. segment.
  2581. @type path: C{str}
  2582. """
  2583. raise NotImplementedError()
  2584. def fileExists(self, path):
  2585. """
  2586. Test if the file exists at C{path}.
  2587. @param path: the relative path to check.
  2588. @type path: C{str}.
  2589. @return: C{True} if C{path} exists and is a file, C{False} if it's not
  2590. the case.
  2591. @rtype: C{bool}
  2592. """
  2593. raise NotImplementedError()
  2594. def createFile(self, path, fileContent=b""):
  2595. """
  2596. Create a file named C{path} with some content.
  2597. @param path: the relative path of the file to create, without
  2598. directory.
  2599. @type path: C{str}
  2600. @param fileContent: the content of the file.
  2601. @type fileContent: C{str}
  2602. """
  2603. raise NotImplementedError()
  2604. def test_createDirectory(self):
  2605. """
  2606. C{directoryExists} should report correctly about directory existence,
  2607. and C{createDirectory} should create a directory detectable by
  2608. C{directoryExists}.
  2609. """
  2610. self.assertFalse(self.directoryExists("bar"))
  2611. self.createDirectory("bar")
  2612. self.assertTrue(self.directoryExists("bar"))
  2613. def test_createFile(self):
  2614. """
  2615. C{fileExists} should report correctly about file existence, and
  2616. C{createFile} should create a file detectable by C{fileExists}.
  2617. """
  2618. self.assertFalse(self.fileExists("file.txt"))
  2619. self.createFile("file.txt")
  2620. self.assertTrue(self.fileExists("file.txt"))
  2621. def test_makeDirectory(self):
  2622. """
  2623. Create a directory and check it ends in the filesystem.
  2624. """
  2625. d = self.shell.makeDirectory(("foo",))
  2626. def cb(result):
  2627. self.assertTrue(self.directoryExists("foo"))
  2628. return d.addCallback(cb)
  2629. def test_makeDirectoryError(self):
  2630. """
  2631. Creating a directory that already exists should fail with a
  2632. C{ftp.FileExistsError}.
  2633. """
  2634. self.createDirectory("foo")
  2635. d = self.shell.makeDirectory(("foo",))
  2636. return self.assertFailure(d, ftp.FileExistsError)
  2637. def test_removeDirectory(self):
  2638. """
  2639. Try to remove a directory and check it's removed from the filesystem.
  2640. """
  2641. self.createDirectory("bar")
  2642. d = self.shell.removeDirectory(("bar",))
  2643. def cb(result):
  2644. self.assertFalse(self.directoryExists("bar"))
  2645. return d.addCallback(cb)
  2646. def test_removeDirectoryOnFile(self):
  2647. """
  2648. removeDirectory should not work in file and fail with a
  2649. C{ftp.IsNotADirectoryError}.
  2650. """
  2651. self.createFile("file.txt")
  2652. d = self.shell.removeDirectory(("file.txt",))
  2653. return self.assertFailure(d, ftp.IsNotADirectoryError)
  2654. def test_removeNotExistingDirectory(self):
  2655. """
  2656. Removing directory that doesn't exist should fail with a
  2657. C{ftp.FileNotFoundError}.
  2658. """
  2659. d = self.shell.removeDirectory(("bar",))
  2660. return self.assertFailure(d, ftp.FileNotFoundError)
  2661. def test_removeFile(self):
  2662. """
  2663. Try to remove a file and check it's removed from the filesystem.
  2664. """
  2665. self.createFile("file.txt")
  2666. d = self.shell.removeFile(("file.txt",))
  2667. def cb(res):
  2668. self.assertFalse(self.fileExists("file.txt"))
  2669. d.addCallback(cb)
  2670. return d
  2671. def test_removeFileOnDirectory(self):
  2672. """
  2673. removeFile should not work on directory.
  2674. """
  2675. self.createDirectory("ned")
  2676. d = self.shell.removeFile(("ned",))
  2677. return self.assertFailure(d, ftp.IsADirectoryError)
  2678. def test_removeNotExistingFile(self):
  2679. """
  2680. Try to remove a non existent file, and check it raises a
  2681. L{ftp.FileNotFoundError}.
  2682. """
  2683. d = self.shell.removeFile(("foo",))
  2684. return self.assertFailure(d, ftp.FileNotFoundError)
  2685. def test_list(self):
  2686. """
  2687. Check the output of the list method.
  2688. """
  2689. self.createDirectory("ned")
  2690. self.createFile("file.txt")
  2691. d = self.shell.list((".",))
  2692. def cb(l):
  2693. l.sort()
  2694. self.assertEqual(l, [("file.txt", []), ("ned", [])])
  2695. return d.addCallback(cb)
  2696. def test_listWithStat(self):
  2697. """
  2698. Check the output of list with asked stats.
  2699. """
  2700. self.createDirectory("ned")
  2701. self.createFile("file.txt")
  2702. d = self.shell.list(
  2703. (".",),
  2704. (
  2705. "size",
  2706. "permissions",
  2707. ),
  2708. )
  2709. def cb(l):
  2710. l.sort()
  2711. self.assertEqual(len(l), 2)
  2712. self.assertEqual(l[0][0], "file.txt")
  2713. self.assertEqual(l[1][0], "ned")
  2714. # Size and permissions are reported differently between platforms
  2715. # so just check they are present
  2716. self.assertEqual(len(l[0][1]), 2)
  2717. self.assertEqual(len(l[1][1]), 2)
  2718. return d.addCallback(cb)
  2719. def test_listWithInvalidStat(self):
  2720. """
  2721. Querying an invalid stat should result to a C{AttributeError}.
  2722. """
  2723. self.createDirectory("ned")
  2724. d = self.shell.list(
  2725. (".",),
  2726. (
  2727. "size",
  2728. "whateverstat",
  2729. ),
  2730. )
  2731. return self.assertFailure(d, AttributeError)
  2732. def test_listFile(self):
  2733. """
  2734. Check the output of the list method on a file.
  2735. """
  2736. self.createFile("file.txt")
  2737. d = self.shell.list(("file.txt",))
  2738. def cb(l):
  2739. l.sort()
  2740. self.assertEqual(l, [("file.txt", [])])
  2741. return d.addCallback(cb)
  2742. def test_listNotExistingDirectory(self):
  2743. """
  2744. list on a directory that doesn't exist should fail with a
  2745. L{ftp.FileNotFoundError}.
  2746. """
  2747. d = self.shell.list(("foo",))
  2748. return self.assertFailure(d, ftp.FileNotFoundError)
  2749. def test_access(self):
  2750. """
  2751. Try to access a resource.
  2752. """
  2753. self.createDirectory("ned")
  2754. d = self.shell.access(("ned",))
  2755. return d
  2756. def test_accessNotFound(self):
  2757. """
  2758. access should fail on a resource that doesn't exist.
  2759. """
  2760. d = self.shell.access(("foo",))
  2761. return self.assertFailure(d, ftp.FileNotFoundError)
  2762. def test_openForReading(self):
  2763. """
  2764. Check that openForReading returns an object providing C{ftp.IReadFile}.
  2765. """
  2766. self.createFile("file.txt")
  2767. d = self.shell.openForReading(("file.txt",))
  2768. def cb(res):
  2769. self.assertTrue(ftp.IReadFile.providedBy(res))
  2770. d.addCallback(cb)
  2771. return d
  2772. def test_openForReadingNotFound(self):
  2773. """
  2774. openForReading should fail with a C{ftp.FileNotFoundError} on a file
  2775. that doesn't exist.
  2776. """
  2777. d = self.shell.openForReading(("ned",))
  2778. return self.assertFailure(d, ftp.FileNotFoundError)
  2779. def test_openForReadingOnDirectory(self):
  2780. """
  2781. openForReading should not work on directory.
  2782. """
  2783. self.createDirectory("ned")
  2784. d = self.shell.openForReading(("ned",))
  2785. return self.assertFailure(d, ftp.IsADirectoryError)
  2786. def test_openForWriting(self):
  2787. """
  2788. Check that openForWriting returns an object providing C{ftp.IWriteFile}.
  2789. """
  2790. d = self.shell.openForWriting(("foo",))
  2791. def cb1(res):
  2792. self.assertTrue(ftp.IWriteFile.providedBy(res))
  2793. return res.receive().addCallback(cb2)
  2794. def cb2(res):
  2795. self.assertTrue(IConsumer.providedBy(res))
  2796. d.addCallback(cb1)
  2797. return d
  2798. def test_openForWritingExistingDirectory(self):
  2799. """
  2800. openForWriting should not be able to open a directory that already
  2801. exists.
  2802. """
  2803. self.createDirectory("ned")
  2804. d = self.shell.openForWriting(("ned",))
  2805. return self.assertFailure(d, ftp.IsADirectoryError)
  2806. def test_openForWritingInNotExistingDirectory(self):
  2807. """
  2808. openForWring should fail with a L{ftp.FileNotFoundError} if you specify
  2809. a file in a directory that doesn't exist.
  2810. """
  2811. self.createDirectory("ned")
  2812. d = self.shell.openForWriting(("ned", "idonotexist", "foo"))
  2813. return self.assertFailure(d, ftp.FileNotFoundError)
  2814. def test_statFile(self):
  2815. """
  2816. Check the output of the stat method on a file.
  2817. """
  2818. fileContent = b"wobble\n"
  2819. self.createFile("file.txt", fileContent)
  2820. d = self.shell.stat(("file.txt",), ("size", "directory"))
  2821. def cb(res):
  2822. self.assertEqual(res[0], len(fileContent))
  2823. self.assertFalse(res[1])
  2824. d.addCallback(cb)
  2825. return d
  2826. def test_statDirectory(self):
  2827. """
  2828. Check the output of the stat method on a directory.
  2829. """
  2830. self.createDirectory("ned")
  2831. d = self.shell.stat(("ned",), ("size", "directory"))
  2832. def cb(res):
  2833. self.assertTrue(res[1])
  2834. d.addCallback(cb)
  2835. return d
  2836. def test_statOwnerGroup(self):
  2837. """
  2838. Check the owner and groups stats.
  2839. """
  2840. self.createDirectory("ned")
  2841. d = self.shell.stat(("ned",), ("owner", "group"))
  2842. def cb(res):
  2843. self.assertEqual(len(res), 2)
  2844. d.addCallback(cb)
  2845. return d
  2846. def test_statHardlinksNotImplemented(self):
  2847. """
  2848. If L{twisted.python.filepath.FilePath.getNumberOfHardLinks} is not
  2849. implemented, the number returned is 0
  2850. """
  2851. pathFunc = self.shell._path
  2852. def raiseNotImplemented():
  2853. raise NotImplementedError
  2854. def notImplementedFilePath(path):
  2855. f = pathFunc(path)
  2856. f.getNumberOfHardLinks = raiseNotImplemented
  2857. return f
  2858. self.shell._path = notImplementedFilePath
  2859. self.createDirectory("ned")
  2860. d = self.shell.stat(("ned",), ("hardlinks",))
  2861. self.assertEqual(self.successResultOf(d), [0])
  2862. def test_statOwnerGroupNotImplemented(self):
  2863. """
  2864. If L{twisted.python.filepath.FilePath.getUserID} or
  2865. L{twisted.python.filepath.FilePath.getGroupID} are not implemented,
  2866. the owner returned is "0" and the group is returned as "0"
  2867. """
  2868. pathFunc = self.shell._path
  2869. def raiseNotImplemented():
  2870. raise NotImplementedError
  2871. def notImplementedFilePath(path):
  2872. f = pathFunc(path)
  2873. f.getUserID = raiseNotImplemented
  2874. f.getGroupID = raiseNotImplemented
  2875. return f
  2876. self.shell._path = notImplementedFilePath
  2877. self.createDirectory("ned")
  2878. d = self.shell.stat(("ned",), ("owner", "group"))
  2879. self.assertEqual(self.successResultOf(d), ["0", "0"])
  2880. def test_statNotExisting(self):
  2881. """
  2882. stat should fail with L{ftp.FileNotFoundError} on a file that doesn't
  2883. exist.
  2884. """
  2885. d = self.shell.stat(("foo",), ("size", "directory"))
  2886. return self.assertFailure(d, ftp.FileNotFoundError)
  2887. def test_invalidStat(self):
  2888. """
  2889. Querying an invalid stat should result to a C{AttributeError}.
  2890. """
  2891. self.createDirectory("ned")
  2892. d = self.shell.stat(("ned",), ("size", "whateverstat"))
  2893. return self.assertFailure(d, AttributeError)
  2894. def test_rename(self):
  2895. """
  2896. Try to rename a directory.
  2897. """
  2898. self.createDirectory("ned")
  2899. d = self.shell.rename(("ned",), ("foo",))
  2900. def cb(res):
  2901. self.assertTrue(self.directoryExists("foo"))
  2902. self.assertFalse(self.directoryExists("ned"))
  2903. return d.addCallback(cb)
  2904. def test_renameNotExisting(self):
  2905. """
  2906. Renaming a directory that doesn't exist should fail with
  2907. L{ftp.FileNotFoundError}.
  2908. """
  2909. d = self.shell.rename(("foo",), ("bar",))
  2910. return self.assertFailure(d, ftp.FileNotFoundError)
  2911. class FTPShellTests(TestCase, IFTPShellTestsMixin):
  2912. """
  2913. Tests for the C{ftp.FTPShell} object.
  2914. """
  2915. def setUp(self):
  2916. """
  2917. Create a root directory and instantiate a shell.
  2918. """
  2919. self.root = filepath.FilePath(self.mktemp())
  2920. self.root.createDirectory()
  2921. self.shell = ftp.FTPShell(self.root)
  2922. def directoryExists(self, path):
  2923. """
  2924. Test if the directory exists at C{path}.
  2925. """
  2926. return self.root.child(path).isdir()
  2927. def createDirectory(self, path):
  2928. """
  2929. Create a directory in C{path}.
  2930. """
  2931. return self.root.child(path).createDirectory()
  2932. def fileExists(self, path):
  2933. """
  2934. Test if the file exists at C{path}.
  2935. """
  2936. return self.root.child(path).isfile()
  2937. def createFile(self, path, fileContent=b""):
  2938. """
  2939. Create a file named C{path} with some content.
  2940. """
  2941. return self.root.child(path).setContent(fileContent)
  2942. @implementer(IConsumer)
  2943. class TestConsumer:
  2944. """
  2945. A simple consumer for tests. It only works with non-streaming producers.
  2946. @ivar producer: an object providing
  2947. L{twisted.internet.interfaces.IPullProducer}.
  2948. """
  2949. producer = None
  2950. def registerProducer(self, producer, streaming):
  2951. """
  2952. Simple register of producer, checks that no register has happened
  2953. before.
  2954. @param producer: pull producer to use
  2955. @param streaming: unused
  2956. """
  2957. assert self.producer is None
  2958. self.buffer = []
  2959. self.producer = producer
  2960. self.producer.resumeProducing()
  2961. def unregisterProducer(self):
  2962. """
  2963. Unregister the producer, it should be done after a register.
  2964. """
  2965. assert self.producer is not None
  2966. self.producer = None
  2967. def write(self, data):
  2968. """
  2969. Save the data received.
  2970. @param data: data to append
  2971. """
  2972. self.buffer.append(data)
  2973. self.producer.resumeProducing()
  2974. class TestProducer:
  2975. """
  2976. A dumb producer.
  2977. """
  2978. def __init__(self, toProduce, consumer):
  2979. """
  2980. @param toProduce: data to write
  2981. @type toProduce: C{str}
  2982. @param consumer: the consumer of data.
  2983. @type consumer: C{IConsumer}
  2984. """
  2985. self.toProduce = toProduce
  2986. self.consumer = consumer
  2987. def start(self):
  2988. """
  2989. Send the data to consume.
  2990. """
  2991. self.consumer.write(self.toProduce)
  2992. class IReadWriteTestsMixin:
  2993. """
  2994. Generic tests for the C{IReadFile} and C{IWriteFile} interfaces.
  2995. """
  2996. def getFileReader(self, content):
  2997. """
  2998. Return an object providing C{IReadFile}, ready to send data C{content}.
  2999. @param content: data to send
  3000. """
  3001. raise NotImplementedError()
  3002. def getFileWriter(self):
  3003. """
  3004. Return an object providing C{IWriteFile}, ready to receive data.
  3005. """
  3006. raise NotImplementedError()
  3007. def getFileContent(self):
  3008. """
  3009. Return the content of the file used.
  3010. """
  3011. raise NotImplementedError()
  3012. def test_read(self):
  3013. """
  3014. Test L{ftp.IReadFile}: the implementation should have a send method
  3015. returning a C{Deferred} which fires when all the data has been sent
  3016. to the consumer, and the data should be correctly send to the consumer.
  3017. """
  3018. content = b"wobble\n"
  3019. consumer = TestConsumer()
  3020. def cbGet(reader):
  3021. return reader.send(consumer).addCallback(cbSend)
  3022. def cbSend(res):
  3023. self.assertEqual(b"".join(consumer.buffer), content)
  3024. return self.getFileReader(content).addCallback(cbGet)
  3025. def test_write(self):
  3026. """
  3027. Test L{ftp.IWriteFile}: the implementation should have a receive
  3028. method returning a C{Deferred} which fires with a consumer ready to
  3029. receive data to be written. It should also have a close() method that
  3030. returns a Deferred.
  3031. """
  3032. content = b"elbbow\n"
  3033. def cbGet(writer):
  3034. return writer.receive().addCallback(cbReceive, writer)
  3035. def cbReceive(consumer, writer):
  3036. producer = TestProducer(content, consumer)
  3037. consumer.registerProducer(None, True)
  3038. producer.start()
  3039. consumer.unregisterProducer()
  3040. return writer.close().addCallback(cbClose)
  3041. def cbClose(ignored):
  3042. self.assertEqual(self.getFileContent(), content)
  3043. return self.getFileWriter().addCallback(cbGet)
  3044. class FTPReadWriteTests(TestCase, IReadWriteTestsMixin):
  3045. """
  3046. Tests for C{ftp._FileReader} and C{ftp._FileWriter}, the objects returned
  3047. by the shell in C{openForReading}/C{openForWriting}.
  3048. """
  3049. def setUp(self):
  3050. """
  3051. Create a temporary file used later.
  3052. """
  3053. self.root = filepath.FilePath(self.mktemp())
  3054. self.root.createDirectory()
  3055. self.shell = ftp.FTPShell(self.root)
  3056. self.filename = "file.txt"
  3057. def getFileReader(self, content):
  3058. """
  3059. Return a C{ftp._FileReader} instance with a file opened for reading.
  3060. """
  3061. self.root.child(self.filename).setContent(content)
  3062. return self.shell.openForReading((self.filename,))
  3063. def getFileWriter(self):
  3064. """
  3065. Return a C{ftp._FileWriter} instance with a file opened for writing.
  3066. """
  3067. return self.shell.openForWriting((self.filename,))
  3068. def getFileContent(self):
  3069. """
  3070. Return the content of the temporary file.
  3071. """
  3072. return self.root.child(self.filename).getContent()
  3073. @implementer(ftp.IWriteFile)
  3074. class CloseTestWriter:
  3075. """
  3076. Close writing to a file.
  3077. """
  3078. closeStarted = False
  3079. def receive(self):
  3080. """
  3081. Receive bytes.
  3082. @return: L{Deferred}
  3083. """
  3084. self.buffer = BytesIO()
  3085. fc = ftp.FileConsumer(self.buffer)
  3086. return defer.succeed(fc)
  3087. def close(self):
  3088. """
  3089. Close bytes.
  3090. @return: L{Deferred}
  3091. """
  3092. self.closeStarted = True
  3093. return self.d
  3094. class CloseTestShell:
  3095. """
  3096. Close writing shell.
  3097. """
  3098. def openForWriting(self, segs):
  3099. return defer.succeed(self.writer)
  3100. class FTPCloseTests(TestCase):
  3101. """
  3102. Tests that the server invokes IWriteFile.close
  3103. """
  3104. def test_write(self):
  3105. """
  3106. Confirm that FTP uploads (i.e. ftp_STOR) correctly call and wait
  3107. upon the IWriteFile object's close() method
  3108. """
  3109. f = ftp.FTP()
  3110. f.workingDirectory = ["root"]
  3111. f.shell = CloseTestShell()
  3112. f.shell.writer = CloseTestWriter()
  3113. f.shell.writer.d = defer.Deferred()
  3114. f.factory = ftp.FTPFactory()
  3115. f.factory.timeOut = None
  3116. f.makeConnection(BytesIO())
  3117. di = ftp.DTP()
  3118. di.factory = ftp.DTPFactory(f)
  3119. f.dtpInstance = di
  3120. di.makeConnection(None)
  3121. stor_done = []
  3122. d = f.ftp_STOR("path")
  3123. d.addCallback(stor_done.append)
  3124. # the writer is still receiving data
  3125. self.assertFalse(f.shell.writer.closeStarted, "close() called early")
  3126. di.dataReceived(b"some data here")
  3127. self.assertFalse(f.shell.writer.closeStarted, "close() called early")
  3128. di.connectionLost("reason is ignored")
  3129. # now we should be waiting in close()
  3130. self.assertTrue(f.shell.writer.closeStarted, "close() not called")
  3131. self.assertFalse(stor_done)
  3132. f.shell.writer.d.callback("allow close() to finish")
  3133. self.assertTrue(stor_done)
  3134. return d # just in case an errback occurred
  3135. class FTPResponseCodeTests(TestCase):
  3136. """
  3137. Tests relating directly to response codes.
  3138. """
  3139. def test_unique(self):
  3140. """
  3141. All of the response code globals (for example C{RESTART_MARKER_REPLY} or
  3142. C{USR_NAME_OK_NEED_PASS}) have unique values and are present in the
  3143. C{RESPONSE} dictionary.
  3144. """
  3145. allValues = set(ftp.RESPONSE)
  3146. seenValues = set()
  3147. for key, value in vars(ftp).items():
  3148. if isinstance(value, str) and key.isupper():
  3149. self.assertIn(
  3150. value,
  3151. allValues,
  3152. "Code {!r} with value {!r} missing from RESPONSE dict".format(
  3153. key, value
  3154. ),
  3155. )
  3156. self.assertNotIn(
  3157. value,
  3158. seenValues,
  3159. f"Duplicate code {key!r} with value {value!r}",
  3160. )
  3161. seenValues.add(value)