ftp.py 109 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443
  1. # -*- test-case-name: twisted.test.test_ftp -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. An FTP protocol implementation
  6. """
  7. # System Imports
  8. import errno
  9. import fnmatch
  10. import ipaddress
  11. import os
  12. import re
  13. import stat
  14. import time
  15. try:
  16. import grp
  17. import pwd
  18. except ImportError:
  19. pwd = grp = None # type: ignore[assignment]
  20. from zope.interface import Interface, implementer
  21. # Twisted Imports
  22. from twisted import copyright
  23. from twisted.cred import checkers, credentials, error as cred_error, portal
  24. from twisted.internet import defer, error, interfaces, protocol, reactor
  25. from twisted.protocols import basic, policies
  26. from twisted.python import failure, filepath, log
  27. # constants
  28. # response codes
  29. RESTART_MARKER_REPLY = "100"
  30. SERVICE_READY_IN_N_MINUTES = "120"
  31. DATA_CNX_ALREADY_OPEN_START_XFR = "125"
  32. FILE_STATUS_OK_OPEN_DATA_CNX = "150"
  33. CMD_OK = "200.1"
  34. TYPE_SET_OK = "200.2"
  35. ENTERING_PORT_MODE = "200.3"
  36. EPSV_ALL_OK = "200.4"
  37. CMD_NOT_IMPLMNTD_SUPERFLUOUS = "202"
  38. SYS_STATUS_OR_HELP_REPLY = "211.1"
  39. FEAT_OK = "211.2"
  40. DIR_STATUS = "212"
  41. FILE_STATUS = "213"
  42. HELP_MSG = "214"
  43. NAME_SYS_TYPE = "215"
  44. SVC_READY_FOR_NEW_USER = "220.1"
  45. WELCOME_MSG = "220.2"
  46. SVC_CLOSING_CTRL_CNX = "221.1"
  47. GOODBYE_MSG = "221.2"
  48. DATA_CNX_OPEN_NO_XFR_IN_PROGRESS = "225"
  49. CLOSING_DATA_CNX = "226.1"
  50. TXFR_COMPLETE_OK = "226.2"
  51. ENTERING_PASV_MODE = "227"
  52. ENTERING_EPSV_MODE = "229"
  53. USR_LOGGED_IN_PROCEED = "230.1" # v1 of code 230
  54. GUEST_LOGGED_IN_PROCEED = "230.2" # v2 of code 230
  55. REQ_FILE_ACTN_COMPLETED_OK = "250"
  56. PWD_REPLY = "257.1"
  57. MKD_REPLY = "257.2"
  58. USR_NAME_OK_NEED_PASS = "331.1" # v1 of Code 331
  59. GUEST_NAME_OK_NEED_EMAIL = "331.2" # v2 of code 331
  60. NEED_ACCT_FOR_LOGIN = "332"
  61. REQ_FILE_ACTN_PENDING_FURTHER_INFO = "350"
  62. SVC_NOT_AVAIL_CLOSING_CTRL_CNX = "421.1"
  63. TOO_MANY_CONNECTIONS = "421.2"
  64. CANT_OPEN_DATA_CNX = "425"
  65. CNX_CLOSED_TXFR_ABORTED = "426"
  66. REQ_ACTN_ABRTD_FILE_UNAVAIL = "450"
  67. REQ_ACTN_ABRTD_LOCAL_ERR = "451"
  68. REQ_ACTN_ABRTD_INSUFF_STORAGE = "452"
  69. SYNTAX_ERR = "500"
  70. SYNTAX_ERR_IN_ARGS = "501"
  71. CMD_NOT_IMPLMNTD = "502.1"
  72. OPTS_NOT_IMPLEMENTED = "502.2"
  73. PASV_IPV6_NOT_IMPLEMENTED = "502.3"
  74. BAD_CMD_SEQ = "503"
  75. CMD_NOT_IMPLMNTD_FOR_PARAM = "504"
  76. UNSUPPORTED_NETWORK_PROTOCOL = "522"
  77. NOT_LOGGED_IN = "530.1" # v1 of code 530 - please log in
  78. AUTH_FAILURE = "530.2" # v2 of code 530 - authorization failure
  79. NEED_ACCT_FOR_STOR = "532"
  80. FILE_NOT_FOUND = "550.1" # no such file or directory
  81. PERMISSION_DENIED = "550.2" # permission denied
  82. ANON_USER_DENIED = "550.3" # anonymous users can't alter filesystem
  83. IS_NOT_A_DIR = "550.4" # rmd called on a path that is not a directory
  84. REQ_ACTN_NOT_TAKEN = "550.5"
  85. FILE_EXISTS = "550.6"
  86. IS_A_DIR = "550.7"
  87. PAGE_TYPE_UNK = "551"
  88. EXCEEDED_STORAGE_ALLOC = "552"
  89. FILENAME_NOT_ALLOWED = "553"
  90. RESPONSE = {
  91. # -- 100's --
  92. # TODO: this must be fixed
  93. RESTART_MARKER_REPLY: "110 MARK yyyy-mmmm",
  94. SERVICE_READY_IN_N_MINUTES: "120 service ready in %s minutes",
  95. DATA_CNX_ALREADY_OPEN_START_XFR: "125 Data connection already open, "
  96. "starting transfer",
  97. FILE_STATUS_OK_OPEN_DATA_CNX: "150 File status okay; about to open "
  98. "data connection.",
  99. # -- 200's --
  100. CMD_OK: "200 Command OK",
  101. TYPE_SET_OK: "200 Type set to %s.",
  102. ENTERING_PORT_MODE: "200 PORT OK",
  103. EPSV_ALL_OK: "200 EPSV ALL OK",
  104. CMD_NOT_IMPLMNTD_SUPERFLUOUS: "202 Command not implemented, "
  105. "superfluous at this site",
  106. SYS_STATUS_OR_HELP_REPLY: "211 System status reply",
  107. FEAT_OK: ["211-Features:", "211 End"],
  108. DIR_STATUS: "212 %s",
  109. FILE_STATUS: "213 %s",
  110. HELP_MSG: "214 help: %s",
  111. NAME_SYS_TYPE: "215 UNIX Type: L8",
  112. WELCOME_MSG: "220 %s",
  113. SVC_READY_FOR_NEW_USER: "220 Service ready",
  114. SVC_CLOSING_CTRL_CNX: "221 Service closing control " "connection",
  115. GOODBYE_MSG: "221 Goodbye.",
  116. DATA_CNX_OPEN_NO_XFR_IN_PROGRESS: "225 data connection open, no "
  117. "transfer in progress",
  118. CLOSING_DATA_CNX: "226 Abort successful",
  119. TXFR_COMPLETE_OK: "226 Transfer Complete.",
  120. ENTERING_PASV_MODE: "227 Entering Passive Mode (%s).",
  121. # RFC 2428 section 3
  122. ENTERING_EPSV_MODE: "229 Entering Extended Passive Mode (|||%s|).",
  123. USR_LOGGED_IN_PROCEED: "230 User logged in, proceed",
  124. GUEST_LOGGED_IN_PROCEED: "230 Anonymous login ok, access " "restrictions apply.",
  125. # i.e. CWD completed OK
  126. REQ_FILE_ACTN_COMPLETED_OK: "250 Requested File Action Completed " "OK",
  127. PWD_REPLY: '257 "%s"',
  128. MKD_REPLY: '257 "%s" created',
  129. # -- 300's --
  130. USR_NAME_OK_NEED_PASS: "331 Password required for %s.",
  131. GUEST_NAME_OK_NEED_EMAIL: "331 Guest login ok, type your email "
  132. "address as password.",
  133. NEED_ACCT_FOR_LOGIN: "332 Need account for login.",
  134. REQ_FILE_ACTN_PENDING_FURTHER_INFO: "350 Requested file action pending "
  135. "further information.",
  136. # -- 400's --
  137. SVC_NOT_AVAIL_CLOSING_CTRL_CNX: "421 Service not available, closing "
  138. "control connection.",
  139. TOO_MANY_CONNECTIONS: "421 Too many users right now, try "
  140. "again in a few minutes.",
  141. CANT_OPEN_DATA_CNX: "425 Can't open data connection.",
  142. CNX_CLOSED_TXFR_ABORTED: "426 Transfer aborted. Data " "connection closed.",
  143. REQ_ACTN_ABRTD_FILE_UNAVAIL: "450 Requested action aborted. " "File unavailable.",
  144. REQ_ACTN_ABRTD_LOCAL_ERR: "451 Requested action aborted. "
  145. "Local error in processing.",
  146. REQ_ACTN_ABRTD_INSUFF_STORAGE: "452 Requested action aborted. "
  147. "Insufficient storage.",
  148. # -- 500's --
  149. SYNTAX_ERR: "500 Syntax error: %s",
  150. SYNTAX_ERR_IN_ARGS: "501 syntax error in argument(s) %s.",
  151. CMD_NOT_IMPLMNTD: "502 Command '%s' not implemented",
  152. OPTS_NOT_IMPLEMENTED: "502 Option '%s' not implemented.",
  153. PASV_IPV6_NOT_IMPLEMENTED: "502 PASV available only for IPv4 (use EPSV instead)",
  154. BAD_CMD_SEQ: "503 Incorrect sequence of commands: " "%s",
  155. CMD_NOT_IMPLMNTD_FOR_PARAM: "504 Not implemented for parameter " "'%s'.",
  156. # RFC 2428 section 2
  157. UNSUPPORTED_NETWORK_PROTOCOL: "522 Network protocol not supported, use (%s)",
  158. NOT_LOGGED_IN: "530 Please login with USER and PASS.",
  159. AUTH_FAILURE: "530 Sorry, Authentication failed.",
  160. NEED_ACCT_FOR_STOR: "532 Need an account for storing " "files",
  161. FILE_NOT_FOUND: "550 %s: No such file or directory.",
  162. PERMISSION_DENIED: "550 %s: Permission denied.",
  163. ANON_USER_DENIED: "550 Anonymous users are forbidden to " "change the filesystem",
  164. IS_NOT_A_DIR: "550 Cannot rmd, %s is not a " "directory",
  165. FILE_EXISTS: "550 %s: File exists",
  166. IS_A_DIR: "550 %s: is a directory",
  167. REQ_ACTN_NOT_TAKEN: "550 Requested action not taken: %s",
  168. PAGE_TYPE_UNK: "551 Page type unknown",
  169. EXCEEDED_STORAGE_ALLOC: "552 Requested file action aborted, "
  170. "exceeded file storage allocation",
  171. FILENAME_NOT_ALLOWED: "553 Requested action not taken, file " "name not allowed",
  172. }
  173. # IANA address family numbers
  174. # (https://www.iana.org/assignments/address-family-numbers).
  175. # We only handle IP and IP6 at the moment.
  176. #
  177. # If these are needed by other parts of Twisted then they should be moved
  178. # somewhere more central, filled out, and exported.
  179. _AFNUM_IP = 1
  180. _AFNUM_IP6 = 2
  181. class InvalidPath(Exception):
  182. """
  183. Internal exception used to signify an error during parsing a path.
  184. """
  185. def toSegments(cwd, path):
  186. """
  187. Normalize a path, as represented by a list of strings each
  188. representing one segment of the path.
  189. """
  190. if path.startswith("/"):
  191. segs = []
  192. else:
  193. segs = cwd[:]
  194. for s in path.split("/"):
  195. if s == "." or s == "":
  196. continue
  197. elif s == "..":
  198. if segs:
  199. segs.pop()
  200. else:
  201. raise InvalidPath(cwd, path)
  202. elif "\0" in s or "/" in s:
  203. raise InvalidPath(cwd, path)
  204. else:
  205. segs.append(s)
  206. return segs
  207. def errnoToFailure(e, path):
  208. """
  209. Map C{OSError} and C{IOError} to standard FTP errors.
  210. """
  211. if e == errno.ENOENT:
  212. return defer.fail(FileNotFoundError(path))
  213. elif e == errno.EACCES or e == errno.EPERM:
  214. return defer.fail(PermissionDeniedError(path))
  215. elif e == errno.ENOTDIR:
  216. return defer.fail(IsNotADirectoryError(path))
  217. elif e == errno.EEXIST:
  218. return defer.fail(FileExistsError(path))
  219. elif e == errno.EISDIR:
  220. return defer.fail(IsADirectoryError(path))
  221. else:
  222. return defer.fail()
  223. _testTranslation = fnmatch.translate("TEST")
  224. def _isGlobbingExpression(segments=None):
  225. """
  226. Helper for checking if a FTPShell `segments` contains a wildcard Unix
  227. expression.
  228. Only filename globbing is supported.
  229. This means that wildcards can only be presents in the last element of
  230. `segments`.
  231. @type segments: C{list}
  232. @param segments: List of path elements as used by the FTP server protocol.
  233. @rtype: Boolean
  234. @return: True if `segments` contains a globbing expression.
  235. """
  236. if not segments:
  237. return False
  238. # To check that something is a glob expression, we convert it to
  239. # Regular Expression.
  240. # We compare it to the translation of a known non-glob expression.
  241. # If the result is the same as the original expression then it contains no
  242. # globbing expression.
  243. globCandidate = segments[-1]
  244. globTranslations = fnmatch.translate(globCandidate)
  245. nonGlobTranslations = _testTranslation.replace("TEST", globCandidate, 1)
  246. if nonGlobTranslations == globTranslations:
  247. return False
  248. else:
  249. return True
  250. class FTPCmdError(Exception):
  251. """
  252. Generic exception for FTP commands.
  253. """
  254. def __init__(self, *msg):
  255. Exception.__init__(self, *msg)
  256. self.errorMessage = msg
  257. def response(self):
  258. """
  259. Generate a FTP response message for this error.
  260. """
  261. return RESPONSE[self.errorCode] % self.errorMessage
  262. class FileNotFoundError(FTPCmdError):
  263. """
  264. Raised when trying to access a non existent file or directory.
  265. """
  266. errorCode = FILE_NOT_FOUND
  267. class AnonUserDeniedError(FTPCmdError):
  268. """
  269. Raised when an anonymous user issues a command that will alter the
  270. filesystem
  271. """
  272. errorCode = ANON_USER_DENIED
  273. class PermissionDeniedError(FTPCmdError):
  274. """
  275. Raised when access is attempted to a resource to which access is
  276. not allowed.
  277. """
  278. errorCode = PERMISSION_DENIED
  279. class IsNotADirectoryError(FTPCmdError):
  280. """
  281. Raised when RMD is called on a path that isn't a directory.
  282. """
  283. errorCode = IS_NOT_A_DIR
  284. class FileExistsError(FTPCmdError):
  285. """
  286. Raised when attempted to override an existing resource.
  287. """
  288. errorCode = FILE_EXISTS
  289. class IsADirectoryError(FTPCmdError):
  290. """
  291. Raised when DELE is called on a path that is a directory.
  292. """
  293. errorCode = IS_A_DIR
  294. class CmdSyntaxError(FTPCmdError):
  295. """
  296. Raised when a command syntax is wrong.
  297. """
  298. errorCode = SYNTAX_ERR
  299. class CmdArgSyntaxError(FTPCmdError):
  300. """
  301. Raised when a command is called with wrong value or a wrong number of
  302. arguments.
  303. """
  304. errorCode = SYNTAX_ERR_IN_ARGS
  305. class CmdNotImplementedError(FTPCmdError):
  306. """
  307. Raised when an unimplemented command is given to the server.
  308. """
  309. errorCode = CMD_NOT_IMPLMNTD
  310. class CmdNotImplementedForArgError(FTPCmdError):
  311. """
  312. Raised when the handling of a parameter for a command is not implemented by
  313. the server.
  314. """
  315. errorCode = CMD_NOT_IMPLMNTD_FOR_PARAM
  316. class PASVIPv6NotImplementedError(FTPCmdError):
  317. """
  318. Raised when PASV is used with IPv6.
  319. """
  320. errorCode = PASV_IPV6_NOT_IMPLEMENTED
  321. class FTPError(Exception):
  322. pass
  323. class PortConnectionError(Exception):
  324. pass
  325. class BadCmdSequenceError(FTPCmdError):
  326. """
  327. Raised when a client sends a series of commands in an illogical sequence.
  328. """
  329. errorCode = BAD_CMD_SEQ
  330. class AuthorizationError(FTPCmdError):
  331. """
  332. Raised when client authentication fails.
  333. """
  334. errorCode = AUTH_FAILURE
  335. class UnsupportedNetworkProtocolError(FTPCmdError):
  336. """
  337. Raised when the client requests an unsupported network protocol.
  338. """
  339. errorCode = UNSUPPORTED_NETWORK_PROTOCOL
  340. def debugDeferred(self, *_):
  341. log.msg("debugDeferred(): %s" % str(_), debug=True)
  342. # -- DTP Protocol --
  343. _months = [
  344. None,
  345. "Jan",
  346. "Feb",
  347. "Mar",
  348. "Apr",
  349. "May",
  350. "Jun",
  351. "Jul",
  352. "Aug",
  353. "Sep",
  354. "Oct",
  355. "Nov",
  356. "Dec",
  357. ]
  358. @implementer(interfaces.IConsumer)
  359. class DTP(protocol.Protocol):
  360. isConnected = False
  361. _cons = None
  362. _onConnLost = None
  363. _buffer = None
  364. _encoding = "latin-1"
  365. def connectionMade(self):
  366. self.isConnected = True
  367. self.factory.deferred.callback(None)
  368. self._buffer = []
  369. def connectionLost(self, reason):
  370. self.isConnected = False
  371. if self._onConnLost is not None:
  372. self._onConnLost.callback(None)
  373. def sendLine(self, line):
  374. """
  375. Send a line to data channel.
  376. @param line: The line to be sent.
  377. @type line: L{bytes}
  378. """
  379. self.transport.write(line + b"\r\n")
  380. def _formatOneListResponse(
  381. self, name, size, directory, permissions, hardlinks, modified, owner, group
  382. ):
  383. """
  384. Helper method to format one entry's info into a text entry like:
  385. 'drwxrwxrwx 0 user group 0 Jan 01 1970 filename.txt'
  386. @param name: C{bytes} name of the entry (file or directory or link)
  387. @param size: C{int} size of the entry
  388. @param directory: evals to C{bool} - whether the entry is a directory
  389. @param permissions: L{twisted.python.filepath.Permissions} object
  390. representing that entry's permissions
  391. @param hardlinks: C{int} number of hardlinks
  392. @param modified: C{float} - entry's last modified time in seconds
  393. since the epoch
  394. @param owner: C{str} username of the owner
  395. @param group: C{str} group name of the owner
  396. @return: C{str} in the requisite format
  397. """
  398. def formatDate(mtime):
  399. now = time.gmtime()
  400. info = {
  401. "month": _months[mtime.tm_mon],
  402. "day": mtime.tm_mday,
  403. "year": mtime.tm_year,
  404. "hour": mtime.tm_hour,
  405. "minute": mtime.tm_min,
  406. }
  407. if now.tm_year != mtime.tm_year:
  408. return "%(month)s %(day)02d %(year)5d" % info
  409. else:
  410. return "%(month)s %(day)02d %(hour)02d:%(minute)02d" % info
  411. format = (
  412. "%(directory)s%(permissions)s%(hardlinks)4d "
  413. "%(owner)-9s %(group)-9s %(size)15d %(date)12s "
  414. )
  415. msg = (
  416. format
  417. % {
  418. "directory": directory and "d" or "-",
  419. "permissions": permissions.shorthand(),
  420. "hardlinks": hardlinks,
  421. "owner": owner[:8],
  422. "group": group[:8],
  423. "size": size,
  424. "date": formatDate(time.gmtime(modified)),
  425. }
  426. ).encode(self._encoding)
  427. return msg + name
  428. def sendListResponse(self, name, response):
  429. self.sendLine(self._formatOneListResponse(name, *response))
  430. # Proxy IConsumer to our transport
  431. def registerProducer(self, producer, streaming):
  432. return self.transport.registerProducer(producer, streaming)
  433. def unregisterProducer(self):
  434. self.transport.unregisterProducer()
  435. self.transport.loseConnection()
  436. def write(self, data):
  437. if self.isConnected:
  438. return self.transport.write(data)
  439. raise Exception("Crap damn crap damn crap damn")
  440. # Pretend to be a producer, too.
  441. def _conswrite(self, bytes):
  442. try:
  443. self._cons.write(bytes)
  444. except BaseException:
  445. self._onConnLost.errback()
  446. def dataReceived(self, bytes):
  447. if self._cons is not None:
  448. self._conswrite(bytes)
  449. else:
  450. self._buffer.append(bytes)
  451. def _unregConsumer(self, ignored):
  452. self._cons.unregisterProducer()
  453. self._cons = None
  454. del self._onConnLost
  455. return ignored
  456. def registerConsumer(self, cons):
  457. assert self._cons is None
  458. self._cons = cons
  459. self._cons.registerProducer(self, True)
  460. for chunk in self._buffer:
  461. self._conswrite(chunk)
  462. self._buffer = None
  463. if self.isConnected:
  464. self._onConnLost = d = defer.Deferred()
  465. d.addBoth(self._unregConsumer)
  466. return d
  467. else:
  468. self._cons.unregisterProducer()
  469. self._cons = None
  470. return defer.succeed(None)
  471. def resumeProducing(self):
  472. self.transport.resumeProducing()
  473. def pauseProducing(self):
  474. self.transport.pauseProducing()
  475. def stopProducing(self):
  476. self.transport.stopProducing()
  477. class DTPFactory(protocol.ClientFactory):
  478. """
  479. Client factory for I{data transfer process} protocols.
  480. @ivar peerCheck: perform checks to make sure the ftp-pi's peer is the same
  481. as the dtp's
  482. @ivar pi: a reference to this factory's protocol interpreter
  483. @ivar _state: Indicates the current state of the DTPFactory. Initially,
  484. this is L{_IN_PROGRESS}. If the connection fails or times out, it is
  485. L{_FAILED}. If the connection succeeds before the timeout, it is
  486. L{_FINISHED}.
  487. @cvar _IN_PROGRESS: Token to signal that connection is active.
  488. @type _IN_PROGRESS: L{object}.
  489. @cvar _FAILED: Token to signal that connection has failed.
  490. @type _FAILED: L{object}.
  491. @cvar _FINISHED: Token to signal that connection was successfully closed.
  492. @type _FINISHED: L{object}.
  493. """
  494. _IN_PROGRESS = object()
  495. _FAILED = object()
  496. _FINISHED = object()
  497. _state = _IN_PROGRESS
  498. # -- configuration variables --
  499. peerCheck = False
  500. # -- class variables --
  501. def __init__(self, pi, peerHost=None, reactor=None):
  502. """
  503. Constructor
  504. @param pi: this factory's protocol interpreter
  505. @param peerHost: if peerCheck is True, this is the tuple that the
  506. generated instance will use to perform security checks
  507. """
  508. self.pi = pi
  509. self.peerHost = peerHost # from FTP.transport.peerHost()
  510. # deferred will fire when instance is connected
  511. self.deferred = defer.Deferred()
  512. self.delayedCall = None
  513. if reactor is None:
  514. from twisted.internet import reactor
  515. self._reactor = reactor
  516. def buildProtocol(self, addr):
  517. log.msg("DTPFactory.buildProtocol", debug=True)
  518. if self._state is not self._IN_PROGRESS:
  519. return None
  520. self._state = self._FINISHED
  521. self.cancelTimeout()
  522. p = DTP()
  523. p.factory = self
  524. p.pi = self.pi
  525. self.pi.dtpInstance = p
  526. return p
  527. def stopFactory(self):
  528. log.msg("dtpFactory.stopFactory", debug=True)
  529. self.cancelTimeout()
  530. def timeoutFactory(self):
  531. log.msg("timed out waiting for DTP connection")
  532. if self._state is not self._IN_PROGRESS:
  533. return
  534. self._state = self._FAILED
  535. d = self.deferred
  536. self.deferred = None
  537. d.errback(PortConnectionError(defer.TimeoutError("DTPFactory timeout")))
  538. def cancelTimeout(self):
  539. if self.delayedCall is not None and self.delayedCall.active():
  540. log.msg("cancelling DTP timeout", debug=True)
  541. self.delayedCall.cancel()
  542. def setTimeout(self, seconds):
  543. log.msg("DTPFactory.setTimeout set to %s seconds" % seconds)
  544. self.delayedCall = self._reactor.callLater(seconds, self.timeoutFactory)
  545. def clientConnectionFailed(self, connector, reason):
  546. if self._state is not self._IN_PROGRESS:
  547. return
  548. self._state = self._FAILED
  549. d = self.deferred
  550. self.deferred = None
  551. d.errback(PortConnectionError(reason))
  552. # -- FTP-PI (Protocol Interpreter) --
  553. class ASCIIConsumerWrapper:
  554. def __init__(self, cons):
  555. self.cons = cons
  556. self.registerProducer = cons.registerProducer
  557. self.unregisterProducer = cons.unregisterProducer
  558. assert (
  559. os.linesep == "\r\n" or len(os.linesep) == 1
  560. ), "Unsupported platform (yea right like this even exists)"
  561. if os.linesep == "\r\n":
  562. self.write = cons.write
  563. def write(self, bytes):
  564. return self.cons.write(bytes.replace(os.linesep, "\r\n"))
  565. @implementer(interfaces.IConsumer)
  566. class FileConsumer:
  567. """
  568. A consumer for FTP input that writes data to a file.
  569. @ivar fObj: a file object opened for writing, used to write data received.
  570. @type fObj: C{file}
  571. """
  572. def __init__(self, fObj):
  573. self.fObj = fObj
  574. def registerProducer(self, producer, streaming):
  575. self.producer = producer
  576. assert streaming
  577. def unregisterProducer(self):
  578. self.producer = None
  579. self.fObj.close()
  580. def write(self, bytes):
  581. self.fObj.write(bytes)
  582. class FTPOverflowProtocol(basic.LineReceiver):
  583. """FTP mini-protocol for when there are too many connections."""
  584. _encoding = "latin-1"
  585. def connectionMade(self):
  586. self.sendLine(RESPONSE[TOO_MANY_CONNECTIONS].encode(self._encoding))
  587. self.transport.loseConnection()
  588. class FTP(basic.LineReceiver, policies.TimeoutMixin):
  589. """
  590. Protocol Interpreter for the File Transfer Protocol
  591. @ivar state: The current server state. One of L{UNAUTH},
  592. L{INAUTH}, L{AUTHED}, L{RENAMING}.
  593. @ivar shell: The connected avatar
  594. @ivar binary: The transfer mode. If false, ASCII.
  595. @ivar dtpFactory: Generates a single DTP for this session
  596. @ivar dtpPort: Port returned from listenTCP
  597. @ivar listenFactory: A callable with the signature of
  598. L{twisted.internet.interfaces.IReactorTCP.listenTCP} which will be used
  599. to create Ports for passive connections (mainly for testing).
  600. @ivar _epsvAll: If true, "EPSV ALL" was received from the client,
  601. requiring the server to reject all data connection setup commands
  602. other than EPSV. See RFC 2428.
  603. @ivar _supportedNetworkProtocols: A collection of network protocol
  604. numbers supported by the EPRT and EPSV commands.
  605. @ivar passivePortRange: iterator used as source of passive port numbers.
  606. @type passivePortRange: C{iterator}
  607. @cvar UNAUTH: Command channel is not yet authenticated.
  608. @type UNAUTH: L{int}
  609. @cvar INAUTH: Command channel is in the process of being authenticated.
  610. @type INAUTH: L{int}
  611. @cvar AUTHED: Command channel was successfully authenticated.
  612. @type AUTHED: L{int}
  613. @cvar RENAMING: Command channel is between the renaming command sequence.
  614. @type RENAMING: L{int}
  615. """
  616. disconnected = False
  617. # States an FTP can be in
  618. UNAUTH, INAUTH, AUTHED, RENAMING = range(4)
  619. # how long the DTP waits for a connection
  620. dtpTimeout = 10
  621. portal = None
  622. shell = None
  623. dtpFactory = None
  624. dtpPort = None
  625. dtpInstance = None
  626. binary = True
  627. _epsvAll = False
  628. _supportedNetworkProtocols = (_AFNUM_IP, _AFNUM_IP6)
  629. PUBLIC_COMMANDS = ["FEAT", "QUIT"]
  630. FEATURES = ["FEAT", "MDTM", "PASV", "SIZE", "TYPE A;I"]
  631. passivePortRange = range(0, 1)
  632. listenFactory = reactor.listenTCP # type: ignore[attr-defined]
  633. _encoding = "latin-1"
  634. def reply(self, key, *args):
  635. msg = RESPONSE[key] % args
  636. self.sendLine(msg)
  637. def sendLine(self, line):
  638. """
  639. (Private) Encodes and sends a line
  640. @param line: L{bytes} or L{unicode}
  641. """
  642. if isinstance(line, str):
  643. line = line.encode(self._encoding)
  644. super().sendLine(line)
  645. def connectionMade(self):
  646. self.state = self.UNAUTH
  647. self.setTimeout(self.timeOut)
  648. self.reply(WELCOME_MSG, self.factory.welcomeMessage)
  649. def connectionLost(self, reason):
  650. # if we have a DTP protocol instance running and
  651. # we lose connection to the client's PI, kill the
  652. # DTP connection and close the port
  653. if self.dtpFactory:
  654. self.cleanupDTP()
  655. self.setTimeout(None)
  656. if hasattr(self.shell, "logout") and self.shell.logout is not None:
  657. self.shell.logout()
  658. self.shell = None
  659. self._epsvAll = False
  660. self.transport = None
  661. def timeoutConnection(self):
  662. self.transport.loseConnection()
  663. def lineReceived(self, line):
  664. self.resetTimeout()
  665. self.pauseProducing()
  666. line = line.decode(self._encoding)
  667. def processFailed(err):
  668. if err.check(FTPCmdError):
  669. self.sendLine(err.value.response())
  670. elif err.check(TypeError) and any(
  671. msg in err.value.args[0]
  672. for msg in ("takes exactly", "required positional argument")
  673. ):
  674. self.reply(SYNTAX_ERR, f"{cmd} requires an argument.")
  675. else:
  676. log.msg("Unexpected FTP error")
  677. log.err(err)
  678. self.reply(REQ_ACTN_NOT_TAKEN, "internal server error")
  679. def processSucceeded(result):
  680. if isinstance(result, tuple):
  681. self.reply(*result)
  682. elif result is not None:
  683. self.reply(result)
  684. def allDone(ignored):
  685. if not self.disconnected:
  686. self.resumeProducing()
  687. spaceIndex = line.find(" ")
  688. if spaceIndex != -1:
  689. cmd = line[:spaceIndex]
  690. args = (line[spaceIndex + 1 :],)
  691. else:
  692. cmd = line
  693. args = ()
  694. d = defer.maybeDeferred(self.processCommand, cmd, *args)
  695. d.addCallbacks(processSucceeded, processFailed)
  696. d.addErrback(log.err)
  697. # XXX It burnsss
  698. # LineReceiver doesn't let you resumeProducing inside
  699. # lineReceived atm
  700. from twisted.internet import reactor
  701. reactor.callLater(0, d.addBoth, allDone)
  702. def processCommand(self, cmd, *params):
  703. def call_ftp_command(command):
  704. method = getattr(self, "ftp_" + command, None)
  705. if method is not None:
  706. return method(*params)
  707. return defer.fail(CmdNotImplementedError(command))
  708. cmd = cmd.upper()
  709. if cmd in self.PUBLIC_COMMANDS:
  710. return call_ftp_command(cmd)
  711. elif self.state == self.UNAUTH:
  712. if cmd == "USER":
  713. return self.ftp_USER(*params)
  714. elif cmd == "PASS":
  715. return BAD_CMD_SEQ, "USER required before PASS"
  716. else:
  717. return NOT_LOGGED_IN
  718. elif self.state == self.INAUTH:
  719. if cmd == "PASS":
  720. return self.ftp_PASS(*params)
  721. else:
  722. return BAD_CMD_SEQ, "PASS required after USER"
  723. elif self.state == self.AUTHED:
  724. return call_ftp_command(cmd)
  725. elif self.state == self.RENAMING:
  726. if cmd == "RNTO":
  727. return self.ftp_RNTO(*params)
  728. else:
  729. return BAD_CMD_SEQ, "RNTO required after RNFR"
  730. def getDTPPort(self, factory, interface=""):
  731. """
  732. Return a port for passive access, using C{self.passivePortRange}
  733. attribute.
  734. @param factory: the protocol factory to connect to the port.
  735. @type factory: L{twisted.internet.protocol.ServerFactory}
  736. @param interface: the local IPv4 or IPv6 address to which to bind;
  737. defaults to "", i.e. all IPv4 addresses.
  738. @type interface: C{str}
  739. """
  740. for portn in self.passivePortRange:
  741. try:
  742. dtpPort = self.listenFactory(portn, factory, interface=interface)
  743. except error.CannotListenError:
  744. continue
  745. else:
  746. return dtpPort
  747. raise error.CannotListenError(
  748. "", portn, f"No port available in range {self.passivePortRange}"
  749. )
  750. def ftp_USER(self, username):
  751. """
  752. First part of login. Get the username the peer wants to
  753. authenticate as.
  754. """
  755. if not username:
  756. return defer.fail(CmdSyntaxError("USER requires an argument"))
  757. self._user = username
  758. self.state = self.INAUTH
  759. if self.factory.allowAnonymous and self._user == self.factory.userAnonymous:
  760. return GUEST_NAME_OK_NEED_EMAIL
  761. else:
  762. return (USR_NAME_OK_NEED_PASS, username)
  763. # TODO: add max auth try before timeout from ip...
  764. # TODO: need to implement minimal ABOR command
  765. def ftp_PASS(self, password):
  766. """
  767. Second part of login. Get the password the peer wants to
  768. authenticate with.
  769. """
  770. if self.factory.allowAnonymous and self._user == self.factory.userAnonymous:
  771. # anonymous login
  772. creds = credentials.Anonymous()
  773. reply = GUEST_LOGGED_IN_PROCEED
  774. else:
  775. # user login
  776. creds = credentials.UsernamePassword(self._user, password)
  777. reply = USR_LOGGED_IN_PROCEED
  778. del self._user
  779. def _cbLogin(result):
  780. (interface, avatar, logout) = result
  781. assert interface is IFTPShell, "The realm is busted, jerk."
  782. self.shell = avatar
  783. self.logout = logout
  784. self.workingDirectory = []
  785. self.state = self.AUTHED
  786. return reply
  787. def _ebLogin(failure):
  788. failure.trap(cred_error.UnauthorizedLogin, cred_error.UnhandledCredentials)
  789. self.state = self.UNAUTH
  790. raise AuthorizationError
  791. d = self.portal.login(creds, None, IFTPShell)
  792. d.addCallbacks(_cbLogin, _ebLogin)
  793. return d
  794. def ftp_PASV(self):
  795. """
  796. Request for a passive connection
  797. from the rfc::
  798. This command requests the server-DTP to \"listen\" on a data port
  799. (which is not its default data port) and to wait for a connection
  800. rather than initiate one upon receipt of a transfer command. The
  801. response to this command includes the host and port address this
  802. server is listening on.
  803. """
  804. if self._epsvAll:
  805. return defer.fail(BadCmdSequenceError("may not send PASV after EPSV ALL"))
  806. host = self.transport.getHost().host
  807. try:
  808. address = ipaddress.IPv6Address(host)
  809. except ipaddress.AddressValueError:
  810. pass
  811. else:
  812. if address.ipv4_mapped is not None:
  813. # IPv4-mapped addresses are usable, but we need to make sure
  814. # they're encoded as IPv4 in the response.
  815. host = str(address.ipv4_mapped)
  816. else:
  817. # There's no standard defining the behaviour of PASV with
  818. # IPv6, so just claim it as unimplemented. (Some servers
  819. # return something like '0,0,0,0' in the host part of the
  820. # response in order that at least clients that ignore the
  821. # host part can work, and if it becomes necessary then we
  822. # could do that too.)
  823. return defer.fail(PASVIPv6NotImplementedError())
  824. # if we have a DTP port set up, lose it.
  825. if self.dtpFactory is not None:
  826. # cleanupDTP sets dtpFactory to none. Later we'll do
  827. # cleanup here or something.
  828. self.cleanupDTP()
  829. self.dtpFactory = DTPFactory(pi=self)
  830. self.dtpFactory.setTimeout(self.dtpTimeout)
  831. self.dtpPort = self.getDTPPort(self.dtpFactory)
  832. port = self.dtpPort.getHost().port
  833. self.reply(ENTERING_PASV_MODE, encodeHostPort(host, port))
  834. return self.dtpFactory.deferred.addCallback(lambda ign: None)
  835. def _validateNetworkProtocol(self, protocol):
  836. """
  837. Validate the network protocol requested in an EPRT or EPSV command.
  838. For now we just hardcode the protocols we support, since this layer
  839. doesn't have a good way to discover that.
  840. @param protocol: An address family number. See RFC 2428 section 2.
  841. @type protocol: L{str}
  842. @raise FTPCmdError: If validation fails.
  843. """
  844. # We can't actually honour an explicit network protocol request
  845. # (violating a SHOULD in RFC 2428 section 3), but let's at least
  846. # validate it.
  847. try:
  848. protocol = int(protocol)
  849. except ValueError:
  850. raise CmdArgSyntaxError(protocol)
  851. if protocol not in self._supportedNetworkProtocols:
  852. raise UnsupportedNetworkProtocolError(
  853. ",".join(str(p) for p in self._supportedNetworkProtocols)
  854. )
  855. def ftp_EPSV(self, protocol=""):
  856. """
  857. Extended request for a passive connection.
  858. As described by U{RFC 2428 section
  859. 3<https://tools.ietf.org/html/rfc2428#section-3>}::
  860. The EPSV command requests that a server listen on a data port
  861. and wait for a connection. The EPSV command takes an optional
  862. argument. The response to this command includes only the TCP
  863. port number of the listening connection. The format of the
  864. response, however, is similar to the argument of the EPRT
  865. command. This allows the same parsing routines to be used for
  866. both commands. In addition, the format leaves a place holder
  867. for the network protocol and/or network address, which may be
  868. needed in the EPSV response in the future.
  869. """
  870. if protocol == "ALL":
  871. self._epsvAll = True
  872. return EPSV_ALL_OK
  873. elif protocol:
  874. try:
  875. self._validateNetworkProtocol(protocol)
  876. except FTPCmdError:
  877. return defer.fail()
  878. # if we have a DTP port set up, lose it.
  879. if self.dtpFactory is not None:
  880. # cleanupDTP sets dtpFactory to none. Later we'll do
  881. # cleanup here or something.
  882. self.cleanupDTP()
  883. self.dtpFactory = DTPFactory(pi=self)
  884. self.dtpFactory.setTimeout(self.dtpTimeout)
  885. if not protocol or protocol == _AFNUM_IP6:
  886. interface = "::"
  887. else:
  888. interface = ""
  889. self.dtpPort = self.getDTPPort(self.dtpFactory, interface=interface)
  890. port = self.dtpPort.getHost().port
  891. self.reply(ENTERING_EPSV_MODE, port)
  892. return self.dtpFactory.deferred.addCallback(lambda ign: None)
  893. def ftp_PORT(self, address):
  894. if self._epsvAll:
  895. return defer.fail(BadCmdSequenceError("may not send PORT after EPSV ALL"))
  896. addr = tuple(map(int, address.split(",")))
  897. ip = "%d.%d.%d.%d" % tuple(addr[:4])
  898. port = addr[4] << 8 | addr[5]
  899. # if we have a DTP port set up, lose it.
  900. if self.dtpFactory is not None:
  901. self.cleanupDTP()
  902. self.dtpFactory = DTPFactory(pi=self, peerHost=self.transport.getPeer().host)
  903. self.dtpFactory.setTimeout(self.dtpTimeout)
  904. self.dtpPort = reactor.connectTCP(ip, port, self.dtpFactory)
  905. def connected(ignored):
  906. return ENTERING_PORT_MODE
  907. def connFailed(err):
  908. err.trap(PortConnectionError)
  909. return CANT_OPEN_DATA_CNX
  910. return self.dtpFactory.deferred.addCallbacks(connected, connFailed)
  911. def ftp_EPRT(self, extendedAddress):
  912. """
  913. Extended request for a data connection.
  914. As described by U{RFC 2428 section
  915. 2<https://tools.ietf.org/html/rfc2428#section-2>}::
  916. The EPRT command allows for the specification of an extended
  917. address for the data connection. The extended address MUST
  918. consist of the network protocol as well as the network and
  919. transport addresses.
  920. """
  921. if self._epsvAll:
  922. return defer.fail(BadCmdSequenceError("may not send EPRT after EPSV ALL"))
  923. try:
  924. protocol, ip, port = decodeExtendedAddress(extendedAddress)
  925. except ValueError:
  926. return defer.fail(CmdArgSyntaxError(extendedAddress))
  927. if protocol:
  928. try:
  929. self._validateNetworkProtocol(protocol)
  930. except FTPCmdError:
  931. return defer.fail()
  932. # if we have a DTP port set up, lose it.
  933. if self.dtpFactory is not None:
  934. self.cleanupDTP()
  935. self.dtpFactory = DTPFactory(pi=self, peerHost=self.transport.getPeer().host)
  936. self.dtpFactory.setTimeout(self.dtpTimeout)
  937. self.dtpPort = reactor.connectTCP(ip, port, self.dtpFactory)
  938. def connected(ignored):
  939. return ENTERING_PORT_MODE
  940. def connFailed(err):
  941. err.trap(PortConnectionError)
  942. return CANT_OPEN_DATA_CNX
  943. return self.dtpFactory.deferred.addCallbacks(connected, connFailed)
  944. def _encodeName(self, name):
  945. """
  946. Encode C{name} to be sent over the wire.
  947. This encodes L{unicode} objects as UTF-8 and leaves L{bytes} as-is.
  948. As described by U{RFC 3659 section
  949. 2.2<https://tools.ietf.org/html/rfc3659#section-2.2>}::
  950. Various FTP commands take pathnames as arguments, or return
  951. pathnames in responses. When the MLST command is supported, as
  952. indicated in the response to the FEAT command, pathnames are to be
  953. transferred in one of the following two formats.
  954. pathname = utf-8-name / raw
  955. utf-8-name = <a UTF-8 encoded Unicode string>
  956. raw = <any string that is not a valid UTF-8 encoding>
  957. Which format is used is at the option of the user-PI or server-PI
  958. sending the pathname.
  959. @param name: Name to be encoded.
  960. @type name: L{bytes} or L{unicode}
  961. @return: Wire format of C{name}.
  962. @rtype: L{bytes}
  963. """
  964. if isinstance(name, str):
  965. return name.encode("utf-8")
  966. return name
  967. def ftp_LIST(self, path=""):
  968. """This command causes a list to be sent from the server to the
  969. passive DTP. If the pathname specifies a directory or other
  970. group of files, the server should transfer a list of files
  971. in the specified directory. If the pathname specifies a
  972. file then the server should send current information on the
  973. file. A null argument implies the user's current working or
  974. default directory.
  975. """
  976. # XXX: why is this check different from ftp_RETR/ftp_STOR? See #4180
  977. if self.dtpInstance is None or not self.dtpInstance.isConnected:
  978. return defer.fail(BadCmdSequenceError("must send PORT or PASV before RETR"))
  979. # Various clients send flags like -L or -al etc. We just ignore them.
  980. if path.lower() in ["-a", "-l", "-la", "-al"]:
  981. path = ""
  982. def gotListing(results):
  983. self.reply(DATA_CNX_ALREADY_OPEN_START_XFR)
  984. for name, attrs in results:
  985. name = self._encodeName(name)
  986. self.dtpInstance.sendListResponse(name, attrs)
  987. self.dtpInstance.transport.loseConnection()
  988. return (TXFR_COMPLETE_OK,)
  989. try:
  990. segments = toSegments(self.workingDirectory, path)
  991. except InvalidPath:
  992. return defer.fail(FileNotFoundError(path))
  993. d = self.shell.list(
  994. segments,
  995. (
  996. "size",
  997. "directory",
  998. "permissions",
  999. "hardlinks",
  1000. "modified",
  1001. "owner",
  1002. "group",
  1003. ),
  1004. )
  1005. d.addCallback(gotListing)
  1006. return d
  1007. def ftp_NLST(self, path):
  1008. """
  1009. This command causes a directory listing to be sent from the server to
  1010. the client. The pathname should specify a directory or other
  1011. system-specific file group descriptor. An empty path implies the
  1012. current working directory. If the path is non-existent, send nothing.
  1013. If the path is to a file, send only the file name.
  1014. @type path: C{str}
  1015. @param path: The path for which a directory listing should be returned.
  1016. @rtype: L{Deferred}
  1017. @return: a L{Deferred} which will be fired when the listing request
  1018. is finished.
  1019. """
  1020. # XXX: why is this check different from ftp_RETR/ftp_STOR? See #4180
  1021. if self.dtpInstance is None or not self.dtpInstance.isConnected:
  1022. return defer.fail(BadCmdSequenceError("must send PORT or PASV before RETR"))
  1023. try:
  1024. segments = toSegments(self.workingDirectory, path)
  1025. except InvalidPath:
  1026. return defer.fail(FileNotFoundError(path))
  1027. def cbList(results, glob):
  1028. """
  1029. Send, line by line, each matching file in the directory listing,
  1030. and then close the connection.
  1031. @type results: A C{list} of C{tuple}. The first element of each
  1032. C{tuple} is a C{str} and the second element is a C{list}.
  1033. @param results: The names of the files in the directory.
  1034. @param glob: A shell-style glob through which to filter results
  1035. (see U{http://docs.python.org/2/library/fnmatch.html}), or
  1036. L{None} for no filtering.
  1037. @type glob: L{str} or L{None}
  1038. @return: A C{tuple} containing the status code for a successful
  1039. transfer.
  1040. @rtype: C{tuple}
  1041. """
  1042. self.reply(DATA_CNX_ALREADY_OPEN_START_XFR)
  1043. for name, ignored in results:
  1044. if not glob or (glob and fnmatch.fnmatch(name, glob)):
  1045. name = self._encodeName(name)
  1046. self.dtpInstance.sendLine(name)
  1047. self.dtpInstance.transport.loseConnection()
  1048. return (TXFR_COMPLETE_OK,)
  1049. def listErr(results):
  1050. """
  1051. RFC 959 specifies that an NLST request may only return directory
  1052. listings. Thus, send nothing and just close the connection.
  1053. @type results: L{Failure}
  1054. @param results: The L{Failure} wrapping a L{FileNotFoundError} that
  1055. occurred while trying to list the contents of a nonexistent
  1056. directory.
  1057. @returns: A C{tuple} containing the status code for a successful
  1058. transfer.
  1059. @rtype: C{tuple}
  1060. """
  1061. self.dtpInstance.transport.loseConnection()
  1062. return (TXFR_COMPLETE_OK,)
  1063. if _isGlobbingExpression(segments):
  1064. # Remove globbing expression from path
  1065. # and keep to be used for filtering.
  1066. glob = segments.pop()
  1067. else:
  1068. glob = None
  1069. d = self.shell.list(segments)
  1070. d.addCallback(cbList, glob)
  1071. # self.shell.list will generate an error if the path is invalid
  1072. d.addErrback(listErr)
  1073. return d
  1074. def ftp_CWD(self, path):
  1075. try:
  1076. segments = toSegments(self.workingDirectory, path)
  1077. except InvalidPath:
  1078. # XXX Eh, what to fail with here?
  1079. return defer.fail(FileNotFoundError(path))
  1080. def accessGranted(result):
  1081. self.workingDirectory = segments
  1082. return (REQ_FILE_ACTN_COMPLETED_OK,)
  1083. return self.shell.access(segments).addCallback(accessGranted)
  1084. def ftp_CDUP(self):
  1085. return self.ftp_CWD("..")
  1086. def ftp_PWD(self):
  1087. return (PWD_REPLY, "/" + "/".join(self.workingDirectory))
  1088. def ftp_RETR(self, path):
  1089. """
  1090. This command causes the content of a file to be sent over the data
  1091. transfer channel. If the path is to a folder, an error will be raised.
  1092. @type path: C{str}
  1093. @param path: The path to the file which should be transferred over the
  1094. data transfer channel.
  1095. @rtype: L{Deferred}
  1096. @return: a L{Deferred} which will be fired when the transfer is done.
  1097. """
  1098. if self.dtpInstance is None:
  1099. raise BadCmdSequenceError("PORT, PASV, EPRT, or EPSV required before RETR")
  1100. try:
  1101. newsegs = toSegments(self.workingDirectory, path)
  1102. except InvalidPath:
  1103. return defer.fail(FileNotFoundError(path))
  1104. # XXX For now, just disable the timeout. Later we'll want to
  1105. # leave it active and have the DTP connection reset it
  1106. # periodically.
  1107. self.setTimeout(None)
  1108. # Put it back later
  1109. def enableTimeout(result):
  1110. self.setTimeout(self.factory.timeOut)
  1111. return result
  1112. # And away she goes
  1113. if not self.binary:
  1114. cons = ASCIIConsumerWrapper(self.dtpInstance)
  1115. else:
  1116. cons = self.dtpInstance
  1117. def cbSent(result):
  1118. return (TXFR_COMPLETE_OK,)
  1119. def ebSent(err):
  1120. log.msg("Unexpected error attempting to transmit file to client:")
  1121. log.err(err)
  1122. if err.check(FTPCmdError):
  1123. return err
  1124. return (CNX_CLOSED_TXFR_ABORTED,)
  1125. def cbOpened(file):
  1126. # Tell them what to doooo
  1127. if self.dtpInstance.isConnected:
  1128. self.reply(DATA_CNX_ALREADY_OPEN_START_XFR)
  1129. else:
  1130. self.reply(FILE_STATUS_OK_OPEN_DATA_CNX)
  1131. d = file.send(cons)
  1132. d.addCallbacks(cbSent, ebSent)
  1133. return d
  1134. def ebOpened(err):
  1135. if not err.check(
  1136. PermissionDeniedError, FileNotFoundError, IsADirectoryError
  1137. ):
  1138. log.msg("Unexpected error attempting to open file for " "transmission:")
  1139. log.err(err)
  1140. if err.check(FTPCmdError):
  1141. return (err.value.errorCode, "/".join(newsegs))
  1142. return (FILE_NOT_FOUND, "/".join(newsegs))
  1143. d = self.shell.openForReading(newsegs)
  1144. d.addCallbacks(cbOpened, ebOpened)
  1145. d.addBoth(enableTimeout)
  1146. # Pass back Deferred that fires when the transfer is done
  1147. return d
  1148. def ftp_STOR(self, path):
  1149. """
  1150. STORE (STOR)
  1151. This command causes the server-DTP to accept the data
  1152. transferred via the data connection and to store the data as
  1153. a file at the server site. If the file specified in the
  1154. pathname exists at the server site, then its contents shall
  1155. be replaced by the data being transferred. A new file is
  1156. created at the server site if the file specified in the
  1157. pathname does not already exist.
  1158. """
  1159. if self.dtpInstance is None:
  1160. raise BadCmdSequenceError("PORT, PASV, EPRT, or EPSV required before STOR")
  1161. try:
  1162. newsegs = toSegments(self.workingDirectory, path)
  1163. except InvalidPath:
  1164. return defer.fail(FileNotFoundError(path))
  1165. # XXX For now, just disable the timeout. Later we'll want to
  1166. # leave it active and have the DTP connection reset it
  1167. # periodically.
  1168. self.setTimeout(None)
  1169. # Put it back later
  1170. def enableTimeout(result):
  1171. self.setTimeout(self.factory.timeOut)
  1172. return result
  1173. def cbOpened(file):
  1174. """
  1175. File was open for reading. Launch the data transfer channel via
  1176. the file consumer.
  1177. """
  1178. d = file.receive()
  1179. d.addCallback(cbConsumer)
  1180. d.addCallback(lambda ignored: file.close())
  1181. d.addCallbacks(cbSent, ebSent)
  1182. return d
  1183. def ebOpened(err):
  1184. """
  1185. Called when failed to open the file for reading.
  1186. For known errors, return the FTP error code.
  1187. For all other, return a file not found error.
  1188. """
  1189. if isinstance(err.value, FTPCmdError):
  1190. return (err.value.errorCode, "/".join(newsegs))
  1191. log.err(err, "Unexpected error received while opening file:")
  1192. return (FILE_NOT_FOUND, "/".join(newsegs))
  1193. def cbConsumer(cons):
  1194. """
  1195. Called after the file was opended for reading.
  1196. Prepare the data transfer channel and send the response
  1197. to the command channel.
  1198. """
  1199. if not self.binary:
  1200. cons = ASCIIConsumerWrapper(cons)
  1201. d = self.dtpInstance.registerConsumer(cons)
  1202. # Tell them what to doooo
  1203. if self.dtpInstance.isConnected:
  1204. self.reply(DATA_CNX_ALREADY_OPEN_START_XFR)
  1205. else:
  1206. self.reply(FILE_STATUS_OK_OPEN_DATA_CNX)
  1207. return d
  1208. def cbSent(result):
  1209. """
  1210. Called from data transport when transfer is done.
  1211. """
  1212. return (TXFR_COMPLETE_OK,)
  1213. def ebSent(err):
  1214. """
  1215. Called from data transport when there are errors during the
  1216. transfer.
  1217. """
  1218. log.err(err, "Unexpected error received during transfer:")
  1219. if err.check(FTPCmdError):
  1220. return err
  1221. return (CNX_CLOSED_TXFR_ABORTED,)
  1222. d = self.shell.openForWriting(newsegs)
  1223. d.addCallbacks(cbOpened, ebOpened)
  1224. d.addBoth(enableTimeout)
  1225. # Pass back Deferred that fires when the transfer is done
  1226. return d
  1227. def ftp_SIZE(self, path):
  1228. """
  1229. File SIZE
  1230. The FTP command, SIZE OF FILE (SIZE), is used to obtain the transfer
  1231. size of a file from the server-FTP process. This is the exact number
  1232. of octets (8 bit bytes) that would be transmitted over the data
  1233. connection should that file be transmitted. This value will change
  1234. depending on the current STRUcture, MODE, and TYPE of the data
  1235. connection or of a data connection that would be created were one
  1236. created now. Thus, the result of the SIZE command is dependent on
  1237. the currently established STRU, MODE, and TYPE parameters.
  1238. The SIZE command returns how many octets would be transferred if the
  1239. file were to be transferred using the current transfer structure,
  1240. mode, and type. This command is normally used in conjunction with
  1241. the RESTART (REST) command when STORing a file to a remote server in
  1242. STREAM mode, to determine the restart point. The server-PI might
  1243. need to read the partially transferred file, do any appropriate
  1244. conversion, and count the number of octets that would be generated
  1245. when sending the file in order to correctly respond to this command.
  1246. Estimates of the file transfer size MUST NOT be returned; only
  1247. precise information is acceptable.
  1248. http://tools.ietf.org/html/rfc3659
  1249. """
  1250. try:
  1251. newsegs = toSegments(self.workingDirectory, path)
  1252. except InvalidPath:
  1253. return defer.fail(FileNotFoundError(path))
  1254. def cbStat(result):
  1255. (size,) = result
  1256. return (FILE_STATUS, str(size))
  1257. return self.shell.stat(newsegs, ("size",)).addCallback(cbStat)
  1258. def ftp_MDTM(self, path):
  1259. """
  1260. File Modification Time (MDTM)
  1261. The FTP command, MODIFICATION TIME (MDTM), can be used to determine
  1262. when a file in the server NVFS was last modified. This command has
  1263. existed in many FTP servers for many years, as an adjunct to the REST
  1264. command for STREAM mode, thus is widely available. However, where
  1265. supported, the "modify" fact that can be provided in the result from
  1266. the new MLST command is recommended as a superior alternative.
  1267. http://tools.ietf.org/html/rfc3659
  1268. """
  1269. try:
  1270. newsegs = toSegments(self.workingDirectory, path)
  1271. except InvalidPath:
  1272. return defer.fail(FileNotFoundError(path))
  1273. def cbStat(result):
  1274. (modified,) = result
  1275. return (FILE_STATUS, time.strftime("%Y%m%d%H%M%S", time.gmtime(modified)))
  1276. return self.shell.stat(newsegs, ("modified",)).addCallback(cbStat)
  1277. def ftp_TYPE(self, type):
  1278. """
  1279. REPRESENTATION TYPE (TYPE)
  1280. The argument specifies the representation type as described
  1281. in the Section on Data Representation and Storage. Several
  1282. types take a second parameter. The first parameter is
  1283. denoted by a single Telnet character, as is the second
  1284. Format parameter for ASCII and EBCDIC; the second parameter
  1285. for local byte is a decimal integer to indicate Bytesize.
  1286. The parameters are separated by a <SP> (Space, ASCII code
  1287. 32).
  1288. """
  1289. p = type.upper()
  1290. if p:
  1291. f = getattr(self, "type_" + p[0], None)
  1292. if f is not None:
  1293. return f(p[1:])
  1294. return self.type_UNKNOWN(p)
  1295. return (SYNTAX_ERR,)
  1296. def type_A(self, code):
  1297. if code == "" or code == "N":
  1298. self.binary = False
  1299. return (TYPE_SET_OK, "A" + code)
  1300. else:
  1301. return defer.fail(CmdArgSyntaxError(code))
  1302. def type_I(self, code):
  1303. if code == "":
  1304. self.binary = True
  1305. return (TYPE_SET_OK, "I")
  1306. else:
  1307. return defer.fail(CmdArgSyntaxError(code))
  1308. def type_UNKNOWN(self, code):
  1309. return defer.fail(CmdNotImplementedForArgError(code))
  1310. def ftp_SYST(self):
  1311. return NAME_SYS_TYPE
  1312. def ftp_STRU(self, structure):
  1313. p = structure.upper()
  1314. if p == "F":
  1315. return (CMD_OK,)
  1316. return defer.fail(CmdNotImplementedForArgError(structure))
  1317. def ftp_MODE(self, mode):
  1318. p = mode.upper()
  1319. if p == "S":
  1320. return (CMD_OK,)
  1321. return defer.fail(CmdNotImplementedForArgError(mode))
  1322. def ftp_MKD(self, path):
  1323. try:
  1324. newsegs = toSegments(self.workingDirectory, path)
  1325. except InvalidPath:
  1326. return defer.fail(FileNotFoundError(path))
  1327. return self.shell.makeDirectory(newsegs).addCallback(
  1328. lambda ign: (MKD_REPLY, path)
  1329. )
  1330. def ftp_RMD(self, path):
  1331. try:
  1332. newsegs = toSegments(self.workingDirectory, path)
  1333. except InvalidPath:
  1334. return defer.fail(FileNotFoundError(path))
  1335. return self.shell.removeDirectory(newsegs).addCallback(
  1336. lambda ign: (REQ_FILE_ACTN_COMPLETED_OK,)
  1337. )
  1338. def ftp_DELE(self, path):
  1339. try:
  1340. newsegs = toSegments(self.workingDirectory, path)
  1341. except InvalidPath:
  1342. return defer.fail(FileNotFoundError(path))
  1343. return self.shell.removeFile(newsegs).addCallback(
  1344. lambda ign: (REQ_FILE_ACTN_COMPLETED_OK,)
  1345. )
  1346. def ftp_NOOP(self):
  1347. return (CMD_OK,)
  1348. def ftp_RNFR(self, fromName):
  1349. self._fromName = fromName
  1350. self.state = self.RENAMING
  1351. return (REQ_FILE_ACTN_PENDING_FURTHER_INFO,)
  1352. def ftp_RNTO(self, toName):
  1353. fromName = self._fromName
  1354. del self._fromName
  1355. self.state = self.AUTHED
  1356. try:
  1357. fromsegs = toSegments(self.workingDirectory, fromName)
  1358. tosegs = toSegments(self.workingDirectory, toName)
  1359. except InvalidPath:
  1360. return defer.fail(FileNotFoundError(fromName))
  1361. return self.shell.rename(fromsegs, tosegs).addCallback(
  1362. lambda ign: (REQ_FILE_ACTN_COMPLETED_OK,)
  1363. )
  1364. def ftp_FEAT(self):
  1365. """
  1366. Advertise the features supported by the server.
  1367. http://tools.ietf.org/html/rfc2389
  1368. """
  1369. self.sendLine(RESPONSE[FEAT_OK][0])
  1370. for feature in self.FEATURES:
  1371. self.sendLine(" " + feature)
  1372. self.sendLine(RESPONSE[FEAT_OK][1])
  1373. def ftp_OPTS(self, option):
  1374. """
  1375. Handle OPTS command.
  1376. http://tools.ietf.org/html/draft-ietf-ftpext-utf-8-option-00
  1377. """
  1378. return self.reply(OPTS_NOT_IMPLEMENTED, option)
  1379. def ftp_QUIT(self):
  1380. self.reply(GOODBYE_MSG)
  1381. self.transport.loseConnection()
  1382. self.disconnected = True
  1383. def cleanupDTP(self):
  1384. """
  1385. Call when DTP connection exits
  1386. """
  1387. log.msg("cleanupDTP", debug=True)
  1388. log.msg(self.dtpPort)
  1389. dtpPort, self.dtpPort = self.dtpPort, None
  1390. if interfaces.IListeningPort.providedBy(dtpPort):
  1391. dtpPort.stopListening()
  1392. elif interfaces.IConnector.providedBy(dtpPort):
  1393. dtpPort.disconnect()
  1394. else:
  1395. assert False, (
  1396. "dtpPort should be an IListeningPort or IConnector, "
  1397. "instead is %r" % (dtpPort,)
  1398. )
  1399. self.dtpFactory.stopFactory()
  1400. self.dtpFactory = None
  1401. if self.dtpInstance is not None:
  1402. self.dtpInstance = None
  1403. class FTPFactory(policies.LimitTotalConnectionsFactory):
  1404. """
  1405. A factory for producing ftp protocol instances
  1406. @ivar timeOut: the protocol interpreter's idle timeout time in seconds,
  1407. default is 600 seconds.
  1408. @ivar passivePortRange: value forwarded to C{protocol.passivePortRange}.
  1409. @type passivePortRange: C{iterator}
  1410. """
  1411. protocol = FTP
  1412. overflowProtocol = FTPOverflowProtocol
  1413. allowAnonymous = True
  1414. userAnonymous = "anonymous"
  1415. timeOut = 600
  1416. welcomeMessage = f"Twisted {copyright.version} FTP Server"
  1417. passivePortRange = range(0, 1)
  1418. def __init__(self, portal=None, userAnonymous="anonymous"):
  1419. self.portal = portal
  1420. self.userAnonymous = userAnonymous
  1421. self.instances = []
  1422. def buildProtocol(self, addr):
  1423. p = policies.LimitTotalConnectionsFactory.buildProtocol(self, addr)
  1424. if p is not None:
  1425. p.wrappedProtocol.portal = self.portal
  1426. p.wrappedProtocol.timeOut = self.timeOut
  1427. p.wrappedProtocol.passivePortRange = self.passivePortRange
  1428. return p
  1429. def stopFactory(self):
  1430. # make sure ftp instance's timeouts are set to None
  1431. # to avoid reactor complaints
  1432. [p.setTimeout(None) for p in self.instances if p.timeOut is not None]
  1433. policies.LimitTotalConnectionsFactory.stopFactory(self)
  1434. # -- Cred Objects --
  1435. class IFTPShell(Interface):
  1436. """
  1437. An abstraction of the shell commands used by the FTP protocol for
  1438. a given user account.
  1439. All path names must be absolute.
  1440. """
  1441. def makeDirectory(path):
  1442. """
  1443. Create a directory.
  1444. @param path: The path, as a list of segments, to create
  1445. @type path: C{list} of C{unicode}
  1446. @return: A Deferred which fires when the directory has been
  1447. created, or which fails if the directory cannot be created.
  1448. """
  1449. def removeDirectory(path):
  1450. """
  1451. Remove a directory.
  1452. @param path: The path, as a list of segments, to remove
  1453. @type path: C{list} of C{unicode}
  1454. @return: A Deferred which fires when the directory has been
  1455. removed, or which fails if the directory cannot be removed.
  1456. """
  1457. def removeFile(path):
  1458. """
  1459. Remove a file.
  1460. @param path: The path, as a list of segments, to remove
  1461. @type path: C{list} of C{unicode}
  1462. @return: A Deferred which fires when the file has been
  1463. removed, or which fails if the file cannot be removed.
  1464. """
  1465. def rename(fromPath, toPath):
  1466. """
  1467. Rename a file or directory.
  1468. @param fromPath: The current name of the path.
  1469. @type fromPath: C{list} of C{unicode}
  1470. @param toPath: The desired new name of the path.
  1471. @type toPath: C{list} of C{unicode}
  1472. @return: A Deferred which fires when the path has been
  1473. renamed, or which fails if the path cannot be renamed.
  1474. """
  1475. def access(path):
  1476. """
  1477. Determine whether access to the given path is allowed.
  1478. @param path: The path, as a list of segments
  1479. @return: A Deferred which fires with None if access is allowed
  1480. or which fails with a specific exception type if access is
  1481. denied.
  1482. """
  1483. def stat(path, keys=()):
  1484. """
  1485. Retrieve information about the given path.
  1486. This is like list, except it will never return results about
  1487. child paths.
  1488. """
  1489. def list(path, keys=()):
  1490. """
  1491. Retrieve information about the given path.
  1492. If the path represents a non-directory, the result list should
  1493. have only one entry with information about that non-directory.
  1494. Otherwise, the result list should have an element for each
  1495. child of the directory.
  1496. @param path: The path, as a list of segments, to list
  1497. @type path: C{list} of C{unicode} or C{bytes}
  1498. @param keys: A tuple of keys desired in the resulting
  1499. dictionaries.
  1500. @return: A Deferred which fires with a list of (name, list),
  1501. where the name is the name of the entry as a unicode string or
  1502. bytes and each list contains values corresponding to the requested
  1503. keys. The following are possible elements of keys, and the
  1504. values which should be returned for them:
  1505. - C{'size'}: size in bytes, as an integer (this is kinda required)
  1506. - C{'directory'}: boolean indicating the type of this entry
  1507. - C{'permissions'}: a bitvector (see os.stat(foo).st_mode)
  1508. - C{'hardlinks'}: Number of hard links to this entry
  1509. - C{'modified'}: number of seconds since the epoch since entry was
  1510. modified
  1511. - C{'owner'}: string indicating the user owner of this entry
  1512. - C{'group'}: string indicating the group owner of this entry
  1513. """
  1514. def openForReading(path):
  1515. """
  1516. @param path: The path, as a list of segments, to open
  1517. @type path: C{list} of C{unicode}
  1518. @rtype: C{Deferred} which will fire with L{IReadFile}
  1519. """
  1520. def openForWriting(path):
  1521. """
  1522. @param path: The path, as a list of segments, to open
  1523. @type path: C{list} of C{unicode}
  1524. @rtype: C{Deferred} which will fire with L{IWriteFile}
  1525. """
  1526. class IReadFile(Interface):
  1527. """
  1528. A file out of which bytes may be read.
  1529. """
  1530. def send(consumer):
  1531. """
  1532. Produce the contents of the given path to the given consumer. This
  1533. method may only be invoked once on each provider.
  1534. @type consumer: C{IConsumer}
  1535. @return: A Deferred which fires when the file has been
  1536. consumed completely.
  1537. """
  1538. class IWriteFile(Interface):
  1539. """
  1540. A file into which bytes may be written.
  1541. """
  1542. def receive():
  1543. """
  1544. Create a consumer which will write to this file. This method may
  1545. only be invoked once on each provider.
  1546. @rtype: C{Deferred} of C{IConsumer}
  1547. """
  1548. def close():
  1549. """
  1550. Perform any post-write work that needs to be done. This method may
  1551. only be invoked once on each provider, and will always be invoked
  1552. after receive().
  1553. @rtype: C{Deferred} of anything: the value is ignored. The FTP client
  1554. will not see their upload request complete until this Deferred has
  1555. been fired.
  1556. """
  1557. def _getgroups(uid):
  1558. """
  1559. Return the primary and supplementary groups for the given UID.
  1560. @type uid: C{int}
  1561. """
  1562. result = []
  1563. pwent = pwd.getpwuid(uid)
  1564. result.append(pwent.pw_gid)
  1565. for grent in grp.getgrall():
  1566. if pwent.pw_name in grent.gr_mem:
  1567. result.append(grent.gr_gid)
  1568. return result
  1569. def _testPermissions(uid, gid, spath, mode="r"):
  1570. """
  1571. checks to see if uid has proper permissions to access path with mode
  1572. @type uid: C{int}
  1573. @param uid: numeric user id
  1574. @type gid: C{int}
  1575. @param gid: numeric group id
  1576. @type spath: C{str}
  1577. @param spath: the path on the server to test
  1578. @type mode: C{str}
  1579. @param mode: 'r' or 'w' (read or write)
  1580. @rtype: C{bool}
  1581. @return: True if the given credentials have the specified form of
  1582. access to the given path
  1583. """
  1584. if mode == "r":
  1585. usr = stat.S_IRUSR
  1586. grp = stat.S_IRGRP
  1587. oth = stat.S_IROTH
  1588. amode = os.R_OK
  1589. elif mode == "w":
  1590. usr = stat.S_IWUSR
  1591. grp = stat.S_IWGRP
  1592. oth = stat.S_IWOTH
  1593. amode = os.W_OK
  1594. else:
  1595. raise ValueError(f"Invalid mode {mode!r}: must specify 'r' or 'w'")
  1596. access = False
  1597. if os.path.exists(spath):
  1598. if uid == 0:
  1599. access = True
  1600. else:
  1601. s = os.stat(spath)
  1602. if usr & s.st_mode and uid == s.st_uid:
  1603. access = True
  1604. elif grp & s.st_mode and gid in _getgroups(uid):
  1605. access = True
  1606. elif oth & s.st_mode:
  1607. access = True
  1608. if access:
  1609. if not os.access(spath, amode):
  1610. access = False
  1611. log.msg(
  1612. "Filesystem grants permission to UID %d but it is "
  1613. "inaccessible to me running as UID %d" % (uid, os.getuid())
  1614. )
  1615. return access
  1616. @implementer(IFTPShell)
  1617. class FTPAnonymousShell:
  1618. """
  1619. An anonymous implementation of IFTPShell
  1620. @type filesystemRoot: L{twisted.python.filepath.FilePath}
  1621. @ivar filesystemRoot: The path which is considered the root of
  1622. this shell.
  1623. """
  1624. def __init__(self, filesystemRoot):
  1625. self.filesystemRoot = filesystemRoot
  1626. def _path(self, path):
  1627. return self.filesystemRoot.descendant(path)
  1628. def makeDirectory(self, path):
  1629. return defer.fail(AnonUserDeniedError())
  1630. def removeDirectory(self, path):
  1631. return defer.fail(AnonUserDeniedError())
  1632. def removeFile(self, path):
  1633. return defer.fail(AnonUserDeniedError())
  1634. def rename(self, fromPath, toPath):
  1635. return defer.fail(AnonUserDeniedError())
  1636. def receive(self, path):
  1637. path = self._path(path)
  1638. return defer.fail(AnonUserDeniedError())
  1639. def openForReading(self, path):
  1640. """
  1641. Open C{path} for reading.
  1642. @param path: The path, as a list of segments, to open.
  1643. @type path: C{list} of C{unicode}
  1644. @return: A L{Deferred} is returned that will fire with an object
  1645. implementing L{IReadFile} if the file is successfully opened. If
  1646. C{path} is a directory, or if an exception is raised while trying
  1647. to open the file, the L{Deferred} will fire with an error.
  1648. """
  1649. p = self._path(path)
  1650. if p.isdir():
  1651. # Normally, we would only check for EISDIR in open, but win32
  1652. # returns EACCES in this case, so we check before
  1653. return defer.fail(IsADirectoryError(path))
  1654. try:
  1655. f = p.open("r")
  1656. except OSError as e:
  1657. return errnoToFailure(e.errno, path)
  1658. except BaseException:
  1659. return defer.fail()
  1660. else:
  1661. return defer.succeed(_FileReader(f))
  1662. def openForWriting(self, path):
  1663. """
  1664. Reject write attempts by anonymous users with
  1665. L{PermissionDeniedError}.
  1666. """
  1667. return defer.fail(PermissionDeniedError("STOR not allowed"))
  1668. def access(self, path):
  1669. p = self._path(path)
  1670. if not p.exists():
  1671. # Again, win32 doesn't report a sane error after, so let's fail
  1672. # early if we can
  1673. return defer.fail(FileNotFoundError(path))
  1674. # For now, just see if we can os.listdir() it
  1675. try:
  1676. p.listdir()
  1677. except OSError as e:
  1678. return errnoToFailure(e.errno, path)
  1679. except BaseException:
  1680. return defer.fail()
  1681. else:
  1682. return defer.succeed(None)
  1683. def stat(self, path, keys=()):
  1684. p = self._path(path)
  1685. if p.isdir():
  1686. try:
  1687. statResult = self._statNode(p, keys)
  1688. except OSError as e:
  1689. return errnoToFailure(e.errno, path)
  1690. except BaseException:
  1691. return defer.fail()
  1692. else:
  1693. return defer.succeed(statResult)
  1694. else:
  1695. return self.list(path, keys).addCallback(lambda res: res[0][1])
  1696. def list(self, path, keys=()):
  1697. """
  1698. Return the list of files at given C{path}, adding C{keys} stat
  1699. informations if specified.
  1700. @param path: the directory or file to check.
  1701. @type path: C{str}
  1702. @param keys: the list of desired metadata
  1703. @type keys: C{list} of C{str}
  1704. """
  1705. filePath = self._path(path)
  1706. if filePath.isdir():
  1707. entries = filePath.listdir()
  1708. fileEntries = [filePath.child(p) for p in entries]
  1709. elif filePath.isfile():
  1710. entries = [os.path.join(*filePath.segmentsFrom(self.filesystemRoot))]
  1711. fileEntries = [filePath]
  1712. else:
  1713. return defer.fail(FileNotFoundError(path))
  1714. results = []
  1715. for fileName, filePath in zip(entries, fileEntries):
  1716. ent = []
  1717. results.append((fileName, ent))
  1718. if keys:
  1719. try:
  1720. ent.extend(self._statNode(filePath, keys))
  1721. except OSError as e:
  1722. return errnoToFailure(e.errno, fileName)
  1723. except BaseException:
  1724. return defer.fail()
  1725. return defer.succeed(results)
  1726. def _statNode(self, filePath, keys):
  1727. """
  1728. Shortcut method to get stat info on a node.
  1729. @param filePath: the node to stat.
  1730. @type filePath: C{filepath.FilePath}
  1731. @param keys: the stat keys to get.
  1732. @type keys: C{iterable}
  1733. """
  1734. filePath.restat()
  1735. return [getattr(self, "_stat_" + k)(filePath) for k in keys]
  1736. def _stat_size(self, fp):
  1737. """
  1738. Get the filepath's size as an int
  1739. @param fp: L{twisted.python.filepath.FilePath}
  1740. @return: C{int} representing the size
  1741. """
  1742. return fp.getsize()
  1743. def _stat_permissions(self, fp):
  1744. """
  1745. Get the filepath's permissions object
  1746. @param fp: L{twisted.python.filepath.FilePath}
  1747. @return: L{twisted.python.filepath.Permissions} of C{fp}
  1748. """
  1749. return fp.getPermissions()
  1750. def _stat_hardlinks(self, fp):
  1751. """
  1752. Get the number of hardlinks for the filepath - if the number of
  1753. hardlinks is not yet implemented (say in Windows), just return 0 since
  1754. stat-ing a file in Windows seems to return C{st_nlink=0}.
  1755. (Reference:
  1756. U{http://stackoverflow.com/questions/5275731/os-stat-on-windows})
  1757. @param fp: L{twisted.python.filepath.FilePath}
  1758. @return: C{int} representing the number of hardlinks
  1759. """
  1760. try:
  1761. return fp.getNumberOfHardLinks()
  1762. except NotImplementedError:
  1763. return 0
  1764. def _stat_modified(self, fp):
  1765. """
  1766. Get the filepath's last modified date
  1767. @param fp: L{twisted.python.filepath.FilePath}
  1768. @return: C{int} as seconds since the epoch
  1769. """
  1770. return fp.getModificationTime()
  1771. def _stat_owner(self, fp):
  1772. """
  1773. Get the filepath's owner's username. If this is not implemented
  1774. (say in Windows) return the string "0" since stat-ing a file in
  1775. Windows seems to return C{st_uid=0}.
  1776. (Reference:
  1777. U{http://stackoverflow.com/questions/5275731/os-stat-on-windows})
  1778. @param fp: L{twisted.python.filepath.FilePath}
  1779. @return: C{str} representing the owner's username
  1780. """
  1781. try:
  1782. userID = fp.getUserID()
  1783. except NotImplementedError:
  1784. return "0"
  1785. else:
  1786. if pwd is not None:
  1787. try:
  1788. return pwd.getpwuid(userID)[0]
  1789. except KeyError:
  1790. pass
  1791. return str(userID)
  1792. def _stat_group(self, fp):
  1793. """
  1794. Get the filepath's owner's group. If this is not implemented
  1795. (say in Windows) return the string "0" since stat-ing a file in
  1796. Windows seems to return C{st_gid=0}.
  1797. (Reference:
  1798. U{http://stackoverflow.com/questions/5275731/os-stat-on-windows})
  1799. @param fp: L{twisted.python.filepath.FilePath}
  1800. @return: C{str} representing the owner's group
  1801. """
  1802. try:
  1803. groupID = fp.getGroupID()
  1804. except NotImplementedError:
  1805. return "0"
  1806. else:
  1807. if grp is not None:
  1808. try:
  1809. return grp.getgrgid(groupID)[0]
  1810. except KeyError:
  1811. pass
  1812. return str(groupID)
  1813. def _stat_directory(self, fp):
  1814. """
  1815. Get whether the filepath is a directory
  1816. @param fp: L{twisted.python.filepath.FilePath}
  1817. @return: C{bool}
  1818. """
  1819. return fp.isdir()
  1820. @implementer(IReadFile)
  1821. class _FileReader:
  1822. def __init__(self, fObj):
  1823. self.fObj = fObj
  1824. self._send = False
  1825. def _close(self, passthrough):
  1826. self._send = True
  1827. self.fObj.close()
  1828. return passthrough
  1829. def send(self, consumer):
  1830. assert not self._send, "Can only call IReadFile.send *once* per instance"
  1831. self._send = True
  1832. d = basic.FileSender().beginFileTransfer(self.fObj, consumer)
  1833. d.addBoth(self._close)
  1834. return d
  1835. class FTPShell(FTPAnonymousShell):
  1836. """
  1837. An authenticated implementation of L{IFTPShell}.
  1838. """
  1839. def makeDirectory(self, path):
  1840. p = self._path(path)
  1841. try:
  1842. p.makedirs()
  1843. except OSError as e:
  1844. return errnoToFailure(e.errno, path)
  1845. except BaseException:
  1846. return defer.fail()
  1847. else:
  1848. return defer.succeed(None)
  1849. def removeDirectory(self, path):
  1850. p = self._path(path)
  1851. if p.isfile():
  1852. # Win32 returns the wrong errno when rmdir is called on a file
  1853. # instead of a directory, so as we have the info here, let's fail
  1854. # early with a pertinent error
  1855. return defer.fail(IsNotADirectoryError(path))
  1856. try:
  1857. os.rmdir(p.path)
  1858. except OSError as e:
  1859. return errnoToFailure(e.errno, path)
  1860. except BaseException:
  1861. return defer.fail()
  1862. else:
  1863. return defer.succeed(None)
  1864. def removeFile(self, path):
  1865. p = self._path(path)
  1866. if p.isdir():
  1867. # Win32 returns the wrong errno when remove is called on a
  1868. # directory instead of a file, so as we have the info here,
  1869. # let's fail early with a pertinent error
  1870. return defer.fail(IsADirectoryError(path))
  1871. try:
  1872. p.remove()
  1873. except OSError as e:
  1874. return errnoToFailure(e.errno, path)
  1875. except BaseException:
  1876. return defer.fail()
  1877. else:
  1878. return defer.succeed(None)
  1879. def rename(self, fromPath, toPath):
  1880. fp = self._path(fromPath)
  1881. tp = self._path(toPath)
  1882. try:
  1883. os.rename(fp.path, tp.path)
  1884. except OSError as e:
  1885. return errnoToFailure(e.errno, fromPath)
  1886. except BaseException:
  1887. return defer.fail()
  1888. else:
  1889. return defer.succeed(None)
  1890. def openForWriting(self, path):
  1891. """
  1892. Open C{path} for writing.
  1893. @param path: The path, as a list of segments, to open.
  1894. @type path: C{list} of C{unicode}
  1895. @return: A L{Deferred} is returned that will fire with an object
  1896. implementing L{IWriteFile} if the file is successfully opened. If
  1897. C{path} is a directory, or if an exception is raised while trying
  1898. to open the file, the L{Deferred} will fire with an error.
  1899. """
  1900. p = self._path(path)
  1901. if p.isdir():
  1902. # Normally, we would only check for EISDIR in open, but win32
  1903. # returns EACCES in this case, so we check before
  1904. return defer.fail(IsADirectoryError(path))
  1905. try:
  1906. fObj = p.open("w")
  1907. except OSError as e:
  1908. return errnoToFailure(e.errno, path)
  1909. except BaseException:
  1910. return defer.fail()
  1911. return defer.succeed(_FileWriter(fObj))
  1912. @implementer(IWriteFile)
  1913. class _FileWriter:
  1914. def __init__(self, fObj):
  1915. self.fObj = fObj
  1916. self._receive = False
  1917. def receive(self):
  1918. assert not self._receive, "Can only call IWriteFile.receive *once* per instance"
  1919. self._receive = True
  1920. # FileConsumer will close the file object
  1921. return defer.succeed(FileConsumer(self.fObj))
  1922. def close(self):
  1923. return defer.succeed(None)
  1924. @implementer(portal.IRealm)
  1925. class BaseFTPRealm:
  1926. """
  1927. Base class for simple FTP realms which provides an easy hook for specifying
  1928. the home directory for each user.
  1929. """
  1930. def __init__(self, anonymousRoot):
  1931. self.anonymousRoot = filepath.FilePath(anonymousRoot)
  1932. def getHomeDirectory(self, avatarId):
  1933. """
  1934. Return a L{FilePath} representing the home directory of the given
  1935. avatar. Override this in a subclass.
  1936. @param avatarId: A user identifier returned from a credentials checker.
  1937. @type avatarId: C{str}
  1938. @rtype: L{FilePath}
  1939. """
  1940. raise NotImplementedError(
  1941. f"{self.__class__!r} did not override getHomeDirectory"
  1942. )
  1943. def requestAvatar(self, avatarId, mind, *interfaces):
  1944. for iface in interfaces:
  1945. if iface is IFTPShell:
  1946. if avatarId is checkers.ANONYMOUS:
  1947. avatar = FTPAnonymousShell(self.anonymousRoot)
  1948. else:
  1949. avatar = FTPShell(self.getHomeDirectory(avatarId))
  1950. return (IFTPShell, avatar, getattr(avatar, "logout", lambda: None))
  1951. raise NotImplementedError("Only IFTPShell interface is supported by this realm")
  1952. class FTPRealm(BaseFTPRealm):
  1953. """
  1954. @type anonymousRoot: L{twisted.python.filepath.FilePath}
  1955. @ivar anonymousRoot: Root of the filesystem to which anonymous
  1956. users will be granted access.
  1957. @type userHome: L{filepath.FilePath}
  1958. @ivar userHome: Root of the filesystem containing user home directories.
  1959. """
  1960. def __init__(self, anonymousRoot, userHome="/home"):
  1961. BaseFTPRealm.__init__(self, anonymousRoot)
  1962. self.userHome = filepath.FilePath(userHome)
  1963. def getHomeDirectory(self, avatarId):
  1964. """
  1965. Use C{avatarId} as a single path segment to construct a child of
  1966. C{self.userHome} and return that child.
  1967. """
  1968. return self.userHome.child(avatarId)
  1969. class SystemFTPRealm(BaseFTPRealm):
  1970. """
  1971. L{SystemFTPRealm} uses system user account information to decide what the
  1972. home directory for a particular avatarId is.
  1973. This works on POSIX but probably is not reliable on Windows.
  1974. """
  1975. def getHomeDirectory(self, avatarId):
  1976. """
  1977. Return the system-defined home directory of the system user account
  1978. with the name C{avatarId}.
  1979. """
  1980. path = os.path.expanduser("~" + avatarId)
  1981. if path.startswith("~"):
  1982. raise cred_error.UnauthorizedLogin()
  1983. return filepath.FilePath(path)
  1984. # --- FTP CLIENT -------------------------------------------------------------
  1985. ####
  1986. # And now for the client...
  1987. # Notes:
  1988. # * Reference: http://cr.yp.to/ftp.html
  1989. # * FIXME: Does not support pipelining (which is not supported by all
  1990. # servers anyway). This isn't a functionality limitation, just a
  1991. # small performance issue.
  1992. # * Only has a rudimentary understanding of FTP response codes (although
  1993. # the full response is passed to the caller if they so choose).
  1994. # * Assumes that USER and PASS should always be sent
  1995. # * Always sets TYPE I (binary mode)
  1996. # * Doesn't understand any of the weird, obscure TELNET stuff (\377...)
  1997. # * FIXME: Doesn't share any code with the FTPServer
  1998. class ConnectionLost(FTPError):
  1999. pass
  2000. class CommandFailed(FTPError):
  2001. pass
  2002. class BadResponse(FTPError):
  2003. pass
  2004. class UnexpectedResponse(FTPError):
  2005. pass
  2006. class UnexpectedData(FTPError):
  2007. pass
  2008. class FTPCommand:
  2009. def __init__(self, text=None, public=0):
  2010. self.text = text
  2011. self.deferred = defer.Deferred()
  2012. self.ready = 1
  2013. self.public = public
  2014. self.transferDeferred = None
  2015. def fail(self, failure):
  2016. if self.public:
  2017. self.deferred.errback(failure)
  2018. class ProtocolWrapper(protocol.Protocol):
  2019. def __init__(self, original, deferred):
  2020. self.original = original
  2021. self.deferred = deferred
  2022. def makeConnection(self, transport):
  2023. self.original.makeConnection(transport)
  2024. def dataReceived(self, data):
  2025. self.original.dataReceived(data)
  2026. def connectionLost(self, reason):
  2027. self.original.connectionLost(reason)
  2028. # Signal that transfer has completed
  2029. self.deferred.callback(None)
  2030. class IFinishableConsumer(interfaces.IConsumer):
  2031. """
  2032. A Consumer for producers that finish.
  2033. @since: 11.0
  2034. """
  2035. def finish():
  2036. """
  2037. The producer has finished producing.
  2038. """
  2039. @implementer(IFinishableConsumer)
  2040. class SenderProtocol(protocol.Protocol):
  2041. def __init__(self):
  2042. # Fired upon connection
  2043. self.connectedDeferred = defer.Deferred()
  2044. # Fired upon disconnection
  2045. self.deferred = defer.Deferred()
  2046. # Protocol stuff
  2047. def dataReceived(self, data):
  2048. raise UnexpectedData(
  2049. "Received data from the server on a " "send-only data-connection"
  2050. )
  2051. def makeConnection(self, transport):
  2052. protocol.Protocol.makeConnection(self, transport)
  2053. self.connectedDeferred.callback(self)
  2054. def connectionLost(self, reason):
  2055. if reason.check(error.ConnectionDone):
  2056. self.deferred.callback("connection done")
  2057. else:
  2058. self.deferred.errback(reason)
  2059. # IFinishableConsumer stuff
  2060. def write(self, data):
  2061. self.transport.write(data)
  2062. def registerProducer(self, producer, streaming):
  2063. """
  2064. Register the given producer with our transport.
  2065. """
  2066. self.transport.registerProducer(producer, streaming)
  2067. def unregisterProducer(self):
  2068. """
  2069. Unregister the previously registered producer.
  2070. """
  2071. self.transport.unregisterProducer()
  2072. def finish(self):
  2073. self.transport.loseConnection()
  2074. def decodeHostPort(line):
  2075. """
  2076. Decode an FTP response specifying a host and port.
  2077. @return: a 2-tuple of (host, port).
  2078. """
  2079. abcdef = re.sub("[^0-9, ]", "", line)
  2080. parsed = [int(p.strip()) for p in abcdef.split(",")]
  2081. for x in parsed:
  2082. if x < 0 or x > 255:
  2083. raise ValueError("Out of range", line, x)
  2084. a, b, c, d, e, f = parsed
  2085. host = f"{a}.{b}.{c}.{d}"
  2086. port = (int(e) << 8) + int(f)
  2087. return host, port
  2088. def encodeHostPort(host, port):
  2089. numbers = host.split(".") + [str(port >> 8), str(port % 256)]
  2090. return ",".join(numbers)
  2091. def decodeExtendedAddress(address):
  2092. """
  2093. Decode an FTP protocol/address/port combination, using the syntax
  2094. defined in RFC 2428 section 2.
  2095. @return: a 3-tuple of (protocol, host, port).
  2096. """
  2097. delim = address[0]
  2098. protocol, host, port, _ = address[1:].split(delim)
  2099. return protocol, host, int(port)
  2100. def _unwrapFirstError(failure):
  2101. failure.trap(defer.FirstError)
  2102. return failure.value.subFailure
  2103. class FTPDataPortFactory(protocol.ServerFactory):
  2104. """
  2105. Factory for data connections that use the PORT command
  2106. (i.e. "active" transfers)
  2107. """
  2108. noisy = False
  2109. def buildProtocol(self, addr):
  2110. # This is a bit hackish -- we already have a Protocol instance,
  2111. # so just return it instead of making a new one
  2112. # FIXME: Reject connections from the wrong address/port
  2113. # (potential security problem)
  2114. self.protocol.factory = self
  2115. self.port.loseConnection()
  2116. return self.protocol
  2117. class FTPClientBasic(basic.LineReceiver):
  2118. """
  2119. Foundations of an FTP client.
  2120. """
  2121. debug = False
  2122. _encoding = "latin-1"
  2123. def __init__(self):
  2124. self.actionQueue = []
  2125. self.greeting = None
  2126. self.nextDeferred = defer.Deferred().addCallback(self._cb_greeting)
  2127. self.nextDeferred.addErrback(self.fail)
  2128. self.response = []
  2129. self._failed = 0
  2130. def fail(self, error):
  2131. """
  2132. Give an error to any queued deferreds.
  2133. """
  2134. self._fail(error)
  2135. def _fail(self, error):
  2136. """
  2137. Errback all queued deferreds.
  2138. """
  2139. if self._failed:
  2140. # We're recursing; bail out here for simplicity
  2141. return error
  2142. self._failed = 1
  2143. if self.nextDeferred:
  2144. try:
  2145. self.nextDeferred.errback(
  2146. failure.Failure(ConnectionLost("FTP connection lost", error))
  2147. )
  2148. except defer.AlreadyCalledError:
  2149. pass
  2150. for ftpCommand in self.actionQueue:
  2151. ftpCommand.fail(
  2152. failure.Failure(ConnectionLost("FTP connection lost", error))
  2153. )
  2154. return error
  2155. def _cb_greeting(self, greeting):
  2156. self.greeting = greeting
  2157. def sendLine(self, line):
  2158. """
  2159. Sends a line, unless line is None.
  2160. @param line: Line to send
  2161. @type line: L{bytes} or L{unicode}
  2162. """
  2163. if line is None:
  2164. return
  2165. elif isinstance(line, str):
  2166. line = line.encode(self._encoding)
  2167. basic.LineReceiver.sendLine(self, line)
  2168. def sendNextCommand(self):
  2169. """
  2170. (Private) Processes the next command in the queue.
  2171. """
  2172. ftpCommand = self.popCommandQueue()
  2173. if ftpCommand is None:
  2174. self.nextDeferred = None
  2175. return
  2176. if not ftpCommand.ready:
  2177. self.actionQueue.insert(0, ftpCommand)
  2178. reactor.callLater(1.0, self.sendNextCommand)
  2179. self.nextDeferred = None
  2180. return
  2181. # FIXME: this if block doesn't belong in FTPClientBasic, it belongs in
  2182. # FTPClient.
  2183. if ftpCommand.text == "PORT":
  2184. self.generatePortCommand(ftpCommand)
  2185. if self.debug:
  2186. log.msg("<-- %s" % ftpCommand.text)
  2187. self.nextDeferred = ftpCommand.deferred
  2188. self.sendLine(ftpCommand.text)
  2189. def queueCommand(self, ftpCommand):
  2190. """
  2191. Add an FTPCommand object to the queue.
  2192. If it's the only thing in the queue, and we are connected and we aren't
  2193. waiting for a response of an earlier command, the command will be sent
  2194. immediately.
  2195. @param ftpCommand: an L{FTPCommand}
  2196. """
  2197. self.actionQueue.append(ftpCommand)
  2198. if (
  2199. len(self.actionQueue) == 1
  2200. and self.transport is not None
  2201. and self.nextDeferred is None
  2202. ):
  2203. self.sendNextCommand()
  2204. def queueStringCommand(self, command, public=1):
  2205. """
  2206. Queues a string to be issued as an FTP command
  2207. @param command: string of an FTP command to queue
  2208. @param public: a flag intended for internal use by FTPClient. Don't
  2209. change it unless you know what you're doing.
  2210. @return: a L{Deferred} that will be called when the response to the
  2211. command has been received.
  2212. """
  2213. ftpCommand = FTPCommand(command, public)
  2214. self.queueCommand(ftpCommand)
  2215. return ftpCommand.deferred
  2216. def popCommandQueue(self):
  2217. """
  2218. Return the front element of the command queue, or None if empty.
  2219. """
  2220. if self.actionQueue:
  2221. return self.actionQueue.pop(0)
  2222. else:
  2223. return None
  2224. def queueLogin(self, username, password):
  2225. """
  2226. Login: send the username, send the password.
  2227. If the password is L{None}, the PASS command won't be sent. Also, if
  2228. the response to the USER command has a response code of 230 (User
  2229. logged in), then PASS won't be sent either.
  2230. """
  2231. # Prepare the USER command
  2232. deferreds = []
  2233. userDeferred = self.queueStringCommand("USER " + username, public=0)
  2234. deferreds.append(userDeferred)
  2235. # Prepare the PASS command (if a password is given)
  2236. if password is not None:
  2237. passwordCmd = FTPCommand("PASS " + password, public=0)
  2238. self.queueCommand(passwordCmd)
  2239. deferreds.append(passwordCmd.deferred)
  2240. # Avoid sending PASS if the response to USER is 230.
  2241. # (ref: http://cr.yp.to/ftp/user.html#user)
  2242. def cancelPasswordIfNotNeeded(response):
  2243. if response[0].startswith("230"):
  2244. # No password needed!
  2245. self.actionQueue.remove(passwordCmd)
  2246. return response
  2247. userDeferred.addCallback(cancelPasswordIfNotNeeded)
  2248. # Error handling.
  2249. for deferred in deferreds:
  2250. # If something goes wrong, call fail
  2251. deferred.addErrback(self.fail)
  2252. # But also swallow the error, so we don't cause spurious errors
  2253. deferred.addErrback(lambda x: None)
  2254. def lineReceived(self, line):
  2255. """
  2256. (Private) Parses the response messages from the FTP server.
  2257. """
  2258. # Add this line to the current response
  2259. line = line.decode(self._encoding)
  2260. if self.debug:
  2261. log.msg("--> %s" % line)
  2262. self.response.append(line)
  2263. # Bail out if this isn't the last line of a response
  2264. # The last line of response starts with 3 digits followed by a space
  2265. codeIsValid = re.match(r"\d{3} ", line)
  2266. if not codeIsValid:
  2267. return
  2268. code = line[0:3]
  2269. # Ignore marks
  2270. if code[0] == "1":
  2271. return
  2272. # Check that we were expecting a response
  2273. if self.nextDeferred is None:
  2274. self.fail(UnexpectedResponse(self.response))
  2275. return
  2276. # Reset the response
  2277. response = self.response
  2278. self.response = []
  2279. # Look for a success or error code, and call the appropriate callback
  2280. if code[0] in ("2", "3"):
  2281. # Success
  2282. self.nextDeferred.callback(response)
  2283. elif code[0] in ("4", "5"):
  2284. # Failure
  2285. self.nextDeferred.errback(failure.Failure(CommandFailed(response)))
  2286. else:
  2287. # This shouldn't happen unless something screwed up.
  2288. log.msg(f"Server sent invalid response code {code}")
  2289. self.nextDeferred.errback(failure.Failure(BadResponse(response)))
  2290. # Run the next command
  2291. self.sendNextCommand()
  2292. def connectionLost(self, reason):
  2293. self._fail(reason)
  2294. class _PassiveConnectionFactory(protocol.ClientFactory):
  2295. noisy = False
  2296. def __init__(self, protoInstance):
  2297. self.protoInstance = protoInstance
  2298. def buildProtocol(self, ignored):
  2299. self.protoInstance.factory = self
  2300. return self.protoInstance
  2301. def clientConnectionFailed(self, connector, reason):
  2302. e = FTPError("Connection Failed", reason)
  2303. self.protoInstance.deferred.errback(e)
  2304. class FTPClient(FTPClientBasic):
  2305. """
  2306. L{FTPClient} is a client implementation of the FTP protocol which
  2307. exposes FTP commands as methods which return L{Deferred}s.
  2308. Each command method returns a L{Deferred} which is called back when a
  2309. successful response code (2xx or 3xx) is received from the server or
  2310. which is error backed if an error response code (4xx or 5xx) is received
  2311. from the server or if a protocol violation occurs. If an error response
  2312. code is received, the L{Deferred} fires with a L{Failure} wrapping a
  2313. L{CommandFailed} instance. The L{CommandFailed} instance is created
  2314. with a list of the response lines received from the server.
  2315. See U{RFC 959<http://www.ietf.org/rfc/rfc959.txt>} for error code
  2316. definitions.
  2317. Both active and passive transfers are supported.
  2318. @ivar passive: See description in __init__.
  2319. """
  2320. connectFactory = reactor.connectTCP # type: ignore[attr-defined]
  2321. def __init__(
  2322. self, username="anonymous", password="twisted@twistedmatrix.com", passive=1
  2323. ):
  2324. """
  2325. Constructor.
  2326. I will login as soon as I receive the welcome message from the server.
  2327. @param username: FTP username
  2328. @param password: FTP password
  2329. @param passive: flag that controls if I use active or passive data
  2330. connections. You can also change this after construction by
  2331. assigning to C{self.passive}.
  2332. """
  2333. FTPClientBasic.__init__(self)
  2334. self.queueLogin(username, password)
  2335. self.passive = passive
  2336. def fail(self, error):
  2337. """
  2338. Disconnect, and also give an error to any queued deferreds.
  2339. """
  2340. self.transport.loseConnection()
  2341. self._fail(error)
  2342. def receiveFromConnection(self, commands, protocol):
  2343. """
  2344. Retrieves a file or listing generated by the given command,
  2345. feeding it to the given protocol.
  2346. @param commands: list of strings of FTP commands to execute then
  2347. receive the results of (e.g. C{LIST}, C{RETR})
  2348. @param protocol: A L{Protocol} B{instance} e.g. an
  2349. L{FTPFileListProtocol}, or something that can be adapted to one.
  2350. Typically this will be an L{IConsumer} implementation.
  2351. @return: L{Deferred}.
  2352. """
  2353. protocol = interfaces.IProtocol(protocol)
  2354. wrapper = ProtocolWrapper(protocol, defer.Deferred())
  2355. return self._openDataConnection(commands, wrapper)
  2356. def queueLogin(self, username, password):
  2357. """
  2358. Login: send the username, send the password, and
  2359. set retrieval mode to binary
  2360. """
  2361. FTPClientBasic.queueLogin(self, username, password)
  2362. d = self.queueStringCommand("TYPE I", public=0)
  2363. # If something goes wrong, call fail
  2364. d.addErrback(self.fail)
  2365. # But also swallow the error, so we don't cause spurious errors
  2366. d.addErrback(lambda x: None)
  2367. def sendToConnection(self, commands):
  2368. """
  2369. XXX
  2370. @return: A tuple of two L{Deferred}s:
  2371. - L{Deferred} L{IFinishableConsumer}. You must call
  2372. the C{finish} method on the IFinishableConsumer when the
  2373. file is completely transferred.
  2374. - L{Deferred} list of control-connection responses.
  2375. """
  2376. s = SenderProtocol()
  2377. r = self._openDataConnection(commands, s)
  2378. return (s.connectedDeferred, r)
  2379. def _openDataConnection(self, commands, protocol):
  2380. """
  2381. This method returns a DeferredList.
  2382. """
  2383. cmds = [FTPCommand(command, public=1) for command in commands]
  2384. cmdsDeferred = defer.DeferredList(
  2385. [cmd.deferred for cmd in cmds], fireOnOneErrback=True, consumeErrors=True
  2386. )
  2387. cmdsDeferred.addErrback(_unwrapFirstError)
  2388. if self.passive:
  2389. # Hack: use a mutable object to sneak a variable out of the
  2390. # scope of doPassive
  2391. _mutable = [None]
  2392. def doPassive(response):
  2393. """Connect to the port specified in the response to PASV"""
  2394. host, port = decodeHostPort(response[-1][4:])
  2395. f = _PassiveConnectionFactory(protocol)
  2396. _mutable[0] = self.connectFactory(host, port, f)
  2397. pasvCmd = FTPCommand("PASV")
  2398. self.queueCommand(pasvCmd)
  2399. pasvCmd.deferred.addCallback(doPassive).addErrback(self.fail)
  2400. results = [cmdsDeferred, pasvCmd.deferred, protocol.deferred]
  2401. d = defer.DeferredList(results, fireOnOneErrback=True, consumeErrors=True)
  2402. d.addErrback(_unwrapFirstError)
  2403. # Ensure the connection is always closed
  2404. def close(x, m=_mutable):
  2405. m[0] and m[0].disconnect()
  2406. return x
  2407. d.addBoth(close)
  2408. else:
  2409. # We just place a marker command in the queue, and will fill in
  2410. # the host and port numbers later (see generatePortCommand)
  2411. portCmd = FTPCommand("PORT")
  2412. # Ok, now we jump through a few hoops here.
  2413. # This is the problem: a transfer is not to be trusted as complete
  2414. # until we get both the "226 Transfer complete" message on the
  2415. # control connection, and the data socket is closed. Thus, we use
  2416. # a DeferredList to make sure we only fire the callback at the
  2417. # right time.
  2418. portCmd.transferDeferred = protocol.deferred
  2419. portCmd.protocol = protocol
  2420. portCmd.deferred.addErrback(portCmd.transferDeferred.errback)
  2421. self.queueCommand(portCmd)
  2422. # Create dummy functions for the next callback to call.
  2423. # These will also be replaced with real functions in
  2424. # generatePortCommand.
  2425. portCmd.loseConnection = lambda result: result
  2426. portCmd.fail = lambda error: error
  2427. # Ensure that the connection always gets closed
  2428. cmdsDeferred.addErrback(lambda e, pc=portCmd: pc.fail(e) or e)
  2429. results = [cmdsDeferred, portCmd.deferred, portCmd.transferDeferred]
  2430. d = defer.DeferredList(results, fireOnOneErrback=True, consumeErrors=True)
  2431. d.addErrback(_unwrapFirstError)
  2432. for cmd in cmds:
  2433. self.queueCommand(cmd)
  2434. return d
  2435. def generatePortCommand(self, portCmd):
  2436. """
  2437. (Private) Generates the text of a given PORT command.
  2438. """
  2439. # The problem is that we don't create the listening port until we need
  2440. # it for various reasons, and so we have to muck about to figure out
  2441. # what interface and port it's listening on, and then finally we can
  2442. # create the text of the PORT command to send to the FTP server.
  2443. # FIXME: This method is far too ugly.
  2444. # FIXME: The best solution is probably to only create the data port
  2445. # once per FTPClient, and just recycle it for each new download.
  2446. # This should be ok, because we don't pipeline commands.
  2447. # Start listening on a port
  2448. factory = FTPDataPortFactory()
  2449. factory.protocol = portCmd.protocol
  2450. listener = reactor.listenTCP(0, factory)
  2451. factory.port = listener
  2452. # Ensure we close the listening port if something goes wrong
  2453. def listenerFail(error, listener=listener):
  2454. if listener.connected:
  2455. listener.loseConnection()
  2456. return error
  2457. portCmd.fail = listenerFail
  2458. # Construct crufty FTP magic numbers that represent host & port
  2459. host = self.transport.getHost().host
  2460. port = listener.getHost().port
  2461. portCmd.text = "PORT " + encodeHostPort(host, port)
  2462. def escapePath(self, path):
  2463. """
  2464. Returns a FTP escaped path (replace newlines with nulls).
  2465. """
  2466. # Escape newline characters
  2467. return path.replace("\n", "\0")
  2468. def retrieveFile(self, path, protocol, offset=0):
  2469. """
  2470. Retrieve a file from the given path
  2471. This method issues the 'RETR' FTP command.
  2472. The file is fed into the given Protocol instance. The data connection
  2473. will be passive if self.passive is set.
  2474. @param path: path to file that you wish to receive.
  2475. @param protocol: a L{Protocol} instance.
  2476. @param offset: offset to start downloading from
  2477. @return: L{Deferred}
  2478. """
  2479. cmds = ["RETR " + self.escapePath(path)]
  2480. if offset:
  2481. cmds.insert(0, ("REST " + str(offset)))
  2482. return self.receiveFromConnection(cmds, protocol)
  2483. retr = retrieveFile
  2484. def storeFile(self, path, offset=0):
  2485. """
  2486. Store a file at the given path.
  2487. This method issues the 'STOR' FTP command.
  2488. @return: A tuple of two L{Deferred}s:
  2489. - L{Deferred} L{IFinishableConsumer}. You must call
  2490. the C{finish} method on the IFinishableConsumer when the
  2491. file is completely transferred.
  2492. - L{Deferred} list of control-connection responses.
  2493. """
  2494. cmds = ["STOR " + self.escapePath(path)]
  2495. if offset:
  2496. cmds.insert(0, ("REST " + str(offset)))
  2497. return self.sendToConnection(cmds)
  2498. stor = storeFile
  2499. def rename(self, pathFrom, pathTo):
  2500. """
  2501. Rename a file.
  2502. This method issues the I{RNFR}/I{RNTO} command sequence to rename
  2503. C{pathFrom} to C{pathTo}.
  2504. @param pathFrom: the absolute path to the file to be renamed
  2505. @type pathFrom: C{str}
  2506. @param pathTo: the absolute path to rename the file to.
  2507. @type pathTo: C{str}
  2508. @return: A L{Deferred} which fires when the rename operation has
  2509. succeeded or failed. If it succeeds, the L{Deferred} is called
  2510. back with a two-tuple of lists. The first list contains the
  2511. responses to the I{RNFR} command. The second list contains the
  2512. responses to the I{RNTO} command. If either I{RNFR} or I{RNTO}
  2513. fails, the L{Deferred} is errbacked with L{CommandFailed} or
  2514. L{BadResponse}.
  2515. @rtype: L{Deferred}
  2516. @since: 8.2
  2517. """
  2518. renameFrom = self.queueStringCommand("RNFR " + self.escapePath(pathFrom))
  2519. renameTo = self.queueStringCommand("RNTO " + self.escapePath(pathTo))
  2520. fromResponse = []
  2521. # Use a separate Deferred for the ultimate result so that Deferred
  2522. # chaining can't interfere with its result.
  2523. result = defer.Deferred()
  2524. # Bundle up all the responses
  2525. result.addCallback(lambda toResponse: (fromResponse, toResponse))
  2526. def ebFrom(failure):
  2527. # Make sure the RNTO doesn't run if the RNFR failed.
  2528. self.popCommandQueue()
  2529. result.errback(failure)
  2530. # Save the RNFR response to pass to the result Deferred later
  2531. renameFrom.addCallbacks(fromResponse.extend, ebFrom)
  2532. # Hook up the RNTO to the result Deferred as well
  2533. renameTo.chainDeferred(result)
  2534. return result
  2535. def list(self, path, protocol):
  2536. """
  2537. Retrieve a file listing into the given protocol instance.
  2538. This method issues the 'LIST' FTP command.
  2539. @param path: path to get a file listing for.
  2540. @param protocol: a L{Protocol} instance, probably a
  2541. L{FTPFileListProtocol} instance. It can cope with most common file
  2542. listing formats.
  2543. @return: L{Deferred}
  2544. """
  2545. if path is None:
  2546. path = ""
  2547. return self.receiveFromConnection(["LIST " + self.escapePath(path)], protocol)
  2548. def nlst(self, path, protocol):
  2549. """
  2550. Retrieve a short file listing into the given protocol instance.
  2551. This method issues the 'NLST' FTP command.
  2552. NLST (should) return a list of filenames, one per line.
  2553. @param path: path to get short file listing for.
  2554. @param protocol: a L{Protocol} instance.
  2555. """
  2556. if path is None:
  2557. path = ""
  2558. return self.receiveFromConnection(["NLST " + self.escapePath(path)], protocol)
  2559. def cwd(self, path):
  2560. """
  2561. Issues the CWD (Change Working Directory) command.
  2562. @return: a L{Deferred} that will be called when done.
  2563. """
  2564. return self.queueStringCommand("CWD " + self.escapePath(path))
  2565. def makeDirectory(self, path):
  2566. """
  2567. Make a directory
  2568. This method issues the MKD command.
  2569. @param path: The path to the directory to create.
  2570. @type path: C{str}
  2571. @return: A L{Deferred} which fires when the server responds. If the
  2572. directory is created, the L{Deferred} is called back with the
  2573. server response. If the server response indicates the directory
  2574. was not created, the L{Deferred} is errbacked with a L{Failure}
  2575. wrapping L{CommandFailed} or L{BadResponse}.
  2576. @rtype: L{Deferred}
  2577. @since: 8.2
  2578. """
  2579. return self.queueStringCommand("MKD " + self.escapePath(path))
  2580. def removeFile(self, path):
  2581. """
  2582. Delete a file on the server.
  2583. L{removeFile} issues a I{DELE} command to the server to remove the
  2584. indicated file. Note that this command cannot remove a directory.
  2585. @param path: The path to the file to delete. May be relative to the
  2586. current dir.
  2587. @type path: C{str}
  2588. @return: A L{Deferred} which fires when the server responds. On error,
  2589. it is errbacked with either L{CommandFailed} or L{BadResponse}. On
  2590. success, it is called back with a list of response lines.
  2591. @rtype: L{Deferred}
  2592. @since: 8.2
  2593. """
  2594. return self.queueStringCommand("DELE " + self.escapePath(path))
  2595. def removeDirectory(self, path):
  2596. """
  2597. Delete a directory on the server.
  2598. L{removeDirectory} issues a I{RMD} command to the server to remove the
  2599. indicated directory. Described in RFC959.
  2600. @param path: The path to the directory to delete. May be relative to
  2601. the current working directory.
  2602. @type path: C{str}
  2603. @return: A L{Deferred} which fires when the server responds. On error,
  2604. it is errbacked with either L{CommandFailed} or L{BadResponse}. On
  2605. success, it is called back with a list of response lines.
  2606. @rtype: L{Deferred}
  2607. @since: 11.1
  2608. """
  2609. return self.queueStringCommand("RMD " + self.escapePath(path))
  2610. def cdup(self):
  2611. """
  2612. Issues the CDUP (Change Directory UP) command.
  2613. @return: a L{Deferred} that will be called when done.
  2614. """
  2615. return self.queueStringCommand("CDUP")
  2616. def pwd(self):
  2617. """
  2618. Issues the PWD (Print Working Directory) command.
  2619. The L{getDirectory} does the same job but automatically parses the
  2620. result.
  2621. @return: a L{Deferred} that will be called when done. It is up to the
  2622. caller to interpret the response, but the L{parsePWDResponse}
  2623. method in this module should work.
  2624. """
  2625. return self.queueStringCommand("PWD")
  2626. def getDirectory(self):
  2627. """
  2628. Returns the current remote directory.
  2629. @return: a L{Deferred} that will be called back with a C{str} giving
  2630. the remote directory or which will errback with L{CommandFailed}
  2631. if an error response is returned.
  2632. """
  2633. def cbParse(result):
  2634. try:
  2635. # The only valid code is 257
  2636. if int(result[0].split(" ", 1)[0]) != 257:
  2637. raise ValueError
  2638. except (IndexError, ValueError):
  2639. return failure.Failure(CommandFailed(result))
  2640. path = parsePWDResponse(result[0])
  2641. if path is None:
  2642. return failure.Failure(CommandFailed(result))
  2643. return path
  2644. return self.pwd().addCallback(cbParse)
  2645. def quit(self):
  2646. """
  2647. Issues the I{QUIT} command.
  2648. @return: A L{Deferred} that fires when the server acknowledges the
  2649. I{QUIT} command. The transport should not be disconnected until
  2650. this L{Deferred} fires.
  2651. """
  2652. return self.queueStringCommand("QUIT")
  2653. class FTPFileListProtocol(basic.LineReceiver):
  2654. """
  2655. Parser for standard FTP file listings
  2656. This is the evil required to match::
  2657. -rw-r--r-- 1 root other 531 Jan 29 03:26 README
  2658. If you need different evil for a wacky FTP server, you can
  2659. override either C{fileLinePattern} or C{parseDirectoryLine()}.
  2660. It populates the instance attribute self.files, which is a list containing
  2661. dicts with the following keys (examples from the above line):
  2662. - filetype: e.g. 'd' for directories, or '-' for an ordinary file
  2663. - perms: e.g. 'rw-r--r--'
  2664. - nlinks: e.g. 1
  2665. - owner: e.g. 'root'
  2666. - group: e.g. 'other'
  2667. - size: e.g. 531
  2668. - date: e.g. 'Jan 29 03:26'
  2669. - filename: e.g. 'README'
  2670. - linktarget: e.g. 'some/file'
  2671. Note that the 'date' value will be formatted differently depending on the
  2672. date. Check U{http://cr.yp.to/ftp.html} if you really want to try to parse
  2673. it.
  2674. It also matches the following::
  2675. -rw-r--r-- 1 root other 531 Jan 29 03:26 I HAVE\\ SPACE
  2676. - filename: e.g. 'I HAVE SPACE'
  2677. -rw-r--r-- 1 root other 531 Jan 29 03:26 LINK -> TARGET
  2678. - filename: e.g. 'LINK'
  2679. - linktarget: e.g. 'TARGET'
  2680. -rw-r--r-- 1 root other 531 Jan 29 03:26 N S -> L S
  2681. - filename: e.g. 'N S'
  2682. - linktarget: e.g. 'L S'
  2683. @ivar files: list of dicts describing the files in this listing
  2684. """
  2685. fileLinePattern = re.compile(
  2686. r"^(?P<filetype>.)(?P<perms>.{9})\s+(?P<nlinks>\d*)\s*"
  2687. r"(?P<owner>\S+)\s+(?P<group>\S+)\s+(?P<size>\d+)\s+"
  2688. r"(?P<date>...\s+\d+\s+[\d:]+)\s+(?P<filename>.{1,}?)"
  2689. r"( -> (?P<linktarget>[^\r]*))?\r?$"
  2690. )
  2691. delimiter = b"\n"
  2692. _encoding = "latin-1"
  2693. def __init__(self):
  2694. self.files = []
  2695. def lineReceived(self, line):
  2696. line = line.decode(self._encoding)
  2697. d = self.parseDirectoryLine(line)
  2698. if d is None:
  2699. self.unknownLine(line)
  2700. else:
  2701. self.addFile(d)
  2702. def parseDirectoryLine(self, line):
  2703. """
  2704. Return a dictionary of fields, or None if line cannot be parsed.
  2705. @param line: line of text expected to contain a directory entry
  2706. @type line: str
  2707. @return: dict
  2708. """
  2709. match = self.fileLinePattern.match(line)
  2710. if match is None:
  2711. return None
  2712. else:
  2713. d = match.groupdict()
  2714. d["filename"] = d["filename"].replace(r"\ ", " ")
  2715. d["nlinks"] = int(d["nlinks"])
  2716. d["size"] = int(d["size"])
  2717. if d["linktarget"]:
  2718. d["linktarget"] = d["linktarget"].replace(r"\ ", " ")
  2719. return d
  2720. def addFile(self, info):
  2721. """
  2722. Append file information dictionary to the list of known files.
  2723. Subclasses can override or extend this method to handle file
  2724. information differently without affecting the parsing of data
  2725. from the server.
  2726. @param info: dictionary containing the parsed representation
  2727. of the file information
  2728. @type info: dict
  2729. """
  2730. self.files.append(info)
  2731. def unknownLine(self, line):
  2732. """
  2733. Deal with received lines which could not be parsed as file
  2734. information.
  2735. Subclasses can override this to perform any special processing
  2736. needed.
  2737. @param line: unparsable line as received
  2738. @type line: str
  2739. """
  2740. pass
  2741. def parsePWDResponse(response):
  2742. """
  2743. Returns the path from a response to a PWD command.
  2744. Responses typically look like::
  2745. 257 "/home/andrew" is current directory.
  2746. For this example, I will return C{'/home/andrew'}.
  2747. If I can't find the path, I return L{None}.
  2748. """
  2749. match = re.search('"(.*)"', response)
  2750. if match:
  2751. return match.groups()[0]
  2752. else:
  2753. return None