twisted / twisted

@@ -5,6 +5,7 @@
Loading
5 5
6 6
7 7
import errno
8 +
import os
8 9
import struct
9 10
import warnings
10 11
from typing import Dict
@@ -35,15 +36,37 @@
Loading
35 36
36 37
    def dataReceived(self, data):
37 38
        self.buf += data
38 -
        while len(self.buf) > 5:
39 -
            length, kind = struct.unpack("!LB", self.buf[:5])
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.
40 56
            if len(self.buf) < 4 + length:
41 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.
42 63
            data, self.buf = self.buf[5 : 4 + length], self.buf[4 + length :]
64 +
43 65
            packetType = self.packetTypes.get(kind, None)
44 66
            if not packetType:
45 67
                self._log.info("no packet type for {kind}", kind=kind)
46 68
                continue
69 +
47 70
            f = getattr(self, f"packet_{packetType}", None)
48 71
            if not f:
49 72
                self._log.info(
@@ -51,12 +74,16 @@
Loading
51 74
                    packetType=packetType,
52 75
                    data=data[4:],
53 76
                )
54 -
                (reqId,) = struct.unpack("!L", data[:4])
55 77
                self._sendStatus(
56 78
                    reqId, FX_OP_UNSUPPORTED, f"don't understand {packetType}"
57 79
                )
58 80
                # XXX not implemented
59 81
                continue
82 +
            self._log.info(
83 +
                "dispatching: {packetType} requestId={reqId}",
84 +
                packetType=packetType,
85 +
                reqId=reqId,
86 +
            )
60 87
            try:
61 88
                f(data)
62 89
            except Exception:
@@ -178,6 +205,11 @@
Loading
178 205
        requestId = data[:4]
179 206
        data = data[4:]
180 207
        handle, data = getNS(data)
208 +
        self._log.info(
209 +
            "closing: {requestId!r} {handle!r}",
210 +
            requestId=requestId,
211 +
            handle=handle,
212 +
        )
181 213
        assert data == b"", f"still have data in CLOSE: {data!r}"
182 214
        if handle in self.openFiles:
183 215
            fileObj = self.openFiles[handle]
@@ -190,7 +222,10 @@
Loading
190 222
            d.addCallback(self._cbClose, handle, requestId, 1)
191 223
            d.addErrback(self._ebStatus, requestId, b"close failed")
192 224
        else:
193 -
            self._ebClose(failure.Failure(KeyError()), requestId)
225 +
            code = errno.ENOENT
226 +
            text = os.strerror(code)
227 +
            err = OSError(code, text)
228 +
            self._ebStatus(failure.Failure(err), requestId)
194 229
195 230
    def _cbClose(self, result, handle, requestId, isDir=0):
196 231
        if isDir:

@@ -12,12 +12,15 @@
Loading
12 12
import struct
13 13
from unittest import skipIf
14 14
15 +
from hamcrest import assert_that, equal_to
16 +
15 17
from twisted.internet import defer
16 18
from twisted.internet.error import ConnectionLost
17 19
from twisted.protocols import loopback
18 20
from twisted.python import components
19 21
from twisted.python.compat import _PY37PLUS
20 22
from twisted.python.filepath import FilePath
23 +
from twisted.test.proto_helpers import StringTransport
21 24
from twisted.trial.unittest import TestCase
22 25
23 26
try:
@@ -852,6 +855,68 @@
Loading
852 855
            self.assertEqual(v, getattr(filetransfer, k))
853 856
854 857
858 +
@skipIf(not cryptography, "Cannot run without cryptography")
859 +
class RawPacketDataServerTests(TestCase):
860 +
    """
861 +
    Tests for L{filetransfer.FileTransferServer} which explicitly craft
862 +
    certain less common situations to exercise their handling.
863 +
    """
864 +
865 +
    def setUp(self):
866 +
        self.fts = filetransfer.FileTransferServer(avatar=TestAvatar())
867 +
868 +
    def test_closeInvalidHandle(self):
869 +
        """
870 +
        A close request with an unknown handle receives an FX_NO_SUCH_FILE error
871 +
        response.
872 +
        """
873 +
        transport = StringTransport()
874 +
        self.fts.makeConnection(transport)
875 +
876 +
        # any four bytes
877 +
        requestId = b"1234"
878 +
        # The handle to close, arbitrary bytes.
879 +
        handle = b"invalid handle"
880 +
881 +
        # Construct a message packet
882 +
        # https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-4
883 +
        close = common.NS(
884 +
            # Packet type - SSH_FXP_CLOSE
885 +
            # https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-4.3
886 +
            bytes([4])
887 +
            + requestId
888 +
            + common.NS(handle)
889 +
        )
890 +
891 +
        self.fts.dataReceived(close)
892 +
893 +
        # An SSH_FXP_STATUS message
894 +
        # https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-9.1
895 +
        expected = common.NS(
896 +
            # Packet type SSH_FXP_STATUS
897 +
            # https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-4.3
898 +
            bytes([101])
899 +
            +
900 +
            # The same request id
901 +
            requestId
902 +
            +
903 +
            # A four byte status code.  SSH_FX_NO_SUCH_FILE in this case.
904 +
            bytes([0, 0, 0, 2])
905 +
            +
906 +
            # Error message
907 +
            common.NS(b"No such file or directory")
908 +
            +
909 +
            # error message language tag - conch doesn't send one at all,
910 +
            # though maybe it should
911 +
            common.NS(b"")
912 +
        )
913 +
914 +
        assert_that(
915 +
            transport.value(),
916 +
            equal_to(expected),
917 +
        )
918 +
919 +
855 920
@skipIf(not cryptography, "Cannot run without cryptography")
856 921
class RawPacketDataTests(TestCase):
857 922
    """
Files Coverage
src/twisted 89.51%
Project Totals (830 files) 89.51%
lnx-3.6.7-nodeps-withcov-posix
Build #1740797597 -
lnx-pypy-3.7-alldeps-withcov-posix
Build #1740797597 -
lnx-3.6-alldeps-withcov-posix-noipv6
Build #1740797597 -
lnx-3.8-alldeps-withcov-posix
Build #1740797597 -
lnx-3.7-alldeps-withcov-posix
Build #1740797597 -
lnx-3.6-alldeps-withcov-posix
Build #1740797597 -
lnx-3.9-alldeps-withcov-posix
Build #1740797597 -
lnx-3.10.0-rc.1-alldeps-withcov-posix
Build #1740797597 -
1
#
2
# For documentation: https://docs.codecov.io/docs/codecovyml-reference
3
# Twisted settings: https://codecov.io/gh/twisted/twisted/settings/yaml
4
#
5
# We want 100% coverage for new patches to make sure we are always increasing
6
# the coverage.
7
#
8
codecov:
9
  require_ci_to_pass: yes
10
  notify:
11
    # We have at least 10 builds in GitHub Actions and 12 in Azure
12
    # and lint + mypy + docs + ReadTheDocs
13
    after_n_builds: 15
14
    wait_for_ci: yes
15

16
coverage:
17
  precision: 2
18
  round: down
19
  status:
20
    patch:
21
      default:
22
        # New code should have 100% CI coverage as the minimum
23
        # quality assurance measurement.
24
        # If there is a good reason for new code not to have coverage,
25
        # add inline pragma comments.
26
        target: 100%
27
    project:
28
      default:
29
        # Temporary allow for a bit of slack in overall code coverage due to
30
        # swinging coverage that is not triggered by changes in a PR.
31
        # See: https://twistedmatrix.com/trac/ticket/10170
32
        threshold: 0.02%
33

34

35
# We don't want to receive general PR comments about coverage.
36
# We have the commit status checks and that should be enough.
37
# See https://docs.codecov.io/docs/pull-request-comments
38
comment: false
39

40
# See https://docs.codecov.io/docs/github-checks
41
github_checks:
42
  # We want codecov to send inline PR comments for missing coverage.
43
  annotations: true
Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file. The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading