| 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 |