1 # -*- test-case-name: twisted.test.test_ftp -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
6 An FTP protocol implementation
24 from zope.interface import Interface, implements
27 from twisted import copyright
28 from twisted.internet import reactor, interfaces, protocol, error, defer
29 from twisted.protocols import basic, policies
31 from twisted.python import log, failure, filepath
32 from twisted.python.compat import reduce
34 from twisted.cred import error as cred_error, portal, credentials, checkers
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"
46 ENTERING_PORT_MODE = "200.3"
47 CMD_NOT_IMPLMNTD_SUPERFLUOUS = "202"
48 SYS_STATUS_OR_HELP_REPLY = "211"
53 SVC_READY_FOR_NEW_USER = "220.1"
55 SVC_CLOSING_CTRL_CNX = "221.1"
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"
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"
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"
82 SYNTAX_ERR_IN_ARGS = "501"
83 CMD_NOT_IMPLMNTD = "502"
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"
97 EXCEEDED_STORAGE_ALLOC = "552"
98 FILENAME_NOT_ALLOWED = "553"
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.',
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',
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.',
138 REQ_FILE_ACTN_PENDING_FURTHER_INFO: '350 Requested file action pending further information.',
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.',
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.',
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'
173 class InvalidPath(Exception):
175 Internal exception used to signify an error during parsing a path.
180 def toSegments(cwd, path):
182 Normalize a path, as represented by a list of strings each
183 representing one segment of the path.
185 if path.startswith('/'):
190 for s in path.split('/'):
191 if s == '.' or s == '':
197 raise InvalidPath(cwd, path)
198 elif '\0' in s or '/' in s:
199 raise InvalidPath(cwd, path)
205 def errnoToFailure(e, path):
207 Map C{OSError} and C{IOError} to standard FTP errors.
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))
224 class FTPCmdError(Exception):
226 Generic exception for FTP commands.
228 def __init__(self, *msg):
229 Exception.__init__(self, *msg)
230 self.errorMessage = msg
235 Generate a FTP response message for this error.
237 return RESPONSE[self.errorCode] % self.errorMessage
241 class FileNotFoundError(FTPCmdError):
243 Raised when trying to access a non existent file or directory.
245 errorCode = FILE_NOT_FOUND
249 class AnonUserDeniedError(FTPCmdError):
251 Raised when an anonymous user issues a command that will alter the
255 errorCode = ANON_USER_DENIED
259 class PermissionDeniedError(FTPCmdError):
261 Raised when access is attempted to a resource to which access is
264 errorCode = PERMISSION_DENIED
268 class IsNotADirectoryError(FTPCmdError):
270 Raised when RMD is called on a path that isn't a directory.
272 errorCode = IS_NOT_A_DIR
276 class FileExistsError(FTPCmdError):
278 Raised when attempted to override an existing resource.
280 errorCode = FILE_EXISTS
284 class IsADirectoryError(FTPCmdError):
286 Raised when DELE is called on a path that is a directory.
292 class CmdSyntaxError(FTPCmdError):
294 Raised when a command syntax is wrong.
296 errorCode = SYNTAX_ERR
300 class CmdArgSyntaxError(FTPCmdError):
302 Raised when a command is called with wrong value or a wrong number of
305 errorCode = SYNTAX_ERR_IN_ARGS
309 class CmdNotImplementedError(FTPCmdError):
311 Raised when an unimplemented command is given to the server.
313 errorCode = CMD_NOT_IMPLMNTD
317 class CmdNotImplementedForArgError(FTPCmdError):
319 Raised when the handling of a parameter for a command is not implemented by
322 errorCode = CMD_NOT_IMPLMNTD_FOR_PARAM
326 class FTPError(Exception):
331 class PortConnectionError(Exception):
336 class BadCmdSequenceError(FTPCmdError):
338 Raised when a client sends a series of commands in an illogical sequence.
340 errorCode = BAD_CMD_SEQ
344 class AuthorizationError(FTPCmdError):
346 Raised when client authentication fails.
348 errorCode = AUTH_FAILURE
352 def debugDeferred(self, *_):
353 log.msg('debugDeferred(): %s' % str(_), debug=True)
361 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
362 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
365 class DTP(object, protocol.Protocol):
366 implements(interfaces.IConsumer)
374 def connectionMade(self):
375 self.isConnected = True
376 self.factory.deferred.callback(None)
379 def connectionLost(self, reason):
380 self.isConnected = False
381 if self._onConnLost is not None:
382 self._onConnLost.callback(None)
384 def sendLine(self, line):
385 self.transport.write(line + '\r\n')
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)])
392 def formatDate(mtime):
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
401 if now.tm_year != mtime.tm_year:
402 return '%(month)s %(day)02d %(year)5d' % info
404 return '%(month)s %(day)02d %(hour)02d:%(minute)02d' % info
406 format = ('%(directory)s%(permissions)s%(hardlinks)4d '
407 '%(owner)-9s %(group)-9s %(size)15d %(date)12s '
411 'directory': directory and 'd' or '-',
412 'permissions': formatMode(permissions),
413 'hardlinks': hardlinks,
417 'date': formatDate(time.gmtime(modified)),
420 def sendListResponse(self, name, response):
421 self.sendLine(self._formatOneListResponse(name, *response))
424 # Proxy IConsumer to our transport
425 def registerProducer(self, producer, streaming):
426 return self.transport.registerProducer(producer, streaming)
428 def unregisterProducer(self):
429 self.transport.unregisterProducer()
430 self.transport.loseConnection()
432 def write(self, data):
434 return self.transport.write(data)
435 raise Exception("Crap damn crap damn crap damn")
438 # Pretend to be a producer, too.
439 def _conswrite(self, bytes):
441 self._cons.write(bytes)
443 self._onConnLost.errback()
445 def dataReceived(self, bytes):
446 if self._cons is not None:
447 self._conswrite(bytes)
449 self._buffer.append(bytes)
451 def _unregConsumer(self, ignored):
452 self._cons.unregisterProducer()
457 def registerConsumer(self, cons):
458 assert self._cons is None
460 self._cons.registerProducer(self, True)
461 for chunk in self._buffer:
462 self._conswrite(chunk)
465 self._onConnLost = d = defer.Deferred()
466 d.addBoth(self._unregConsumer)
469 self._cons.unregisterProducer()
471 return defer.succeed(None)
473 def resumeProducing(self):
474 self.transport.resumeProducing()
476 def pauseProducing(self):
477 self.transport.pauseProducing()
479 def stopProducing(self):
480 self.transport.stopProducing()
482 class DTPFactory(protocol.ClientFactory):
484 Client factory for I{data transfer process} protocols.
486 @ivar peerCheck: perform checks to make sure the ftp-pi's peer is the same
488 @ivar pi: a reference to this factory's protocol interpreter
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
496 _IN_PROGRESS = object()
500 _state = _IN_PROGRESS
502 # -- configuration variables --
505 # -- class variables --
506 def __init__(self, pi, peerHost=None, reactor=None):
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
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
517 from twisted.internet import reactor
518 self._reactor = reactor
521 def buildProtocol(self, addr):
522 log.msg('DTPFactory.buildProtocol', debug=True)
524 if self._state is not self._IN_PROGRESS:
526 self._state = self._FINISHED
532 self.pi.dtpInstance = p
536 def stopFactory(self):
537 log.msg('dtpFactory.stopFactory', debug=True)
541 def timeoutFactory(self):
542 log.msg('timed out waiting for DTP connection')
543 if self._state is not self._IN_PROGRESS:
545 self._state = self._FAILED
550 PortConnectionError(defer.TimeoutError("DTPFactory timeout")))
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()
559 def setTimeout(self, seconds):
560 log.msg('DTPFactory.setTimeout set to %s seconds' % seconds)
561 self.delayedCall = self._reactor.callLater(seconds, self.timeoutFactory)
564 def clientConnectionFailed(self, connector, reason):
565 if self._state is not self._IN_PROGRESS:
567 self._state = self._FAILED
570 d.errback(PortConnectionError(reason))
573 # -- FTP-PI (Protocol Interpreter) --
575 class ASCIIConsumerWrapper(object):
576 def __init__(self, cons):
578 self.registerProducer = cons.registerProducer
579 self.unregisterProducer = cons.unregisterProducer
581 assert os.linesep == "\r\n" or len(os.linesep) == 1, "Unsupported platform (yea right like this even exists)"
583 if os.linesep == "\r\n":
584 self.write = cons.write
586 def write(self, bytes):
587 return self.cons.write(bytes.replace(os.linesep, "\r\n"))
591 class FileConsumer(object):
593 A consumer for FTP input that writes data to a file.
595 @ivar fObj: a file object opened for writing, used to write data received.
599 implements(interfaces.IConsumer)
601 def __init__(self, fObj):
605 def registerProducer(self, producer, streaming):
606 self.producer = producer
610 def unregisterProducer(self):
615 def write(self, bytes):
616 self.fObj.write(bytes)
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()
627 class FTP(object, basic.LineReceiver, policies.TimeoutMixin):
629 Protocol Interpreter for the File Transfer Protocol
631 @ivar state: The current server state. One of L{UNAUTH},
632 L{INAUTH}, L{AUTHED}, L{RENAMING}.
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).
642 @ivar passivePortRange: iterator used as source of passive port numbers.
643 @type passivePortRange: C{iterator}
648 # States an FTP can be in
649 UNAUTH, INAUTH, AUTHED, RENAMING = range(4)
651 # how long the DTP waits for a connection
661 passivePortRange = xrange(0, 1)
663 listenFactory = reactor.listenTCP
665 def reply(self, key, *args):
666 msg = RESPONSE[key] % args
670 def connectionMade(self):
671 self.state = self.UNAUTH
672 self.setTimeout(self.timeOut)
673 self.reply(WELCOME_MSG, self.factory.welcomeMessage)
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
681 self.setTimeout(None)
682 if hasattr(self.shell, 'logout') and self.shell.logout is not None:
685 self.transport = None
687 def timeoutConnection(self):
688 self.transport.loseConnection()
690 def lineReceived(self, line):
692 self.pauseProducing()
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,))
701 log.msg("Unexpected FTP error")
703 self.reply(REQ_ACTN_NOT_TAKEN, "internal server error")
705 def processSucceeded(result):
706 if isinstance(result, tuple):
708 elif result is not None:
711 def allDone(ignored):
712 if not self.disconnected:
713 self.resumeProducing()
715 spaceIndex = line.find(' ')
717 cmd = line[:spaceIndex]
718 args = (line[spaceIndex + 1:],)
722 d = defer.maybeDeferred(self.processCommand, cmd, *args)
723 d.addCallbacks(processSucceeded, processFailed)
724 d.addErrback(log.err)
727 # LineReceiver doesn't let you resumeProducing inside
729 from twisted.internet import reactor
730 reactor.callLater(0, d.addBoth, allDone)
733 def processCommand(self, cmd, *params):
736 if self.state == self.UNAUTH:
738 return self.ftp_USER(*params)
740 return BAD_CMD_SEQ, "USER required before PASS"
744 elif self.state == self.INAUTH:
746 return self.ftp_PASS(*params)
748 return BAD_CMD_SEQ, "PASS required after USER"
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))
756 elif self.state == self.RENAMING:
758 return self.ftp_RNTO(*params)
760 return BAD_CMD_SEQ, "RNTO required after RNFR"
763 def getDTPPort(self, factory):
765 Return a port for passive access, using C{self.passivePortRange}
768 for portn in self.passivePortRange:
770 dtpPort = self.listenFactory(portn, factory)
771 except error.CannotListenError:
775 raise error.CannotListenError('', portn,
776 "No port available in range %s" %
777 (self.passivePortRange,))
780 def ftp_USER(self, username):
782 First part of login. Get the username the peer wants to
786 return defer.fail(CmdSyntaxError('USER requires an argument'))
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
793 return (USR_NAME_OK_NEED_PASS, username)
795 # TODO: add max auth try before timeout from ip...
796 # TODO: need to implement minimal ABOR command
798 def ftp_PASS(self, password):
800 Second part of login. Get the password the peer wants to
803 if self.factory.allowAnonymous and self._user == self.factory.userAnonymous:
805 creds = credentials.Anonymous()
806 reply = GUEST_LOGGED_IN_PROCEED
809 creds = credentials.UsernamePassword(self._user, password)
810 reply = USR_LOGGED_IN_PROCEED
813 def _cbLogin((interface, avatar, logout)):
814 assert interface is IFTPShell, "The realm is busted, jerk."
817 self.workingDirectory = []
818 self.state = self.AUTHED
821 def _ebLogin(failure):
822 failure.trap(cred_error.UnauthorizedLogin, cred_error.UnhandledCredentials)
823 self.state = self.UNAUTH
824 raise AuthorizationError
826 d = self.portal.login(creds, None, IFTPShell)
827 d.addCallbacks(_cbLogin, _ebLogin)
832 """Request for a passive connection
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.
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.
847 self.dtpFactory = DTPFactory(pi=self)
848 self.dtpFactory.setTimeout(self.dtpTimeout)
849 self.dtpPort = self.getDTPPort(self.dtpFactory)
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)
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]
862 # if we have a DTP port set up, lose it.
863 if self.dtpFactory is not None:
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)
870 def connected(ignored):
871 return ENTERING_PORT_MODE
873 err.trap(PortConnectionError)
874 return CANT_OPEN_DATA_CNX
875 return self.dtpFactory.deferred.addCallbacks(connected, connFailed)
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
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'))
897 # bug in Nautilus 2.10.0
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,)
912 segments = toSegments(self.workingDirectory, path)
914 return defer.fail(FileNotFoundError(path))
918 ('size', 'directory', 'permissions', 'hardlinks',
919 'modified', 'owner', 'group'))
920 d.addCallback(gotListing)
924 def ftp_NLST(self, path):
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.
933 @param path: The path for which a directory listing should be returned.
936 @return: a L{Deferred} which will be fired when the listing request
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:
942 BadCmdSequenceError('must send PORT or PASV before RETR'))
945 segments = toSegments(self.workingDirectory, path)
947 return defer.fail(FileNotFoundError(path))
951 Send, line by line, each file in the directory listing, and then
952 close the connection.
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.
959 @return: A C{tuple} containing the status code for a successful
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,)
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,)
976 def listErr(results):
978 RFC 959 specifies that an NLST request may only return directory
979 listings. Thus, send nothing and just close the connection.
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
987 @returns: A C{tuple} containing the status code for a successful
990 self.dtpInstance.transport.loseConnection()
991 return (TXFR_COMPLETE_OK,)
993 # XXX This globbing may be incomplete: see #4181
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)
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)
1007 def ftp_CWD(self, path):
1009 segments = toSegments(self.workingDirectory, path)
1011 # XXX Eh, what to fail with here?
1012 return defer.fail(FileNotFoundError(path))
1014 def accessGranted(result):
1015 self.workingDirectory = segments
1016 return (REQ_FILE_ACTN_COMPLETED_OK,)
1018 return self.shell.access(segments).addCallback(accessGranted)
1022 return self.ftp_CWD('..')
1026 return (PWD_REPLY, '/' + '/'.join(self.workingDirectory))
1029 def ftp_RETR(self, path):
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.
1035 @param path: The path to the file which should be transferred over the
1036 data transfer channel.
1039 @return: a L{Deferred} which will be fired when the transfer is done.
1041 if self.dtpInstance is None:
1042 raise BadCmdSequenceError('PORT or PASV required before RETR')
1045 newsegs = toSegments(self.workingDirectory, path)
1047 return defer.fail(FileNotFoundError(path))
1049 # XXX For now, just disable the timeout. Later we'll want to
1050 # leave it active and have the DTP connection reset it
1052 self.setTimeout(None)
1055 def enableTimeout(result):
1056 self.setTimeout(self.factory.timeOut)
1061 cons = ASCIIConsumerWrapper(self.dtpInstance)
1063 cons = self.dtpInstance
1066 return (TXFR_COMPLETE_OK,)
1069 log.msg("Unexpected error attempting to transmit file to client:")
1071 return (CNX_CLOSED_TXFR_ABORTED,)
1074 # Tell them what to doooo
1075 if self.dtpInstance.isConnected:
1076 self.reply(DATA_CNX_ALREADY_OPEN_START_XFR)
1078 self.reply(FILE_STATUS_OK_OPEN_DATA_CNX)
1081 d.addCallbacks(cbSent, ebSent)
1085 if not err.check(PermissionDeniedError, FileNotFoundError, IsADirectoryError):
1086 log.msg("Unexpected error attempting to open file for transmission:")
1088 if err.check(FTPCmdError):
1089 return (err.value.errorCode, '/'.join(newsegs))
1090 return (FILE_NOT_FOUND, '/'.join(newsegs))
1092 d = self.shell.openForReading(newsegs)
1093 d.addCallbacks(cbOpened, ebOpened)
1094 d.addBoth(enableTimeout)
1096 # Pass back Deferred that fires when the transfer is done
1100 def ftp_STOR(self, path):
1101 if self.dtpInstance is None:
1102 raise BadCmdSequenceError('PORT or PASV required before STOR')
1105 newsegs = toSegments(self.workingDirectory, path)
1107 return defer.fail(FileNotFoundError(path))
1109 # XXX For now, just disable the timeout. Later we'll want to
1110 # leave it active and have the DTP connection reset it
1112 self.setTimeout(None)
1115 def enableTimeout(result):
1116 self.setTimeout(self.factory.timeOut)
1120 return (TXFR_COMPLETE_OK,)
1123 log.msg("Unexpected error receiving file from client:")
1125 if err.check(FTPCmdError):
1127 return (CNX_CLOSED_TXFR_ABORTED,)
1129 def cbConsumer(cons):
1131 cons = ASCIIConsumerWrapper(cons)
1133 d = self.dtpInstance.registerConsumer(cons)
1135 # Tell them what to doooo
1136 if self.dtpInstance.isConnected:
1137 self.reply(DATA_CNX_ALREADY_OPEN_START_XFR)
1139 self.reply(FILE_STATUS_OK_OPEN_DATA_CNX)
1145 d.addCallback(cbConsumer)
1146 d.addCallback(lambda ignored: file.close())
1147 d.addCallbacks(cbSent, ebSent)
1151 if not err.check(PermissionDeniedError, FileNotFoundError, IsNotADirectoryError):
1152 log.msg("Unexpected error attempting to open file for upload:")
1154 if isinstance(err.value, FTPCmdError):
1155 return (err.value.errorCode, '/'.join(newsegs))
1156 return (FILE_NOT_FOUND, '/'.join(newsegs))
1158 d = self.shell.openForWriting(newsegs)
1159 d.addCallbacks(cbOpened, ebOpened)
1160 d.addBoth(enableTimeout)
1162 # Pass back Deferred that fires when the transfer is done
1166 def ftp_SIZE(self, path):
1168 newsegs = toSegments(self.workingDirectory, path)
1170 return defer.fail(FileNotFoundError(path))
1172 def cbStat((size,)):
1173 return (FILE_STATUS, str(size))
1175 return self.shell.stat(newsegs, ('size',)).addCallback(cbStat)
1178 def ftp_MDTM(self, path):
1180 newsegs = toSegments(self.workingDirectory, path)
1182 return defer.fail(FileNotFoundError(path))
1184 def cbStat((modified,)):
1185 return (FILE_STATUS, time.strftime('%Y%m%d%H%M%S', time.gmtime(modified)))
1187 return self.shell.stat(newsegs, ('modified',)).addCallback(cbStat)
1190 def ftp_TYPE(self, type):
1193 f = getattr(self, 'type_' + p[0], None)
1196 return self.type_UNKNOWN(p)
1197 return (SYNTAX_ERR,)
1199 def type_A(self, code):
1200 if code == '' or code == 'N':
1202 return (TYPE_SET_OK, 'A' + code)
1204 return defer.fail(CmdArgSyntaxError(code))
1206 def type_I(self, code):
1209 return (TYPE_SET_OK, 'I')
1211 return defer.fail(CmdArgSyntaxError(code))
1213 def type_UNKNOWN(self, code):
1214 return defer.fail(CmdNotImplementedForArgError(code))
1219 return NAME_SYS_TYPE
1222 def ftp_STRU(self, structure):
1223 p = structure.upper()
1226 return defer.fail(CmdNotImplementedForArgError(structure))
1229 def ftp_MODE(self, mode):
1233 return defer.fail(CmdNotImplementedForArgError(mode))
1236 def ftp_MKD(self, path):
1238 newsegs = toSegments(self.workingDirectory, path)
1240 return defer.fail(FileNotFoundError(path))
1241 return self.shell.makeDirectory(newsegs).addCallback(lambda ign: (MKD_REPLY, path))
1244 def ftp_RMD(self, path):
1246 newsegs = toSegments(self.workingDirectory, path)
1248 return defer.fail(FileNotFoundError(path))
1249 return self.shell.removeDirectory(newsegs).addCallback(lambda ign: (REQ_FILE_ACTN_COMPLETED_OK,))
1252 def ftp_DELE(self, path):
1254 newsegs = toSegments(self.workingDirectory, path)
1256 return defer.fail(FileNotFoundError(path))
1257 return self.shell.removeFile(newsegs).addCallback(lambda ign: (REQ_FILE_ACTN_COMPLETED_OK,))
1264 def ftp_RNFR(self, fromName):
1265 self._fromName = fromName
1266 self.state = self.RENAMING
1267 return (REQ_FILE_ACTN_PENDING_FURTHER_INFO,)
1270 def ftp_RNTO(self, toName):
1271 fromName = self._fromName
1273 self.state = self.AUTHED
1276 fromsegs = toSegments(self.workingDirectory, fromName)
1277 tosegs = toSegments(self.workingDirectory, toName)
1279 return defer.fail(FileNotFoundError(fromName))
1280 return self.shell.rename(fromsegs, tosegs).addCallback(lambda ign: (REQ_FILE_ACTN_COMPLETED_OK,))
1284 self.reply(GOODBYE_MSG)
1285 self.transport.loseConnection()
1286 self.disconnected = True
1289 def cleanupDTP(self):
1290 """call when DTP connection exits
1292 log.msg('cleanupDTP', debug=True)
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()
1301 assert False, "dtpPort should be an IListeningPort or IConnector, instead is %r" % (dtpPort,)
1303 self.dtpFactory.stopFactory()
1304 self.dtpFactory = None
1306 if self.dtpInstance is not None:
1307 self.dtpInstance = None
1310 class FTPFactory(policies.LimitTotalConnectionsFactory):
1312 A factory for producing ftp protocol instances
1314 @ivar timeOut: the protocol interpreter's idle timeout time in seconds,
1315 default is 600 seconds.
1317 @ivar passivePortRange: value forwarded to C{protocol.passivePortRange}.
1318 @type passivePortRange: C{iterator}
1321 overflowProtocol = FTPOverflowProtocol
1322 allowAnonymous = True
1323 userAnonymous = 'anonymous'
1326 welcomeMessage = "Twisted %s FTP Server" % (copyright.version,)
1328 passivePortRange = xrange(0, 1)
1330 def __init__(self, portal=None, userAnonymous='anonymous'):
1331 self.portal = portal
1332 self.userAnonymous = userAnonymous
1335 def buildProtocol(self, addr):
1336 p = policies.LimitTotalConnectionsFactory.buildProtocol(self, addr)
1338 p.wrappedProtocol.portal = self.portal
1339 p.wrappedProtocol.timeOut = self.timeOut
1340 p.wrappedProtocol.passivePortRange = self.passivePortRange
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)
1349 # -- Cred Objects --
1352 class IFTPShell(Interface):
1354 An abstraction of the shell commands used by the FTP protocol for
1355 a given user account.
1357 All path names must be absolute.
1360 def makeDirectory(path):
1364 @param path: The path, as a list of segments, to create
1365 @type path: C{list} of C{unicode}
1367 @return: A Deferred which fires when the directory has been
1368 created, or which fails if the directory cannot be created.
1372 def removeDirectory(path):
1376 @param path: The path, as a list of segments, to remove
1377 @type path: C{list} of C{unicode}
1379 @return: A Deferred which fires when the directory has been
1380 removed, or which fails if the directory cannot be removed.
1384 def removeFile(path):
1388 @param path: The path, as a list of segments, to remove
1389 @type path: C{list} of C{unicode}
1391 @return: A Deferred which fires when the file has been
1392 removed, or which fails if the file cannot be removed.
1396 def rename(fromPath, toPath):
1398 Rename a file or directory.
1400 @param fromPath: The current name of the path.
1401 @type fromPath: C{list} of C{unicode}
1403 @param toPath: The desired new name of the path.
1404 @type toPath: C{list} of C{unicode}
1406 @return: A Deferred which fires when the path has been
1407 renamed, or which fails if the path cannot be renamed.
1413 Determine whether access to the given path is allowed.
1415 @param path: The path, as a list of segments
1417 @return: A Deferred which fires with None if access is allowed
1418 or which fails with a specific exception type if access is
1423 def stat(path, keys=()):
1425 Retrieve information about the given path.
1427 This is like list, except it will never return results about
1432 def list(path, keys=()):
1434 Retrieve information about the given path.
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.
1441 @param path: The path, as a list of segments, to list
1442 @type path: C{list} of C{unicode}
1444 @param keys: A tuple of keys desired in the resulting
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:
1453 - C{'size'}: size in bytes, as an integer (this is kinda required)
1455 - C{'directory'}: boolean indicating the type of this entry
1457 - C{'permissions'}: a bitvector (see os.stat(foo).st_mode)
1459 - C{'hardlinks'}: Number of hard links to this entry
1461 - C{'modified'}: number of seconds since the epoch since entry was
1464 - C{'owner'}: string indicating the user owner of this entry
1466 - C{'group'}: string indicating the group owner of this entry
1470 def openForReading(path):
1472 @param path: The path, as a list of segments, to open
1473 @type path: C{list} of C{unicode}
1475 @rtype: C{Deferred} which will fire with L{IReadFile}
1479 def openForWriting(path):
1481 @param path: The path, as a list of segments, to open
1482 @type path: C{list} of C{unicode}
1484 @rtype: C{Deferred} which will fire with L{IWriteFile}
1489 class IReadFile(Interface):
1491 A file out of which bytes may be read.
1496 Produce the contents of the given path to the given consumer. This
1497 method may only be invoked once on each provider.
1499 @type consumer: C{IConsumer}
1501 @return: A Deferred which fires when the file has been
1502 consumed completely.
1507 class IWriteFile(Interface):
1509 A file into which bytes may be written.
1514 Create a consumer which will write to this file. This method may
1515 only be invoked once on each provider.
1517 @rtype: C{Deferred} of C{IConsumer}
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
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
1531 def _getgroups(uid):
1532 """Return the primary and supplementary groups for the given UID.
1537 pwent = pwd.getpwuid(uid)
1539 result.append(pwent.pw_gid)
1541 for grent in grp.getgrall():
1542 if pwent.pw_name in grent.gr_mem:
1543 result.append(grent.gr_gid)
1548 def _testPermissions(uid, gid, spath, mode='r'):
1550 checks to see if uid has proper permissions to access path with mode
1553 @param uid: numeric user id
1556 @param gid: numeric group id
1559 @param spath: the path on the server to test
1562 @param mode: 'r' or 'w' (read or write)
1565 @return: True if the given credentials have the specified form of
1566 access to the given path
1579 raise ValueError("Invalid mode %r: must specify 'r' or 'w'" % (mode,))
1582 if os.path.exists(spath):
1587 if usr & s.st_mode and uid == s.st_uid:
1589 elif grp & s.st_mode and gid in _getgroups(uid):
1591 elif oth & s.st_mode:
1595 if not os.access(spath, amode):
1597 log.msg("Filesystem grants permission to UID %d but it is inaccessible to me running as UID %d" % (
1603 class FTPAnonymousShell(object):
1605 An anonymous implementation of IFTPShell
1607 @type filesystemRoot: L{twisted.python.filepath.FilePath}
1608 @ivar filesystemRoot: The path which is considered the root of
1611 implements(IFTPShell)
1613 def __init__(self, filesystemRoot):
1614 self.filesystemRoot = filesystemRoot
1617 def _path(self, path):
1618 return reduce(filepath.FilePath.child, path, self.filesystemRoot)
1621 def makeDirectory(self, path):
1622 return defer.fail(AnonUserDeniedError())
1625 def removeDirectory(self, path):
1626 return defer.fail(AnonUserDeniedError())
1629 def removeFile(self, path):
1630 return defer.fail(AnonUserDeniedError())
1633 def rename(self, fromPath, toPath):
1634 return defer.fail(AnonUserDeniedError())
1637 def receive(self, path):
1638 path = self._path(path)
1639 return defer.fail(AnonUserDeniedError())
1642 def openForReading(self, path):
1644 Open C{path} for reading.
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.
1653 p = self._path(path)
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))
1660 except (IOError, OSError), e:
1661 return errnoToFailure(e.errno, path)
1665 return defer.succeed(_FileReader(f))
1668 def openForWriting(self, path):
1670 Reject write attempts by anonymous users with
1671 L{PermissionDeniedError}.
1673 return defer.fail(PermissionDeniedError("STOR not allowed"))
1676 def access(self, path):
1677 p = self._path(path)
1679 # Again, win32 doesn't report a sane error after, so let's fail
1681 return defer.fail(FileNotFoundError(path))
1682 # For now, just see if we can os.listdir() it
1685 except (IOError, OSError), e:
1686 return errnoToFailure(e.errno, path)
1690 return defer.succeed(None)
1693 def stat(self, path, keys=()):
1694 p = self._path(path)
1697 statResult = self._statNode(p, keys)
1698 except (IOError, OSError), e:
1699 return errnoToFailure(e.errno, path)
1703 return defer.succeed(statResult)
1705 return self.list(path, keys).addCallback(lambda res: res[0][1])
1708 def list(self, path, keys=()):
1710 Return the list of files at given C{path}, adding C{keys} stat
1711 informations if specified.
1713 @param path: the directory or file to check.
1716 @param keys: the list of desired metadata
1717 @type keys: C{list} of C{str}
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]
1727 return defer.fail(FileNotFoundError(path))
1730 for fileName, filePath in zip(entries, fileEntries):
1732 results.append((fileName, ent))
1735 ent.extend(self._statNode(filePath, keys))
1736 except (IOError, OSError), e:
1737 return errnoToFailure(e.errno, fileName)
1741 return defer.succeed(results)
1744 def _statNode(self, filePath, keys):
1746 Shortcut method to get stat info on a node.
1748 @param filePath: the node to stat.
1749 @type filePath: C{filepath.FilePath}
1751 @param keys: the stat keys to get.
1752 @type keys: C{iterable}
1755 return [getattr(self, '_stat_' + k)(filePath.statinfo) for k in keys]
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')
1763 def _stat_owner(self, st):
1766 return pwd.getpwuid(st.st_uid)[0]
1769 return str(st.st_uid)
1772 def _stat_group(self, st):
1775 return grp.getgrgid(st.st_gid)[0]
1778 return str(st.st_gid)
1781 def _stat_directory(self, st):
1782 return bool(st.st_mode & stat.S_IFDIR)
1786 class _FileReader(object):
1787 implements(IReadFile)
1789 def __init__(self, fObj):
1793 def _close(self, passthrough):
1798 def send(self, consumer):
1799 assert not self._send, "Can only call IReadFile.send *once* per instance"
1801 d = basic.FileSender().beginFileTransfer(self.fObj, consumer)
1802 d.addBoth(self._close)
1807 class FTPShell(FTPAnonymousShell):
1809 An authenticated implementation of L{IFTPShell}.
1812 def makeDirectory(self, path):
1813 p = self._path(path)
1816 except (IOError, OSError), e:
1817 return errnoToFailure(e.errno, path)
1821 return defer.succeed(None)
1824 def removeDirectory(self, path):
1825 p = self._path(path)
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))
1833 except (IOError, OSError), e:
1834 return errnoToFailure(e.errno, path)
1838 return defer.succeed(None)
1841 def removeFile(self, path):
1842 p = self._path(path)
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))
1850 except (IOError, OSError), e:
1851 return errnoToFailure(e.errno, path)
1855 return defer.succeed(None)
1858 def rename(self, fromPath, toPath):
1859 fp = self._path(fromPath)
1860 tp = self._path(toPath)
1862 os.rename(fp.path, tp.path)
1863 except (IOError, OSError), e:
1864 return errnoToFailure(e.errno, fromPath)
1868 return defer.succeed(None)
1871 def openForWriting(self, path):
1873 Open C{path} for writing.
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.
1882 p = self._path(path)
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))
1889 except (IOError, OSError), e:
1890 return errnoToFailure(e.errno, path)
1893 return defer.succeed(_FileWriter(fObj))
1897 class _FileWriter(object):
1898 implements(IWriteFile)
1900 def __init__(self, fObj):
1902 self._receive = False
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))
1911 return defer.succeed(None)
1917 Base class for simple FTP realms which provides an easy hook for specifying
1918 the home directory for each user.
1920 implements(portal.IRealm)
1922 def __init__(self, anonymousRoot):
1923 self.anonymousRoot = filepath.FilePath(anonymousRoot)
1926 def getHomeDirectory(self, avatarId):
1928 Return a L{FilePath} representing the home directory of the given
1929 avatar. Override this in a subclass.
1931 @param avatarId: A user identifier returned from a credentials checker.
1932 @type avatarId: C{str}
1936 raise NotImplementedError(
1937 "%r did not override getHomeDirectory" % (self.__class__,))
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)
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")
1954 class FTPRealm(BaseFTPRealm):
1956 @type anonymousRoot: L{twisted.python.filepath.FilePath}
1957 @ivar anonymousRoot: Root of the filesystem to which anonymous
1958 users will be granted access.
1960 @type userHome: L{filepath.FilePath}
1961 @ivar userHome: Root of the filesystem containing user home directories.
1963 def __init__(self, anonymousRoot, userHome='/home'):
1964 BaseFTPRealm.__init__(self, anonymousRoot)
1965 self.userHome = filepath.FilePath(userHome)
1968 def getHomeDirectory(self, avatarId):
1970 Use C{avatarId} as a single path segment to construct a child of
1971 C{self.userHome} and return that child.
1973 return self.userHome.child(avatarId)
1977 class SystemFTPRealm(BaseFTPRealm):
1979 L{SystemFTPRealm} uses system user account information to decide what the
1980 home directory for a particular avatarId is.
1982 This works on POSIX but probably is not reliable on Windows.
1984 def getHomeDirectory(self, avatarId):
1986 Return the system-defined home directory of the system user account with
1987 the name C{avatarId}.
1989 path = os.path.expanduser('~' + avatarId)
1990 if path.startswith('~'):
1991 raise cred_error.UnauthorizedLogin()
1992 return filepath.FilePath(path)
1996 # --- FTP CLIENT -------------------------------------------------------------
1999 # And now for the client...
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
2013 class ConnectionLost(FTPError):
2016 class CommandFailed(FTPError):
2019 class BadResponse(FTPError):
2022 class UnexpectedResponse(FTPError):
2025 class UnexpectedData(FTPError):
2029 def __init__(self, text=None, public=0):
2031 self.deferred = defer.Deferred()
2033 self.public = public
2034 self.transferDeferred = None
2036 def fail(self, failure):
2038 self.deferred.errback(failure)
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)
2056 class IFinishableConsumer(interfaces.IConsumer):
2058 A Consumer for producers that finish.
2065 The producer has finished producing.
2070 class SenderProtocol(protocol.Protocol):
2071 implements(IFinishableConsumer)
2074 # Fired upon connection
2075 self.connectedDeferred = defer.Deferred()
2077 # Fired upon disconnection
2078 self.deferred = defer.Deferred()
2081 def dataReceived(self, data):
2082 raise UnexpectedData(
2083 "Received data from the server on a "
2084 "send-only data-connection"
2087 def makeConnection(self, transport):
2088 protocol.Protocol.makeConnection(self, transport)
2089 self.connectedDeferred.callback(self)
2091 def connectionLost(self, reason):
2092 if reason.check(error.ConnectionDone):
2093 self.deferred.callback('connection done')
2095 self.deferred.errback(reason)
2097 #IFinishableConsumer stuff
2098 def write(self, data):
2099 self.transport.write(data)
2101 def registerProducer(self, producer, streaming):
2103 Register the given producer with our transport.
2105 self.transport.registerProducer(producer, streaming)
2107 def unregisterProducer(self):
2109 Unregister the previously registered producer.
2111 self.transport.unregisterProducer()
2114 self.transport.loseConnection()
2117 def decodeHostPort(line):
2118 """Decode an FTP response specifying a host and port.
2120 @return: a 2-tuple of (host, port).
2122 abcdef = re.sub('[^0-9, ]', '', line)
2123 parsed = [int(p.strip()) for p in abcdef.split(',')]
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)
2132 def encodeHostPort(host, port):
2133 numbers = host.split('.') + [str(port >> 8), str(port % 256)]
2134 return ','.join(numbers)
2136 def _unwrapFirstError(failure):
2137 failure.trap(defer.FirstError)
2138 return failure.value.subFailure
2140 class FTPDataPortFactory(protocol.ServerFactory):
2141 """Factory for data connections that use the PORT command
2143 (i.e. "active" transfers)
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
2156 class FTPClientBasic(basic.LineReceiver):
2158 Foundations of an FTP client.
2163 self.actionQueue = []
2164 self.greeting = None
2165 self.nextDeferred = defer.Deferred().addCallback(self._cb_greeting)
2166 self.nextDeferred.addErrback(self.fail)
2170 def fail(self, error):
2172 Give an error to any queued deferreds.
2176 def _fail(self, error):
2178 Errback all queued deferreds.
2181 # We're recursing; bail out here for simplicity
2184 if self.nextDeferred:
2186 self.nextDeferred.errback(failure.Failure(ConnectionLost('FTP connection lost', error)))
2187 except defer.AlreadyCalledError:
2189 for ftpCommand in self.actionQueue:
2190 ftpCommand.fail(failure.Failure(ConnectionLost('FTP connection lost', error)))
2193 def _cb_greeting(self, greeting):
2194 self.greeting = greeting
2196 def sendLine(self, line):
2198 (Private) Sends a line, unless line is None.
2202 basic.LineReceiver.sendLine(self, line)
2204 def sendNextCommand(self):
2206 (Private) Processes the next command in the queue.
2208 ftpCommand = self.popCommandQueue()
2209 if ftpCommand is None:
2210 self.nextDeferred = None
2212 if not ftpCommand.ready:
2213 self.actionQueue.insert(0, ftpCommand)
2214 reactor.callLater(1.0, self.sendNextCommand)
2215 self.nextDeferred = None
2218 # FIXME: this if block doesn't belong in FTPClientBasic, it belongs in
2220 if ftpCommand.text == 'PORT':
2221 self.generatePortCommand(ftpCommand)
2224 log.msg('<-- %s' % ftpCommand.text)
2225 self.nextDeferred = ftpCommand.deferred
2226 self.sendLine(ftpCommand.text)
2228 def queueCommand(self, ftpCommand):
2230 Add an FTPCommand object to the queue.
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
2236 @param ftpCommand: an L{FTPCommand}
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()
2243 def queueStringCommand(self, command, public=1):
2245 Queues a string to be issued as an FTP command
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.
2251 @return: a L{Deferred} that will be called when the response to the
2252 command has been received.
2254 ftpCommand = FTPCommand(command, public)
2255 self.queueCommand(ftpCommand)
2256 return ftpCommand.deferred
2258 def popCommandQueue(self):
2260 Return the front element of the command queue, or None if empty.
2262 if self.actionQueue:
2263 return self.actionQueue.pop(0)
2267 def queueLogin(self, username, password):
2269 Login: send the username, send the password.
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.
2275 # Prepare the USER command
2277 userDeferred = self.queueStringCommand('USER ' + username, public=0)
2278 deferreds.append(userDeferred)
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)
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)
2293 userDeferred.addCallback(cancelPasswordIfNotNeeded)
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)
2302 def lineReceived(self, line):
2304 (Private) Parses the response messages from the FTP server.
2306 # Add this line to the current response
2308 log.msg('--> %s' % line)
2309 self.response.append(line)
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)
2323 # Check that we were expecting a response
2324 if self.nextDeferred is None:
2325 self.fail(UnexpectedResponse(self.response))
2328 # Reset the response
2329 response = self.response
2332 # Look for a success or error code, and call the appropriate callback
2333 if code[0] in ('2', '3'):
2335 self.nextDeferred.callback(response)
2336 elif code[0] in ('4', '5'):
2338 self.nextDeferred.errback(failure.Failure(CommandFailed(response)))
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)))
2344 # Run the next command
2345 self.sendNextCommand()
2347 def connectionLost(self, reason):
2352 class _PassiveConnectionFactory(protocol.ClientFactory):
2355 def __init__(self, protoInstance):
2356 self.protoInstance = protoInstance
2358 def buildProtocol(self, ignored):
2359 self.protoInstance.factory = self
2360 return self.protoInstance
2362 def clientConnectionFailed(self, connector, reason):
2363 e = FTPError('Connection Failed', reason)
2364 self.protoInstance.deferred.errback(e)
2368 class FTPClient(FTPClientBasic):
2370 L{FTPClient} is a client implementation of the FTP protocol which
2371 exposes FTP commands as methods which return L{Deferred}s.
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.
2381 See U{RFC 959<http://www.ietf.org/rfc/rfc959.txt>} for error code
2384 Both active and passive transfers are supported.
2386 @ivar passive: See description in __init__.
2388 connectFactory = reactor.connectTCP
2390 def __init__(self, username='anonymous',
2391 password='twisted@twistedmatrix.com',
2396 I will login as soon as I receive the welcome message from the server.
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}.
2404 FTPClientBasic.__init__(self)
2405 self.queueLogin(username, password)
2407 self.passive = passive
2409 def fail(self, error):
2411 Disconnect, and also give an error to any queued deferreds.
2413 self.transport.loseConnection()
2416 def receiveFromConnection(self, commands, protocol):
2418 Retrieves a file or listing generated by the given command,
2419 feeding it to the given protocol.
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.
2427 @return: L{Deferred}.
2429 protocol = interfaces.IProtocol(protocol)
2430 wrapper = ProtocolWrapper(protocol, defer.Deferred())
2431 return self._openDataConnection(commands, wrapper)
2433 def queueLogin(self, username, password):
2435 Login: send the username, send the password, and
2436 set retrieval mode to binary
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)
2445 def sendToConnection(self, commands):
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.
2455 s = SenderProtocol()
2456 r = self._openDataConnection(commands, s)
2457 return (s.connectedDeferred, r)
2459 def _openDataConnection(self, commands, protocol):
2461 This method returns a DeferredList.
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)
2469 # Hack: use a mutable object to sneak a variable out of the
2470 # scope of doPassive
2472 def doPassive(response):
2473 """Connect to the port specified in the response to PASV"""
2474 host, port = decodeHostPort(response[-1][4:])
2476 f = _PassiveConnectionFactory(protocol)
2477 _mutable[0] = self.connectFactory(host, port, f)
2479 pasvCmd = FTPCommand('PASV')
2480 self.queueCommand(pasvCmd)
2481 pasvCmd.deferred.addCallback(doPassive).addErrback(self.fail)
2483 results = [cmdsDeferred, pasvCmd.deferred, protocol.deferred]
2484 d = defer.DeferredList(results, fireOnOneErrback=True, consumeErrors=True)
2485 d.addErrback(_unwrapFirstError)
2487 # Ensure the connection is always closed
2488 def close(x, m=_mutable):
2489 m[0] and m[0].disconnect()
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')
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
2505 portCmd.transferDeferred = protocol.deferred
2506 portCmd.protocol = protocol
2507 portCmd.deferred.addErrback(portCmd.transferDeferred.errback)
2508 self.queueCommand(portCmd)
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
2516 # Ensure that the connection always gets closed
2517 cmdsDeferred.addErrback(lambda e, pc=portCmd: pc.fail(e) or e)
2519 results = [cmdsDeferred, portCmd.deferred, portCmd.transferDeferred]
2520 d = defer.DeferredList(results, fireOnOneErrback=True, consumeErrors=True)
2521 d.addErrback(_unwrapFirstError)
2524 self.queueCommand(cmd)
2527 def generatePortCommand(self, portCmd):
2529 (Private) Generates the text of a given PORT command.
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.
2537 # FIXME: This method is far too ugly.
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.
2543 # Start listening on a port
2544 factory = FTPDataPortFactory()
2545 factory.protocol = portCmd.protocol
2546 listener = reactor.listenTCP(0, factory)
2547 factory.port = listener
2549 # Ensure we close the listening port if something goes wrong
2550 def listenerFail(error, listener=listener):
2551 if listener.connected:
2552 listener.loseConnection()
2554 portCmd.fail = listenerFail
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)
2561 def escapePath(self, path):
2563 Returns a FTP escaped path (replace newlines with nulls).
2565 # Escape newline characters
2566 return path.replace('\n', '\0')
2568 def retrieveFile(self, path, protocol, offset=0):
2570 Retrieve a file from the given path
2572 This method issues the 'RETR' FTP command.
2574 The file is fed into the given Protocol instance. The data connection
2575 will be passive if self.passive is set.
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
2581 @return: L{Deferred}
2583 cmds = ['RETR ' + self.escapePath(path)]
2585 cmds.insert(0, ('REST ' + str(offset)))
2586 return self.receiveFromConnection(cmds, protocol)
2590 def storeFile(self, path, offset=0):
2592 Store a file at the given path.
2594 This method issues the 'STOR' FTP command.
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.
2602 cmds = ['STOR ' + self.escapePath(path)]
2604 cmds.insert(0, ('REST ' + str(offset)))
2605 return self.sendToConnection(cmds)
2610 def rename(self, pathFrom, pathTo):
2614 This method issues the I{RNFR}/I{RNTO} command sequence to rename
2615 C{pathFrom} to C{pathTo}.
2617 @param: pathFrom: the absolute path to the file to be renamed
2618 @type pathFrom: C{str}
2620 @param: pathTo: the absolute path to rename the file to.
2621 @type pathTo: C{str}
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
2634 renameFrom = self.queueStringCommand('RNFR ' + self.escapePath(pathFrom))
2635 renameTo = self.queueStringCommand('RNTO ' + self.escapePath(pathTo))
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))
2645 def ebFrom(failure):
2646 # Make sure the RNTO doesn't run if the RNFR failed.
2647 self.popCommandQueue()
2648 result.errback(failure)
2650 # Save the RNFR response to pass to the result Deferred later
2651 renameFrom.addCallbacks(fromResponse.extend, ebFrom)
2653 # Hook up the RNTO to the result Deferred as well
2654 renameTo.chainDeferred(result)
2659 def list(self, path, protocol):
2661 Retrieve a file listing into the given protocol instance.
2663 This method issues the 'LIST' FTP command.
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
2670 @return: L{Deferred}
2674 return self.receiveFromConnection(['LIST ' + self.escapePath(path)], protocol)
2677 def nlst(self, path, protocol):
2679 Retrieve a short file listing into the given protocol instance.
2681 This method issues the 'NLST' FTP command.
2683 NLST (should) return a list of filenames, one per line.
2685 @param path: path to get short file listing for.
2686 @param protocol: a L{Protocol} instance.
2690 return self.receiveFromConnection(['NLST ' + self.escapePath(path)], protocol)
2693 def cwd(self, path):
2695 Issues the CWD (Change Working Directory) command. It's also
2696 available as changeDirectory, which parses the result.
2698 @return: a L{Deferred} that will be called when done.
2700 return self.queueStringCommand('CWD ' + self.escapePath(path))
2703 def changeDirectory(self, path):
2705 Change the directory on the server and parse the result to determine
2706 if it was successful or not.
2709 @param path: The path to which to change.
2711 @return: a L{Deferred} which will be called back when the directory
2712 change has succeeded or errbacked if an error occurrs.
2715 "FTPClient.changeDirectory is deprecated in Twisted 8.2 and "
2716 "newer. Use FTPClient.cwd instead.",
2717 category=DeprecationWarning,
2720 def cbResult(result):
2721 if result[-1][:3] != '250':
2722 return failure.Failure(CommandFailed(result))
2724 return self.cwd(path).addCallback(cbResult)
2727 def makeDirectory(self, path):
2731 This method issues the MKD command.
2733 @param path: The path to the directory to create.
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}.
2745 return self.queueStringCommand('MKD ' + self.escapePath(path))
2748 def removeFile(self, path):
2750 Delete a file on the server.
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.
2755 @param path: The path to the file to delete. May be relative to the
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.
2766 return self.queueStringCommand('DELE ' + self.escapePath(path))
2769 def removeDirectory(self, path):
2771 Delete a directory on the server.
2773 L{removeDirectory} issues a I{RMD} command to the server to remove the
2774 indicated directory. Described in RFC959.
2776 @param path: The path to the directory to delete. May be relative to
2777 the current working directory.
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.
2787 return self.queueStringCommand('RMD ' + self.escapePath(path))
2792 Issues the CDUP (Change Directory UP) command.
2794 @return: a L{Deferred} that will be called when done.
2796 return self.queueStringCommand('CDUP')
2801 Issues the PWD (Print Working Directory) command.
2803 The L{getDirectory} does the same job but automatically parses the
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.
2810 return self.queueStringCommand('PWD')
2813 def getDirectory(self):
2815 Returns the current remote directory.
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.
2821 def cbParse(result):
2823 # The only valid code is 257
2824 if int(result[0].split(' ', 1)[0]) != 257:
2826 except (IndexError, ValueError):
2827 return failure.Failure(CommandFailed(result))
2828 path = parsePWDResponse(result[0])
2830 return failure.Failure(CommandFailed(result))
2832 return self.pwd().addCallback(cbParse)
2837 Issues the I{QUIT} command.
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.
2843 return self.queueStringCommand('QUIT')
2847 class FTPFileListProtocol(basic.LineReceiver):
2848 """Parser for standard FTP file listings
2850 This is the evil required to match::
2852 -rw-r--r-- 1 root other 531 Jan 29 03:26 README
2854 If you need different evil for a wacky FTP server, you can
2855 override either C{fileLinePattern} or C{parseDirectoryLine()}.
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--'
2862 - owner: e.g. 'root'
2863 - group: e.g. 'other'
2865 - date: e.g. 'Jan 29 03:26'
2866 - filename: e.g. 'README'
2867 - linktarget: e.g. 'some/file'
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
2873 @ivar files: list of dicts describing the files in this listing
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?$'
2886 def lineReceived(self, line):
2887 d = self.parseDirectoryLine(line)
2889 self.unknownLine(line)
2893 def parseDirectoryLine(self, line):
2894 """Return a dictionary of fields, or None if line cannot be parsed.
2896 @param line: line of text expected to contain a directory entry
2901 match = self.fileLinePattern.match(line)
2905 d = match.groupdict()
2906 d['filename'] = d['filename'].replace(r'\ ', ' ')
2907 d['nlinks'] = int(d['nlinks'])
2908 d['size'] = int(d['size'])
2910 d['linktarget'] = d['linktarget'].replace(r'\ ', ' ')
2913 def addFile(self, info):
2914 """Append file information dictionary to the list of known files.
2916 Subclasses can override or extend this method to handle file
2917 information differently without affecting the parsing of data
2920 @param info: dictionary containing the parsed representation
2921 of the file information
2924 self.files.append(info)
2926 def unknownLine(self, line):
2927 """Deal with received lines which could not be parsed as file
2930 Subclasses can override this to perform any special processing
2933 @param line: unparsable line as received
2938 def parsePWDResponse(response):
2939 """Returns the path from a response to a PWD command.
2941 Responses typically look like::
2943 257 "/home/andrew" is current directory.
2945 For this example, I will return C{'/home/andrew'}.
2947 If I can't find the path, I return C{None}.
2949 match = re.search('"(.*)"', response)
2951 return match.groups()[0]