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