Index: third_party/twisted_8_1/twisted/protocols/ftp.py |
diff --git a/third_party/twisted_8_1/twisted/protocols/ftp.py b/third_party/twisted_8_1/twisted/protocols/ftp.py |
deleted file mode 100644 |
index 7bf6f8f599797eac23bec4b834c11c85512c401f..0000000000000000000000000000000000000000 |
--- a/third_party/twisted_8_1/twisted/protocols/ftp.py |
+++ /dev/null |
@@ -1,2625 +0,0 @@ |
-# -*- test-case-name: twisted.test.test_ftp -*- |
-# Copyright (c) 2001-2007 Twisted Matrix Laboratories. |
-# See LICENSE for details. |
- |
-""" |
-An FTP protocol implementation |
- |
-@author: U{Itamar Shtull-Trauring<mailto:itamarst@twistedmatrix.com>} |
-@author: U{Jp Calderone<mailto:exarkun@divmod.com>} |
-@author: U{Andrew Bennetts<mailto:spiv@twistedmatrix.com>} |
-""" |
- |
-# System Imports |
-import os |
-import time |
-import re |
-import operator |
-import stat |
-import errno |
-import fnmatch |
- |
-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.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" |
-GOODBYE_MSG = "221" |
-DATA_CNX_OPEN_NO_XFR_IN_PROGRESS = "225" |
-CLOSING_DATA_CNX = "226" |
-TXFR_COMPLETE_OK = "226" |
-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', |
- 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 -- |
- 'userotp': '331 Response to %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.', |
- |
- 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_LOCAL_ERR: '451 Requested action aborted. Local error in processing.', |
- |
- |
- # -- 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', |
- 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 |
- """ |
- def __init__(self): |
- # No message |
- FTPCmdError.__init__(self, None) |
- |
- 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): |
- """ |
- DTP protocol factory. |
- |
- @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 |
- """ |
- |
- # -- configuration variables -- |
- peerCheck = False |
- |
- # -- class variables -- |
- def __init__(self, pi, peerHost=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.deferred.addBoth(lambda ign: (delattr(self, 'deferred'), ign)[1]) |
- self.delayedCall = None |
- |
- def buildProtocol(self, addr): |
- log.msg('DTPFactory.buildProtocol', debug=True) |
- self.cancelTimeout() |
- if self.pi.dtpInstance: # only create one instance |
- return |
- 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.deferred: |
- d, self.deferred = self.deferred, None |
- |
- # TODO: LEFT OFF HERE! |
- |
- d.addErrback(debugDeferred, 'timeoutFactory firing errback') |
- d.errback(defer.TimeoutError()) |
- self.stopFactory() |
- |
- def cancelTimeout(self): |
- if not self.delayedCall.called and not self.delayedCall.cancelled: |
- log.msg('cancelling DTP timeout', debug=True) |
- self.delayedCall.cancel() |
- assert self.delayedCall.cancelled |
- log.msg('timeout has been cancelled', debug=True) |
- |
- def setTimeout(self, seconds): |
- log.msg('DTPFactory.setTimeout set to %s seconds' % seconds) |
- self.delayedCall = reactor.callLater(seconds, self.timeoutFactory) |
- |
- def clientConnectionFailed(self, connector, reason): |
- self.deferred.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, e: |
- 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): |
- # XXX: why is this check different to ftp_RETR/ftp_STOR? |
- 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, e: |
- return defer.fail(FileNotFoundError(path)) |
- |
- def cbList(results): |
- 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,) |
- |
- # XXX Maybe this globbing is incomplete, but who cares. |
- # Stupid people probably. |
- 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) |
- return d |
- |
- |
- def ftp_CWD(self, path): |
- try: |
- segments = toSegments(self.workingDirectory, path) |
- except InvalidPath, e: |
- # 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): |
- 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) |
- 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, IsNotADirectoryError): |
- 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) |
- return (CNX_CLOSED_TXFR_ABORTED,) |
- |
- def cbConsumer(cons): |
- if not self.binary: |
- cons = ASCIIConsumerWrapper(cons) |
- |
- d = self.dtpInstance.registerConsumer(cons) |
- d.addCallbacks(cbSent, ebSent) |
- |
- # 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) |
- 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 = 'anonymous' |
- 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.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 _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): |
- 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('rb') |
- 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): |
- 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('wb') |
- 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)) |
- |
- |
- |
-class FTPRealm: |
- """ |
- @type anonymousRoot: L{twisted.python.filepath.FilePath} |
- @ivar anonymousRoot: Root of the filesystem to which anonymous |
- users will be granted access. |
- """ |
- implements(portal.IRealm) |
- |
- def __init__(self, anonymousRoot): |
- self.anonymousRoot = filepath.FilePath(anonymousRoot) |
- |
- 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(filepath.FilePath("/home/" + avatarId)) |
- return IFTPShell, avatar, getattr(avatar, 'logout', lambda: None) |
- raise NotImplementedError("Only IFTPShell interface is supported by this realm") |
- |
-# --- 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 SenderProtocol(protocol.Protocol): |
- implements(interfaces.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): |
- """ |
- A Twisted FTP Client |
- |
- Supports active and passive transfers. |
- |
- @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 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 command: list of strings of FTP commands to execute then receive |
- the results of (e.g. LIST, RETR) |
- @param protocol: A L{Protocol} *instance* e.g. an |
- L{FTPFileListProtocol}, or something that can be adapted to one. |
- Typically this will be an L{IConsumer} implemenation. |
- |
- @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 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 and errbacked if an error occurrs. |
- """ |
- def cbParse(result): |
- try: |
- # The only valid code is 250 |
- if int(result[0].split(' ', 1)[0]) == 250: |
- return True |
- else: |
- raise ValueError |
- except (IndexError, ValueError), e: |
- return failure.Failure(CommandFailed(result)) |
- return self.cwd(path).addCallback(cbParse) |
- |
- 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), e: |
- 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 QUIT command. |
- """ |
- 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 |