diff options
Diffstat (limited to 'lib/python2.7/site-packages/Twisted-12.2.0-py2.7-linux-x86_64.egg/twisted/protocols/ftp.py')
-rwxr-xr-x | lib/python2.7/site-packages/Twisted-12.2.0-py2.7-linux-x86_64.egg/twisted/protocols/ftp.py | 2955 |
1 files changed, 0 insertions, 2955 deletions
diff --git a/lib/python2.7/site-packages/Twisted-12.2.0-py2.7-linux-x86_64.egg/twisted/protocols/ftp.py b/lib/python2.7/site-packages/Twisted-12.2.0-py2.7-linux-x86_64.egg/twisted/protocols/ftp.py deleted file mode 100755 index b035f04e..00000000 --- a/lib/python2.7/site-packages/Twisted-12.2.0-py2.7-linux-x86_64.egg/twisted/protocols/ftp.py +++ /dev/null @@ -1,2955 +0,0 @@ -# -*- test-case-name: twisted.test.test_ftp -*- -# Copyright (c) Twisted Matrix Laboratories. -# See LICENSE for details. - -""" -An FTP protocol implementation -""" - -# System Imports -import os -import time -import re -import operator -import stat -import errno -import fnmatch -import warnings - -try: - import pwd, grp -except ImportError: - pwd = grp = None - -from zope.interface import Interface, implements - -# Twisted Imports -from twisted import copyright -from twisted.internet import reactor, interfaces, protocol, error, defer -from twisted.protocols import basic, policies - -from twisted.python import log, failure, filepath -from twisted.python.compat import reduce - -from twisted.cred import error as cred_error, portal, credentials, checkers - -# constants -# response codes - -RESTART_MARKER_REPLY = "100" -SERVICE_READY_IN_N_MINUTES = "120" -DATA_CNX_ALREADY_OPEN_START_XFR = "125" -FILE_STATUS_OK_OPEN_DATA_CNX = "150" - -CMD_OK = "200.1" -TYPE_SET_OK = "200.2" -ENTERING_PORT_MODE = "200.3" -CMD_NOT_IMPLMNTD_SUPERFLUOUS = "202" -SYS_STATUS_OR_HELP_REPLY = "211" -DIR_STATUS = "212" -FILE_STATUS = "213" -HELP_MSG = "214" -NAME_SYS_TYPE = "215" -SVC_READY_FOR_NEW_USER = "220.1" -WELCOME_MSG = "220.2" -SVC_CLOSING_CTRL_CNX = "221.1" -GOODBYE_MSG = "221.2" -DATA_CNX_OPEN_NO_XFR_IN_PROGRESS = "225" -CLOSING_DATA_CNX = "226.1" -TXFR_COMPLETE_OK = "226.2" -ENTERING_PASV_MODE = "227" -ENTERING_EPSV_MODE = "229" -USR_LOGGED_IN_PROCEED = "230.1" # v1 of code 230 -GUEST_LOGGED_IN_PROCEED = "230.2" # v2 of code 230 -REQ_FILE_ACTN_COMPLETED_OK = "250" -PWD_REPLY = "257.1" -MKD_REPLY = "257.2" - -USR_NAME_OK_NEED_PASS = "331.1" # v1 of Code 331 -GUEST_NAME_OK_NEED_EMAIL = "331.2" # v2 of code 331 -NEED_ACCT_FOR_LOGIN = "332" -REQ_FILE_ACTN_PENDING_FURTHER_INFO = "350" - -SVC_NOT_AVAIL_CLOSING_CTRL_CNX = "421.1" -TOO_MANY_CONNECTIONS = "421.2" -CANT_OPEN_DATA_CNX = "425" -CNX_CLOSED_TXFR_ABORTED = "426" -REQ_ACTN_ABRTD_FILE_UNAVAIL = "450" -REQ_ACTN_ABRTD_LOCAL_ERR = "451" -REQ_ACTN_ABRTD_INSUFF_STORAGE = "452" - -SYNTAX_ERR = "500" -SYNTAX_ERR_IN_ARGS = "501" -CMD_NOT_IMPLMNTD = "502" -BAD_CMD_SEQ = "503" -CMD_NOT_IMPLMNTD_FOR_PARAM = "504" -NOT_LOGGED_IN = "530.1" # v1 of code 530 - please log in -AUTH_FAILURE = "530.2" # v2 of code 530 - authorization failure -NEED_ACCT_FOR_STOR = "532" -FILE_NOT_FOUND = "550.1" # no such file or directory -PERMISSION_DENIED = "550.2" # permission denied -ANON_USER_DENIED = "550.3" # anonymous users can't alter filesystem -IS_NOT_A_DIR = "550.4" # rmd called on a path that is not a directory -REQ_ACTN_NOT_TAKEN = "550.5" -FILE_EXISTS = "550.6" -IS_A_DIR = "550.7" -PAGE_TYPE_UNK = "551" -EXCEEDED_STORAGE_ALLOC = "552" -FILENAME_NOT_ALLOWED = "553" - - -RESPONSE = { - # -- 100's -- - RESTART_MARKER_REPLY: '110 MARK yyyy-mmmm', # TODO: this must be fixed - SERVICE_READY_IN_N_MINUTES: '120 service ready in %s minutes', - DATA_CNX_ALREADY_OPEN_START_XFR: '125 Data connection already open, starting transfer', - FILE_STATUS_OK_OPEN_DATA_CNX: '150 File status okay; about to open data connection.', - - # -- 200's -- - CMD_OK: '200 Command OK', - TYPE_SET_OK: '200 Type set to %s.', - ENTERING_PORT_MODE: '200 PORT OK', - CMD_NOT_IMPLMNTD_SUPERFLUOUS: '202 Command not implemented, superfluous at this site', - SYS_STATUS_OR_HELP_REPLY: '211 System status reply', - DIR_STATUS: '212 %s', - FILE_STATUS: '213 %s', - HELP_MSG: '214 help: %s', - NAME_SYS_TYPE: '215 UNIX Type: L8', - WELCOME_MSG: "220 %s", - SVC_READY_FOR_NEW_USER: '220 Service ready', - SVC_CLOSING_CTRL_CNX: '221 Service closing control connection', - GOODBYE_MSG: '221 Goodbye.', - DATA_CNX_OPEN_NO_XFR_IN_PROGRESS: '225 data connection open, no transfer in progress', - CLOSING_DATA_CNX: '226 Abort successful', - TXFR_COMPLETE_OK: '226 Transfer Complete.', - ENTERING_PASV_MODE: '227 Entering Passive Mode (%s).', - ENTERING_EPSV_MODE: '229 Entering Extended Passive Mode (|||%s|).', # where is epsv defined in the rfc's? - USR_LOGGED_IN_PROCEED: '230 User logged in, proceed', - GUEST_LOGGED_IN_PROCEED: '230 Anonymous login ok, access restrictions apply.', - REQ_FILE_ACTN_COMPLETED_OK: '250 Requested File Action Completed OK', #i.e. CWD completed ok - PWD_REPLY: '257 "%s"', - MKD_REPLY: '257 "%s" created', - - # -- 300's -- - USR_NAME_OK_NEED_PASS: '331 Password required for %s.', - GUEST_NAME_OK_NEED_EMAIL: '331 Guest login ok, type your email address as password.', - NEED_ACCT_FOR_LOGIN: '332 Need account for login.', - - REQ_FILE_ACTN_PENDING_FURTHER_INFO: '350 Requested file action pending further information.', - -# -- 400's -- - SVC_NOT_AVAIL_CLOSING_CTRL_CNX: '421 Service not available, closing control connection.', - TOO_MANY_CONNECTIONS: '421 Too many users right now, try again in a few minutes.', - CANT_OPEN_DATA_CNX: "425 Can't open data connection.", - CNX_CLOSED_TXFR_ABORTED: '426 Transfer aborted. Data connection closed.', - - REQ_ACTN_ABRTD_FILE_UNAVAIL: '450 Requested action aborted. File unavailable.', - REQ_ACTN_ABRTD_LOCAL_ERR: '451 Requested action aborted. Local error in processing.', - REQ_ACTN_ABRTD_INSUFF_STORAGE: '452 Requested action aborted. Insufficient storage.', - - # -- 500's -- - SYNTAX_ERR: "500 Syntax error: %s", - SYNTAX_ERR_IN_ARGS: '501 syntax error in argument(s) %s.', - CMD_NOT_IMPLMNTD: "502 Command '%s' not implemented", - BAD_CMD_SEQ: '503 Incorrect sequence of commands: %s', - CMD_NOT_IMPLMNTD_FOR_PARAM: "504 Not implemented for parameter '%s'.", - NOT_LOGGED_IN: '530 Please login with USER and PASS.', - AUTH_FAILURE: '530 Sorry, Authentication failed.', - NEED_ACCT_FOR_STOR: '532 Need an account for storing files', - FILE_NOT_FOUND: '550 %s: No such file or directory.', - PERMISSION_DENIED: '550 %s: Permission denied.', - ANON_USER_DENIED: '550 Anonymous users are forbidden to change the filesystem', - IS_NOT_A_DIR: '550 Cannot rmd, %s is not a directory', - FILE_EXISTS: '550 %s: File exists', - IS_A_DIR: '550 %s: is a directory', - REQ_ACTN_NOT_TAKEN: '550 Requested action not taken: %s', - PAGE_TYPE_UNK: '551 Page type unknown', - EXCEEDED_STORAGE_ALLOC: '552 Requested file action aborted, exceeded file storage allocation', - FILENAME_NOT_ALLOWED: '553 Requested action not taken, file name not allowed' -} - - - -class InvalidPath(Exception): - """ - Internal exception used to signify an error during parsing a path. - """ - - - -def toSegments(cwd, path): - """ - Normalize a path, as represented by a list of strings each - representing one segment of the path. - """ - if path.startswith('/'): - segs = [] - else: - segs = cwd[:] - - for s in path.split('/'): - if s == '.' or s == '': - continue - elif s == '..': - if segs: - segs.pop() - else: - raise InvalidPath(cwd, path) - elif '\0' in s or '/' in s: - raise InvalidPath(cwd, path) - else: - segs.append(s) - return segs - - -def errnoToFailure(e, path): - """ - Map C{OSError} and C{IOError} to standard FTP errors. - """ - if e == errno.ENOENT: - return defer.fail(FileNotFoundError(path)) - elif e == errno.EACCES or e == errno.EPERM: - return defer.fail(PermissionDeniedError(path)) - elif e == errno.ENOTDIR: - return defer.fail(IsNotADirectoryError(path)) - elif e == errno.EEXIST: - return defer.fail(FileExistsError(path)) - elif e == errno.EISDIR: - return defer.fail(IsADirectoryError(path)) - else: - return defer.fail() - - - -class FTPCmdError(Exception): - """ - Generic exception for FTP commands. - """ - def __init__(self, *msg): - Exception.__init__(self, *msg) - self.errorMessage = msg - - - def response(self): - """ - Generate a FTP response message for this error. - """ - return RESPONSE[self.errorCode] % self.errorMessage - - - -class FileNotFoundError(FTPCmdError): - """ - Raised when trying to access a non existent file or directory. - """ - errorCode = FILE_NOT_FOUND - - - -class AnonUserDeniedError(FTPCmdError): - """ - Raised when an anonymous user issues a command that will alter the - filesystem - """ - - errorCode = ANON_USER_DENIED - - - -class PermissionDeniedError(FTPCmdError): - """ - Raised when access is attempted to a resource to which access is - not allowed. - """ - errorCode = PERMISSION_DENIED - - - -class IsNotADirectoryError(FTPCmdError): - """ - Raised when RMD is called on a path that isn't a directory. - """ - errorCode = IS_NOT_A_DIR - - - -class FileExistsError(FTPCmdError): - """ - Raised when attempted to override an existing resource. - """ - errorCode = FILE_EXISTS - - - -class IsADirectoryError(FTPCmdError): - """ - Raised when DELE is called on a path that is a directory. - """ - errorCode = IS_A_DIR - - - -class CmdSyntaxError(FTPCmdError): - """ - Raised when a command syntax is wrong. - """ - errorCode = SYNTAX_ERR - - - -class CmdArgSyntaxError(FTPCmdError): - """ - Raised when a command is called with wrong value or a wrong number of - arguments. - """ - errorCode = SYNTAX_ERR_IN_ARGS - - - -class CmdNotImplementedError(FTPCmdError): - """ - Raised when an unimplemented command is given to the server. - """ - errorCode = CMD_NOT_IMPLMNTD - - - -class CmdNotImplementedForArgError(FTPCmdError): - """ - Raised when the handling of a parameter for a command is not implemented by - the server. - """ - errorCode = CMD_NOT_IMPLMNTD_FOR_PARAM - - - -class FTPError(Exception): - pass - - - -class PortConnectionError(Exception): - pass - - - -class BadCmdSequenceError(FTPCmdError): - """ - Raised when a client sends a series of commands in an illogical sequence. - """ - errorCode = BAD_CMD_SEQ - - - -class AuthorizationError(FTPCmdError): - """ - Raised when client authentication fails. - """ - errorCode = AUTH_FAILURE - - - -def debugDeferred(self, *_): - log.msg('debugDeferred(): %s' % str(_), debug=True) - - -# -- DTP Protocol -- - - -_months = [ - None, - 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] - - -class DTP(object, protocol.Protocol): - implements(interfaces.IConsumer) - - isConnected = False - - _cons = None - _onConnLost = None - _buffer = None - - def connectionMade(self): - self.isConnected = True - self.factory.deferred.callback(None) - self._buffer = [] - - def connectionLost(self, reason): - self.isConnected = False - if self._onConnLost is not None: - self._onConnLost.callback(None) - - def sendLine(self, line): - self.transport.write(line + '\r\n') - - - def _formatOneListResponse(self, name, size, directory, permissions, hardlinks, modified, owner, group): - def formatMode(mode): - return ''.join([mode & (256 >> n) and 'rwx'[n % 3] or '-' for n in range(9)]) - - def formatDate(mtime): - now = time.gmtime() - info = { - 'month': _months[mtime.tm_mon], - 'day': mtime.tm_mday, - 'year': mtime.tm_year, - 'hour': mtime.tm_hour, - 'minute': mtime.tm_min - } - if now.tm_year != mtime.tm_year: - return '%(month)s %(day)02d %(year)5d' % info - else: - return '%(month)s %(day)02d %(hour)02d:%(minute)02d' % info - - format = ('%(directory)s%(permissions)s%(hardlinks)4d ' - '%(owner)-9s %(group)-9s %(size)15d %(date)12s ' - '%(name)s') - - return format % { - 'directory': directory and 'd' or '-', - 'permissions': formatMode(permissions), - 'hardlinks': hardlinks, - 'owner': owner[:8], - 'group': group[:8], - 'size': size, - 'date': formatDate(time.gmtime(modified)), - 'name': name} - - def sendListResponse(self, name, response): - self.sendLine(self._formatOneListResponse(name, *response)) - - - # Proxy IConsumer to our transport - def registerProducer(self, producer, streaming): - return self.transport.registerProducer(producer, streaming) - - def unregisterProducer(self): - self.transport.unregisterProducer() - self.transport.loseConnection() - - def write(self, data): - if self.isConnected: - return self.transport.write(data) - raise Exception("Crap damn crap damn crap damn") - - - # Pretend to be a producer, too. - def _conswrite(self, bytes): - try: - self._cons.write(bytes) - except: - self._onConnLost.errback() - - def dataReceived(self, bytes): - if self._cons is not None: - self._conswrite(bytes) - else: - self._buffer.append(bytes) - - def _unregConsumer(self, ignored): - self._cons.unregisterProducer() - self._cons = None - del self._onConnLost - return ignored - - def registerConsumer(self, cons): - assert self._cons is None - self._cons = cons - self._cons.registerProducer(self, True) - for chunk in self._buffer: - self._conswrite(chunk) - self._buffer = None - if self.isConnected: - self._onConnLost = d = defer.Deferred() - d.addBoth(self._unregConsumer) - return d - else: - self._cons.unregisterProducer() - self._cons = None - return defer.succeed(None) - - def resumeProducing(self): - self.transport.resumeProducing() - - def pauseProducing(self): - self.transport.pauseProducing() - - def stopProducing(self): - self.transport.stopProducing() - -class DTPFactory(protocol.ClientFactory): - """ - Client factory for I{data transfer process} protocols. - - @ivar peerCheck: perform checks to make sure the ftp-pi's peer is the same - as the dtp's - @ivar pi: a reference to this factory's protocol interpreter - - @ivar _state: Indicates the current state of the DTPFactory. Initially, - this is L{_IN_PROGRESS}. If the connection fails or times out, it is - L{_FAILED}. If the connection succeeds before the timeout, it is - L{_FINISHED}. - """ - - _IN_PROGRESS = object() - _FAILED = object() - _FINISHED = object() - - _state = _IN_PROGRESS - - # -- configuration variables -- - peerCheck = False - - # -- class variables -- - def __init__(self, pi, peerHost=None, reactor=None): - """Constructor - @param pi: this factory's protocol interpreter - @param peerHost: if peerCheck is True, this is the tuple that the - generated instance will use to perform security checks - """ - self.pi = pi # the protocol interpreter that is using this factory - self.peerHost = peerHost # the from FTP.transport.peerHost() - self.deferred = defer.Deferred() # deferred will fire when instance is connected - self.delayedCall = None - if reactor is None: - from twisted.internet import reactor - self._reactor = reactor - - - def buildProtocol(self, addr): - log.msg('DTPFactory.buildProtocol', debug=True) - - if self._state is not self._IN_PROGRESS: - return None - self._state = self._FINISHED - - self.cancelTimeout() - p = DTP() - p.factory = self - p.pi = self.pi - self.pi.dtpInstance = p - return p - - - def stopFactory(self): - log.msg('dtpFactory.stopFactory', debug=True) - self.cancelTimeout() - - - def timeoutFactory(self): - log.msg('timed out waiting for DTP connection') - if self._state is not self._IN_PROGRESS: - return - self._state = self._FAILED - - d = self.deferred - self.deferred = None - d.errback( - PortConnectionError(defer.TimeoutError("DTPFactory timeout"))) - - - def cancelTimeout(self): - if self.delayedCall is not None and self.delayedCall.active(): - log.msg('cancelling DTP timeout', debug=True) - self.delayedCall.cancel() - - - def setTimeout(self, seconds): - log.msg('DTPFactory.setTimeout set to %s seconds' % seconds) - self.delayedCall = self._reactor.callLater(seconds, self.timeoutFactory) - - - def clientConnectionFailed(self, connector, reason): - if self._state is not self._IN_PROGRESS: - return - self._state = self._FAILED - d = self.deferred - self.deferred = None - d.errback(PortConnectionError(reason)) - - -# -- FTP-PI (Protocol Interpreter) -- - -class ASCIIConsumerWrapper(object): - def __init__(self, cons): - self.cons = cons - self.registerProducer = cons.registerProducer - self.unregisterProducer = cons.unregisterProducer - - assert os.linesep == "\r\n" or len(os.linesep) == 1, "Unsupported platform (yea right like this even exists)" - - if os.linesep == "\r\n": - self.write = cons.write - - def write(self, bytes): - return self.cons.write(bytes.replace(os.linesep, "\r\n")) - - - -class FileConsumer(object): - """ - A consumer for FTP input that writes data to a file. - - @ivar fObj: a file object opened for writing, used to write data received. - @type fObj: C{file} - """ - - implements(interfaces.IConsumer) - - def __init__(self, fObj): - self.fObj = fObj - - - def registerProducer(self, producer, streaming): - self.producer = producer - assert streaming - - - def unregisterProducer(self): - self.producer = None - self.fObj.close() - - - def write(self, bytes): - self.fObj.write(bytes) - - - -class FTPOverflowProtocol(basic.LineReceiver): - """FTP mini-protocol for when there are too many connections.""" - def connectionMade(self): - self.sendLine(RESPONSE[TOO_MANY_CONNECTIONS]) - self.transport.loseConnection() - - -class FTP(object, basic.LineReceiver, policies.TimeoutMixin): - """ - Protocol Interpreter for the File Transfer Protocol - - @ivar state: The current server state. One of L{UNAUTH}, - L{INAUTH}, L{AUTHED}, L{RENAMING}. - - @ivar shell: The connected avatar - @ivar binary: The transfer mode. If false, ASCII. - @ivar dtpFactory: Generates a single DTP for this session - @ivar dtpPort: Port returned from listenTCP - @ivar listenFactory: A callable with the signature of - L{twisted.internet.interfaces.IReactorTCP.listenTCP} which will be used - to create Ports for passive connections (mainly for testing). - - @ivar passivePortRange: iterator used as source of passive port numbers. - @type passivePortRange: C{iterator} - """ - - disconnected = False - - # States an FTP can be in - UNAUTH, INAUTH, AUTHED, RENAMING = range(4) - - # how long the DTP waits for a connection - dtpTimeout = 10 - - portal = None - shell = None - dtpFactory = None - dtpPort = None - dtpInstance = None - binary = True - - passivePortRange = xrange(0, 1) - - listenFactory = reactor.listenTCP - - def reply(self, key, *args): - msg = RESPONSE[key] % args - self.sendLine(msg) - - - def connectionMade(self): - self.state = self.UNAUTH - self.setTimeout(self.timeOut) - self.reply(WELCOME_MSG, self.factory.welcomeMessage) - - def connectionLost(self, reason): - # if we have a DTP protocol instance running and - # we lose connection to the client's PI, kill the - # DTP connection and close the port - if self.dtpFactory: - self.cleanupDTP() - self.setTimeout(None) - if hasattr(self.shell, 'logout') and self.shell.logout is not None: - self.shell.logout() - self.shell = None - self.transport = None - - def timeoutConnection(self): - self.transport.loseConnection() - - def lineReceived(self, line): - self.resetTimeout() - self.pauseProducing() - - def processFailed(err): - if err.check(FTPCmdError): - self.sendLine(err.value.response()) - elif (err.check(TypeError) and - err.value.args[0].find('takes exactly') != -1): - self.reply(SYNTAX_ERR, "%s requires an argument." % (cmd,)) - else: - log.msg("Unexpected FTP error") - log.err(err) - self.reply(REQ_ACTN_NOT_TAKEN, "internal server error") - - def processSucceeded(result): - if isinstance(result, tuple): - self.reply(*result) - elif result is not None: - self.reply(result) - - def allDone(ignored): - if not self.disconnected: - self.resumeProducing() - - spaceIndex = line.find(' ') - if spaceIndex != -1: - cmd = line[:spaceIndex] - args = (line[spaceIndex + 1:],) - else: - cmd = line - args = () - d = defer.maybeDeferred(self.processCommand, cmd, *args) - d.addCallbacks(processSucceeded, processFailed) - d.addErrback(log.err) - - # XXX It burnsss - # LineReceiver doesn't let you resumeProducing inside - # lineReceived atm - from twisted.internet import reactor - reactor.callLater(0, d.addBoth, allDone) - - - def processCommand(self, cmd, *params): - cmd = cmd.upper() - - if self.state == self.UNAUTH: - if cmd == 'USER': - return self.ftp_USER(*params) - elif cmd == 'PASS': - return BAD_CMD_SEQ, "USER required before PASS" - else: - return NOT_LOGGED_IN - - elif self.state == self.INAUTH: - if cmd == 'PASS': - return self.ftp_PASS(*params) - else: - return BAD_CMD_SEQ, "PASS required after USER" - - elif self.state == self.AUTHED: - method = getattr(self, "ftp_" + cmd, None) - if method is not None: - return method(*params) - return defer.fail(CmdNotImplementedError(cmd)) - - elif self.state == self.RENAMING: - if cmd == 'RNTO': - return self.ftp_RNTO(*params) - else: - return BAD_CMD_SEQ, "RNTO required after RNFR" - - - def getDTPPort(self, factory): - """ - Return a port for passive access, using C{self.passivePortRange} - attribute. - """ - for portn in self.passivePortRange: - try: - dtpPort = self.listenFactory(portn, factory) - except error.CannotListenError: - continue - else: - return dtpPort - raise error.CannotListenError('', portn, - "No port available in range %s" % - (self.passivePortRange,)) - - - def ftp_USER(self, username): - """ - First part of login. Get the username the peer wants to - authenticate as. - """ - if not username: - return defer.fail(CmdSyntaxError('USER requires an argument')) - - self._user = username - self.state = self.INAUTH - if self.factory.allowAnonymous and self._user == self.factory.userAnonymous: - return GUEST_NAME_OK_NEED_EMAIL - else: - return (USR_NAME_OK_NEED_PASS, username) - - # TODO: add max auth try before timeout from ip... - # TODO: need to implement minimal ABOR command - - def ftp_PASS(self, password): - """ - Second part of login. Get the password the peer wants to - authenticate with. - """ - if self.factory.allowAnonymous and self._user == self.factory.userAnonymous: - # anonymous login - creds = credentials.Anonymous() - reply = GUEST_LOGGED_IN_PROCEED - else: - # user login - creds = credentials.UsernamePassword(self._user, password) - reply = USR_LOGGED_IN_PROCEED - del self._user - - def _cbLogin((interface, avatar, logout)): - assert interface is IFTPShell, "The realm is busted, jerk." - self.shell = avatar - self.logout = logout - self.workingDirectory = [] - self.state = self.AUTHED - return reply - - def _ebLogin(failure): - failure.trap(cred_error.UnauthorizedLogin, cred_error.UnhandledCredentials) - self.state = self.UNAUTH - raise AuthorizationError - - d = self.portal.login(creds, None, IFTPShell) - d.addCallbacks(_cbLogin, _ebLogin) - return d - - - def ftp_PASV(self): - """Request for a passive connection - - from the rfc:: - - This command requests the server-DTP to \"listen\" on a data port - (which is not its default data port) and to wait for a connection - rather than initiate one upon receipt of a transfer command. The - response to this command includes the host and port address this - server is listening on. - """ - # if we have a DTP port set up, lose it. - if self.dtpFactory is not None: - # cleanupDTP sets dtpFactory to none. Later we'll do - # cleanup here or something. - self.cleanupDTP() - self.dtpFactory = DTPFactory(pi=self) - self.dtpFactory.setTimeout(self.dtpTimeout) - self.dtpPort = self.getDTPPort(self.dtpFactory) - - host = self.transport.getHost().host - port = self.dtpPort.getHost().port - self.reply(ENTERING_PASV_MODE, encodeHostPort(host, port)) - return self.dtpFactory.deferred.addCallback(lambda ign: None) - - - def ftp_PORT(self, address): - addr = map(int, address.split(',')) - ip = '%d.%d.%d.%d' % tuple(addr[:4]) - port = addr[4] << 8 | addr[5] - - # if we have a DTP port set up, lose it. - if self.dtpFactory is not None: - self.cleanupDTP() - - self.dtpFactory = DTPFactory(pi=self, peerHost=self.transport.getPeer().host) - self.dtpFactory.setTimeout(self.dtpTimeout) - self.dtpPort = reactor.connectTCP(ip, port, self.dtpFactory) - - def connected(ignored): - return ENTERING_PORT_MODE - def connFailed(err): - err.trap(PortConnectionError) - return CANT_OPEN_DATA_CNX - return self.dtpFactory.deferred.addCallbacks(connected, connFailed) - - - def ftp_LIST(self, path=''): - """ This command causes a list to be sent from the server to the - passive DTP. If the pathname specifies a directory or other - group of files, the server should transfer a list of files - in the specified directory. If the pathname specifies a - file then the server should send current information on the - file. A null argument implies the user's current working or - default directory. - """ - # Uh, for now, do this retarded thing. - if self.dtpInstance is None or not self.dtpInstance.isConnected: - return defer.fail(BadCmdSequenceError('must send PORT or PASV before RETR')) - - # bug in konqueror - if path == "-a": - path = '' - # bug in gFTP 2.0.15 - if path == "-aL": - path = '' - # bug in Nautilus 2.10.0 - if path == "-L": - path = '' - # bug in ange-ftp - if path == "-la": - path = '' - - def gotListing(results): - self.reply(DATA_CNX_ALREADY_OPEN_START_XFR) - for (name, attrs) in results: - self.dtpInstance.sendListResponse(name, attrs) - self.dtpInstance.transport.loseConnection() - return (TXFR_COMPLETE_OK,) - - try: - segments = toSegments(self.workingDirectory, path) - except InvalidPath: - return defer.fail(FileNotFoundError(path)) - - d = self.shell.list( - segments, - ('size', 'directory', 'permissions', 'hardlinks', - 'modified', 'owner', 'group')) - d.addCallback(gotListing) - return d - - - def ftp_NLST(self, path): - """ - This command causes a directory listing to be sent from the server to - the client. The pathname should specify a directory or other - system-specific file group descriptor. An empty path implies the current - working directory. If the path is non-existent, send nothing. If the - path is to a file, send only the file name. - - @type path: C{str} - @param path: The path for which a directory listing should be returned. - - @rtype: L{Deferred} - @return: a L{Deferred} which will be fired when the listing request - is finished. - """ - # XXX: why is this check different from ftp_RETR/ftp_STOR? See #4180 - if self.dtpInstance is None or not self.dtpInstance.isConnected: - return defer.fail( - BadCmdSequenceError('must send PORT or PASV before RETR')) - - try: - segments = toSegments(self.workingDirectory, path) - except InvalidPath: - return defer.fail(FileNotFoundError(path)) - - def cbList(results): - """ - Send, line by line, each file in the directory listing, and then - close the connection. - - @type results: A C{list} of C{tuple}. The first element of each - C{tuple} is a C{str} and the second element is a C{list}. - @param results: The names of the files in the directory. - - @rtype: C{tuple} - @return: A C{tuple} containing the status code for a successful - transfer. - """ - self.reply(DATA_CNX_ALREADY_OPEN_START_XFR) - for (name, ignored) in results: - self.dtpInstance.sendLine(name) - self.dtpInstance.transport.loseConnection() - return (TXFR_COMPLETE_OK,) - - def cbGlob(results): - self.reply(DATA_CNX_ALREADY_OPEN_START_XFR) - for (name, ignored) in results: - if fnmatch.fnmatch(name, segments[-1]): - self.dtpInstance.sendLine(name) - self.dtpInstance.transport.loseConnection() - return (TXFR_COMPLETE_OK,) - - def listErr(results): - """ - RFC 959 specifies that an NLST request may only return directory - listings. Thus, send nothing and just close the connection. - - @type results: L{Failure} - @param results: The L{Failure} wrapping a L{FileNotFoundError} that - occurred while trying to list the contents of a nonexistent - directory. - - @rtype: C{tuple} - @returns: A C{tuple} containing the status code for a successful - transfer. - """ - self.dtpInstance.transport.loseConnection() - return (TXFR_COMPLETE_OK,) - - # XXX This globbing may be incomplete: see #4181 - if segments and ( - '*' in segments[-1] or '?' in segments[-1] or - ('[' in segments[-1] and ']' in segments[-1])): - d = self.shell.list(segments[:-1]) - d.addCallback(cbGlob) - else: - d = self.shell.list(segments) - d.addCallback(cbList) - # self.shell.list will generate an error if the path is invalid - d.addErrback(listErr) - return d - - - def ftp_CWD(self, path): - try: - segments = toSegments(self.workingDirectory, path) - except InvalidPath: - # XXX Eh, what to fail with here? - return defer.fail(FileNotFoundError(path)) - - def accessGranted(result): - self.workingDirectory = segments - return (REQ_FILE_ACTN_COMPLETED_OK,) - - return self.shell.access(segments).addCallback(accessGranted) - - - def ftp_CDUP(self): - return self.ftp_CWD('..') - - - def ftp_PWD(self): - return (PWD_REPLY, '/' + '/'.join(self.workingDirectory)) - - - def ftp_RETR(self, path): - """ - This command causes the content of a file to be sent over the data - transfer channel. If the path is to a folder, an error will be raised. - - @type path: C{str} - @param path: The path to the file which should be transferred over the - data transfer channel. - - @rtype: L{Deferred} - @return: a L{Deferred} which will be fired when the transfer is done. - """ - if self.dtpInstance is None: - raise BadCmdSequenceError('PORT or PASV required before RETR') - - try: - newsegs = toSegments(self.workingDirectory, path) - except InvalidPath: - return defer.fail(FileNotFoundError(path)) - - # XXX For now, just disable the timeout. Later we'll want to - # leave it active and have the DTP connection reset it - # periodically. - self.setTimeout(None) - - # Put it back later - def enableTimeout(result): - self.setTimeout(self.factory.timeOut) - return result - - # And away she goes - if not self.binary: - cons = ASCIIConsumerWrapper(self.dtpInstance) - else: - cons = self.dtpInstance - - def cbSent(result): - return (TXFR_COMPLETE_OK,) - - def ebSent(err): - log.msg("Unexpected error attempting to transmit file to client:") - log.err(err) - if err.check(FTPCmdError): - return err - return (CNX_CLOSED_TXFR_ABORTED,) - - def cbOpened(file): - # Tell them what to doooo - if self.dtpInstance.isConnected: - self.reply(DATA_CNX_ALREADY_OPEN_START_XFR) - else: - self.reply(FILE_STATUS_OK_OPEN_DATA_CNX) - - d = file.send(cons) - d.addCallbacks(cbSent, ebSent) - return d - - def ebOpened(err): - if not err.check(PermissionDeniedError, FileNotFoundError, IsADirectoryError): - log.msg("Unexpected error attempting to open file for transmission:") - log.err(err) - if err.check(FTPCmdError): - return (err.value.errorCode, '/'.join(newsegs)) - return (FILE_NOT_FOUND, '/'.join(newsegs)) - - d = self.shell.openForReading(newsegs) - d.addCallbacks(cbOpened, ebOpened) - d.addBoth(enableTimeout) - - # Pass back Deferred that fires when the transfer is done - return d - - - def ftp_STOR(self, path): - if self.dtpInstance is None: - raise BadCmdSequenceError('PORT or PASV required before STOR') - - try: - newsegs = toSegments(self.workingDirectory, path) - except InvalidPath: - return defer.fail(FileNotFoundError(path)) - - # XXX For now, just disable the timeout. Later we'll want to - # leave it active and have the DTP connection reset it - # periodically. - self.setTimeout(None) - - # Put it back later - def enableTimeout(result): - self.setTimeout(self.factory.timeOut) - return result - - def cbSent(result): - return (TXFR_COMPLETE_OK,) - - def ebSent(err): - log.msg("Unexpected error receiving file from client:") - log.err(err) - if err.check(FTPCmdError): - return err - return (CNX_CLOSED_TXFR_ABORTED,) - - def cbConsumer(cons): - if not self.binary: - cons = ASCIIConsumerWrapper(cons) - - d = self.dtpInstance.registerConsumer(cons) - - # Tell them what to doooo - if self.dtpInstance.isConnected: - self.reply(DATA_CNX_ALREADY_OPEN_START_XFR) - else: - self.reply(FILE_STATUS_OK_OPEN_DATA_CNX) - - return d - - def cbOpened(file): - d = file.receive() - d.addCallback(cbConsumer) - d.addCallback(lambda ignored: file.close()) - d.addCallbacks(cbSent, ebSent) - return d - - def ebOpened(err): - if not err.check(PermissionDeniedError, FileNotFoundError, IsNotADirectoryError): - log.msg("Unexpected error attempting to open file for upload:") - log.err(err) - if isinstance(err.value, FTPCmdError): - return (err.value.errorCode, '/'.join(newsegs)) - return (FILE_NOT_FOUND, '/'.join(newsegs)) - - d = self.shell.openForWriting(newsegs) - d.addCallbacks(cbOpened, ebOpened) - d.addBoth(enableTimeout) - - # Pass back Deferred that fires when the transfer is done - return d - - - def ftp_SIZE(self, path): - try: - newsegs = toSegments(self.workingDirectory, path) - except InvalidPath: - return defer.fail(FileNotFoundError(path)) - - def cbStat((size,)): - return (FILE_STATUS, str(size)) - - return self.shell.stat(newsegs, ('size',)).addCallback(cbStat) - - - def ftp_MDTM(self, path): - try: - newsegs = toSegments(self.workingDirectory, path) - except InvalidPath: - return defer.fail(FileNotFoundError(path)) - - def cbStat((modified,)): - return (FILE_STATUS, time.strftime('%Y%m%d%H%M%S', time.gmtime(modified))) - - return self.shell.stat(newsegs, ('modified',)).addCallback(cbStat) - - - def ftp_TYPE(self, type): - p = type.upper() - if p: - f = getattr(self, 'type_' + p[0], None) - if f is not None: - return f(p[1:]) - return self.type_UNKNOWN(p) - return (SYNTAX_ERR,) - - def type_A(self, code): - if code == '' or code == 'N': - self.binary = False - return (TYPE_SET_OK, 'A' + code) - else: - return defer.fail(CmdArgSyntaxError(code)) - - def type_I(self, code): - if code == '': - self.binary = True - return (TYPE_SET_OK, 'I') - else: - return defer.fail(CmdArgSyntaxError(code)) - - def type_UNKNOWN(self, code): - return defer.fail(CmdNotImplementedForArgError(code)) - - - - def ftp_SYST(self): - return NAME_SYS_TYPE - - - def ftp_STRU(self, structure): - p = structure.upper() - if p == 'F': - return (CMD_OK,) - return defer.fail(CmdNotImplementedForArgError(structure)) - - - def ftp_MODE(self, mode): - p = mode.upper() - if p == 'S': - return (CMD_OK,) - return defer.fail(CmdNotImplementedForArgError(mode)) - - - def ftp_MKD(self, path): - try: - newsegs = toSegments(self.workingDirectory, path) - except InvalidPath: - return defer.fail(FileNotFoundError(path)) - return self.shell.makeDirectory(newsegs).addCallback(lambda ign: (MKD_REPLY, path)) - - - def ftp_RMD(self, path): - try: - newsegs = toSegments(self.workingDirectory, path) - except InvalidPath: - return defer.fail(FileNotFoundError(path)) - return self.shell.removeDirectory(newsegs).addCallback(lambda ign: (REQ_FILE_ACTN_COMPLETED_OK,)) - - - def ftp_DELE(self, path): - try: - newsegs = toSegments(self.workingDirectory, path) - except InvalidPath: - return defer.fail(FileNotFoundError(path)) - return self.shell.removeFile(newsegs).addCallback(lambda ign: (REQ_FILE_ACTN_COMPLETED_OK,)) - - - def ftp_NOOP(self): - return (CMD_OK,) - - - def ftp_RNFR(self, fromName): - self._fromName = fromName - self.state = self.RENAMING - return (REQ_FILE_ACTN_PENDING_FURTHER_INFO,) - - - def ftp_RNTO(self, toName): - fromName = self._fromName - del self._fromName - self.state = self.AUTHED - - try: - fromsegs = toSegments(self.workingDirectory, fromName) - tosegs = toSegments(self.workingDirectory, toName) - except InvalidPath: - return defer.fail(FileNotFoundError(fromName)) - return self.shell.rename(fromsegs, tosegs).addCallback(lambda ign: (REQ_FILE_ACTN_COMPLETED_OK,)) - - - def ftp_QUIT(self): - self.reply(GOODBYE_MSG) - self.transport.loseConnection() - self.disconnected = True - - - def cleanupDTP(self): - """call when DTP connection exits - """ - log.msg('cleanupDTP', debug=True) - - log.msg(self.dtpPort) - dtpPort, self.dtpPort = self.dtpPort, None - if interfaces.IListeningPort.providedBy(dtpPort): - dtpPort.stopListening() - elif interfaces.IConnector.providedBy(dtpPort): - dtpPort.disconnect() - else: - assert False, "dtpPort should be an IListeningPort or IConnector, instead is %r" % (dtpPort,) - - self.dtpFactory.stopFactory() - self.dtpFactory = None - - if self.dtpInstance is not None: - self.dtpInstance = None - - -class FTPFactory(policies.LimitTotalConnectionsFactory): - """ - A factory for producing ftp protocol instances - - @ivar timeOut: the protocol interpreter's idle timeout time in seconds, - default is 600 seconds. - - @ivar passivePortRange: value forwarded to C{protocol.passivePortRange}. - @type passivePortRange: C{iterator} - """ - protocol = FTP - overflowProtocol = FTPOverflowProtocol - allowAnonymous = True - userAnonymous = 'anonymous' - timeOut = 600 - - welcomeMessage = "Twisted %s FTP Server" % (copyright.version,) - - passivePortRange = xrange(0, 1) - - def __init__(self, portal=None, userAnonymous='anonymous'): - self.portal = portal - self.userAnonymous = userAnonymous - self.instances = [] - - def buildProtocol(self, addr): - p = policies.LimitTotalConnectionsFactory.buildProtocol(self, addr) - if p is not None: - p.wrappedProtocol.portal = self.portal - p.wrappedProtocol.timeOut = self.timeOut - p.wrappedProtocol.passivePortRange = self.passivePortRange - return p - - def stopFactory(self): - # make sure ftp instance's timeouts are set to None - # to avoid reactor complaints - [p.setTimeout(None) for p in self.instances if p.timeOut is not None] - policies.LimitTotalConnectionsFactory.stopFactory(self) - -# -- Cred Objects -- - - -class IFTPShell(Interface): - """ - An abstraction of the shell commands used by the FTP protocol for - a given user account. - - All path names must be absolute. - """ - - def makeDirectory(path): - """ - Create a directory. - - @param path: The path, as a list of segments, to create - @type path: C{list} of C{unicode} - - @return: A Deferred which fires when the directory has been - created, or which fails if the directory cannot be created. - """ - - - def removeDirectory(path): - """ - Remove a directory. - - @param path: The path, as a list of segments, to remove - @type path: C{list} of C{unicode} - - @return: A Deferred which fires when the directory has been - removed, or which fails if the directory cannot be removed. - """ - - - def removeFile(path): - """ - Remove a file. - - @param path: The path, as a list of segments, to remove - @type path: C{list} of C{unicode} - - @return: A Deferred which fires when the file has been - removed, or which fails if the file cannot be removed. - """ - - - def rename(fromPath, toPath): - """ - Rename a file or directory. - - @param fromPath: The current name of the path. - @type fromPath: C{list} of C{unicode} - - @param toPath: The desired new name of the path. - @type toPath: C{list} of C{unicode} - - @return: A Deferred which fires when the path has been - renamed, or which fails if the path cannot be renamed. - """ - - - def access(path): - """ - Determine whether access to the given path is allowed. - - @param path: The path, as a list of segments - - @return: A Deferred which fires with None if access is allowed - or which fails with a specific exception type if access is - denied. - """ - - - def stat(path, keys=()): - """ - Retrieve information about the given path. - - This is like list, except it will never return results about - child paths. - """ - - - def list(path, keys=()): - """ - Retrieve information about the given path. - - If the path represents a non-directory, the result list should - have only one entry with information about that non-directory. - Otherwise, the result list should have an element for each - child of the directory. - - @param path: The path, as a list of segments, to list - @type path: C{list} of C{unicode} - - @param keys: A tuple of keys desired in the resulting - dictionaries. - - @return: A Deferred which fires with a list of (name, list), - where the name is the name of the entry as a unicode string - and each list contains values corresponding to the requested - keys. The following are possible elements of keys, and the - values which should be returned for them: - - - C{'size'}: size in bytes, as an integer (this is kinda required) - - - C{'directory'}: boolean indicating the type of this entry - - - C{'permissions'}: a bitvector (see os.stat(foo).st_mode) - - - C{'hardlinks'}: Number of hard links to this entry - - - C{'modified'}: number of seconds since the epoch since entry was - modified - - - C{'owner'}: string indicating the user owner of this entry - - - C{'group'}: string indicating the group owner of this entry - """ - - - def openForReading(path): - """ - @param path: The path, as a list of segments, to open - @type path: C{list} of C{unicode} - - @rtype: C{Deferred} which will fire with L{IReadFile} - """ - - - def openForWriting(path): - """ - @param path: The path, as a list of segments, to open - @type path: C{list} of C{unicode} - - @rtype: C{Deferred} which will fire with L{IWriteFile} - """ - - - -class IReadFile(Interface): - """ - A file out of which bytes may be read. - """ - - def send(consumer): - """ - Produce the contents of the given path to the given consumer. This - method may only be invoked once on each provider. - - @type consumer: C{IConsumer} - - @return: A Deferred which fires when the file has been - consumed completely. - """ - - - -class IWriteFile(Interface): - """ - A file into which bytes may be written. - """ - - def receive(): - """ - Create a consumer which will write to this file. This method may - only be invoked once on each provider. - - @rtype: C{Deferred} of C{IConsumer} - """ - - def close(): - """ - Perform any post-write work that needs to be done. This method may - only be invoked once on each provider, and will always be invoked - after receive(). - - @rtype: C{Deferred} of anything: the value is ignored. The FTP client - will not see their upload request complete until this Deferred has - been fired. - """ - -def _getgroups(uid): - """Return the primary and supplementary groups for the given UID. - - @type uid: C{int} - """ - result = [] - pwent = pwd.getpwuid(uid) - - result.append(pwent.pw_gid) - - for grent in grp.getgrall(): - if pwent.pw_name in grent.gr_mem: - result.append(grent.gr_gid) - - return result - - -def _testPermissions(uid, gid, spath, mode='r'): - """ - checks to see if uid has proper permissions to access path with mode - - @type uid: C{int} - @param uid: numeric user id - - @type gid: C{int} - @param gid: numeric group id - - @type spath: C{str} - @param spath: the path on the server to test - - @type mode: C{str} - @param mode: 'r' or 'w' (read or write) - - @rtype: C{bool} - @return: True if the given credentials have the specified form of - access to the given path - """ - if mode == 'r': - usr = stat.S_IRUSR - grp = stat.S_IRGRP - oth = stat.S_IROTH - amode = os.R_OK - elif mode == 'w': - usr = stat.S_IWUSR - grp = stat.S_IWGRP - oth = stat.S_IWOTH - amode = os.W_OK - else: - raise ValueError("Invalid mode %r: must specify 'r' or 'w'" % (mode,)) - - access = False - if os.path.exists(spath): - if uid == 0: - access = True - else: - s = os.stat(spath) - if usr & s.st_mode and uid == s.st_uid: - access = True - elif grp & s.st_mode and gid in _getgroups(uid): - access = True - elif oth & s.st_mode: - access = True - - if access: - if not os.access(spath, amode): - access = False - log.msg("Filesystem grants permission to UID %d but it is inaccessible to me running as UID %d" % ( - uid, os.getuid())) - return access - - - -class FTPAnonymousShell(object): - """ - An anonymous implementation of IFTPShell - - @type filesystemRoot: L{twisted.python.filepath.FilePath} - @ivar filesystemRoot: The path which is considered the root of - this shell. - """ - implements(IFTPShell) - - def __init__(self, filesystemRoot): - self.filesystemRoot = filesystemRoot - - - def _path(self, path): - return reduce(filepath.FilePath.child, path, self.filesystemRoot) - - - def makeDirectory(self, path): - return defer.fail(AnonUserDeniedError()) - - - def removeDirectory(self, path): - return defer.fail(AnonUserDeniedError()) - - - def removeFile(self, path): - return defer.fail(AnonUserDeniedError()) - - - def rename(self, fromPath, toPath): - return defer.fail(AnonUserDeniedError()) - - - def receive(self, path): - path = self._path(path) - return defer.fail(AnonUserDeniedError()) - - - def openForReading(self, path): - """ - Open C{path} for reading. - - @param path: The path, as a list of segments, to open. - @type path: C{list} of C{unicode} - @return: A L{Deferred} is returned that will fire with an object - implementing L{IReadFile} if the file is successfully opened. If - C{path} is a directory, or if an exception is raised while trying - to open the file, the L{Deferred} will fire with an error. - """ - p = self._path(path) - if p.isdir(): - # Normally, we would only check for EISDIR in open, but win32 - # returns EACCES in this case, so we check before - return defer.fail(IsADirectoryError(path)) - try: - f = p.open('r') - except (IOError, OSError), e: - return errnoToFailure(e.errno, path) - except: - return defer.fail() - else: - return defer.succeed(_FileReader(f)) - - - def openForWriting(self, path): - """ - Reject write attempts by anonymous users with - L{PermissionDeniedError}. - """ - return defer.fail(PermissionDeniedError("STOR not allowed")) - - - def access(self, path): - p = self._path(path) - if not p.exists(): - # Again, win32 doesn't report a sane error after, so let's fail - # early if we can - return defer.fail(FileNotFoundError(path)) - # For now, just see if we can os.listdir() it - try: - p.listdir() - except (IOError, OSError), e: - return errnoToFailure(e.errno, path) - except: - return defer.fail() - else: - return defer.succeed(None) - - - def stat(self, path, keys=()): - p = self._path(path) - if p.isdir(): - try: - statResult = self._statNode(p, keys) - except (IOError, OSError), e: - return errnoToFailure(e.errno, path) - except: - return defer.fail() - else: - return defer.succeed(statResult) - else: - return self.list(path, keys).addCallback(lambda res: res[0][1]) - - - def list(self, path, keys=()): - """ - Return the list of files at given C{path}, adding C{keys} stat - informations if specified. - - @param path: the directory or file to check. - @type path: C{str} - - @param keys: the list of desired metadata - @type keys: C{list} of C{str} - """ - filePath = self._path(path) - if filePath.isdir(): - entries = filePath.listdir() - fileEntries = [filePath.child(p) for p in entries] - elif filePath.isfile(): - entries = [os.path.join(*filePath.segmentsFrom(self.filesystemRoot))] - fileEntries = [filePath] - else: - return defer.fail(FileNotFoundError(path)) - - results = [] - for fileName, filePath in zip(entries, fileEntries): - ent = [] - results.append((fileName, ent)) - if keys: - try: - ent.extend(self._statNode(filePath, keys)) - except (IOError, OSError), e: - return errnoToFailure(e.errno, fileName) - except: - return defer.fail() - - return defer.succeed(results) - - - def _statNode(self, filePath, keys): - """ - Shortcut method to get stat info on a node. - - @param filePath: the node to stat. - @type filePath: C{filepath.FilePath} - - @param keys: the stat keys to get. - @type keys: C{iterable} - """ - filePath.restat() - return [getattr(self, '_stat_' + k)(filePath.statinfo) for k in keys] - - _stat_size = operator.attrgetter('st_size') - _stat_permissions = operator.attrgetter('st_mode') - _stat_hardlinks = operator.attrgetter('st_nlink') - _stat_modified = operator.attrgetter('st_mtime') - - - def _stat_owner(self, st): - if pwd is not None: - try: - return pwd.getpwuid(st.st_uid)[0] - except KeyError: - pass - return str(st.st_uid) - - - def _stat_group(self, st): - if grp is not None: - try: - return grp.getgrgid(st.st_gid)[0] - except KeyError: - pass - return str(st.st_gid) - - - def _stat_directory(self, st): - return bool(st.st_mode & stat.S_IFDIR) - - - -class _FileReader(object): - implements(IReadFile) - - def __init__(self, fObj): - self.fObj = fObj - self._send = False - - def _close(self, passthrough): - self._send = True - self.fObj.close() - return passthrough - - def send(self, consumer): - assert not self._send, "Can only call IReadFile.send *once* per instance" - self._send = True - d = basic.FileSender().beginFileTransfer(self.fObj, consumer) - d.addBoth(self._close) - return d - - - -class FTPShell(FTPAnonymousShell): - """ - An authenticated implementation of L{IFTPShell}. - """ - - def makeDirectory(self, path): - p = self._path(path) - try: - p.makedirs() - except (IOError, OSError), e: - return errnoToFailure(e.errno, path) - except: - return defer.fail() - else: - return defer.succeed(None) - - - def removeDirectory(self, path): - p = self._path(path) - if p.isfile(): - # Win32 returns the wrong errno when rmdir is called on a file - # instead of a directory, so as we have the info here, let's fail - # early with a pertinent error - return defer.fail(IsNotADirectoryError(path)) - try: - os.rmdir(p.path) - except (IOError, OSError), e: - return errnoToFailure(e.errno, path) - except: - return defer.fail() - else: - return defer.succeed(None) - - - def removeFile(self, path): - p = self._path(path) - if p.isdir(): - # Win32 returns the wrong errno when remove is called on a - # directory instead of a file, so as we have the info here, - # let's fail early with a pertinent error - return defer.fail(IsADirectoryError(path)) - try: - p.remove() - except (IOError, OSError), e: - return errnoToFailure(e.errno, path) - except: - return defer.fail() - else: - return defer.succeed(None) - - - def rename(self, fromPath, toPath): - fp = self._path(fromPath) - tp = self._path(toPath) - try: - os.rename(fp.path, tp.path) - except (IOError, OSError), e: - return errnoToFailure(e.errno, fromPath) - except: - return defer.fail() - else: - return defer.succeed(None) - - - def openForWriting(self, path): - """ - Open C{path} for writing. - - @param path: The path, as a list of segments, to open. - @type path: C{list} of C{unicode} - @return: A L{Deferred} is returned that will fire with an object - implementing L{IWriteFile} if the file is successfully opened. If - C{path} is a directory, or if an exception is raised while trying - to open the file, the L{Deferred} will fire with an error. - """ - p = self._path(path) - if p.isdir(): - # Normally, we would only check for EISDIR in open, but win32 - # returns EACCES in this case, so we check before - return defer.fail(IsADirectoryError(path)) - try: - fObj = p.open('w') - except (IOError, OSError), e: - return errnoToFailure(e.errno, path) - except: - return defer.fail() - return defer.succeed(_FileWriter(fObj)) - - - -class _FileWriter(object): - implements(IWriteFile) - - def __init__(self, fObj): - self.fObj = fObj - self._receive = False - - def receive(self): - assert not self._receive, "Can only call IWriteFile.receive *once* per instance" - self._receive = True - # FileConsumer will close the file object - return defer.succeed(FileConsumer(self.fObj)) - - def close(self): - return defer.succeed(None) - - - -class BaseFTPRealm: - """ - Base class for simple FTP realms which provides an easy hook for specifying - the home directory for each user. - """ - implements(portal.IRealm) - - def __init__(self, anonymousRoot): - self.anonymousRoot = filepath.FilePath(anonymousRoot) - - - def getHomeDirectory(self, avatarId): - """ - Return a L{FilePath} representing the home directory of the given - avatar. Override this in a subclass. - - @param avatarId: A user identifier returned from a credentials checker. - @type avatarId: C{str} - - @rtype: L{FilePath} - """ - raise NotImplementedError( - "%r did not override getHomeDirectory" % (self.__class__,)) - - - def requestAvatar(self, avatarId, mind, *interfaces): - for iface in interfaces: - if iface is IFTPShell: - if avatarId is checkers.ANONYMOUS: - avatar = FTPAnonymousShell(self.anonymousRoot) - else: - avatar = FTPShell(self.getHomeDirectory(avatarId)) - return (IFTPShell, avatar, - getattr(avatar, 'logout', lambda: None)) - raise NotImplementedError( - "Only IFTPShell interface is supported by this realm") - - - -class FTPRealm(BaseFTPRealm): - """ - @type anonymousRoot: L{twisted.python.filepath.FilePath} - @ivar anonymousRoot: Root of the filesystem to which anonymous - users will be granted access. - - @type userHome: L{filepath.FilePath} - @ivar userHome: Root of the filesystem containing user home directories. - """ - def __init__(self, anonymousRoot, userHome='/home'): - BaseFTPRealm.__init__(self, anonymousRoot) - self.userHome = filepath.FilePath(userHome) - - - def getHomeDirectory(self, avatarId): - """ - Use C{avatarId} as a single path segment to construct a child of - C{self.userHome} and return that child. - """ - return self.userHome.child(avatarId) - - - -class SystemFTPRealm(BaseFTPRealm): - """ - L{SystemFTPRealm} uses system user account information to decide what the - home directory for a particular avatarId is. - - This works on POSIX but probably is not reliable on Windows. - """ - def getHomeDirectory(self, avatarId): - """ - Return the system-defined home directory of the system user account with - the name C{avatarId}. - """ - path = os.path.expanduser('~' + avatarId) - if path.startswith('~'): - raise cred_error.UnauthorizedLogin() - return filepath.FilePath(path) - - - -# --- FTP CLIENT ------------------------------------------------------------- - -#### -# And now for the client... - -# Notes: -# * Reference: http://cr.yp.to/ftp.html -# * FIXME: Does not support pipelining (which is not supported by all -# servers anyway). This isn't a functionality limitation, just a -# small performance issue. -# * Only has a rudimentary understanding of FTP response codes (although -# the full response is passed to the caller if they so choose). -# * Assumes that USER and PASS should always be sent -# * Always sets TYPE I (binary mode) -# * Doesn't understand any of the weird, obscure TELNET stuff (\377...) -# * FIXME: Doesn't share any code with the FTPServer - -class ConnectionLost(FTPError): - pass - -class CommandFailed(FTPError): - pass - -class BadResponse(FTPError): - pass - -class UnexpectedResponse(FTPError): - pass - -class UnexpectedData(FTPError): - pass - -class FTPCommand: - def __init__(self, text=None, public=0): - self.text = text - self.deferred = defer.Deferred() - self.ready = 1 - self.public = public - self.transferDeferred = None - - def fail(self, failure): - if self.public: - self.deferred.errback(failure) - - -class ProtocolWrapper(protocol.Protocol): - def __init__(self, original, deferred): - self.original = original - self.deferred = deferred - def makeConnection(self, transport): - self.original.makeConnection(transport) - def dataReceived(self, data): - self.original.dataReceived(data) - def connectionLost(self, reason): - self.original.connectionLost(reason) - # Signal that transfer has completed - self.deferred.callback(None) - - - -class IFinishableConsumer(interfaces.IConsumer): - """ - A Consumer for producers that finish. - - @since: 11.0 - """ - - def finish(): - """ - The producer has finished producing. - """ - - - -class SenderProtocol(protocol.Protocol): - implements(IFinishableConsumer) - - def __init__(self): - # Fired upon connection - self.connectedDeferred = defer.Deferred() - - # Fired upon disconnection - self.deferred = defer.Deferred() - - #Protocol stuff - def dataReceived(self, data): - raise UnexpectedData( - "Received data from the server on a " - "send-only data-connection" - ) - - def makeConnection(self, transport): - protocol.Protocol.makeConnection(self, transport) - self.connectedDeferred.callback(self) - - def connectionLost(self, reason): - if reason.check(error.ConnectionDone): - self.deferred.callback('connection done') - else: - self.deferred.errback(reason) - - #IFinishableConsumer stuff - def write(self, data): - self.transport.write(data) - - def registerProducer(self, producer, streaming): - """ - Register the given producer with our transport. - """ - self.transport.registerProducer(producer, streaming) - - def unregisterProducer(self): - """ - Unregister the previously registered producer. - """ - self.transport.unregisterProducer() - - def finish(self): - self.transport.loseConnection() - - -def decodeHostPort(line): - """Decode an FTP response specifying a host and port. - - @return: a 2-tuple of (host, port). - """ - abcdef = re.sub('[^0-9, ]', '', line) - parsed = [int(p.strip()) for p in abcdef.split(',')] - for x in parsed: - if x < 0 or x > 255: - raise ValueError("Out of range", line, x) - a, b, c, d, e, f = parsed - host = "%s.%s.%s.%s" % (a, b, c, d) - port = (int(e) << 8) + int(f) - return host, port - -def encodeHostPort(host, port): - numbers = host.split('.') + [str(port >> 8), str(port % 256)] - return ','.join(numbers) - -def _unwrapFirstError(failure): - failure.trap(defer.FirstError) - return failure.value.subFailure - -class FTPDataPortFactory(protocol.ServerFactory): - """Factory for data connections that use the PORT command - - (i.e. "active" transfers) - """ - noisy = 0 - def buildProtocol(self, addr): - # This is a bit hackish -- we already have a Protocol instance, - # so just return it instead of making a new one - # FIXME: Reject connections from the wrong address/port - # (potential security problem) - self.protocol.factory = self - self.port.loseConnection() - return self.protocol - - -class FTPClientBasic(basic.LineReceiver): - """ - Foundations of an FTP client. - """ - debug = False - - def __init__(self): - self.actionQueue = [] - self.greeting = None - self.nextDeferred = defer.Deferred().addCallback(self._cb_greeting) - self.nextDeferred.addErrback(self.fail) - self.response = [] - self._failed = 0 - - def fail(self, error): - """ - Give an error to any queued deferreds. - """ - self._fail(error) - - def _fail(self, error): - """ - Errback all queued deferreds. - """ - if self._failed: - # We're recursing; bail out here for simplicity - return error - self._failed = 1 - if self.nextDeferred: - try: - self.nextDeferred.errback(failure.Failure(ConnectionLost('FTP connection lost', error))) - except defer.AlreadyCalledError: - pass - for ftpCommand in self.actionQueue: - ftpCommand.fail(failure.Failure(ConnectionLost('FTP connection lost', error))) - return error - - def _cb_greeting(self, greeting): - self.greeting = greeting - - def sendLine(self, line): - """ - (Private) Sends a line, unless line is None. - """ - if line is None: - return - basic.LineReceiver.sendLine(self, line) - - def sendNextCommand(self): - """ - (Private) Processes the next command in the queue. - """ - ftpCommand = self.popCommandQueue() - if ftpCommand is None: - self.nextDeferred = None - return - if not ftpCommand.ready: - self.actionQueue.insert(0, ftpCommand) - reactor.callLater(1.0, self.sendNextCommand) - self.nextDeferred = None - return - - # FIXME: this if block doesn't belong in FTPClientBasic, it belongs in - # FTPClient. - if ftpCommand.text == 'PORT': - self.generatePortCommand(ftpCommand) - - if self.debug: - log.msg('<-- %s' % ftpCommand.text) - self.nextDeferred = ftpCommand.deferred - self.sendLine(ftpCommand.text) - - def queueCommand(self, ftpCommand): - """ - Add an FTPCommand object to the queue. - - If it's the only thing in the queue, and we are connected and we aren't - waiting for a response of an earlier command, the command will be sent - immediately. - - @param ftpCommand: an L{FTPCommand} - """ - self.actionQueue.append(ftpCommand) - if (len(self.actionQueue) == 1 and self.transport is not None and - self.nextDeferred is None): - self.sendNextCommand() - - def queueStringCommand(self, command, public=1): - """ - Queues a string to be issued as an FTP command - - @param command: string of an FTP command to queue - @param public: a flag intended for internal use by FTPClient. Don't - change it unless you know what you're doing. - - @return: a L{Deferred} that will be called when the response to the - command has been received. - """ - ftpCommand = FTPCommand(command, public) - self.queueCommand(ftpCommand) - return ftpCommand.deferred - - def popCommandQueue(self): - """ - Return the front element of the command queue, or None if empty. - """ - if self.actionQueue: - return self.actionQueue.pop(0) - else: - return None - - def queueLogin(self, username, password): - """ - Login: send the username, send the password. - - If the password is C{None}, the PASS command won't be sent. Also, if - the response to the USER command has a response code of 230 (User logged - in), then PASS won't be sent either. - """ - # Prepare the USER command - deferreds = [] - userDeferred = self.queueStringCommand('USER ' + username, public=0) - deferreds.append(userDeferred) - - # Prepare the PASS command (if a password is given) - if password is not None: - passwordCmd = FTPCommand('PASS ' + password, public=0) - self.queueCommand(passwordCmd) - deferreds.append(passwordCmd.deferred) - - # Avoid sending PASS if the response to USER is 230. - # (ref: http://cr.yp.to/ftp/user.html#user) - def cancelPasswordIfNotNeeded(response): - if response[0].startswith('230'): - # No password needed! - self.actionQueue.remove(passwordCmd) - return response - userDeferred.addCallback(cancelPasswordIfNotNeeded) - - # Error handling. - for deferred in deferreds: - # If something goes wrong, call fail - deferred.addErrback(self.fail) - # But also swallow the error, so we don't cause spurious errors - deferred.addErrback(lambda x: None) - - def lineReceived(self, line): - """ - (Private) Parses the response messages from the FTP server. - """ - # Add this line to the current response - if self.debug: - log.msg('--> %s' % line) - self.response.append(line) - - # Bail out if this isn't the last line of a response - # The last line of response starts with 3 digits followed by a space - codeIsValid = re.match(r'\d{3} ', line) - if not codeIsValid: - return - - code = line[0:3] - - # Ignore marks - if code[0] == '1': - return - - # Check that we were expecting a response - if self.nextDeferred is None: - self.fail(UnexpectedResponse(self.response)) - return - - # Reset the response - response = self.response - self.response = [] - - # Look for a success or error code, and call the appropriate callback - if code[0] in ('2', '3'): - # Success - self.nextDeferred.callback(response) - elif code[0] in ('4', '5'): - # Failure - self.nextDeferred.errback(failure.Failure(CommandFailed(response))) - else: - # This shouldn't happen unless something screwed up. - log.msg('Server sent invalid response code %s' % (code,)) - self.nextDeferred.errback(failure.Failure(BadResponse(response))) - - # Run the next command - self.sendNextCommand() - - def connectionLost(self, reason): - self._fail(reason) - - - -class _PassiveConnectionFactory(protocol.ClientFactory): - noisy = False - - def __init__(self, protoInstance): - self.protoInstance = protoInstance - - def buildProtocol(self, ignored): - self.protoInstance.factory = self - return self.protoInstance - - def clientConnectionFailed(self, connector, reason): - e = FTPError('Connection Failed', reason) - self.protoInstance.deferred.errback(e) - - - -class FTPClient(FTPClientBasic): - """ - L{FTPClient} is a client implementation of the FTP protocol which - exposes FTP commands as methods which return L{Deferred}s. - - Each command method returns a L{Deferred} which is called back when a - successful response code (2xx or 3xx) is received from the server or - which is error backed if an error response code (4xx or 5xx) is received - from the server or if a protocol violation occurs. If an error response - code is received, the L{Deferred} fires with a L{Failure} wrapping a - L{CommandFailed} instance. The L{CommandFailed} instance is created - with a list of the response lines received from the server. - - See U{RFC 959<http://www.ietf.org/rfc/rfc959.txt>} for error code - definitions. - - Both active and passive transfers are supported. - - @ivar passive: See description in __init__. - """ - connectFactory = reactor.connectTCP - - def __init__(self, username='anonymous', - password='twisted@twistedmatrix.com', - passive=1): - """ - Constructor. - - I will login as soon as I receive the welcome message from the server. - - @param username: FTP username - @param password: FTP password - @param passive: flag that controls if I use active or passive data - connections. You can also change this after construction by - assigning to C{self.passive}. - """ - FTPClientBasic.__init__(self) - self.queueLogin(username, password) - - self.passive = passive - - def fail(self, error): - """ - Disconnect, and also give an error to any queued deferreds. - """ - self.transport.loseConnection() - self._fail(error) - - def receiveFromConnection(self, commands, protocol): - """ - Retrieves a file or listing generated by the given command, - feeding it to the given protocol. - - @param commands: list of strings of FTP commands to execute then receive - the results of (e.g. C{LIST}, C{RETR}) - @param protocol: A L{Protocol} B{instance} e.g. an - L{FTPFileListProtocol}, or something that can be adapted to one. - Typically this will be an L{IConsumer} implementation. - - @return: L{Deferred}. - """ - protocol = interfaces.IProtocol(protocol) - wrapper = ProtocolWrapper(protocol, defer.Deferred()) - return self._openDataConnection(commands, wrapper) - - def queueLogin(self, username, password): - """ - Login: send the username, send the password, and - set retrieval mode to binary - """ - FTPClientBasic.queueLogin(self, username, password) - d = self.queueStringCommand('TYPE I', public=0) - # If something goes wrong, call fail - d.addErrback(self.fail) - # But also swallow the error, so we don't cause spurious errors - d.addErrback(lambda x: None) - - def sendToConnection(self, commands): - """ - XXX - - @return: A tuple of two L{Deferred}s: - - L{Deferred} L{IFinishableConsumer}. You must call - the C{finish} method on the IFinishableConsumer when the file - is completely transferred. - - L{Deferred} list of control-connection responses. - """ - s = SenderProtocol() - r = self._openDataConnection(commands, s) - return (s.connectedDeferred, r) - - def _openDataConnection(self, commands, protocol): - """ - This method returns a DeferredList. - """ - cmds = [FTPCommand(command, public=1) for command in commands] - cmdsDeferred = defer.DeferredList([cmd.deferred for cmd in cmds], - fireOnOneErrback=True, consumeErrors=True) - cmdsDeferred.addErrback(_unwrapFirstError) - - if self.passive: - # Hack: use a mutable object to sneak a variable out of the - # scope of doPassive - _mutable = [None] - def doPassive(response): - """Connect to the port specified in the response to PASV""" - host, port = decodeHostPort(response[-1][4:]) - - f = _PassiveConnectionFactory(protocol) - _mutable[0] = self.connectFactory(host, port, f) - - pasvCmd = FTPCommand('PASV') - self.queueCommand(pasvCmd) - pasvCmd.deferred.addCallback(doPassive).addErrback(self.fail) - - results = [cmdsDeferred, pasvCmd.deferred, protocol.deferred] - d = defer.DeferredList(results, fireOnOneErrback=True, consumeErrors=True) - d.addErrback(_unwrapFirstError) - - # Ensure the connection is always closed - def close(x, m=_mutable): - m[0] and m[0].disconnect() - return x - d.addBoth(close) - - else: - # We just place a marker command in the queue, and will fill in - # the host and port numbers later (see generatePortCommand) - portCmd = FTPCommand('PORT') - - # Ok, now we jump through a few hoops here. - # This is the problem: a transfer is not to be trusted as complete - # until we get both the "226 Transfer complete" message on the - # control connection, and the data socket is closed. Thus, we use - # a DeferredList to make sure we only fire the callback at the - # right time. - - portCmd.transferDeferred = protocol.deferred - portCmd.protocol = protocol - portCmd.deferred.addErrback(portCmd.transferDeferred.errback) - self.queueCommand(portCmd) - - # Create dummy functions for the next callback to call. - # These will also be replaced with real functions in - # generatePortCommand. - portCmd.loseConnection = lambda result: result - portCmd.fail = lambda error: error - - # Ensure that the connection always gets closed - cmdsDeferred.addErrback(lambda e, pc=portCmd: pc.fail(e) or e) - - results = [cmdsDeferred, portCmd.deferred, portCmd.transferDeferred] - d = defer.DeferredList(results, fireOnOneErrback=True, consumeErrors=True) - d.addErrback(_unwrapFirstError) - - for cmd in cmds: - self.queueCommand(cmd) - return d - - def generatePortCommand(self, portCmd): - """ - (Private) Generates the text of a given PORT command. - """ - - # The problem is that we don't create the listening port until we need - # it for various reasons, and so we have to muck about to figure out - # what interface and port it's listening on, and then finally we can - # create the text of the PORT command to send to the FTP server. - - # FIXME: This method is far too ugly. - - # FIXME: The best solution is probably to only create the data port - # once per FTPClient, and just recycle it for each new download. - # This should be ok, because we don't pipeline commands. - - # Start listening on a port - factory = FTPDataPortFactory() - factory.protocol = portCmd.protocol - listener = reactor.listenTCP(0, factory) - factory.port = listener - - # Ensure we close the listening port if something goes wrong - def listenerFail(error, listener=listener): - if listener.connected: - listener.loseConnection() - return error - portCmd.fail = listenerFail - - # Construct crufty FTP magic numbers that represent host & port - host = self.transport.getHost().host - port = listener.getHost().port - portCmd.text = 'PORT ' + encodeHostPort(host, port) - - def escapePath(self, path): - """ - Returns a FTP escaped path (replace newlines with nulls). - """ - # Escape newline characters - return path.replace('\n', '\0') - - def retrieveFile(self, path, protocol, offset=0): - """ - Retrieve a file from the given path - - This method issues the 'RETR' FTP command. - - The file is fed into the given Protocol instance. The data connection - will be passive if self.passive is set. - - @param path: path to file that you wish to receive. - @param protocol: a L{Protocol} instance. - @param offset: offset to start downloading from - - @return: L{Deferred} - """ - cmds = ['RETR ' + self.escapePath(path)] - if offset: - cmds.insert(0, ('REST ' + str(offset))) - return self.receiveFromConnection(cmds, protocol) - - retr = retrieveFile - - def storeFile(self, path, offset=0): - """ - Store a file at the given path. - - This method issues the 'STOR' FTP command. - - @return: A tuple of two L{Deferred}s: - - L{Deferred} L{IFinishableConsumer}. You must call - the C{finish} method on the IFinishableConsumer when the file - is completely transferred. - - L{Deferred} list of control-connection responses. - """ - cmds = ['STOR ' + self.escapePath(path)] - if offset: - cmds.insert(0, ('REST ' + str(offset))) - return self.sendToConnection(cmds) - - stor = storeFile - - - def rename(self, pathFrom, pathTo): - """ - Rename a file. - - This method issues the I{RNFR}/I{RNTO} command sequence to rename - C{pathFrom} to C{pathTo}. - - @param: pathFrom: the absolute path to the file to be renamed - @type pathFrom: C{str} - - @param: pathTo: the absolute path to rename the file to. - @type pathTo: C{str} - - @return: A L{Deferred} which fires when the rename operation has - succeeded or failed. If it succeeds, the L{Deferred} is called - back with a two-tuple of lists. The first list contains the - responses to the I{RNFR} command. The second list contains the - responses to the I{RNTO} command. If either I{RNFR} or I{RNTO} - fails, the L{Deferred} is errbacked with L{CommandFailed} or - L{BadResponse}. - @rtype: L{Deferred} - - @since: 8.2 - """ - renameFrom = self.queueStringCommand('RNFR ' + self.escapePath(pathFrom)) - renameTo = self.queueStringCommand('RNTO ' + self.escapePath(pathTo)) - - fromResponse = [] - - # Use a separate Deferred for the ultimate result so that Deferred - # chaining can't interfere with its result. - result = defer.Deferred() - # Bundle up all the responses - result.addCallback(lambda toResponse: (fromResponse, toResponse)) - - def ebFrom(failure): - # Make sure the RNTO doesn't run if the RNFR failed. - self.popCommandQueue() - result.errback(failure) - - # Save the RNFR response to pass to the result Deferred later - renameFrom.addCallbacks(fromResponse.extend, ebFrom) - - # Hook up the RNTO to the result Deferred as well - renameTo.chainDeferred(result) - - return result - - - def list(self, path, protocol): - """ - Retrieve a file listing into the given protocol instance. - - This method issues the 'LIST' FTP command. - - @param path: path to get a file listing for. - @param protocol: a L{Protocol} instance, probably a - L{FTPFileListProtocol} instance. It can cope with most common file - listing formats. - - @return: L{Deferred} - """ - if path is None: - path = '' - return self.receiveFromConnection(['LIST ' + self.escapePath(path)], protocol) - - - def nlst(self, path, protocol): - """ - Retrieve a short file listing into the given protocol instance. - - This method issues the 'NLST' FTP command. - - NLST (should) return a list of filenames, one per line. - - @param path: path to get short file listing for. - @param protocol: a L{Protocol} instance. - """ - if path is None: - path = '' - return self.receiveFromConnection(['NLST ' + self.escapePath(path)], protocol) - - - def cwd(self, path): - """ - Issues the CWD (Change Working Directory) command. It's also - available as changeDirectory, which parses the result. - - @return: a L{Deferred} that will be called when done. - """ - return self.queueStringCommand('CWD ' + self.escapePath(path)) - - - def changeDirectory(self, path): - """ - Change the directory on the server and parse the result to determine - if it was successful or not. - - @type path: C{str} - @param path: The path to which to change. - - @return: a L{Deferred} which will be called back when the directory - change has succeeded or errbacked if an error occurrs. - """ - warnings.warn( - "FTPClient.changeDirectory is deprecated in Twisted 8.2 and " - "newer. Use FTPClient.cwd instead.", - category=DeprecationWarning, - stacklevel=2) - - def cbResult(result): - if result[-1][:3] != '250': - return failure.Failure(CommandFailed(result)) - return True - return self.cwd(path).addCallback(cbResult) - - - def makeDirectory(self, path): - """ - Make a directory - - This method issues the MKD command. - - @param path: The path to the directory to create. - @type path: C{str} - - @return: A L{Deferred} which fires when the server responds. If the - directory is created, the L{Deferred} is called back with the - server response. If the server response indicates the directory - was not created, the L{Deferred} is errbacked with a L{Failure} - wrapping L{CommandFailed} or L{BadResponse}. - @rtype: L{Deferred} - - @since: 8.2 - """ - return self.queueStringCommand('MKD ' + self.escapePath(path)) - - - def removeFile(self, path): - """ - Delete a file on the server. - - L{removeFile} issues a I{DELE} command to the server to remove the - indicated file. Note that this command cannot remove a directory. - - @param path: The path to the file to delete. May be relative to the - current dir. - @type path: C{str} - - @return: A L{Deferred} which fires when the server responds. On error, - it is errbacked with either L{CommandFailed} or L{BadResponse}. On - success, it is called back with a list of response lines. - @rtype: L{Deferred} - - @since: 8.2 - """ - return self.queueStringCommand('DELE ' + self.escapePath(path)) - - - def removeDirectory(self, path): - """ - Delete a directory on the server. - - L{removeDirectory} issues a I{RMD} command to the server to remove the - indicated directory. Described in RFC959. - - @param path: The path to the directory to delete. May be relative to - the current working directory. - @type path: C{str} - - @return: A L{Deferred} which fires when the server responds. On error, - it is errbacked with either L{CommandFailed} or L{BadResponse}. On - success, it is called back with a list of response lines. - @rtype: L{Deferred} - - @since: 11.1 - """ - return self.queueStringCommand('RMD ' + self.escapePath(path)) - - - def cdup(self): - """ - Issues the CDUP (Change Directory UP) command. - - @return: a L{Deferred} that will be called when done. - """ - return self.queueStringCommand('CDUP') - - - def pwd(self): - """ - Issues the PWD (Print Working Directory) command. - - The L{getDirectory} does the same job but automatically parses the - result. - - @return: a L{Deferred} that will be called when done. It is up to the - caller to interpret the response, but the L{parsePWDResponse} method - in this module should work. - """ - return self.queueStringCommand('PWD') - - - def getDirectory(self): - """ - Returns the current remote directory. - - @return: a L{Deferred} that will be called back with a C{str} giving - the remote directory or which will errback with L{CommandFailed} - if an error response is returned. - """ - def cbParse(result): - try: - # The only valid code is 257 - if int(result[0].split(' ', 1)[0]) != 257: - raise ValueError - except (IndexError, ValueError): - return failure.Failure(CommandFailed(result)) - path = parsePWDResponse(result[0]) - if path is None: - return failure.Failure(CommandFailed(result)) - return path - return self.pwd().addCallback(cbParse) - - - def quit(self): - """ - Issues the I{QUIT} command. - - @return: A L{Deferred} that fires when the server acknowledges the - I{QUIT} command. The transport should not be disconnected until - this L{Deferred} fires. - """ - return self.queueStringCommand('QUIT') - - - -class FTPFileListProtocol(basic.LineReceiver): - """Parser for standard FTP file listings - - This is the evil required to match:: - - -rw-r--r-- 1 root other 531 Jan 29 03:26 README - - If you need different evil for a wacky FTP server, you can - override either C{fileLinePattern} or C{parseDirectoryLine()}. - - It populates the instance attribute self.files, which is a list containing - dicts with the following keys (examples from the above line): - - filetype: e.g. 'd' for directories, or '-' for an ordinary file - - perms: e.g. 'rw-r--r--' - - nlinks: e.g. 1 - - owner: e.g. 'root' - - group: e.g. 'other' - - size: e.g. 531 - - date: e.g. 'Jan 29 03:26' - - filename: e.g. 'README' - - linktarget: e.g. 'some/file' - - Note that the 'date' value will be formatted differently depending on the - date. Check U{http://cr.yp.to/ftp.html} if you really want to try to parse - it. - - @ivar files: list of dicts describing the files in this listing - """ - fileLinePattern = re.compile( - r'^(?P<filetype>.)(?P<perms>.{9})\s+(?P<nlinks>\d*)\s*' - r'(?P<owner>\S+)\s+(?P<group>\S+)\s+(?P<size>\d+)\s+' - r'(?P<date>...\s+\d+\s+[\d:]+)\s+(?P<filename>([^ ]|\\ )*?)' - r'( -> (?P<linktarget>[^\r]*))?\r?$' - ) - delimiter = '\n' - - def __init__(self): - self.files = [] - - def lineReceived(self, line): - d = self.parseDirectoryLine(line) - if d is None: - self.unknownLine(line) - else: - self.addFile(d) - - def parseDirectoryLine(self, line): - """Return a dictionary of fields, or None if line cannot be parsed. - - @param line: line of text expected to contain a directory entry - @type line: str - - @return: dict - """ - match = self.fileLinePattern.match(line) - if match is None: - return None - else: - d = match.groupdict() - d['filename'] = d['filename'].replace(r'\ ', ' ') - d['nlinks'] = int(d['nlinks']) - d['size'] = int(d['size']) - if d['linktarget']: - d['linktarget'] = d['linktarget'].replace(r'\ ', ' ') - return d - - def addFile(self, info): - """Append file information dictionary to the list of known files. - - Subclasses can override or extend this method to handle file - information differently without affecting the parsing of data - from the server. - - @param info: dictionary containing the parsed representation - of the file information - @type info: dict - """ - self.files.append(info) - - def unknownLine(self, line): - """Deal with received lines which could not be parsed as file - information. - - Subclasses can override this to perform any special processing - needed. - - @param line: unparsable line as received - @type line: str - """ - pass - -def parsePWDResponse(response): - """Returns the path from a response to a PWD command. - - Responses typically look like:: - - 257 "/home/andrew" is current directory. - - For this example, I will return C{'/home/andrew'}. - - If I can't find the path, I return C{None}. - """ - match = re.search('"(.*)"', response) - if match: - return match.groups()[0] - else: - return None |