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