ftp.py 104 KB

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