1
# -*- test-case-name: twisted.test.test_ftp -*-
2
# Copyright (c) 2001-2010 Twisted Matrix Laboratories.
3
# See LICENSE for details.
6
An FTP protocol implementation
8
@author: Itamar Shtull-Trauring
10
@author: Andrew Bennetts
28
from zope.interface import Interface, implements
31
from twisted import copyright
32
from twisted.internet import reactor, interfaces, protocol, error, defer
33
from twisted.protocols import basic, policies
35
from twisted.python import log, failure, filepath
36
from twisted.python.compat import reduce
38
from twisted.cred import error as cred_error, portal, credentials, checkers
43
RESTART_MARKER_REPLY = "100"
44
SERVICE_READY_IN_N_MINUTES = "120"
45
DATA_CNX_ALREADY_OPEN_START_XFR = "125"
46
FILE_STATUS_OK_OPEN_DATA_CNX = "150"
50
ENTERING_PORT_MODE = "200.3"
51
CMD_NOT_IMPLMNTD_SUPERFLUOUS = "202"
52
SYS_STATUS_OR_HELP_REPLY = "211"
57
SVC_READY_FOR_NEW_USER = "220.1"
59
SVC_CLOSING_CTRL_CNX = "221"
61
DATA_CNX_OPEN_NO_XFR_IN_PROGRESS = "225"
62
CLOSING_DATA_CNX = "226"
63
TXFR_COMPLETE_OK = "226"
64
ENTERING_PASV_MODE = "227"
65
ENTERING_EPSV_MODE = "229"
66
USR_LOGGED_IN_PROCEED = "230.1" # v1 of code 230
67
GUEST_LOGGED_IN_PROCEED = "230.2" # v2 of code 230
68
REQ_FILE_ACTN_COMPLETED_OK = "250"
72
USR_NAME_OK_NEED_PASS = "331.1" # v1 of Code 331
73
GUEST_NAME_OK_NEED_EMAIL = "331.2" # v2 of code 331
74
NEED_ACCT_FOR_LOGIN = "332"
75
REQ_FILE_ACTN_PENDING_FURTHER_INFO = "350"
77
SVC_NOT_AVAIL_CLOSING_CTRL_CNX = "421.1"
78
TOO_MANY_CONNECTIONS = "421.2"
79
CANT_OPEN_DATA_CNX = "425"
80
CNX_CLOSED_TXFR_ABORTED = "426"
81
REQ_ACTN_ABRTD_FILE_UNAVAIL = "450"
82
REQ_ACTN_ABRTD_LOCAL_ERR = "451"
83
REQ_ACTN_ABRTD_INSUFF_STORAGE = "452"
86
SYNTAX_ERR_IN_ARGS = "501"
87
CMD_NOT_IMPLMNTD = "502"
89
CMD_NOT_IMPLMNTD_FOR_PARAM = "504"
90
NOT_LOGGED_IN = "530.1" # v1 of code 530 - please log in
91
AUTH_FAILURE = "530.2" # v2 of code 530 - authorization failure
92
NEED_ACCT_FOR_STOR = "532"
93
FILE_NOT_FOUND = "550.1" # no such file or directory
94
PERMISSION_DENIED = "550.2" # permission denied
95
ANON_USER_DENIED = "550.3" # anonymous users can't alter filesystem
96
IS_NOT_A_DIR = "550.4" # rmd called on a path that is not a directory
97
REQ_ACTN_NOT_TAKEN = "550.5"
100
PAGE_TYPE_UNK = "551"
101
EXCEEDED_STORAGE_ALLOC = "552"
102
FILENAME_NOT_ALLOWED = "553"
107
RESTART_MARKER_REPLY: '110 MARK yyyy-mmmm', # TODO: this must be fixed
108
SERVICE_READY_IN_N_MINUTES: '120 service ready in %s minutes',
109
DATA_CNX_ALREADY_OPEN_START_XFR: '125 Data connection already open, starting transfer',
110
FILE_STATUS_OK_OPEN_DATA_CNX: '150 File status okay; about to open data connection.',
113
CMD_OK: '200 Command OK',
114
TYPE_SET_OK: '200 Type set to %s.',
115
ENTERING_PORT_MODE: '200 PORT OK',
116
CMD_NOT_IMPLMNTD_SUPERFLUOUS: '202 Command not implemented, superfluous at this site',
117
SYS_STATUS_OR_HELP_REPLY: '211 System status reply',
118
DIR_STATUS: '212 %s',
119
FILE_STATUS: '213 %s',
120
HELP_MSG: '214 help: %s',
121
NAME_SYS_TYPE: '215 UNIX Type: L8',
122
WELCOME_MSG: "220 %s",
123
SVC_READY_FOR_NEW_USER: '220 Service ready',
124
GOODBYE_MSG: '221 Goodbye.',
125
DATA_CNX_OPEN_NO_XFR_IN_PROGRESS: '225 data connection open, no transfer in progress',
126
CLOSING_DATA_CNX: '226 Abort successful',
127
TXFR_COMPLETE_OK: '226 Transfer Complete.',
128
ENTERING_PASV_MODE: '227 Entering Passive Mode (%s).',
129
ENTERING_EPSV_MODE: '229 Entering Extended Passive Mode (|||%s|).', # where is epsv defined in the rfc's?
130
USR_LOGGED_IN_PROCEED: '230 User logged in, proceed',
131
GUEST_LOGGED_IN_PROCEED: '230 Anonymous login ok, access restrictions apply.',
132
REQ_FILE_ACTN_COMPLETED_OK: '250 Requested File Action Completed OK', #i.e. CWD completed ok
133
PWD_REPLY: '257 "%s"',
134
MKD_REPLY: '257 "%s" created',
137
'userotp': '331 Response to %s.', # ???
138
USR_NAME_OK_NEED_PASS: '331 Password required for %s.',
139
GUEST_NAME_OK_NEED_EMAIL: '331 Guest login ok, type your email address as password.',
141
REQ_FILE_ACTN_PENDING_FURTHER_INFO: '350 Requested file action pending further information.',
144
SVC_NOT_AVAIL_CLOSING_CTRL_CNX: '421 Service not available, closing control connection.',
145
TOO_MANY_CONNECTIONS: '421 Too many users right now, try again in a few minutes.',
146
CANT_OPEN_DATA_CNX: "425 Can't open data connection.",
147
CNX_CLOSED_TXFR_ABORTED: '426 Transfer aborted. Data connection closed.',
149
REQ_ACTN_ABRTD_LOCAL_ERR: '451 Requested action aborted. Local error in processing.',
153
SYNTAX_ERR: "500 Syntax error: %s",
154
SYNTAX_ERR_IN_ARGS: '501 syntax error in argument(s) %s.',
155
CMD_NOT_IMPLMNTD: "502 Command '%s' not implemented",
156
BAD_CMD_SEQ: '503 Incorrect sequence of commands: %s',
157
CMD_NOT_IMPLMNTD_FOR_PARAM: "504 Not implemented for parameter '%s'.",
158
NOT_LOGGED_IN: '530 Please login with USER and PASS.',
159
AUTH_FAILURE: '530 Sorry, Authentication failed.',
160
NEED_ACCT_FOR_STOR: '532 Need an account for storing files',
161
FILE_NOT_FOUND: '550 %s: No such file or directory.',
162
PERMISSION_DENIED: '550 %s: Permission denied.',
163
ANON_USER_DENIED: '550 Anonymous users are forbidden to change the filesystem',
164
IS_NOT_A_DIR: '550 Cannot rmd, %s is not a directory',
165
FILE_EXISTS: '550 %s: File exists',
166
IS_A_DIR: '550 %s: is a directory',
167
REQ_ACTN_NOT_TAKEN: '550 Requested action not taken: %s',
168
EXCEEDED_STORAGE_ALLOC: '552 Requested file action aborted, exceeded file storage allocation',
169
FILENAME_NOT_ALLOWED: '553 Requested action not taken, file name not allowed'
174
class InvalidPath(Exception):
176
Internal exception used to signify an error during parsing a path.
181
def toSegments(cwd, path):
183
Normalize a path, as represented by a list of strings each
184
representing one segment of the path.
186
if path.startswith('/'):
191
for s in path.split('/'):
192
if s == '.' or s == '':
198
raise InvalidPath(cwd, path)
199
elif '\0' in s or '/' in s:
200
raise InvalidPath(cwd, path)
206
def errnoToFailure(e, path):
208
Map C{OSError} and C{IOError} to standard FTP errors.
210
if e == errno.ENOENT:
211
return defer.fail(FileNotFoundError(path))
212
elif e == errno.EACCES or e == errno.EPERM:
213
return defer.fail(PermissionDeniedError(path))
214
elif e == errno.ENOTDIR:
215
return defer.fail(IsNotADirectoryError(path))
216
elif e == errno.EEXIST:
217
return defer.fail(FileExistsError(path))
218
elif e == errno.EISDIR:
219
return defer.fail(IsADirectoryError(path))
225
class FTPCmdError(Exception):
227
Generic exception for FTP commands.
229
def __init__(self, *msg):
230
Exception.__init__(self, *msg)
231
self.errorMessage = msg
236
Generate a FTP response message for this error.
238
return RESPONSE[self.errorCode] % self.errorMessage
242
class FileNotFoundError(FTPCmdError):
244
Raised when trying to access a non existent file or directory.
246
errorCode = FILE_NOT_FOUND
250
class AnonUserDeniedError(FTPCmdError):
252
Raised when an anonymous user issues a command that will alter the
257
FTPCmdError.__init__(self, None)
259
errorCode = ANON_USER_DENIED
263
class PermissionDeniedError(FTPCmdError):
265
Raised when access is attempted to a resource to which access is
268
errorCode = PERMISSION_DENIED
272
class IsNotADirectoryError(FTPCmdError):
274
Raised when RMD is called on a path that isn't a directory.
276
errorCode = IS_NOT_A_DIR
280
class FileExistsError(FTPCmdError):
282
Raised when attempted to override an existing resource.
284
errorCode = FILE_EXISTS
288
class IsADirectoryError(FTPCmdError):
290
Raised when DELE is called on a path that is a directory.
296
class CmdSyntaxError(FTPCmdError):
298
Raised when a command syntax is wrong.
300
errorCode = SYNTAX_ERR
304
class CmdArgSyntaxError(FTPCmdError):
306
Raised when a command is called with wrong value or a wrong number of
309
errorCode = SYNTAX_ERR_IN_ARGS
313
class CmdNotImplementedError(FTPCmdError):
315
Raised when an unimplemented command is given to the server.
317
errorCode = CMD_NOT_IMPLMNTD
321
class CmdNotImplementedForArgError(FTPCmdError):
323
Raised when the handling of a parameter for a command is not implemented by
326
errorCode = CMD_NOT_IMPLMNTD_FOR_PARAM
330
class FTPError(Exception):
335
class PortConnectionError(Exception):
340
class BadCmdSequenceError(FTPCmdError):
342
Raised when a client sends a series of commands in an illogical sequence.
344
errorCode = BAD_CMD_SEQ
348
class AuthorizationError(FTPCmdError):
350
Raised when client authentication fails.
352
errorCode = AUTH_FAILURE
356
def debugDeferred(self, *_):
357
log.msg('debugDeferred(): %s' % str(_), debug=True)
365
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
366
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
369
class DTP(object, protocol.Protocol):
370
implements(interfaces.IConsumer)
378
def connectionMade(self):
379
self.isConnected = True
380
self.factory.deferred.callback(None)
383
def connectionLost(self, reason):
384
self.isConnected = False
385
if self._onConnLost is not None:
386
self._onConnLost.callback(None)
388
def sendLine(self, line):
389
self.transport.write(line + '\r\n')
392
def _formatOneListResponse(self, name, size, directory, permissions, hardlinks, modified, owner, group):
393
def formatMode(mode):
394
return ''.join([mode & (256 >> n) and 'rwx'[n % 3] or '-' for n in range(9)])
396
def formatDate(mtime):
399
'month': _months[mtime.tm_mon],
400
'day': mtime.tm_mday,
401
'year': mtime.tm_year,
402
'hour': mtime.tm_hour,
403
'minute': mtime.tm_min
405
if now.tm_year != mtime.tm_year:
406
return '%(month)s %(day)02d %(year)5d' % info
408
return '%(month)s %(day)02d %(hour)02d:%(minute)02d' % info
410
format = ('%(directory)s%(permissions)s%(hardlinks)4d '
411
'%(owner)-9s %(group)-9s %(size)15d %(date)12s '
415
'directory': directory and 'd' or '-',
416
'permissions': formatMode(permissions),
417
'hardlinks': hardlinks,
421
'date': formatDate(time.gmtime(modified)),
424
def sendListResponse(self, name, response):
425
self.sendLine(self._formatOneListResponse(name, *response))
428
# Proxy IConsumer to our transport
429
def registerProducer(self, producer, streaming):
430
return self.transport.registerProducer(producer, streaming)
432
def unregisterProducer(self):
433
self.transport.unregisterProducer()
434
self.transport.loseConnection()
436
def write(self, data):
438
return self.transport.write(data)
439
raise Exception("Crap damn crap damn crap damn")
442
# Pretend to be a producer, too.
443
def _conswrite(self, bytes):
445
self._cons.write(bytes)
447
self._onConnLost.errback()
449
def dataReceived(self, bytes):
450
if self._cons is not None:
451
self._conswrite(bytes)
453
self._buffer.append(bytes)
455
def _unregConsumer(self, ignored):
456
self._cons.unregisterProducer()
461
def registerConsumer(self, cons):
462
assert self._cons is None
464
self._cons.registerProducer(self, True)
465
for chunk in self._buffer:
466
self._conswrite(chunk)
469
self._onConnLost = d = defer.Deferred()
470
d.addBoth(self._unregConsumer)
473
self._cons.unregisterProducer()
475
return defer.succeed(None)
477
def resumeProducing(self):
478
self.transport.resumeProducing()
480
def pauseProducing(self):
481
self.transport.pauseProducing()
483
def stopProducing(self):
484
self.transport.stopProducing()
486
class DTPFactory(protocol.ClientFactory):
488
Client factory for I{data transfer process} protocols.
490
@ivar peerCheck: perform checks to make sure the ftp-pi's peer is the same
492
@ivar pi: a reference to this factory's protocol interpreter
494
@ivar _state: Indicates the current state of the DTPFactory. Initially,
495
this is L{_IN_PROGRESS}. If the connection fails or times out, it is
496
L{_FAILED}. If the connection succeeds before the timeout, it is
500
_IN_PROGRESS = object()
504
_state = _IN_PROGRESS
506
# -- configuration variables --
509
# -- class variables --
510
def __init__(self, pi, peerHost=None, reactor=None):
512
@param pi: this factory's protocol interpreter
513
@param peerHost: if peerCheck is True, this is the tuple that the
514
generated instance will use to perform security checks
516
self.pi = pi # the protocol interpreter that is using this factory
517
self.peerHost = peerHost # the from FTP.transport.peerHost()
518
self.deferred = defer.Deferred() # deferred will fire when instance is connected
519
self.delayedCall = None
521
from twisted.internet import reactor
522
self._reactor = reactor
525
def buildProtocol(self, addr):
526
log.msg('DTPFactory.buildProtocol', debug=True)
528
if self._state is not self._IN_PROGRESS:
530
self._state = self._FINISHED
536
self.pi.dtpInstance = p
540
def stopFactory(self):
541
log.msg('dtpFactory.stopFactory', debug=True)
545
def timeoutFactory(self):
546
log.msg('timed out waiting for DTP connection')
547
if self._state is not self._IN_PROGRESS:
549
self._state = self._FAILED
554
PortConnectionError(defer.TimeoutError("DTPFactory timeout")))
557
def cancelTimeout(self):
558
if self.delayedCall is not None and self.delayedCall.active():
559
log.msg('cancelling DTP timeout', debug=True)
560
self.delayedCall.cancel()
563
def setTimeout(self, seconds):
564
log.msg('DTPFactory.setTimeout set to %s seconds' % seconds)
565
self.delayedCall = self._reactor.callLater(seconds, self.timeoutFactory)
568
def clientConnectionFailed(self, connector, reason):
569
if self._state is not self._IN_PROGRESS:
571
self._state = self._FAILED
574
d.errback(PortConnectionError(reason))
577
# -- FTP-PI (Protocol Interpreter) --
579
class ASCIIConsumerWrapper(object):
580
def __init__(self, cons):
582
self.registerProducer = cons.registerProducer
583
self.unregisterProducer = cons.unregisterProducer
585
assert os.linesep == "\r\n" or len(os.linesep) == 1, "Unsupported platform (yea right like this even exists)"
587
if os.linesep == "\r\n":
588
self.write = cons.write
590
def write(self, bytes):
591
return self.cons.write(bytes.replace(os.linesep, "\r\n"))
595
class FileConsumer(object):
597
A consumer for FTP input that writes data to a file.
599
@ivar fObj: a file object opened for writing, used to write data received.
603
implements(interfaces.IConsumer)
605
def __init__(self, fObj):
609
def registerProducer(self, producer, streaming):
610
self.producer = producer
614
def unregisterProducer(self):
619
def write(self, bytes):
620
self.fObj.write(bytes)
624
class FTPOverflowProtocol(basic.LineReceiver):
625
"""FTP mini-protocol for when there are too many connections."""
626
def connectionMade(self):
627
self.sendLine(RESPONSE[TOO_MANY_CONNECTIONS])
628
self.transport.loseConnection()
631
class FTP(object, basic.LineReceiver, policies.TimeoutMixin):
633
Protocol Interpreter for the File Transfer Protocol
635
@ivar state: The current server state. One of L{UNAUTH},
636
L{INAUTH}, L{AUTHED}, L{RENAMING}.
638
@ivar shell: The connected avatar
639
@ivar binary: The transfer mode. If false, ASCII.
640
@ivar dtpFactory: Generates a single DTP for this session
641
@ivar dtpPort: Port returned from listenTCP
642
@ivar listenFactory: A callable with the signature of
643
L{twisted.internet.interfaces.IReactorTCP.listenTCP} which will be used
644
to create Ports for passive connections (mainly for testing).
646
@ivar passivePortRange: iterator used as source of passive port numbers.
647
@type passivePortRange: C{iterator}
652
# States an FTP can be in
653
UNAUTH, INAUTH, AUTHED, RENAMING = range(4)
655
# how long the DTP waits for a connection
665
passivePortRange = xrange(0, 1)
667
listenFactory = reactor.listenTCP
669
def reply(self, key, *args):
670
msg = RESPONSE[key] % args
674
def connectionMade(self):
675
self.state = self.UNAUTH
676
self.setTimeout(self.timeOut)
677
self.reply(WELCOME_MSG, self.factory.welcomeMessage)
679
def connectionLost(self, reason):
680
# if we have a DTP protocol instance running and
681
# we lose connection to the client's PI, kill the
682
# DTP connection and close the port
685
self.setTimeout(None)
686
if hasattr(self.shell, 'logout') and self.shell.logout is not None:
689
self.transport = None
691
def timeoutConnection(self):
692
self.transport.loseConnection()
694
def lineReceived(self, line):
696
self.pauseProducing()
698
def processFailed(err):
699
if err.check(FTPCmdError):
700
self.sendLine(err.value.response())
701
elif (err.check(TypeError) and
702
err.value.args[0].find('takes exactly') != -1):
703
self.reply(SYNTAX_ERR, "%s requires an argument." % (cmd,))
705
log.msg("Unexpected FTP error")
707
self.reply(REQ_ACTN_NOT_TAKEN, "internal server error")
709
def processSucceeded(result):
710
if isinstance(result, tuple):
712
elif result is not None:
715
def allDone(ignored):
716
if not self.disconnected:
717
self.resumeProducing()
719
spaceIndex = line.find(' ')
721
cmd = line[:spaceIndex]
722
args = (line[spaceIndex + 1:],)
726
d = defer.maybeDeferred(self.processCommand, cmd, *args)
727
d.addCallbacks(processSucceeded, processFailed)
728
d.addErrback(log.err)
731
# LineReceiver doesn't let you resumeProducing inside
733
from twisted.internet import reactor
734
reactor.callLater(0, d.addBoth, allDone)
737
def processCommand(self, cmd, *params):
740
if self.state == self.UNAUTH:
742
return self.ftp_USER(*params)
744
return BAD_CMD_SEQ, "USER required before PASS"
748
elif self.state == self.INAUTH:
750
return self.ftp_PASS(*params)
752
return BAD_CMD_SEQ, "PASS required after USER"
754
elif self.state == self.AUTHED:
755
method = getattr(self, "ftp_" + cmd, None)
756
if method is not None:
757
return method(*params)
758
return defer.fail(CmdNotImplementedError(cmd))
760
elif self.state == self.RENAMING:
762
return self.ftp_RNTO(*params)
764
return BAD_CMD_SEQ, "RNTO required after RNFR"
767
def getDTPPort(self, factory):
769
Return a port for passive access, using C{self.passivePortRange}
772
for portn in self.passivePortRange:
774
dtpPort = self.listenFactory(portn, factory)
775
except error.CannotListenError:
779
raise error.CannotListenError('', portn,
780
"No port available in range %s" %
781
(self.passivePortRange,))
784
def ftp_USER(self, username):
786
First part of login. Get the username the peer wants to
790
return defer.fail(CmdSyntaxError('USER requires an argument'))
792
self._user = username
793
self.state = self.INAUTH
794
if self.factory.allowAnonymous and self._user == self.factory.userAnonymous:
795
return GUEST_NAME_OK_NEED_EMAIL
797
return (USR_NAME_OK_NEED_PASS, username)
799
# TODO: add max auth try before timeout from ip...
800
# TODO: need to implement minimal ABOR command
802
def ftp_PASS(self, password):
804
Second part of login. Get the password the peer wants to
807
if self.factory.allowAnonymous and self._user == self.factory.userAnonymous:
809
creds = credentials.Anonymous()
810
reply = GUEST_LOGGED_IN_PROCEED
813
creds = credentials.UsernamePassword(self._user, password)
814
reply = USR_LOGGED_IN_PROCEED
817
def _cbLogin((interface, avatar, logout)):
818
assert interface is IFTPShell, "The realm is busted, jerk."
821
self.workingDirectory = []
822
self.state = self.AUTHED
825
def _ebLogin(failure):
826
failure.trap(cred_error.UnauthorizedLogin, cred_error.UnhandledCredentials)
827
self.state = self.UNAUTH
828
raise AuthorizationError
830
d = self.portal.login(creds, None, IFTPShell)
831
d.addCallbacks(_cbLogin, _ebLogin)
836
"""Request for a passive connection
840
This command requests the server-DTP to \"listen\" on a data port
841
(which is not its default data port) and to wait for a connection
842
rather than initiate one upon receipt of a transfer command. The
843
response to this command includes the host and port address this
844
server is listening on.
846
# if we have a DTP port set up, lose it.
847
if self.dtpFactory is not None:
848
# cleanupDTP sets dtpFactory to none. Later we'll do
849
# cleanup here or something.
851
self.dtpFactory = DTPFactory(pi=self)
852
self.dtpFactory.setTimeout(self.dtpTimeout)
853
self.dtpPort = self.getDTPPort(self.dtpFactory)
855
host = self.transport.getHost().host
856
port = self.dtpPort.getHost().port
857
self.reply(ENTERING_PASV_MODE, encodeHostPort(host, port))
858
return self.dtpFactory.deferred.addCallback(lambda ign: None)
861
def ftp_PORT(self, address):
862
addr = map(int, address.split(','))
863
ip = '%d.%d.%d.%d' % tuple(addr[:4])
864
port = addr[4] << 8 | addr[5]
866
# if we have a DTP port set up, lose it.
867
if self.dtpFactory is not None:
870
self.dtpFactory = DTPFactory(pi=self, peerHost=self.transport.getPeer().host)
871
self.dtpFactory.setTimeout(self.dtpTimeout)
872
self.dtpPort = reactor.connectTCP(ip, port, self.dtpFactory)
874
def connected(ignored):
875
return ENTERING_PORT_MODE
877
err.trap(PortConnectionError)
878
return CANT_OPEN_DATA_CNX
879
return self.dtpFactory.deferred.addCallbacks(connected, connFailed)
882
def ftp_LIST(self, path=''):
883
""" This command causes a list to be sent from the server to the
884
passive DTP. If the pathname specifies a directory or other
885
group of files, the server should transfer a list of files
886
in the specified directory. If the pathname specifies a
887
file then the server should send current information on the
888
file. A null argument implies the user's current working or
891
# Uh, for now, do this retarded thing.
892
if self.dtpInstance is None or not self.dtpInstance.isConnected:
893
return defer.fail(BadCmdSequenceError('must send PORT or PASV before RETR'))
901
# bug in Nautilus 2.10.0
908
def gotListing(results):
909
self.reply(DATA_CNX_ALREADY_OPEN_START_XFR)
910
for (name, attrs) in results:
911
self.dtpInstance.sendListResponse(name, attrs)
912
self.dtpInstance.transport.loseConnection()
913
return (TXFR_COMPLETE_OK,)
916
segments = toSegments(self.workingDirectory, path)
917
except InvalidPath, e:
918
return defer.fail(FileNotFoundError(path))
922
('size', 'directory', 'permissions', 'hardlinks',
923
'modified', 'owner', 'group'))
924
d.addCallback(gotListing)
928
def ftp_NLST(self, path):
930
This command causes a directory listing to be sent from the server to
931
the client. The pathname should specify a directory or other
932
system-specific file group descriptor. An empty path implies the current
933
working directory. If the path is non-existent, send nothing. If the
934
path is to a file, send only the file name.
937
@param path: The path for which a directory listing should be returned.
940
@return: a L{Deferred} which will be fired when the listing request
943
# XXX: why is this check different from ftp_RETR/ftp_STOR? See #4180
944
if self.dtpInstance is None or not self.dtpInstance.isConnected:
946
BadCmdSequenceError('must send PORT or PASV before RETR'))
949
segments = toSegments(self.workingDirectory, path)
950
except InvalidPath, e:
951
return defer.fail(FileNotFoundError(path))
955
Send, line by line, each file in the directory listing, and then
956
close the connection.
958
@type results: A C{list} of C{tuple}. The first element of each
959
C{tuple} is a C{str} and the second element is a C{list}.
960
@param results: The names of the files in the directory.
963
@return: A C{tuple} containing the status code for a successful
966
self.reply(DATA_CNX_ALREADY_OPEN_START_XFR)
967
for (name, ignored) in results:
968
self.dtpInstance.sendLine(name)
969
self.dtpInstance.transport.loseConnection()
970
return (TXFR_COMPLETE_OK,)
973
self.reply(DATA_CNX_ALREADY_OPEN_START_XFR)
974
for (name, ignored) in results:
975
if fnmatch.fnmatch(name, segments[-1]):
976
self.dtpInstance.sendLine(name)
977
self.dtpInstance.transport.loseConnection()
978
return (TXFR_COMPLETE_OK,)
980
def listErr(results):
982
RFC 959 specifies that an NLST request may only return directory
983
listings. Thus, send nothing and just close the connection.
985
@type results: L{Failure}
986
@param results: The L{Failure} wrapping a L{FileNotFoundError} that
987
occurred while trying to list the contents of a nonexistent
991
@returns: A C{tuple} containing the status code for a successful
994
self.dtpInstance.transport.loseConnection()
995
return (TXFR_COMPLETE_OK,)
997
# XXX This globbing may be incomplete: see #4181
999
'*' in segments[-1] or '?' in segments[-1] or
1000
('[' in segments[-1] and ']' in segments[-1])):
1001
d = self.shell.list(segments[:-1])
1002
d.addCallback(cbGlob)
1004
d = self.shell.list(segments)
1005
d.addCallback(cbList)
1006
# self.shell.list will generate an error if the path is invalid
1007
d.addErrback(listErr)
1011
def ftp_CWD(self, path):
1013
segments = toSegments(self.workingDirectory, path)
1014
except InvalidPath, e:
1015
# XXX Eh, what to fail with here?
1016
return defer.fail(FileNotFoundError(path))
1018
def accessGranted(result):
1019
self.workingDirectory = segments
1020
return (REQ_FILE_ACTN_COMPLETED_OK,)
1022
return self.shell.access(segments).addCallback(accessGranted)
1026
return self.ftp_CWD('..')
1030
return (PWD_REPLY, '/' + '/'.join(self.workingDirectory))
1033
def ftp_RETR(self, path):
1034
if self.dtpInstance is None:
1035
raise BadCmdSequenceError('PORT or PASV required before RETR')
1038
newsegs = toSegments(self.workingDirectory, path)
1040
return defer.fail(FileNotFoundError(path))
1042
# XXX For now, just disable the timeout. Later we'll want to
1043
# leave it active and have the DTP connection reset it
1045
self.setTimeout(None)
1048
def enableTimeout(result):
1049
self.setTimeout(self.factory.timeOut)
1054
cons = ASCIIConsumerWrapper(self.dtpInstance)
1056
cons = self.dtpInstance
1059
return (TXFR_COMPLETE_OK,)
1062
log.msg("Unexpected error attempting to transmit file to client:")
1064
return (CNX_CLOSED_TXFR_ABORTED,)
1067
# Tell them what to doooo
1068
if self.dtpInstance.isConnected:
1069
self.reply(DATA_CNX_ALREADY_OPEN_START_XFR)
1071
self.reply(FILE_STATUS_OK_OPEN_DATA_CNX)
1074
d.addCallbacks(cbSent, ebSent)
1078
if not err.check(PermissionDeniedError, FileNotFoundError, IsNotADirectoryError):
1079
log.msg("Unexpected error attempting to open file for transmission:")
1081
if err.check(FTPCmdError):
1082
return (err.value.errorCode, '/'.join(newsegs))
1083
return (FILE_NOT_FOUND, '/'.join(newsegs))
1085
d = self.shell.openForReading(newsegs)
1086
d.addCallbacks(cbOpened, ebOpened)
1087
d.addBoth(enableTimeout)
1089
# Pass back Deferred that fires when the transfer is done
1093
def ftp_STOR(self, path):
1094
if self.dtpInstance is None:
1095
raise BadCmdSequenceError('PORT or PASV required before STOR')
1098
newsegs = toSegments(self.workingDirectory, path)
1100
return defer.fail(FileNotFoundError(path))
1102
# XXX For now, just disable the timeout. Later we'll want to
1103
# leave it active and have the DTP connection reset it
1105
self.setTimeout(None)
1108
def enableTimeout(result):
1109
self.setTimeout(self.factory.timeOut)
1113
return (TXFR_COMPLETE_OK,)
1116
log.msg("Unexpected error receiving file from client:")
1118
return (CNX_CLOSED_TXFR_ABORTED,)
1120
def cbConsumer(cons):
1122
cons = ASCIIConsumerWrapper(cons)
1124
d = self.dtpInstance.registerConsumer(cons)
1125
d.addCallbacks(cbSent, ebSent)
1127
# Tell them what to doooo
1128
if self.dtpInstance.isConnected:
1129
self.reply(DATA_CNX_ALREADY_OPEN_START_XFR)
1131
self.reply(FILE_STATUS_OK_OPEN_DATA_CNX)
1137
d.addCallback(cbConsumer)
1141
if not err.check(PermissionDeniedError, FileNotFoundError, IsNotADirectoryError):
1142
log.msg("Unexpected error attempting to open file for upload:")
1144
if isinstance(err.value, FTPCmdError):
1145
return (err.value.errorCode, '/'.join(newsegs))
1146
return (FILE_NOT_FOUND, '/'.join(newsegs))
1148
d = self.shell.openForWriting(newsegs)
1149
d.addCallbacks(cbOpened, ebOpened)
1150
d.addBoth(enableTimeout)
1152
# Pass back Deferred that fires when the transfer is done
1156
def ftp_SIZE(self, path):
1158
newsegs = toSegments(self.workingDirectory, path)
1160
return defer.fail(FileNotFoundError(path))
1162
def cbStat((size,)):
1163
return (FILE_STATUS, str(size))
1165
return self.shell.stat(newsegs, ('size',)).addCallback(cbStat)
1168
def ftp_MDTM(self, path):
1170
newsegs = toSegments(self.workingDirectory, path)
1172
return defer.fail(FileNotFoundError(path))
1174
def cbStat((modified,)):
1175
return (FILE_STATUS, time.strftime('%Y%m%d%H%M%S', time.gmtime(modified)))
1177
return self.shell.stat(newsegs, ('modified',)).addCallback(cbStat)
1180
def ftp_TYPE(self, type):
1183
f = getattr(self, 'type_' + p[0], None)
1186
return self.type_UNKNOWN(p)
1187
return (SYNTAX_ERR,)
1189
def type_A(self, code):
1190
if code == '' or code == 'N':
1192
return (TYPE_SET_OK, 'A' + code)
1194
return defer.fail(CmdArgSyntaxError(code))
1196
def type_I(self, code):
1199
return (TYPE_SET_OK, 'I')
1201
return defer.fail(CmdArgSyntaxError(code))
1203
def type_UNKNOWN(self, code):
1204
return defer.fail(CmdNotImplementedForArgError(code))
1209
return NAME_SYS_TYPE
1212
def ftp_STRU(self, structure):
1213
p = structure.upper()
1216
return defer.fail(CmdNotImplementedForArgError(structure))
1219
def ftp_MODE(self, mode):
1223
return defer.fail(CmdNotImplementedForArgError(mode))
1226
def ftp_MKD(self, path):
1228
newsegs = toSegments(self.workingDirectory, path)
1230
return defer.fail(FileNotFoundError(path))
1231
return self.shell.makeDirectory(newsegs).addCallback(lambda ign: (MKD_REPLY, path))
1234
def ftp_RMD(self, path):
1236
newsegs = toSegments(self.workingDirectory, path)
1238
return defer.fail(FileNotFoundError(path))
1239
return self.shell.removeDirectory(newsegs).addCallback(lambda ign: (REQ_FILE_ACTN_COMPLETED_OK,))
1242
def ftp_DELE(self, path):
1244
newsegs = toSegments(self.workingDirectory, path)
1246
return defer.fail(FileNotFoundError(path))
1247
return self.shell.removeFile(newsegs).addCallback(lambda ign: (REQ_FILE_ACTN_COMPLETED_OK,))
1254
def ftp_RNFR(self, fromName):
1255
self._fromName = fromName
1256
self.state = self.RENAMING
1257
return (REQ_FILE_ACTN_PENDING_FURTHER_INFO,)
1260
def ftp_RNTO(self, toName):
1261
fromName = self._fromName
1263
self.state = self.AUTHED
1266
fromsegs = toSegments(self.workingDirectory, fromName)
1267
tosegs = toSegments(self.workingDirectory, toName)
1269
return defer.fail(FileNotFoundError(fromName))
1270
return self.shell.rename(fromsegs, tosegs).addCallback(lambda ign: (REQ_FILE_ACTN_COMPLETED_OK,))
1274
self.reply(GOODBYE_MSG)
1275
self.transport.loseConnection()
1276
self.disconnected = True
1279
def cleanupDTP(self):
1280
"""call when DTP connection exits
1282
log.msg('cleanupDTP', debug=True)
1284
log.msg(self.dtpPort)
1285
dtpPort, self.dtpPort = self.dtpPort, None
1286
if interfaces.IListeningPort.providedBy(dtpPort):
1287
dtpPort.stopListening()
1288
elif interfaces.IConnector.providedBy(dtpPort):
1289
dtpPort.disconnect()
1291
assert False, "dtpPort should be an IListeningPort or IConnector, instead is %r" % (dtpPort,)
1293
self.dtpFactory.stopFactory()
1294
self.dtpFactory = None
1296
if self.dtpInstance is not None:
1297
self.dtpInstance = None
1300
class FTPFactory(policies.LimitTotalConnectionsFactory):
1302
A factory for producing ftp protocol instances
1304
@ivar timeOut: the protocol interpreter's idle timeout time in seconds,
1305
default is 600 seconds.
1307
@ivar passivePortRange: value forwarded to C{protocol.passivePortRange}.
1308
@type passivePortRange: C{iterator}
1311
overflowProtocol = FTPOverflowProtocol
1312
allowAnonymous = True
1313
userAnonymous = 'anonymous'
1316
welcomeMessage = "Twisted %s FTP Server" % (copyright.version,)
1318
passivePortRange = xrange(0, 1)
1320
def __init__(self, portal=None, userAnonymous='anonymous'):
1321
self.portal = portal
1322
self.userAnonymous = userAnonymous
1325
def buildProtocol(self, addr):
1326
p = policies.LimitTotalConnectionsFactory.buildProtocol(self, addr)
1328
p.wrappedProtocol.portal = self.portal
1329
p.wrappedProtocol.timeOut = self.timeOut
1330
p.wrappedProtocol.passivePortRange = self.passivePortRange
1333
def stopFactory(self):
1334
# make sure ftp instance's timeouts are set to None
1335
# to avoid reactor complaints
1336
[p.setTimeout(None) for p in self.instances if p.timeOut is not None]
1337
policies.LimitTotalConnectionsFactory.stopFactory(self)
1339
# -- Cred Objects --
1342
class IFTPShell(Interface):
1344
An abstraction of the shell commands used by the FTP protocol for
1345
a given user account.
1347
All path names must be absolute.
1350
def makeDirectory(path):
1354
@param path: The path, as a list of segments, to create
1355
@type path: C{list} of C{unicode}
1357
@return: A Deferred which fires when the directory has been
1358
created, or which fails if the directory cannot be created.
1362
def removeDirectory(path):
1366
@param path: The path, as a list of segments, to remove
1367
@type path: C{list} of C{unicode}
1369
@return: A Deferred which fires when the directory has been
1370
removed, or which fails if the directory cannot be removed.
1374
def removeFile(path):
1378
@param path: The path, as a list of segments, to remove
1379
@type path: C{list} of C{unicode}
1381
@return: A Deferred which fires when the file has been
1382
removed, or which fails if the file cannot be removed.
1386
def rename(fromPath, toPath):
1388
Rename a file or directory.
1390
@param fromPath: The current name of the path.
1391
@type fromPath: C{list} of C{unicode}
1393
@param toPath: The desired new name of the path.
1394
@type toPath: C{list} of C{unicode}
1396
@return: A Deferred which fires when the path has been
1397
renamed, or which fails if the path cannot be renamed.
1403
Determine whether access to the given path is allowed.
1405
@param path: The path, as a list of segments
1407
@return: A Deferred which fires with None if access is allowed
1408
or which fails with a specific exception type if access is
1413
def stat(path, keys=()):
1415
Retrieve information about the given path.
1417
This is like list, except it will never return results about
1422
def list(path, keys=()):
1424
Retrieve information about the given path.
1426
If the path represents a non-directory, the result list should
1427
have only one entry with information about that non-directory.
1428
Otherwise, the result list should have an element for each
1429
child of the directory.
1431
@param path: The path, as a list of segments, to list
1432
@type path: C{list} of C{unicode}
1434
@param keys: A tuple of keys desired in the resulting
1437
@return: A Deferred which fires with a list of (name, list),
1438
where the name is the name of the entry as a unicode string
1439
and each list contains values corresponding to the requested
1440
keys. The following are possible elements of keys, and the
1441
values which should be returned for them:
1443
- C{'size'}: size in bytes, as an integer (this is kinda required)
1445
- C{'directory'}: boolean indicating the type of this entry
1447
- C{'permissions'}: a bitvector (see os.stat(foo).st_mode)
1449
- C{'hardlinks'}: Number of hard links to this entry
1451
- C{'modified'}: number of seconds since the epoch since entry was
1454
- C{'owner'}: string indicating the user owner of this entry
1456
- C{'group'}: string indicating the group owner of this entry
1460
def openForReading(path):
1462
@param path: The path, as a list of segments, to open
1463
@type path: C{list} of C{unicode}
1465
@rtype: C{Deferred} which will fire with L{IReadFile}
1469
def openForWriting(path):
1471
@param path: The path, as a list of segments, to open
1472
@type path: C{list} of C{unicode}
1474
@rtype: C{Deferred} which will fire with L{IWriteFile}
1479
class IReadFile(Interface):
1481
A file out of which bytes may be read.
1486
Produce the contents of the given path to the given consumer. This
1487
method may only be invoked once on each provider.
1489
@type consumer: C{IConsumer}
1491
@return: A Deferred which fires when the file has been
1492
consumed completely.
1497
class IWriteFile(Interface):
1499
A file into which bytes may be written.
1504
Create a consumer which will write to this file. This method may
1505
only be invoked once on each provider.
1507
@rtype: C{Deferred} of C{IConsumer}
1512
def _getgroups(uid):
1513
"""Return the primary and supplementary groups for the given UID.
1518
pwent = pwd.getpwuid(uid)
1520
result.append(pwent.pw_gid)
1522
for grent in grp.getgrall():
1523
if pwent.pw_name in grent.gr_mem:
1524
result.append(grent.gr_gid)
1529
def _testPermissions(uid, gid, spath, mode='r'):
1531
checks to see if uid has proper permissions to access path with mode
1534
@param uid: numeric user id
1537
@param gid: numeric group id
1540
@param spath: the path on the server to test
1543
@param mode: 'r' or 'w' (read or write)
1546
@return: True if the given credentials have the specified form of
1547
access to the given path
1560
raise ValueError("Invalid mode %r: must specify 'r' or 'w'" % (mode,))
1563
if os.path.exists(spath):
1568
if usr & s.st_mode and uid == s.st_uid:
1570
elif grp & s.st_mode and gid in _getgroups(uid):
1572
elif oth & s.st_mode:
1576
if not os.access(spath, amode):
1578
log.msg("Filesystem grants permission to UID %d but it is inaccessible to me running as UID %d" % (
1584
class FTPAnonymousShell(object):
1586
An anonymous implementation of IFTPShell
1588
@type filesystemRoot: L{twisted.python.filepath.FilePath}
1589
@ivar filesystemRoot: The path which is considered the root of
1592
implements(IFTPShell)
1594
def __init__(self, filesystemRoot):
1595
self.filesystemRoot = filesystemRoot
1598
def _path(self, path):
1599
return reduce(filepath.FilePath.child, path, self.filesystemRoot)
1602
def makeDirectory(self, path):
1603
return defer.fail(AnonUserDeniedError())
1606
def removeDirectory(self, path):
1607
return defer.fail(AnonUserDeniedError())
1610
def removeFile(self, path):
1611
return defer.fail(AnonUserDeniedError())
1614
def rename(self, fromPath, toPath):
1615
return defer.fail(AnonUserDeniedError())
1618
def receive(self, path):
1619
path = self._path(path)
1620
return defer.fail(AnonUserDeniedError())
1623
def openForReading(self, path):
1624
p = self._path(path)
1626
# Normally, we would only check for EISDIR in open, but win32
1627
# returns EACCES in this case, so we check before
1628
return defer.fail(IsADirectoryError(path))
1631
except (IOError, OSError), e:
1632
return errnoToFailure(e.errno, path)
1636
return defer.succeed(_FileReader(f))
1639
def openForWriting(self, path):
1641
Reject write attempts by anonymous users with
1642
L{PermissionDeniedError}.
1644
return defer.fail(PermissionDeniedError("STOR not allowed"))
1647
def access(self, path):
1648
p = self._path(path)
1650
# Again, win32 doesn't report a sane error after, so let's fail
1652
return defer.fail(FileNotFoundError(path))
1653
# For now, just see if we can os.listdir() it
1656
except (IOError, OSError), e:
1657
return errnoToFailure(e.errno, path)
1661
return defer.succeed(None)
1664
def stat(self, path, keys=()):
1665
p = self._path(path)
1668
statResult = self._statNode(p, keys)
1669
except (IOError, OSError), e:
1670
return errnoToFailure(e.errno, path)
1674
return defer.succeed(statResult)
1676
return self.list(path, keys).addCallback(lambda res: res[0][1])
1679
def list(self, path, keys=()):
1681
Return the list of files at given C{path}, adding C{keys} stat
1682
informations if specified.
1684
@param path: the directory or file to check.
1687
@param keys: the list of desired metadata
1688
@type keys: C{list} of C{str}
1690
filePath = self._path(path)
1691
if filePath.isdir():
1692
entries = filePath.listdir()
1693
fileEntries = [filePath.child(p) for p in entries]
1694
elif filePath.isfile():
1695
entries = [os.path.join(*filePath.segmentsFrom(self.filesystemRoot))]
1696
fileEntries = [filePath]
1698
return defer.fail(FileNotFoundError(path))
1701
for fileName, filePath in zip(entries, fileEntries):
1703
results.append((fileName, ent))
1706
ent.extend(self._statNode(filePath, keys))
1707
except (IOError, OSError), e:
1708
return errnoToFailure(e.errno, fileName)
1712
return defer.succeed(results)
1715
def _statNode(self, filePath, keys):
1717
Shortcut method to get stat info on a node.
1719
@param filePath: the node to stat.
1720
@type filePath: C{filepath.FilePath}
1722
@param keys: the stat keys to get.
1723
@type keys: C{iterable}
1726
return [getattr(self, '_stat_' + k)(filePath.statinfo) for k in keys]
1728
_stat_size = operator.attrgetter('st_size')
1729
_stat_permissions = operator.attrgetter('st_mode')
1730
_stat_hardlinks = operator.attrgetter('st_nlink')
1731
_stat_modified = operator.attrgetter('st_mtime')
1734
def _stat_owner(self, st):
1737
return pwd.getpwuid(st.st_uid)[0]
1740
return str(st.st_uid)
1743
def _stat_group(self, st):
1746
return grp.getgrgid(st.st_gid)[0]
1749
return str(st.st_gid)
1752
def _stat_directory(self, st):
1753
return bool(st.st_mode & stat.S_IFDIR)
1757
class _FileReader(object):
1758
implements(IReadFile)
1760
def __init__(self, fObj):
1764
def _close(self, passthrough):
1769
def send(self, consumer):
1770
assert not self._send, "Can only call IReadFile.send *once* per instance"
1772
d = basic.FileSender().beginFileTransfer(self.fObj, consumer)
1773
d.addBoth(self._close)
1778
class FTPShell(FTPAnonymousShell):
1780
An authenticated implementation of L{IFTPShell}.
1783
def makeDirectory(self, path):
1784
p = self._path(path)
1787
except (IOError, OSError), e:
1788
return errnoToFailure(e.errno, path)
1792
return defer.succeed(None)
1795
def removeDirectory(self, path):
1796
p = self._path(path)
1798
# Win32 returns the wrong errno when rmdir is called on a file
1799
# instead of a directory, so as we have the info here, let's fail
1800
# early with a pertinent error
1801
return defer.fail(IsNotADirectoryError(path))
1804
except (IOError, OSError), e:
1805
return errnoToFailure(e.errno, path)
1809
return defer.succeed(None)
1812
def removeFile(self, path):
1813
p = self._path(path)
1815
# Win32 returns the wrong errno when remove is called on a
1816
# directory instead of a file, so as we have the info here,
1817
# let's fail early with a pertinent error
1818
return defer.fail(IsADirectoryError(path))
1821
except (IOError, OSError), e:
1822
return errnoToFailure(e.errno, path)
1826
return defer.succeed(None)
1829
def rename(self, fromPath, toPath):
1830
fp = self._path(fromPath)
1831
tp = self._path(toPath)
1833
os.rename(fp.path, tp.path)
1834
except (IOError, OSError), e:
1835
return errnoToFailure(e.errno, fromPath)
1839
return defer.succeed(None)
1842
def openForWriting(self, path):
1843
p = self._path(path)
1845
# Normally, we would only check for EISDIR in open, but win32
1846
# returns EACCES in this case, so we check before
1847
return defer.fail(IsADirectoryError(path))
1850
except (IOError, OSError), e:
1851
return errnoToFailure(e.errno, path)
1854
return defer.succeed(_FileWriter(fObj))
1858
class _FileWriter(object):
1859
implements(IWriteFile)
1861
def __init__(self, fObj):
1863
self._receive = False
1866
assert not self._receive, "Can only call IWriteFile.receive *once* per instance"
1867
self._receive = True
1868
# FileConsumer will close the file object
1869
return defer.succeed(FileConsumer(self.fObj))
1875
@type anonymousRoot: L{twisted.python.filepath.FilePath}
1876
@ivar anonymousRoot: Root of the filesystem to which anonymous
1877
users will be granted access.
1879
implements(portal.IRealm)
1881
def __init__(self, anonymousRoot):
1882
self.anonymousRoot = filepath.FilePath(anonymousRoot)
1884
def requestAvatar(self, avatarId, mind, *interfaces):
1885
for iface in interfaces:
1886
if iface is IFTPShell:
1887
if avatarId is checkers.ANONYMOUS:
1888
avatar = FTPAnonymousShell(self.anonymousRoot)
1890
avatar = FTPShell(filepath.FilePath("/home/" + avatarId))
1891
return IFTPShell, avatar, getattr(avatar, 'logout', lambda: None)
1892
raise NotImplementedError("Only IFTPShell interface is supported by this realm")
1894
# --- FTP CLIENT -------------------------------------------------------------
1897
# And now for the client...
1900
# * Reference: http://cr.yp.to/ftp.html
1901
# * FIXME: Does not support pipelining (which is not supported by all
1902
# servers anyway). This isn't a functionality limitation, just a
1903
# small performance issue.
1904
# * Only has a rudimentary understanding of FTP response codes (although
1905
# the full response is passed to the caller if they so choose).
1906
# * Assumes that USER and PASS should always be sent
1907
# * Always sets TYPE I (binary mode)
1908
# * Doesn't understand any of the weird, obscure TELNET stuff (\377...)
1909
# * FIXME: Doesn't share any code with the FTPServer
1911
class ConnectionLost(FTPError):
1914
class CommandFailed(FTPError):
1917
class BadResponse(FTPError):
1920
class UnexpectedResponse(FTPError):
1923
class UnexpectedData(FTPError):
1927
def __init__(self, text=None, public=0):
1929
self.deferred = defer.Deferred()
1931
self.public = public
1932
self.transferDeferred = None
1934
def fail(self, failure):
1936
self.deferred.errback(failure)
1939
class ProtocolWrapper(protocol.Protocol):
1940
def __init__(self, original, deferred):
1941
self.original = original
1942
self.deferred = deferred
1943
def makeConnection(self, transport):
1944
self.original.makeConnection(transport)
1945
def dataReceived(self, data):
1946
self.original.dataReceived(data)
1947
def connectionLost(self, reason):
1948
self.original.connectionLost(reason)
1949
# Signal that transfer has completed
1950
self.deferred.callback(None)
1953
class SenderProtocol(protocol.Protocol):
1954
implements(interfaces.IFinishableConsumer)
1957
# Fired upon connection
1958
self.connectedDeferred = defer.Deferred()
1960
# Fired upon disconnection
1961
self.deferred = defer.Deferred()
1964
def dataReceived(self, data):
1965
raise UnexpectedData(
1966
"Received data from the server on a "
1967
"send-only data-connection"
1970
def makeConnection(self, transport):
1971
protocol.Protocol.makeConnection(self, transport)
1972
self.connectedDeferred.callback(self)
1974
def connectionLost(self, reason):
1975
if reason.check(error.ConnectionDone):
1976
self.deferred.callback('connection done')
1978
self.deferred.errback(reason)
1980
#IFinishableConsumer stuff
1981
def write(self, data):
1982
self.transport.write(data)
1984
def registerProducer(self, producer, streaming):
1986
Register the given producer with our transport.
1988
self.transport.registerProducer(producer, streaming)
1990
def unregisterProducer(self):
1992
Unregister the previously registered producer.
1994
self.transport.unregisterProducer()
1997
self.transport.loseConnection()
2000
def decodeHostPort(line):
2001
"""Decode an FTP response specifying a host and port.
2003
@return: a 2-tuple of (host, port).
2005
abcdef = re.sub('[^0-9, ]', '', line)
2006
parsed = [int(p.strip()) for p in abcdef.split(',')]
2008
if x < 0 or x > 255:
2009
raise ValueError("Out of range", line, x)
2010
a, b, c, d, e, f = parsed
2011
host = "%s.%s.%s.%s" % (a, b, c, d)
2012
port = (int(e) << 8) + int(f)
2015
def encodeHostPort(host, port):
2016
numbers = host.split('.') + [str(port >> 8), str(port % 256)]
2017
return ','.join(numbers)
2019
def _unwrapFirstError(failure):
2020
failure.trap(defer.FirstError)
2021
return failure.value.subFailure
2023
class FTPDataPortFactory(protocol.ServerFactory):
2024
"""Factory for data connections that use the PORT command
2026
(i.e. "active" transfers)
2029
def buildProtocol(self, addr):
2030
# This is a bit hackish -- we already have a Protocol instance,
2031
# so just return it instead of making a new one
2032
# FIXME: Reject connections from the wrong address/port
2033
# (potential security problem)
2034
self.protocol.factory = self
2035
self.port.loseConnection()
2036
return self.protocol
2039
class FTPClientBasic(basic.LineReceiver):
2041
Foundations of an FTP client.
2046
self.actionQueue = []
2047
self.greeting = None
2048
self.nextDeferred = defer.Deferred().addCallback(self._cb_greeting)
2049
self.nextDeferred.addErrback(self.fail)
2053
def fail(self, error):
2055
Give an error to any queued deferreds.
2059
def _fail(self, error):
2061
Errback all queued deferreds.
2064
# We're recursing; bail out here for simplicity
2067
if self.nextDeferred:
2069
self.nextDeferred.errback(failure.Failure(ConnectionLost('FTP connection lost', error)))
2070
except defer.AlreadyCalledError:
2072
for ftpCommand in self.actionQueue:
2073
ftpCommand.fail(failure.Failure(ConnectionLost('FTP connection lost', error)))
2076
def _cb_greeting(self, greeting):
2077
self.greeting = greeting
2079
def sendLine(self, line):
2081
(Private) Sends a line, unless line is None.
2085
basic.LineReceiver.sendLine(self, line)
2087
def sendNextCommand(self):
2089
(Private) Processes the next command in the queue.
2091
ftpCommand = self.popCommandQueue()
2092
if ftpCommand is None:
2093
self.nextDeferred = None
2095
if not ftpCommand.ready:
2096
self.actionQueue.insert(0, ftpCommand)
2097
reactor.callLater(1.0, self.sendNextCommand)
2098
self.nextDeferred = None
2101
# FIXME: this if block doesn't belong in FTPClientBasic, it belongs in
2103
if ftpCommand.text == 'PORT':
2104
self.generatePortCommand(ftpCommand)
2107
log.msg('<-- %s' % ftpCommand.text)
2108
self.nextDeferred = ftpCommand.deferred
2109
self.sendLine(ftpCommand.text)
2111
def queueCommand(self, ftpCommand):
2113
Add an FTPCommand object to the queue.
2115
If it's the only thing in the queue, and we are connected and we aren't
2116
waiting for a response of an earlier command, the command will be sent
2119
@param ftpCommand: an L{FTPCommand}
2121
self.actionQueue.append(ftpCommand)
2122
if (len(self.actionQueue) == 1 and self.transport is not None and
2123
self.nextDeferred is None):
2124
self.sendNextCommand()
2126
def queueStringCommand(self, command, public=1):
2128
Queues a string to be issued as an FTP command
2130
@param command: string of an FTP command to queue
2131
@param public: a flag intended for internal use by FTPClient. Don't
2132
change it unless you know what you're doing.
2134
@return: a L{Deferred} that will be called when the response to the
2135
command has been received.
2137
ftpCommand = FTPCommand(command, public)
2138
self.queueCommand(ftpCommand)
2139
return ftpCommand.deferred
2141
def popCommandQueue(self):
2143
Return the front element of the command queue, or None if empty.
2145
if self.actionQueue:
2146
return self.actionQueue.pop(0)
2150
def queueLogin(self, username, password):
2152
Login: send the username, send the password.
2154
If the password is C{None}, the PASS command won't be sent. Also, if
2155
the response to the USER command has a response code of 230 (User logged
2156
in), then PASS won't be sent either.
2158
# Prepare the USER command
2160
userDeferred = self.queueStringCommand('USER ' + username, public=0)
2161
deferreds.append(userDeferred)
2163
# Prepare the PASS command (if a password is given)
2164
if password is not None:
2165
passwordCmd = FTPCommand('PASS ' + password, public=0)
2166
self.queueCommand(passwordCmd)
2167
deferreds.append(passwordCmd.deferred)
2169
# Avoid sending PASS if the response to USER is 230.
2170
# (ref: http://cr.yp.to/ftp/user.html#user)
2171
def cancelPasswordIfNotNeeded(response):
2172
if response[0].startswith('230'):
2173
# No password needed!
2174
self.actionQueue.remove(passwordCmd)
2176
userDeferred.addCallback(cancelPasswordIfNotNeeded)
2179
for deferred in deferreds:
2180
# If something goes wrong, call fail
2181
deferred.addErrback(self.fail)
2182
# But also swallow the error, so we don't cause spurious errors
2183
deferred.addErrback(lambda x: None)
2185
def lineReceived(self, line):
2187
(Private) Parses the response messages from the FTP server.
2189
# Add this line to the current response
2191
log.msg('--> %s' % line)
2192
self.response.append(line)
2194
# Bail out if this isn't the last line of a response
2195
# The last line of response starts with 3 digits followed by a space
2196
codeIsValid = re.match(r'\d{3} ', line)
2206
# Check that we were expecting a response
2207
if self.nextDeferred is None:
2208
self.fail(UnexpectedResponse(self.response))
2211
# Reset the response
2212
response = self.response
2215
# Look for a success or error code, and call the appropriate callback
2216
if code[0] in ('2', '3'):
2218
self.nextDeferred.callback(response)
2219
elif code[0] in ('4', '5'):
2221
self.nextDeferred.errback(failure.Failure(CommandFailed(response)))
2223
# This shouldn't happen unless something screwed up.
2224
log.msg('Server sent invalid response code %s' % (code,))
2225
self.nextDeferred.errback(failure.Failure(BadResponse(response)))
2227
# Run the next command
2228
self.sendNextCommand()
2230
def connectionLost(self, reason):
2235
class _PassiveConnectionFactory(protocol.ClientFactory):
2238
def __init__(self, protoInstance):
2239
self.protoInstance = protoInstance
2241
def buildProtocol(self, ignored):
2242
self.protoInstance.factory = self
2243
return self.protoInstance
2245
def clientConnectionFailed(self, connector, reason):
2246
e = FTPError('Connection Failed', reason)
2247
self.protoInstance.deferred.errback(e)
2251
class FTPClient(FTPClientBasic):
2253
L{FTPClient} is a client implementation of the FTP protocol which
2254
exposes FTP commands as methods which return L{Deferred}s.
2256
Each command method returns a L{Deferred} which is called back when a
2257
successful response code (2xx or 3xx) is received from the server or
2258
which is error backed if an error response code (4xx or 5xx) is received
2259
from the server or if a protocol violation occurs. If an error response
2260
code is received, the L{Deferred} fires with a L{Failure} wrapping a
2261
L{CommandFailed} instance. The L{CommandFailed} instance is created
2262
with a list of the response lines received from the server.
2264
See U{RFC 959<http://www.ietf.org/rfc/rfc959.txt>} for error code
2267
Both active and passive transfers are supported.
2269
@ivar passive: See description in __init__.
2271
connectFactory = reactor.connectTCP
2273
def __init__(self, username='anonymous',
2274
password='twisted@twistedmatrix.com',
2279
I will login as soon as I receive the welcome message from the server.
2281
@param username: FTP username
2282
@param password: FTP password
2283
@param passive: flag that controls if I use active or passive data
2284
connections. You can also change this after construction by
2285
assigning to C{self.passive}.
2287
FTPClientBasic.__init__(self)
2288
self.queueLogin(username, password)
2290
self.passive = passive
2292
def fail(self, error):
2294
Disconnect, and also give an error to any queued deferreds.
2296
self.transport.loseConnection()
2299
def receiveFromConnection(self, commands, protocol):
2301
Retrieves a file or listing generated by the given command,
2302
feeding it to the given protocol.
2304
@param commands: list of strings of FTP commands to execute then receive
2305
the results of (e.g. C{LIST}, C{RETR})
2306
@param protocol: A L{Protocol} B{instance} e.g. an
2307
L{FTPFileListProtocol}, or something that can be adapted to one.
2308
Typically this will be an L{IConsumer} implementation.
2310
@return: L{Deferred}.
2312
protocol = interfaces.IProtocol(protocol)
2313
wrapper = ProtocolWrapper(protocol, defer.Deferred())
2314
return self._openDataConnection(commands, wrapper)
2316
def queueLogin(self, username, password):
2318
Login: send the username, send the password, and
2319
set retrieval mode to binary
2321
FTPClientBasic.queueLogin(self, username, password)
2322
d = self.queueStringCommand('TYPE I', public=0)
2323
# If something goes wrong, call fail
2324
d.addErrback(self.fail)
2325
# But also swallow the error, so we don't cause spurious errors
2326
d.addErrback(lambda x: None)
2328
def sendToConnection(self, commands):
2332
@return: A tuple of two L{Deferred}s:
2333
- L{Deferred} L{IFinishableConsumer}. You must call
2334
the C{finish} method on the IFinishableConsumer when the file
2335
is completely transferred.
2336
- L{Deferred} list of control-connection responses.
2338
s = SenderProtocol()
2339
r = self._openDataConnection(commands, s)
2340
return (s.connectedDeferred, r)
2342
def _openDataConnection(self, commands, protocol):
2344
This method returns a DeferredList.
2346
cmds = [FTPCommand(command, public=1) for command in commands]
2347
cmdsDeferred = defer.DeferredList([cmd.deferred for cmd in cmds],
2348
fireOnOneErrback=True, consumeErrors=True)
2349
cmdsDeferred.addErrback(_unwrapFirstError)
2352
# Hack: use a mutable object to sneak a variable out of the
2353
# scope of doPassive
2355
def doPassive(response):
2356
"""Connect to the port specified in the response to PASV"""
2357
host, port = decodeHostPort(response[-1][4:])
2359
f = _PassiveConnectionFactory(protocol)
2360
_mutable[0] = self.connectFactory(host, port, f)
2362
pasvCmd = FTPCommand('PASV')
2363
self.queueCommand(pasvCmd)
2364
pasvCmd.deferred.addCallback(doPassive).addErrback(self.fail)
2366
results = [cmdsDeferred, pasvCmd.deferred, protocol.deferred]
2367
d = defer.DeferredList(results, fireOnOneErrback=True, consumeErrors=True)
2368
d.addErrback(_unwrapFirstError)
2370
# Ensure the connection is always closed
2371
def close(x, m=_mutable):
2372
m[0] and m[0].disconnect()
2377
# We just place a marker command in the queue, and will fill in
2378
# the host and port numbers later (see generatePortCommand)
2379
portCmd = FTPCommand('PORT')
2381
# Ok, now we jump through a few hoops here.
2382
# This is the problem: a transfer is not to be trusted as complete
2383
# until we get both the "226 Transfer complete" message on the
2384
# control connection, and the data socket is closed. Thus, we use
2385
# a DeferredList to make sure we only fire the callback at the
2388
portCmd.transferDeferred = protocol.deferred
2389
portCmd.protocol = protocol
2390
portCmd.deferred.addErrback(portCmd.transferDeferred.errback)
2391
self.queueCommand(portCmd)
2393
# Create dummy functions for the next callback to call.
2394
# These will also be replaced with real functions in
2395
# generatePortCommand.
2396
portCmd.loseConnection = lambda result: result
2397
portCmd.fail = lambda error: error
2399
# Ensure that the connection always gets closed
2400
cmdsDeferred.addErrback(lambda e, pc=portCmd: pc.fail(e) or e)
2402
results = [cmdsDeferred, portCmd.deferred, portCmd.transferDeferred]
2403
d = defer.DeferredList(results, fireOnOneErrback=True, consumeErrors=True)
2404
d.addErrback(_unwrapFirstError)
2407
self.queueCommand(cmd)
2410
def generatePortCommand(self, portCmd):
2412
(Private) Generates the text of a given PORT command.
2415
# The problem is that we don't create the listening port until we need
2416
# it for various reasons, and so we have to muck about to figure out
2417
# what interface and port it's listening on, and then finally we can
2418
# create the text of the PORT command to send to the FTP server.
2420
# FIXME: This method is far too ugly.
2422
# FIXME: The best solution is probably to only create the data port
2423
# once per FTPClient, and just recycle it for each new download.
2424
# This should be ok, because we don't pipeline commands.
2426
# Start listening on a port
2427
factory = FTPDataPortFactory()
2428
factory.protocol = portCmd.protocol
2429
listener = reactor.listenTCP(0, factory)
2430
factory.port = listener
2432
# Ensure we close the listening port if something goes wrong
2433
def listenerFail(error, listener=listener):
2434
if listener.connected:
2435
listener.loseConnection()
2437
portCmd.fail = listenerFail
2439
# Construct crufty FTP magic numbers that represent host & port
2440
host = self.transport.getHost().host
2441
port = listener.getHost().port
2442
portCmd.text = 'PORT ' + encodeHostPort(host, port)
2444
def escapePath(self, path):
2446
Returns a FTP escaped path (replace newlines with nulls).
2448
# Escape newline characters
2449
return path.replace('\n', '\0')
2451
def retrieveFile(self, path, protocol, offset=0):
2453
Retrieve a file from the given path
2455
This method issues the 'RETR' FTP command.
2457
The file is fed into the given Protocol instance. The data connection
2458
will be passive if self.passive is set.
2460
@param path: path to file that you wish to receive.
2461
@param protocol: a L{Protocol} instance.
2462
@param offset: offset to start downloading from
2464
@return: L{Deferred}
2466
cmds = ['RETR ' + self.escapePath(path)]
2468
cmds.insert(0, ('REST ' + str(offset)))
2469
return self.receiveFromConnection(cmds, protocol)
2473
def storeFile(self, path, offset=0):
2475
Store a file at the given path.
2477
This method issues the 'STOR' FTP command.
2479
@return: A tuple of two L{Deferred}s:
2480
- L{Deferred} L{IFinishableConsumer}. You must call
2481
the C{finish} method on the IFinishableConsumer when the file
2482
is completely transferred.
2483
- L{Deferred} list of control-connection responses.
2485
cmds = ['STOR ' + self.escapePath(path)]
2487
cmds.insert(0, ('REST ' + str(offset)))
2488
return self.sendToConnection(cmds)
2493
def rename(self, pathFrom, pathTo):
2497
This method issues the I{RNFR}/I{RNTO} command sequence to rename
2498
C{pathFrom} to C{pathTo}.
2500
@param: pathFrom: the absolute path to the file to be renamed
2501
@type pathFrom: C{str}
2503
@param: pathTo: the absolute path to rename the file to.
2504
@type pathTo: C{str}
2506
@return: A L{Deferred} which fires when the rename operation has
2507
succeeded or failed. If it succeeds, the L{Deferred} is called
2508
back with a two-tuple of lists. The first list contains the
2509
responses to the I{RNFR} command. The second list contains the
2510
responses to the I{RNTO} command. If either I{RNFR} or I{RNTO}
2511
fails, the L{Deferred} is errbacked with L{CommandFailed} or
2517
renameFrom = self.queueStringCommand('RNFR ' + self.escapePath(pathFrom))
2518
renameTo = self.queueStringCommand('RNTO ' + self.escapePath(pathTo))
2522
# Use a separate Deferred for the ultimate result so that Deferred
2523
# chaining can't interfere with its result.
2524
result = defer.Deferred()
2525
# Bundle up all the responses
2526
result.addCallback(lambda toResponse: (fromResponse, toResponse))
2528
def ebFrom(failure):
2529
# Make sure the RNTO doesn't run if the RNFR failed.
2530
self.popCommandQueue()
2531
result.errback(failure)
2533
# Save the RNFR response to pass to the result Deferred later
2534
renameFrom.addCallbacks(fromResponse.extend, ebFrom)
2536
# Hook up the RNTO to the result Deferred as well
2537
renameTo.chainDeferred(result)
2542
def list(self, path, protocol):
2544
Retrieve a file listing into the given protocol instance.
2546
This method issues the 'LIST' FTP command.
2548
@param path: path to get a file listing for.
2549
@param protocol: a L{Protocol} instance, probably a
2550
L{FTPFileListProtocol} instance. It can cope with most common file
2553
@return: L{Deferred}
2557
return self.receiveFromConnection(['LIST ' + self.escapePath(path)], protocol)
2560
def nlst(self, path, protocol):
2562
Retrieve a short file listing into the given protocol instance.
2564
This method issues the 'NLST' FTP command.
2566
NLST (should) return a list of filenames, one per line.
2568
@param path: path to get short file listing for.
2569
@param protocol: a L{Protocol} instance.
2573
return self.receiveFromConnection(['NLST ' + self.escapePath(path)], protocol)
2576
def cwd(self, path):
2578
Issues the CWD (Change Working Directory) command. It's also
2579
available as changeDirectory, which parses the result.
2581
@return: a L{Deferred} that will be called when done.
2583
return self.queueStringCommand('CWD ' + self.escapePath(path))
2586
def changeDirectory(self, path):
2588
Change the directory on the server and parse the result to determine
2589
if it was successful or not.
2592
@param path: The path to which to change.
2594
@return: a L{Deferred} which will be called back when the directory
2595
change has succeeded or errbacked if an error occurrs.
2598
"FTPClient.changeDirectory is deprecated in Twisted 8.2 and "
2599
"newer. Use FTPClient.cwd instead.",
2600
category=DeprecationWarning,
2603
def cbResult(result):
2604
if result[-1][:3] != '250':
2605
return failure.Failure(CommandFailed(result))
2607
return self.cwd(path).addCallback(cbResult)
2610
def makeDirectory(self, path):
2614
This method issues the MKD command.
2616
@param path: The path to the directory to create.
2619
@return: A L{Deferred} which fires when the server responds. If the
2620
directory is created, the L{Deferred} is called back with the
2621
server response. If the server response indicates the directory
2622
was not created, the L{Deferred} is errbacked with a L{Failure}
2623
wrapping L{CommandFailed} or L{BadResponse}.
2628
return self.queueStringCommand('MKD ' + self.escapePath(path))
2631
def removeFile(self, path):
2633
Delete a file on the server.
2635
L{removeFile} issues a I{DELE} command to the server to remove the
2636
indicated file. Note that this command cannot remove a directory.
2638
@param path: The path to the file to delete. May be relative to the
2642
@return: A L{Deferred} which fires when the server responds. On error,
2643
it is errbacked with either L{CommandFailed} or L{BadResponse}. On
2644
success, it is called back with a list of response lines.
2649
return self.queueStringCommand('DELE ' + self.escapePath(path))
2654
Issues the CDUP (Change Directory UP) command.
2656
@return: a L{Deferred} that will be called when done.
2658
return self.queueStringCommand('CDUP')
2663
Issues the PWD (Print Working Directory) command.
2665
The L{getDirectory} does the same job but automatically parses the
2668
@return: a L{Deferred} that will be called when done. It is up to the
2669
caller to interpret the response, but the L{parsePWDResponse} method
2670
in this module should work.
2672
return self.queueStringCommand('PWD')
2674
def getDirectory(self):
2676
Returns the current remote directory.
2678
@return: a L{Deferred} that will be called back with a C{str} giving
2679
the remote directory or which will errback with L{CommandFailed}
2680
if an error response is returned.
2682
def cbParse(result):
2684
# The only valid code is 257
2685
if int(result[0].split(' ', 1)[0]) != 257:
2687
except (IndexError, ValueError), e:
2688
return failure.Failure(CommandFailed(result))
2689
path = parsePWDResponse(result[0])
2691
return failure.Failure(CommandFailed(result))
2693
return self.pwd().addCallback(cbParse)
2698
Issues the I{QUIT} command.
2700
@return: A L{Deferred} that fires when the server acknowledges the
2701
I{QUIT} command. The transport should not be disconnected until
2702
this L{Deferred} fires.
2704
return self.queueStringCommand('QUIT')
2708
class FTPFileListProtocol(basic.LineReceiver):
2709
"""Parser for standard FTP file listings
2711
This is the evil required to match::
2713
-rw-r--r-- 1 root other 531 Jan 29 03:26 README
2715
If you need different evil for a wacky FTP server, you can
2716
override either C{fileLinePattern} or C{parseDirectoryLine()}.
2718
It populates the instance attribute self.files, which is a list containing
2719
dicts with the following keys (examples from the above line):
2720
- filetype: e.g. 'd' for directories, or '-' for an ordinary file
2721
- perms: e.g. 'rw-r--r--'
2723
- owner: e.g. 'root'
2724
- group: e.g. 'other'
2726
- date: e.g. 'Jan 29 03:26'
2727
- filename: e.g. 'README'
2728
- linktarget: e.g. 'some/file'
2730
Note that the 'date' value will be formatted differently depending on the
2731
date. Check U{http://cr.yp.to/ftp.html} if you really want to try to parse
2734
@ivar files: list of dicts describing the files in this listing
2736
fileLinePattern = re.compile(
2737
r'^(?P<filetype>.)(?P<perms>.{9})\s+(?P<nlinks>\d*)\s*'
2738
r'(?P<owner>\S+)\s+(?P<group>\S+)\s+(?P<size>\d+)\s+'
2739
r'(?P<date>...\s+\d+\s+[\d:]+)\s+(?P<filename>([^ ]|\\ )*?)'
2740
r'( -> (?P<linktarget>[^\r]*))?\r?$'
2747
def lineReceived(self, line):
2748
d = self.parseDirectoryLine(line)
2750
self.unknownLine(line)
2754
def parseDirectoryLine(self, line):
2755
"""Return a dictionary of fields, or None if line cannot be parsed.
2757
@param line: line of text expected to contain a directory entry
2762
match = self.fileLinePattern.match(line)
2766
d = match.groupdict()
2767
d['filename'] = d['filename'].replace(r'\ ', ' ')
2768
d['nlinks'] = int(d['nlinks'])
2769
d['size'] = int(d['size'])
2771
d['linktarget'] = d['linktarget'].replace(r'\ ', ' ')
2774
def addFile(self, info):
2775
"""Append file information dictionary to the list of known files.
2777
Subclasses can override or extend this method to handle file
2778
information differently without affecting the parsing of data
2781
@param info: dictionary containing the parsed representation
2782
of the file information
2785
self.files.append(info)
2787
def unknownLine(self, line):
2788
"""Deal with received lines which could not be parsed as file
2791
Subclasses can override this to perform any special processing
2794
@param line: unparsable line as received
2799
def parsePWDResponse(response):
2800
"""Returns the path from a response to a PWD command.
2802
Responses typically look like::
2804
257 "/home/andrew" is current directory.
2806
For this example, I will return C{'/home/andrew'}.
2808
If I can't find the path, I return C{None}.
2810
match = re.search('"(.*)"', response)
2812
return match.groups()[0]