~ntt-pf-lab/nova/monkey_patch_notification

« back to all changes in this revision

Viewing changes to vendor/Twisted-10.0.0/twisted/protocols/ftp.py

  • Committer: Jesse Andrews
  • Date: 2010-05-28 06:05:26 UTC
  • Revision ID: git-v1:bf6e6e718cdc7488e2da87b21e258ccc065fe499
initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- test-case-name: twisted.test.test_ftp -*-
 
2
# Copyright (c) 2001-2010 Twisted Matrix Laboratories.
 
3
# See LICENSE for details.
 
4
 
 
5
"""
 
6
An FTP protocol implementation
 
7
 
 
8
@author: Itamar Shtull-Trauring
 
9
@author: Jp Calderone
 
10
@author: Andrew Bennetts
 
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
import warnings
 
22
 
 
23
try:
 
24
    import pwd, grp
 
25
except ImportError:
 
26
    pwd = grp = None
 
27
 
 
28
from zope.interface import Interface, implements
 
29
 
 
30
# Twisted Imports
 
31
from twisted import copyright
 
32
from twisted.internet import reactor, interfaces, protocol, error, defer
 
33
from twisted.protocols import basic, policies
 
34
 
 
35
from twisted.python import log, failure, filepath
 
36
from twisted.python.compat import reduce
 
37
 
 
38
from twisted.cred import error as cred_error, portal, credentials, checkers
 
39
 
 
40
# constants
 
41
# response codes
 
42
 
 
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"
 
47
 
 
48
CMD_OK                                  = "200.1"
 
49
TYPE_SET_OK                             = "200.2"
 
50
ENTERING_PORT_MODE                      = "200.3"
 
51
CMD_NOT_IMPLMNTD_SUPERFLUOUS            = "202"
 
52
SYS_STATUS_OR_HELP_REPLY                = "211"
 
53
DIR_STATUS                              = "212"
 
54
FILE_STATUS                             = "213"
 
55
HELP_MSG                                = "214"
 
56
NAME_SYS_TYPE                           = "215"
 
57
SVC_READY_FOR_NEW_USER                  = "220.1"
 
58
WELCOME_MSG                             = "220.2"
 
59
SVC_CLOSING_CTRL_CNX                    = "221"
 
60
GOODBYE_MSG                             = "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"
 
69
PWD_REPLY                               = "257.1"
 
70
MKD_REPLY                               = "257.2"
 
71
 
 
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"
 
76
 
 
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"
 
84
 
 
85
SYNTAX_ERR                              = "500"
 
86
SYNTAX_ERR_IN_ARGS                      = "501"
 
87
CMD_NOT_IMPLMNTD                        = "502"
 
88
BAD_CMD_SEQ                             = "503"
 
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"
 
98
FILE_EXISTS                             = "550.6"
 
99
IS_A_DIR                                = "550.7"
 
100
PAGE_TYPE_UNK                           = "551"
 
101
EXCEEDED_STORAGE_ALLOC                  = "552"
 
102
FILENAME_NOT_ALLOWED                    = "553"
 
103
 
 
104
 
 
105
RESPONSE = {
 
106
    # -- 100's --
 
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.',
 
111
 
 
112
    # -- 200's --
 
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',
 
135
 
 
136
    # -- 300's --
 
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.',
 
140
 
 
141
    REQ_FILE_ACTN_PENDING_FURTHER_INFO: '350 Requested file action pending further information.',
 
142
 
 
143
# -- 400's --
 
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.',
 
148
 
 
149
    REQ_ACTN_ABRTD_LOCAL_ERR:           '451 Requested action aborted. Local error in processing.',
 
150
 
 
151
 
 
152
    # -- 500's --
 
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'
 
170
}
 
171
 
 
172
 
 
173
 
 
174
class InvalidPath(Exception):
 
175
    """
 
176
    Internal exception used to signify an error during parsing a path.
 
177
    """
 
178
 
 
179
 
 
180
 
 
181
def toSegments(cwd, path):
 
182
    """
 
183
    Normalize a path, as represented by a list of strings each
 
184
    representing one segment of the path.
 
185
    """
 
186
    if path.startswith('/'):
 
187
        segs = []
 
188
    else:
 
189
        segs = cwd[:]
 
190
 
 
191
    for s in path.split('/'):
 
192
        if s == '.' or s == '':
 
193
            continue
 
194
        elif s == '..':
 
195
            if segs:
 
196
                segs.pop()
 
197
            else:
 
198
                raise InvalidPath(cwd, path)
 
199
        elif '\0' in s or '/' in s:
 
200
            raise InvalidPath(cwd, path)
 
201
        else:
 
202
            segs.append(s)
 
203
    return segs
 
204
 
 
205
 
 
206
def errnoToFailure(e, path):
 
207
    """
 
208
    Map C{OSError} and C{IOError} to standard FTP errors.
 
209
    """
 
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))
 
220
    else:
 
221
        return defer.fail()
 
222
 
 
223
 
 
224
 
 
225
class FTPCmdError(Exception):
 
226
    """
 
227
    Generic exception for FTP commands.
 
228
    """
 
229
    def __init__(self, *msg):
 
230
        Exception.__init__(self, *msg)
 
231
        self.errorMessage = msg
 
232
 
 
233
 
 
234
    def response(self):
 
235
        """
 
236
        Generate a FTP response message for this error.
 
237
        """
 
238
        return RESPONSE[self.errorCode] % self.errorMessage
 
239
 
 
240
 
 
241
 
 
242
class FileNotFoundError(FTPCmdError):
 
243
    """
 
244
    Raised when trying to access a non existent file or directory.
 
245
    """
 
246
    errorCode = FILE_NOT_FOUND
 
247
 
 
248
 
 
249
 
 
250
class AnonUserDeniedError(FTPCmdError):
 
251
    """
 
252
    Raised when an anonymous user issues a command that will alter the
 
253
    filesystem
 
254
    """
 
255
    def __init__(self):
 
256
        # No message
 
257
        FTPCmdError.__init__(self, None)
 
258
 
 
259
    errorCode = ANON_USER_DENIED
 
260
 
 
261
 
 
262
 
 
263
class PermissionDeniedError(FTPCmdError):
 
264
    """
 
265
    Raised when access is attempted to a resource to which access is
 
266
    not allowed.
 
267
    """
 
268
    errorCode = PERMISSION_DENIED
 
269
 
 
270
 
 
271
 
 
272
class IsNotADirectoryError(FTPCmdError):
 
273
    """
 
274
    Raised when RMD is called on a path that isn't a directory.
 
275
    """
 
276
    errorCode = IS_NOT_A_DIR
 
277
 
 
278
 
 
279
 
 
280
class FileExistsError(FTPCmdError):
 
281
    """
 
282
    Raised when attempted to override an existing resource.
 
283
    """
 
284
    errorCode = FILE_EXISTS
 
285
 
 
286
 
 
287
 
 
288
class IsADirectoryError(FTPCmdError):
 
289
    """
 
290
    Raised when DELE is called on a path that is a directory.
 
291
    """
 
292
    errorCode = IS_A_DIR
 
293
 
 
294
 
 
295
 
 
296
class CmdSyntaxError(FTPCmdError):
 
297
    """
 
298
    Raised when a command syntax is wrong.
 
299
    """
 
300
    errorCode = SYNTAX_ERR
 
301
 
 
302
 
 
303
 
 
304
class CmdArgSyntaxError(FTPCmdError):
 
305
    """
 
306
    Raised when a command is called with wrong value or a wrong number of
 
307
    arguments.
 
308
    """
 
309
    errorCode = SYNTAX_ERR_IN_ARGS
 
310
 
 
311
 
 
312
 
 
313
class CmdNotImplementedError(FTPCmdError):
 
314
    """
 
315
    Raised when an unimplemented command is given to the server.
 
316
    """
 
317
    errorCode = CMD_NOT_IMPLMNTD
 
318
 
 
319
 
 
320
 
 
321
class CmdNotImplementedForArgError(FTPCmdError):
 
322
    """
 
323
    Raised when the handling of a parameter for a command is not implemented by
 
324
    the server.
 
325
    """
 
326
    errorCode = CMD_NOT_IMPLMNTD_FOR_PARAM
 
327
 
 
328
 
 
329
 
 
330
class FTPError(Exception):
 
331
    pass
 
332
 
 
333
 
 
334
 
 
335
class PortConnectionError(Exception):
 
336
    pass
 
337
 
 
338
 
 
339
 
 
340
class BadCmdSequenceError(FTPCmdError):
 
341
    """
 
342
    Raised when a client sends a series of commands in an illogical sequence.
 
343
    """
 
344
    errorCode = BAD_CMD_SEQ
 
345
 
 
346
 
 
347
 
 
348
class AuthorizationError(FTPCmdError):
 
349
    """
 
350
    Raised when client authentication fails.
 
351
    """
 
352
    errorCode = AUTH_FAILURE
 
353
 
 
354
 
 
355
 
 
356
def debugDeferred(self, *_):
 
357
    log.msg('debugDeferred(): %s' % str(_), debug=True)
 
358
 
 
359
 
 
360
# -- DTP Protocol --
 
361
 
 
362
 
 
363
_months = [
 
364
    None,
 
365
    'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
 
366
    'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
 
367
 
 
368
 
 
369
class DTP(object, protocol.Protocol):
 
370
    implements(interfaces.IConsumer)
 
371
 
 
372
    isConnected = False
 
373
 
 
374
    _cons = None
 
375
    _onConnLost = None
 
376
    _buffer = None
 
377
 
 
378
    def connectionMade(self):
 
379
        self.isConnected = True
 
380
        self.factory.deferred.callback(None)
 
381
        self._buffer = []
 
382
 
 
383
    def connectionLost(self, reason):
 
384
        self.isConnected = False
 
385
        if self._onConnLost is not None:
 
386
            self._onConnLost.callback(None)
 
387
 
 
388
    def sendLine(self, line):
 
389
        self.transport.write(line + '\r\n')
 
390
 
 
391
 
 
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)])
 
395
 
 
396
        def formatDate(mtime):
 
397
            now = time.gmtime()
 
398
            info = {
 
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
 
404
                }
 
405
            if now.tm_year != mtime.tm_year:
 
406
                return '%(month)s %(day)02d %(year)5d' % info
 
407
            else:
 
408
                return '%(month)s %(day)02d %(hour)02d:%(minute)02d' % info
 
409
 
 
410
        format = ('%(directory)s%(permissions)s%(hardlinks)4d '
 
411
                  '%(owner)-9s %(group)-9s %(size)15d %(date)12s '
 
412
                  '%(name)s')
 
413
 
 
414
        return format % {
 
415
            'directory': directory and 'd' or '-',
 
416
            'permissions': formatMode(permissions),
 
417
            'hardlinks': hardlinks,
 
418
            'owner': owner[:8],
 
419
            'group': group[:8],
 
420
            'size': size,
 
421
            'date': formatDate(time.gmtime(modified)),
 
422
            'name': name}
 
423
 
 
424
    def sendListResponse(self, name, response):
 
425
        self.sendLine(self._formatOneListResponse(name, *response))
 
426
 
 
427
 
 
428
    # Proxy IConsumer to our transport
 
429
    def registerProducer(self, producer, streaming):
 
430
        return self.transport.registerProducer(producer, streaming)
 
431
 
 
432
    def unregisterProducer(self):
 
433
        self.transport.unregisterProducer()
 
434
        self.transport.loseConnection()
 
435
 
 
436
    def write(self, data):
 
437
        if self.isConnected:
 
438
            return self.transport.write(data)
 
439
        raise Exception("Crap damn crap damn crap damn")
 
440
 
 
441
 
 
442
    # Pretend to be a producer, too.
 
443
    def _conswrite(self, bytes):
 
444
        try:
 
445
            self._cons.write(bytes)
 
446
        except:
 
447
            self._onConnLost.errback()
 
448
 
 
449
    def dataReceived(self, bytes):
 
450
        if self._cons is not None:
 
451
            self._conswrite(bytes)
 
452
        else:
 
453
            self._buffer.append(bytes)
 
454
 
 
455
    def _unregConsumer(self, ignored):
 
456
        self._cons.unregisterProducer()
 
457
        self._cons = None
 
458
        del self._onConnLost
 
459
        return ignored
 
460
 
 
461
    def registerConsumer(self, cons):
 
462
        assert self._cons is None
 
463
        self._cons = cons
 
464
        self._cons.registerProducer(self, True)
 
465
        for chunk in self._buffer:
 
466
            self._conswrite(chunk)
 
467
        self._buffer = None
 
468
        if self.isConnected:
 
469
            self._onConnLost = d = defer.Deferred()
 
470
            d.addBoth(self._unregConsumer)
 
471
            return d
 
472
        else:
 
473
            self._cons.unregisterProducer()
 
474
            self._cons = None
 
475
            return defer.succeed(None)
 
476
 
 
477
    def resumeProducing(self):
 
478
        self.transport.resumeProducing()
 
479
 
 
480
    def pauseProducing(self):
 
481
        self.transport.pauseProducing()
 
482
 
 
483
    def stopProducing(self):
 
484
        self.transport.stopProducing()
 
485
 
 
486
class DTPFactory(protocol.ClientFactory):
 
487
    """
 
488
    Client factory for I{data transfer process} protocols.
 
489
 
 
490
    @ivar peerCheck: perform checks to make sure the ftp-pi's peer is the same
 
491
        as the dtp's
 
492
    @ivar pi: a reference to this factory's protocol interpreter
 
493
 
 
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
 
497
        L{_FINISHED}.
 
498
    """
 
499
 
 
500
    _IN_PROGRESS = object()
 
501
    _FAILED = object()
 
502
    _FINISHED = object()
 
503
 
 
504
    _state = _IN_PROGRESS
 
505
 
 
506
    # -- configuration variables --
 
507
    peerCheck = False
 
508
 
 
509
    # -- class variables --
 
510
    def __init__(self, pi, peerHost=None, reactor=None):
 
511
        """Constructor
 
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
 
515
        """
 
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
 
520
        if reactor is None:
 
521
            from twisted.internet import reactor
 
522
        self._reactor = reactor
 
523
 
 
524
 
 
525
    def buildProtocol(self, addr):
 
526
        log.msg('DTPFactory.buildProtocol', debug=True)
 
527
 
 
528
        if self._state is not self._IN_PROGRESS:
 
529
            return None
 
530
        self._state = self._FINISHED
 
531
 
 
532
        self.cancelTimeout()
 
533
        p = DTP()
 
534
        p.factory = self
 
535
        p.pi = self.pi
 
536
        self.pi.dtpInstance = p
 
537
        return p
 
538
 
 
539
 
 
540
    def stopFactory(self):
 
541
        log.msg('dtpFactory.stopFactory', debug=True)
 
542
        self.cancelTimeout()
 
543
 
 
544
 
 
545
    def timeoutFactory(self):
 
546
        log.msg('timed out waiting for DTP connection')
 
547
        if self._state is not self._IN_PROGRESS:
 
548
            return
 
549
        self._state = self._FAILED
 
550
 
 
551
        d = self.deferred
 
552
        self.deferred = None
 
553
        d.errback(
 
554
            PortConnectionError(defer.TimeoutError("DTPFactory timeout")))
 
555
 
 
556
 
 
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()
 
561
 
 
562
 
 
563
    def setTimeout(self, seconds):
 
564
        log.msg('DTPFactory.setTimeout set to %s seconds' % seconds)
 
565
        self.delayedCall = self._reactor.callLater(seconds, self.timeoutFactory)
 
566
 
 
567
 
 
568
    def clientConnectionFailed(self, connector, reason):
 
569
        if self._state is not self._IN_PROGRESS:
 
570
            return
 
571
        self._state = self._FAILED
 
572
        d = self.deferred
 
573
        self.deferred = None
 
574
        d.errback(PortConnectionError(reason))
 
575
 
 
576
 
 
577
# -- FTP-PI (Protocol Interpreter) --
 
578
 
 
579
class ASCIIConsumerWrapper(object):
 
580
    def __init__(self, cons):
 
581
        self.cons = cons
 
582
        self.registerProducer = cons.registerProducer
 
583
        self.unregisterProducer = cons.unregisterProducer
 
584
 
 
585
        assert os.linesep == "\r\n" or len(os.linesep) == 1, "Unsupported platform (yea right like this even exists)"
 
586
 
 
587
        if os.linesep == "\r\n":
 
588
            self.write = cons.write
 
589
 
 
590
    def write(self, bytes):
 
591
        return self.cons.write(bytes.replace(os.linesep, "\r\n"))
 
592
 
 
593
 
 
594
 
 
595
class FileConsumer(object):
 
596
    """
 
597
    A consumer for FTP input that writes data to a file.
 
598
 
 
599
    @ivar fObj: a file object opened for writing, used to write data received.
 
600
    @type fObj: C{file}
 
601
    """
 
602
 
 
603
    implements(interfaces.IConsumer)
 
604
 
 
605
    def __init__(self, fObj):
 
606
        self.fObj = fObj
 
607
 
 
608
 
 
609
    def registerProducer(self, producer, streaming):
 
610
        self.producer = producer
 
611
        assert streaming
 
612
 
 
613
 
 
614
    def unregisterProducer(self):
 
615
        self.producer = None
 
616
        self.fObj.close()
 
617
 
 
618
 
 
619
    def write(self, bytes):
 
620
        self.fObj.write(bytes)
 
621
 
 
622
 
 
623
 
 
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()
 
629
 
 
630
 
 
631
class FTP(object, basic.LineReceiver, policies.TimeoutMixin):
 
632
    """
 
633
    Protocol Interpreter for the File Transfer Protocol
 
634
 
 
635
    @ivar state: The current server state.  One of L{UNAUTH},
 
636
        L{INAUTH}, L{AUTHED}, L{RENAMING}.
 
637
 
 
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).
 
645
 
 
646
    @ivar passivePortRange: iterator used as source of passive port numbers.
 
647
    @type passivePortRange: C{iterator}
 
648
    """
 
649
 
 
650
    disconnected = False
 
651
 
 
652
    # States an FTP can be in
 
653
    UNAUTH, INAUTH, AUTHED, RENAMING = range(4)
 
654
 
 
655
    # how long the DTP waits for a connection
 
656
    dtpTimeout = 10
 
657
 
 
658
    portal = None
 
659
    shell = None
 
660
    dtpFactory = None
 
661
    dtpPort = None
 
662
    dtpInstance = None
 
663
    binary = True
 
664
 
 
665
    passivePortRange = xrange(0, 1)
 
666
 
 
667
    listenFactory = reactor.listenTCP
 
668
 
 
669
    def reply(self, key, *args):
 
670
        msg = RESPONSE[key] % args
 
671
        self.sendLine(msg)
 
672
 
 
673
 
 
674
    def connectionMade(self):
 
675
        self.state = self.UNAUTH
 
676
        self.setTimeout(self.timeOut)
 
677
        self.reply(WELCOME_MSG, self.factory.welcomeMessage)
 
678
 
 
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
 
683
        if self.dtpFactory:
 
684
            self.cleanupDTP()
 
685
        self.setTimeout(None)
 
686
        if hasattr(self.shell, 'logout') and self.shell.logout is not None:
 
687
            self.shell.logout()
 
688
        self.shell = None
 
689
        self.transport = None
 
690
 
 
691
    def timeoutConnection(self):
 
692
        self.transport.loseConnection()
 
693
 
 
694
    def lineReceived(self, line):
 
695
        self.resetTimeout()
 
696
        self.pauseProducing()
 
697
 
 
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,))
 
704
            else:
 
705
                log.msg("Unexpected FTP error")
 
706
                log.err(err)
 
707
                self.reply(REQ_ACTN_NOT_TAKEN, "internal server error")
 
708
 
 
709
        def processSucceeded(result):
 
710
            if isinstance(result, tuple):
 
711
                self.reply(*result)
 
712
            elif result is not None:
 
713
                self.reply(result)
 
714
 
 
715
        def allDone(ignored):
 
716
            if not self.disconnected:
 
717
                self.resumeProducing()
 
718
 
 
719
        spaceIndex = line.find(' ')
 
720
        if spaceIndex != -1:
 
721
            cmd = line[:spaceIndex]
 
722
            args = (line[spaceIndex + 1:],)
 
723
        else:
 
724
            cmd = line
 
725
            args = ()
 
726
        d = defer.maybeDeferred(self.processCommand, cmd, *args)
 
727
        d.addCallbacks(processSucceeded, processFailed)
 
728
        d.addErrback(log.err)
 
729
 
 
730
        # XXX It burnsss
 
731
        # LineReceiver doesn't let you resumeProducing inside
 
732
        # lineReceived atm
 
733
        from twisted.internet import reactor
 
734
        reactor.callLater(0, d.addBoth, allDone)
 
735
 
 
736
 
 
737
    def processCommand(self, cmd, *params):
 
738
        cmd = cmd.upper()
 
739
 
 
740
        if self.state == self.UNAUTH:
 
741
            if cmd == 'USER':
 
742
                return self.ftp_USER(*params)
 
743
            elif cmd == 'PASS':
 
744
                return BAD_CMD_SEQ, "USER required before PASS"
 
745
            else:
 
746
                return NOT_LOGGED_IN
 
747
 
 
748
        elif self.state == self.INAUTH:
 
749
            if cmd == 'PASS':
 
750
                return self.ftp_PASS(*params)
 
751
            else:
 
752
                return BAD_CMD_SEQ, "PASS required after USER"
 
753
 
 
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))
 
759
 
 
760
        elif self.state == self.RENAMING:
 
761
            if cmd == 'RNTO':
 
762
                return self.ftp_RNTO(*params)
 
763
            else:
 
764
                return BAD_CMD_SEQ, "RNTO required after RNFR"
 
765
 
 
766
 
 
767
    def getDTPPort(self, factory):
 
768
        """
 
769
        Return a port for passive access, using C{self.passivePortRange}
 
770
        attribute.
 
771
        """
 
772
        for portn in self.passivePortRange:
 
773
            try:
 
774
                dtpPort = self.listenFactory(portn, factory)
 
775
            except error.CannotListenError:
 
776
                continue
 
777
            else:
 
778
                return dtpPort
 
779
        raise error.CannotListenError('', portn,
 
780
            "No port available in range %s" %
 
781
            (self.passivePortRange,))
 
782
 
 
783
 
 
784
    def ftp_USER(self, username):
 
785
        """
 
786
        First part of login.  Get the username the peer wants to
 
787
        authenticate as.
 
788
        """
 
789
        if not username:
 
790
            return defer.fail(CmdSyntaxError('USER requires an argument'))
 
791
 
 
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
 
796
        else:
 
797
            return (USR_NAME_OK_NEED_PASS, username)
 
798
 
 
799
    # TODO: add max auth try before timeout from ip...
 
800
    # TODO: need to implement minimal ABOR command
 
801
 
 
802
    def ftp_PASS(self, password):
 
803
        """
 
804
        Second part of login.  Get the password the peer wants to
 
805
        authenticate with.
 
806
        """
 
807
        if self.factory.allowAnonymous and self._user == self.factory.userAnonymous:
 
808
            # anonymous login
 
809
            creds = credentials.Anonymous()
 
810
            reply = GUEST_LOGGED_IN_PROCEED
 
811
        else:
 
812
            # user login
 
813
            creds = credentials.UsernamePassword(self._user, password)
 
814
            reply = USR_LOGGED_IN_PROCEED
 
815
        del self._user
 
816
 
 
817
        def _cbLogin((interface, avatar, logout)):
 
818
            assert interface is IFTPShell, "The realm is busted, jerk."
 
819
            self.shell = avatar
 
820
            self.logout = logout
 
821
            self.workingDirectory = []
 
822
            self.state = self.AUTHED
 
823
            return reply
 
824
 
 
825
        def _ebLogin(failure):
 
826
            failure.trap(cred_error.UnauthorizedLogin, cred_error.UnhandledCredentials)
 
827
            self.state = self.UNAUTH
 
828
            raise AuthorizationError
 
829
 
 
830
        d = self.portal.login(creds, None, IFTPShell)
 
831
        d.addCallbacks(_cbLogin, _ebLogin)
 
832
        return d
 
833
 
 
834
 
 
835
    def ftp_PASV(self):
 
836
        """Request for a passive connection
 
837
 
 
838
        from the rfc::
 
839
 
 
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.
 
845
        """
 
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.
 
850
            self.cleanupDTP()
 
851
        self.dtpFactory = DTPFactory(pi=self)
 
852
        self.dtpFactory.setTimeout(self.dtpTimeout)
 
853
        self.dtpPort = self.getDTPPort(self.dtpFactory)
 
854
 
 
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)
 
859
 
 
860
 
 
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]
 
865
 
 
866
        # if we have a DTP port set up, lose it.
 
867
        if self.dtpFactory is not None:
 
868
            self.cleanupDTP()
 
869
 
 
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)
 
873
 
 
874
        def connected(ignored):
 
875
            return ENTERING_PORT_MODE
 
876
        def connFailed(err):
 
877
            err.trap(PortConnectionError)
 
878
            return CANT_OPEN_DATA_CNX
 
879
        return self.dtpFactory.deferred.addCallbacks(connected, connFailed)
 
880
 
 
881
 
 
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
 
889
        default directory.
 
890
        """
 
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'))
 
894
 
 
895
        # bug in konqueror
 
896
        if path == "-a":
 
897
            path = ''
 
898
        # bug in gFTP 2.0.15
 
899
        if path == "-aL":
 
900
            path = ''
 
901
        # bug in Nautilus 2.10.0
 
902
        if path == "-L":
 
903
            path = ''
 
904
        # bug in ange-ftp
 
905
        if path == "-la":
 
906
            path = ''
 
907
 
 
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,)
 
914
 
 
915
        try:
 
916
            segments = toSegments(self.workingDirectory, path)
 
917
        except InvalidPath, e:
 
918
            return defer.fail(FileNotFoundError(path))
 
919
 
 
920
        d = self.shell.list(
 
921
            segments,
 
922
            ('size', 'directory', 'permissions', 'hardlinks',
 
923
             'modified', 'owner', 'group'))
 
924
        d.addCallback(gotListing)
 
925
        return d
 
926
 
 
927
 
 
928
    def ftp_NLST(self, path):
 
929
        """
 
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.
 
935
 
 
936
        @type path: C{str}
 
937
        @param path: The path for which a directory listing should be returned.
 
938
 
 
939
        @rtype: L{Deferred}
 
940
        @return: a L{Deferred} which will be fired when the listing request
 
941
            is finished.
 
942
        """
 
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:
 
945
            return defer.fail(
 
946
                BadCmdSequenceError('must send PORT or PASV before RETR'))
 
947
 
 
948
        try:
 
949
            segments = toSegments(self.workingDirectory, path)
 
950
        except InvalidPath, e:
 
951
            return defer.fail(FileNotFoundError(path))
 
952
 
 
953
        def cbList(results):
 
954
            """
 
955
            Send, line by line, each file in the directory listing, and then
 
956
            close the connection.
 
957
 
 
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.
 
961
 
 
962
            @rtype: C{tuple}
 
963
            @return: A C{tuple} containing the status code for a successful
 
964
                transfer.
 
965
            """
 
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,)
 
971
 
 
972
        def cbGlob(results):
 
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,)
 
979
 
 
980
        def listErr(results):
 
981
            """
 
982
            RFC 959 specifies that an NLST request may only return directory
 
983
            listings. Thus, send nothing and just close the connection.
 
984
 
 
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
 
988
                directory.
 
989
 
 
990
            @rtype: C{tuple}
 
991
            @returns: A C{tuple} containing the status code for a successful
 
992
                transfer.
 
993
            """
 
994
            self.dtpInstance.transport.loseConnection()
 
995
            return (TXFR_COMPLETE_OK,)
 
996
 
 
997
        # XXX This globbing may be incomplete: see #4181
 
998
        if segments and (
 
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)
 
1003
        else:
 
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)
 
1008
        return d
 
1009
 
 
1010
 
 
1011
    def ftp_CWD(self, path):
 
1012
        try:
 
1013
            segments = toSegments(self.workingDirectory, path)
 
1014
        except InvalidPath, e:
 
1015
            # XXX Eh, what to fail with here?
 
1016
            return defer.fail(FileNotFoundError(path))
 
1017
 
 
1018
        def accessGranted(result):
 
1019
            self.workingDirectory = segments
 
1020
            return (REQ_FILE_ACTN_COMPLETED_OK,)
 
1021
 
 
1022
        return self.shell.access(segments).addCallback(accessGranted)
 
1023
 
 
1024
 
 
1025
    def ftp_CDUP(self):
 
1026
        return self.ftp_CWD('..')
 
1027
 
 
1028
 
 
1029
    def ftp_PWD(self):
 
1030
        return (PWD_REPLY, '/' + '/'.join(self.workingDirectory))
 
1031
 
 
1032
 
 
1033
    def ftp_RETR(self, path):
 
1034
        if self.dtpInstance is None:
 
1035
            raise BadCmdSequenceError('PORT or PASV required before RETR')
 
1036
 
 
1037
        try:
 
1038
            newsegs = toSegments(self.workingDirectory, path)
 
1039
        except InvalidPath:
 
1040
            return defer.fail(FileNotFoundError(path))
 
1041
 
 
1042
        # XXX For now, just disable the timeout.  Later we'll want to
 
1043
        # leave it active and have the DTP connection reset it
 
1044
        # periodically.
 
1045
        self.setTimeout(None)
 
1046
 
 
1047
        # Put it back later
 
1048
        def enableTimeout(result):
 
1049
            self.setTimeout(self.factory.timeOut)
 
1050
            return result
 
1051
 
 
1052
        # And away she goes
 
1053
        if not self.binary:
 
1054
            cons = ASCIIConsumerWrapper(self.dtpInstance)
 
1055
        else:
 
1056
            cons = self.dtpInstance
 
1057
 
 
1058
        def cbSent(result):
 
1059
            return (TXFR_COMPLETE_OK,)
 
1060
 
 
1061
        def ebSent(err):
 
1062
            log.msg("Unexpected error attempting to transmit file to client:")
 
1063
            log.err(err)
 
1064
            return (CNX_CLOSED_TXFR_ABORTED,)
 
1065
 
 
1066
        def cbOpened(file):
 
1067
            # Tell them what to doooo
 
1068
            if self.dtpInstance.isConnected:
 
1069
                self.reply(DATA_CNX_ALREADY_OPEN_START_XFR)
 
1070
            else:
 
1071
                self.reply(FILE_STATUS_OK_OPEN_DATA_CNX)
 
1072
 
 
1073
            d = file.send(cons)
 
1074
            d.addCallbacks(cbSent, ebSent)
 
1075
            return d
 
1076
 
 
1077
        def ebOpened(err):
 
1078
            if not err.check(PermissionDeniedError, FileNotFoundError, IsNotADirectoryError):
 
1079
                log.msg("Unexpected error attempting to open file for transmission:")
 
1080
                log.err(err)
 
1081
            if err.check(FTPCmdError):
 
1082
                return (err.value.errorCode, '/'.join(newsegs))
 
1083
            return (FILE_NOT_FOUND, '/'.join(newsegs))
 
1084
 
 
1085
        d = self.shell.openForReading(newsegs)
 
1086
        d.addCallbacks(cbOpened, ebOpened)
 
1087
        d.addBoth(enableTimeout)
 
1088
 
 
1089
        # Pass back Deferred that fires when the transfer is done
 
1090
        return d
 
1091
 
 
1092
 
 
1093
    def ftp_STOR(self, path):
 
1094
        if self.dtpInstance is None:
 
1095
            raise BadCmdSequenceError('PORT or PASV required before STOR')
 
1096
 
 
1097
        try:
 
1098
            newsegs = toSegments(self.workingDirectory, path)
 
1099
        except InvalidPath:
 
1100
            return defer.fail(FileNotFoundError(path))
 
1101
 
 
1102
        # XXX For now, just disable the timeout.  Later we'll want to
 
1103
        # leave it active and have the DTP connection reset it
 
1104
        # periodically.
 
1105
        self.setTimeout(None)
 
1106
 
 
1107
        # Put it back later
 
1108
        def enableTimeout(result):
 
1109
            self.setTimeout(self.factory.timeOut)
 
1110
            return result
 
1111
 
 
1112
        def cbSent(result):
 
1113
            return (TXFR_COMPLETE_OK,)
 
1114
 
 
1115
        def ebSent(err):
 
1116
            log.msg("Unexpected error receiving file from client:")
 
1117
            log.err(err)
 
1118
            return (CNX_CLOSED_TXFR_ABORTED,)
 
1119
 
 
1120
        def cbConsumer(cons):
 
1121
            if not self.binary:
 
1122
                cons = ASCIIConsumerWrapper(cons)
 
1123
 
 
1124
            d = self.dtpInstance.registerConsumer(cons)
 
1125
            d.addCallbacks(cbSent, ebSent)
 
1126
 
 
1127
            # Tell them what to doooo
 
1128
            if self.dtpInstance.isConnected:
 
1129
                self.reply(DATA_CNX_ALREADY_OPEN_START_XFR)
 
1130
            else:
 
1131
                self.reply(FILE_STATUS_OK_OPEN_DATA_CNX)
 
1132
 
 
1133
            return d
 
1134
 
 
1135
        def cbOpened(file):
 
1136
            d = file.receive()
 
1137
            d.addCallback(cbConsumer)
 
1138
            return d
 
1139
 
 
1140
        def ebOpened(err):
 
1141
            if not err.check(PermissionDeniedError, FileNotFoundError, IsNotADirectoryError):
 
1142
                log.msg("Unexpected error attempting to open file for upload:")
 
1143
                log.err(err)
 
1144
            if isinstance(err.value, FTPCmdError):
 
1145
                return (err.value.errorCode, '/'.join(newsegs))
 
1146
            return (FILE_NOT_FOUND, '/'.join(newsegs))
 
1147
 
 
1148
        d = self.shell.openForWriting(newsegs)
 
1149
        d.addCallbacks(cbOpened, ebOpened)
 
1150
        d.addBoth(enableTimeout)
 
1151
 
 
1152
        # Pass back Deferred that fires when the transfer is done
 
1153
        return d
 
1154
 
 
1155
 
 
1156
    def ftp_SIZE(self, path):
 
1157
        try:
 
1158
            newsegs = toSegments(self.workingDirectory, path)
 
1159
        except InvalidPath:
 
1160
            return defer.fail(FileNotFoundError(path))
 
1161
 
 
1162
        def cbStat((size,)):
 
1163
            return (FILE_STATUS, str(size))
 
1164
 
 
1165
        return self.shell.stat(newsegs, ('size',)).addCallback(cbStat)
 
1166
 
 
1167
 
 
1168
    def ftp_MDTM(self, path):
 
1169
        try:
 
1170
            newsegs = toSegments(self.workingDirectory, path)
 
1171
        except InvalidPath:
 
1172
            return defer.fail(FileNotFoundError(path))
 
1173
 
 
1174
        def cbStat((modified,)):
 
1175
            return (FILE_STATUS, time.strftime('%Y%m%d%H%M%S', time.gmtime(modified)))
 
1176
 
 
1177
        return self.shell.stat(newsegs, ('modified',)).addCallback(cbStat)
 
1178
 
 
1179
 
 
1180
    def ftp_TYPE(self, type):
 
1181
        p = type.upper()
 
1182
        if p:
 
1183
            f = getattr(self, 'type_' + p[0], None)
 
1184
            if f is not None:
 
1185
                return f(p[1:])
 
1186
            return self.type_UNKNOWN(p)
 
1187
        return (SYNTAX_ERR,)
 
1188
 
 
1189
    def type_A(self, code):
 
1190
        if code == '' or code == 'N':
 
1191
            self.binary = False
 
1192
            return (TYPE_SET_OK, 'A' + code)
 
1193
        else:
 
1194
            return defer.fail(CmdArgSyntaxError(code))
 
1195
 
 
1196
    def type_I(self, code):
 
1197
        if code == '':
 
1198
            self.binary = True
 
1199
            return (TYPE_SET_OK, 'I')
 
1200
        else:
 
1201
            return defer.fail(CmdArgSyntaxError(code))
 
1202
 
 
1203
    def type_UNKNOWN(self, code):
 
1204
        return defer.fail(CmdNotImplementedForArgError(code))
 
1205
 
 
1206
 
 
1207
 
 
1208
    def ftp_SYST(self):
 
1209
        return NAME_SYS_TYPE
 
1210
 
 
1211
 
 
1212
    def ftp_STRU(self, structure):
 
1213
        p = structure.upper()
 
1214
        if p == 'F':
 
1215
            return (CMD_OK,)
 
1216
        return defer.fail(CmdNotImplementedForArgError(structure))
 
1217
 
 
1218
 
 
1219
    def ftp_MODE(self, mode):
 
1220
        p = mode.upper()
 
1221
        if p == 'S':
 
1222
            return (CMD_OK,)
 
1223
        return defer.fail(CmdNotImplementedForArgError(mode))
 
1224
 
 
1225
 
 
1226
    def ftp_MKD(self, path):
 
1227
        try:
 
1228
            newsegs = toSegments(self.workingDirectory, path)
 
1229
        except InvalidPath:
 
1230
            return defer.fail(FileNotFoundError(path))
 
1231
        return self.shell.makeDirectory(newsegs).addCallback(lambda ign: (MKD_REPLY, path))
 
1232
 
 
1233
 
 
1234
    def ftp_RMD(self, path):
 
1235
        try:
 
1236
            newsegs = toSegments(self.workingDirectory, path)
 
1237
        except InvalidPath:
 
1238
            return defer.fail(FileNotFoundError(path))
 
1239
        return self.shell.removeDirectory(newsegs).addCallback(lambda ign: (REQ_FILE_ACTN_COMPLETED_OK,))
 
1240
 
 
1241
 
 
1242
    def ftp_DELE(self, path):
 
1243
        try:
 
1244
            newsegs = toSegments(self.workingDirectory, path)
 
1245
        except InvalidPath:
 
1246
            return defer.fail(FileNotFoundError(path))
 
1247
        return self.shell.removeFile(newsegs).addCallback(lambda ign: (REQ_FILE_ACTN_COMPLETED_OK,))
 
1248
 
 
1249
 
 
1250
    def ftp_NOOP(self):
 
1251
        return (CMD_OK,)
 
1252
 
 
1253
 
 
1254
    def ftp_RNFR(self, fromName):
 
1255
        self._fromName = fromName
 
1256
        self.state = self.RENAMING
 
1257
        return (REQ_FILE_ACTN_PENDING_FURTHER_INFO,)
 
1258
 
 
1259
 
 
1260
    def ftp_RNTO(self, toName):
 
1261
        fromName = self._fromName
 
1262
        del self._fromName
 
1263
        self.state = self.AUTHED
 
1264
 
 
1265
        try:
 
1266
            fromsegs = toSegments(self.workingDirectory, fromName)
 
1267
            tosegs = toSegments(self.workingDirectory, toName)
 
1268
        except InvalidPath:
 
1269
            return defer.fail(FileNotFoundError(fromName))
 
1270
        return self.shell.rename(fromsegs, tosegs).addCallback(lambda ign: (REQ_FILE_ACTN_COMPLETED_OK,))
 
1271
 
 
1272
 
 
1273
    def ftp_QUIT(self):
 
1274
        self.reply(GOODBYE_MSG)
 
1275
        self.transport.loseConnection()
 
1276
        self.disconnected = True
 
1277
 
 
1278
 
 
1279
    def cleanupDTP(self):
 
1280
        """call when DTP connection exits
 
1281
        """
 
1282
        log.msg('cleanupDTP', debug=True)
 
1283
 
 
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()
 
1290
        else:
 
1291
            assert False, "dtpPort should be an IListeningPort or IConnector, instead is %r" % (dtpPort,)
 
1292
 
 
1293
        self.dtpFactory.stopFactory()
 
1294
        self.dtpFactory = None
 
1295
 
 
1296
        if self.dtpInstance is not None:
 
1297
            self.dtpInstance = None
 
1298
 
 
1299
 
 
1300
class FTPFactory(policies.LimitTotalConnectionsFactory):
 
1301
    """
 
1302
    A factory for producing ftp protocol instances
 
1303
 
 
1304
    @ivar timeOut: the protocol interpreter's idle timeout time in seconds,
 
1305
        default is 600 seconds.
 
1306
 
 
1307
    @ivar passivePortRange: value forwarded to C{protocol.passivePortRange}.
 
1308
    @type passivePortRange: C{iterator}
 
1309
    """
 
1310
    protocol = FTP
 
1311
    overflowProtocol = FTPOverflowProtocol
 
1312
    allowAnonymous = True
 
1313
    userAnonymous = 'anonymous'
 
1314
    timeOut = 600
 
1315
 
 
1316
    welcomeMessage = "Twisted %s FTP Server" % (copyright.version,)
 
1317
 
 
1318
    passivePortRange = xrange(0, 1)
 
1319
 
 
1320
    def __init__(self, portal=None, userAnonymous='anonymous'):
 
1321
        self.portal = portal
 
1322
        self.userAnonymous = userAnonymous
 
1323
        self.instances = []
 
1324
 
 
1325
    def buildProtocol(self, addr):
 
1326
        p = policies.LimitTotalConnectionsFactory.buildProtocol(self, addr)
 
1327
        if p is not None:
 
1328
            p.wrappedProtocol.portal = self.portal
 
1329
            p.wrappedProtocol.timeOut = self.timeOut
 
1330
            p.wrappedProtocol.passivePortRange = self.passivePortRange
 
1331
        return p
 
1332
 
 
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)
 
1338
 
 
1339
# -- Cred Objects --
 
1340
 
 
1341
 
 
1342
class IFTPShell(Interface):
 
1343
    """
 
1344
    An abstraction of the shell commands used by the FTP protocol for
 
1345
    a given user account.
 
1346
 
 
1347
    All path names must be absolute.
 
1348
    """
 
1349
 
 
1350
    def makeDirectory(path):
 
1351
        """
 
1352
        Create a directory.
 
1353
 
 
1354
        @param path: The path, as a list of segments, to create
 
1355
        @type path: C{list} of C{unicode}
 
1356
 
 
1357
        @return: A Deferred which fires when the directory has been
 
1358
        created, or which fails if the directory cannot be created.
 
1359
        """
 
1360
 
 
1361
 
 
1362
    def removeDirectory(path):
 
1363
        """
 
1364
        Remove a directory.
 
1365
 
 
1366
        @param path: The path, as a list of segments, to remove
 
1367
        @type path: C{list} of C{unicode}
 
1368
 
 
1369
        @return: A Deferred which fires when the directory has been
 
1370
        removed, or which fails if the directory cannot be removed.
 
1371
        """
 
1372
 
 
1373
 
 
1374
    def removeFile(path):
 
1375
        """
 
1376
        Remove a file.
 
1377
 
 
1378
        @param path: The path, as a list of segments, to remove
 
1379
        @type path: C{list} of C{unicode}
 
1380
 
 
1381
        @return: A Deferred which fires when the file has been
 
1382
        removed, or which fails if the file cannot be removed.
 
1383
        """
 
1384
 
 
1385
 
 
1386
    def rename(fromPath, toPath):
 
1387
        """
 
1388
        Rename a file or directory.
 
1389
 
 
1390
        @param fromPath: The current name of the path.
 
1391
        @type fromPath: C{list} of C{unicode}
 
1392
 
 
1393
        @param toPath: The desired new name of the path.
 
1394
        @type toPath: C{list} of C{unicode}
 
1395
 
 
1396
        @return: A Deferred which fires when the path has been
 
1397
        renamed, or which fails if the path cannot be renamed.
 
1398
        """
 
1399
 
 
1400
 
 
1401
    def access(path):
 
1402
        """
 
1403
        Determine whether access to the given path is allowed.
 
1404
 
 
1405
        @param path: The path, as a list of segments
 
1406
 
 
1407
        @return: A Deferred which fires with None if access is allowed
 
1408
        or which fails with a specific exception type if access is
 
1409
        denied.
 
1410
        """
 
1411
 
 
1412
 
 
1413
    def stat(path, keys=()):
 
1414
        """
 
1415
        Retrieve information about the given path.
 
1416
 
 
1417
        This is like list, except it will never return results about
 
1418
        child paths.
 
1419
        """
 
1420
 
 
1421
 
 
1422
    def list(path, keys=()):
 
1423
        """
 
1424
        Retrieve information about the given path.
 
1425
 
 
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.
 
1430
 
 
1431
        @param path: The path, as a list of segments, to list
 
1432
        @type path: C{list} of C{unicode}
 
1433
 
 
1434
        @param keys: A tuple of keys desired in the resulting
 
1435
        dictionaries.
 
1436
 
 
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:
 
1442
 
 
1443
            - C{'size'}: size in bytes, as an integer (this is kinda required)
 
1444
 
 
1445
            - C{'directory'}: boolean indicating the type of this entry
 
1446
 
 
1447
            - C{'permissions'}: a bitvector (see os.stat(foo).st_mode)
 
1448
 
 
1449
            - C{'hardlinks'}: Number of hard links to this entry
 
1450
 
 
1451
            - C{'modified'}: number of seconds since the epoch since entry was
 
1452
              modified
 
1453
 
 
1454
            - C{'owner'}: string indicating the user owner of this entry
 
1455
 
 
1456
            - C{'group'}: string indicating the group owner of this entry
 
1457
        """
 
1458
 
 
1459
 
 
1460
    def openForReading(path):
 
1461
        """
 
1462
        @param path: The path, as a list of segments, to open
 
1463
        @type path: C{list} of C{unicode}
 
1464
 
 
1465
        @rtype: C{Deferred} which will fire with L{IReadFile}
 
1466
        """
 
1467
 
 
1468
 
 
1469
    def openForWriting(path):
 
1470
        """
 
1471
        @param path: The path, as a list of segments, to open
 
1472
        @type path: C{list} of C{unicode}
 
1473
 
 
1474
        @rtype: C{Deferred} which will fire with L{IWriteFile}
 
1475
        """
 
1476
 
 
1477
 
 
1478
 
 
1479
class IReadFile(Interface):
 
1480
    """
 
1481
    A file out of which bytes may be read.
 
1482
    """
 
1483
 
 
1484
    def send(consumer):
 
1485
        """
 
1486
        Produce the contents of the given path to the given consumer.  This
 
1487
        method may only be invoked once on each provider.
 
1488
 
 
1489
        @type consumer: C{IConsumer}
 
1490
 
 
1491
        @return: A Deferred which fires when the file has been
 
1492
        consumed completely.
 
1493
        """
 
1494
 
 
1495
 
 
1496
 
 
1497
class IWriteFile(Interface):
 
1498
    """
 
1499
    A file into which bytes may be written.
 
1500
    """
 
1501
 
 
1502
    def receive():
 
1503
        """
 
1504
        Create a consumer which will write to this file.  This method may
 
1505
        only be invoked once on each provider.
 
1506
 
 
1507
        @rtype: C{Deferred} of C{IConsumer}
 
1508
        """
 
1509
 
 
1510
 
 
1511
 
 
1512
def _getgroups(uid):
 
1513
    """Return the primary and supplementary groups for the given UID.
 
1514
 
 
1515
    @type uid: C{int}
 
1516
    """
 
1517
    result = []
 
1518
    pwent = pwd.getpwuid(uid)
 
1519
 
 
1520
    result.append(pwent.pw_gid)
 
1521
 
 
1522
    for grent in grp.getgrall():
 
1523
        if pwent.pw_name in grent.gr_mem:
 
1524
            result.append(grent.gr_gid)
 
1525
 
 
1526
    return result
 
1527
 
 
1528
 
 
1529
def _testPermissions(uid, gid, spath, mode='r'):
 
1530
    """
 
1531
    checks to see if uid has proper permissions to access path with mode
 
1532
 
 
1533
    @type uid: C{int}
 
1534
    @param uid: numeric user id
 
1535
 
 
1536
    @type gid: C{int}
 
1537
    @param gid: numeric group id
 
1538
 
 
1539
    @type spath: C{str}
 
1540
    @param spath: the path on the server to test
 
1541
 
 
1542
    @type mode: C{str}
 
1543
    @param mode: 'r' or 'w' (read or write)
 
1544
 
 
1545
    @rtype: C{bool}
 
1546
    @return: True if the given credentials have the specified form of
 
1547
        access to the given path
 
1548
    """
 
1549
    if mode == 'r':
 
1550
        usr = stat.S_IRUSR
 
1551
        grp = stat.S_IRGRP
 
1552
        oth = stat.S_IROTH
 
1553
        amode = os.R_OK
 
1554
    elif mode == 'w':
 
1555
        usr = stat.S_IWUSR
 
1556
        grp = stat.S_IWGRP
 
1557
        oth = stat.S_IWOTH
 
1558
        amode = os.W_OK
 
1559
    else:
 
1560
        raise ValueError("Invalid mode %r: must specify 'r' or 'w'" % (mode,))
 
1561
 
 
1562
    access = False
 
1563
    if os.path.exists(spath):
 
1564
        if uid == 0:
 
1565
            access = True
 
1566
        else:
 
1567
            s = os.stat(spath)
 
1568
            if usr & s.st_mode and uid == s.st_uid:
 
1569
                access = True
 
1570
            elif grp & s.st_mode and gid in _getgroups(uid):
 
1571
                access = True
 
1572
            elif oth & s.st_mode:
 
1573
                access = True
 
1574
 
 
1575
    if access:
 
1576
        if not os.access(spath, amode):
 
1577
            access = False
 
1578
            log.msg("Filesystem grants permission to UID %d but it is inaccessible to me running as UID %d" % (
 
1579
                uid, os.getuid()))
 
1580
    return access
 
1581
 
 
1582
 
 
1583
 
 
1584
class FTPAnonymousShell(object):
 
1585
    """
 
1586
    An anonymous implementation of IFTPShell
 
1587
 
 
1588
    @type filesystemRoot: L{twisted.python.filepath.FilePath}
 
1589
    @ivar filesystemRoot: The path which is considered the root of
 
1590
    this shell.
 
1591
    """
 
1592
    implements(IFTPShell)
 
1593
 
 
1594
    def __init__(self, filesystemRoot):
 
1595
        self.filesystemRoot = filesystemRoot
 
1596
 
 
1597
 
 
1598
    def _path(self, path):
 
1599
        return reduce(filepath.FilePath.child, path, self.filesystemRoot)
 
1600
 
 
1601
 
 
1602
    def makeDirectory(self, path):
 
1603
        return defer.fail(AnonUserDeniedError())
 
1604
 
 
1605
 
 
1606
    def removeDirectory(self, path):
 
1607
        return defer.fail(AnonUserDeniedError())
 
1608
 
 
1609
 
 
1610
    def removeFile(self, path):
 
1611
        return defer.fail(AnonUserDeniedError())
 
1612
 
 
1613
 
 
1614
    def rename(self, fromPath, toPath):
 
1615
        return defer.fail(AnonUserDeniedError())
 
1616
 
 
1617
 
 
1618
    def receive(self, path):
 
1619
        path = self._path(path)
 
1620
        return defer.fail(AnonUserDeniedError())
 
1621
 
 
1622
 
 
1623
    def openForReading(self, path):
 
1624
        p = self._path(path)
 
1625
        if p.isdir():
 
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))
 
1629
        try:
 
1630
            f = p.open('rb')
 
1631
        except (IOError, OSError), e:
 
1632
            return errnoToFailure(e.errno, path)
 
1633
        except:
 
1634
            return defer.fail()
 
1635
        else:
 
1636
            return defer.succeed(_FileReader(f))
 
1637
 
 
1638
 
 
1639
    def openForWriting(self, path):
 
1640
        """
 
1641
        Reject write attempts by anonymous users with
 
1642
        L{PermissionDeniedError}.
 
1643
        """
 
1644
        return defer.fail(PermissionDeniedError("STOR not allowed"))
 
1645
 
 
1646
 
 
1647
    def access(self, path):
 
1648
        p = self._path(path)
 
1649
        if not p.exists():
 
1650
            # Again, win32 doesn't report a sane error after, so let's fail
 
1651
            # early if we can
 
1652
            return defer.fail(FileNotFoundError(path))
 
1653
        # For now, just see if we can os.listdir() it
 
1654
        try:
 
1655
            p.listdir()
 
1656
        except (IOError, OSError), e:
 
1657
            return errnoToFailure(e.errno, path)
 
1658
        except:
 
1659
            return defer.fail()
 
1660
        else:
 
1661
            return defer.succeed(None)
 
1662
 
 
1663
 
 
1664
    def stat(self, path, keys=()):
 
1665
        p = self._path(path)
 
1666
        if p.isdir():
 
1667
            try:
 
1668
                statResult = self._statNode(p, keys)
 
1669
            except (IOError, OSError), e:
 
1670
                return errnoToFailure(e.errno, path)
 
1671
            except:
 
1672
                return defer.fail()
 
1673
            else:
 
1674
                return defer.succeed(statResult)
 
1675
        else:
 
1676
            return self.list(path, keys).addCallback(lambda res: res[0][1])
 
1677
 
 
1678
 
 
1679
    def list(self, path, keys=()):
 
1680
        """
 
1681
        Return the list of files at given C{path}, adding C{keys} stat
 
1682
        informations if specified.
 
1683
 
 
1684
        @param path: the directory or file to check.
 
1685
        @type path: C{str}
 
1686
 
 
1687
        @param keys: the list of desired metadata
 
1688
        @type keys: C{list} of C{str}
 
1689
        """
 
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]
 
1697
        else:
 
1698
            return defer.fail(FileNotFoundError(path))
 
1699
 
 
1700
        results = []
 
1701
        for fileName, filePath in zip(entries, fileEntries):
 
1702
            ent = []
 
1703
            results.append((fileName, ent))
 
1704
            if keys:
 
1705
                try:
 
1706
                    ent.extend(self._statNode(filePath, keys))
 
1707
                except (IOError, OSError), e:
 
1708
                    return errnoToFailure(e.errno, fileName)
 
1709
                except:
 
1710
                    return defer.fail()
 
1711
 
 
1712
        return defer.succeed(results)
 
1713
 
 
1714
 
 
1715
    def _statNode(self, filePath, keys):
 
1716
        """
 
1717
        Shortcut method to get stat info on a node.
 
1718
 
 
1719
        @param filePath: the node to stat.
 
1720
        @type filePath: C{filepath.FilePath}
 
1721
 
 
1722
        @param keys: the stat keys to get.
 
1723
        @type keys: C{iterable}
 
1724
        """
 
1725
        filePath.restat()
 
1726
        return [getattr(self, '_stat_' + k)(filePath.statinfo) for k in keys]
 
1727
 
 
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')
 
1732
 
 
1733
 
 
1734
    def _stat_owner(self, st):
 
1735
        if pwd is not None:
 
1736
            try:
 
1737
                return pwd.getpwuid(st.st_uid)[0]
 
1738
            except KeyError:
 
1739
                pass
 
1740
        return str(st.st_uid)
 
1741
 
 
1742
 
 
1743
    def _stat_group(self, st):
 
1744
        if grp is not None:
 
1745
            try:
 
1746
                return grp.getgrgid(st.st_gid)[0]
 
1747
            except KeyError:
 
1748
                pass
 
1749
        return str(st.st_gid)
 
1750
 
 
1751
 
 
1752
    def _stat_directory(self, st):
 
1753
        return bool(st.st_mode & stat.S_IFDIR)
 
1754
 
 
1755
 
 
1756
 
 
1757
class _FileReader(object):
 
1758
    implements(IReadFile)
 
1759
 
 
1760
    def __init__(self, fObj):
 
1761
        self.fObj = fObj
 
1762
        self._send = False
 
1763
 
 
1764
    def _close(self, passthrough):
 
1765
        self._send = True
 
1766
        self.fObj.close()
 
1767
        return passthrough
 
1768
 
 
1769
    def send(self, consumer):
 
1770
        assert not self._send, "Can only call IReadFile.send *once* per instance"
 
1771
        self._send = True
 
1772
        d = basic.FileSender().beginFileTransfer(self.fObj, consumer)
 
1773
        d.addBoth(self._close)
 
1774
        return d
 
1775
 
 
1776
 
 
1777
 
 
1778
class FTPShell(FTPAnonymousShell):
 
1779
    """
 
1780
    An authenticated implementation of L{IFTPShell}.
 
1781
    """
 
1782
 
 
1783
    def makeDirectory(self, path):
 
1784
        p = self._path(path)
 
1785
        try:
 
1786
            p.makedirs()
 
1787
        except (IOError, OSError), e:
 
1788
            return errnoToFailure(e.errno, path)
 
1789
        except:
 
1790
            return defer.fail()
 
1791
        else:
 
1792
            return defer.succeed(None)
 
1793
 
 
1794
 
 
1795
    def removeDirectory(self, path):
 
1796
        p = self._path(path)
 
1797
        if p.isfile():
 
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))
 
1802
        try:
 
1803
            os.rmdir(p.path)
 
1804
        except (IOError, OSError), e:
 
1805
            return errnoToFailure(e.errno, path)
 
1806
        except:
 
1807
            return defer.fail()
 
1808
        else:
 
1809
            return defer.succeed(None)
 
1810
 
 
1811
 
 
1812
    def removeFile(self, path):
 
1813
        p = self._path(path)
 
1814
        if p.isdir():
 
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))
 
1819
        try:
 
1820
            p.remove()
 
1821
        except (IOError, OSError), e:
 
1822
            return errnoToFailure(e.errno, path)
 
1823
        except:
 
1824
            return defer.fail()
 
1825
        else:
 
1826
            return defer.succeed(None)
 
1827
 
 
1828
 
 
1829
    def rename(self, fromPath, toPath):
 
1830
        fp = self._path(fromPath)
 
1831
        tp = self._path(toPath)
 
1832
        try:
 
1833
            os.rename(fp.path, tp.path)
 
1834
        except (IOError, OSError), e:
 
1835
            return errnoToFailure(e.errno, fromPath)
 
1836
        except:
 
1837
            return defer.fail()
 
1838
        else:
 
1839
            return defer.succeed(None)
 
1840
 
 
1841
 
 
1842
    def openForWriting(self, path):
 
1843
        p = self._path(path)
 
1844
        if p.isdir():
 
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))
 
1848
        try:
 
1849
            fObj = p.open('wb')
 
1850
        except (IOError, OSError), e:
 
1851
            return errnoToFailure(e.errno, path)
 
1852
        except:
 
1853
            return defer.fail()
 
1854
        return defer.succeed(_FileWriter(fObj))
 
1855
 
 
1856
 
 
1857
 
 
1858
class _FileWriter(object):
 
1859
    implements(IWriteFile)
 
1860
 
 
1861
    def __init__(self, fObj):
 
1862
        self.fObj = fObj
 
1863
        self._receive = False
 
1864
 
 
1865
    def receive(self):
 
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))
 
1870
 
 
1871
 
 
1872
 
 
1873
class FTPRealm:
 
1874
    """
 
1875
    @type anonymousRoot: L{twisted.python.filepath.FilePath}
 
1876
    @ivar anonymousRoot: Root of the filesystem to which anonymous
 
1877
    users will be granted access.
 
1878
    """
 
1879
    implements(portal.IRealm)
 
1880
 
 
1881
    def __init__(self, anonymousRoot):
 
1882
        self.anonymousRoot = filepath.FilePath(anonymousRoot)
 
1883
 
 
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)
 
1889
                else:
 
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")
 
1893
 
 
1894
# --- FTP CLIENT  -------------------------------------------------------------
 
1895
 
 
1896
####
 
1897
# And now for the client...
 
1898
 
 
1899
# Notes:
 
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
 
1910
 
 
1911
class ConnectionLost(FTPError):
 
1912
    pass
 
1913
 
 
1914
class CommandFailed(FTPError):
 
1915
    pass
 
1916
 
 
1917
class BadResponse(FTPError):
 
1918
    pass
 
1919
 
 
1920
class UnexpectedResponse(FTPError):
 
1921
    pass
 
1922
 
 
1923
class UnexpectedData(FTPError):
 
1924
    pass
 
1925
 
 
1926
class FTPCommand:
 
1927
    def __init__(self, text=None, public=0):
 
1928
        self.text = text
 
1929
        self.deferred = defer.Deferred()
 
1930
        self.ready = 1
 
1931
        self.public = public
 
1932
        self.transferDeferred = None
 
1933
 
 
1934
    def fail(self, failure):
 
1935
        if self.public:
 
1936
            self.deferred.errback(failure)
 
1937
 
 
1938
 
 
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)
 
1951
 
 
1952
 
 
1953
class SenderProtocol(protocol.Protocol):
 
1954
    implements(interfaces.IFinishableConsumer)
 
1955
 
 
1956
    def __init__(self):
 
1957
        # Fired upon connection
 
1958
        self.connectedDeferred = defer.Deferred()
 
1959
 
 
1960
        # Fired upon disconnection
 
1961
        self.deferred = defer.Deferred()
 
1962
 
 
1963
    #Protocol stuff
 
1964
    def dataReceived(self, data):
 
1965
        raise UnexpectedData(
 
1966
            "Received data from the server on a "
 
1967
            "send-only data-connection"
 
1968
        )
 
1969
 
 
1970
    def makeConnection(self, transport):
 
1971
        protocol.Protocol.makeConnection(self, transport)
 
1972
        self.connectedDeferred.callback(self)
 
1973
 
 
1974
    def connectionLost(self, reason):
 
1975
        if reason.check(error.ConnectionDone):
 
1976
            self.deferred.callback('connection done')
 
1977
        else:
 
1978
            self.deferred.errback(reason)
 
1979
 
 
1980
    #IFinishableConsumer stuff
 
1981
    def write(self, data):
 
1982
        self.transport.write(data)
 
1983
 
 
1984
    def registerProducer(self, producer, streaming):
 
1985
        """
 
1986
        Register the given producer with our transport.
 
1987
        """
 
1988
        self.transport.registerProducer(producer, streaming)
 
1989
 
 
1990
    def unregisterProducer(self):
 
1991
        """
 
1992
        Unregister the previously registered producer.
 
1993
        """
 
1994
        self.transport.unregisterProducer()
 
1995
 
 
1996
    def finish(self):
 
1997
        self.transport.loseConnection()
 
1998
 
 
1999
 
 
2000
def decodeHostPort(line):
 
2001
    """Decode an FTP response specifying a host and port.
 
2002
 
 
2003
    @return: a 2-tuple of (host, port).
 
2004
    """
 
2005
    abcdef = re.sub('[^0-9, ]', '', line)
 
2006
    parsed = [int(p.strip()) for p in abcdef.split(',')]
 
2007
    for x in parsed:
 
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)
 
2013
    return host, port
 
2014
 
 
2015
def encodeHostPort(host, port):
 
2016
    numbers = host.split('.') + [str(port >> 8), str(port % 256)]
 
2017
    return ','.join(numbers)
 
2018
 
 
2019
def _unwrapFirstError(failure):
 
2020
    failure.trap(defer.FirstError)
 
2021
    return failure.value.subFailure
 
2022
 
 
2023
class FTPDataPortFactory(protocol.ServerFactory):
 
2024
    """Factory for data connections that use the PORT command
 
2025
 
 
2026
    (i.e. "active" transfers)
 
2027
    """
 
2028
    noisy = 0
 
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
 
2037
 
 
2038
 
 
2039
class FTPClientBasic(basic.LineReceiver):
 
2040
    """
 
2041
    Foundations of an FTP client.
 
2042
    """
 
2043
    debug = False
 
2044
 
 
2045
    def __init__(self):
 
2046
        self.actionQueue = []
 
2047
        self.greeting = None
 
2048
        self.nextDeferred = defer.Deferred().addCallback(self._cb_greeting)
 
2049
        self.nextDeferred.addErrback(self.fail)
 
2050
        self.response = []
 
2051
        self._failed = 0
 
2052
 
 
2053
    def fail(self, error):
 
2054
        """
 
2055
        Give an error to any queued deferreds.
 
2056
        """
 
2057
        self._fail(error)
 
2058
 
 
2059
    def _fail(self, error):
 
2060
        """
 
2061
        Errback all queued deferreds.
 
2062
        """
 
2063
        if self._failed:
 
2064
            # We're recursing; bail out here for simplicity
 
2065
            return error
 
2066
        self._failed = 1
 
2067
        if self.nextDeferred:
 
2068
            try:
 
2069
                self.nextDeferred.errback(failure.Failure(ConnectionLost('FTP connection lost', error)))
 
2070
            except defer.AlreadyCalledError:
 
2071
                pass
 
2072
        for ftpCommand in self.actionQueue:
 
2073
            ftpCommand.fail(failure.Failure(ConnectionLost('FTP connection lost', error)))
 
2074
        return error
 
2075
 
 
2076
    def _cb_greeting(self, greeting):
 
2077
        self.greeting = greeting
 
2078
 
 
2079
    def sendLine(self, line):
 
2080
        """
 
2081
        (Private) Sends a line, unless line is None.
 
2082
        """
 
2083
        if line is None:
 
2084
            return
 
2085
        basic.LineReceiver.sendLine(self, line)
 
2086
 
 
2087
    def sendNextCommand(self):
 
2088
        """
 
2089
        (Private) Processes the next command in the queue.
 
2090
        """
 
2091
        ftpCommand = self.popCommandQueue()
 
2092
        if ftpCommand is None:
 
2093
            self.nextDeferred = None
 
2094
            return
 
2095
        if not ftpCommand.ready:
 
2096
            self.actionQueue.insert(0, ftpCommand)
 
2097
            reactor.callLater(1.0, self.sendNextCommand)
 
2098
            self.nextDeferred = None
 
2099
            return
 
2100
 
 
2101
        # FIXME: this if block doesn't belong in FTPClientBasic, it belongs in
 
2102
        #        FTPClient.
 
2103
        if ftpCommand.text == 'PORT':
 
2104
            self.generatePortCommand(ftpCommand)
 
2105
 
 
2106
        if self.debug:
 
2107
            log.msg('<-- %s' % ftpCommand.text)
 
2108
        self.nextDeferred = ftpCommand.deferred
 
2109
        self.sendLine(ftpCommand.text)
 
2110
 
 
2111
    def queueCommand(self, ftpCommand):
 
2112
        """
 
2113
        Add an FTPCommand object to the queue.
 
2114
 
 
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
 
2117
        immediately.
 
2118
 
 
2119
        @param ftpCommand: an L{FTPCommand}
 
2120
        """
 
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()
 
2125
 
 
2126
    def queueStringCommand(self, command, public=1):
 
2127
        """
 
2128
        Queues a string to be issued as an FTP command
 
2129
 
 
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.
 
2133
 
 
2134
        @return: a L{Deferred} that will be called when the response to the
 
2135
            command has been received.
 
2136
        """
 
2137
        ftpCommand = FTPCommand(command, public)
 
2138
        self.queueCommand(ftpCommand)
 
2139
        return ftpCommand.deferred
 
2140
 
 
2141
    def popCommandQueue(self):
 
2142
        """
 
2143
        Return the front element of the command queue, or None if empty.
 
2144
        """
 
2145
        if self.actionQueue:
 
2146
            return self.actionQueue.pop(0)
 
2147
        else:
 
2148
            return None
 
2149
 
 
2150
    def queueLogin(self, username, password):
 
2151
        """
 
2152
        Login: send the username, send the password.
 
2153
 
 
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.
 
2157
        """
 
2158
        # Prepare the USER command
 
2159
        deferreds = []
 
2160
        userDeferred = self.queueStringCommand('USER ' + username, public=0)
 
2161
        deferreds.append(userDeferred)
 
2162
 
 
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)
 
2168
 
 
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)
 
2175
                return response
 
2176
            userDeferred.addCallback(cancelPasswordIfNotNeeded)
 
2177
 
 
2178
        # Error handling.
 
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)
 
2184
 
 
2185
    def lineReceived(self, line):
 
2186
        """
 
2187
        (Private) Parses the response messages from the FTP server.
 
2188
        """
 
2189
        # Add this line to the current response
 
2190
        if self.debug:
 
2191
            log.msg('--> %s' % line)
 
2192
        self.response.append(line)
 
2193
 
 
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)
 
2197
        if not codeIsValid:
 
2198
            return
 
2199
 
 
2200
        code = line[0:3]
 
2201
 
 
2202
        # Ignore marks
 
2203
        if code[0] == '1':
 
2204
            return
 
2205
 
 
2206
        # Check that we were expecting a response
 
2207
        if self.nextDeferred is None:
 
2208
            self.fail(UnexpectedResponse(self.response))
 
2209
            return
 
2210
 
 
2211
        # Reset the response
 
2212
        response = self.response
 
2213
        self.response = []
 
2214
 
 
2215
        # Look for a success or error code, and call the appropriate callback
 
2216
        if code[0] in ('2', '3'):
 
2217
            # Success
 
2218
            self.nextDeferred.callback(response)
 
2219
        elif code[0] in ('4', '5'):
 
2220
            # Failure
 
2221
            self.nextDeferred.errback(failure.Failure(CommandFailed(response)))
 
2222
        else:
 
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)))
 
2226
 
 
2227
        # Run the next command
 
2228
        self.sendNextCommand()
 
2229
 
 
2230
    def connectionLost(self, reason):
 
2231
        self._fail(reason)
 
2232
 
 
2233
 
 
2234
 
 
2235
class _PassiveConnectionFactory(protocol.ClientFactory):
 
2236
    noisy = False
 
2237
 
 
2238
    def __init__(self, protoInstance):
 
2239
        self.protoInstance = protoInstance
 
2240
 
 
2241
    def buildProtocol(self, ignored):
 
2242
        self.protoInstance.factory = self
 
2243
        return self.protoInstance
 
2244
 
 
2245
    def clientConnectionFailed(self, connector, reason):
 
2246
        e = FTPError('Connection Failed', reason)
 
2247
        self.protoInstance.deferred.errback(e)
 
2248
 
 
2249
 
 
2250
 
 
2251
class FTPClient(FTPClientBasic):
 
2252
    """
 
2253
    L{FTPClient} is a client implementation of the FTP protocol which
 
2254
    exposes FTP commands as methods which return L{Deferred}s.
 
2255
 
 
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.
 
2263
 
 
2264
    See U{RFC 959<http://www.ietf.org/rfc/rfc959.txt>} for error code
 
2265
    definitions.
 
2266
 
 
2267
    Both active and passive transfers are supported.
 
2268
 
 
2269
    @ivar passive: See description in __init__.
 
2270
    """
 
2271
    connectFactory = reactor.connectTCP
 
2272
 
 
2273
    def __init__(self, username='anonymous',
 
2274
                 password='twisted@twistedmatrix.com',
 
2275
                 passive=1):
 
2276
        """
 
2277
        Constructor.
 
2278
 
 
2279
        I will login as soon as I receive the welcome message from the server.
 
2280
 
 
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}.
 
2286
        """
 
2287
        FTPClientBasic.__init__(self)
 
2288
        self.queueLogin(username, password)
 
2289
 
 
2290
        self.passive = passive
 
2291
 
 
2292
    def fail(self, error):
 
2293
        """
 
2294
        Disconnect, and also give an error to any queued deferreds.
 
2295
        """
 
2296
        self.transport.loseConnection()
 
2297
        self._fail(error)
 
2298
 
 
2299
    def receiveFromConnection(self, commands, protocol):
 
2300
        """
 
2301
        Retrieves a file or listing generated by the given command,
 
2302
        feeding it to the given protocol.
 
2303
 
 
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.
 
2309
 
 
2310
        @return: L{Deferred}.
 
2311
        """
 
2312
        protocol = interfaces.IProtocol(protocol)
 
2313
        wrapper = ProtocolWrapper(protocol, defer.Deferred())
 
2314
        return self._openDataConnection(commands, wrapper)
 
2315
 
 
2316
    def queueLogin(self, username, password):
 
2317
        """
 
2318
        Login: send the username, send the password, and
 
2319
        set retrieval mode to binary
 
2320
        """
 
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)
 
2327
 
 
2328
    def sendToConnection(self, commands):
 
2329
        """
 
2330
        XXX
 
2331
 
 
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.
 
2337
        """
 
2338
        s = SenderProtocol()
 
2339
        r = self._openDataConnection(commands, s)
 
2340
        return (s.connectedDeferred, r)
 
2341
 
 
2342
    def _openDataConnection(self, commands, protocol):
 
2343
        """
 
2344
        This method returns a DeferredList.
 
2345
        """
 
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)
 
2350
 
 
2351
        if self.passive:
 
2352
            # Hack: use a mutable object to sneak a variable out of the
 
2353
            # scope of doPassive
 
2354
            _mutable = [None]
 
2355
            def doPassive(response):
 
2356
                """Connect to the port specified in the response to PASV"""
 
2357
                host, port = decodeHostPort(response[-1][4:])
 
2358
 
 
2359
                f = _PassiveConnectionFactory(protocol)
 
2360
                _mutable[0] = self.connectFactory(host, port, f)
 
2361
 
 
2362
            pasvCmd = FTPCommand('PASV')
 
2363
            self.queueCommand(pasvCmd)
 
2364
            pasvCmd.deferred.addCallback(doPassive).addErrback(self.fail)
 
2365
 
 
2366
            results = [cmdsDeferred, pasvCmd.deferred, protocol.deferred]
 
2367
            d = defer.DeferredList(results, fireOnOneErrback=True, consumeErrors=True)
 
2368
            d.addErrback(_unwrapFirstError)
 
2369
 
 
2370
            # Ensure the connection is always closed
 
2371
            def close(x, m=_mutable):
 
2372
                m[0] and m[0].disconnect()
 
2373
                return x
 
2374
            d.addBoth(close)
 
2375
 
 
2376
        else:
 
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')
 
2380
 
 
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
 
2386
            # right time.
 
2387
 
 
2388
            portCmd.transferDeferred = protocol.deferred
 
2389
            portCmd.protocol = protocol
 
2390
            portCmd.deferred.addErrback(portCmd.transferDeferred.errback)
 
2391
            self.queueCommand(portCmd)
 
2392
 
 
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
 
2398
 
 
2399
            # Ensure that the connection always gets closed
 
2400
            cmdsDeferred.addErrback(lambda e, pc=portCmd: pc.fail(e) or e)
 
2401
 
 
2402
            results = [cmdsDeferred, portCmd.deferred, portCmd.transferDeferred]
 
2403
            d = defer.DeferredList(results, fireOnOneErrback=True, consumeErrors=True)
 
2404
            d.addErrback(_unwrapFirstError)
 
2405
 
 
2406
        for cmd in cmds:
 
2407
            self.queueCommand(cmd)
 
2408
        return d
 
2409
 
 
2410
    def generatePortCommand(self, portCmd):
 
2411
        """
 
2412
        (Private) Generates the text of a given PORT command.
 
2413
        """
 
2414
 
 
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.
 
2419
 
 
2420
        # FIXME: This method is far too ugly.
 
2421
 
 
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.
 
2425
 
 
2426
        # Start listening on a port
 
2427
        factory = FTPDataPortFactory()
 
2428
        factory.protocol = portCmd.protocol
 
2429
        listener = reactor.listenTCP(0, factory)
 
2430
        factory.port = listener
 
2431
 
 
2432
        # Ensure we close the listening port if something goes wrong
 
2433
        def listenerFail(error, listener=listener):
 
2434
            if listener.connected:
 
2435
                listener.loseConnection()
 
2436
            return error
 
2437
        portCmd.fail = listenerFail
 
2438
 
 
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)
 
2443
 
 
2444
    def escapePath(self, path):
 
2445
        """
 
2446
        Returns a FTP escaped path (replace newlines with nulls).
 
2447
        """
 
2448
        # Escape newline characters
 
2449
        return path.replace('\n', '\0')
 
2450
 
 
2451
    def retrieveFile(self, path, protocol, offset=0):
 
2452
        """
 
2453
        Retrieve a file from the given path
 
2454
 
 
2455
        This method issues the 'RETR' FTP command.
 
2456
 
 
2457
        The file is fed into the given Protocol instance.  The data connection
 
2458
        will be passive if self.passive is set.
 
2459
 
 
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
 
2463
 
 
2464
        @return: L{Deferred}
 
2465
        """
 
2466
        cmds = ['RETR ' + self.escapePath(path)]
 
2467
        if offset:
 
2468
            cmds.insert(0, ('REST ' + str(offset)))
 
2469
        return self.receiveFromConnection(cmds, protocol)
 
2470
 
 
2471
    retr = retrieveFile
 
2472
 
 
2473
    def storeFile(self, path, offset=0):
 
2474
        """
 
2475
        Store a file at the given path.
 
2476
 
 
2477
        This method issues the 'STOR' FTP command.
 
2478
 
 
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.
 
2484
        """
 
2485
        cmds = ['STOR ' + self.escapePath(path)]
 
2486
        if offset:
 
2487
            cmds.insert(0, ('REST ' + str(offset)))
 
2488
        return self.sendToConnection(cmds)
 
2489
 
 
2490
    stor = storeFile
 
2491
 
 
2492
 
 
2493
    def rename(self, pathFrom, pathTo):
 
2494
        """
 
2495
        Rename a file.
 
2496
 
 
2497
        This method issues the I{RNFR}/I{RNTO} command sequence to rename
 
2498
        C{pathFrom} to C{pathTo}.
 
2499
 
 
2500
        @param: pathFrom: the absolute path to the file to be renamed
 
2501
        @type pathFrom: C{str}
 
2502
 
 
2503
        @param: pathTo: the absolute path to rename the file to.
 
2504
        @type pathTo: C{str}
 
2505
 
 
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
 
2512
            L{BadResponse}.
 
2513
        @rtype: L{Deferred}
 
2514
 
 
2515
        @since: 8.2
 
2516
        """
 
2517
        renameFrom = self.queueStringCommand('RNFR ' + self.escapePath(pathFrom))
 
2518
        renameTo = self.queueStringCommand('RNTO ' + self.escapePath(pathTo))
 
2519
 
 
2520
        fromResponse = []
 
2521
 
 
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))
 
2527
 
 
2528
        def ebFrom(failure):
 
2529
            # Make sure the RNTO doesn't run if the RNFR failed.
 
2530
            self.popCommandQueue()
 
2531
            result.errback(failure)
 
2532
 
 
2533
        # Save the RNFR response to pass to the result Deferred later
 
2534
        renameFrom.addCallbacks(fromResponse.extend, ebFrom)
 
2535
 
 
2536
        # Hook up the RNTO to the result Deferred as well
 
2537
        renameTo.chainDeferred(result)
 
2538
 
 
2539
        return result
 
2540
 
 
2541
 
 
2542
    def list(self, path, protocol):
 
2543
        """
 
2544
        Retrieve a file listing into the given protocol instance.
 
2545
 
 
2546
        This method issues the 'LIST' FTP command.
 
2547
 
 
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
 
2551
            listing formats.
 
2552
 
 
2553
        @return: L{Deferred}
 
2554
        """
 
2555
        if path is None:
 
2556
            path = ''
 
2557
        return self.receiveFromConnection(['LIST ' + self.escapePath(path)], protocol)
 
2558
 
 
2559
 
 
2560
    def nlst(self, path, protocol):
 
2561
        """
 
2562
        Retrieve a short file listing into the given protocol instance.
 
2563
 
 
2564
        This method issues the 'NLST' FTP command.
 
2565
 
 
2566
        NLST (should) return a list of filenames, one per line.
 
2567
 
 
2568
        @param path: path to get short file listing for.
 
2569
        @param protocol: a L{Protocol} instance.
 
2570
        """
 
2571
        if path is None:
 
2572
            path = ''
 
2573
        return self.receiveFromConnection(['NLST ' + self.escapePath(path)], protocol)
 
2574
 
 
2575
 
 
2576
    def cwd(self, path):
 
2577
        """
 
2578
        Issues the CWD (Change Working Directory) command. It's also
 
2579
        available as changeDirectory, which parses the result.
 
2580
 
 
2581
        @return: a L{Deferred} that will be called when done.
 
2582
        """
 
2583
        return self.queueStringCommand('CWD ' + self.escapePath(path))
 
2584
 
 
2585
 
 
2586
    def changeDirectory(self, path):
 
2587
        """
 
2588
        Change the directory on the server and parse the result to determine
 
2589
        if it was successful or not.
 
2590
 
 
2591
        @type path: C{str}
 
2592
        @param path: The path to which to change.
 
2593
 
 
2594
        @return: a L{Deferred} which will be called back when the directory
 
2595
            change has succeeded or errbacked if an error occurrs.
 
2596
        """
 
2597
        warnings.warn(
 
2598
            "FTPClient.changeDirectory is deprecated in Twisted 8.2 and "
 
2599
            "newer.  Use FTPClient.cwd instead.",
 
2600
            category=DeprecationWarning,
 
2601
            stacklevel=2)
 
2602
 
 
2603
        def cbResult(result):
 
2604
            if result[-1][:3] != '250':
 
2605
                return failure.Failure(CommandFailed(result))
 
2606
            return True
 
2607
        return self.cwd(path).addCallback(cbResult)
 
2608
 
 
2609
 
 
2610
    def makeDirectory(self, path):
 
2611
        """
 
2612
        Make a directory
 
2613
 
 
2614
        This method issues the MKD command.
 
2615
 
 
2616
        @param path: The path to the directory to create.
 
2617
        @type path: C{str}
 
2618
 
 
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}.
 
2624
        @rtype: L{Deferred}
 
2625
 
 
2626
        @since: 8.2
 
2627
        """
 
2628
        return self.queueStringCommand('MKD ' + self.escapePath(path))
 
2629
 
 
2630
 
 
2631
    def removeFile(self, path):
 
2632
        """
 
2633
        Delete a file on the server.
 
2634
 
 
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.
 
2637
 
 
2638
        @param path: The path to the file to delete. May be relative to the
 
2639
            current dir.
 
2640
        @type path: C{str}
 
2641
 
 
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.
 
2645
        @rtype: L{Deferred}
 
2646
 
 
2647
        @since: 8.2
 
2648
        """
 
2649
        return self.queueStringCommand('DELE ' + self.escapePath(path))
 
2650
 
 
2651
 
 
2652
    def cdup(self):
 
2653
        """
 
2654
        Issues the CDUP (Change Directory UP) command.
 
2655
 
 
2656
        @return: a L{Deferred} that will be called when done.
 
2657
        """
 
2658
        return self.queueStringCommand('CDUP')
 
2659
 
 
2660
 
 
2661
    def pwd(self):
 
2662
        """
 
2663
        Issues the PWD (Print Working Directory) command.
 
2664
 
 
2665
        The L{getDirectory} does the same job but automatically parses the
 
2666
        result.
 
2667
 
 
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.
 
2671
        """
 
2672
        return self.queueStringCommand('PWD')
 
2673
 
 
2674
    def getDirectory(self):
 
2675
        """
 
2676
        Returns the current remote directory.
 
2677
 
 
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.
 
2681
        """
 
2682
        def cbParse(result):
 
2683
            try:
 
2684
                # The only valid code is 257
 
2685
                if int(result[0].split(' ', 1)[0]) != 257:
 
2686
                    raise ValueError
 
2687
            except (IndexError, ValueError), e:
 
2688
                return failure.Failure(CommandFailed(result))
 
2689
            path = parsePWDResponse(result[0])
 
2690
            if path is None:
 
2691
                return failure.Failure(CommandFailed(result))
 
2692
            return path
 
2693
        return self.pwd().addCallback(cbParse)
 
2694
 
 
2695
 
 
2696
    def quit(self):
 
2697
        """
 
2698
        Issues the I{QUIT} command.
 
2699
 
 
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.
 
2703
        """
 
2704
        return self.queueStringCommand('QUIT')
 
2705
 
 
2706
 
 
2707
 
 
2708
class FTPFileListProtocol(basic.LineReceiver):
 
2709
    """Parser for standard FTP file listings
 
2710
 
 
2711
    This is the evil required to match::
 
2712
 
 
2713
        -rw-r--r--   1 root     other        531 Jan 29 03:26 README
 
2714
 
 
2715
    If you need different evil for a wacky FTP server, you can
 
2716
    override either C{fileLinePattern} or C{parseDirectoryLine()}.
 
2717
 
 
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--'
 
2722
        - nlinks:     e.g. 1
 
2723
        - owner:      e.g. 'root'
 
2724
        - group:      e.g. 'other'
 
2725
        - size:       e.g. 531
 
2726
        - date:       e.g. 'Jan 29 03:26'
 
2727
        - filename:   e.g. 'README'
 
2728
        - linktarget: e.g. 'some/file'
 
2729
 
 
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
 
2732
    it.
 
2733
 
 
2734
    @ivar files: list of dicts describing the files in this listing
 
2735
    """
 
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?$'
 
2741
    )
 
2742
    delimiter = '\n'
 
2743
 
 
2744
    def __init__(self):
 
2745
        self.files = []
 
2746
 
 
2747
    def lineReceived(self, line):
 
2748
        d = self.parseDirectoryLine(line)
 
2749
        if d is None:
 
2750
            self.unknownLine(line)
 
2751
        else:
 
2752
            self.addFile(d)
 
2753
 
 
2754
    def parseDirectoryLine(self, line):
 
2755
        """Return a dictionary of fields, or None if line cannot be parsed.
 
2756
 
 
2757
        @param line: line of text expected to contain a directory entry
 
2758
        @type line: str
 
2759
 
 
2760
        @return: dict
 
2761
        """
 
2762
        match = self.fileLinePattern.match(line)
 
2763
        if match is None:
 
2764
            return None
 
2765
        else:
 
2766
            d = match.groupdict()
 
2767
            d['filename'] = d['filename'].replace(r'\ ', ' ')
 
2768
            d['nlinks'] = int(d['nlinks'])
 
2769
            d['size'] = int(d['size'])
 
2770
            if d['linktarget']:
 
2771
                d['linktarget'] = d['linktarget'].replace(r'\ ', ' ')
 
2772
            return d
 
2773
 
 
2774
    def addFile(self, info):
 
2775
        """Append file information dictionary to the list of known files.
 
2776
 
 
2777
        Subclasses can override or extend this method to handle file
 
2778
        information differently without affecting the parsing of data
 
2779
        from the server.
 
2780
 
 
2781
        @param info: dictionary containing the parsed representation
 
2782
                     of the file information
 
2783
        @type info: dict
 
2784
        """
 
2785
        self.files.append(info)
 
2786
 
 
2787
    def unknownLine(self, line):
 
2788
        """Deal with received lines which could not be parsed as file
 
2789
        information.
 
2790
 
 
2791
        Subclasses can override this to perform any special processing
 
2792
        needed.
 
2793
 
 
2794
        @param line: unparsable line as received
 
2795
        @type line: str
 
2796
        """
 
2797
        pass
 
2798
 
 
2799
def parsePWDResponse(response):
 
2800
    """Returns the path from a response to a PWD command.
 
2801
 
 
2802
    Responses typically look like::
 
2803
 
 
2804
        257 "/home/andrew" is current directory.
 
2805
 
 
2806
    For this example, I will return C{'/home/andrew'}.
 
2807
 
 
2808
    If I can't find the path, I return C{None}.
 
2809
    """
 
2810
    match = re.search('"(.*)"', response)
 
2811
    if match:
 
2812
        return match.groups()[0]
 
2813
    else:
 
2814
        return None