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