handlers.py 130 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321
  1. #!/usr/bin/env python
  2. # $Id: handlers.py 1218 2013-04-19 01:48:39Z g.rodola $
  3. # ======================================================================
  4. # Copyright (C) 2007-2013 Giampaolo Rodola' <g.rodola@gmail.com>
  5. #
  6. # All Rights Reserved
  7. #
  8. # Permission is hereby granted, free of charge, to any person
  9. # obtaining a copy of this software and associated documentation
  10. # files (the "Software"), to deal in the Software without
  11. # restriction, including without limitation the rights to use,
  12. # copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. # copies of the Software, and to permit persons to whom the
  14. # Software is furnished to do so, subject to the following
  15. # conditions:
  16. #
  17. # The above copyright notice and this permission notice shall be
  18. # included in all copies or substantial portions of the Software.
  19. #
  20. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  21. # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
  22. # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  23. # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  24. # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  25. # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  26. # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
  27. # OTHER DEALINGS IN THE SOFTWARE.
  28. #
  29. # ======================================================================
  30. import asynchat
  31. import time
  32. import sys
  33. import os
  34. import errno
  35. import socket
  36. import traceback
  37. import glob
  38. import random
  39. import warnings
  40. import logging
  41. try:
  42. import pwd
  43. import grp
  44. except ImportError:
  45. pwd = grp = None
  46. from pyftpdlib import __ver__
  47. from pyftpdlib.log import logger
  48. from pyftpdlib.filesystems import FilesystemError, AbstractedFS
  49. from pyftpdlib._compat import PY3, b, u, getcwdu, unicode, xrange, next
  50. from pyftpdlib.ioloop import AsyncChat, Connector, Acceptor, timer, _DISCONNECTED
  51. from pyftpdlib.authorizers import (DummyAuthorizer, AuthenticationFailed,
  52. AuthorizerError)
  53. def _import_sendfile():
  54. # By default attempt to use os.sendfile introduced in Python 3.3:
  55. # http://bugs.python.org/issue10882
  56. # ...otherwise fallback on using third-party pysendfile module:
  57. # http://code.google.com/p/pysendfile/
  58. if os.name == 'posix':
  59. try:
  60. return os.sendfile # py >= 3.3
  61. except AttributeError:
  62. try:
  63. import sendfile as sf
  64. # dirty hack to detect whether old 1.2.4 version is installed
  65. if hasattr(sf, 'has_sf_hdtr'):
  66. raise ImportError
  67. return sf.sendfile
  68. except ImportError:
  69. pass
  70. sendfile = _import_sendfile()
  71. proto_cmds = {
  72. 'ABOR' : dict(perm=None, auth=True, arg=False,
  73. help='Syntax: ABOR (abort transfer).'),
  74. 'ALLO' : dict(perm=None, auth=True, arg=True,
  75. help='Syntax: ALLO <SP> bytes (noop; allocate storage).'),
  76. 'APPE' : dict(perm='a', auth=True, arg=True,
  77. help='Syntax: APPE <SP> file-name (append data to file).'),
  78. 'CDUP' : dict(perm='e', auth=True, arg=False,
  79. help='Syntax: CDUP (go to parent directory).'),
  80. 'CWD' : dict(perm='e', auth=True, arg=None,
  81. help='Syntax: CWD [<SP> dir-name] (change working directory).'),
  82. 'DELE' : dict(perm='d', auth=True, arg=True,
  83. help='Syntax: DELE <SP> file-name (delete file).'),
  84. 'EPRT' : dict(perm=None, auth=True, arg=True,
  85. help='Syntax: EPRT <SP> |proto|ip|port| (extended active mode).'),
  86. 'EPSV' : dict(perm=None, auth=True, arg=None,
  87. help='Syntax: EPSV [<SP> proto/"ALL"] (extended passive mode).'),
  88. 'FEAT' : dict(perm=None, auth=False, arg=False,
  89. help='Syntax: FEAT (list all new features supported).'),
  90. 'HELP' : dict(perm=None, auth=False, arg=None,
  91. help='Syntax: HELP [<SP> cmd] (show help).'),
  92. 'LIST' : dict(perm='l', auth=True, arg=None,
  93. help='Syntax: LIST [<SP> path] (list files).'),
  94. 'MDTM' : dict(perm='l', auth=True, arg=True,
  95. help='Syntax: MDTM [<SP> path] (file last modification time).'),
  96. 'MLSD' : dict(perm='l', auth=True, arg=None,
  97. help='Syntax: MLSD [<SP> path] (list directory).'),
  98. 'MLST' : dict(perm='l', auth=True, arg=None,
  99. help='Syntax: MLST [<SP> path] (show information about path).'),
  100. 'MODE' : dict(perm=None, auth=True, arg=True,
  101. help='Syntax: MODE <SP> mode (noop; set data transfer mode).'),
  102. 'MKD' : dict(perm='m', auth=True, arg=True,
  103. help='Syntax: MKD <SP> path (create directory).'),
  104. 'NLST' : dict(perm='l', auth=True, arg=None,
  105. help='Syntax: NLST [<SP> path] (list path in a compact form).'),
  106. 'NOOP' : dict(perm=None, auth=False, arg=False,
  107. help='Syntax: NOOP (just do nothing).'),
  108. 'OPTS' : dict(perm=None, auth=True, arg=True,
  109. help='Syntax: OPTS <SP> cmd [<SP> option] (set option for command).'),
  110. 'PASS' : dict(perm=None, auth=False, arg=None,
  111. help='Syntax: PASS [<SP> password] (set user password).'),
  112. 'PASV' : dict(perm=None, auth=True, arg=False,
  113. help='Syntax: PASV (open passive data connection).'),
  114. 'PORT' : dict(perm=None, auth=True, arg=True,
  115. help='Syntax: PORT <sp> h1,h2,h3,h4,p1,p2 (open active data connection).'),
  116. 'PWD' : dict(perm=None, auth=True, arg=False,
  117. help='Syntax: PWD (get current working directory).'),
  118. 'QUIT' : dict(perm=None, auth=False, arg=False,
  119. help='Syntax: QUIT (quit current session).'),
  120. 'REIN' : dict(perm=None, auth=True, arg=False,
  121. help='Syntax: REIN (flush account).'),
  122. 'REST' : dict(perm=None, auth=True, arg=True,
  123. help='Syntax: REST <SP> offset (set file offset).'),
  124. 'RETR' : dict(perm='r', auth=True, arg=True,
  125. help='Syntax: RETR <SP> file-name (retrieve a file).'),
  126. 'RMD' : dict(perm='d', auth=True, arg=True,
  127. help='Syntax: RMD <SP> dir-name (remove directory).'),
  128. 'RNFR' : dict(perm='f', auth=True, arg=True,
  129. help='Syntax: RNFR <SP> file-name (rename (source name)).'),
  130. 'RNTO' : dict(perm='f', auth=True, arg=True,
  131. help='Syntax: RNTO <SP> file-name (rename (destination name)).'),
  132. 'SITE' : dict(perm=None, auth=False, arg=True,
  133. help='Syntax: SITE <SP> site-command (execute SITE command).'),
  134. 'SITE HELP' : dict(perm=None, auth=False, arg=None,
  135. help='Syntax: SITE HELP [<SP> site-command] (show SITE command help).'),
  136. 'SITE CHMOD': dict(perm='M', auth=True, arg=True,
  137. help='Syntax: SITE CHMOD <SP> mode path (change file mode).'),
  138. 'SIZE' : dict(perm='l', auth=True, arg=True,
  139. help='Syntax: SIZE <SP> file-name (get file size).'),
  140. 'STAT' : dict(perm='l', auth=False, arg=None,
  141. help='Syntax: STAT [<SP> path name] (server stats [list files]).'),
  142. 'STOR' : dict(perm='w', auth=True, arg=True,
  143. help='Syntax: STOR <SP> file-name (store a file).'),
  144. 'STOU' : dict(perm='w', auth=True, arg=None,
  145. help='Syntax: STOU [<SP> file-name] (store a file with a unique name).'),
  146. 'STRU' : dict(perm=None, auth=True, arg=True,
  147. help='Syntax: STRU <SP> type (noop; set file structure).'),
  148. 'SYST' : dict(perm=None, auth=False, arg=False,
  149. help='Syntax: SYST (get operating system type).'),
  150. 'TYPE' : dict(perm=None, auth=True, arg=True,
  151. help='Syntax: TYPE <SP> [A | I] (set transfer type).'),
  152. 'USER' : dict(perm=None, auth=False, arg=True,
  153. help='Syntax: USER <SP> user-name (set username).'),
  154. 'XCUP' : dict(perm='e', auth=True, arg=False,
  155. help='Syntax: XCUP (obsolete; go to parent directory).'),
  156. 'XCWD' : dict(perm='e', auth=True, arg=None,
  157. help='Syntax: XCWD [<SP> dir-name] (obsolete; change directory).'),
  158. 'XMKD' : dict(perm='m', auth=True, arg=True,
  159. help='Syntax: XMKD <SP> dir-name (obsolete; create directory).'),
  160. 'XPWD' : dict(perm=None, auth=True, arg=False,
  161. help='Syntax: XPWD (obsolete; get current dir).'),
  162. 'XRMD' : dict(perm='d', auth=True, arg=True,
  163. help='Syntax: XRMD <SP> dir-name (obsolete; remove directory).'),
  164. }
  165. if not hasattr(os, 'chmod'):
  166. del proto_cmds['SITE CHMOD']
  167. def _strerror(err):
  168. if isinstance(err, EnvironmentError):
  169. try:
  170. return os.strerror(err.errno)
  171. except AttributeError:
  172. # not available on PythonCE
  173. if not hasattr(os, 'strerror'):
  174. return err.strerror
  175. raise
  176. else:
  177. return str(err)
  178. def _support_hybrid_ipv6():
  179. """Return True if it is possible to use hybrid IPv6/IPv4 sockets
  180. on this platform.
  181. """
  182. # Note: IPPROTO_IPV6 constant is broken on Windows, see:
  183. # http://bugs.python.org/issue6926
  184. sock = None
  185. try:
  186. try:
  187. if not socket.has_ipv6:
  188. return False
  189. sock = socket.socket(socket.AF_INET6)
  190. return not sock.getsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY)
  191. except (socket.error, AttributeError):
  192. return False
  193. finally:
  194. if sock is not None:
  195. sock.close()
  196. SUPPORTS_HYBRID_IPV6 = _support_hybrid_ipv6()
  197. class _FileReadWriteError(OSError):
  198. """Exception raised when reading or writing a file during a transfer."""
  199. # --- DTP classes
  200. class PassiveDTP(Acceptor):
  201. """Creates a socket listening on a local port, dispatching the
  202. resultant connection to DTPHandler. Used for handling PASV command.
  203. - (int) timeout: the timeout for a remote client to establish
  204. connection with the listening socket. Defaults to 30 seconds.
  205. - (int) backlog: the maximum number of queued connections passed
  206. to listen(). If a connection request arrives when the queue is
  207. full the client may raise ECONNRESET. Defaults to 5.
  208. """
  209. timeout = 30
  210. backlog = None
  211. def __init__(self, cmd_channel, extmode=False):
  212. """Initialize the passive data server.
  213. - (instance) cmd_channel: the command channel class instance.
  214. - (bool) extmode: wheter use extended passive mode response type.
  215. """
  216. self.cmd_channel = cmd_channel
  217. self.log = cmd_channel.log
  218. self.log_exception = cmd_channel.log_exception
  219. self._closed = False
  220. self._idler = None
  221. Acceptor.__init__(self, ioloop=cmd_channel.ioloop)
  222. local_ip = self.cmd_channel.socket.getsockname()[0]
  223. if local_ip in self.cmd_channel.masquerade_address_map:
  224. masqueraded_ip = self.cmd_channel.masquerade_address_map[local_ip]
  225. elif self.cmd_channel.masquerade_address:
  226. masqueraded_ip = self.cmd_channel.masquerade_address
  227. else:
  228. masqueraded_ip = None
  229. if self.cmd_channel.server._af != socket.AF_INET:
  230. # dual stack IPv4/IPv6 support
  231. af = self.bind_af_unspecified((local_ip, 0))
  232. self.socket.close()
  233. else:
  234. af = self.cmd_channel._af
  235. self.create_socket(af, socket.SOCK_STREAM)
  236. if self.cmd_channel.passive_ports is None:
  237. # By using 0 as port number value we let kernel choose a
  238. # free unprivileged random port.
  239. self.bind((local_ip, 0))
  240. else:
  241. ports = list(self.cmd_channel.passive_ports)
  242. while ports:
  243. port = ports.pop(random.randint(0, len(ports) - 1))
  244. self.set_reuse_addr()
  245. try:
  246. self.bind((local_ip, port))
  247. except socket.error:
  248. err = sys.exc_info()[1]
  249. if err.args[0] == errno.EADDRINUSE: # port already in use
  250. if ports:
  251. continue
  252. # If cannot use one of the ports in the configured
  253. # range we'll use a kernel-assigned port, and log
  254. # a message reporting the issue.
  255. # By using 0 as port number value we let kernel
  256. # choose a free unprivileged random port.
  257. else:
  258. self.bind((local_ip, 0))
  259. self.cmd_channel.log(
  260. "Can't find a valid passive port in the "
  261. "configured range. A random kernel-assigned "
  262. "port will be used.",
  263. logfun=logger.warning
  264. )
  265. else:
  266. raise
  267. else:
  268. break
  269. self.listen(self.backlog or self.cmd_channel.server.backlog)
  270. port = self.socket.getsockname()[1]
  271. if not extmode:
  272. ip = masqueraded_ip or local_ip
  273. if ip.startswith('::ffff:'):
  274. # In this scenario, the server has an IPv6 socket, but
  275. # the remote client is using IPv4 and its address is
  276. # represented as an IPv4-mapped IPv6 address which
  277. # looks like this ::ffff:151.12.5.65, see:
  278. # http://en.wikipedia.org/wiki/IPv6#IPv4-mapped_addresses
  279. # http://tools.ietf.org/html/rfc3493.html#section-3.7
  280. # We truncate the first bytes to make it look like a
  281. # common IPv4 address.
  282. ip = ip[7:]
  283. # The format of 227 response in not standardized.
  284. # This is the most expected:
  285. self.cmd_channel.respond('227 Entering passive mode (%s,%d,%d).' % (
  286. ip.replace('.', ','), port // 256, port % 256))
  287. else:
  288. self.cmd_channel.respond('229 Entering extended passive mode '
  289. '(|||%d|).' % port)
  290. if self.timeout:
  291. self._idler = self.ioloop.call_later(self.timeout,
  292. self.handle_timeout,
  293. _errback=self.handle_error)
  294. # --- connection / overridden
  295. def handle_accepted(self, sock, addr):
  296. """Called when remote client initiates a connection."""
  297. if not self.cmd_channel.connected:
  298. return self.close()
  299. # Check the origin of data connection. If not expressively
  300. # configured we drop the incoming data connection if remote
  301. # IP address does not match the client's IP address.
  302. if self.cmd_channel.remote_ip != addr[0]:
  303. if not self.cmd_channel.permit_foreign_addresses:
  304. try:
  305. sock.close()
  306. except socket.error:
  307. pass
  308. msg = '425 Rejected data connection from foreign address %s:%s.' \
  309. %(addr[0], addr[1])
  310. self.cmd_channel.respond_w_warning(msg)
  311. # do not close listening socket: it couldn't be client's blame
  312. return
  313. else:
  314. # site-to-site FTP allowed
  315. msg = 'Established data connection with foreign address %s:%s.'\
  316. % (addr[0], addr[1])
  317. self.cmd_channel.log(msg, logfun=logger.warning)
  318. # Immediately close the current channel (we accept only one
  319. # connection at time) and avoid running out of max connections
  320. # limit.
  321. self.close()
  322. # delegate such connection to DTP handler
  323. if self.cmd_channel.connected:
  324. handler = self.cmd_channel.dtp_handler(sock, self.cmd_channel)
  325. if handler.connected:
  326. self.cmd_channel.data_channel = handler
  327. self.cmd_channel._on_dtp_connection()
  328. def handle_timeout(self):
  329. if self.cmd_channel.connected:
  330. self.cmd_channel.respond("421 Passive data channel timed out.",
  331. logfun=logging.info)
  332. self.close()
  333. def handle_error(self):
  334. """Called to handle any uncaught exceptions."""
  335. try:
  336. raise
  337. except Exception:
  338. logger.error(traceback.format_exc())
  339. try:
  340. self.close()
  341. except Exception:
  342. logger.critical(traceback.format_exc())
  343. def close(self):
  344. if not self._closed:
  345. self._closed = True
  346. Acceptor.close(self)
  347. if self._idler is not None and not self._idler.cancelled:
  348. self._idler.cancel()
  349. class ActiveDTP(Connector):
  350. """Connects to remote client and dispatches the resulting connection
  351. to DTPHandler. Used for handling PORT command.
  352. - (int) timeout: the timeout for us to establish connection with
  353. the client's listening data socket.
  354. """
  355. timeout = 30
  356. def __init__(self, ip, port, cmd_channel):
  357. """Initialize the active data channel attemping to connect
  358. to remote data socket.
  359. - (str) ip: the remote IP address.
  360. - (int) port: the remote port.
  361. - (instance) cmd_channel: the command channel class instance.
  362. """
  363. Connector.__init__(self, ioloop=cmd_channel.ioloop)
  364. self.cmd_channel = cmd_channel
  365. self.log = cmd_channel.log
  366. self.log_exception = cmd_channel.log_exception
  367. self._closed = False
  368. self._idler = None
  369. if self.timeout:
  370. self._idler = self.ioloop.call_later(self.timeout,
  371. self.handle_timeout,
  372. _errback=self.handle_error)
  373. if ip.count('.') == 4:
  374. self._cmd = "PORT"
  375. self._normalized_addr = "%s:%s" % (ip, port)
  376. else:
  377. self._cmd = "EPRT"
  378. self._normalized_addr = "[%s]:%s" % (ip, port)
  379. source_ip = self.cmd_channel.socket.getsockname()[0]
  380. # dual stack IPv4/IPv6 support
  381. try:
  382. self.connect_af_unspecified((ip, port), (source_ip, 0))
  383. except (socket.gaierror, socket.error):
  384. self.handle_close()
  385. def readable(self):
  386. return False
  387. def handle_write(self):
  388. # overridden to prevent unhandled read/write event messages to
  389. # be printed by asyncore on Python < 2.6
  390. pass
  391. def handle_connect(self):
  392. """Called when connection is established."""
  393. self.del_channel()
  394. if self._idler is not None and not self._idler.cancelled:
  395. self._idler.cancel()
  396. if not self.cmd_channel.connected:
  397. return self.close()
  398. # fix for asyncore on python < 2.6, meaning we aren't
  399. # actually connected.
  400. # test_active_conn_error tests this condition
  401. err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
  402. if err != 0:
  403. raise socket.error(err)
  404. #
  405. msg = 'Active data connection established.'
  406. self.cmd_channel.respond('200 ' + msg)
  407. self.cmd_channel.log_cmd(self._cmd, self._normalized_addr, 200, msg)
  408. #
  409. if not self.cmd_channel.connected:
  410. return self.close()
  411. # delegate such connection to DTP handler
  412. handler = self.cmd_channel.dtp_handler(self.socket, self.cmd_channel)
  413. self.cmd_channel.data_channel = handler
  414. self.cmd_channel._on_dtp_connection()
  415. def handle_timeout(self):
  416. if self.cmd_channel.connected:
  417. msg = "Active data channel timed out."
  418. self.cmd_channel.respond("421 " + msg, logfun=logger.info)
  419. self.cmd_channel.log_cmd(self._cmd, self._normalized_addr, 421, msg)
  420. self.close()
  421. def handle_close(self):
  422. # With the new IO loop, handle_close() gets called in case
  423. # the fd appears in the list of exceptional fds.
  424. # This means connect() failed.
  425. if not self._closed:
  426. self.close()
  427. if self.cmd_channel.connected:
  428. msg = "Can't connect to specified address."
  429. self.cmd_channel.respond("425 " + msg)
  430. self.cmd_channel.log_cmd(self._cmd, self._normalized_addr, 425, msg)
  431. def handle_error(self):
  432. """Called to handle any uncaught exceptions."""
  433. try:
  434. raise
  435. except (socket.gaierror, socket.error):
  436. pass
  437. except Exception:
  438. self.log_exception(self)
  439. try:
  440. self.handle_close()
  441. except Exception:
  442. logger.critical(traceback.format_exc())
  443. def close(self):
  444. if not self._closed:
  445. self._closed = True
  446. if self.socket is not None:
  447. Connector.close(self)
  448. if self._idler is not None and not self._idler.cancelled:
  449. self._idler.cancel()
  450. class DTPHandler(AsyncChat):
  451. """Class handling server-data-transfer-process (server-DTP, see
  452. RFC-959) managing data-transfer operations involving sending
  453. and receiving data.
  454. Class attributes:
  455. - (int) timeout: the timeout which roughly is the maximum time we
  456. permit data transfers to stall for with no progress. If the
  457. timeout triggers, the remote client will be kicked off
  458. (defaults 300).
  459. - (int) ac_in_buffer_size: incoming data buffer size (defaults 65536)
  460. - (int) ac_out_buffer_size: outgoing data buffer size (defaults 65536)
  461. """
  462. timeout = 300
  463. ac_in_buffer_size = 65536
  464. ac_out_buffer_size = 65536
  465. def __init__(self, sock, cmd_channel):
  466. """Initialize the command channel.
  467. - (instance) sock: the socket object instance of the newly
  468. established connection.
  469. - (instance) cmd_channel: the command channel class instance.
  470. """
  471. self.cmd_channel = cmd_channel
  472. self.file_obj = None
  473. self.receive = False
  474. self.transfer_finished = False
  475. self.tot_bytes_sent = 0
  476. self.tot_bytes_received = 0
  477. self.cmd = None
  478. self.log = cmd_channel.log
  479. self.log_exception = cmd_channel.log_exception
  480. self._data_wrapper = None
  481. self._lastdata = 0
  482. self._closed = False
  483. self._had_cr = False
  484. self._start_time = timer()
  485. self._resp = ()
  486. self._offset = None
  487. self._filefd = None
  488. self._idler = None
  489. self._initialized = False
  490. try:
  491. AsyncChat.__init__(self, sock, ioloop=cmd_channel.ioloop)
  492. except socket.error:
  493. err = sys.exc_info()[1]
  494. # if we get an exception here we want the dispatcher
  495. # instance to set socket attribute before closing, see:
  496. # http://code.google.com/p/pyftpdlib/issues/detail?id=188
  497. AsyncChat.__init__(self, socket.socket(), ioloop=cmd_channel.ioloop)
  498. # http://code.google.com/p/pyftpdlib/issues/detail?id=143
  499. self.close()
  500. if err.args[0] == errno.EINVAL:
  501. return
  502. self.handle_error()
  503. return
  504. # remove this instance from IOLoop's socket map
  505. if not self.connected:
  506. self.close()
  507. return
  508. if self.timeout:
  509. self._idler = self.ioloop.call_every(self.timeout,
  510. self.handle_timeout,
  511. _errback=self.handle_error)
  512. def __repr__(self):
  513. try:
  514. addr = "%s:%s" % self.socket.getpeername()[:2]
  515. except socket.error:
  516. addr = None
  517. status = [self.__class__.__module__+ "." + self.__class__.__name__]
  518. status.append("(addr=%s, user=%r, receive=%r, file=%r)" \
  519. % (addr, self.cmd_channel.username or '',
  520. self.receive, getattr(self.file_obj, 'name', '')))
  521. return '<%s at %#x>' % (' '.join(status), id(self))
  522. __str__ = __repr__
  523. def _use_sendfile(self, producer):
  524. return self.cmd_channel.use_sendfile \
  525. and isinstance(producer, FileProducer) \
  526. and producer.type == 'i'
  527. def push(self, data):
  528. self._initialized = True
  529. self.ioloop.modify(self._fileno, self.ioloop.WRITE)
  530. AsyncChat.push(self, data)
  531. def push_with_producer(self, producer):
  532. self._initialized = True
  533. self.ioloop.modify(self._fileno, self.ioloop.WRITE)
  534. if self._use_sendfile(producer):
  535. self._offset = producer.file.tell()
  536. self._filefd = self.file_obj.fileno()
  537. self.initiate_sendfile()
  538. self.initiate_send = self.initiate_sendfile
  539. else:
  540. AsyncChat.push_with_producer(self, producer)
  541. def close_when_done(self):
  542. asynchat.async_chat.close_when_done(self)
  543. def initiate_send(self):
  544. asynchat.async_chat.initiate_send(self)
  545. def initiate_sendfile(self):
  546. """A wrapper around sendfile."""
  547. try:
  548. sent = sendfile(self._fileno, self._filefd, self._offset,
  549. self.ac_out_buffer_size)
  550. except OSError:
  551. err = sys.exc_info()[1]
  552. if err.errno in (errno.EAGAIN, errno.EWOULDBLOCK, errno.EBUSY):
  553. return
  554. elif err.errno in _DISCONNECTED:
  555. self.handle_close()
  556. else:
  557. raise
  558. else:
  559. if sent == 0:
  560. # this signals the channel that the transfer is completed
  561. self.discard_buffers()
  562. self.handle_close()
  563. else:
  564. self._offset += sent
  565. self.tot_bytes_sent += sent
  566. # --- utility methods
  567. def _posix_ascii_data_wrapper(self, chunk):
  568. """The data wrapper used for receiving data in ASCII mode on
  569. systems using a single line terminator, handling those cases
  570. where CRLF ('\r\n') gets delivered in two chunks.
  571. """
  572. if self._had_cr:
  573. chunk = b('\r') + chunk
  574. if chunk.endswith(b('\r')):
  575. self._had_cr = True
  576. chunk = chunk[:-1]
  577. else:
  578. self._had_cr = False
  579. return chunk.replace(b('\r\n'), b(os.linesep))
  580. def enable_receiving(self, type, cmd):
  581. """Enable receiving of data over the channel. Depending on the
  582. TYPE currently in use it creates an appropriate wrapper for the
  583. incoming data.
  584. - (str) type: current transfer type, 'a' (ASCII) or 'i' (binary).
  585. """
  586. self._initialized = True
  587. self.ioloop.modify(self._fileno, self.ioloop.READ)
  588. self.cmd = cmd
  589. if type == 'a':
  590. if os.linesep == '\r\n':
  591. self._data_wrapper = None
  592. else:
  593. self._data_wrapper = self._posix_ascii_data_wrapper
  594. elif type == 'i':
  595. self._data_wrapper = None
  596. else:
  597. raise TypeError("unsupported type")
  598. self.receive = True
  599. def get_transmitted_bytes(self):
  600. """Return the number of transmitted bytes."""
  601. return self.tot_bytes_sent + self.tot_bytes_received
  602. def get_elapsed_time(self):
  603. """Return the transfer elapsed time in seconds."""
  604. return timer() - self._start_time
  605. def transfer_in_progress(self):
  606. """Return True if a transfer is in progress, else False."""
  607. return self.get_transmitted_bytes() != 0
  608. # --- connection
  609. def send(self, data):
  610. result = AsyncChat.send(self, data)
  611. self.tot_bytes_sent += result
  612. return result
  613. def refill_buffer(self):
  614. """Overridden as a fix around http://bugs.python.org/issue1740572
  615. (when the producer is consumed, close() was called instead of
  616. handle_close()).
  617. """
  618. while 1:
  619. if len(self.producer_fifo):
  620. p = self.producer_fifo.first()
  621. # a 'None' in the producer fifo is a sentinel,
  622. # telling us to close the channel.
  623. if p is None:
  624. if not self.ac_out_buffer:
  625. self.producer_fifo.pop()
  626. #self.close()
  627. self.handle_close()
  628. return
  629. elif isinstance(p, str):
  630. self.producer_fifo.pop()
  631. self.ac_out_buffer += p
  632. return
  633. data = p.more()
  634. if data:
  635. self.ac_out_buffer = self.ac_out_buffer + data
  636. return
  637. else:
  638. self.producer_fifo.pop()
  639. else:
  640. return
  641. def handle_read(self):
  642. """Called when there is data waiting to be read."""
  643. try:
  644. chunk = self.recv(self.ac_in_buffer_size)
  645. except socket.error:
  646. self.handle_error()
  647. else:
  648. self.tot_bytes_received += len(chunk)
  649. if not chunk:
  650. self.transfer_finished = True
  651. #self.close() # <-- asyncore.recv() already do that...
  652. return
  653. if self._data_wrapper is not None:
  654. chunk = self._data_wrapper(chunk)
  655. try:
  656. self.file_obj.write(chunk)
  657. except OSError:
  658. err = sys.exc_info()[1]
  659. raise _FileReadWriteError(err)
  660. handle_read_event = handle_read # small speedup
  661. def readable(self):
  662. """Predicate for inclusion in the readable for select()."""
  663. # It the channel is not supposed to be receiving but yet it's
  664. # in the list of readable events, that means it has been
  665. # disconnected, in which case we explicitly close() it.
  666. # This is necessary as differently from FTPHandler this channel
  667. # is not supposed to be readable/writable at first, meaning the
  668. # upper IOLoop might end up calling readable() repeatedly,
  669. # hogging CPU resources.
  670. if not self.receive and not self._initialized:
  671. return self.close()
  672. return self.receive
  673. def writable(self):
  674. """Predicate for inclusion in the writable for select()."""
  675. return not self.receive and asynchat.async_chat.writable(self)
  676. def handle_timeout(self):
  677. """Called cyclically to check if data trasfer is stalling with
  678. no progress in which case the client is kicked off.
  679. """
  680. if self.get_transmitted_bytes() > self._lastdata:
  681. self._lastdata = self.get_transmitted_bytes()
  682. else:
  683. msg = "Data connection timed out."
  684. self._resp = ("421 " + msg, logger.info)
  685. self.close()
  686. self.cmd_channel.close_when_done()
  687. def handle_error(self):
  688. """Called when an exception is raised and not otherwise handled."""
  689. try:
  690. raise
  691. # an error could occur in case we fail reading / writing
  692. # from / to file (e.g. file system gets full)
  693. except _FileReadWriteError:
  694. err = sys.exc_info()[1]
  695. error = _strerror(err.args[0])
  696. except Exception:
  697. # some other exception occurred; we don't want to provide
  698. # confidential error messages
  699. self.log_exception(self)
  700. error = "Internal error"
  701. try:
  702. self._resp = ("426 %s; transfer aborted." % error, logger.warning)
  703. self.close()
  704. except Exception:
  705. logger.critical(traceback.format_exc())
  706. def handle_close(self):
  707. """Called when the socket is closed."""
  708. # If we used channel for receiving we assume that transfer is
  709. # finished when client closes the connection, if we used channel
  710. # for sending we have to check that all data has been sent
  711. # (responding with 226) or not (responding with 426).
  712. # In both cases handle_close() is automatically called by the
  713. # underlying asynchat module.
  714. if not self._closed:
  715. if self.receive:
  716. self.transfer_finished = True
  717. else:
  718. self.transfer_finished = len(self.producer_fifo) == 0
  719. try:
  720. if self.transfer_finished:
  721. self._resp = ("226 Transfer complete.", logger.debug)
  722. else:
  723. tot_bytes = self.get_transmitted_bytes()
  724. self._resp = ("426 Transfer aborted; %d bytes transmitted." \
  725. % tot_bytes, logger.debug)
  726. finally:
  727. self.close()
  728. def close(self):
  729. """Close the data channel, first attempting to close any remaining
  730. file handles."""
  731. if not self._closed:
  732. self._closed = True
  733. # RFC-959 says we must close the connection before replying
  734. AsyncChat.close(self)
  735. if self._resp:
  736. self.cmd_channel.respond(self._resp[0], logfun=self._resp[1])
  737. if self.file_obj is not None and not self.file_obj.closed:
  738. self.file_obj.close()
  739. if self._idler is not None and not self._idler.cancelled:
  740. self._idler.cancel()
  741. if self.file_obj is not None:
  742. filename = self.file_obj.name
  743. elapsed_time = round(self.get_elapsed_time(), 3)
  744. self.cmd_channel.log_transfer(cmd=self.cmd,
  745. filename=self.file_obj.name,
  746. receive=self.receive,
  747. completed=self.transfer_finished,
  748. elapsed=elapsed_time,
  749. bytes=self.get_transmitted_bytes())
  750. if self.transfer_finished:
  751. if self.receive:
  752. self.cmd_channel.on_file_received(filename)
  753. else:
  754. self.cmd_channel.on_file_sent(filename)
  755. else:
  756. if self.receive:
  757. self.cmd_channel.on_incomplete_file_received(filename)
  758. else:
  759. self.cmd_channel.on_incomplete_file_sent(filename)
  760. self.cmd_channel._on_dtp_close()
  761. # dirty hack in order to turn AsyncChat into a new style class in
  762. # python 2.x so that we can use super()
  763. if PY3:
  764. class _AsyncChatNewStyle(AsyncChat):
  765. pass
  766. else:
  767. class _AsyncChatNewStyle(object, AsyncChat):
  768. def __init__(self, *args, **kwargs):
  769. super(object, self).__init__(*args, **kwargs) # bypass object
  770. class ThrottledDTPHandler(_AsyncChatNewStyle, DTPHandler):
  771. """A DTPHandler subclass which wraps sending and receiving in a data
  772. counter and temporarily "sleeps" the channel so that you burst to no
  773. more than x Kb/sec average.
  774. - (int) read_limit: the maximum number of bytes to read (receive)
  775. in one second (defaults to 0 == no limit).
  776. - (int) write_limit: the maximum number of bytes to write (send)
  777. in one second (defaults to 0 == no limit).
  778. - (bool) auto_sized_buffers: this option only applies when read
  779. and/or write limits are specified. When enabled it bumps down
  780. the data buffer sizes so that they are never greater than read
  781. and write limits which results in a less bursty and smoother
  782. throughput (default: True).
  783. """
  784. read_limit = 0
  785. write_limit = 0
  786. auto_sized_buffers = True
  787. def __init__(self, sock, cmd_channel):
  788. super(ThrottledDTPHandler, self).__init__(sock, cmd_channel)
  789. self._timenext = 0
  790. self._datacount = 0
  791. self.sleeping = False
  792. self._throttler = None
  793. if self.auto_sized_buffers:
  794. if self.read_limit:
  795. while self.ac_in_buffer_size > self.read_limit:
  796. self.ac_in_buffer_size /= 2
  797. if self.write_limit:
  798. while self.ac_out_buffer_size > self.write_limit:
  799. self.ac_out_buffer_size /= 2
  800. self.ac_in_buffer_size = int(self.ac_in_buffer_size)
  801. self.ac_out_buffer_size = int(self.ac_out_buffer_size)
  802. def _use_sendfile(self, producer):
  803. return False
  804. def recv(self, buffer_size):
  805. chunk = super(ThrottledDTPHandler, self).recv(buffer_size)
  806. if self.read_limit:
  807. self._throttle_bandwidth(len(chunk), self.read_limit)
  808. return chunk
  809. def send(self, data):
  810. num_sent = super(ThrottledDTPHandler, self).send(data)
  811. if self.write_limit:
  812. self._throttle_bandwidth(num_sent, self.write_limit)
  813. return num_sent
  814. def _cancel_throttler(self):
  815. if self._throttler is not None and not self._throttler.cancelled:
  816. self._throttler.cancel()
  817. def _throttle_bandwidth(self, len_chunk, max_speed):
  818. """A method which counts data transmitted so that you burst to
  819. no more than x Kb/sec average.
  820. """
  821. self._datacount += len_chunk
  822. if self._datacount >= max_speed:
  823. self._datacount = 0
  824. now = timer()
  825. sleepfor = (self._timenext - now) * 2
  826. if sleepfor > 0:
  827. # we've passed bandwidth limits
  828. def unsleep():
  829. if self.receive:
  830. event = self.ioloop.READ
  831. else:
  832. event = self.ioloop.WRITE
  833. self.add_channel(events=event)
  834. self.del_channel()
  835. self._cancel_throttler()
  836. self._throttler = self.ioloop.call_later(sleepfor, unsleep,
  837. _errback=self.handle_error)
  838. self._timenext = now + 1
  839. def close(self):
  840. self._cancel_throttler()
  841. super(ThrottledDTPHandler, self).close()
  842. # --- producers
  843. class FileProducer(object):
  844. """Producer wrapper for file[-like] objects."""
  845. buffer_size = 65536
  846. def __init__(self, file, type):
  847. """Initialize the producer with a data_wrapper appropriate to TYPE.
  848. - (file) file: the file[-like] object.
  849. - (str) type: the current TYPE, 'a' (ASCII) or 'i' (binary).
  850. """
  851. self.file = file
  852. self.type = type
  853. if type == 'a' and os.linesep != '\r\n':
  854. self._data_wrapper = lambda x: x.replace(b(os.linesep), b('\r\n'))
  855. else:
  856. self._data_wrapper = None
  857. def more(self):
  858. """Attempt a chunk of data of size self.buffer_size."""
  859. try:
  860. data = self.file.read(self.buffer_size)
  861. except OSError:
  862. err = sys.exc_info()[1]
  863. raise _FileReadWriteError(err)
  864. else:
  865. if self._data_wrapper is not None:
  866. data = self._data_wrapper(data)
  867. return data
  868. class BufferedIteratorProducer(object):
  869. """Producer for iterator objects with buffer capabilities."""
  870. # how many times iterator.next() will be called before
  871. # returning some data
  872. loops = 20
  873. def __init__(self, iterator):
  874. self.iterator = iterator
  875. def more(self):
  876. """Attempt a chunk of data from iterator by calling
  877. its next() method different times.
  878. """
  879. buffer = []
  880. for x in xrange(self.loops):
  881. try:
  882. buffer.append(next(self.iterator))
  883. except StopIteration:
  884. break
  885. return b('').join(buffer)
  886. # --- FTP
  887. class FTPHandler(AsyncChat):
  888. """Implements the FTP server Protocol Interpreter (see RFC-959),
  889. handling commands received from the client on the control channel.
  890. All relevant session information is stored in class attributes
  891. reproduced below and can be modified before instantiating this
  892. class.
  893. - (int) timeout:
  894. The timeout which is the maximum time a remote client may spend
  895. between FTP commands. If the timeout triggers, the remote client
  896. will be kicked off. Defaults to 300 seconds.
  897. - (str) banner: the string sent when client connects.
  898. - (int) max_login_attempts:
  899. the maximum number of wrong authentications before disconnecting
  900. the client (default 3).
  901. - (bool)permit_foreign_addresses:
  902. FTP site-to-site transfer feature: also referenced as "FXP" it
  903. permits for transferring a file between two remote FTP servers
  904. without the transfer going through the client's host (not
  905. recommended for security reasons as described in RFC-2577).
  906. Having this attribute set to False means that all data
  907. connections from/to remote IP addresses which do not match the
  908. client's IP address will be dropped (defualt False).
  909. - (bool) permit_privileged_ports:
  910. set to True if you want to permit active data connections (PORT)
  911. over privileged ports (not recommended, defaulting to False).
  912. - (str) masquerade_address:
  913. the "masqueraded" IP address to provide along PASV reply when
  914. pyftpdlib is running behind a NAT or other types of gateways.
  915. When configured pyftpdlib will hide its local address and
  916. instead use the public address of your NAT (default None).
  917. - (dict) masquerade_address_map:
  918. in case the server has multiple IP addresses which are all
  919. behind a NAT router, you may wish to specify individual
  920. masquerade_addresses for each of them. The map expects a
  921. dictionary containing private IP addresses as keys, and their
  922. corresponding public (masquerade) addresses as values.
  923. - (list) passive_ports:
  924. what ports the ftpd will use for its passive data transfers.
  925. Value expected is a list of integers (e.g. range(60000, 65535)).
  926. When configured pyftpdlib will no longer use kernel-assigned
  927. random ports (default None).
  928. - (bool) use_gmt_times:
  929. when True causes the server to report all ls and MDTM times in
  930. GMT and not local time (default True).
  931. - (bool) use_sendfile: when True uses sendfile() system call to
  932. send a file resulting in faster uploads (from server to client).
  933. Works on UNIX only and requires pysendfile module to be
  934. installed separately:
  935. http://code.google.com/p/pysendfile/
  936. Automatically defaults to True if pysendfile module is
  937. installed.
  938. - (bool) tcp_no_delay: controls the use of the TCP_NODELAY socket
  939. option which disables the Nagle algorithm resulting in
  940. significantly better performances (default True on all systems
  941. where it is supported).
  942. - (str) unicode_errors:
  943. the error handler passed to ''.encode() and ''.decode():
  944. http://docs.python.org/library/stdtypes.html#str.decode
  945. (detaults to 'replace').
  946. - (str) log_prefix:
  947. the prefix string preceding any log line; all instance
  948. attributes can be used as arguments.
  949. All relevant instance attributes initialized when client connects
  950. are reproduced below. You may be interested in them in case you
  951. want to subclass the original FTPHandler.
  952. - (bool) authenticated: True if client authenticated himself.
  953. - (str) username: the name of the connected user (if any).
  954. - (int) attempted_logins: number of currently attempted logins.
  955. - (str) current_type: the current transfer type (default "a")
  956. - (int) af: the connection's address family (IPv4/IPv6)
  957. - (instance) server: the FTPServer class instance.
  958. - (instance) data_channel: the data channel instance (if any).
  959. """
  960. # these are overridable defaults
  961. # default classes
  962. authorizer = DummyAuthorizer()
  963. active_dtp = ActiveDTP
  964. passive_dtp = PassiveDTP
  965. dtp_handler = DTPHandler
  966. abstracted_fs = AbstractedFS
  967. proto_cmds = proto_cmds
  968. # session attributes (explained in the docstring)
  969. timeout = 300
  970. banner = "pyftpdlib %s ready." % __ver__
  971. max_login_attempts = 3
  972. permit_foreign_addresses = False
  973. permit_privileged_ports = False
  974. masquerade_address = None
  975. masquerade_address_map = {}
  976. passive_ports = None
  977. use_gmt_times = True
  978. use_sendfile = sendfile is not None
  979. tcp_no_delay = hasattr(socket, "TCP_NODELAY")
  980. unicode_errors = 'replace'
  981. log_prefix = '%(remote_ip)s:%(remote_port)s-[%(username)s]'
  982. def __init__(self, conn, server, ioloop=None):
  983. """Initialize the command channel.
  984. - (instance) conn: the socket object instance of the newly
  985. established connection.
  986. - (instance) server: the ftp server class instance.
  987. """
  988. # public session attributes
  989. self.server = server
  990. self.fs = None
  991. self.authenticated = False
  992. self.username = ""
  993. self.password = ""
  994. self.attempted_logins = 0
  995. self.data_channel = None
  996. self.remote_ip = ""
  997. self.remote_port = ""
  998. # private session attributes
  999. self._last_response = ""
  1000. self._current_type = 'a'
  1001. self._restart_position = 0
  1002. self._quit_pending = False
  1003. self._af = -1
  1004. self._in_buffer = []
  1005. self._in_buffer_len = 0
  1006. self._epsvall = False
  1007. self._dtp_acceptor = None
  1008. self._dtp_connector = None
  1009. self._in_dtp_queue = None
  1010. self._out_dtp_queue = None
  1011. self._closed = False
  1012. self._extra_feats = []
  1013. self._current_facts = ['type', 'perm', 'size', 'modify']
  1014. self._rnfr = None
  1015. self._idler = None
  1016. self._log_debug = logging.getLogger('pyftpdlib').getEffectiveLevel() \
  1017. <= logging.DEBUG
  1018. if os.name == 'posix':
  1019. self._current_facts.append('unique')
  1020. self._available_facts = self._current_facts[:]
  1021. if pwd and grp:
  1022. self._available_facts += ['unix.mode', 'unix.uid', 'unix.gid']
  1023. if os.name == 'nt':
  1024. self._available_facts.append('create')
  1025. try:
  1026. AsyncChat.__init__(self, conn, ioloop=ioloop)
  1027. except socket.error:
  1028. err = sys.exc_info()[1]
  1029. # if we get an exception here we want the dispatcher
  1030. # instance to set socket attribute before closing, see:
  1031. # http://code.google.com/p/pyftpdlib/issues/detail?id=188
  1032. AsyncChat.__init__(self, socket.socket(), ioloop=ioloop)
  1033. self.close()
  1034. if err.args[0] == errno.EINVAL:
  1035. # http://code.google.com/p/pyftpdlib/issues/detail?id=143
  1036. return
  1037. self.handle_error()
  1038. return
  1039. self.set_terminator(b("\r\n"))
  1040. # connection properties
  1041. try:
  1042. self.remote_ip, self.remote_port = self.socket.getpeername()[:2]
  1043. except socket.error:
  1044. err = sys.exc_info()[1]
  1045. # A race condition may occur if the other end is closing
  1046. # before we can get the peername, hence ENOTCONN (see issue
  1047. # #100) while EINVAL can occur on OSX (see issue #143).
  1048. self.connected = False
  1049. if err.args[0] in (errno.ENOTCONN, errno.EINVAL):
  1050. self.close()
  1051. else:
  1052. self.handle_error()
  1053. return
  1054. else:
  1055. self.log("FTP session opened (connect)")
  1056. if hasattr(self.socket, 'family'):
  1057. self._af = self.socket.family
  1058. else: # python < 2.5
  1059. ip, port = self.socket.getsockname()[:2]
  1060. self._af = socket.getaddrinfo(ip, port, socket.AF_UNSPEC,
  1061. socket.SOCK_STREAM)[0][0]
  1062. # try to handle urgent data inline
  1063. try:
  1064. self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_OOBINLINE, 1)
  1065. except socket.error:
  1066. pass
  1067. # disable Nagle algorithm for the control socket only, resulting
  1068. # in significantly better performances
  1069. if self.tcp_no_delay:
  1070. try:
  1071. self.socket.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1)
  1072. except socket.error:
  1073. pass
  1074. # remove this instance from IOLoop's socket_map
  1075. if not self.connected:
  1076. self.close()
  1077. return
  1078. if self.timeout:
  1079. self._idler = self.ioloop.call_later(self.timeout, self.handle_timeout,
  1080. _errback=self.handle_error)
  1081. def __repr__(self):
  1082. status = [self.__class__.__module__+ "." + self.__class__.__name__]
  1083. status.append("(addr=%s:%s, user=%r)" % (self.remote_ip,
  1084. self.remote_port, self.username or ''))
  1085. return '<%s at %#x>' % (' '.join(status), id(self))
  1086. __str__ = __repr__
  1087. def handle(self):
  1088. """Return a 220 'ready' response to the client over the command
  1089. channel.
  1090. """
  1091. self.on_connect()
  1092. if not self._closed and not self._closing:
  1093. if len(self.banner) <= 75:
  1094. self.respond("220 %s" % str(self.banner))
  1095. else:
  1096. self.push('220-%s\r\n' % str(self.banner))
  1097. self.respond('220 ')
  1098. def handle_max_cons(self):
  1099. """Called when limit for maximum number of connections is reached."""
  1100. msg = "421 Too many connections. Service temporarily unavailable."
  1101. self.respond_w_warning(msg)
  1102. # If self.push is used, data could not be sent immediately in
  1103. # which case a new "loop" will occur exposing us to the risk of
  1104. # accepting new connections. Since this could cause asyncore to
  1105. # run out of fds in case we're using select() on Windows we
  1106. # immediately close the channel by using close() instead of
  1107. # close_when_done(). If data has not been sent yet client will
  1108. # be silently disconnected.
  1109. self.close()
  1110. def handle_max_cons_per_ip(self):
  1111. """Called when too many clients are connected from the same IP."""
  1112. msg = "421 Too many connections from the same IP address."
  1113. self.respond_w_warning(msg)
  1114. self.close_when_done()
  1115. def handle_timeout(self):
  1116. """Called when client does not send any command within the time
  1117. specified in <timeout> attribute."""
  1118. msg = "Control connection timed out."
  1119. self.respond("421 " + msg, logfun=logger.info)
  1120. self.close_when_done()
  1121. # --- asyncore / asynchat overridden methods
  1122. def readable(self):
  1123. # Checking for self.connected seems to be necessary as per:
  1124. # http://code.google.com/p/pyftpdlib/issues/detail?id=188#c18
  1125. # In contrast to DTPHandler, here we are not interested in
  1126. # attempting to receive any further data from a closed socket.
  1127. return self.connected and AsyncChat.readable(self)
  1128. def writable(self):
  1129. return self.connected and AsyncChat.writable(self)
  1130. def collect_incoming_data(self, data):
  1131. """Read incoming data and append to the input buffer."""
  1132. self._in_buffer.append(data)
  1133. self._in_buffer_len += len(data)
  1134. # Flush buffer if it gets too long (possible DoS attacks).
  1135. # RFC-959 specifies that a 500 response could be given in
  1136. # such cases
  1137. buflimit = 2048
  1138. if self._in_buffer_len > buflimit:
  1139. self.respond_w_warning('500 Command too long.')
  1140. self._in_buffer = []
  1141. self._in_buffer_len = 0
  1142. def decode(self, bytes):
  1143. return bytes.decode('utf8', self.unicode_errors)
  1144. def found_terminator(self):
  1145. r"""Called when the incoming data stream matches the \r\n
  1146. terminator.
  1147. """
  1148. if self._idler is not None and not self._idler.cancelled:
  1149. self._idler.reset()
  1150. line = b('').join(self._in_buffer)
  1151. try:
  1152. line = self.decode(line)
  1153. except UnicodeDecodeError:
  1154. # By default we'll never get here as we replace errors
  1155. # but user might want to override this behavior.
  1156. # RFC-2640 doesn't mention what to do in this case so
  1157. # we'll just return 501 (bad arg).
  1158. return self.respond("501 Can't decode command.")
  1159. self._in_buffer = []
  1160. self._in_buffer_len = 0
  1161. cmd = line.split(' ')[0].upper()
  1162. arg = line[len(cmd)+1:]
  1163. try:
  1164. self.pre_process_command(line, cmd, arg)
  1165. except UnicodeEncodeError:
  1166. self.respond("501 can't decode path (server filesystem encoding " \
  1167. "is %s)" % sys.getfilesystemencoding())
  1168. def pre_process_command(self, line, cmd, arg):
  1169. kwargs = {}
  1170. if cmd == "SITE" and arg:
  1171. cmd = "SITE %s" % arg.split(' ')[0].upper()
  1172. arg = line[len(cmd)+1:]
  1173. if cmd != 'PASS':
  1174. self.logline("<- %s" % line)
  1175. else:
  1176. self.logline("<- %s %s" % (line.split(' ')[0], '*' * 6))
  1177. # Recognize those commands having a "special semantic". They
  1178. # should be sent by following the RFC-959 procedure of sending
  1179. # Telnet IP/Synch sequence (chr 242 and 255) as OOB data but
  1180. # since many ftp clients don't do it correctly we check the
  1181. # last 4 characters only.
  1182. if not cmd in self.proto_cmds:
  1183. if cmd[-4:] in ('ABOR', 'STAT', 'QUIT'):
  1184. cmd = cmd[-4:]
  1185. else:
  1186. msg = 'Command "%s" not understood.' % cmd
  1187. self.respond('500 ' + msg)
  1188. if cmd:
  1189. self.log_cmd(cmd, arg, 500, msg)
  1190. return
  1191. if not arg and self.proto_cmds[cmd]['arg'] == True:
  1192. msg = "Syntax error: command needs an argument."
  1193. self.respond("501 " + msg)
  1194. self.log_cmd(cmd, "", 501, msg)
  1195. return
  1196. if arg and self.proto_cmds[cmd]['arg'] == False:
  1197. msg = "Syntax error: command does not accept arguments."
  1198. self.respond("501 " + msg)
  1199. self.log_cmd(cmd, arg, 501, msg)
  1200. return
  1201. if not self.authenticated:
  1202. if self.proto_cmds[cmd]['auth'] or (cmd == 'STAT' and arg):
  1203. msg = "Log in with USER and PASS first."
  1204. self.respond("530 " + msg)
  1205. self.log_cmd(cmd, arg, 530, msg)
  1206. else:
  1207. # call the proper ftp_* method
  1208. self.process_command(cmd, arg)
  1209. return
  1210. else:
  1211. if (cmd == 'STAT') and not arg:
  1212. self.ftp_STAT(u(''))
  1213. return
  1214. # for file-system related commands check whether real path
  1215. # destination is valid
  1216. if self.proto_cmds[cmd]['perm'] and (cmd != 'STOU'):
  1217. if cmd in ('CWD', 'XCWD'):
  1218. arg = self.fs.ftp2fs(arg or u('/'))
  1219. elif cmd in ('CDUP', 'XCUP'):
  1220. arg = self.fs.ftp2fs(u('..'))
  1221. elif cmd == 'LIST':
  1222. if arg.lower() in ('-a', '-l', '-al', '-la'):
  1223. arg = self.fs.ftp2fs(self.fs.cwd)
  1224. else:
  1225. arg = self.fs.ftp2fs(arg or self.fs.cwd)
  1226. elif cmd == 'STAT':
  1227. if glob.has_magic(arg):
  1228. msg = 'Globbing not supported.'
  1229. self.respond('550 ' + msg)
  1230. self.log_cmd(cmd, arg, 550, msg)
  1231. return
  1232. arg = self.fs.ftp2fs(arg or self.fs.cwd)
  1233. elif cmd == 'SITE CHMOD':
  1234. if not ' ' in arg:
  1235. msg = "Syntax error: command needs two arguments."
  1236. self.respond("501 " + msg)
  1237. self.log_cmd(cmd, "", 501, msg)
  1238. return
  1239. else:
  1240. mode, arg = arg.split(' ', 1)
  1241. arg = self.fs.ftp2fs(arg)
  1242. kwargs = dict(mode=mode)
  1243. else: # LIST, NLST, MLSD, MLST
  1244. arg = self.fs.ftp2fs(arg or self.fs.cwd)
  1245. if not self.fs.validpath(arg):
  1246. line = self.fs.fs2ftp(arg)
  1247. msg = '"%s" points to a path which is outside ' \
  1248. "the user's root directory" % line
  1249. self.respond("550 %s." % msg)
  1250. self.log_cmd(cmd, arg, 550, msg)
  1251. return
  1252. # check permission
  1253. perm = self.proto_cmds[cmd]['perm']
  1254. if perm is not None and cmd != 'STOU':
  1255. if not self.authorizer.has_perm(self.username, perm, arg):
  1256. msg = "Not enough privileges."
  1257. self.respond("550 " + msg)
  1258. self.log_cmd(cmd, arg, 550, msg)
  1259. return
  1260. # call the proper ftp_* method
  1261. self.process_command(cmd, arg, **kwargs)
  1262. def process_command(self, cmd, *args, **kwargs):
  1263. """Process command by calling the corresponding ftp_* class
  1264. method (e.g. for received command "MKD pathname", ftp_MKD()
  1265. method is called with "pathname" as the argument).
  1266. """
  1267. if self._closed:
  1268. return
  1269. self._last_response = ""
  1270. method = getattr(self, 'ftp_' + cmd.replace(' ', '_'))
  1271. method(*args, **kwargs)
  1272. if self._last_response:
  1273. code = int(self._last_response[:3])
  1274. resp = self._last_response[4:]
  1275. self.log_cmd(cmd, args[0], code, resp)
  1276. def handle_error(self):
  1277. try:
  1278. self.log_exception(self)
  1279. self.close()
  1280. except Exception:
  1281. logger.critical(traceback.format_exc())
  1282. def handle_close(self):
  1283. self.close()
  1284. def close(self):
  1285. """Close the current channel disconnecting the client."""
  1286. if not self._closed:
  1287. self._closed = True
  1288. self._closing = False
  1289. self.connected = False
  1290. AsyncChat.close(self)
  1291. self._shutdown_connecting_dtp()
  1292. if self.data_channel is not None:
  1293. self.data_channel.close()
  1294. del self.data_channel
  1295. if self._out_dtp_queue is not None:
  1296. file = self._out_dtp_queue[2]
  1297. if file is not None:
  1298. file.close()
  1299. if self._in_dtp_queue is not None:
  1300. file = self._in_dtp_queue[0]
  1301. if file is not None:
  1302. file.close()
  1303. del self._out_dtp_queue
  1304. del self._in_dtp_queue
  1305. if self._idler is not None and not self._idler.cancelled:
  1306. self._idler.cancel()
  1307. # remove client IP address from ip map
  1308. if self.remote_ip in self.server.ip_map:
  1309. self.server.ip_map.remove(self.remote_ip)
  1310. if self.fs is not None:
  1311. self.fs.cmd_channel = None
  1312. self.fs = None
  1313. self.log("FTP session closed (disconnect).")
  1314. # Having self.remote_ip not set means that no connection
  1315. # actually took place, hence we're not interested in
  1316. # invoking the callback.
  1317. if self.remote_ip:
  1318. self.ioloop.call_later(0, self.on_disconnect,
  1319. _errback=self.handle_error)
  1320. def _shutdown_connecting_dtp(self):
  1321. """Close any ActiveDTP or PassiveDTP instance waiting to
  1322. establish a connection (passive or active).
  1323. """
  1324. if self._dtp_acceptor is not None:
  1325. self._dtp_acceptor.close()
  1326. self._dtp_acceptor = None
  1327. if self._dtp_connector is not None:
  1328. self._dtp_connector.close()
  1329. self._dtp_connector = None
  1330. # --- public callbacks
  1331. # Note: to run a time consuming task make sure to use a separate
  1332. # process or thread (see FAQs).
  1333. def on_connect(self):
  1334. """Called when client connects, *before* sending the initial
  1335. 220 reply.
  1336. """
  1337. def on_disconnect(self):
  1338. """Called when connection is closed."""
  1339. def on_login(self, username):
  1340. """Called on user login."""
  1341. def on_login_failed(self, username, password):
  1342. """Called on failed login attempt.
  1343. At this point client might have already been disconnected if it
  1344. failed too many times.
  1345. """
  1346. def on_logout(self, username):
  1347. """Called when user "cleanly" logs out due to QUIT or USER
  1348. issued twice (re-login). This is not called if the connection
  1349. is simply closed by client.
  1350. """
  1351. def on_file_sent(self, file):
  1352. """Called every time a file has been succesfully sent.
  1353. "file" is the absolute name of the file just being sent.
  1354. """
  1355. def on_file_received(self, file):
  1356. """Called every time a file has been succesfully received.
  1357. "file" is the absolute name of the file just being received.
  1358. """
  1359. def on_incomplete_file_sent(self, file):
  1360. """Called every time a file has not been entirely sent.
  1361. (e.g. ABOR during transfer or client disconnected).
  1362. "file" is the absolute name of that file.
  1363. """
  1364. def on_incomplete_file_received(self, file):
  1365. """Called every time a file has not been entirely received
  1366. (e.g. ABOR during transfer or client disconnected).
  1367. "file" is the absolute name of that file.
  1368. """
  1369. # --- internal callbacks
  1370. def _on_dtp_connection(self):
  1371. """Called every time data channel connects, either active or
  1372. passive.
  1373. Incoming and outgoing queues are checked for pending data.
  1374. If outbound data is pending, it is pushed into the data channel.
  1375. If awaiting inbound data, the data channel is enabled for
  1376. receiving.
  1377. """
  1378. # Close accepting DTP only. By closing ActiveDTP DTPHandler
  1379. # would receive a closed socket object.
  1380. #self._shutdown_connecting_dtp()
  1381. if self._dtp_acceptor is not None:
  1382. self._dtp_acceptor.close()
  1383. self._dtp_acceptor = None
  1384. # stop the idle timer as long as the data transfer is not finished
  1385. if self._idler is not None and not self._idler.cancelled:
  1386. self._idler.cancel()
  1387. # check for data to send
  1388. if self._out_dtp_queue is not None:
  1389. data, isproducer, file, cmd = self._out_dtp_queue
  1390. self._out_dtp_queue = None
  1391. self.data_channel.cmd = cmd
  1392. if file:
  1393. self.data_channel.file_obj = file
  1394. try:
  1395. if not isproducer:
  1396. self.data_channel.push(data)
  1397. else:
  1398. self.data_channel.push_with_producer(data)
  1399. if self.data_channel is not None:
  1400. self.data_channel.close_when_done()
  1401. except:
  1402. # dealing with this exception is up to DTP (see bug #84)
  1403. self.data_channel.handle_error()
  1404. # check for data to receive
  1405. elif self._in_dtp_queue is not None:
  1406. file, cmd = self._in_dtp_queue
  1407. self.data_channel.file_obj = file
  1408. self._in_dtp_queue = None
  1409. self.data_channel.enable_receiving(self._current_type, cmd)
  1410. def _on_dtp_close(self):
  1411. """Called every time the data channel is closed."""
  1412. self.data_channel = None
  1413. if self._quit_pending:
  1414. self.close()
  1415. elif self.timeout:
  1416. # data transfer finished, restart the idle timer
  1417. if self._idler is not None and not self._idler.cancelled:
  1418. self._idler.cancel()
  1419. self._idler = self.ioloop.call_later(self.timeout,
  1420. self.handle_timeout,
  1421. _errback=self.handle_error)
  1422. # --- utility
  1423. def push(self, s):
  1424. asynchat.async_chat.push(self, s.encode('utf8'))
  1425. def respond(self, resp, logfun=logger.debug):
  1426. """Send a response to the client using the command channel."""
  1427. self._last_response = resp
  1428. self.push(resp + '\r\n')
  1429. if self._log_debug:
  1430. self.logline('-> %s' % resp, logfun=logfun)
  1431. else:
  1432. self.log(resp[4:], logfun=logfun)
  1433. def respond_w_warning(self, resp):
  1434. self.respond(resp, logfun=logger.warning)
  1435. def push_dtp_data(self, data, isproducer=False, file=None, cmd=None):
  1436. """Pushes data into the data channel.
  1437. It is usually called for those commands requiring some data to
  1438. be sent over the data channel (e.g. RETR).
  1439. If data channel does not exist yet, it queues the data to send
  1440. later; data will then be pushed into data channel when
  1441. _on_dtp_connection() will be called.
  1442. - (str/classobj) data: the data to send which may be a string
  1443. or a producer object).
  1444. - (bool) isproducer: whether treat data as a producer.
  1445. - (file) file: the file[-like] object to send (if any).
  1446. """
  1447. if self.data_channel is not None:
  1448. self.respond("125 Data connection already open. Transfer starting.")
  1449. if file:
  1450. self.data_channel.file_obj = file
  1451. try:
  1452. if not isproducer:
  1453. self.data_channel.push(data)
  1454. else:
  1455. self.data_channel.push_with_producer(data)
  1456. if self.data_channel is not None:
  1457. self.data_channel.cmd = cmd
  1458. self.data_channel.close_when_done()
  1459. except:
  1460. # dealing with this exception is up to DTP (see bug #84)
  1461. self.data_channel.handle_error()
  1462. else:
  1463. self.respond("150 File status okay. About to open data connection.")
  1464. self._out_dtp_queue = (data, isproducer, file, cmd)
  1465. def flush_account(self):
  1466. """Flush account information by clearing attributes that need
  1467. to be reset on a REIN or new USER command.
  1468. """
  1469. self._shutdown_connecting_dtp()
  1470. # if there's a transfer in progress RFC-959 states we are
  1471. # supposed to let it finish
  1472. if self.data_channel is not None:
  1473. if not self.data_channel.transfer_in_progress():
  1474. self.data_channel.close()
  1475. self.data_channel = None
  1476. username = self.username
  1477. if self.authenticated and username:
  1478. self.on_logout(username)
  1479. self.authenticated = False
  1480. self.username = ""
  1481. self.password = ""
  1482. self.attempted_logins = 0
  1483. self._current_type = 'a'
  1484. self._restart_position = 0
  1485. self._quit_pending = False
  1486. self._in_dtp_queue = None
  1487. self._rnfr = None
  1488. self._out_dtp_queue = None
  1489. def run_as_current_user(self, function, *args, **kwargs):
  1490. """Execute a function impersonating the current logged-in user."""
  1491. self.authorizer.impersonate_user(self.username, self.password)
  1492. try:
  1493. return function(*args, **kwargs)
  1494. finally:
  1495. self.authorizer.terminate_impersonation(self.username)
  1496. # --- logging wrappers
  1497. # this is defined earlier
  1498. #log_prefix = '%(remote_ip)s:%(remote_port)s-[%(username)s]'
  1499. def log(self, msg, logfun=logger.info):
  1500. """Log a message, including additional identifying session data."""
  1501. prefix = self.log_prefix % self.__dict__
  1502. logfun("%s %s" % (prefix, msg))
  1503. def logline(self, msg, logfun=logger.debug):
  1504. """Log a line including additional indentifying session data.
  1505. By default this is disabled unless logging level == DEBUG.
  1506. """
  1507. if self._log_debug:
  1508. prefix = self.log_prefix % self.__dict__
  1509. logfun("%s %s" % (prefix, msg))
  1510. def logerror(self, msg):
  1511. """Log an error including additional indentifying session data."""
  1512. prefix = self.log_prefix % self.__dict__
  1513. logger.error("%s %s" % (prefix, msg))
  1514. def log_exception(self, instance):
  1515. """Log an unhandled exception. 'instance' is the instance
  1516. where the exception was generated.
  1517. """
  1518. logger.exception("unhandled exception in instance %r", instance)
  1519. # the list of commands which gets logged when logging level
  1520. # is >= logging.INFO
  1521. log_cmds_list = ["DELE", "RNFR", "RNTO", "MKD", "RMD", "CWD",
  1522. "XMKD", "XRMD", "XCWD",
  1523. "REIN", "SITE CHMOD"]
  1524. def log_cmd(self, cmd, arg, respcode, respstr):
  1525. """Log commands and responses in a standardized format.
  1526. This is disabled in case the logging level is set to DEBUG.
  1527. - (str) cmd:
  1528. the command sent by client
  1529. - (str) arg:
  1530. the command argument sent by client.
  1531. For filesystem commands such as DELE, MKD, etc. this is
  1532. already represented as an absolute real filesystem path
  1533. like "/home/user/file.ext".
  1534. - (int) respcode:
  1535. the response code as being sent by server. Response codes
  1536. starting with 4xx or 5xx are returned if the command has
  1537. been rejected for some reason.
  1538. - (str) respstr:
  1539. the response string as being sent by server.
  1540. By default only DELE, RMD, RNTO, MKD, CWD, ABOR, REIN, SITE CHMOD
  1541. commands are logged and the output is redirected to self.log
  1542. method.
  1543. Can be overridden to provide alternate formats or to log
  1544. further commands.
  1545. """
  1546. if not self._log_debug and cmd in self.log_cmds_list:
  1547. line = '%s %s' % (' '.join([cmd, arg]).strip(), respcode)
  1548. if str(respcode)[0] in ('4', '5'):
  1549. line += ' %r' % respstr
  1550. self.log(line)
  1551. def log_transfer(self, cmd, filename, receive, completed, elapsed, bytes):
  1552. """Log all file transfers in a standardized format.
  1553. - (str) cmd:
  1554. the original command who caused the tranfer.
  1555. - (str) filename:
  1556. the absolutized name of the file on disk.
  1557. - (bool) receive:
  1558. True if the transfer was used for client uploading (STOR,
  1559. STOU, APPE), False otherwise (RETR).
  1560. - (bool) completed:
  1561. True if the file has been entirely sent, else False.
  1562. - (float) elapsed:
  1563. transfer elapsed time in seconds.
  1564. - (int) bytes:
  1565. number of bytes transmitted.
  1566. """
  1567. line = '%s %s completed=%s bytes=%s seconds=%s' % \
  1568. (cmd, filename, completed and 1 or 0, bytes, elapsed)
  1569. self.log(line)
  1570. # --- connection
  1571. def _make_eport(self, ip, port):
  1572. """Establish an active data channel with remote client which
  1573. issued a PORT or EPRT command.
  1574. """
  1575. # FTP bounce attacks protection: according to RFC-2577 it's
  1576. # recommended to reject PORT if IP address specified in it
  1577. # does not match client IP address.
  1578. remote_ip = self.remote_ip
  1579. if remote_ip.startswith('::ffff:'):
  1580. # In this scenario, the server has an IPv6 socket, but
  1581. # the remote client is using IPv4 and its address is
  1582. # represented as an IPv4-mapped IPv6 address which
  1583. # looks like this ::ffff:151.12.5.65, see:
  1584. # http://en.wikipedia.org/wiki/IPv6#IPv4-mapped_addresses
  1585. # http://tools.ietf.org/html/rfc3493.html#section-3.7
  1586. # We truncate the first bytes to make it look like a
  1587. # common IPv4 address.
  1588. remote_ip = remote_ip[7:]
  1589. if not self.permit_foreign_addresses and ip != remote_ip:
  1590. msg = "501 Rejected data connection to foreign address %s:%s." \
  1591. % (ip, port)
  1592. self.respond_w_warning(msg)
  1593. return
  1594. # ...another RFC-2577 recommendation is rejecting connections
  1595. # to privileged ports (< 1024) for security reasons.
  1596. if not self.permit_privileged_ports and port < 1024:
  1597. msg = '501 PORT against the privileged port "%s" refused.' % port
  1598. self.respond_w_warning(msg)
  1599. return
  1600. # close establishing DTP instances, if any
  1601. self._shutdown_connecting_dtp()
  1602. if self.data_channel is not None:
  1603. self.data_channel.close()
  1604. self.data_channel = None
  1605. # make sure we are not hitting the max connections limit
  1606. if not self.server._accept_new_cons():
  1607. msg = "425 Too many connections. Can't open data channel."
  1608. self.respond_w_warning(msg)
  1609. return
  1610. # open data channel
  1611. self._dtp_connector = self.active_dtp(ip, port, self)
  1612. def _make_epasv(self, extmode=False):
  1613. """Initialize a passive data channel with remote client which
  1614. issued a PASV or EPSV command.
  1615. If extmode argument is True we assume that client issued EPSV in
  1616. which case extended passive mode will be used (see RFC-2428).
  1617. """
  1618. # close establishing DTP instances, if any
  1619. self._shutdown_connecting_dtp()
  1620. # close established data connections, if any
  1621. if self.data_channel is not None:
  1622. self.data_channel.close()
  1623. self.data_channel = None
  1624. # make sure we are not hitting the max connections limit
  1625. if not self.server._accept_new_cons():
  1626. msg = "425 Too many connections. Can't open data channel."
  1627. self.respond_w_warning(msg)
  1628. return
  1629. # open data channel
  1630. self._dtp_acceptor = self.passive_dtp(self, extmode)
  1631. def ftp_PORT(self, line):
  1632. """Start an active data channel by using IPv4."""
  1633. if self._epsvall:
  1634. self.respond("501 PORT not allowed after EPSV ALL.")
  1635. return
  1636. # Parse PORT request for getting IP and PORT.
  1637. # Request comes in as:
  1638. # > h1,h2,h3,h4,p1,p2
  1639. # ...where the client's IP address is h1.h2.h3.h4 and the TCP
  1640. # port number is (p1 * 256) + p2.
  1641. try:
  1642. addr = list(map(int, line.split(',')))
  1643. if len(addr) != 6:
  1644. raise ValueError
  1645. for x in addr[:4]:
  1646. if not 0 <= x <= 255:
  1647. raise ValueError
  1648. ip = '%d.%d.%d.%d' % tuple(addr[:4])
  1649. port = (addr[4] * 256) + addr[5]
  1650. if not 0 <= port <= 65535:
  1651. raise ValueError
  1652. except (ValueError, OverflowError):
  1653. self.respond("501 Invalid PORT format.")
  1654. return
  1655. self._make_eport(ip, port)
  1656. def ftp_EPRT(self, line):
  1657. """Start an active data channel by choosing the network protocol
  1658. to use (IPv4/IPv6) as defined in RFC-2428.
  1659. """
  1660. if self._epsvall:
  1661. self.respond("501 EPRT not allowed after EPSV ALL.")
  1662. return
  1663. # Parse EPRT request for getting protocol, IP and PORT.
  1664. # Request comes in as:
  1665. # <d>proto<d>ip<d>port<d>
  1666. # ...where <d> is an arbitrary delimiter character (usually "|") and
  1667. # <proto> is the network protocol to use (1 for IPv4, 2 for IPv6).
  1668. try:
  1669. af, ip, port = line.split(line[0])[1:-1]
  1670. port = int(port)
  1671. if not 0 <= port <= 65535:
  1672. raise ValueError
  1673. except (ValueError, IndexError, OverflowError):
  1674. self.respond("501 Invalid EPRT format.")
  1675. return
  1676. if af == "1":
  1677. # test if AF_INET6 and IPV6_V6ONLY
  1678. if self._af == socket.AF_INET6 and not SUPPORTS_HYBRID_IPV6:
  1679. self.respond('522 Network protocol not supported (use 2).')
  1680. else:
  1681. try:
  1682. octs = list(map(int, ip.split('.')))
  1683. if len(octs) != 4:
  1684. raise ValueError
  1685. for x in octs:
  1686. if not 0 <= x <= 255:
  1687. raise ValueError
  1688. except (ValueError, OverflowError):
  1689. self.respond("501 Invalid EPRT format.")
  1690. else:
  1691. self._make_eport(ip, port)
  1692. elif af == "2":
  1693. if self._af == socket.AF_INET:
  1694. self.respond('522 Network protocol not supported (use 1).')
  1695. else:
  1696. self._make_eport(ip, port)
  1697. else:
  1698. if self._af == socket.AF_INET:
  1699. self.respond('501 Unknown network protocol (use 1).')
  1700. else:
  1701. self.respond('501 Unknown network protocol (use 2).')
  1702. def ftp_PASV(self, line):
  1703. """Start a passive data channel by using IPv4."""
  1704. if self._epsvall:
  1705. self.respond("501 PASV not allowed after EPSV ALL.")
  1706. return
  1707. self._make_epasv(extmode=False)
  1708. def ftp_EPSV(self, line):
  1709. """Start a passive data channel by using IPv4 or IPv6 as defined
  1710. in RFC-2428.
  1711. """
  1712. # RFC-2428 specifies that if an optional parameter is given,
  1713. # we have to determine the address family from that otherwise
  1714. # use the same address family used on the control connection.
  1715. # In such a scenario a client may use IPv4 on the control channel
  1716. # and choose to use IPv6 for the data channel.
  1717. # But how could we use IPv6 on the data channel without knowing
  1718. # which IPv6 address to use for binding the socket?
  1719. # Unfortunately RFC-2428 does not provide satisfing information
  1720. # on how to do that. The assumption is that we don't have any way
  1721. # to know wich address to use, hence we just use the same address
  1722. # family used on the control connection.
  1723. if not line:
  1724. self._make_epasv(extmode=True)
  1725. # IPv4
  1726. elif line == "1":
  1727. if self._af != socket.AF_INET:
  1728. self.respond('522 Network protocol not supported (use 2).')
  1729. else:
  1730. self._make_epasv(extmode=True)
  1731. # IPv6
  1732. elif line == "2":
  1733. if self._af == socket.AF_INET:
  1734. self.respond('522 Network protocol not supported (use 1).')
  1735. else:
  1736. self._make_epasv(extmode=True)
  1737. elif line.lower() == 'all':
  1738. self._epsvall = True
  1739. self.respond('220 Other commands other than EPSV are now disabled.')
  1740. else:
  1741. if self._af == socket.AF_INET:
  1742. self.respond('501 Unknown network protocol (use 1).')
  1743. else:
  1744. self.respond('501 Unknown network protocol (use 2).')
  1745. def ftp_QUIT(self, line):
  1746. """Quit the current session disconnecting the client."""
  1747. if self.authenticated:
  1748. msg_quit = self.authorizer.get_msg_quit(self.username)
  1749. else:
  1750. msg_quit = "Goodbye."
  1751. if len(msg_quit) <= 75:
  1752. self.respond("221 %s" % msg_quit)
  1753. else:
  1754. self.push("221-%s\r\n" % msg_quit)
  1755. self.respond("221 ")
  1756. # From RFC-959:
  1757. # If file transfer is in progress, the connection must remain
  1758. # open for result response and the server will then close it.
  1759. # We also stop responding to any further command.
  1760. if self.data_channel:
  1761. self._quit_pending = True
  1762. self.del_channel()
  1763. else:
  1764. self._shutdown_connecting_dtp()
  1765. self.close_when_done()
  1766. if self.authenticated and self.username:
  1767. self.on_logout(self.username)
  1768. # --- data transferring
  1769. def ftp_LIST(self, path):
  1770. """Return a list of files in the specified directory to the
  1771. client.
  1772. On success return the directory path, else None.
  1773. """
  1774. # - If no argument, fall back on cwd as default.
  1775. # - Some older FTP clients erroneously issue /bin/ls-like LIST
  1776. # formats in which case we fall back on cwd as default.
  1777. try:
  1778. iterator = self.run_as_current_user(self.fs.get_list_dir, path)
  1779. except (OSError, FilesystemError):
  1780. err = sys.exc_info()[1]
  1781. why = _strerror(err)
  1782. self.respond('550 %s.' % why)
  1783. else:
  1784. producer = BufferedIteratorProducer(iterator)
  1785. self.push_dtp_data(producer, isproducer=True, cmd="LIST")
  1786. return path
  1787. def ftp_NLST(self, path):
  1788. """Return a list of files in the specified directory in a
  1789. compact form to the client.
  1790. On success return the directory path, else None.
  1791. """
  1792. try:
  1793. if self.fs.isdir(path):
  1794. listing = self.run_as_current_user(self.fs.listdir, path)
  1795. else:
  1796. # if path is a file we just list its name
  1797. self.fs.lstat(path) # raise exc in case of problems
  1798. listing = [os.path.basename(path)]
  1799. except (OSError, FilesystemError):
  1800. err = sys.exc_info()[1]
  1801. self.respond('550 %s.' % _strerror(err))
  1802. else:
  1803. data = ''
  1804. if listing:
  1805. try:
  1806. listing.sort()
  1807. except UnicodeDecodeError:
  1808. # (Python 2 only) might happen on filesystem not
  1809. # supporting UTF8 meaning os.listdir() returned a list
  1810. # of mixed bytes and unicode strings:
  1811. # http://goo.gl/6DLHD
  1812. # http://bugs.python.org/issue683592
  1813. ls = []
  1814. for x in listing:
  1815. if not isinstance(x, unicode):
  1816. x = unicode(x, 'utf8')
  1817. ls.append(x)
  1818. listing = sorted(ls)
  1819. data = '\r\n'.join(listing) + '\r\n'
  1820. data = data.encode('utf8', self.unicode_errors)
  1821. self.push_dtp_data(data, cmd="NLST")
  1822. return path
  1823. # --- MLST and MLSD commands
  1824. # The MLST and MLSD commands are intended to standardize the file and
  1825. # directory information returned by the server-FTP process. These
  1826. # commands differ from the LIST command in that the format of the
  1827. # replies is strictly defined although extensible.
  1828. def ftp_MLST(self, path):
  1829. """Return information about a pathname in a machine-processable
  1830. form as defined in RFC-3659.
  1831. On success return the path just listed, else None.
  1832. """
  1833. line = self.fs.fs2ftp(path)
  1834. basedir, basename = os.path.split(path)
  1835. perms = self.authorizer.get_perms(self.username)
  1836. try:
  1837. iterator = self.run_as_current_user(self.fs.format_mlsx, basedir,
  1838. [basename], perms, self._current_facts, ignore_err=False)
  1839. data = b('').join(iterator)
  1840. except (OSError, FilesystemError):
  1841. err = sys.exc_info()[1]
  1842. self.respond('550 %s.' % _strerror(err))
  1843. else:
  1844. data = data.decode('utf8', self.unicode_errors)
  1845. # since TVFS is supported (see RFC-3659 chapter 6), a fully
  1846. # qualified pathname should be returned
  1847. data = data.split(' ')[0] + ' %s\r\n' % line
  1848. # response is expected on the command channel
  1849. self.push('250-Listing "%s":\r\n' % line)
  1850. # the fact set must be preceded by a space
  1851. self.push(' ' + data)
  1852. self.respond('250 End MLST.')
  1853. return path
  1854. def ftp_MLSD(self, path):
  1855. """Return contents of a directory in a machine-processable form
  1856. as defined in RFC-3659.
  1857. On success return the path just listed, else None.
  1858. """
  1859. # RFC-3659 requires 501 response code if path is not a directory
  1860. if not self.fs.isdir(path):
  1861. self.respond("501 No such directory.")
  1862. return
  1863. try:
  1864. listing = self.run_as_current_user(self.fs.listdir, path)
  1865. except (OSError, FilesystemError):
  1866. err = sys.exc_info()[1]
  1867. why = _strerror(err)
  1868. self.respond('550 %s.' % why)
  1869. else:
  1870. perms = self.authorizer.get_perms(self.username)
  1871. iterator = self.fs.format_mlsx(path, listing, perms,
  1872. self._current_facts)
  1873. producer = BufferedIteratorProducer(iterator)
  1874. self.push_dtp_data(producer, isproducer=True, cmd="MLSD")
  1875. return path
  1876. def ftp_RETR(self, file):
  1877. """Retrieve the specified file (transfer from the server to the
  1878. client). On success return the file path else None.
  1879. """
  1880. rest_pos = self._restart_position
  1881. self._restart_position = 0
  1882. try:
  1883. fd = self.run_as_current_user(self.fs.open, file, 'rb')
  1884. except (EnvironmentError, FilesystemError):
  1885. err = sys.exc_info()[1]
  1886. why = _strerror(err)
  1887. self.respond('550 %s.' % why)
  1888. return
  1889. if rest_pos:
  1890. # Make sure that the requested offset is valid (within the
  1891. # size of the file being resumed).
  1892. # According to RFC-1123 a 554 reply may result in case that
  1893. # the existing file cannot be repositioned as specified in
  1894. # the REST.
  1895. ok = 0
  1896. try:
  1897. if rest_pos > self.fs.getsize(file):
  1898. raise ValueError
  1899. fd.seek(rest_pos)
  1900. ok = 1
  1901. except ValueError:
  1902. why = "Invalid REST parameter"
  1903. except (EnvironmentError, FilesystemError):
  1904. err = sys.exc_info()[1]
  1905. why = _strerror(err)
  1906. if not ok:
  1907. fd.close()
  1908. self.respond('554 %s' % why)
  1909. return
  1910. producer = FileProducer(fd, self._current_type)
  1911. self.push_dtp_data(producer, isproducer=True, file=fd, cmd="RETR")
  1912. return file
  1913. def ftp_STOR(self, file, mode='w'):
  1914. """Store a file (transfer from the client to the server).
  1915. On success return the file path, else None.
  1916. """
  1917. # A resume could occur in case of APPE or REST commands.
  1918. # In that case we have to open file object in different ways:
  1919. # STOR: mode = 'w'
  1920. # APPE: mode = 'a'
  1921. # REST: mode = 'r+' (to permit seeking on file object)
  1922. if 'a' in mode:
  1923. cmd = 'APPE'
  1924. else:
  1925. cmd = 'STOR'
  1926. rest_pos = self._restart_position
  1927. self._restart_position = 0
  1928. if rest_pos:
  1929. mode = 'r+'
  1930. try:
  1931. fd = self.run_as_current_user(self.fs.open, file, mode + 'b')
  1932. except (EnvironmentError, FilesystemError):
  1933. err = sys.exc_info()[1]
  1934. why = _strerror(err)
  1935. self.respond('550 %s.' %why)
  1936. return
  1937. if rest_pos:
  1938. # Make sure that the requested offset is valid (within the
  1939. # size of the file being resumed).
  1940. # According to RFC-1123 a 554 reply may result in case
  1941. # that the existing file cannot be repositioned as
  1942. # specified in the REST.
  1943. ok = 0
  1944. try:
  1945. if rest_pos > self.fs.getsize(file):
  1946. raise ValueError
  1947. fd.seek(rest_pos)
  1948. ok = 1
  1949. except ValueError:
  1950. why = "Invalid REST parameter"
  1951. except (EnvironmentError, FilesystemError):
  1952. err = sys.exc_info()[1]
  1953. why = _strerror(err)
  1954. if not ok:
  1955. fd.close()
  1956. self.respond('554 %s' % why)
  1957. return
  1958. if self.data_channel is not None:
  1959. resp = "Data connection already open. Transfer starting."
  1960. self.respond("125 " + resp)
  1961. self.data_channel.file_obj = fd
  1962. self.data_channel.enable_receiving(self._current_type, cmd)
  1963. else:
  1964. resp = "File status okay. About to open data connection."
  1965. self.respond("150 " + resp)
  1966. self._in_dtp_queue = (fd, cmd)
  1967. return file
  1968. def ftp_STOU(self, line):
  1969. """Store a file on the server with a unique name.
  1970. On success return the file path, else None.
  1971. """
  1972. # Note 1: RFC-959 prohibited STOU parameters, but this
  1973. # prohibition is obsolete.
  1974. # Note 2: 250 response wanted by RFC-959 has been declared
  1975. # incorrect in RFC-1123 that wants 125/150 instead.
  1976. # Note 3: RFC-1123 also provided an exact output format
  1977. # defined to be as follow:
  1978. # > 125 FILE: pppp
  1979. # ...where pppp represents the unique path name of the
  1980. # file that will be written.
  1981. # watch for STOU preceded by REST, which makes no sense.
  1982. if self._restart_position:
  1983. self.respond("450 Can't STOU while REST request is pending.")
  1984. return
  1985. if line:
  1986. basedir, prefix = os.path.split(self.fs.ftp2fs(line))
  1987. prefix = prefix + '.'
  1988. else:
  1989. basedir = self.fs.ftp2fs(self.fs.cwd)
  1990. prefix = 'ftpd.'
  1991. try:
  1992. fd = self.run_as_current_user(self.fs.mkstemp, prefix=prefix,
  1993. dir=basedir)
  1994. except (EnvironmentError, FilesystemError):
  1995. err = sys.exc_info()[1]
  1996. # likely, we hit the max number of retries to find out a
  1997. # file with a unique name
  1998. if getattr(err, "errno", -1) == errno.EEXIST:
  1999. why = 'No usable unique file name found'
  2000. # something else happened
  2001. else:
  2002. why = _strerror(err)
  2003. self.respond("450 %s." % why)
  2004. return
  2005. if not self.authorizer.has_perm(self.username, 'w', fd.name):
  2006. try:
  2007. fd.close()
  2008. self.run_as_current_user(self.fs.remove, fd.name)
  2009. except (OSError, FilesystemError):
  2010. pass
  2011. self.respond("550 Not enough privileges.")
  2012. return
  2013. # now just acts like STOR except that restarting isn't allowed
  2014. filename = os.path.basename(fd.name)
  2015. if self.data_channel is not None:
  2016. self.respond("125 FILE: %s" % filename)
  2017. self.data_channel.file_obj = fd
  2018. self.data_channel.enable_receiving(self._current_type, "STOU")
  2019. else:
  2020. self.respond("150 FILE: %s" % filename)
  2021. self._in_dtp_queue = (fd, "STOU")
  2022. return filename
  2023. def ftp_APPE(self, file):
  2024. """Append data to an existing file on the server.
  2025. On success return the file path, else None.
  2026. """
  2027. # watch for APPE preceded by REST, which makes no sense.
  2028. if self._restart_position:
  2029. self.respond("450 Can't APPE while REST request is pending.")
  2030. else:
  2031. return self.ftp_STOR(file, mode='a')
  2032. def ftp_REST(self, line):
  2033. """Restart a file transfer from a previous mark."""
  2034. if self._current_type == 'a':
  2035. self.respond('501 Resuming transfers not allowed in ASCII mode.')
  2036. return
  2037. try:
  2038. marker = int(line)
  2039. if marker < 0:
  2040. raise ValueError
  2041. except (ValueError, OverflowError):
  2042. self.respond("501 Invalid parameter.")
  2043. else:
  2044. self.respond("350 Restarting at position %s." % marker)
  2045. self._restart_position = marker
  2046. def ftp_ABOR(self, line):
  2047. """Abort the current data transfer."""
  2048. # ABOR received while no data channel exists
  2049. if (self._dtp_acceptor is None) and (self._dtp_connector is None) \
  2050. and (self.data_channel is None):
  2051. self.respond("225 No transfer to abort.")
  2052. return
  2053. else:
  2054. # a PASV or PORT was received but connection wasn't made yet
  2055. if self._dtp_acceptor is not None or self._dtp_connector is not None:
  2056. self._shutdown_connecting_dtp()
  2057. resp = "225 ABOR command successful; data channel closed."
  2058. # If a data transfer is in progress the server must first
  2059. # close the data connection, returning a 426 reply to
  2060. # indicate that the transfer terminated abnormally, then it
  2061. # must send a 226 reply, indicating that the abort command
  2062. # was successfully processed.
  2063. # If no data has been transmitted we just respond with 225
  2064. # indicating that no transfer was in progress.
  2065. if self.data_channel is not None:
  2066. if self.data_channel.transfer_in_progress():
  2067. self.data_channel.close()
  2068. self.data_channel = None
  2069. self.respond("426 Transfer aborted via ABOR.",
  2070. logfun=logging.info)
  2071. resp = "226 ABOR command successful."
  2072. else:
  2073. self.data_channel.close()
  2074. self.data_channel = None
  2075. resp = "225 ABOR command successful; data channel closed."
  2076. self.respond(resp)
  2077. # --- authentication
  2078. def ftp_USER(self, line):
  2079. """Set the username for the current session."""
  2080. # RFC-959 specifies a 530 response to the USER command if the
  2081. # username is not valid. If the username is valid is required
  2082. # ftpd returns a 331 response instead. In order to prevent a
  2083. # malicious client from determining valid usernames on a server,
  2084. # it is suggested by RFC-2577 that a server always return 331 to
  2085. # the USER command and then reject the combination of username
  2086. # and password for an invalid username when PASS is provided later.
  2087. if not self.authenticated:
  2088. self.respond('331 Username ok, send password.')
  2089. else:
  2090. # a new USER command could be entered at any point in order
  2091. # to change the access control flushing any user, password,
  2092. # and account information already supplied and beginning the
  2093. # login sequence again.
  2094. self.flush_account()
  2095. msg = 'Previous account information was flushed'
  2096. self.respond('331 %s, send password.' % msg, logfun=logging.info)
  2097. self.username = line
  2098. _auth_failed_timeout = 5
  2099. def ftp_PASS(self, line):
  2100. """Check username's password against the authorizer."""
  2101. if self.authenticated:
  2102. self.respond("503 User already authenticated.")
  2103. return
  2104. if not self.username:
  2105. self.respond("503 Login with USER first.")
  2106. return
  2107. try:
  2108. self.authorizer.validate_authentication(self.username, line, self)
  2109. home = self.authorizer.get_home_dir(self.username)
  2110. msg_login = self.authorizer.get_msg_login(self.username)
  2111. except (AuthenticationFailed, AuthorizerError):
  2112. def auth_failed(username, password, msg):
  2113. self.add_channel()
  2114. if hasattr(self, '_closed') and not self._closed:
  2115. self.attempted_logins += 1
  2116. if self.attempted_logins >= self.max_login_attempts:
  2117. msg += " Disconnecting."
  2118. self.respond("530 " + msg)
  2119. self.close_when_done()
  2120. else:
  2121. self.respond("530 " + msg)
  2122. self.log("USER '%s' failed login." % username)
  2123. self.on_login_failed(username, password)
  2124. msg = str(sys.exc_info()[1])
  2125. if not msg:
  2126. if self.username == 'anonymous':
  2127. msg = "Anonymous access not allowed."
  2128. else:
  2129. msg = "Authentication failed."
  2130. else:
  2131. # response string should be capitalized as per RFC-959
  2132. msg = msg.capitalize()
  2133. self.del_channel()
  2134. self.ioloop.call_later(self._auth_failed_timeout, auth_failed,
  2135. self.username, line, msg,
  2136. _errback=self.handle_error)
  2137. self.username = ""
  2138. else:
  2139. if not isinstance(home, unicode):
  2140. if PY3:
  2141. raise ValueError('type(home) != text')
  2142. else:
  2143. warnings.warn(
  2144. '%s.get_home_dir returned a non-unicode string; now ' \
  2145. 'casting to unicode' % self.authorizer.__class__.__name__,
  2146. RuntimeWarning)
  2147. home = home.decode('utf8')
  2148. if len(msg_login) <= 75:
  2149. self.respond('230 %s' % msg_login)
  2150. else:
  2151. self.push("230-%s\r\n" % msg_login)
  2152. self.respond("230 ")
  2153. self.log("USER '%s' logged in." % self.username)
  2154. self.authenticated = True
  2155. self.password = line
  2156. self.attempted_logins = 0
  2157. self.fs = self.abstracted_fs(home, self)
  2158. self.on_login(self.username)
  2159. def ftp_REIN(self, line):
  2160. """Reinitialize user's current session."""
  2161. # From RFC-959:
  2162. # REIN command terminates a USER, flushing all I/O and account
  2163. # information, except to allow any transfer in progress to be
  2164. # completed. All parameters are reset to the default settings
  2165. # and the control connection is left open. This is identical
  2166. # to the state in which a user finds himself immediately after
  2167. # the control connection is opened.
  2168. self.flush_account()
  2169. # Note: RFC-959 erroneously mention "220" as the correct response
  2170. # code to be given in this case, but this is wrong...
  2171. self.respond("230 Ready for new user.")
  2172. # --- filesystem operations
  2173. def ftp_PWD(self, line):
  2174. """Return the name of the current working directory to the client."""
  2175. # The 257 response is supposed to include the directory
  2176. # name and in case it contains embedded double-quotes
  2177. # they must be doubled (see RFC-959, chapter 7, appendix 2).
  2178. cwd = self.fs.cwd
  2179. assert isinstance(cwd, unicode), cwd
  2180. self.respond('257 "%s" is the current directory.'
  2181. % cwd.replace('"', '""'))
  2182. def ftp_CWD(self, path):
  2183. """Change the current working directory.
  2184. On success return the new directory path, else None.
  2185. """
  2186. # Temporarily join the specified directory to see if we have
  2187. # permissions to do so, then get back to original process's
  2188. # current working directory.
  2189. # Note that if for some reason os.getcwd() gets removed after
  2190. # the process is started we'll get into troubles (os.getcwd()
  2191. # will fail with ENOENT) but we can't do anything about that
  2192. # except logging an error.
  2193. init_cwd = getcwdu()
  2194. try:
  2195. self.run_as_current_user(self.fs.chdir, path)
  2196. except (OSError, FilesystemError):
  2197. err = sys.exc_info()[1]
  2198. why = _strerror(err)
  2199. self.respond('550 %s.' % why)
  2200. else:
  2201. cwd = self.fs.cwd
  2202. assert isinstance(cwd, unicode), cwd
  2203. self.respond('250 "%s" is the current directory.' % cwd)
  2204. if getcwdu() != init_cwd:
  2205. os.chdir(init_cwd)
  2206. return path
  2207. def ftp_CDUP(self, path):
  2208. """Change into the parent directory.
  2209. On success return the new directory, else None.
  2210. """
  2211. # Note: RFC-959 says that code 200 is required but it also says
  2212. # that CDUP uses the same codes as CWD.
  2213. return self.ftp_CWD(path)
  2214. def ftp_SIZE(self, path):
  2215. """Return size of file in a format suitable for using with
  2216. RESTart as defined in RFC-3659."""
  2217. # Implementation note: properly handling the SIZE command when
  2218. # TYPE ASCII is used would require to scan the entire file to
  2219. # perform the ASCII translation logic
  2220. # (file.read().replace(os.linesep, '\r\n')) and then calculating
  2221. # the len of such data which may be different than the actual
  2222. # size of the file on the server. Considering that calculating
  2223. # such result could be very resource-intensive and also dangerous
  2224. # (DoS) we reject SIZE when the current TYPE is ASCII.
  2225. # However, clients in general should not be resuming downloads
  2226. # in ASCII mode. Resuming downloads in binary mode is the
  2227. # recommended way as specified in RFC-3659.
  2228. line = self.fs.fs2ftp(path)
  2229. if self._current_type == 'a':
  2230. why = "SIZE not allowed in ASCII mode"
  2231. self.respond("550 %s." %why)
  2232. return
  2233. if not self.fs.isfile(self.fs.realpath(path)):
  2234. why = "%s is not retrievable" % line
  2235. self.respond("550 %s." % why)
  2236. return
  2237. try:
  2238. size = self.run_as_current_user(self.fs.getsize, path)
  2239. except (OSError, FilesystemError):
  2240. err = sys.exc_info()[1]
  2241. why = _strerror(err)
  2242. self.respond('550 %s.' % why)
  2243. else:
  2244. self.respond("213 %s" % size)
  2245. def ftp_MDTM(self, path):
  2246. """Return last modification time of file to the client as an ISO
  2247. 3307 style timestamp (YYYYMMDDHHMMSS) as defined in RFC-3659.
  2248. On success return the file path, else None.
  2249. """
  2250. line = self.fs.fs2ftp(path)
  2251. if not self.fs.isfile(self.fs.realpath(path)):
  2252. self.respond("550 %s is not retrievable" % line)
  2253. return
  2254. if self.use_gmt_times:
  2255. timefunc = time.gmtime
  2256. else:
  2257. timefunc = time.localtime
  2258. try:
  2259. secs = self.run_as_current_user(self.fs.getmtime, path)
  2260. lmt = time.strftime("%Y%m%d%H%M%S", timefunc(secs))
  2261. except (ValueError, OSError, FilesystemError):
  2262. err = sys.exc_info()[1]
  2263. if isinstance(err, ValueError):
  2264. # It could happen if file's last modification time
  2265. # happens to be too old (prior to year 1900)
  2266. why = "Can't determine file's last modification time"
  2267. else:
  2268. why = _strerror(err)
  2269. self.respond('550 %s.' % why)
  2270. else:
  2271. self.respond("213 %s" % lmt)
  2272. return path
  2273. def ftp_MKD(self, path):
  2274. """Create the specified directory.
  2275. On success return the directory path, else None.
  2276. """
  2277. line = self.fs.fs2ftp(path)
  2278. try:
  2279. self.run_as_current_user(self.fs.mkdir, path)
  2280. except (OSError, FilesystemError):
  2281. err = sys.exc_info()[1]
  2282. why = _strerror(err)
  2283. self.respond('550 %s.' %why)
  2284. else:
  2285. # The 257 response is supposed to include the directory
  2286. # name and in case it contains embedded double-quotes
  2287. # they must be doubled (see RFC-959, chapter 7, appendix 2).
  2288. self.respond('257 "%s" directory created.' % line.replace('"', '""'))
  2289. return path
  2290. def ftp_RMD(self, path):
  2291. """Remove the specified directory.
  2292. On success return the directory path, else None.
  2293. """
  2294. if self.fs.realpath(path) == self.fs.realpath(self.fs.root):
  2295. msg = "Can't remove root directory."
  2296. self.respond("550 %s" % msg)
  2297. return
  2298. try:
  2299. self.run_as_current_user(self.fs.rmdir, path)
  2300. except (OSError, FilesystemError):
  2301. err = sys.exc_info()[1]
  2302. why = _strerror(err)
  2303. self.respond('550 %s.' % why)
  2304. else:
  2305. self.respond("250 Directory removed.")
  2306. def ftp_DELE(self, path):
  2307. """Delete the specified file.
  2308. On success return the file path, else None.
  2309. """
  2310. try:
  2311. self.run_as_current_user(self.fs.remove, path)
  2312. except (OSError, FilesystemError):
  2313. err = sys.exc_info()[1]
  2314. why = _strerror(err)
  2315. self.respond('550 %s.' % why)
  2316. else:
  2317. self.respond("250 File removed.")
  2318. return path
  2319. def ftp_RNFR(self, path):
  2320. """Rename the specified (only the source name is specified
  2321. here, see RNTO command)"""
  2322. if not self.fs.lexists(path):
  2323. self.respond("550 No such file or directory.")
  2324. elif self.fs.realpath(path) == self.fs.realpath(self.fs.root):
  2325. self.respond("550 Can't rename home directory.")
  2326. else:
  2327. self._rnfr = path
  2328. self.respond("350 Ready for destination name.")
  2329. def ftp_RNTO(self, path):
  2330. """Rename file (destination name only, source is specified with
  2331. RNFR).
  2332. On success return a (source_path, destination_path) tuple.
  2333. """
  2334. if not self._rnfr:
  2335. self.respond("503 Bad sequence of commands: use RNFR first.")
  2336. return
  2337. src = self._rnfr
  2338. self._rnfr = None
  2339. try:
  2340. self.run_as_current_user(self.fs.rename, src, path)
  2341. except (OSError, FilesystemError):
  2342. err = sys.exc_info()[1]
  2343. why = _strerror(err)
  2344. self.respond('550 %s.' % why)
  2345. else:
  2346. self.respond("250 Renaming ok.")
  2347. return (src, path)
  2348. # --- others
  2349. def ftp_TYPE(self, line):
  2350. """Set current type data type to binary/ascii"""
  2351. type = line.upper().replace(' ', '')
  2352. if type in ("A", "L7"):
  2353. self.respond("200 Type set to: ASCII.")
  2354. self._current_type = 'a'
  2355. elif type in ("I", "L8"):
  2356. self.respond("200 Type set to: Binary.")
  2357. self._current_type = 'i'
  2358. else:
  2359. self.respond('504 Unsupported type "%s".' % line)
  2360. def ftp_STRU(self, line):
  2361. """Set file structure ("F" is the only one supported (noop))."""
  2362. stru = line.upper()
  2363. if stru == 'F':
  2364. self.respond('200 File transfer structure set to: F.')
  2365. elif stru in ('P', 'R'):
  2366. # R is required in minimum implementations by RFC-959, 5.1.
  2367. # RFC-1123, 4.1.2.13, amends this to only apply to servers
  2368. # whose file systems support record structures, but also
  2369. # suggests that such a server "may still accept files with
  2370. # STRU R, recording the byte stream literally".
  2371. # Should we accept R but with no operational difference from
  2372. # F? proftpd and wu-ftpd don't accept STRU R. We just do
  2373. # the same.
  2374. #
  2375. # RFC-1123 recommends against implementing P.
  2376. self.respond('504 Unimplemented STRU type.')
  2377. else:
  2378. self.respond('501 Unrecognized STRU type.')
  2379. def ftp_MODE(self, line):
  2380. """Set data transfer mode ("S" is the only one supported (noop))."""
  2381. mode = line.upper()
  2382. if mode == 'S':
  2383. self.respond('200 Transfer mode set to: S')
  2384. elif mode in ('B', 'C'):
  2385. self.respond('504 Unimplemented MODE type.')
  2386. else:
  2387. self.respond('501 Unrecognized MODE type.')
  2388. def ftp_STAT(self, path):
  2389. """Return statistics about current ftp session. If an argument
  2390. is provided return directory listing over command channel.
  2391. Implementation note:
  2392. RFC-959 does not explicitly mention globbing but many FTP
  2393. servers do support it as a measure of convenience for FTP
  2394. clients and users.
  2395. In order to search for and match the given globbing expression,
  2396. the code has to search (possibly) many directories, examine
  2397. each contained filename, and build a list of matching files in
  2398. memory. Since this operation can be quite intensive, both CPU-
  2399. and memory-wise, we do not support globbing.
  2400. """
  2401. # return STATus information about ftpd
  2402. if not path:
  2403. s = []
  2404. s.append('Connected to: %s:%s' % self.socket.getsockname()[:2])
  2405. if self.authenticated:
  2406. s.append('Logged in as: %s' % self.username)
  2407. else:
  2408. if not self.username:
  2409. s.append("Waiting for username.")
  2410. else:
  2411. s.append("Waiting for password.")
  2412. if self._current_type == 'a':
  2413. type = 'ASCII'
  2414. else:
  2415. type = 'Binary'
  2416. s.append("TYPE: %s; STRUcture: File; MODE: Stream" % type)
  2417. if self._dtp_acceptor is not None:
  2418. s.append('Passive data channel waiting for connection.')
  2419. elif self.data_channel is not None:
  2420. bytes_sent = self.data_channel.tot_bytes_sent
  2421. bytes_recv = self.data_channel.tot_bytes_received
  2422. elapsed_time = self.data_channel.get_elapsed_time()
  2423. s.append('Data connection open:')
  2424. s.append('Total bytes sent: %s' % bytes_sent)
  2425. s.append('Total bytes received: %s' % bytes_recv)
  2426. s.append('Transfer elapsed time: %s secs' % elapsed_time)
  2427. else:
  2428. s.append('Data connection closed.')
  2429. self.push('211-FTP server status:\r\n')
  2430. self.push(''.join([' %s\r\n' % item for item in s]))
  2431. self.respond('211 End of status.')
  2432. # return directory LISTing over the command channel
  2433. else:
  2434. line = self.fs.fs2ftp(path)
  2435. try:
  2436. iterator = self.run_as_current_user(self.fs.get_list_dir, path)
  2437. except (OSError, FilesystemError):
  2438. err = sys.exc_info()[1]
  2439. why = _strerror(err)
  2440. self.respond('550 %s.' %why)
  2441. else:
  2442. self.push('213-Status of "%s":\r\n' % line)
  2443. self.push_with_producer(BufferedIteratorProducer(iterator))
  2444. self.respond('213 End of status.')
  2445. return path
  2446. def ftp_FEAT(self, line):
  2447. """List all new features supported as defined in RFC-2398."""
  2448. features = set(['UTF8', 'TVFS'])
  2449. features.update([feat for feat in ('EPRT', 'EPSV', 'MDTM', 'SIZE') \
  2450. if feat in self.proto_cmds])
  2451. features.update(self._extra_feats)
  2452. if 'MLST' in self.proto_cmds or 'MLSD' in self.proto_cmds:
  2453. facts = ''
  2454. for fact in self._available_facts:
  2455. if fact in self._current_facts:
  2456. facts += fact + '*;'
  2457. else:
  2458. facts += fact + ';'
  2459. features.add('MLST ' + facts)
  2460. if 'REST' in self.proto_cmds:
  2461. features.add('REST STREAM')
  2462. features = sorted(features)
  2463. self.push("211-Features supported:\r\n")
  2464. self.push("".join([" %s\r\n" % x for x in features]))
  2465. self.respond('211 End FEAT.')
  2466. def ftp_OPTS(self, line):
  2467. """Specify options for FTP commands as specified in RFC-2389."""
  2468. try:
  2469. if line.count(' ') > 1:
  2470. raise ValueError('Invalid number of arguments')
  2471. if ' ' in line:
  2472. cmd, arg = line.split(' ')
  2473. if ';' not in arg:
  2474. raise ValueError('Invalid argument')
  2475. else:
  2476. cmd, arg = line, ''
  2477. # actually the only command able to accept options is MLST
  2478. if cmd.upper() != 'MLST' or 'MLST' not in self.proto_cmds:
  2479. raise ValueError('Unsupported command "%s"' % cmd)
  2480. except ValueError:
  2481. err = sys.exc_info()[1]
  2482. self.respond('501 %s.' % err)
  2483. else:
  2484. facts = [x.lower() for x in arg.split(';')]
  2485. self._current_facts = [x for x in facts if x in self._available_facts]
  2486. f = ''.join([x + ';' for x in self._current_facts])
  2487. self.respond('200 MLST OPTS ' + f)
  2488. def ftp_NOOP(self, line):
  2489. """Do nothing."""
  2490. self.respond("200 I successfully done nothin'.")
  2491. def ftp_SYST(self, line):
  2492. """Return system type (always returns UNIX type: L8)."""
  2493. # This command is used to find out the type of operating system
  2494. # at the server. The reply shall have as its first word one of
  2495. # the system names listed in RFC-943.
  2496. # Since that we always return a "/bin/ls -lA"-like output on
  2497. # LIST we prefer to respond as if we would on Unix in any case.
  2498. self.respond("215 UNIX Type: L8")
  2499. def ftp_ALLO(self, line):
  2500. """Allocate bytes for storage (noop)."""
  2501. # not necessary (always respond with 202)
  2502. self.respond("202 No storage allocation necessary.")
  2503. def ftp_HELP(self, line):
  2504. """Return help text to the client."""
  2505. if line:
  2506. line = line.upper()
  2507. if line in self.proto_cmds:
  2508. self.respond("214 %s" % self.proto_cmds[line]['help'])
  2509. else:
  2510. self.respond("501 Unrecognized command.")
  2511. else:
  2512. # provide a compact list of recognized commands
  2513. def formatted_help():
  2514. cmds = []
  2515. keys = [x for x in self.proto_cmds.keys() if not x.startswith('SITE ')]
  2516. keys.sort()
  2517. while keys:
  2518. elems = tuple((keys[0:8]))
  2519. cmds.append(' %-6s' * len(elems) % elems + '\r\n')
  2520. del keys[0:8]
  2521. return ''.join(cmds)
  2522. self.push("214-The following commands are recognized:\r\n")
  2523. self.push(formatted_help())
  2524. self.respond("214 Help command successful.")
  2525. # --- site commands
  2526. # The user willing to add support for a specific SITE command must
  2527. # update self.proto_cmds dictionary and define a new ftp_SITE_%CMD%
  2528. # method in the subclass.
  2529. def ftp_SITE_CHMOD(self, path, mode):
  2530. """Change file mode.
  2531. On success return a (file_path, mode) tuple.
  2532. """
  2533. # Note: although most UNIX servers implement it, SITE CHMOD is not
  2534. # defined in any official RFC.
  2535. try:
  2536. assert len(mode) in (3, 4)
  2537. for x in mode:
  2538. assert 0 <= int(x) <= 7
  2539. mode = int(mode, 8)
  2540. except (AssertionError, ValueError):
  2541. self.respond("501 Invalid SITE CHMOD format.")
  2542. else:
  2543. try:
  2544. self.run_as_current_user(self.fs.chmod, path, mode)
  2545. except (OSError, FilesystemError):
  2546. err = sys.exc_info()[1]
  2547. why = _strerror(err)
  2548. self.respond('550 %s.' % why)
  2549. else:
  2550. self.respond('200 SITE CHMOD successful.')
  2551. return (path, mode)
  2552. def ftp_SITE_HELP(self, line):
  2553. """Return help text to the client for a given SITE command."""
  2554. if line:
  2555. line = line.upper()
  2556. if line in self.proto_cmds:
  2557. self.respond("214 %s" % self.proto_cmds[line]['help'])
  2558. else:
  2559. self.respond("501 Unrecognized SITE command.")
  2560. else:
  2561. self.push("214-The following SITE commands are recognized:\r\n")
  2562. site_cmds = []
  2563. for cmd in sorted(self.proto_cmds.keys()):
  2564. if cmd.startswith('SITE '):
  2565. site_cmds.append(' %s\r\n' % cmd[5:])
  2566. self.push(''.join(site_cmds))
  2567. self.respond("214 Help SITE command successful.")
  2568. # --- support for deprecated cmds
  2569. # RFC-1123 requires that the server treat XCUP, XCWD, XMKD, XPWD
  2570. # and XRMD commands as synonyms for CDUP, CWD, MKD, LIST and RMD.
  2571. # Such commands are obsoleted but some ftp clients (e.g. Windows
  2572. # ftp.exe) still use them.
  2573. def ftp_XCUP(self, line):
  2574. """Change to the parent directory. Synonym for CDUP. Deprecated."""
  2575. return self.ftp_CDUP(line)
  2576. def ftp_XCWD(self, line):
  2577. """Change the current working directory. Synonym for CWD. Deprecated."""
  2578. return self.ftp_CWD(line)
  2579. def ftp_XMKD(self, line):
  2580. """Create the specified directory. Synonym for MKD. Deprecated."""
  2581. return self.ftp_MKD(line)
  2582. def ftp_XPWD(self, line):
  2583. """Return the current working directory. Synonym for PWD. Deprecated."""
  2584. return self.ftp_PWD(line)
  2585. def ftp_XRMD(self, line):
  2586. """Remove the specified directory. Synonym for RMD. Deprecated."""
  2587. return self.ftp_RMD(line)
  2588. # ===================================================================
  2589. # --- FTP over SSL
  2590. # ===================================================================
  2591. # requires PyOpenSSL - http://pypi.python.org/pypi/pyOpenSSL
  2592. try:
  2593. from OpenSSL import SSL
  2594. except ImportError:
  2595. pass
  2596. else:
  2597. _ssl_proto_cmds = proto_cmds.copy()
  2598. _ssl_proto_cmds.update({
  2599. 'AUTH': dict(perm=None, auth=False, arg=True,
  2600. help='Syntax: AUTH <SP> TLS|SSL (set up secure control channel).'),
  2601. 'PBSZ': dict(perm=None, auth=False, arg=True,
  2602. help='Syntax: PBSZ <SP> 0 (negotiate TLS buffer).'),
  2603. 'PROT': dict(perm=None, auth=False, arg=True,
  2604. help='Syntax: PROT <SP> [C|P] (set up un/secure data channel).'),
  2605. })
  2606. class SSLConnection(_AsyncChatNewStyle):
  2607. """An AsyncChat subclass supporting TLS/SSL."""
  2608. _ssl_accepting = False
  2609. _ssl_established = False
  2610. _ssl_closing = False
  2611. def __init__(self, *args, **kwargs):
  2612. super(SSLConnection, self).__init__(*args, **kwargs)
  2613. self._error = False
  2614. def secure_connection(self, ssl_context):
  2615. """Secure the connection switching from plain-text to
  2616. SSL/TLS.
  2617. """
  2618. try:
  2619. self.socket = SSL.Connection(ssl_context, self.socket)
  2620. except socket.error:
  2621. self.close()
  2622. except ValueError:
  2623. # may happen in case the client connects/disconnects
  2624. # very quickly
  2625. if self.socket.fileno() == -1:
  2626. return
  2627. raise
  2628. else:
  2629. self.socket.set_accept_state()
  2630. self._ssl_accepting = True
  2631. def _do_ssl_handshake(self):
  2632. self._ssl_accepting = True
  2633. try:
  2634. self.socket.do_handshake()
  2635. except (SSL.WantReadError, SSL.WantWriteError):
  2636. return
  2637. except SSL.SysCallError:
  2638. err = sys.exc_info()[1]
  2639. retval, desc = err.args
  2640. if (retval == -1 and desc == 'Unexpected EOF') or retval > 0:
  2641. return self.handle_close()
  2642. raise
  2643. except SSL.Error:
  2644. return self.handle_failed_ssl_handshake()
  2645. else:
  2646. self._ssl_accepting = False
  2647. self._ssl_established = True
  2648. self.handle_ssl_established()
  2649. def handle_ssl_established(self):
  2650. """Called when SSL handshake has completed."""
  2651. pass
  2652. def handle_ssl_shutdown(self):
  2653. """Called when SSL shutdown() has completed."""
  2654. super(SSLConnection, self).close()
  2655. def handle_failed_ssl_handshake(self):
  2656. raise NotImplementedError("must be implemented in subclass")
  2657. def handle_read_event(self):
  2658. if self._ssl_accepting:
  2659. self._do_ssl_handshake()
  2660. elif self._ssl_closing:
  2661. self._do_ssl_shutdown()
  2662. else:
  2663. super(SSLConnection, self).handle_read_event()
  2664. def handle_write_event(self):
  2665. if self._ssl_accepting:
  2666. self._do_ssl_handshake()
  2667. elif self._ssl_closing:
  2668. self._do_ssl_shutdown()
  2669. else:
  2670. super(SSLConnection, self).handle_write_event()
  2671. def handle_error(self):
  2672. self._error = True
  2673. try:
  2674. raise
  2675. except (KeyboardInterrupt, SystemExit):
  2676. raise
  2677. except:
  2678. self.log_exception(self)
  2679. # when facing an unhandled exception in here it's better
  2680. # to rely on base class (FTPHandler or DTPHandler)
  2681. # close() method as it does not imply SSL shutdown logic
  2682. try:
  2683. super(SSLConnection, self).close()
  2684. except Exception:
  2685. logger.critical(traceback.format_exc())
  2686. def send(self, data):
  2687. try:
  2688. return super(SSLConnection, self).send(data)
  2689. except (SSL.WantReadError, SSL.WantWriteError):
  2690. return 0
  2691. except SSL.ZeroReturnError:
  2692. super(SSLConnection, self).handle_close()
  2693. return 0
  2694. except SSL.SysCallError:
  2695. err = sys.exc_info()[1]
  2696. errnum, errstr = err.args
  2697. if errnum == errno.EWOULDBLOCK:
  2698. return 0
  2699. elif errnum in _DISCONNECTED or errstr == 'Unexpected EOF':
  2700. super(SSLConnection, self).handle_close()
  2701. return 0
  2702. else:
  2703. raise
  2704. def recv(self, buffer_size):
  2705. try:
  2706. return super(SSLConnection, self).recv(buffer_size)
  2707. except (SSL.WantReadError, SSL.WantWriteError):
  2708. return b('')
  2709. except SSL.ZeroReturnError:
  2710. super(SSLConnection, self).handle_close()
  2711. return b('')
  2712. except SSL.SysCallError:
  2713. err = sys.exc_info()[1]
  2714. errnum, errstr = err.args
  2715. if errnum in _DISCONNECTED or errstr == 'Unexpected EOF':
  2716. super(SSLConnection, self).handle_close()
  2717. return b('')
  2718. else:
  2719. raise
  2720. def _do_ssl_shutdown(self):
  2721. """Executes a SSL_shutdown() call to revert the connection
  2722. back to clear-text.
  2723. twisted/internet/tcp.py code has been used as an example.
  2724. """
  2725. self._ssl_closing = True
  2726. # since SSL_shutdown() doesn't report errors, an empty
  2727. # write call is done first, to try to detect if the
  2728. # connection has gone away
  2729. try:
  2730. os.write(self.socket.fileno(), b(''))
  2731. except (OSError, socket.error):
  2732. err = sys.exc_info()[1]
  2733. if err.args[0] in (errno.EINTR, errno.EWOULDBLOCK, errno.ENOBUFS):
  2734. return
  2735. elif err.args[0] in _DISCONNECTED:
  2736. return super(SSLConnection, self).close()
  2737. else:
  2738. raise
  2739. # Ok, this a mess, but the underlying OpenSSL API simply
  2740. # *SUCKS* and I really couldn't do any better.
  2741. #
  2742. # Here we just want to shutdown() the SSL layer and then
  2743. # close() the connection so we're not interested in a
  2744. # complete SSL shutdown() handshake, so let's pretend
  2745. # we already received a "RECEIVED" shutdown notification
  2746. # from the client.
  2747. # Once the client received our "SENT" shutdown notification
  2748. # then we close() the connection.
  2749. #
  2750. # Since it is not clear what errors to expect during the
  2751. # entire procedure we catch them all and assume the
  2752. # following:
  2753. # - WantReadError and WantWriteError means "retry"
  2754. # - ZeroReturnError, SysCallError[EOF], Error[] are all
  2755. # aliases for disconnection
  2756. try:
  2757. laststate = self.socket.get_shutdown()
  2758. self.socket.set_shutdown(laststate | SSL.RECEIVED_SHUTDOWN)
  2759. done = self.socket.shutdown()
  2760. if not (laststate & SSL.RECEIVED_SHUTDOWN):
  2761. self.socket.set_shutdown(SSL.SENT_SHUTDOWN)
  2762. except (SSL.WantReadError, SSL.WantWriteError):
  2763. pass
  2764. except SSL.ZeroReturnError:
  2765. super(SSLConnection, self).close()
  2766. except SSL.SysCallError:
  2767. err = sys.exc_info()[1]
  2768. errnum, errstr = err.args
  2769. if errnum in _DISCONNECTED or errstr == 'Unexpected EOF':
  2770. super(SSLConnection, self).close()
  2771. else:
  2772. raise
  2773. except SSL.Error:
  2774. # see:
  2775. # http://code.google.com/p/pyftpdlib/issues/detail?id=171
  2776. # https://bugs.launchpad.net/pyopenssl/+bug/785985
  2777. err = sys.exc_info()[1]
  2778. if err.args and not err.args[0]:
  2779. pass
  2780. else:
  2781. raise
  2782. except socket.error:
  2783. err = sys.exc_info()[1]
  2784. if err.args[0] in _DISCONNECTED:
  2785. super(SSLConnection, self).close()
  2786. else:
  2787. raise
  2788. else:
  2789. if done:
  2790. self._ssl_established = False
  2791. self._ssl_closing = False
  2792. self.handle_ssl_shutdown()
  2793. def close(self):
  2794. if self._ssl_established and not self._error:
  2795. self._do_ssl_shutdown()
  2796. else:
  2797. self._ssl_accepting = False
  2798. self._ssl_established = False
  2799. self._ssl_closing = False
  2800. super(SSLConnection, self).close()
  2801. class TLS_DTPHandler(SSLConnection, DTPHandler):
  2802. """A DTPHandler subclass supporting TLS/SSL."""
  2803. def __init__(self, sock, cmd_channel):
  2804. super(TLS_DTPHandler, self).__init__(sock, cmd_channel)
  2805. if self.cmd_channel._prot:
  2806. self.secure_connection(self.cmd_channel.ssl_context)
  2807. def _use_sendfile(self, producer):
  2808. return False
  2809. def handle_failed_ssl_handshake(self):
  2810. # TLS/SSL handshake failure, probably client's fault which
  2811. # used a SSL version different from server's.
  2812. # RFC-4217, chapter 10.2 expects us to return 522 over the
  2813. # command channel.
  2814. self.cmd_channel.respond("522 SSL handshake failed.")
  2815. self.cmd_channel.log_cmd("PROT", "P", 522, "SSL handshake failed.")
  2816. self.close()
  2817. class TLS_FTPHandler(SSLConnection, FTPHandler):
  2818. """A FTPHandler subclass supporting TLS/SSL.
  2819. Implements AUTH, PBSZ and PROT commands (RFC-2228 and RFC-4217).
  2820. Configurable attributes:
  2821. - (bool) tls_control_required:
  2822. When True requires SSL/TLS to be established on the control
  2823. channel, before logging in. This means the user will have
  2824. to issue AUTH before USER/PASS (default False).
  2825. - (bool) tls_data_required:
  2826. When True requires SSL/TLS to be established on the data
  2827. channel. This means the user will have to issue PROT
  2828. before PASV or PORT (default False).
  2829. SSL-specific options:
  2830. - (string) certfile:
  2831. the path to the file which contains a certificate to be
  2832. used to identify the local side of the connection.
  2833. This must always be specified, unless context is provided
  2834. instead.
  2835. - (string) keyfile:
  2836. the path to the file containing the private RSA key;
  2837. can be omitted if certfile already contains the private
  2838. key (defaults: None).
  2839. - (int) protocol:
  2840. specifies which version of the SSL protocol to use when
  2841. establishing SSL/TLS sessions; clients can then only
  2842. connect using the configured protocol (defaults to SSLv23,
  2843. allowing SSLv3 and TLSv1 protocols).
  2844. Possible values:
  2845. * SSL.SSLv2_METHOD - allow only SSLv2
  2846. * SSL.SSLv3_METHOD - allow only SSLv3
  2847. * SSL.SSLv23_METHOD - allow both SSLv3 and TLSv1
  2848. * SSL.TLSv1_METHOD - allow only TLSv1
  2849. - (instance) context:
  2850. a SSL Context object previously configured; if specified
  2851. all other parameters will be ignored.
  2852. (default None).
  2853. """
  2854. # configurable attributes
  2855. tls_control_required = False
  2856. tls_data_required = False
  2857. certfile = None
  2858. keyfile = None
  2859. ssl_protocol = SSL.SSLv23_METHOD
  2860. ssl_context = None
  2861. # overridden attributes
  2862. proto_cmds = _ssl_proto_cmds
  2863. dtp_handler = TLS_DTPHandler
  2864. def __init__(self, conn, server, ioloop=None):
  2865. super(TLS_FTPHandler, self).__init__(conn, server, ioloop)
  2866. if not self.connected:
  2867. return
  2868. self._extra_feats = ['AUTH TLS', 'AUTH SSL', 'PBSZ', 'PROT']
  2869. self._pbsz = False
  2870. self._prot = False
  2871. self.ssl_context = self.get_ssl_context()
  2872. @classmethod
  2873. def get_ssl_context(cls):
  2874. if cls.ssl_context is None:
  2875. if cls.certfile is None:
  2876. raise ValueError("at least certfile must be specified")
  2877. cls.ssl_context = SSL.Context(cls.ssl_protocol)
  2878. if cls.ssl_protocol != SSL.SSLv2_METHOD:
  2879. cls.ssl_context.set_options(SSL.OP_NO_SSLv2)
  2880. else:
  2881. warnings.warn("SSLv2 protocol is insecure", RuntimeWarning)
  2882. cls.ssl_context.use_certificate_file(cls.certfile)
  2883. if not cls.keyfile:
  2884. cls.keyfile = cls.certfile
  2885. cls.ssl_context.use_privatekey_file(cls.keyfile)
  2886. return cls.ssl_context
  2887. # --- overridden methods
  2888. def flush_account(self):
  2889. FTPHandler.flush_account(self)
  2890. self._pbsz = False
  2891. self._prot = False
  2892. def process_command(self, cmd, *args, **kwargs):
  2893. if cmd in ('USER', 'PASS'):
  2894. if self.tls_control_required and not self._ssl_established:
  2895. msg = "SSL/TLS required on the control channel."
  2896. self.respond("550 " + msg)
  2897. self.log_cmd(cmd, args[0], 550, msg)
  2898. return
  2899. elif cmd in ('PASV', 'EPSV', 'PORT', 'EPRT'):
  2900. if self.tls_data_required and not self._prot:
  2901. msg = "SSL/TLS required on the data channel."
  2902. self.respond("550 " + msg)
  2903. self.log_cmd(cmd, args[0], 550, msg)
  2904. return
  2905. FTPHandler.process_command(self, cmd, *args, **kwargs)
  2906. # --- new methods
  2907. def handle_failed_ssl_handshake(self):
  2908. # TLS/SSL handshake failure, probably client's fault which
  2909. # used a SSL version different from server's.
  2910. # We can't rely on the control connection anymore so we just
  2911. # disconnect the client without sending any response.
  2912. self.log("SSL handshake failed.")
  2913. self.close()
  2914. def ftp_AUTH(self, line):
  2915. """Set up secure control channel."""
  2916. arg = line.upper()
  2917. if isinstance(self.socket, SSL.Connection):
  2918. self.respond("503 Already using TLS.")
  2919. elif arg in ('TLS', 'TLS-C', 'SSL', 'TLS-P'):
  2920. # From RFC-4217: "As the SSL/TLS protocols self-negotiate
  2921. # their levels, there is no need to distinguish between SSL
  2922. # and TLS in the application layer".
  2923. self.respond('234 AUTH %s successful.' %arg)
  2924. self.secure_connection(self.ssl_context)
  2925. else:
  2926. self.respond("502 Unrecognized encryption type (use TLS or SSL).")
  2927. def ftp_PBSZ(self, line):
  2928. """Negotiate size of buffer for secure data transfer.
  2929. For TLS/SSL the only valid value for the parameter is '0'.
  2930. Any other value is accepted but ignored.
  2931. """
  2932. if not isinstance(self.socket, SSL.Connection):
  2933. self.respond("503 PBSZ not allowed on insecure control connection.")
  2934. else:
  2935. self.respond('200 PBSZ=0 successful.')
  2936. self._pbsz = True
  2937. def ftp_PROT(self, line):
  2938. """Setup un/secure data channel."""
  2939. arg = line.upper()
  2940. if not isinstance(self.socket, SSL.Connection):
  2941. self.respond("503 PROT not allowed on insecure control connection.")
  2942. elif not self._pbsz:
  2943. self.respond("503 You must issue the PBSZ command prior to PROT.")
  2944. elif arg == 'C':
  2945. self.respond('200 Protection set to Clear')
  2946. self._prot = False
  2947. elif arg == 'P':
  2948. self.respond('200 Protection set to Private')
  2949. self._prot = True
  2950. elif arg in ('S', 'E'):
  2951. self.respond('521 PROT %s unsupported (use C or P).' %arg)
  2952. else:
  2953. self.respond("502 Unrecognized PROT type (use C or P).")