Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(214)

Side by Side Diff: third_party/twisted_8_1/twisted/protocols/ftp.py

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

Powered by Google App Engine
This is Rietveld 408576698