1# -*- test-case-name: twisted.conch.test.test_filetransfer -*- 2# 3# Copyright (c) Twisted Matrix Laboratories. 4# See LICENSE for details. 5 6 7import errno 8import os 9import struct 10import warnings 11from typing import Dict 12 13from zope.interface import implementer 14 15from twisted.conch.interfaces import ISFTPFile, ISFTPServer 16from twisted.conch.ssh.common import NS, getNS 17from twisted.internet import defer, error, protocol 18from twisted.logger import Logger 19from twisted.python import failure 20from twisted.python.compat import nativeString, networkString 21 22 23class FileTransferBase(protocol.Protocol): 24 _log = Logger() 25 26 versions = (3,) 27 28 packetTypes: Dict[int, str] = {} 29 30 def __init__(self): 31 self.buf = b"" 32 self.otherVersion = None # This gets set 33 34 def sendPacket(self, kind, data): 35 self.transport.write(struct.pack("!LB", len(data) + 1, kind) + data) 36 37 def dataReceived(self, data): 38 self.buf += data 39 40 # Continue processing the input buffer as long as there is a chance it 41 # could contain a complete request. The "General Packet Format" 42 # (format all requests follow) is a 4 byte length prefix, a 1 byte 43 # type field, and a 4 byte request id. If we have fewer than 4 + 1 + 44 # 4 == 9 bytes we cannot possibly have a complete request. 45 while len(self.buf) >= 9: 46 header = self.buf[:9] 47 length, kind, reqId = struct.unpack("!LBL", header) 48 # From draft-ietf-secsh-filexfer-13 (the draft we implement): 49 # 50 # The `length' is the length of the data area [including the 51 # kind byte], and does not include the `length' field itself. 52 # 53 # If the input buffer doesn't have enough bytes to satisfy the 54 # full length then we cannot process it now. Wait until we have 55 # more bytes. 56 if len(self.buf) < 4 + length: 57 return 58 59 # We parsed the request id out of the input buffer above but the 60 # interface to the `packet_TYPE` methods involves passing them a 61 # data buffer which still includes the request id ... So leave 62 # those bytes in the `data` we slice off here. 63 data, self.buf = self.buf[5 : 4 + length], self.buf[4 + length :] 64 65 packetType = self.packetTypes.get(kind, None) 66 if not packetType: 67 self._log.info("no packet type for {kind}", kind=kind) 68 continue 69 70 f = getattr(self, f"packet_{packetType}", None) 71 if not f: 72 self._log.info( 73 "not implemented: {packetType} data={data!r}", 74 packetType=packetType, 75 data=data[4:], 76 ) 77 self._sendStatus( 78 reqId, FX_OP_UNSUPPORTED, f"don't understand {packetType}" 79 ) 80 # XXX not implemented 81 continue 82 self._log.info( 83 "dispatching: {packetType} requestId={reqId}", 84 packetType=packetType, 85 reqId=reqId, 86 ) 87 try: 88 f(data) 89 except Exception: 90 self._log.failure( 91 "Failed to handle packet of type {packetType}", 92 packetType=packetType, 93 ) 94 continue 95 96 def _parseAttributes(self, data): 97 (flags,) = struct.unpack("!L", data[:4]) 98 attrs = {} 99 data = data[4:] 100 if flags & FILEXFER_ATTR_SIZE == FILEXFER_ATTR_SIZE: 101 (size,) = struct.unpack("!Q", data[:8]) 102 attrs["size"] = size 103 data = data[8:] 104 if flags & FILEXFER_ATTR_OWNERGROUP == FILEXFER_ATTR_OWNERGROUP: 105 uid, gid = struct.unpack("!2L", data[:8]) 106 attrs["uid"] = uid 107 attrs["gid"] = gid 108 data = data[8:] 109 if flags & FILEXFER_ATTR_PERMISSIONS == FILEXFER_ATTR_PERMISSIONS: 110 (perms,) = struct.unpack("!L", data[:4]) 111 attrs["permissions"] = perms 112 data = data[4:] 113 if flags & FILEXFER_ATTR_ACMODTIME == FILEXFER_ATTR_ACMODTIME: 114 atime, mtime = struct.unpack("!2L", data[:8]) 115 attrs["atime"] = atime 116 attrs["mtime"] = mtime 117 data = data[8:] 118 if flags & FILEXFER_ATTR_EXTENDED == FILEXFER_ATTR_EXTENDED: 119 (extendedCount,) = struct.unpack("!L", data[:4]) 120 data = data[4:] 121 for i in range(extendedCount): 122 (extendedType, data) = getNS(data) 123 (extendedData, data) = getNS(data) 124 attrs[f"ext_{nativeString(extendedType)}"] = extendedData 125 return attrs, data 126 127 def _packAttributes(self, attrs): 128 flags = 0 129 data = b"" 130 if "size" in attrs: 131 data += struct.pack("!Q", attrs["size"]) 132 flags |= FILEXFER_ATTR_SIZE 133 if "uid" in attrs and "gid" in attrs: 134 data += struct.pack("!2L", attrs["uid"], attrs["gid"]) 135 flags |= FILEXFER_ATTR_OWNERGROUP 136 if "permissions" in attrs: 137 data += struct.pack("!L", attrs["permissions"]) 138 flags |= FILEXFER_ATTR_PERMISSIONS 139 if "atime" in attrs and "mtime" in attrs: 140 data += struct.pack("!2L", attrs["atime"], attrs["mtime"]) 141 flags |= FILEXFER_ATTR_ACMODTIME 142 extended = [] 143 for k in attrs: 144 if k.startswith("ext_"): 145 extType = NS(networkString(k[4:])) 146 extData = NS(attrs[k]) 147 extended.append(extType + extData) 148 if extended: 149 data += struct.pack("!L", len(extended)) 150 data += b"".join(extended) 151 flags |= FILEXFER_ATTR_EXTENDED 152 return struct.pack("!L", flags) + data 153 154 def connectionLost(self, reason): 155 """ 156 Called when connection to the remote subsystem was lost. 157 """ 158 159 super().connectionLost(reason) 160 self.connected = False 161 162 163class FileTransferServer(FileTransferBase): 164 def __init__(self, data=None, avatar=None): 165 FileTransferBase.__init__(self) 166 self.client = ISFTPServer(avatar) # yay interfaces 167 self.openFiles = {} 168 self.openDirs = {} 169 170 def packet_INIT(self, data): 171 (version,) = struct.unpack("!L", data[:4]) 172 self.version = min(list(self.versions) + [version]) 173 data = data[4:] 174 ext = {} 175 while data: 176 extName, data = getNS(data) 177 extData, data = getNS(data) 178 ext[extName] = extData 179 ourExt = self.client.gotVersion(version, ext) 180 ourExtData = b"" 181 for (k, v) in ourExt.items(): 182 ourExtData += NS(k) + NS(v) 183 self.sendPacket(FXP_VERSION, struct.pack("!L", self.version) + ourExtData) 184 185 def packet_OPEN(self, data): 186 requestId = data[:4] 187 data = data[4:] 188 filename, data = getNS(data) 189 (flags,) = struct.unpack("!L", data[:4]) 190 data = data[4:] 191 attrs, data = self._parseAttributes(data) 192 assert data == b"", f"still have data in OPEN: {data!r}" 193 d = defer.maybeDeferred(self.client.openFile, filename, flags, attrs) 194 d.addCallback(self._cbOpenFile, requestId) 195 d.addErrback(self._ebStatus, requestId, b"open failed") 196 197 def _cbOpenFile(self, fileObj, requestId): 198 fileId = networkString(str(hash(fileObj))) 199 if fileId in self.openFiles: 200 raise KeyError("id already open") 201 self.openFiles[fileId] = fileObj 202 self.sendPacket(FXP_HANDLE, requestId + NS(fileId)) 203 204 def packet_CLOSE(self, data): 205 requestId = data[:4] 206 data = data[4:] 207 handle, data = getNS(data) 208 self._log.info( 209 "closing: {requestId!r} {handle!r}", 210 requestId=requestId, 211 handle=handle, 212 ) 213 assert data == b"", f"still have data in CLOSE: {data!r}" 214 if handle in self.openFiles: 215 fileObj = self.openFiles[handle] 216 d = defer.maybeDeferred(fileObj.close) 217 d.addCallback(self._cbClose, handle, requestId) 218 d.addErrback(self._ebStatus, requestId, b"close failed") 219 elif handle in self.openDirs: 220 dirObj = self.openDirs[handle][0] 221 d = defer.maybeDeferred(dirObj.close) 222 d.addCallback(self._cbClose, handle, requestId, 1) 223 d.addErrback(self._ebStatus, requestId, b"close failed") 224 else: 225 code = errno.ENOENT 226 text = os.strerror(code) 227 err = OSError(code, text) 228 self._ebStatus(failure.Failure(err), requestId) 229 230 def _cbClose(self, result, handle, requestId, isDir=0): 231 if isDir: 232 del self.openDirs[handle] 233 else: 234 del self.openFiles[handle] 235 self._sendStatus(requestId, FX_OK, b"file closed") 236 237 def packet_READ(self, data): 238 requestId = data[:4] 239 data = data[4:] 240 handle, data = getNS(data) 241 (offset, length), data = struct.unpack("!QL", data[:12]), data[12:] 242 assert data == b"", f"still have data in READ: {data!r}" 243 if handle not in self.openFiles: 244 self._ebRead(failure.Failure(KeyError()), requestId) 245 else: 246 fileObj = self.openFiles[handle] 247 d = defer.maybeDeferred(fileObj.readChunk, offset, length) 248 d.addCallback(self._cbRead, requestId) 249 d.addErrback(self._ebStatus, requestId, b"read failed") 250 251 def _cbRead(self, result, requestId): 252 if result == b"": # Python's read will return this for EOF 253 raise EOFError() 254 self.sendPacket(FXP_DATA, requestId + NS(result)) 255 256 def packet_WRITE(self, data): 257 requestId = data[:4] 258 data = data[4:] 259 handle, data = getNS(data) 260 (offset,) = struct.unpack("!Q", data[:8]) 261 data = data[8:] 262 writeData, data = getNS(data) 263 assert data == b"", f"still have data in WRITE: {data!r}" 264 if handle not in self.openFiles: 265 self._ebWrite(failure.Failure(KeyError()), requestId) 266 else: 267 fileObj = self.openFiles[handle] 268 d = defer.maybeDeferred(fileObj.writeChunk, offset, writeData) 269 d.addCallback(self._cbStatus, requestId, b"write succeeded") 270 d.addErrback(self._ebStatus, requestId, b"write failed") 271 272 def packet_REMOVE(self, data): 273 requestId = data[:4] 274 data = data[4:] 275 filename, data = getNS(data) 276 assert data == b"", f"still have data in REMOVE: {data!r}" 277 d = defer.maybeDeferred(self.client.removeFile, filename) 278 d.addCallback(self._cbStatus, requestId, b"remove succeeded") 279 d.addErrback(self._ebStatus, requestId, b"remove failed") 280 281 def packet_RENAME(self, data): 282 requestId = data[:4] 283 data = data[4:] 284 oldPath, data = getNS(data) 285 newPath, data = getNS(data) 286 assert data == b"", f"still have data in RENAME: {data!r}" 287 d = defer.maybeDeferred(self.client.renameFile, oldPath, newPath) 288 d.addCallback(self._cbStatus, requestId, b"rename succeeded") 289 d.addErrback(self._ebStatus, requestId, b"rename failed") 290 291 def packet_MKDIR(self, data): 292 requestId = data[:4] 293 data = data[4:] 294 path, data = getNS(data) 295 attrs, data = self._parseAttributes(data) 296 assert data == b"", f"still have data in MKDIR: {data!r}" 297 d = defer.maybeDeferred(self.client.makeDirectory, path, attrs) 298 d.addCallback(self._cbStatus, requestId, b"mkdir succeeded") 299 d.addErrback(self._ebStatus, requestId, b"mkdir failed") 300 301 def packet_RMDIR(self, data): 302 requestId = data[:4] 303 data = data[4:] 304 path, data = getNS(data) 305 assert data == b"", f"still have data in RMDIR: {data!r}" 306 d = defer.maybeDeferred(self.client.removeDirectory, path) 307 d.addCallback(self._cbStatus, requestId, b"rmdir succeeded") 308 d.addErrback(self._ebStatus, requestId, b"rmdir failed") 309 310 def packet_OPENDIR(self, data): 311 requestId = data[:4] 312 data = data[4:] 313 path, data = getNS(data) 314 assert data == b"", f"still have data in OPENDIR: {data!r}" 315 d = defer.maybeDeferred(self.client.openDirectory, path) 316 d.addCallback(self._cbOpenDirectory, requestId) 317 d.addErrback(self._ebStatus, requestId, b"opendir failed") 318 319 def _cbOpenDirectory(self, dirObj, requestId): 320 handle = networkString(str(hash(dirObj))) 321 if handle in self.openDirs: 322 raise KeyError("already opened this directory") 323 self.openDirs[handle] = [dirObj, iter(dirObj)] 324 self.sendPacket(FXP_HANDLE, requestId + NS(handle)) 325 326 def packet_READDIR(self, data): 327 requestId = data[:4] 328 data = data[4:] 329 handle, data = getNS(data) 330 assert data == b"", f"still have data in READDIR: {data!r}" 331 if handle not in self.openDirs: 332 self._ebStatus(failure.Failure(KeyError()), requestId) 333 else: 334 dirObj, dirIter = self.openDirs[handle] 335 d = defer.maybeDeferred(self._scanDirectory, dirIter, []) 336 d.addCallback(self._cbSendDirectory, requestId) 337 d.addErrback(self._ebStatus, requestId, b"scan directory failed") 338 339 def _scanDirectory(self, dirIter, f): 340 while len(f) < 250: 341 try: 342 info = next(dirIter) 343 except StopIteration: 344 if not f: 345 raise EOFError 346 return f 347 if isinstance(info, defer.Deferred): 348 info.addCallback(self._cbScanDirectory, dirIter, f) 349 return 350 else: 351 f.append(info) 352 return f 353 354 def _cbScanDirectory(self, result, dirIter, f): 355 f.append(result) 356 return self._scanDirectory(dirIter, f) 357 358 def _cbSendDirectory(self, result, requestId): 359 data = b"" 360 for (filename, longname, attrs) in result: 361 data += NS(filename) 362 data += NS(longname) 363 data += self._packAttributes(attrs) 364 self.sendPacket(FXP_NAME, requestId + struct.pack("!L", len(result)) + data) 365 366 def packet_STAT(self, data, followLinks=1): 367 requestId = data[:4] 368 data = data[4:] 369 path, data = getNS(data) 370 assert data == b"", f"still have data in STAT/LSTAT: {data!r}" 371 d = defer.maybeDeferred(self.client.getAttrs, path, followLinks) 372 d.addCallback(self._cbStat, requestId) 373 d.addErrback(self._ebStatus, requestId, b"stat/lstat failed") 374 375 def packet_LSTAT(self, data): 376 self.packet_STAT(data, 0) 377 378 def packet_FSTAT(self, data): 379 requestId = data[:4] 380 data = data[4:] 381 handle, data = getNS(data) 382 assert data == b"", f"still have data in FSTAT: {data!r}" 383 if handle not in self.openFiles: 384 self._ebStatus( 385 failure.Failure(KeyError(f"{handle} not in self.openFiles")), 386 requestId, 387 ) 388 else: 389 fileObj = self.openFiles[handle] 390 d = defer.maybeDeferred(fileObj.getAttrs) 391 d.addCallback(self._cbStat, requestId) 392 d.addErrback(self._ebStatus, requestId, b"fstat failed") 393 394 def _cbStat(self, result, requestId): 395 data = requestId + self._packAttributes(result) 396 self.sendPacket(FXP_ATTRS, data) 397 398 def packet_SETSTAT(self, data): 399 requestId = data[:4] 400 data = data[4:] 401 path, data = getNS(data) 402 attrs, data = self._parseAttributes(data) 403 if data != b"": 404 self._log.warn("Still have data in SETSTAT: {data!r}", data=data) 405 d = defer.maybeDeferred(self.client.setAttrs, path, attrs) 406 d.addCallback(self._cbStatus, requestId, b"setstat succeeded") 407 d.addErrback(self._ebStatus, requestId, b"setstat failed") 408 409 def packet_FSETSTAT(self, data): 410 requestId = data[:4] 411 data = data[4:] 412 handle, data = getNS(data) 413 attrs, data = self._parseAttributes(data) 414 assert data == b"", f"still have data in FSETSTAT: {data!r}" 415 if handle not in self.openFiles: 416 self._ebStatus(failure.Failure(KeyError()), requestId) 417 else: 418 fileObj = self.openFiles[handle] 419 d = defer.maybeDeferred(fileObj.setAttrs, attrs) 420 d.addCallback(self._cbStatus, requestId, b"fsetstat succeeded") 421 d.addErrback(self._ebStatus, requestId, b"fsetstat failed") 422 423 def packet_READLINK(self, data): 424 requestId = data[:4] 425 data = data[4:] 426 path, data = getNS(data) 427 assert data == b"", f"still have data in READLINK: {data!r}" 428 d = defer.maybeDeferred(self.client.readLink, path) 429 d.addCallback(self._cbReadLink, requestId) 430 d.addErrback(self._ebStatus, requestId, b"readlink failed") 431 432 def _cbReadLink(self, result, requestId): 433 self._cbSendDirectory([(result, b"", {})], requestId) 434 435 def packet_SYMLINK(self, data): 436 requestId = data[:4] 437 data = data[4:] 438 linkPath, data = getNS(data) 439 targetPath, data = getNS(data) 440 d = defer.maybeDeferred(self.client.makeLink, linkPath, targetPath) 441 d.addCallback(self._cbStatus, requestId, b"symlink succeeded") 442 d.addErrback(self._ebStatus, requestId, b"symlink failed") 443 444 def packet_REALPATH(self, data): 445 requestId = data[:4] 446 data = data[4:] 447 path, data = getNS(data) 448 assert data == b"", f"still have data in REALPATH: {data!r}" 449 d = defer.maybeDeferred(self.client.realPath, path) 450 d.addCallback(self._cbReadLink, requestId) # Same return format 451 d.addErrback(self._ebStatus, requestId, b"realpath failed") 452 453 def packet_EXTENDED(self, data): 454 requestId = data[:4] 455 data = data[4:] 456 extName, extData = getNS(data) 457 d = defer.maybeDeferred(self.client.extendedRequest, extName, extData) 458 d.addCallback(self._cbExtended, requestId) 459 d.addErrback(self._ebStatus, requestId, b"extended " + extName + b" failed") 460 461 def _cbExtended(self, data, requestId): 462 self.sendPacket(FXP_EXTENDED_REPLY, requestId + data) 463 464 def _cbStatus(self, result, requestId, msg=b"request succeeded"): 465 self._sendStatus(requestId, FX_OK, msg) 466 467 def _ebStatus(self, reason, requestId, msg=b"request failed"): 468 code = FX_FAILURE 469 message = msg 470 if isinstance(reason.value, (IOError, OSError)): 471 if reason.value.errno == errno.ENOENT: # No such file 472 code = FX_NO_SUCH_FILE 473 message = networkString(reason.value.strerror) 474 elif reason.value.errno == errno.EACCES: # Permission denied 475 code = FX_PERMISSION_DENIED 476 message = networkString(reason.value.strerror) 477 elif reason.value.errno == errno.EEXIST: 478 code = FX_FILE_ALREADY_EXISTS 479 else: 480 self._log.failure( 481 "Request {requestId} failed: {message}", 482 failure=reason, 483 requestId=requestId, 484 message=message, 485 ) 486 elif isinstance(reason.value, EOFError): # EOF 487 code = FX_EOF 488 if reason.value.args: 489 message = networkString(reason.value.args[0]) 490 elif isinstance(reason.value, NotImplementedError): 491 code = FX_OP_UNSUPPORTED 492 if reason.value.args: 493 message = networkString(reason.value.args[0]) 494 elif isinstance(reason.value, SFTPError): 495 code = reason.value.code 496 message = networkString(reason.value.message) 497 else: 498 self._log.failure( 499 "Request {requestId} failed with unknown error: {message}", 500 failure=reason, 501 requestId=requestId, 502 message=message, 503 ) 504 self._sendStatus(requestId, code, message) 505 506 def _sendStatus(self, requestId, code, message, lang=b""): 507 """ 508 Helper method to send a FXP_STATUS message. 509 """ 510 data = requestId + struct.pack("!L", code) 511 data += NS(message) 512 data += NS(lang) 513 self.sendPacket(FXP_STATUS, data) 514 515 def connectionLost(self, reason): 516 """ 517 Called when connection to the remote subsystem was lost. 518 519 Clean all opened files and directories. 520 """ 521 522 FileTransferBase.connectionLost(self, reason) 523 524 for fileObj in self.openFiles.values(): 525 fileObj.close() 526 self.openFiles = {} 527 for (dirObj, dirIter) in self.openDirs.values(): 528 dirObj.close() 529 self.openDirs = {} 530 531 532class FileTransferClient(FileTransferBase): 533 def __init__(self, extData={}): 534 """ 535 @param extData: a dict of extended_name : extended_data items 536 to be sent to the server. 537 """ 538 FileTransferBase.__init__(self) 539 self.extData = {} 540 self.counter = 0 541 self.openRequests = {} # id -> Deferred 542 543 def connectionMade(self): 544 data = struct.pack("!L", max(self.versions)) 545 for k, v in self.extData.values(): 546 data += NS(k) + NS(v) 547 self.sendPacket(FXP_INIT, data) 548 549 def connectionLost(self, reason): 550 """ 551 Called when connection to the remote subsystem was lost. 552 553 Any pending requests are aborted. 554 """ 555 556 FileTransferBase.connectionLost(self, reason) 557 558 # If there are still requests waiting for responses when the 559 # connection is lost, fail them. 560 if self.openRequests: 561 562 # Even if our transport was lost "cleanly", our 563 # requests were still not cancelled "cleanly". 564 requestError = error.ConnectionLost() 565 requestError.__cause__ = reason.value 566 requestFailure = failure.Failure(requestError) 567 while self.openRequests: 568 _, deferred = self.openRequests.popitem() 569 deferred.errback(requestFailure) 570 571 def _sendRequest(self, msg, data): 572 """ 573 Send a request and return a deferred which waits for the result. 574 575 @type msg: L{int} 576 @param msg: The request type (e.g., C{FXP_READ}). 577 578 @type data: L{bytes} 579 @param data: The body of the request. 580 """ 581 if not self.connected: 582 return defer.fail(error.ConnectionLost()) 583 584 data = struct.pack("!L", self.counter) + data 585 d = defer.Deferred() 586 self.openRequests[self.counter] = d 587 self.counter += 1 588 self.sendPacket(msg, data) 589 return d 590 591 def _parseRequest(self, data): 592 (id,) = struct.unpack("!L", data[:4]) 593 d = self.openRequests[id] 594 del self.openRequests[id] 595 return d, data[4:] 596 597 def openFile(self, filename, flags, attrs): 598 """ 599 Open a file. 600 601 This method returns a L{Deferred} that is called back with an object 602 that provides the L{ISFTPFile} interface. 603 604 @type filename: L{bytes} 605 @param filename: a string representing the file to open. 606 607 @param flags: an integer of the flags to open the file with, ORed together. 608 The flags and their values are listed at the bottom of this file. 609 610 @param attrs: a list of attributes to open the file with. It is a 611 dictionary, consisting of 0 or more keys. The possible keys are:: 612 613 size: the size of the file in bytes 614 uid: the user ID of the file as an integer 615 gid: the group ID of the file as an integer 616 permissions: the permissions of the file with as an integer. 617 the bit representation of this field is defined by POSIX. 618 atime: the access time of the file as seconds since the epoch. 619 mtime: the modification time of the file as seconds since the epoch. 620 ext_*: extended attributes. The server is not required to 621 understand this, but it may. 622 623 NOTE: there is no way to indicate text or binary files. it is up 624 to the SFTP client to deal with this. 625 """ 626 data = NS(filename) + struct.pack("!L", flags) + self._packAttributes(attrs) 627 d = self._sendRequest(FXP_OPEN, data) 628 d.addCallback(self._cbOpenHandle, ClientFile, filename) 629 return d 630 631 def _cbOpenHandle(self, handle, handleClass, name): 632 """ 633 Callback invoked when an OPEN or OPENDIR request succeeds. 634 635 @param handle: The handle returned by the server 636 @type handle: L{bytes} 637 @param handleClass: The class that will represent the 638 newly-opened file or directory to the user (either L{ClientFile} or 639 L{ClientDirectory}). 640 @param name: The name of the file or directory represented 641 by C{handle}. 642 @type name: L{bytes} 643 """ 644 cb = handleClass(self, handle) 645 cb.name = name 646 return cb 647 648 def removeFile(self, filename): 649 """ 650 Remove the given file. 651 652 This method returns a Deferred that is called back when it succeeds. 653 654 @type filename: L{bytes} 655 @param filename: the name of the file as a string. 656 """ 657 return self._sendRequest(FXP_REMOVE, NS(filename)) 658 659 def renameFile(self, oldpath, newpath): 660 """ 661 Rename the given file. 662 663 This method returns a Deferred that is called back when it succeeds. 664 665 @type oldpath: L{bytes} 666 @param oldpath: the current location of the file. 667 @type newpath: L{bytes} 668 @param newpath: the new file name. 669 """ 670 return self._sendRequest(FXP_RENAME, NS(oldpath) + NS(newpath)) 671 672 def makeDirectory(self, path, attrs): 673 """ 674 Make a directory. 675 676 This method returns a Deferred that is called back when it is 677 created. 678 679 @type path: L{bytes} 680 @param path: the name of the directory to create as a string. 681 682 @param attrs: a dictionary of attributes to create the directory 683 with. Its meaning is the same as the attrs in the openFile method. 684 """ 685 return self._sendRequest(FXP_MKDIR, NS(path) + self._packAttributes(attrs)) 686 687 def removeDirectory(self, path): 688 """ 689 Remove a directory (non-recursively) 690 691 It is an error to remove a directory that has files or directories in 692 it. 693 694 This method returns a Deferred that is called back when it is removed. 695 696 @type path: L{bytes} 697 @param path: the directory to remove. 698 """ 699 return self._sendRequest(FXP_RMDIR, NS(path)) 700 701 def openDirectory(self, path): 702 """ 703 Open a directory for scanning. 704 705 This method returns a Deferred that is called back with an iterable 706 object that has a close() method. 707 708 The close() method is called when the client is finished reading 709 from the directory. At this point, the iterable will no longer 710 be used. 711 712 The iterable returns triples of the form (filename, longname, attrs) 713 or a Deferred that returns the same. The sequence must support 714 __getitem__, but otherwise may be any 'sequence-like' object. 715 716 filename is the name of the file relative to the directory. 717 logname is an expanded format of the filename. The recommended format 718 is: 719 -rwxr-xr-x 1 mjos staff 348911 Mar 25 14:29 t-filexfer 720 1234567890 123 12345678 12345678 12345678 123456789012 721 722 The first line is sample output, the second is the length of the field. 723 The fields are: permissions, link count, user owner, group owner, 724 size in bytes, modification time. 725 726 attrs is a dictionary in the format of the attrs argument to openFile. 727 728 @type path: L{bytes} 729 @param path: the directory to open. 730 """ 731 d = self._sendRequest(FXP_OPENDIR, NS(path)) 732 d.addCallback(self._cbOpenHandle, ClientDirectory, path) 733 return d 734 735 def getAttrs(self, path, followLinks=0): 736 """ 737 Return the attributes for the given path. 738 739 This method returns a dictionary in the same format as the attrs 740 argument to openFile or a Deferred that is called back with same. 741 742 @type path: L{bytes} 743 @param path: the path to return attributes for as a string. 744 @param followLinks: a boolean. if it is True, follow symbolic links 745 and return attributes for the real path at the base. if it is False, 746 return attributes for the specified path. 747 """ 748 if followLinks: 749 m = FXP_STAT 750 else: 751 m = FXP_LSTAT 752 return self._sendRequest(m, NS(path)) 753 754 def setAttrs(self, path, attrs): 755 """ 756 Set the attributes for the path. 757 758 This method returns when the attributes are set or a Deferred that is 759 called back when they are. 760 761 @type path: L{bytes} 762 @param path: the path to set attributes for as a string. 763 @param attrs: a dictionary in the same format as the attrs argument to 764 openFile. 765 """ 766 data = NS(path) + self._packAttributes(attrs) 767 return self._sendRequest(FXP_SETSTAT, data) 768 769 def readLink(self, path): 770 """ 771 Find the root of a set of symbolic links. 772 773 This method returns the target of the link, or a Deferred that 774 returns the same. 775 776 @type path: L{bytes} 777 @param path: the path of the symlink to read. 778 """ 779 d = self._sendRequest(FXP_READLINK, NS(path)) 780 return d.addCallback(self._cbRealPath) 781 782 def makeLink(self, linkPath, targetPath): 783 """ 784 Create a symbolic link. 785 786 This method returns when the link is made, or a Deferred that 787 returns the same. 788 789 @type linkPath: L{bytes} 790 @param linkPath: the pathname of the symlink as a string 791 @type targetPath: L{bytes} 792 @param targetPath: the path of the target of the link as a string. 793 """ 794 return self._sendRequest(FXP_SYMLINK, NS(linkPath) + NS(targetPath)) 795 796 def realPath(self, path): 797 """ 798 Convert any path to an absolute path. 799 800 This method returns the absolute path as a string, or a Deferred 801 that returns the same. 802 803 @type path: L{bytes} 804 @param path: the path to convert as a string. 805 """ 806 d = self._sendRequest(FXP_REALPATH, NS(path)) 807 return d.addCallback(self._cbRealPath) 808 809 def _cbRealPath(self, result): 810 name, longname, attrs = result[0] 811 name = name.decode("utf-8") 812 return name 813 814 def extendedRequest(self, request, data): 815 """ 816 Make an extended request of the server. 817 818 The method returns a Deferred that is called back with 819 the result of the extended request. 820 821 @type request: L{bytes} 822 @param request: the name of the extended request to make. 823 @type data: L{bytes} 824 @param data: any other data that goes along with the request. 825 """ 826 return self._sendRequest(FXP_EXTENDED, NS(request) + data) 827 828 def packet_VERSION(self, data): 829 (version,) = struct.unpack("!L", data[:4]) 830 data = data[4:] 831 d = {} 832 while data: 833 k, data = getNS(data) 834 v, data = getNS(data) 835 d[k] = v 836 self.version = version 837 self.gotServerVersion(version, d) 838 839 def packet_STATUS(self, data): 840 d, data = self._parseRequest(data) 841 (code,) = struct.unpack("!L", data[:4]) 842 data = data[4:] 843 if len(data) >= 4: 844 msg, data = getNS(data) 845 if len(data) >= 4: 846 lang, data = getNS(data) 847 else: 848 lang = b"" 849 else: 850 msg = b"" 851 lang = b"" 852 if code == FX_OK: 853 d.callback((msg, lang)) 854 elif code == FX_EOF: 855 d.errback(EOFError(msg)) 856 elif code == FX_OP_UNSUPPORTED: 857 d.errback(NotImplementedError(msg)) 858 else: 859 d.errback(SFTPError(code, nativeString(msg), lang)) 860 861 def packet_HANDLE(self, data): 862 d, data = self._parseRequest(data) 863 handle, _ = getNS(data) 864 d.callback(handle) 865 866 def packet_DATA(self, data): 867 d, data = self._parseRequest(data) 868 d.callback(getNS(data)[0]) 869 870 def packet_NAME(self, data): 871 d, data = self._parseRequest(data) 872 (count,) = struct.unpack("!L", data[:4]) 873 data = data[4:] 874 files = [] 875 for i in range(count): 876 filename, data = getNS(data) 877 longname, data = getNS(data) 878 attrs, data = self._parseAttributes(data) 879 files.append((filename, longname, attrs)) 880 d.callback(files) 881 882 def packet_ATTRS(self, data): 883 d, data = self._parseRequest(data) 884 d.callback(self._parseAttributes(data)[0]) 885 886 def packet_EXTENDED_REPLY(self, data): 887 d, data = self._parseRequest(data) 888 d.callback(data) 889 890 def gotServerVersion(self, serverVersion, extData): 891 """ 892 Called when the client sends their version info. 893 894 @param serverVersion: an integer representing the version of the SFTP 895 protocol they are claiming. 896 @param extData: a dictionary of extended_name : extended_data items. 897 These items are sent by the client to indicate additional features. 898 """ 899 900 901@implementer(ISFTPFile) 902class ClientFile: 903 def __init__(self, parent, handle): 904 self.parent = parent 905 self.handle = NS(handle) 906 907 def close(self): 908 return self.parent._sendRequest(FXP_CLOSE, self.handle) 909 910 def readChunk(self, offset, length): 911 data = self.handle + struct.pack("!QL", offset, length) 912 return self.parent._sendRequest(FXP_READ, data) 913 914 def writeChunk(self, offset, chunk): 915 data = self.handle + struct.pack("!Q", offset) + NS(chunk) 916 return self.parent._sendRequest(FXP_WRITE, data) 917 918 def getAttrs(self): 919 return self.parent._sendRequest(FXP_FSTAT, self.handle) 920 921 def setAttrs(self, attrs): 922 data = self.handle + self.parent._packAttributes(attrs) 923 return self.parent._sendRequest(FXP_FSTAT, data) 924 925 926class ClientDirectory: 927 def __init__(self, parent, handle): 928 self.parent = parent 929 self.handle = NS(handle) 930 self.filesCache = [] 931 932 def read(self): 933 return self.parent._sendRequest(FXP_READDIR, self.handle) 934 935 def close(self): 936 if self.handle is None: 937 return defer.succeed(None) 938 d = self.parent._sendRequest(FXP_CLOSE, self.handle) 939 self.handle = None 940 return d 941 942 def __iter__(self): 943 return self 944 945 def __next__(self): 946 warnings.warn( 947 ( 948 "Using twisted.conch.ssh.filetransfer.ClientDirectory " 949 "as an iterator was deprecated in Twisted 18.9.0." 950 ), 951 category=DeprecationWarning, 952 stacklevel=2, 953 ) 954 if self.filesCache: 955 return self.filesCache.pop(0) 956 if self.filesCache is None: 957 raise StopIteration() 958 d = self.read() 959 d.addCallbacks(self._cbReadDir, self._ebReadDir) 960 return d 961 962 next = __next__ 963 964 def _cbReadDir(self, names): 965 self.filesCache = names[1:] 966 return names[0] 967 968 def _ebReadDir(self, reason): 969 reason.trap(EOFError) 970 self.filesCache = None 971 return failure.Failure(StopIteration()) 972 973 974class SFTPError(Exception): 975 def __init__(self, errorCode, errorMessage, lang=""): 976 Exception.__init__(self) 977 self.code = errorCode 978 self._message = errorMessage 979 self.lang = lang 980 981 @property 982 def message(self): 983 """ 984 A string received over the network that explains the error to a human. 985 """ 986 # Python 2.6 deprecates assigning to the 'message' attribute of an 987 # exception. We define this read-only property here in order to 988 # prevent the warning about deprecation while maintaining backwards 989 # compatibility with object clients that rely on the 'message' 990 # attribute being set correctly. See bug #3897. 991 return self._message 992 993 def __str__(self) -> str: 994 return f"SFTPError {self.code}: {self.message}" 995 996 997FXP_INIT = 1 998FXP_VERSION = 2 999FXP_OPEN = 3 1000FXP_CLOSE = 4 1001FXP_READ = 5 1002FXP_WRITE = 6 1003FXP_LSTAT = 7 1004FXP_FSTAT = 8 1005FXP_SETSTAT = 9 1006FXP_FSETSTAT = 10 1007FXP_OPENDIR = 11 1008FXP_READDIR = 12 1009FXP_REMOVE = 13 1010FXP_MKDIR = 14 1011FXP_RMDIR = 15 1012FXP_REALPATH = 16 1013FXP_STAT = 17 1014FXP_RENAME = 18 1015FXP_READLINK = 19 1016FXP_SYMLINK = 20 1017FXP_STATUS = 101 1018FXP_HANDLE = 102 1019FXP_DATA = 103 1020FXP_NAME = 104 1021FXP_ATTRS = 105 1022FXP_EXTENDED = 200 1023FXP_EXTENDED_REPLY = 201 1024 1025FILEXFER_ATTR_SIZE = 0x00000001 1026FILEXFER_ATTR_UIDGID = 0x00000002 1027FILEXFER_ATTR_OWNERGROUP = FILEXFER_ATTR_UIDGID 1028FILEXFER_ATTR_PERMISSIONS = 0x00000004 1029FILEXFER_ATTR_ACMODTIME = 0x00000008 1030FILEXFER_ATTR_EXTENDED = 0x80000000 1031 1032FILEXFER_TYPE_REGULAR = 1 1033FILEXFER_TYPE_DIRECTORY = 2 1034FILEXFER_TYPE_SYMLINK = 3 1035FILEXFER_TYPE_SPECIAL = 4 1036FILEXFER_TYPE_UNKNOWN = 5 1037 1038FXF_READ = 0x00000001 1039FXF_WRITE = 0x00000002 1040FXF_APPEND = 0x00000004 1041FXF_CREAT = 0x00000008 1042FXF_TRUNC = 0x00000010 1043FXF_EXCL = 0x00000020 1044FXF_TEXT = 0x00000040 1045 1046FX_OK = 0 1047FX_EOF = 1 1048FX_NO_SUCH_FILE = 2 1049FX_PERMISSION_DENIED = 3 1050FX_FAILURE = 4 1051FX_BAD_MESSAGE = 5 1052FX_NO_CONNECTION = 6 1053FX_CONNECTION_LOST = 7 1054FX_OP_UNSUPPORTED = 8 1055FX_FILE_ALREADY_EXISTS = 11 1056# http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/ defines more 1057# useful error codes, but so far OpenSSH doesn't implement them. We use them 1058# internally for clarity, but for now define them all as FX_FAILURE to be 1059# compatible with existing software. 1060FX_NOT_A_DIRECTORY = FX_FAILURE 1061FX_FILE_IS_A_DIRECTORY = FX_FAILURE 1062 1063 1064# initialize FileTransferBase.packetTypes: 1065g = globals() 1066for name in list(g.keys()): 1067 if name.startswith("FXP_"): 1068 value = g[name] 1069 FileTransferBase.packetTypes[value] = name[4:] 1070del g, name, value 1071