diff options
author | robot-piglet <[email protected]> | 2025-06-22 18:50:56 +0300 |
---|---|---|
committer | robot-piglet <[email protected]> | 2025-06-22 19:04:42 +0300 |
commit | c7cbc6d480c5488ff6e921c709680fd2c1340a10 (patch) | |
tree | 10843f44b67c0fb5717ad555556064095f701d8c /contrib/python/Twisted/py3 | |
parent | 26d391cdb94d2ce5efc8d0cc5cea7607dc363c0b (diff) |
Intermediate changes
commit_hash:28750b74281710ec1ab5bdc2403c8ab24bdd164b
Diffstat (limited to 'contrib/python/Twisted/py3')
47 files changed, 1890 insertions, 1006 deletions
diff --git a/contrib/python/Twisted/py3/.dist-info/METADATA b/contrib/python/Twisted/py3/.dist-info/METADATA index 6c1a7ea42c3..9414ada6988 100644 --- a/contrib/python/Twisted/py3/.dist-info/METADATA +++ b/contrib/python/Twisted/py3/.dist-info/METADATA @@ -1,6 +1,6 @@ -Metadata-Version: 2.3 +Metadata-Version: 2.4 Name: Twisted -Version: 24.11.0 +Version: 25.5.0 Summary: An asynchronous networking framework written in Python Project-URL: Changelog, https://github.com/twisted/twisted/blob/HEAD/NEWS.rst Project-URL: Documentation, https://docs.twisted.org/ @@ -11,6 +11,7 @@ Project-URL: Funding-PSF, https://psfmember.org/civicrm/contribute/transact/?res Project-URL: Funding-GitHub, https://github.com/sponsors/twisted Author-email: Twisted Matrix Community <[email protected]> License: MIT License +License-File: LICENSE Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.8 @@ -42,6 +43,7 @@ Requires-Dist: pyopenssl>=21.0.0; extra == 'all-non-platform' Requires-Dist: pyserial>=3.0; extra == 'all-non-platform' Requires-Dist: pywin32!=226; (platform_system == 'Windows') and extra == 'all-non-platform' Requires-Dist: service-identity>=18.1.0; extra == 'all-non-platform' +Requires-Dist: wsproto; extra == 'all-non-platform' Provides-Extra: all_non_platform Requires-Dist: appdirs>=1.4.0; extra == 'all_non_platform' Requires-Dist: bcrypt>=3.1.3; extra == 'all_non_platform' @@ -57,6 +59,7 @@ Requires-Dist: pyopenssl>=21.0.0; extra == 'all_non_platform' Requires-Dist: pyserial>=3.0; extra == 'all_non_platform' Requires-Dist: pywin32!=226; (platform_system == 'Windows') and extra == 'all_non_platform' Requires-Dist: service-identity>=18.1.0; extra == 'all_non_platform' +Requires-Dist: wsproto; extra == 'all_non_platform' Provides-Extra: conch Requires-Dist: appdirs>=1.4.0; extra == 'conch' Requires-Dist: bcrypt>=3.1.3; extra == 'conch' @@ -66,7 +69,7 @@ Requires-Dist: coverage~=7.5; extra == 'dev' Requires-Dist: cython-test-exception-raiser<2,>=1.0.2; extra == 'dev' Requires-Dist: httpx[http2]>=0.27; extra == 'dev' Requires-Dist: hypothesis>=6.56; extra == 'dev' -Requires-Dist: pydoctor~=23.9.0; extra == 'dev' +Requires-Dist: pydoctor~=24.11.1; extra == 'dev' Requires-Dist: pyflakes~=2.2; extra == 'dev' Requires-Dist: pyhamcrest>=2; extra == 'dev' Requires-Dist: python-subunit~=1.4; extra == 'dev' @@ -75,12 +78,12 @@ Requires-Dist: sphinx<7,>=6; extra == 'dev' Requires-Dist: towncrier~=23.6; extra == 'dev' Requires-Dist: twistedchecker~=0.7; extra == 'dev' Provides-Extra: dev-release -Requires-Dist: pydoctor~=23.9.0; extra == 'dev-release' +Requires-Dist: pydoctor~=24.11.1; extra == 'dev-release' Requires-Dist: sphinx-rtd-theme~=1.3; extra == 'dev-release' Requires-Dist: sphinx<7,>=6; extra == 'dev-release' Requires-Dist: towncrier~=23.6; extra == 'dev-release' Provides-Extra: dev_release -Requires-Dist: pydoctor~=23.9.0; extra == 'dev_release' +Requires-Dist: pydoctor~=24.11.1; extra == 'dev_release' Requires-Dist: sphinx-rtd-theme~=1.3; extra == 'dev_release' Requires-Dist: sphinx<7,>=6; extra == 'dev_release' Requires-Dist: towncrier~=23.6; extra == 'dev_release' @@ -100,6 +103,7 @@ Requires-Dist: pyopenssl>=21.0.0; extra == 'gtk-platform' Requires-Dist: pyserial>=3.0; extra == 'gtk-platform' Requires-Dist: pywin32!=226; (platform_system == 'Windows') and extra == 'gtk-platform' Requires-Dist: service-identity>=18.1.0; extra == 'gtk-platform' +Requires-Dist: wsproto; extra == 'gtk-platform' Provides-Extra: gtk_platform Requires-Dist: appdirs>=1.4.0; extra == 'gtk_platform' Requires-Dist: bcrypt>=3.1.3; extra == 'gtk_platform' @@ -116,6 +120,7 @@ Requires-Dist: pyopenssl>=21.0.0; extra == 'gtk_platform' Requires-Dist: pyserial>=3.0; extra == 'gtk_platform' Requires-Dist: pywin32!=226; (platform_system == 'Windows') and extra == 'gtk_platform' Requires-Dist: service-identity>=18.1.0; extra == 'gtk_platform' +Requires-Dist: wsproto; extra == 'gtk_platform' Provides-Extra: http2 Requires-Dist: h2<5.0,>=3.2; extra == 'http2' Requires-Dist: priority<2.0,>=1.1.0; extra == 'http2' @@ -130,13 +135,17 @@ Requires-Dist: hypothesis>=6.56; extra == 'macos-platform' Requires-Dist: idna>=2.4; extra == 'macos-platform' Requires-Dist: priority<2.0,>=1.1.0; extra == 'macos-platform' Requires-Dist: pyhamcrest>=2; extra == 'macos-platform' -Requires-Dist: pyobjc-core; extra == 'macos-platform' -Requires-Dist: pyobjc-framework-cfnetwork; extra == 'macos-platform' -Requires-Dist: pyobjc-framework-cocoa; extra == 'macos-platform' +Requires-Dist: pyobjc-core; (python_version >= '3.9') and extra == 'macos-platform' +Requires-Dist: pyobjc-core<11; (python_version < '3.9') and extra == 'macos-platform' +Requires-Dist: pyobjc-framework-cfnetwork; (python_version >= '3.9') and extra == 'macos-platform' +Requires-Dist: pyobjc-framework-cfnetwork<11; (python_version < '3.9') and extra == 'macos-platform' +Requires-Dist: pyobjc-framework-cocoa; (python_version >= '3.9') and extra == 'macos-platform' +Requires-Dist: pyobjc-framework-cocoa<11; (python_version < '3.9') and extra == 'macos-platform' Requires-Dist: pyopenssl>=21.0.0; extra == 'macos-platform' Requires-Dist: pyserial>=3.0; extra == 'macos-platform' Requires-Dist: pywin32!=226; (platform_system == 'Windows') and extra == 'macos-platform' Requires-Dist: service-identity>=18.1.0; extra == 'macos-platform' +Requires-Dist: wsproto; extra == 'macos-platform' Provides-Extra: macos_platform Requires-Dist: appdirs>=1.4.0; extra == 'macos_platform' Requires-Dist: bcrypt>=3.1.3; extra == 'macos_platform' @@ -148,13 +157,17 @@ Requires-Dist: hypothesis>=6.56; extra == 'macos_platform' Requires-Dist: idna>=2.4; extra == 'macos_platform' Requires-Dist: priority<2.0,>=1.1.0; extra == 'macos_platform' Requires-Dist: pyhamcrest>=2; extra == 'macos_platform' -Requires-Dist: pyobjc-core; extra == 'macos_platform' -Requires-Dist: pyobjc-framework-cfnetwork; extra == 'macos_platform' -Requires-Dist: pyobjc-framework-cocoa; extra == 'macos_platform' +Requires-Dist: pyobjc-core; (python_version >= '3.9') and extra == 'macos_platform' +Requires-Dist: pyobjc-core<11; (python_version < '3.9') and extra == 'macos_platform' +Requires-Dist: pyobjc-framework-cfnetwork; (python_version >= '3.9') and extra == 'macos_platform' +Requires-Dist: pyobjc-framework-cfnetwork<11; (python_version < '3.9') and extra == 'macos_platform' +Requires-Dist: pyobjc-framework-cocoa; (python_version >= '3.9') and extra == 'macos_platform' +Requires-Dist: pyobjc-framework-cocoa<11; (python_version < '3.9') and extra == 'macos_platform' Requires-Dist: pyopenssl>=21.0.0; extra == 'macos_platform' Requires-Dist: pyserial>=3.0; extra == 'macos_platform' Requires-Dist: pywin32!=226; (platform_system == 'Windows') and extra == 'macos_platform' Requires-Dist: service-identity>=18.1.0; extra == 'macos_platform' +Requires-Dist: wsproto; extra == 'macos_platform' Provides-Extra: mypy Requires-Dist: appdirs>=1.4.0; extra == 'mypy' Requires-Dist: bcrypt>=3.1.3; extra == 'mypy' @@ -168,7 +181,7 @@ Requires-Dist: idna>=2.4; extra == 'mypy' Requires-Dist: mypy-zope==1.0.6; extra == 'mypy' Requires-Dist: mypy==1.10.1; extra == 'mypy' Requires-Dist: priority<2.0,>=1.1.0; extra == 'mypy' -Requires-Dist: pydoctor~=23.9.0; extra == 'mypy' +Requires-Dist: pydoctor~=24.11.1; extra == 'mypy' Requires-Dist: pyflakes~=2.2; extra == 'mypy' Requires-Dist: pyhamcrest>=2; extra == 'mypy' Requires-Dist: pyopenssl>=21.0.0; extra == 'mypy' @@ -182,6 +195,7 @@ Requires-Dist: towncrier~=23.6; extra == 'mypy' Requires-Dist: twistedchecker~=0.7; extra == 'mypy' Requires-Dist: types-pyopenssl; extra == 'mypy' Requires-Dist: types-setuptools; extra == 'mypy' +Requires-Dist: wsproto; extra == 'mypy' Provides-Extra: osx-platform Requires-Dist: appdirs>=1.4.0; extra == 'osx-platform' Requires-Dist: bcrypt>=3.1.3; extra == 'osx-platform' @@ -193,13 +207,17 @@ Requires-Dist: hypothesis>=6.56; extra == 'osx-platform' Requires-Dist: idna>=2.4; extra == 'osx-platform' Requires-Dist: priority<2.0,>=1.1.0; extra == 'osx-platform' Requires-Dist: pyhamcrest>=2; extra == 'osx-platform' -Requires-Dist: pyobjc-core; extra == 'osx-platform' -Requires-Dist: pyobjc-framework-cfnetwork; extra == 'osx-platform' -Requires-Dist: pyobjc-framework-cocoa; extra == 'osx-platform' +Requires-Dist: pyobjc-core; (python_version >= '3.9') and extra == 'osx-platform' +Requires-Dist: pyobjc-core<11; (python_version < '3.9') and extra == 'osx-platform' +Requires-Dist: pyobjc-framework-cfnetwork; (python_version >= '3.9') and extra == 'osx-platform' +Requires-Dist: pyobjc-framework-cfnetwork<11; (python_version < '3.9') and extra == 'osx-platform' +Requires-Dist: pyobjc-framework-cocoa; (python_version >= '3.9') and extra == 'osx-platform' +Requires-Dist: pyobjc-framework-cocoa<11; (python_version < '3.9') and extra == 'osx-platform' Requires-Dist: pyopenssl>=21.0.0; extra == 'osx-platform' Requires-Dist: pyserial>=3.0; extra == 'osx-platform' Requires-Dist: pywin32!=226; (platform_system == 'Windows') and extra == 'osx-platform' Requires-Dist: service-identity>=18.1.0; extra == 'osx-platform' +Requires-Dist: wsproto; extra == 'osx-platform' Provides-Extra: osx_platform Requires-Dist: appdirs>=1.4.0; extra == 'osx_platform' Requires-Dist: bcrypt>=3.1.3; extra == 'osx_platform' @@ -211,13 +229,17 @@ Requires-Dist: hypothesis>=6.56; extra == 'osx_platform' Requires-Dist: idna>=2.4; extra == 'osx_platform' Requires-Dist: priority<2.0,>=1.1.0; extra == 'osx_platform' Requires-Dist: pyhamcrest>=2; extra == 'osx_platform' -Requires-Dist: pyobjc-core; extra == 'osx_platform' -Requires-Dist: pyobjc-framework-cfnetwork; extra == 'osx_platform' -Requires-Dist: pyobjc-framework-cocoa; extra == 'osx_platform' +Requires-Dist: pyobjc-core; (python_version >= '3.9') and extra == 'osx_platform' +Requires-Dist: pyobjc-core<11; (python_version < '3.9') and extra == 'osx_platform' +Requires-Dist: pyobjc-framework-cfnetwork; (python_version >= '3.9') and extra == 'osx_platform' +Requires-Dist: pyobjc-framework-cfnetwork<11; (python_version < '3.9') and extra == 'osx_platform' +Requires-Dist: pyobjc-framework-cocoa; (python_version >= '3.9') and extra == 'osx_platform' +Requires-Dist: pyobjc-framework-cocoa<11; (python_version < '3.9') and extra == 'osx_platform' Requires-Dist: pyopenssl>=21.0.0; extra == 'osx_platform' Requires-Dist: pyserial>=3.0; extra == 'osx_platform' Requires-Dist: pywin32!=226; (platform_system == 'Windows') and extra == 'osx_platform' Requires-Dist: service-identity>=18.1.0; extra == 'osx_platform' +Requires-Dist: wsproto; extra == 'osx_platform' Provides-Extra: serial Requires-Dist: pyserial>=3.0; extra == 'serial' Requires-Dist: pywin32!=226; (platform_system == 'Windows') and extra == 'serial' @@ -230,6 +252,8 @@ Provides-Extra: tls Requires-Dist: idna>=2.4; extra == 'tls' Requires-Dist: pyopenssl>=21.0.0; extra == 'tls' Requires-Dist: service-identity>=18.1.0; extra == 'tls' +Provides-Extra: websocket +Requires-Dist: wsproto; extra == 'websocket' Provides-Extra: windows-platform Requires-Dist: appdirs>=1.4.0; extra == 'windows-platform' Requires-Dist: bcrypt>=3.1.3; extra == 'windows-platform' @@ -247,6 +271,7 @@ Requires-Dist: pywin32!=226; extra == 'windows-platform' Requires-Dist: pywin32!=226; (platform_system == 'Windows') and extra == 'windows-platform' Requires-Dist: service-identity>=18.1.0; extra == 'windows-platform' Requires-Dist: twisted-iocpsupport>=1.0.2; extra == 'windows-platform' +Requires-Dist: wsproto; extra == 'windows-platform' Provides-Extra: windows_platform Requires-Dist: appdirs>=1.4.0; extra == 'windows_platform' Requires-Dist: bcrypt>=3.1.3; extra == 'windows_platform' @@ -264,6 +289,7 @@ Requires-Dist: pywin32!=226; extra == 'windows_platform' Requires-Dist: pywin32!=226; (platform_system == 'Windows') and extra == 'windows_platform' Requires-Dist: service-identity>=18.1.0; extra == 'windows_platform' Requires-Dist: twisted-iocpsupport>=1.0.2; extra == 'windows_platform' +Requires-Dist: wsproto; extra == 'windows_platform' Description-Content-Type: text/x-rst Twisted @@ -367,7 +393,7 @@ Or, for speed, use pre-commit directly:: Copyright --------- -All of the code in this distribution is Copyright (c) 2001-2024 Twisted Matrix Laboratories. +All of the code in this distribution is Copyright (c) 2001-2025 Twisted Matrix Laboratories. Twisted is made available under the MIT license. The included `LICENSE <https://github.com/twisted/twisted/blob/trunk/LICENSE>`_ file describes this in detail. diff --git a/contrib/python/Twisted/py3/LICENSE b/contrib/python/Twisted/py3/LICENSE index 46747386e3e..3a9fdc29b98 100644 --- a/contrib/python/Twisted/py3/LICENSE +++ b/contrib/python/Twisted/py3/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2001-2024 +Copyright (c) 2001-2025 Allen Short Amber Hawkie Brown Andrew Bennetts diff --git a/contrib/python/Twisted/py3/README.rst b/contrib/python/Twisted/py3/README.rst index bc2e901c67f..9afa4ffb3db 100644 --- a/contrib/python/Twisted/py3/README.rst +++ b/contrib/python/Twisted/py3/README.rst @@ -99,7 +99,7 @@ Or, for speed, use pre-commit directly:: Copyright --------- -All of the code in this distribution is Copyright (c) 2001-2024 Twisted Matrix Laboratories. +All of the code in this distribution is Copyright (c) 2001-2025 Twisted Matrix Laboratories. Twisted is made available under the MIT license. The included `LICENSE <LICENSE>`_ file describes this in detail. diff --git a/contrib/python/Twisted/py3/twisted/_version.py b/contrib/python/Twisted/py3/twisted/_version.py index 43da56319b4..02290f83ab6 100644 --- a/contrib/python/Twisted/py3/twisted/_version.py +++ b/contrib/python/Twisted/py3/twisted/_version.py @@ -7,5 +7,5 @@ Provides Twisted version information. from incremental import Version -__version__ = Version("Twisted", 24, 11, 0) +__version__ = Version("Twisted", 25, 5, 0) __all__ = ["__version__"] diff --git a/contrib/python/Twisted/py3/twisted/conch/client/connect.py b/contrib/python/Twisted/py3/twisted/conch/client/connect.py index f21f16768bb..1683e7f0704 100644 --- a/contrib/python/Twisted/py3/twisted/conch/client/connect.py +++ b/contrib/python/Twisted/py3/twisted/conch/client/connect.py @@ -1,24 +1,46 @@ # Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. -# +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable + +from twisted.internet.defer import Deferred +from twisted.python.failure import Failure + +if TYPE_CHECKING: + from twisted.conch.client.options import ConchOptions + from twisted.conch.ssh.userauth import SSHUserAuthClient + from twisted.conch.client import direct -connectTypes = {"direct": direct.connect} +connectTypes: dict[ + str, + Callable[[str, int, ConchOptions, direct._VHK, SSHUserAuthClient], Deferred[None]], +] = { + "direct": direct.connect, +} -def connect(host, port, options, verifyHostKey, userAuthObject): +def connect( + host: str, + port: int, + options: ConchOptions, + verifyHostKey: direct._VHK, + userAuthObject: SSHUserAuthClient, +) -> Deferred[None]: useConnects = ["direct"] - return _ebConnect( - None, useConnects, host, port, options, verifyHostKey, userAuthObject - ) - - -def _ebConnect(f, useConnects, host, port, options, vhk, uao): - if not useConnects: - return f - connectType = useConnects.pop(0) - f = connectTypes[connectType] - d = f(host, port, options, vhk, uao) - d.addErrback(_ebConnect, useConnects, host, port, options, vhk, uao) - return d + + def _ebConnect(interimResult: Failure | None, /) -> Deferred[None] | None | Failure: + if not useConnects: + return interimResult + connectType = useConnects.pop(0) + f = connectTypes[connectType] + d = f(host, port, options, verifyHostKey, userAuthObject) + d.addErrback(_ebConnect) + return d + + start: Deferred[None] = Deferred() + start.callback(None) + start.addCallback(_ebConnect) + return start diff --git a/contrib/python/Twisted/py3/twisted/conch/client/default.py b/contrib/python/Twisted/py3/twisted/conch/client/default.py index daf4cf33719..7038f8c0107 100644 --- a/contrib/python/Twisted/py3/twisted/conch/client/default.py +++ b/contrib/python/Twisted/py3/twisted/conch/client/default.py @@ -17,12 +17,15 @@ import io import os import sys from base64 import decodebytes +from typing import TYPE_CHECKING from twisted.conch.client import agent from twisted.conch.client.knownhosts import ConsoleUI, KnownHostsFile from twisted.conch.error import ConchError from twisted.conch.ssh import common, keys, userauth +from twisted.conch.ssh.transport import SSHClientTransport from twisted.internet import defer, protocol, reactor +from twisted.internet.defer import Deferred from twisted.python.compat import nativeString from twisted.python.filepath import FilePath @@ -36,7 +39,9 @@ _open = open _input = input -def verifyHostKey(transport, host, pubKey, fingerprint): +def verifyHostKey( + transport: SSHClientTransport, host: bytes, pubKey: bytes, fingerprint: str +) -> Deferred[bool]: """ Verify a host's key. @@ -56,26 +61,29 @@ def verifyHostKey(transport, host, pubKey, fingerprint): equivalent that could be used. @param host: Due to a bug in L{SSHClientTransport.verifyHostKey}, this is - always the dotted-quad IP address of the host being connected to. - @type host: L{str} + always the dotted-quad IP address of the host being connected to. @param transport: the client transport which is attempting to connect to - the given host. - @type transport: L{SSHClientTransport} + the given host. @param fingerprint: the fingerprint of the given public key, in - xx:xx:xx:... format. This is ignored in favor of getting the fingerprint - from the key itself. - @type fingerprint: L{str} + xx:xx:xx:... format. This is ignored in favor of getting the + fingerprint from the key itself. @param pubKey: The public key of the server being connected to. - @type pubKey: L{str} - @return: a L{Deferred} which fires with C{1} if the key was successfully - verified, or fails if the key could not be successfully verified. Failure - types may include L{HostKeyChanged}, L{UserRejectedKey}, L{IOError} or - L{KeyboardInterrupt}. + @return: a L{Deferred} which fires with C{True} if the key was successfully + verified, or fails if the key could not be successfully verified. + Failure types may include L{HostKeyChanged}, L{UserRejectedKey}, + L{IOError} or L{KeyboardInterrupt}. """ + if TYPE_CHECKING: + # this is just a structured assumption that we are making about the + # transport's factory; behind a TYPE_CHECKING flag because we use some + # test fakes and don't want to nail down the type that much. + from twisted.conch.client.direct import SSHClientFactory + + assert isinstance(transport.factory, SSHClientFactory) actualHost = transport.factory.options["host"] actualKey = keys.Key.fromString(pubKey) kh = KnownHostsFile.fromPath( diff --git a/contrib/python/Twisted/py3/twisted/conch/client/direct.py b/contrib/python/Twisted/py3/twisted/conch/client/direct.py index d9f4828ec5f..33fd1d2df46 100644 --- a/contrib/python/Twisted/py3/twisted/conch/client/direct.py +++ b/contrib/python/Twisted/py3/twisted/conch/client/direct.py @@ -1,51 +1,83 @@ # Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable from twisted.conch import error from twisted.conch.ssh import transport from twisted.internet import defer, protocol, reactor +from twisted.internet.address import IPv4Address, IPv6Address +from twisted.internet.defer import Deferred, maybeDeferred +from twisted.internet.interfaces import ( + IAddress, + IConnector, + IListeningPort, + IReactorTCP, +) +from twisted.python.failure import Failure + +if TYPE_CHECKING: + from twisted.conch.client.options import ConchOptions + from twisted.conch.ssh.userauth import SSHUserAuthClient class SSHClientFactory(protocol.ClientFactory): - def __init__(self, d, options, verifyHostKey, userAuthObject): - self.d = d + def __init__( + self, + d: Deferred[None], + options: ConchOptions, + verifyHostKey: _VHK, + userAuthObject: SSHUserAuthClient, + ) -> None: + self.d: Deferred[None] | None = d self.options = options self.verifyHostKey = verifyHostKey self.userAuthObject = userAuthObject - def clientConnectionLost(self, connector, reason): + def clientConnectionLost(self, connector: IConnector, reason: Failure) -> None: if self.options["reconnect"]: connector.connect() - def clientConnectionFailed(self, connector, reason): + def clientConnectionFailed(self, connector: IConnector, reason: Failure) -> None: if self.d is None: return d, self.d = self.d, None d.errback(reason) - def buildProtocol(self, addr): + def buildProtocol(self, addr: IAddress) -> SSHClientTransport: trans = SSHClientTransport(self) if self.options["ciphers"]: trans.supportedCiphers = self.options["ciphers"] if self.options["macs"]: trans.supportedMACs = self.options["macs"] if self.options["compress"]: - trans.supportedCompressions[0:1] = ["zlib"] + trans.supportedCompressions[0:1] = [b"zlib"] if self.options["host-key-algorithms"]: trans.supportedPublicKeys = self.options["host-key-algorithms"] return trans class SSHClientTransport(transport.SSHClientTransport): - def __init__(self, factory): + # pre-mypy LSP violation + factory: SSHClientFactory # type:ignore[assignment] + + def __init__(self, factory: SSHClientFactory) -> None: self.factory = factory - self.unixServer = None + self.unixServer: None | IListeningPort = None - def connectionLost(self, reason): + def connectionLost(self, reason: Failure | None = None) -> None: if self.unixServer: - d = self.unixServer.stopListening() - self.unixServer = None + # The C{unixServer} attribute is untested, and it's not entirely + # clear that it does anything at all. It appears to be a vestigial + # attempt to support something like OpenSSH's ControlMaster client + # option; at some point we should either document and test it, or + # remove it. + + # https://github.com/twisted/twisted/issues/12418 + d = maybeDeferred(self.unixServer.stopListening) # pragma: no cover + self.unixServer = None # pragma: no cover else: d = defer.succeed(None) d.addCallback( @@ -75,9 +107,15 @@ class SSHClientTransport(transport.SSHClientTransport): if alwaysDisplay: # XXX what should happen here? print(message) - def verifyHostKey(self, pubKey, fingerprint): + def verifyHostKey(self, pubKey: bytes, fingerprint: str) -> Deferred[bool]: + transport = self.transport + assert transport is not None + peer = transport.getPeer() + assert isinstance( + peer, (IPv4Address, IPv6Address) + ), "Address must have a host to verify against." return self.factory.verifyHostKey( - self, self.transport.getPeer().host, pubKey, fingerprint + self, peer.host.encode("utf-8"), pubKey, fingerprint ) def setService(self, service): @@ -91,8 +129,17 @@ class SSHClientTransport(transport.SSHClientTransport): self.requestService(self.factory.userAuthObject) -def connect(host, port, options, verifyHostKey, userAuthObject): - d = defer.Deferred() +_VHK = Callable[[SSHClientTransport, bytes, bytes, str], Deferred[bool]] + + +def connect( + host: str, + port: int, + options: ConchOptions, + verifyHostKey: _VHK, + userAuthObject: SSHUserAuthClient, +) -> Deferred[None]: + d: Deferred[None] = defer.Deferred() factory = SSHClientFactory(d, options, verifyHostKey, userAuthObject) - reactor.connectTCP(host, port, factory) + IReactorTCP(reactor).connectTCP(host, port, factory) return d diff --git a/contrib/python/Twisted/py3/twisted/conch/client/knownhosts.py b/contrib/python/Twisted/py3/twisted/conch/client/knownhosts.py index 44118512bd2..1aa4b477c27 100644 --- a/contrib/python/Twisted/py3/twisted/conch/client/knownhosts.py +++ b/contrib/python/Twisted/py3/twisted/conch/client/knownhosts.py @@ -15,7 +15,7 @@ import sys from binascii import Error as DecodeError, a2b_base64, b2a_base64 from contextlib import closing from hashlib import sha1 -from typing import IO, Callable, Literal +from typing import IO, Callable, Iterable, Literal from zope.interface import implementer @@ -33,31 +33,27 @@ from twisted.python.util import FancyEqMixin log = Logger() -def _b64encode(s): +def _b64encode(s: bytes) -> bytes: """ Encode a binary string as base64 with no trailing newline. @param s: The string to encode. - @type s: L{bytes} @return: The base64-encoded string. - @rtype: L{bytes} """ return b2a_base64(s).strip() -def _extractCommon(string): +def _extractCommon(string: bytes) -> tuple[bytes, bytes, Key, bytes | None]: """ Extract common elements of base64 keys from an entry in a hosts file. @param string: A known hosts file entry (a single line). - @type string: L{bytes} @return: a 4-tuple of hostname data (L{bytes}), ssh key type (L{bytes}), key (L{Key}), and comment (L{bytes} or L{None}). The hostname data is simply the beginning of the line up to the first occurrence of whitespace. - @rtype: L{tuple} """ elements = string.split(None, 2) if len(elements) != 3: @@ -86,26 +82,25 @@ class _BaseEntry: @type publicKey: L{twisted.conch.ssh.keys.Key} @ivar comment: Trailing garbage after the key line. - @type comment: L{bytes} + @type comment: L{bytes} or C{None} """ - def __init__(self, keyType, publicKey, comment): + def __init__(self, keyType: bytes, publicKey: Key, comment: bytes | None) -> None: self.keyType = keyType self.publicKey = publicKey self.comment = comment - def matchesKey(self, keyObject): + def matchesKey(self, keyObject: Key) -> bool: """ Check to see if this entry matches a given key object. @param keyObject: A public key object to check. - @type keyObject: L{Key} @return: C{True} if this entry's key matches C{keyObject}, C{False} otherwise. - @rtype: L{bool} """ - return self.publicKey == keyObject + result = self.publicKey == keyObject + return result @implementer(IKnownHostEntry) @@ -118,7 +113,11 @@ class PlainEntry(_BaseEntry): """ def __init__( - self, hostnames: list[bytes], keyType: bytes, publicKey: Key, comment: bytes + self, + hostnames: list[bytes], + keyType: bytes, + publicKey: Key, + comment: bytes | None, ): self._hostnames: list[bytes] = hostnames super().__init__(keyType, publicKey, comment) @@ -188,26 +187,28 @@ class UnparsedEntry: parsed; therefore it matches no keys and no hosts. """ - def __init__(self, string): + keyType: None = None + + def __init__(self, string: bytes) -> None: """ Create an unparsed entry from a line in a known_hosts file which cannot otherwise be parsed. """ self._string = string - def matchesHost(self, hostname): + def matchesHost(self, hostname: bytes) -> bool: """ Always returns False. """ return False - def matchesKey(self, key): + def matchesKey(self, key: Key) -> bool: """ Always returns False. """ return False - def toString(self): + def toString(self) -> bytes: """ Returns the input line, without its newline if one was given. @@ -218,18 +219,15 @@ class UnparsedEntry: return self._string.rstrip(b"\n") -def _hmacedString(key, string): +def _hmacedString(key: bytes, string: bytes | str) -> bytes: """ Return the SHA-1 HMAC hash of the given key and string. @param key: The HMAC key. - @type key: L{bytes} @param string: The string to be hashed. - @type string: L{bytes} @return: The keyed hash value. - @rtype: L{bytes} """ hash = hmac.HMAC(key, digestmod=sha1) if isinstance(string, str): @@ -298,7 +296,7 @@ class HashedEntry(_BaseEntry, FancyEqMixin): self = cls(a2b_base64(hostSalt), a2b_base64(hostHash), keyType, key, comment) return self - def matchesHost(self, hostname): + def matchesHost(self, hostname: bytes) -> bool: """ Implement L{IKnownHostEntry.matchesHost} to compare the hash of the input to the stored hash. @@ -315,7 +313,7 @@ class HashedEntry(_BaseEntry, FancyEqMixin): _hmacedString(self._hostSalt, hostname), self._hostHash ) - def toString(self): + def toString(self) -> bytes: """ Implement L{IKnownHostEntry.toString} by base64-encoding the salt, host hash, and key. @@ -373,7 +371,7 @@ class KnownHostsFile: """ return self._savePath - def iterentries(self): + def iterentries(self) -> Iterable[IKnownHostEntry]: """ Iterate over the host entries in this file. @@ -404,25 +402,22 @@ class KnownHostsFile: entry = UnparsedEntry(line) yield entry - def hasHostKey(self, hostname, key): + def hasHostKey(self, hostname: bytes, key: Key) -> bool: """ Check for an entry with matching hostname and key. @param hostname: A hostname or IP address literal to check for. - @type hostname: L{bytes} @param key: The public key to check for. - @type key: L{Key} - @return: C{True} if the given hostname and key are present in this file, - C{False} if they are not. - @rtype: L{bool} + @return: C{True} if the given hostname and key are present in this + file, C{False} if they are not. @raise HostKeyChanged: if the host key found for the given hostname does not match the given key. """ for lineidx, entry in enumerate(self.iterentries(), -len(self._added)): - if entry.matchesHost(hostname) and entry.keyType == key.sshType(): + if entry.keyType == key.sshType() and entry.matchesHost(hostname): if entry.matchesKey(key): return True else: @@ -569,7 +564,7 @@ class ConsoleUI: console, to be used during key verification. """ - def __init__(self, opener: Callable[[], IO[bytes]]): + def __init__(self, opener: Callable[[], IO[bytes]]) -> None: """ @param opener: A no-argument callable which should open a console binary-mode file-like object to be used for reading and writing. diff --git a/contrib/python/Twisted/py3/twisted/conch/endpoints.py b/contrib/python/Twisted/py3/twisted/conch/endpoints.py index 3269532acd1..966669edec7 100644 --- a/contrib/python/Twisted/py3/twisted/conch/endpoints.py +++ b/contrib/python/Twisted/py3/twisted/conch/endpoints.py @@ -31,13 +31,18 @@ from twisted.conch.ssh.connection import SSHConnection from twisted.conch.ssh.keys import Key from twisted.conch.ssh.transport import SSHClientTransport from twisted.conch.ssh.userauth import SSHUserAuthClient +from twisted.internet.address import IPv4Address, IPv6Address from twisted.internet.defer import CancelledError, Deferred, succeed from twisted.internet.endpoints import TCP4ClientEndpoint, connectProtocol from twisted.internet.error import ConnectionDone, ProcessTerminated -from twisted.internet.interfaces import IStreamClientEndpoint +from twisted.internet.interfaces import ( + IReactorTCP, + IStreamClientEndpoint, + ITCPTransport, +) from twisted.internet.protocol import Factory from twisted.logger import Logger -from twisted.python.compat import nativeString, networkString +from twisted.python.compat import nativeString from twisted.python.failure import Failure from twisted.python.filepath import FilePath @@ -297,8 +302,8 @@ class _UserAuth(SSHUserAuthClient): authentication, and delegating authentication to an agent. """ - password = None - keys = None + password: bytes | None = None + keys: list[Key] | None = None agent = None def getPublicKey(self): @@ -414,16 +419,23 @@ class _CommandTransport(SSHClientTransport): _hostKeyFailure = None - _userauth = None + _userauth: _UserAuth | None = None - def __init__(self, creator): + def __init__(self, creator: _NewConnectionHelper) -> None: """ @param creator: The L{_NewConnectionHelper} that created this connection. - - @type creator: L{_NewConnectionHelper}. """ - self.connectionReady = Deferred(lambda d: self.transport.abortConnection()) + + def cancelReady(d: Deferred[None]) -> None: + transport = ITCPTransport(self.transport, None) + # adaptation is papering over an annoying type-punning issue here, + # we more or less have to run over an abortable transport, so not + # testing the negative branch. + if transport is not None: # pragma: no branch + transport.abortConnection() + + self.connectionReady: Deferred[None] = Deferred(cancelReady) # Clear the reference to that deferred to help the garbage collector # and to signal to other parts of this implementation (in particular # connectionLost) that it has already been fired and does not need to @@ -436,7 +448,7 @@ class _CommandTransport(SSHClientTransport): self.connectionReady.addBoth(readyFired) self.creator = creator - def verifyHostKey(self, hostKey, fingerprint): + def verifyHostKey(self, hostKey: Key, fingerprint: bytes) -> Deferred[bool]: """ Ask the L{KnownHostsFile} provider available on the factory which created this protocol this protocol to verify the given host key. @@ -445,30 +457,29 @@ class _CommandTransport(SSHClientTransport): L{KnownHostsFile.verifyHostKey}. """ hostname = self.creator.hostname - ip = networkString(self.transport.getPeer().host) - + transport = self.transport + assert transport is not None + peer = transport.getPeer() + assert isinstance(peer, (IPv4Address, IPv6Address)) + ip = peer.host.encode("ascii") self._state = b"SECURING" - d = self.creator.knownHosts.verifyHostKey( + return self.creator.knownHosts.verifyHostKey( self.creator.ui, hostname, ip, Key.fromString(hostKey) - ) - d.addErrback(self._saveHostKeyFailure) - return d + ).addErrback(self._saveHostKeyFailure) - def _saveHostKeyFailure(self, reason): + def _saveHostKeyFailure(self, reason: Failure) -> Failure: """ When host key verification fails, record the reason for the failure in order to fire a L{Deferred} with it later. @param reason: The cause of the host key verification failure. - @type reason: L{Failure} @return: C{reason} - @rtype: L{Failure} """ self._hostKeyFailure = reason return reason - def connectionSecure(self): + def connectionSecure(self) -> None: """ When the connection is secure, start the authentication process. """ @@ -481,17 +492,18 @@ class _CommandTransport(SSHClientTransport): if self.creator.keys: self._userauth.keys = list(self.creator.keys) - if self.creator.agentEndpoint is not None: - d = self._userauth.connectToAgent(self.creator.agentEndpoint) - else: - d = succeed(None) + d = ( + succeed(None) + if self.creator.agentEndpoint is None + else self._userauth.connectToAgent(self.creator.agentEndpoint) + ) def maybeGotAgent(ignored): self.requestService(self._userauth) d.addBoth(maybeGotAgent) - def connectionLost(self, reason): + def connectionLost(self, reason: Failure | None = None) -> None: """ When the underlying connection to the SSH server is lost, if there were any connection setup errors, propagate them. Also, clean up the @@ -529,7 +541,7 @@ class SSHCommandClientEndpoint: command invocations over a single SSH connection. """ - def __init__(self, creator, command): + def __init__(self, creator: _ISSHConnectionCreator, command: bytes) -> None: """ @param creator: An L{_ISSHConnectionCreator} provider which will be used to set up the SSH connection which will be used to run a @@ -550,17 +562,17 @@ class SSHCommandClientEndpoint: @classmethod def newConnection( cls, - reactor, - command, - username, - hostname, - port=None, - keys=None, - password=None, - agentEndpoint=None, - knownHosts=None, - ui=None, - ): + reactor: IReactorTCP, + command: bytes, + username: bytes, + hostname: bytes, + port: int | None = None, + keys: list[Key] | None = None, + password: bytes | None = None, + agentEndpoint: IStreamClientEndpoint | None = None, + knownHosts: str | None = None, + ui: ConsoleUI | None = None, + ) -> SSHCommandClientEndpoint: """ Create and return a new endpoint which will try to create a new connection to an SSH server and run a command over it. It will also @@ -569,44 +581,36 @@ class SSHCommandClientEndpoint: L{Deferred} is cancelled. @param reactor: The reactor to use to establish the connection. - @type reactor: L{IReactorTCP} provider @param command: See L{__init__}'s C{command} argument. @param username: The username with which to authenticate to the SSH server. - @type username: L{bytes} @param hostname: The hostname of the SSH server. - @type hostname: L{bytes} @param port: The port number of the SSH server. By default, the standard SSH port number is used. - @type port: L{int} @param keys: Private keys with which to authenticate to the SSH server, if key authentication is to be attempted (otherwise L{None}). - @type keys: L{list} of L{Key} @param password: The password with which to authenticate to the SSH server, if password authentication is to be attempted (otherwise L{None}). - @type password: L{bytes} or L{None} @param agentEndpoint: An L{IStreamClientEndpoint} provider which may be used to connect to an SSH agent, if one is to be used to help with authentication. - @type agentEndpoint: L{IStreamClientEndpoint} provider - @param knownHosts: The currently known host keys, used to check the - host key presented by the server we actually connect to. - @type knownHosts: L{KnownHostsFile} + @param knownHosts: The path to the currently known host keys file, used + to check the host key presented by the server we actually connect + to. @param ui: An object for interacting with users to make decisions about whether to accept the server host keys. If L{None}, a L{ConsoleUI} connected to /dev/tty will be used; if /dev/tty is unavailable, an object which answers C{b"no"} to all prompts will be used. - @type ui: L{None} or L{ConsoleUI} @return: A new instance of C{cls} (probably L{SSHCommandClientEndpoint}). @@ -707,17 +711,19 @@ class _NewConnectionHelper: _KNOWN_HOSTS = _KNOWN_HOSTS port = 22 + knownHosts: KnownHostsFile + def __init__( self, reactor: Any, - hostname: str, - port: int, - command: str, - username: str, - keys: str, - password: str, - agentEndpoint: str, - knownHosts: str | None, + hostname: bytes, + port: int | None, + command: bytes, + username: bytes, + keys: list[Key] | None, + password: bytes | None, + agentEndpoint: IStreamClientEndpoint | None, + knownHosts: str | None | KnownHostsFile, ui: ConsoleUI | None, tty: FilePath[bytes] | FilePath[str] = FilePath(b"/dev/tty"), ): @@ -733,12 +739,13 @@ class _NewConnectionHelper: self.port = port self.command = command self.username = username - self.keys = keys + self.keys = [] if keys is None else keys self.password = password self.agentEndpoint = agentEndpoint - if knownHosts is None: - knownHosts = self._knownHosts() - self.knownHosts = knownHosts + if isinstance(knownHosts, KnownHostsFile): + self.knownHosts = knownHosts + else: + self.knownHosts = self._knownHosts(knownHosts) if ui is None: ui = ConsoleUI(self._opener) @@ -760,14 +767,19 @@ class _NewConnectionHelper: return BytesIO(b"no") @classmethod - def _knownHosts(cls): + def _knownHosts(cls, path: str | None = None) -> KnownHostsFile: """ - @return: A L{KnownHostsFile} instance pointed at the user's personal I{known hosts} file. @rtype: L{KnownHostsFile} """ - return KnownHostsFile.fromPath(FilePath(expanduser(cls._KNOWN_HOSTS))) + if path is None: # pragma: no branch + # negative branch untested because this fallback path requires user + # configuration that tests shouldn't be messing with + # directly. (This should be factored out for better testability in + # terms of coverage.) + path = expanduser(cls._KNOWN_HOSTS) + return KnownHostsFile.fromPath(FilePath(path)) def secureConnection(self): """ diff --git a/contrib/python/Twisted/py3/twisted/conch/error.py b/contrib/python/Twisted/py3/twisted/conch/error.py index a923b9a4c4a..fcf2c1d2f43 100644 --- a/contrib/python/Twisted/py3/twisted/conch/error.py +++ b/contrib/python/Twisted/py3/twisted/conch/error.py @@ -90,7 +90,7 @@ class HostKeyChanged(Exception): """ def __init__(self, offendingEntry, path, lineno): - Exception.__init__(self) + Exception.__init__(self, offendingEntry, path, lineno) self.offendingEntry = offendingEntry self.path = path self.lineno = lineno diff --git a/contrib/python/Twisted/py3/twisted/conch/interfaces.py b/contrib/python/Twisted/py3/twisted/conch/interfaces.py index 965519b0ea5..59fe7145f05 100644 --- a/contrib/python/Twisted/py3/twisted/conch/interfaces.py +++ b/contrib/python/Twisted/py3/twisted/conch/interfaces.py @@ -370,6 +370,12 @@ class IKnownHostEntry(Interface): @since: 8.2 """ + keyType: bytes | None = Attribute( + """ + The SSH key type identifier for this key. + """ + ) + def matchesKey(key: Key) -> bool: """ Return True if this entry matches the given Key object, False diff --git a/contrib/python/Twisted/py3/twisted/conch/scripts/conch.py b/contrib/python/Twisted/py3/twisted/conch/scripts/conch.py index f3e5479bd91..74e3a9f9fe0 100644 --- a/contrib/python/Twisted/py3/twisted/conch/scripts/conch.py +++ b/contrib/python/Twisted/py3/twisted/conch/scripts/conch.py @@ -16,7 +16,7 @@ import signal import struct import sys import tty -from typing import List, Tuple +from typing import Any, List, Tuple from twisted.conch.client import connect, default from twisted.conch.client.options import ConchOptions @@ -113,7 +113,7 @@ class ClientOptions(ConchOptions): # Rest of code in "run" -options = None +options: Any = None conn = None exitStatus = 0 old = None @@ -198,20 +198,21 @@ def _stopReactor(): pass -def doConnect(): +def doConnect() -> None: if "@" in options["host"]: options["user"], options["host"] = options["host"].split("@", 1) if not options.identitys: options.identitys = ["~/.ssh/id_rsa", "~/.ssh/id_dsa"] - host = options["host"] + if not options["user"]: options["user"] = getpass.getuser() if not options["port"]: options["port"] = 22 else: options["port"] = int(options["port"]) - host = options["host"] - port = options["port"] + + host: str = options["host"] + port: int = options["port"] vhk = default.verifyHostKey if not options["host-key-algorithms"]: options["host-key-algorithms"] = default.getHostKeyAlgorithms(host, options) diff --git a/contrib/python/Twisted/py3/twisted/conch/ssh/_kex.py b/contrib/python/Twisted/py3/twisted/conch/ssh/_kex.py index c23acec219c..0b04f1f5f6b 100644 --- a/contrib/python/Twisted/py3/twisted/conch/ssh/_kex.py +++ b/contrib/python/Twisted/py3/twisted/conch/ssh/_kex.py @@ -6,26 +6,37 @@ SSH key exchange handling. """ +from __future__ import annotations from hashlib import sha1, sha256, sha384, sha512 +from typing import TYPE_CHECKING, Protocol from zope.interface import Attribute, Interface, implementer from twisted.conch import error +if TYPE_CHECKING: + # NB: Not a real attribute at runtime. + from hashlib import _Hash + + +class _HashFactory(Protocol): + def __call__(self, data: bytes = ...) -> _Hash: + ... + class _IKexAlgorithm(Interface): """ An L{_IKexAlgorithm} describes a key exchange algorithm. """ - preference = Attribute( + preference: int = Attribute( "An L{int} giving the preference of the algorithm when negotiating " "key exchange. Algorithms with lower precedence values are more " "preferred." ) - hashProcessor = Attribute( + hashProcessor: _HashFactory = Attribute( "A callable hash algorithm constructor (e.g. C{hashlib.sha256}) " "suitable for use with this key exchange algorithm." ) @@ -175,7 +186,7 @@ class _DHGroup14SHA1: # Which ECDH hash function to use is dependent on the size. -_kexAlgorithms = { +_kexAlgorithms: dict[bytes, _IKexAlgorithm] = { b"curve25519-sha256": _Curve25519SHA256(), b"[email protected]": _Curve25519SHA256LibSSH(), b"diffie-hellman-group-exchange-sha256": _DHGroupExchangeSHA256(), @@ -187,7 +198,7 @@ _kexAlgorithms = { } -def getKex(kexAlgorithm): +def getKex(kexAlgorithm: bytes) -> _IKexAlgorithm: """ Get a description of a named key exchange algorithm. @@ -201,53 +212,47 @@ def getKex(kexAlgorithm): @raises ConchError: if the key exchange algorithm is not found. """ if kexAlgorithm not in _kexAlgorithms: - raise error.ConchError(f"Unsupported key exchange algorithm: {kexAlgorithm}") + raise error.ConchError(f"Unsupported key exchange algorithm: {kexAlgorithm!r}") return _kexAlgorithms[kexAlgorithm] -def isEllipticCurve(kexAlgorithm): +def isEllipticCurve(kexAlgorithm: bytes) -> bool: """ Returns C{True} if C{kexAlgorithm} is an elliptic curve. @param kexAlgorithm: The key exchange algorithm name. - @type kexAlgorithm: C{str} - @return: C{True} if C{kexAlgorithm} is an elliptic curve, - otherwise C{False}. - @rtype: C{bool} + @return: C{True} if C{kexAlgorithm} is an elliptic curve, otherwise + C{False}. """ return _IEllipticCurveExchangeKexAlgorithm.providedBy(getKex(kexAlgorithm)) -def isFixedGroup(kexAlgorithm): +def isFixedGroup(kexAlgorithm: bytes) -> bool: """ Returns C{True} if C{kexAlgorithm} has a fixed prime / generator group. @param kexAlgorithm: The key exchange algorithm name. - @type kexAlgorithm: L{bytes} @return: C{True} if C{kexAlgorithm} has a fixed prime / generator group, otherwise C{False}. - @rtype: L{bool} """ return _IFixedGroupKexAlgorithm.providedBy(getKex(kexAlgorithm)) -def getHashProcessor(kexAlgorithm): +def getHashProcessor(kexAlgorithm: bytes) -> _HashFactory: """ Get the hash algorithm callable to use in key exchange. @param kexAlgorithm: The key exchange algorithm name. - @type kexAlgorithm: L{bytes} @return: A callable hash algorithm constructor (e.g. C{hashlib.sha256}). - @rtype: C{callable} """ kex = getKex(kexAlgorithm) return kex.hashProcessor -def getDHGeneratorAndPrime(kexAlgorithm): +def getDHGeneratorAndPrime(kexAlgorithm: bytes) -> tuple[int, int]: """ Get the generator and the prime to use in key exchange. @@ -257,17 +262,16 @@ def getDHGeneratorAndPrime(kexAlgorithm): @return: A L{tuple} containing L{int} generator and L{int} prime. @rtype: L{tuple} """ - kex = getKex(kexAlgorithm) + kex = _IFixedGroupKexAlgorithm(getKex(kexAlgorithm)) return kex.generator, kex.prime -def getSupportedKeyExchanges(): +def getSupportedKeyExchanges() -> list[bytes]: """ Get a list of supported key exchange algorithm names in order of preference. @return: A C{list} of supported key exchange algorithm names. - @rtype: C{list} of L{bytes} """ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import ec diff --git a/contrib/python/Twisted/py3/twisted/conch/ssh/common.py b/contrib/python/Twisted/py3/twisted/conch/ssh/common.py index 8bb6a286c3b..8d01ab14e50 100644 --- a/contrib/python/Twisted/py3/twisted/conch/ssh/common.py +++ b/contrib/python/Twisted/py3/twisted/conch/ssh/common.py @@ -4,12 +4,11 @@ """ Common functions for the SSH classes. - -Maintainer: Paul Swartz """ - +from __future__ import annotations import struct +from typing import Sequence, overload from cryptography.utils import int_to_bytes @@ -19,7 +18,7 @@ from twisted.python.versions import Version __all__ = ["NS", "getNS", "MP", "getMP", "ffs"] -def NS(t): +def NS(t: bytes | str) -> bytes: """ net string """ @@ -28,7 +27,7 @@ def NS(t): return struct.pack("!L", len(t)) + t -def getNS(s, count=1): +def getNS(s: bytes, count: int = 1) -> Sequence[bytes]: """ get net string """ @@ -41,7 +40,7 @@ def getNS(s, count=1): return tuple(ns) + (s[c:],) -def MP(number): +def MP(number: int) -> bytes: if number == 0: return b"\000" * 4 assert number > 0 @@ -51,7 +50,17 @@ def MP(number): return struct.pack(">L", len(bn)) + bn -def getMP(data, count=1): +@overload +def getMP(data: bytes) -> tuple[int, bytes]: + ... + + +@overload +def getMP(data: bytes, count: int) -> Sequence[int | bytes]: + ... + + +def getMP(data: bytes, count: int = 1) -> Sequence[int | bytes]: """ Get multiple precision integer out of the string. A multiple precision integer is stored as a 4-byte length followed by length bytes of the diff --git a/contrib/python/Twisted/py3/twisted/conch/ssh/keys.py b/contrib/python/Twisted/py3/twisted/conch/ssh/keys.py index e0e4a4b2c54..e52608df9aa 100644 --- a/contrib/python/Twisted/py3/twisted/conch/ssh/keys.py +++ b/contrib/python/Twisted/py3/twisted/conch/ssh/keys.py @@ -1675,20 +1675,17 @@ class Key: values = (data["p"], data["q"], data["g"], data["y"], data["x"]) return common.NS(self.sshType()) + b"".join(map(common.MP, values)) - def sign(self, data, signatureType=None): + def sign(self, data: bytes, signatureType: bytes | None = None) -> bytes: """ Sign some data with this key. SECSH-TRANS RFC 4253 Section 6.6. - @type data: L{bytes} @param data: The data to sign. - @type signatureType: L{bytes} @param signatureType: The SSH public key algorithm name to sign this - data with, or L{None} to use a reasonable default for the key. + data with, or L{None} to use a reasonable default for the key. - @rtype: L{bytes} @return: A signature for the given data. """ keyType = self.type() @@ -1702,7 +1699,7 @@ class Key: hashAlgorithm = self._getHashAlgorithm(signatureType) if hashAlgorithm is None: raise BadSignatureAlgorithmError( - f"public key signature algorithm {signatureType} is not " + f"public key signature algorithm {signatureType!r} is not " f"defined for {keyType} keys" ) @@ -1726,21 +1723,13 @@ class Key: rb = int_to_bytes(r) sb = int_to_bytes(s) - # Int_to_bytes returns rb[0] as a str in python2 - # and an as int in python3 - if type(rb[0]) is str: - rcomp = ord(rb[0]) - else: - rcomp = rb[0] + rcomp = rb[0] # If the MSB is set, prepend a null byte for correct formatting. if rcomp & 0x80: rb = b"\x00" + rb - if type(sb[0]) is str: - scomp = ord(sb[0]) - else: - scomp = sb[0] + scomp = sb[0] if scomp & 0x80: sb = b"\x00" + sb diff --git a/contrib/python/Twisted/py3/twisted/conch/ssh/service.py b/contrib/python/Twisted/py3/twisted/conch/ssh/service.py index 7d0d41c4aed..acfd40ee6a7 100644 --- a/contrib/python/Twisted/py3/twisted/conch/ssh/service.py +++ b/contrib/python/Twisted/py3/twisted/conch/ssh/service.py @@ -7,18 +7,22 @@ are ssh-userauth and ssh-connection. Maintainer: Paul Swartz """ +from __future__ import annotations -from typing import Dict +from typing import TYPE_CHECKING, Dict from twisted.logger import Logger +if TYPE_CHECKING: + from twisted.conch.ssh.transport import SSHTransportBase + class SSHService: # this is the ssh name for the service: name: bytes = None # type:ignore[assignment] protocolMessages: Dict[int, str] = {} # map #'s -> protocol names - transport = None # gets set later + transport: SSHTransportBase | None = None # gets set later _log = Logger() diff --git a/contrib/python/Twisted/py3/twisted/conch/ssh/transport.py b/contrib/python/Twisted/py3/twisted/conch/ssh/transport.py index 545c010f76e..323236f4720 100644 --- a/contrib/python/Twisted/py3/twisted/conch/ssh/transport.py +++ b/contrib/python/Twisted/py3/twisted/conch/ssh/transport.py @@ -17,7 +17,7 @@ import struct import types import zlib from hashlib import md5, sha1, sha256, sha384, sha512 -from typing import Any, Callable, Dict, Tuple, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple, Union from cryptography.exceptions import UnsupportedAlgorithm from cryptography.hazmat.backends import default_backend @@ -29,14 +29,19 @@ from typing_extensions import Literal from twisted import __version__ as twisted_version from twisted.conch.ssh import _kex, address, keys from twisted.conch.ssh.common import MP, NS, ffs, getMP, getNS +from twisted.conch.ssh.service import SSHService from twisted.internet import defer, protocol from twisted.logger import Logger from twisted.python import randbytes from twisted.python.compat import iterbytes, networkString +from twisted.python.failure import Failure # This import is needed if SHA256 hashing is used. # from twisted.python.compat import nativeString +if TYPE_CHECKING: + from twisted.conch.ssh.factory import SSHFactory + def _mpFromBytes(data): """Make an SSH multiple-precision integer from big-endian L{bytes}. @@ -311,8 +316,8 @@ def _getSupportedCiphers(): class SSHTransportBase(protocol.Protocol): """ - Protocol supporting basic SSH functionality: sending/receiving packets - and message dispatch. To connect to or run a server, you must use + Protocol supporting basic SSH functionality: sending/receiving packets and + message dispatch. To connect to or run a server, you must use SSHClientTransport or SSHServerTransport. @ivar protocolVersion: A string representing the version of the SSH @@ -321,23 +326,22 @@ class SSHTransportBase(protocol.Protocol): @ivar version: A string representing the version of the server or client. Currently defaults to 'Twisted'. - @ivar comment: An optional string giving more information about the - server or client. + @ivar comment: An optional string giving more information about the server + or client. @ivar supportedCiphers: A list of strings representing the encryption algorithms supported, in order from most-preferred to least. @ivar supportedMACs: A list of strings representing the message authentication codes (hashes) supported, in order from most-preferred - to least. Both this and supportedCiphers can include 'none' to use - no encryption or authentication, but that must be done manually, + to least. Both this and supportedCiphers can include 'none' to use no + encryption or authentication, but that must be done manually, - @ivar supportedKeyExchanges: A list of strings representing the - key exchanges supported, in order from most-preferred to least. + @ivar supportedKeyExchanges: A list of strings representing the key + exchanges supported, in order from most-preferred to least. - @ivar supportedPublicKeys: A list of strings representing the - public key algorithms supported, in order from most-preferred to - least. + @ivar supportedPublicKeys: A list of strings representing the public key + algorithms supported, in order from most-preferred to least. @ivar supportedCompressions: A list of strings representing compression types supported, from most-preferred to least. @@ -350,16 +354,16 @@ class SSHTransportBase(protocol.Protocol): @ivar isClient: A boolean indicating whether this is a client or server. - @ivar gotVersion: A boolean indicating whether we have received the - version string from the other side. + @ivar gotVersion: A boolean indicating whether we have received the version + string from the other side. @ivar buf: Data we've received but hasn't been parsed into a packet. @ivar outgoingPacketSequence: the sequence number of the next packet we will send. - @ivar incomingPacketSequence: the sequence number of the next packet we - are expecting from the other side. + @ivar incomingPacketSequence: the sequence number of the next packet we are + expecting from the other side. @ivar outgoingCompression: an object supporting the .compress(str) and .flush() methods, or None if there is no outgoing compression. Used to @@ -391,8 +395,8 @@ class SSHTransportBase(protocol.Protocol): part of the key exchange, sessionID is used to generate the various encryption and authentication keys. - @ivar service: an SSHService instance, or None. If it's set to an object, - it's the currently running service. + @ivar service: an L{SSHService} instance, or None. If it's set to an + object, it's the currently running service. @ivar kexAlg: the agreed-upon key exchange algorithm. @@ -476,8 +480,8 @@ class SSHTransportBase(protocol.Protocol): incomingPacketSequence = 0 outgoingCompression = None incomingCompression = None - sessionID = None - service = None + sessionID: bytes | None = None + service: SSHService | None = None # There is no key exchange activity in progress. _KEY_EXCHANGE_NONE = "_KEY_EXCHANGE_NONE" @@ -507,7 +511,13 @@ class SSHTransportBase(protocol.Protocol): _peerSupportsExtensions = False peerExtensions: Dict[bytes, bytes] = {} - def connectionLost(self, reason): + factory: SSHFactory + + # Set by twisted.conch.ssh.userauth.SSHUserAuthServer._cbFinishedAuth + avatar: object + logoutFunction: Callable[[], None] + + def connectionLost(self, reason: Failure | None = None) -> None: """ When the underlying connection is closed, stop the running service (if any), and log out the avatar (if any). @@ -1171,41 +1181,35 @@ class SSHTransportBase(protocol.Protocol): prefix = struct.pack(">L", len(secret)) return prefix + secret - def _getKey(self, c, sharedSecret, exchangeHash): + def _getKey(self, c: bytes, sharedSecret: bytes, exchangeHash: bytes) -> bytes: """ Get one of the keys for authentication/encryption. - @type c: L{bytes} @param c: The letter identifying which key this is. - @type sharedSecret: L{bytes} @param sharedSecret: The shared secret K. - @type exchangeHash: L{bytes} @param exchangeHash: The hash H from key exchange. - @rtype: L{bytes} @return: The derived key. """ hashProcessor = _kex.getHashProcessor(self.kexAlg) - k1 = hashProcessor(sharedSecret + exchangeHash + c + self.sessionID) - k1 = k1.digest() + assert self.sessionID is not None, "session ID must already have been assigned" + k1 = hashProcessor(sharedSecret + exchangeHash + c + self.sessionID).digest() k2 = hashProcessor(sharedSecret + exchangeHash + k1).digest() k3 = hashProcessor(sharedSecret + exchangeHash + k1 + k2).digest() k4 = hashProcessor(sharedSecret + exchangeHash + k1 + k2 + k3).digest() return k1 + k2 + k3 + k4 - def _keySetup(self, sharedSecret, exchangeHash): + def _keySetup(self, sharedSecret: bytes, exchangeHash: bytes) -> None: """ - Set up the keys for the connection and sends MSG_NEWKEYS when - finished, + Set up the keys for the connection and sends MSG_NEWKEYS when finished. @param sharedSecret: a secret string agreed upon using a Diffie- - Hellman exchange, so it is only shared between - the server and the client. - @type sharedSecret: L{str} + Hellman exchange, so it is only shared between the server and the + client. + @param exchangeHash: A hash of various data known by both sides. - @type exchangeHash: L{str} """ if not self.sessionID: self.sessionID = exchangeHash @@ -1442,7 +1446,7 @@ class SSHServerTransport(SSHTransportBase): isClient = False ignoreNextPacket = 0 - def _getHostKeys(self, keyAlg): + def _getHostKeys(self, keyAlg: bytes) -> tuple[keys.Key, keys.Key]: """ Get the public and private host keys corresponding to the given public key signature algorithm. @@ -1467,7 +1471,7 @@ class SSHServerTransport(SSHTransportBase): keyFormat = keyAlg return self.factory.publicKeys[keyFormat], self.factory.privateKeys[keyFormat] - def ssh_KEXINIT(self, packet): + def ssh_KEXINIT(self, packet: bytes) -> None: """ Called when we receive a MSG_KEXINIT message. For a description of the packet, see SSHTransportBase.ssh_KEXINIT(). Additionally, @@ -1487,29 +1491,26 @@ class SSHServerTransport(SSHTransportBase): ): self.ignoreNextPacket = True # Guess was wrong - def _ssh_KEX_ECDH_INIT(self, packet): + def _ssh_KEX_ECDH_INIT(self, packet: bytes) -> None: """ - Called from L{ssh_KEX_DH_GEX_REQUEST_OLD} to handle - elliptic curve key exchanges. + Called from L{ssh_KEX_DH_GEX_REQUEST_OLD} to handle elliptic curve key + exchanges. Payload:: string client Elliptic Curve Diffie-Hellman public key Just like L{_ssh_KEXDH_INIT} this message type is also not dispatched - directly. Extra check to determine if this is really KEX_ECDH_INIT - is required. + directly. Extra check to determine if this is really KEX_ECDH_INIT is + required. - First we load the host's public/private keys. - Then we generate the ECDH public/private keypair for the given curve. - With that we generate the shared secret key. - Then we compute the hash to sign and send back to the client - Along with the server's public key and the ECDH public key. + First we load the host's public/private keys. Then we generate the + ECDH public/private keypair for the given curve. With that we generate + the shared secret key. Then we compute the hash to sign and send back + to the client Along with the server's public key and the ECDH public + key. - @type packet: L{bytes} @param packet: The message data. - - @return: None. """ # Get the raw client public key. pktPub, packet = getNS(packet) @@ -1547,7 +1548,7 @@ class SSHServerTransport(SSHTransportBase): ) self._keySetup(sharedSecret, exchangeHash) - def _ssh_KEXDH_INIT(self, packet): + def _ssh_KEXDH_INIT(self, packet: bytes) -> None: """ Called to handle the beginning of a non-group key exchange. @@ -1588,7 +1589,7 @@ class SSHServerTransport(SSHTransportBase): ) self._keySetup(sharedSecret, exchangeHash) - def ssh_KEX_DH_GEX_REQUEST_OLD(self, packet): + def ssh_KEX_DH_GEX_REQUEST_OLD(self, packet: bytes) -> None: """ This represents different key exchange methods that share the same integer value. If the message is determined to be a KEXDH_INIT, @@ -1625,7 +1626,7 @@ class SSHServerTransport(SSHTransportBase): self._startEphemeralDH() self.sendPacket(MSG_KEX_DH_GEX_GROUP, MP(self.p) + MP(self.g)) - def ssh_KEX_DH_GEX_REQUEST(self, packet): + def ssh_KEX_DH_GEX_REQUEST(self, packet: bytes) -> None: """ Called when we receive a MSG_KEX_DH_GEX_REQUEST message. Payload:: integer minimum @@ -1651,7 +1652,7 @@ class SSHServerTransport(SSHTransportBase): self._startEphemeralDH() self.sendPacket(MSG_KEX_DH_GEX_GROUP, MP(self.p) + MP(self.g)) - def ssh_KEX_DH_GEX_INIT(self, packet): + def ssh_KEX_DH_GEX_INIT(self, packet: bytes) -> None: """ Called when we get a MSG_KEX_DH_GEX_INIT message. Payload:: integer e (client DH public key) @@ -1693,7 +1694,7 @@ class SSHServerTransport(SSHTransportBase): ) self._keySetup(sharedSecret, exchangeHash) - def _keySetup(self, sharedSecret, exchangeHash): + def _keySetup(self, sharedSecret: bytes, exchangeHash: bytes) -> None: """ See SSHTransportBase._keySetup(). """ @@ -1709,13 +1710,12 @@ class SSHServerTransport(SSHTransportBase): [(b"server-sig-algs", b",".join(self.supportedPublicKeys))] ) - def ssh_NEWKEYS(self, packet): + def ssh_NEWKEYS(self, packet: bytes) -> None: """ Called when we get a MSG_NEWKEYS message. No payload. When we get this, the keys have been set on both sides, and we start using them to encrypt and authenticate the connection. - @type packet: L{bytes} @param packet: The message data. """ if packet != b"": @@ -1723,7 +1723,7 @@ class SSHServerTransport(SSHTransportBase): return self._newKeys() - def ssh_SERVICE_REQUEST(self, packet): + def ssh_SERVICE_REQUEST(self, packet: bytes) -> None: """ Called when we get a MSG_SERVICE_REQUEST message. Payload:: string serviceName @@ -1906,7 +1906,8 @@ class SSHClientTransport(SSHTransportBase): d.addCallback(_continue_KEX_ECDH_REPLY, hostKey, pubKey, signature) d.addErrback( lambda unused: self.sendDisconnect( - DISCONNECT_HOST_KEY_NOT_VERIFIABLE, b"bad host key" + DISCONNECT_HOST_KEY_NOT_VERIFIABLE, + f"bad host key [ecdh] {unused}".encode("utf-8"), ) ) return d @@ -2122,7 +2123,7 @@ class SSHClientTransport(SSHTransportBase): ) self.setService(self.instance) - def requestService(self, instance): + def requestService(self, instance: SSHService) -> None: """ Request that a service be run over this transport. diff --git a/contrib/python/Twisted/py3/twisted/conch/ssh/userauth.py b/contrib/python/Twisted/py3/twisted/conch/ssh/userauth.py index 310f5f09f2e..0d24df00f92 100644 --- a/contrib/python/Twisted/py3/twisted/conch/ssh/userauth.py +++ b/contrib/python/Twisted/py3/twisted/conch/ssh/userauth.py @@ -8,20 +8,28 @@ Currently implemented authentication types are public-key and password. Maintainer: Paul Swartz """ - +from __future__ import annotations import struct +from typing import Callable, Tuple, Type from twisted.conch import error, interfaces from twisted.conch.ssh import keys, service, transport from twisted.conch.ssh.common import NS, getNS +from twisted.conch.ssh.keys import Key from twisted.cred import credentials from twisted.cred.error import UnauthorizedLogin from twisted.internet import defer, reactor +from twisted.internet.defer import Deferred +from twisted.internet.interfaces import IReactorTime from twisted.logger import Logger from twisted.python import failure from twisted.python.compat import nativeString +_ConchPortalTuple = Tuple[ + Type[interfaces.IConchUser], interfaces.IConchUser, Callable[[], None] +] + class SSHUserAuthServer(service.SSHService): """ @@ -72,7 +80,7 @@ class SSHUserAuthServer(service.SSHService): attemptsBeforeDisconnect = 20 # 20 login attempts before a disconnect passwordDelay = 1 # number of seconds to delay on a failed password - clock = reactor + clock: IReactorTime = IReactorTime(reactor) interfaceToMethod = { credentials.ISSHPrivateKey: b"publickey", credentials.IUsernamePassword: b"password", @@ -124,37 +132,40 @@ class SSHUserAuthServer(service.SSHService): transport.DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, b"you took too long" ) - def tryAuth(self, kind, user, data): + def tryAuth( + self, kind: bytes, user: bytes, data: bytes + ) -> Deferred[_ConchPortalTuple]: """ Try to authenticate the user with the given method. Dispatches to a auth_* method. @param kind: the authentication method to try. - @type kind: L{bytes} + @param user: the username the client is authenticating with. - @type user: L{bytes} + @param data: authentication specific data sent by the client. - @type data: L{bytes} + @return: A Deferred called back if the method succeeded, or erred back if it failed. - @rtype: C{defer.Deferred} """ self._log.debug("{user!r} trying auth {kind!r}", user=user, kind=kind) if kind not in self.supportedAuthentications: return defer.fail(error.ConchError("unsupported authentication, failing")) - kind = nativeString(kind.replace(b"-", b"_")) - f = getattr(self, f"auth_{kind}", None) - if f: + strkind = kind.replace(b"-", b"_").decode("ascii") + f: Callable[[bytes], Deferred[_ConchPortalTuple] | None] | None = getattr( + self, f"auth_{strkind}", None + ) + if f is not None: ret = f(data) - if not ret: + if ret is None: return defer.fail( - error.ConchError(f"{kind} return None instead of a Deferred") + error.ConchError(f"{strkind} return None instead of a Deferred") ) else: return ret - return defer.fail(error.ConchError(f"bad auth type: {kind}")) + return defer.fail(error.ConchError(f"bad auth type: {strkind}")) - def ssh_USERAUTH_REQUEST(self, packet): + def ssh_USERAUTH_REQUEST(self, packet: bytes) -> Deferred[_ConchPortalTuple] | None: """ The client has requested authentication. Payload:: string user @@ -173,19 +184,21 @@ class SSHUserAuthServer(service.SSHService): d = self.tryAuth(method, user, rest) if not d: self._ebBadAuth(failure.Failure(error.ConchError("auth returned none"))) - return - d.addCallback(self._cbFinishedAuth) - d.addErrback(self._ebMaybeBadAuth) - d.addErrback(self._ebBadAuth) - return d + return None + return ( + d.addCallback(self._cbFinishedAuth) + .addErrback(self._ebMaybeBadAuth) + .addErrback(self._ebBadAuth) + ) - def _cbFinishedAuth(self, result): + def _cbFinishedAuth(self, result: _ConchPortalTuple) -> None: """ The callback when user has successfully been authenticated. For a description of the arguments, see L{twisted.cred.portal.Portal.login}. We start the service requested by the user. """ (interface, avatar, logout) = result + assert self.transport is not None self.transport.avatar = avatar self.transport.logoutFunction = logout service = self.transport.factory.getService(self.transport, self.nextService) @@ -249,7 +262,7 @@ class SSHUserAuthServer(service.SSHService): MSG_USERAUTH_FAILURE, NS(b",".join(self.supportedAuthentications)) + b"\x00" ) - def auth_publickey(self, packet): + def auth_publickey(self, packet: bytes) -> Deferred[_ConchPortalTuple]: """ Public key authentication. Payload:: byte has signature @@ -262,6 +275,7 @@ class SSHUserAuthServer(service.SSHService): hasSig = ord(packet[0:1]) algName, blob, rest = getNS(packet[1:], 2) + result: Deferred[_ConchPortalTuple] try: keys.Key.fromString(blob) except keys.BadKeyError: @@ -271,6 +285,8 @@ class SSHUserAuthServer(service.SSHService): signature = hasSig and getNS(rest)[0] or None if hasSig: + assert self.transport is not None, "must have transport for auth" + assert self.transport.sessionID is not None, "must have session for auth" b = ( NS(self.transport.sessionID) + bytes((MSG_USERAUTH_REQUEST,)) @@ -282,19 +298,21 @@ class SSHUserAuthServer(service.SSHService): + NS(blob) ) c = credentials.SSHPrivateKey(self.user, algName, blob, b, signature) - return self.portal.login(c, None, interfaces.IConchUser) + result = self.portal.login(c, None, interfaces.IConchUser) else: c = credentials.SSHPrivateKey(self.user, algName, blob, None, None) - return self.portal.login(c, None, interfaces.IConchUser).addErrback( + result = self.portal.login(c, None, interfaces.IConchUser).addErrback( self._ebCheckKey, packet[1:] ) + return result - def _ebCheckKey(self, reason, packet): + def _ebCheckKey(self, reason: failure.Failure, packet: bytes) -> failure.Failure: """ Called back if the user did not sent a signature. If reason is error.ValidPublicKey then this key is valid for the user to authenticate with. Send MSG_USERAUTH_PK_OK. """ + assert self.transport is not None reason.trap(error.ValidPublicKey) # if we make it here, it means that the publickey is valid self.transport.sendPacket(MSG_USERAUTH_PK_OK, packet) @@ -331,64 +349,67 @@ class SSHUserAuthClient(service.SSHService): making callbacks for more information when necessary. @ivar name: the name of this service: 'ssh-userauth' - @type name: L{str} + @ivar preferredOrder: a list of authentication methods that should be used first, in order of preference, if supported by the server - @type preferredOrder: L{list} + @ivar user: the name of the user to authenticate as - @type user: L{bytes} + @ivar instance: the service to start after authentication has finished - @type instance: L{service.SSHService} - @ivar authenticatedWith: a list of strings of authentication methods we've tried - @type authenticatedWith: L{list} of L{bytes} + + @ivar authenticatedWith: a list of strings of authentication methods we've + tried + @ivar triedPublicKeys: a list of public key objects that we've tried to authenticate with - @type triedPublicKeys: L{list} of L{Key} + @ivar lastPublicKey: the last public key object we've tried to authenticate with - @type lastPublicKey: L{Key} """ - name = b"ssh-userauth" - preferredOrder = [b"publickey", b"password", b"keyboard-interactive"] + name: bytes = b"ssh-userauth" + preferredOrder: list[bytes] = [ + b"publickey", + b"password", + b"keyboard-interactive", + ] - def __init__(self, user, instance): + def __init__(self, user: bytes, instance: service.SSHService): self.user = user self.instance = instance - def serviceStarted(self): - self.authenticatedWith = [] - self.triedPublicKeys = [] - self.lastPublicKey = None + def serviceStarted(self) -> None: + self.authenticatedWith: list[bytes] = [] + self.triedPublicKeys: list[Key] = [] + self.lastPublicKey: Key | None = None self.askForAuth(b"none", b"") - def askForAuth(self, kind, extraData): + def askForAuth(self, kind: bytes, extraData: bytes) -> None: """ - Send a MSG_USERAUTH_REQUEST. + Send a C{MSG_USERAUTH_REQUEST}. @param kind: the authentication method to try. - @type kind: L{bytes} + @param extraData: method-specific data to go in the packet - @type extraData: L{bytes} """ + assert self.transport is not None self.lastAuth = kind self.transport.sendPacket( MSG_USERAUTH_REQUEST, NS(self.user) + NS(self.instance.name) + NS(kind) + extraData, ) - def tryAuth(self, kind): + def tryAuth(self, kind: bytes) -> None | Deferred[bool]: """ Dispatch to an authentication method. @param kind: the authentication method @type kind: L{bytes} """ - kind = nativeString(kind.replace(b"-", b"_")) + strkind = kind.replace(b"-", b"_").decode("ascii") self._log.debug("trying to auth with {kind}", kind=kind) - f = getattr(self, "auth_" + kind, None) - if f: - return f() + f: Callable[[], Deferred[bool]] | None = getattr(self, "auth_" + strkind, None) + return f() if f is not None else None def _ebAuth(self, ignored, *args): """ @@ -597,19 +618,15 @@ class SSHUserAuthClient(service.SSHService): data += NS(r.encode("UTF8")) self.transport.sendPacket(MSG_USERAUTH_INFO_RESPONSE, data) - def auth_publickey(self): + def auth_publickey(self) -> Deferred[bool]: """ Try to authenticate with a public key. Ask the user for a public key; if the user has one, send the request to the server and return True. Otherwise, return False. - - @rtype: L{bool} """ - d = defer.maybeDeferred(self.getPublicKey) - d.addBoth(self._cbGetPublicKey) - return d + return defer.maybeDeferred(self.getPublicKey).addBoth(self._cbGetPublicKey) - def _cbGetPublicKey(self, publicKey): + def _cbGetPublicKey(self, publicKey: Key | failure.Failure | None) -> bool: if not isinstance(publicKey, keys.Key): # failure or None publicKey = None if publicKey is not None: @@ -623,13 +640,15 @@ class SSHUserAuthClient(service.SSHService): else: return False - def auth_password(self): + # Section defining C{auth_}-prefixed methods begins here: they must each be + # defined with the signature (() -> bool), as described by + # L{SSHUserAuthClient.tryAuth}. + + def auth_password(self) -> bool: """ Try to authenticate with a password. Ask the user for a password. If the user will return a password, return True. Otherwise, return False. - - @rtype: L{bool} """ d = self.getPassword() if d: @@ -638,83 +657,75 @@ class SSHUserAuthClient(service.SSHService): else: # returned None, don't do password auth return False - def auth_keyboard_interactive(self): + def auth_keyboard_interactive(self) -> bool: """ Try to authenticate with keyboard-interactive authentication. Send the request to the server and return True. - - @rtype: L{bool} """ self._log.debug("authing with keyboard-interactive") self.askForAuth(b"keyboard-interactive", NS(b"") + NS(b"")) return True - def _cbPassword(self, password): + # Section defining C{auth_}-prefixed methods ends here. + + def _cbPassword(self, password: bytes) -> None: """ Called back when the user gives a password. Send the request to the server. @param password: the password the user entered - @type password: L{bytes} """ self.askForAuth(b"password", b"\x00" + NS(password)) - def signData(self, publicKey, signData): + def signData(self, publicKey: keys.Key, signData: bytes) -> Deferred[bytes] | None: """ Sign the given data with the given public key. - By default, this will call getPrivateKey to get the private key, - then sign the data using Key.sign(). + By default, this will call getPrivateKey to get the private key, then + sign the data using Key.sign(). This method is factored out so that it can be overridden to use alternate methods, such as a key agent. @param publicKey: The public key object returned from L{getPublicKey} - @type publicKey: L{keys.Key} @param signData: the data to be signed by the private key. - @type signData: L{bytes} + @return: a Deferred that's called back with the signature - @rtype: L{defer.Deferred} """ key = self.getPrivateKey() if not key: - return + return None return key.addCallback(self._cbSignData, signData) - def _cbSignData(self, privateKey, signData): + def _cbSignData(self, privateKey: keys.Key, signData: bytes) -> bytes: """ - Called back when the private key is returned. Sign the data and - return the signature. + Called back when the private key is returned. Sign the data and return + the signature. @param privateKey: the private key object - @type privateKey: L{keys.Key} + @param signData: the data to be signed by the private key. - @type signData: L{bytes} + @return: the signature - @rtype: L{bytes} """ return privateKey.sign(signData) - def getPublicKey(self): + def getPublicKey(self) -> Key | None: """ Return a public key for the user. If no more public keys are available, return L{None}. This implementation always returns L{None}. Override it in a subclass to actually find and return a public key object. - - @rtype: L{Key} or L{None} """ return None - def getPrivateKey(self): + def getPrivateKey(self) -> Deferred[Key]: """ Return a L{Deferred} that will be called back with the private key object corresponding to the last public key from getPublicKey(). If the private key is not available, errback on the Deferred. - - @rtype: L{Deferred} called back with L{Key} """ return defer.fail(NotImplementedError()) diff --git a/contrib/python/Twisted/py3/twisted/copyright.py b/contrib/python/Twisted/py3/twisted/copyright.py index 848c7c2fb26..8a21533ba74 100644 --- a/contrib/python/Twisted/py3/twisted/copyright.py +++ b/contrib/python/Twisted/py3/twisted/copyright.py @@ -13,7 +13,7 @@ from twisted import __version__ as version, version as _longversion longversion = str(_longversion) copyright = """\ -Copyright (c) 2001-2024 Twisted Matrix Laboratories. +Copyright (c) 2001-2025 Twisted Matrix Laboratories. See LICENSE for details.""" disclaimer = """ diff --git a/contrib/python/Twisted/py3/twisted/cred/credentials.py b/contrib/python/Twisted/py3/twisted/cred/credentials.py index 662913951c3..b2e9013c09b 100644 --- a/contrib/python/Twisted/py3/twisted/cred/credentials.py +++ b/contrib/python/Twisted/py3/twisted/cred/credentials.py @@ -7,7 +7,7 @@ This module defines L{ICredentials}, an interface for objects that represent authentication credentials to provide, and also includes a number of useful implementations of that interface. """ - +from __future__ import annotations import base64 import hmac @@ -17,10 +17,11 @@ import time from binascii import hexlify from hashlib import md5 -from zope.interface import Interface, implementer +from zope.interface import Attribute, Interface, implementer from twisted.cred import error from twisted.cred._digest import calcHA1, calcHA2, calcResponse +from twisted.internet.defer import Deferred from twisted.python.compat import nativeString, networkString from twisted.python.deprecate import deprecatedModuleAttribute from twisted.python.randbytes import secureRandom @@ -61,11 +62,14 @@ class IUsernameHashedPassword(ICredentials): kind of credential must store the passwords in plaintext (or as password-equivalent hashes) form so that they can be hashed in a manner appropriate for the particular credentials class. - - @type username: L{bytes} - @ivar username: The username associated with these credentials. """ + username: bytes = Attribute( + """ + The username associated with these credentials. + """ + ) + def checkPassword(password): """ Validate these credentials against the correct password. @@ -101,18 +105,16 @@ class IUsernamePassword(ICredentials): username: bytes password: bytes - def checkPassword(password: bytes) -> bool: + def checkPassword(password: bytes) -> bool | Deferred[bool]: """ Validate these credentials against the correct password. - @type password: L{bytes} @param password: The correct, plaintext password against which to - check. + check. - @rtype: C{bool} or L{Deferred} - @return: C{True} if the credentials represented by this object match the - given password, C{False} if they do not, or a L{Deferred} which will - be called back with one of these values. + @return: C{True} if the credentials represented by this object match + the given password, C{False} if they do not, or a L{Deferred} which + will be called back with one of these values. """ @@ -462,6 +464,14 @@ class UsernameHashedPassword: @implementer(IUsernamePassword) class UsernamePassword: + """ + A trivial implementation of L{IUsernamePassword}, containing a username and + a password. + """ + + username: bytes + password: bytes + def __init__(self, username: bytes, password: bytes) -> None: self.username = username self.password = password diff --git a/contrib/python/Twisted/py3/twisted/internet/defer.py b/contrib/python/Twisted/py3/twisted/internet/defer.py index 951ca87d6b5..1892eb483ee 100644 --- a/contrib/python/Twisted/py3/twisted/internet/defer.py +++ b/contrib/python/Twisted/py3/twisted/internet/defer.py @@ -49,7 +49,7 @@ from twisted.internet.interfaces import IDelayedCall, IReactorTime from twisted.logger import Logger from twisted.python import lockfile from twisted.python.compat import _PYPY, cmp, comparable -from twisted.python.deprecate import deprecated, warnAboutFunction +from twisted.python.deprecate import deprecated, deprecatedProperty, warnAboutFunction from twisted.python.failure import Failure, _extraneous log = Logger() @@ -469,12 +469,16 @@ class Deferred(Awaitable[_SelfResultT]): @type canceller: a 1-argument callable which takes a L{Deferred}. The return result is ignored. """ - self.callbacks: List[_CallbackChain] = [] + self._callbacks: List[_CallbackChain] = [] self._canceller = canceller if self.debug: self._debugInfo = DebugInfo() self._debugInfo.creator = traceback.format_stack()[:-1] + @deprecatedProperty(Version("Twisted", 25, 5, 0)) + def callbacks(self) -> List[_CallbackChain]: + return self._callbacks + def addCallbacks( self, callback: Union[ @@ -528,7 +532,7 @@ class Deferred(Awaitable[_SelfResultT]): # Note that this logic is duplicated in addCallbac/addErrback/addBoth # for performance reasons. - self.callbacks.append( + self._callbacks.append( ( (callback, callbackArgs, callbackKeywords), (errback, errbackArgs, errbackKeywords), @@ -622,7 +626,7 @@ class Deferred(Awaitable[_SelfResultT]): """ # This could be implemented as a call to addCallbacks, but doing it # directly is faster. - self.callbacks.append(((callback, args, kwargs), (_failthru, (), {}))) + self._callbacks.append(((callback, args, kwargs), (_failthru, (), {}))) if self.called: self._runCallbacks() @@ -664,7 +668,7 @@ class Deferred(Awaitable[_SelfResultT]): """ # This could be implemented as a call to addCallbacks, but doing it # directly is faster. - self.callbacks.append(((passthru, (), {}), (errback, args, kwargs))) + self._callbacks.append(((passthru, (), {}), (errback, args, kwargs))) if self.called: self._runCallbacks() @@ -754,7 +758,7 @@ class Deferred(Awaitable[_SelfResultT]): # This could be implemented as a call to addCallbacks, but doing it # directly is faster. call = (callback, args, kwargs) - self.callbacks.append((call, call)) + self._callbacks.append((call, call)) if self.called: self._runCallbacks() @@ -1048,8 +1052,8 @@ class Deferred(Awaitable[_SelfResultT]): finished = True current._chainedTo = None - while current.callbacks: - item = current.callbacks.pop(0) + while current._callbacks: + item = current._callbacks.pop(0) if not isinstance(current.result, Failure): callback, args, kwargs = item[0] else: @@ -1123,7 +1127,7 @@ class Deferred(Awaitable[_SelfResultT]): # running its callbacks right now. Therefore we can # append to the callbacks list directly instead of # using addCallbacks. - currentResult.callbacks.append(current._continuation()) + currentResult._callbacks.append(current._continuation()) break else: # Yep, it did. Steal it. @@ -1729,170 +1733,6 @@ SUCCESS = True FAILURE = False -## deferredGenerator -class waitForDeferred: - """ - See L{deferredGenerator}. - """ - - result: Any = _NO_RESULT - - def __init__(self, d: Deferred[object]) -> None: - warnings.warn( - "twisted.internet.defer.waitForDeferred was deprecated in " - "Twisted 15.0.0; please use twisted.internet.defer.inlineCallbacks " - "instead", - DeprecationWarning, - stacklevel=2, - ) - - if not isinstance(d, Deferred): - raise TypeError( - f"You must give waitForDeferred a Deferred. You gave it {d!r}." - ) - self.d = d - - def getResult(self) -> Any: - if isinstance(self.result, Failure): - self.result.raiseException() - self.result is not _NO_RESULT - return self.result - - -_DeferableGenerator = Generator[object, None, None] - - -def _deferGenerator( - g: _DeferableGenerator, deferred: Deferred[object] -) -> Deferred[Any]: - """ - See L{deferredGenerator}. - """ - - result = None - - # This function is complicated by the need to prevent unbounded recursion - # arising from repeatedly yielding immediately ready deferreds. This while - # loop and the waiting variable solve that by manually unfolding the - # recursion. - - # defgen is waiting for result? # result - # type note: List[Any] because you can't annotate List items by index. - # …better fix would be to create a class, but we need to jettison - # deferredGenerator anyway. - waiting: List[Any] = [True, None] - - while 1: - try: - result = next(g) - except StopIteration: - deferred.callback(result) - return deferred - except BaseException: - deferred.errback() - return deferred - - # Deferred.callback(Deferred) raises an error; we catch this case - # early here and give a nicer error message to the user in case - # they yield a Deferred. - if isinstance(result, Deferred): - return fail(TypeError("Yield waitForDeferred(d), not d!")) - - if isinstance(result, waitForDeferred): - # a waitForDeferred was yielded, get the result. - # Pass result in so it don't get changed going around the loop - # This isn't a problem for waiting, as it's only reused if - # gotResult has already been executed. - def gotResult( - r: object, result: waitForDeferred = cast(waitForDeferred, result) - ) -> None: - result.result = r - if waiting[0]: - waiting[0] = False - waiting[1] = r - else: - _deferGenerator(g, deferred) - - result.d.addBoth(gotResult) - if waiting[0]: - # Haven't called back yet, set flag so that we get reinvoked - # and return from the loop - waiting[0] = False - return deferred - # Reset waiting to initial values for next loop - waiting[0] = True - waiting[1] = None - - result = None - - -@deprecated(Version("Twisted", 15, 0, 0), "twisted.internet.defer.inlineCallbacks") -def deferredGenerator( - f: Callable[..., _DeferableGenerator] -) -> Callable[..., Deferred[object]]: - """ - L{deferredGenerator} and L{waitForDeferred} help you write - L{Deferred}-using code that looks like a regular sequential function. - Consider the use of L{inlineCallbacks} instead, which can accomplish - the same thing in a more concise manner. - - There are two important functions involved: L{waitForDeferred}, and - L{deferredGenerator}. They are used together, like this:: - - @deferredGenerator - def thingummy(): - thing = waitForDeferred(makeSomeRequestResultingInDeferred()) - yield thing - thing = thing.getResult() - print(thing) #the result! hoorj! - - L{waitForDeferred} returns something that you should immediately yield; when - your generator is resumed, calling C{thing.getResult()} will either give you - the result of the L{Deferred} if it was a success, or raise an exception if it - was a failure. Calling C{getResult} is B{absolutely mandatory}. If you do - not call it, I{your program will not work}. - - L{deferredGenerator} takes one of these waitForDeferred-using generator - functions and converts it into a function that returns a L{Deferred}. The - result of the L{Deferred} will be the last value that your generator yielded - unless the last value is a L{waitForDeferred} instance, in which case the - result will be L{None}. If the function raises an unhandled exception, the - L{Deferred} will errback instead. Remember that C{return result} won't work; - use C{yield result; return} in place of that. - - Note that not yielding anything from your generator will make the L{Deferred} - result in L{None}. Yielding a L{Deferred} from your generator is also an error - condition; always yield C{waitForDeferred(d)} instead. - - The L{Deferred} returned from your deferred generator may also errback if your - generator raised an exception. For example:: - - @deferredGenerator - def thingummy(): - thing = waitForDeferred(makeSomeRequestResultingInDeferred()) - yield thing - thing = thing.getResult() - if thing == 'I love Twisted': - # will become the result of the Deferred - yield 'TWISTED IS GREAT!' - return - else: - # will trigger an errback - raise Exception('DESTROY ALL LIFE') - - Put succinctly, these functions connect deferred-using code with this 'fake - blocking' style in both directions: L{waitForDeferred} converts from a - L{Deferred} to the 'blocking' style, and L{deferredGenerator} converts from the - 'blocking' style to a L{Deferred}. - """ - - @wraps(f) - def unwindGenerator(*args: object, **kwargs: object) -> Deferred[object]: - return _deferGenerator(f(*args, **kwargs), Deferred()) - - return unwindGenerator - - ## inlineCallbacks @@ -2154,9 +1994,9 @@ def _addCancelCallbackToDeferred( @param it: The L{Deferred} to add the errback to. @param status: a L{_CancellationStatus} tracking the current status of C{gen} """ - it.callbacks, tmp = [], it.callbacks + it._callbacks, tmp = [], it._callbacks it = it.addErrback(_handleCancelInlineCallbacks, status) - it.callbacks.extend(tmp) + it._callbacks.extend(tmp) it.errback(_InternalInlineCallbacksCancelledError()) @@ -2724,8 +2564,6 @@ __all__ = [ "gatherResults", "maybeDeferred", "ensureDeferred", - "waitForDeferred", - "deferredGenerator", "inlineCallbacks", "returnValue", "DeferredLock", diff --git a/contrib/python/Twisted/py3/twisted/internet/endpoints.py b/contrib/python/Twisted/py3/twisted/internet/endpoints.py index dfa0cc43ce8..b73b2acc100 100644 --- a/contrib/python/Twisted/py3/twisted/internet/endpoints.py +++ b/contrib/python/Twisted/py3/twisted/internet/endpoints.py @@ -1,4 +1,4 @@ -# -*- test-case-name: twisted.internet.test.test_endpoints -*- +# -*- test-case-name: twisted.internet.test.test_endpoints.HostnameEndpointMemoryIPv4ReactorTests.test_errorsLogged -*- # Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. @@ -18,7 +18,7 @@ import os import re import socket import warnings -from typing import Any, Iterable, Optional, Sequence, Type +from typing import Any, Callable, Iterable, List, Optional, Sequence, Tuple, Type, Union from unicodedata import normalize from zope.interface import directlyProvides, implementer @@ -699,6 +699,23 @@ class TCP6ClientEndpoint: return defer.fail() +_gairesult = List[ + Tuple[ + socket.AddressFamily, + socket.SocketKind, + int, + str, + Union[ + Tuple[str, int], + Tuple[str, int, int, int], + ], + ] +] +""" +Alias for the result type of L{socket.getaddrinfo}C{()} +""" + + @implementer(IHostnameResolver) class _SimpleHostnameResolver: """ @@ -714,7 +731,9 @@ class _SimpleHostnameResolver: _log = Logger() - def __init__(self, nameResolution): + def __init__( + self, nameResolution: Callable[[str, int], Deferred[_gairesult]] + ) -> None: """ Create a L{_SimpleHostnameResolver} instance. """ @@ -847,7 +866,17 @@ class HostnameEndpoint: """ self._reactor = reactor - self._nameResolver = self._getNameResolverAndMaybeWarn(reactor) + + # We retrieve the actual name resolver to use from the reactor at + # C{connect()} time, in case the reactor modifies its name-resolution + # configuration after this HostnameEndpoint has been constructed. + # However, in order to make any warnings a bit more legible in the much + # more common case that the reactor's name resolution is configured + # before any endpoints are constructed, this eagerly validates the name + # resolver's configuration during endpoint construction but discards + # the actual resolver retrieved. + self._getNameResolverAndMaybeWarn(reactor) + [self._badHostname, self._hostBytes, self._hostText] = self._hostAsBytesAndText( host ) @@ -990,7 +1019,8 @@ class HostnameEndpoint: def resolutionComplete() -> None: resolved.callback(addresses) - self._nameResolver.resolveHostName( + nameResolver = self._getNameResolverAndMaybeWarn(self._reactor) + nameResolver.resolveHostName( EndpointReceiver(), self._hostText, portNumber=self._port ) diff --git a/contrib/python/Twisted/py3/twisted/internet/testing.py b/contrib/python/Twisted/py3/twisted/internet/testing.py index 6563184edf9..a7e1de67286 100644 --- a/contrib/python/Twisted/py3/twisted/internet/testing.py +++ b/contrib/python/Twisted/py3/twisted/internet/testing.py @@ -7,6 +7,8 @@ Assorted functionality which is commonly useful when writing unit tests. """ from __future__ import annotations +import typing +from dataclasses import dataclass from io import BytesIO from socket import AF_INET, AF_INET6 from time import time @@ -33,17 +35,22 @@ from twisted.internet.address import IPv4Address, IPv6Address, UNIXAddress from twisted.internet.defer import Deferred, ensureDeferred, succeed from twisted.internet.error import UnsupportedAddressFamily from twisted.internet.interfaces import ( + IAddress, IConnector, IConsumer, + IHostnameResolver, + IHostResolution, IListeningPort, IProtocol, IPushProducer, IReactorCore, IReactorFDSet, + IReactorPluggableNameResolver, IReactorSocket, IReactorSSL, IReactorTCP, IReactorUNIX, + IResolutionReceiver, ITransport, ) from twisted.internet.task import Clock @@ -72,6 +79,14 @@ __all__ = [ _P = ParamSpec("_P") +class _ProtocolConnectionMadeHaver(typing.Protocol): + """ + Explicit stipulation of the implicit requirement of L{AccumulatingProtocol}'s factory. + """ + + protocolConnectionMade: Deferred[AccumulatingProtocol] | None + + class AccumulatingProtocol(protocol.Protocol): """ L{AccumulatingProtocol} is an L{IProtocol} implementation which collects @@ -87,26 +102,29 @@ class AccumulatingProtocol(protocol.Protocol): C{connectionLost} is called. """ + made: int + closed: int made = closed = 0 - closedReason = None - - closedDeferred = None + closedReason: failure.Failure | None = None + closedDeferred: Deferred[None] | None = None + data: bytes = b"" - data = b"" + factory: protocol.Factory | None = None - factory = None - - def connectionMade(self): + def connectionMade(self) -> None: self.made = 1 - if self.factory is not None and self.factory.protocolConnectionMade is not None: - d = self.factory.protocolConnectionMade - self.factory.protocolConnectionMade = None + factory: _ProtocolConnectionMadeHaver | None = ( + self.factory # type:ignore[assignment] + ) + if factory is not None and factory.protocolConnectionMade is not None: + d = factory.protocolConnectionMade + factory.protocolConnectionMade = None d.callback(self) - def dataReceived(self, data): + def dataReceived(self, data: bytes) -> None: self.data += data - def connectionLost(self, reason): + def connectionLost(self, reason: failure.Failure | None = None) -> None: self.closed = 1 self.closedReason = reason if self.closedDeferred is not None: @@ -414,8 +432,54 @@ class _FakeConnector: return self._address +@implementer(IHostResolution) +@dataclass +class _SynchronousResolution: + name: str + + def cancel(self) -> None: + """ + Provided just for interface compliance; it should be impossible to + reach here, since it's resolved synchronously. + """ + raise Exception("already resolved") # pragma: no cover + + +@implementer(IHostnameResolver) +class SynchronousResolver: + """ + A very simple L{IHostnameResolver} that immediately, synchronously resolves + all host names to a single static address (TCPv4, 127.0.0.1) while + preserving any requested port number. + """ + + def resolveHostName( + self, + resolutionReceiver: IResolutionReceiver, + hostName: str, + portNumber: int = 0, + addressTypes: Sequence[type[IAddress]] | None = None, + transportSemantics: str = "TCP", + ) -> IHostResolution: + """ + Implement L{IHostnameResolver.resolveHostName} to synchronously resolve + the name and complete resolution before returning. + """ + resolution = _SynchronousResolution(hostName) + resolutionReceiver.resolutionBegan(resolution) + resolutionReceiver.addressResolved(IPv4Address("TCP", "127.0.0.1", portNumber)) + resolutionReceiver.resolutionComplete() + return resolution + + @implementer( - IReactorCore, IReactorTCP, IReactorSSL, IReactorUNIX, IReactorSocket, IReactorFDSet + IReactorCore, + IReactorTCP, + IReactorSSL, + IReactorUNIX, + IReactorSocket, + IReactorFDSet, + IReactorPluggableNameResolver, ) class MemoryReactor: """ @@ -474,6 +538,8 @@ class MemoryReactor: connections added using C{adoptStreamConnection}. """ + nameResolver: IHostnameResolver + def __init__(self): """ Initialize the tracking lists. @@ -500,6 +566,15 @@ class MemoryReactor: self.readers = set() self.writers = set() + self.nameResolver = SynchronousResolver() + + def installNameResolver(self, resolver: IHostnameResolver) -> IHostnameResolver: + """ + Implement L{IReactorPluggableNameResolver}. + """ + oldResolver = self.nameResolver + self.nameResolver = resolver + return oldResolver def install(self): """ @@ -761,7 +836,13 @@ class MemoryReactorClock(MemoryReactor, Clock): Clock.__init__(self) -@implementer(IReactorTCP, IReactorSSL, IReactorUNIX, IReactorSocket) +@implementer( + IReactorTCP, + IReactorSSL, + IReactorUNIX, + IReactorSocket, + IReactorPluggableNameResolver, +) class RaisingMemoryReactor: """ A fake reactor to be used in tests. It accepts TCP connection setup @@ -771,7 +852,11 @@ class RaisingMemoryReactor: @ivar _connectException: An instance of an L{Exception} """ - def __init__(self, listenException=None, connectException=None): + def __init__( + self, + listenException: Exception | None = None, + connectException: Exception | None = None, + ) -> None: """ @param listenException: An instance of an L{Exception} to raise when any C{listen} method is called. @@ -781,6 +866,14 @@ class RaisingMemoryReactor: """ self._listenException = listenException self._connectException = connectException + self.nameResolver: IHostnameResolver = SynchronousResolver() + + def installNameResolver(self, nameResolver: IHostnameResolver) -> IHostnameResolver: + """ + Implement L{IReactorPluggableNameResolver}. + """ + previous, self.nameResolver = self.nameResolver, nameResolver + return previous def adoptStreamPort(self, fileno, addressFamily, factory): """ @@ -991,7 +1084,7 @@ def _benchmarkWithReactor( Generator[Deferred[Any], Any, _T], Deferred[_T], ], - ] + ], ) -> Callable[[Any], None]: # pragma: no cover """ Decorator for running a benchmark tests that loops the reactor. diff --git a/contrib/python/Twisted/py3/twisted/mail/interfaces.py b/contrib/python/Twisted/py3/twisted/mail/interfaces.py index dd87d35a63c..0bb78e1bfa1 100644 --- a/contrib/python/Twisted/py3/twisted/mail/interfaces.py +++ b/contrib/python/Twisted/py3/twisted/mail/interfaces.py @@ -6,10 +6,14 @@ Interfaces for L{twisted.mail}. @since: 16.5 """ +from __future__ import annotations +from typing import overload from zope.interface import Interface +from twisted.cred.portal import IRealm + class IChallengeResponse(Interface): """ @@ -138,7 +142,15 @@ class IMailboxPOP3(Interface): remain on the server before being deleted. """ - def listMessages(index=None): + @overload + def listMessages() -> list[int]: + ... + + @overload + def listMessages(i: int) -> int: + ... + + def listMessages(i: int | None = None) -> int | list[int]: """ Retrieve the size of a message, or, if none is specified, the size of each message in the mailbox. @@ -215,7 +227,7 @@ class IMailboxPOP3(Interface): """ -class IDomain(Interface): +class IDomain(IRealm): """ An interface for email domains. """ diff --git a/contrib/python/Twisted/py3/twisted/mail/mail.py b/contrib/python/Twisted/py3/twisted/mail/mail.py index 2dc405344b6..bf84ae2f3e1 100644 --- a/contrib/python/Twisted/py3/twisted/mail/mail.py +++ b/contrib/python/Twisted/py3/twisted/mail/mail.py @@ -5,22 +5,20 @@ """ Mail service support. """ +from __future__ import annotations # System imports import os import warnings +from typing import NoReturn -from zope.interface import implementer +from zope.interface import Interface, implementer from twisted.application import internet, service -from twisted.cred.portal import Portal - -# Twisted imports +from twisted.cred.portal import IRealm, Portal from twisted.internet import defer - -# Sibling imports from twisted.mail import protocols, smtp -from twisted.mail.interfaces import IAliasableDomain, IDomain +from twisted.mail.interfaces import IAlias, IAliasableDomain, IDomain from twisted.python import log, util @@ -396,6 +394,14 @@ class BounceDomain: """ return [] + def requestAvatar( + self, avatarId: bytes | tuple[()], mind: object, *interfaces: type[Interface] + ) -> NoReturn: + """ + Bounce domains cannot authenticate users. + """ + raise NotImplementedError() + @implementer(smtp.IMessage) class FileMessage: @@ -453,6 +459,7 @@ class FileMessage: os.remove(self.name) +@implementer(IRealm) class MailService(service.MultiService): """ An email service. @@ -466,11 +473,8 @@ class MailService(service.MultiService): @type portals: L{dict} of L{bytes} -> L{Portal} @ivar portals: A mapping of domain name to authentication portal. - @type aliases: L{None} or L{dict} of - L{bytes} -> L{IAlias} provider @ivar aliases: A mapping of domain name to alias. - @type smtpPortal: L{Portal} @ivar smtpPortal: A portal for authentication for the SMTP server. @type monitor: L{FileMonitoringService} @@ -478,19 +482,17 @@ class MailService(service.MultiService): """ queue = None - domains = None - portals = None - aliases = None - smtpPortal = None + aliases: dict[bytes, IAlias] | None = None + smtpPortal: Portal - def __init__(self): + def __init__(self) -> None: """ Initialize the mail service. """ service.MultiService.__init__(self) # Domains and portals for "client" protocols - POP3, IMAP4, etc self.domains = DomainWithDefaultDict({}, BounceDomain()) - self.portals = {} + self.portals: dict[bytes, Portal] = {} self.monitor = FileMonitoringService() self.monitor.setServiceParent(self) @@ -523,18 +525,17 @@ class MailService(service.MultiService): """ return protocols.ESMTPFactory(self, self.smtpPortal) - def addDomain(self, name, domain): + def addDomain(self, name: bytes, domain: IDomain) -> None: """ Add a domain for which the service will accept email. - @type name: L{bytes} @param name: A domain name. - @type domain: L{IDomain} provider @param domain: A domain object. """ portal = Portal(domain) - map(portal.registerChecker, domain.getCredentialsCheckers()) + for checker in domain.getCredentialsCheckers(): + portal.registerChecker(checker) self.domains[name] = domain self.portals[name] = portal if self.aliases and IAliasableDomain.providedBy(domain): diff --git a/contrib/python/Twisted/py3/twisted/mail/maildir.py b/contrib/python/Twisted/py3/twisted/mail/maildir.py index c58bf31a941..aca2f4f851f 100644 --- a/contrib/python/Twisted/py3/twisted/mail/maildir.py +++ b/contrib/python/Twisted/py3/twisted/mail/maildir.py @@ -6,20 +6,26 @@ """ Maildir-style mailbox support. """ +from __future__ import annotations import io import os import socket import stat from hashlib import md5 -from typing import IO +from typing import IO, Callable, overload -from zope.interface import implementer +from zope.interface import Interface, implementer from twisted.cred import checkers, credentials, portal +from twisted.cred.credentials import IUsernameHashedPassword, IUsernamePassword from twisted.cred.error import UnauthorizedLogin from twisted.internet import defer, interfaces, reactor +from twisted.internet.defer import Deferred, succeed +from twisted.internet.interfaces import IProducer from twisted.mail import mail, pop3, smtp +from twisted.mail.alias import AliasBase +from twisted.mail.mail import MailService from twisted.persisted import dirdbm from twisted.protocols import basic from twisted.python import failure, log @@ -60,7 +66,7 @@ class _MaildirNameGenerator: """ self._clock = clock - def generate(self): + def generate(self) -> bytes: """ Generate a string which is intended to be unique across all calls to this function (across all processes, reboots, etc). @@ -76,28 +82,31 @@ class _MaildirNameGenerator: t = self._clock.seconds() seconds = str(int(t)) microseconds = "%07d" % (int((t - int(t)) * 10e6),) - return f"{seconds}.M{microseconds}P{self.p}Q{self.n}.{self.s}" + return os.fsencode(f"{seconds}.M{microseconds}P{self.p}Q{self.n}.{self.s}") _generateMaildirName = _MaildirNameGenerator(reactor).generate -def initializeMaildir(dir): +def initializeMaildir(dir: bytes | str) -> None: """ Create a maildir user directory if it doesn't already exist. - @type dir: L{bytes} @param dir: The path name for a user directory. """ - dir = os.fsdecode(dir) - if not os.path.isdir(dir): - os.mkdir(dir, 0o700) - for subdir in ["new", "cur", "tmp", ".Trash"]: - os.mkdir(os.path.join(dir, subdir), 0o700) - for subdir in ["new", "cur", "tmp"]: - os.mkdir(os.path.join(dir, ".Trash", subdir), 0o700) + bdir: bytes + if isinstance(dir, bytes): + bdir = dir + else: + bdir = os.fsencode(dir) + if not os.path.isdir(bdir): + os.mkdir(bdir, 0o700) + for subdir in [b"new", b"cur", b"tmp", b".Trash"]: + os.mkdir(os.path.join(bdir, subdir), 0o700) + for subdir in [b"new", b"cur", b"tmp"]: + os.mkdir(os.path.join(bdir, b".Trash", subdir), 0o700) # touch - open(os.path.join(dir, ".Trash", "maildirfolder"), "w").close() + open(os.path.join(bdir, b".Trash", b"maildirfolder"), "w").close() class MaildirMessage(mail.FileMessage): @@ -160,22 +169,17 @@ class AbstractMaildirDomain: """ An abstract maildir-backed domain. - @type alias: L{None} or L{dict} mapping - L{bytes} to L{AliasBase} @ivar alias: A mapping of username to alias. @ivar root: See L{__init__}. """ - alias = None - root = None + alias: None | dict[bytes, AliasBase] = None - def __init__(self, service, root): + def __init__(self, service: MailService, root: bytes) -> None: """ - @type service: L{MailService} @param service: An email service. - @type root: L{bytes} @param root: The maildir root directory. """ self.root = root @@ -274,16 +278,14 @@ class AbstractMaildirDomain: """ return False - def addUser(self, user, password): + def addUser(self, user: bytes, password: bytes) -> None: """ Add a user to this domain. Subclasses should override this method. - @type user: L{bytes} @param user: A username. - @type password: L{bytes} @param password: A password. """ raise NotImplementedError @@ -300,6 +302,14 @@ class AbstractMaildirDomain: """ raise NotImplementedError + def requestAvatar( + self, avatarId: bytes | tuple[()], mind: object, *interfaces: type[Interface] + ) -> tuple[type[Interface], object, Callable[[], None]]: + """ + Abstract domains cannot authenticate users. + """ + raise NotImplementedError() + @implementer(interfaces.IConsumer) class _MaildirMailboxAppendMessageTask: @@ -342,7 +352,7 @@ class _MaildirMailboxAppendMessageTask: osclose = staticmethod(os.close) osrename = staticmethod(os.rename) - def __init__(self, mbox, msg): + def __init__(self, mbox: MaildirMailbox, msg: bytes | IO[bytes]) -> None: """ @type mbox: L{MaildirMailbox} @param mbox: A maildir mailbox. @@ -351,13 +361,13 @@ class _MaildirMailboxAppendMessageTask: @param msg: The message to add. """ self.mbox = mbox - self.defer = defer.Deferred() + self.defer: defer.Deferred[None] = defer.Deferred() self.openCall = None if not hasattr(msg, "read"): msg = io.BytesIO(msg) self.msg = msg - def startUp(self): + def startUp(self) -> None: """ Start transferring the message to the mailbox. """ @@ -366,15 +376,13 @@ class _MaildirMailboxAppendMessageTask: self.filesender = basic.FileSender() self.filesender.beginFileTransfer(self.msg, self) - def registerProducer(self, producer, streaming): + def registerProducer(self, producer: IProducer, streaming: bool) -> None: """ Register a producer and start asking it for data if it is non-streaming. - @type producer: L{IProducer <interfaces.IProducer>} @param producer: A producer. - @type streaming: L{bool} @param streaming: A flag indicating whether the producer provides a streaming interface. """ @@ -401,11 +409,10 @@ class _MaildirMailboxAppendMessageTask: self.osclose(self.fh) self.moveFileToNew() - def write(self, data): + def write(self, data: bytes) -> None: """ Write data to the maildir file. - @type data: L{bytes} @param data: Data to be written to the file. """ try: @@ -434,7 +441,7 @@ class _MaildirMailboxAppendMessageTask: successfully. """ while True: - newname = os.path.join(self.mbox.path, "new", _generateMaildirName()) + newname = os.path.join(self.mbox.path, b"new", _generateMaildirName()) try: self.osrename(self.tmpname, newname) break @@ -466,7 +473,7 @@ class _MaildirMailboxAppendMessageTask: tries = 0 self.fh = -1 while True: - self.tmpname = os.path.join(self.mbox.path, "tmp", _generateMaildirName()) + self.tmpname = os.path.join(self.mbox.path, b"tmp", _generateMaildirName()) try: self.fh = self.osopen(self.tmpname, attr, 0o600) return None @@ -488,38 +495,40 @@ class MaildirMailbox(pop3.Mailbox): @ivar path: See L{__init__}. - @type list: L{list} of L{int} or 2-L{tuple} of (0) file-like object, - (1) L{bytes} - @ivar list: Information about the messages in the mailbox. For undeleted - messages, the file containing the message and the - full path name of the file are stored. Deleted messages are indicated - by 0. + @ivar list: Information about the messages in the mailbox. For undeleted + messages, the full path name of the message storing the file is stored. + Deleted messages are indicated by 0. - @type deleted: L{dict} mapping 2-L{tuple} of (0) file-like object, - (1) L{bytes} to L{bytes} - @type deleted: A mapping of the information about a file before it was + @type deleted: A mapping of the path to a deleted file before it was deleted to the full path name of the deleted file in the I{.Trash/} subfolder. """ AppendFactory = _MaildirMailboxAppendMessageTask - def __init__(self, path): + def __init__(self, path: bytes) -> None: """ - @type path: L{bytes} @param path: The directory name for a maildir mailbox. """ self.path = path - self.list = [] - self.deleted = {} + self.deleted: dict[bytes, bytes] = {} initializeMaildir(path) - for name in ("cur", "new"): + computing = [] + for name in (b"cur", b"new"): for file in os.listdir(os.path.join(path, name)): - self.list.append((file, os.path.join(path, name, file))) - self.list.sort() - self.list = [e[1] for e in self.list] + computing.append((file, os.path.join(path, name, file))) + computing.sort() + self.list: list[int | bytes] = [el[1] for el in computing] - def listMessages(self, i=None): + @overload + def listMessages(self) -> list[int]: + ... + + @overload + def listMessages(self, i: int) -> int: + ... + + def listMessages(self, i: int | None = None) -> int | list[int]: """ Retrieve the size of a message, or, if none is specified, the size of each message in the mailbox. @@ -546,7 +555,7 @@ class MaildirMailbox(pop3.Mailbox): return ret return self.list[i] and os.stat(self.list[i])[stat.ST_SIZE] or 0 - def getMessage(self, i): + def getMessage(self, i: int) -> IO[str]: """ Retrieve a file-like object with the contents of a message. @@ -624,16 +633,14 @@ class MaildirMailbox(pop3.Mailbox): self.list.append(real) self.deleted.clear() - def appendMessage(self, txt): + def appendMessage(self, txt: bytes | IO[bytes]) -> Deferred[None]: """ Add a message to the mailbox. - @type txt: L{bytes} or file-like object @param txt: A message to add. - @rtype: L{Deferred <defer.Deferred>} - @return: A deferred which fires when the message has been added to - the mailbox. + @return: A deferred which fires when the message has been added to the + mailbox. """ task = self.AppendFactory(self, txt) result = task.defer @@ -759,18 +766,17 @@ class MaildirDirdbmDomain(AbstractMaildirDomain): portal = None _credcheckers = None - def __init__(self, service, root, postmaster=0): + def __init__( + self, service: MailService, root: bytes, postmaster: bool = False + ) -> None: """ - @type service: L{MailService} @param service: An email service. - @type root: L{bytes} @param root: The maildir root directory. - @type postmaster: L{bool} @param postmaster: A flag indicating whether non-existent addresses - should be forwarded to the postmaster (C{True}) or - bounced (C{False}). + should be forwarded to the postmaster (C{True}) or bounced + (C{False}). """ root = os.fsencode(root) AbstractMaildirDomain.__init__(self, service, root) @@ -780,14 +786,12 @@ class MaildirDirdbmDomain(AbstractMaildirDomain): self.dbm = dirdbm.open(dbm) self.postmaster = postmaster - def userDirectory(self, name): + def userDirectory(self, name: bytes) -> bytes | None: """ Return the path to a user's mail directory. - @type name: L{bytes} @param name: A username. - @rtype: L{bytes} or L{None} @return: The path to the user's mail directory for a valid user. For an invalid user, the path to the postmaster's mailbox if bounces are redirected there. Otherwise, L{None}. @@ -795,13 +799,13 @@ class MaildirDirdbmDomain(AbstractMaildirDomain): if name not in self.dbm: if not self.postmaster: return None - name = "postmaster" + name = b"postmaster" dir = os.path.join(self.root, name) if not os.path.exists(dir): initializeMaildir(dir) return dir - def addUser(self, user, password): + def addUser(self, user: bytes, password: bytes) -> None: """ Add a user to this domain by adding an entry in the authentication database and initializing the user's mail directory. @@ -828,7 +832,12 @@ class MaildirDirdbmDomain(AbstractMaildirDomain): self._credcheckers = [DirdbmDatabase(self.dbm)] return self._credcheckers - def requestAvatar(self, avatarId, mind, *interfaces): + def requestAvatar( + self, + avatarId: bytes | tuple[()], + mind: object, + *interfaces: type[Interface], + ) -> tuple[type[Interface], object, Callable[[], None]]: """ Get the mailbox for an authenticated user. @@ -859,11 +868,15 @@ class MaildirDirdbmDomain(AbstractMaildirDomain): """ if pop3.IMailbox not in interfaces: raise NotImplementedError("No interface") + mbox: pop3.IMailbox if avatarId == checkers.ANONYMOUS: mbox = StringListMailbox([INTERNAL_ERROR]) else: - mbox = MaildirMailbox(os.path.join(self.root, avatarId)) - + assert isinstance( + avatarId, bytes + ), "avatar ID must be bytes, already checked ANONYMOUS" + mboxroot = os.path.join(self.root, avatarId) + mbox = MaildirMailbox(mboxroot) return (pop3.IMailbox, mbox, lambda: None) @@ -873,7 +886,6 @@ class DirdbmDatabase: A credentials checker which authenticates users out of a L{DirDBM <dirdbm.DirDBM>} database. - @type dirdbm: L{DirDBM <dirdbm.DirDBM>} @ivar dirdbm: An authentication database. """ @@ -883,14 +895,15 @@ class DirdbmDatabase: credentials.IUsernameHashedPassword, ) - def __init__(self, dbm): + def __init__(self, dbm: dirdbm.DirDBM) -> None: """ - @type dbm: L{DirDBM <dirdbm.DirDBM>} @param dbm: An authentication database. """ self.dirdbm = dbm - def requestAvatarId(self, c): + def requestAvatarId( + self, c: IUsernamePassword | IUsernameHashedPassword + ) -> Deferred[bytes | tuple[()]]: """ Authenticate a user and, if successful, return their username. @@ -905,6 +918,7 @@ class DirdbmDatabase: @raise UnauthorizedLogin: When the credentials check fails. """ if c.username in self.dirdbm: - if c.checkPassword(self.dirdbm[c.username]): - return c.username + password = self.dirdbm[c.username] + if c.checkPassword(password): + return succeed(c.username) raise UnauthorizedLogin() diff --git a/contrib/python/Twisted/py3/twisted/mail/pop3.py b/contrib/python/Twisted/py3/twisted/mail/pop3.py index 7b230d20591..8b4405cb3b7 100644 --- a/contrib/python/Twisted/py3/twisted/mail/pop3.py +++ b/contrib/python/Twisted/py3/twisted/mail/pop3.py @@ -10,12 +10,13 @@ Post-office Protocol version 3. @author: Glyph Lefkowitz @author: Jp Calderone """ +from __future__ import annotations import base64 import binascii import warnings from hashlib import md5 -from typing import Optional +from typing import IO, Optional, overload from zope.interface import implementer @@ -1330,7 +1331,15 @@ class Mailbox: A base class for mailboxes. """ - def listMessages(self, i=None): + @overload + def listMessages(self) -> list[int]: + ... + + @overload + def listMessages(self, i: int) -> int: + ... + + def listMessages(self, i: int | None = None) -> int | list[int]: """ Retrieve the size of a message, or, if none is specified, the size of each message in the mailbox. @@ -1350,7 +1359,7 @@ class Mailbox: """ return [] - def getMessage(self, i): + def getMessage(self, i: int) -> IO[str]: """ Retrieve a file containing the contents of a message. @@ -1365,7 +1374,7 @@ class Mailbox: """ raise ValueError - def getUidl(self, i): + def getUidl(self, i: int) -> bytes: """ Get a unique identifier for a message. @@ -1381,7 +1390,7 @@ class Mailbox: """ raise ValueError - def deleteMessage(self, i): + def deleteMessage(self, i: int) -> None: """ Mark a message for deletion. @@ -1397,7 +1406,7 @@ class Mailbox: """ raise ValueError - def undeleteMessages(self): + def undeleteMessages(self) -> None: """ Undelete all messages marked for deletion. @@ -1406,7 +1415,7 @@ class Mailbox: """ pass - def sync(self): + def sync(self) -> None: """ Discard the contents of any message marked for deletion. """ diff --git a/contrib/python/Twisted/py3/twisted/mail/protocols.py b/contrib/python/Twisted/py3/twisted/mail/protocols.py index 7bd1eebbee7..035a1b0c12b 100644 --- a/contrib/python/Twisted/py3/twisted/mail/protocols.py +++ b/contrib/python/Twisted/py3/twisted/mail/protocols.py @@ -5,7 +5,9 @@ """ Mail protocol support. """ +from __future__ import annotations +from typing import TYPE_CHECKING, Callable from zope.interface import implementer @@ -16,6 +18,9 @@ from twisted.internet import defer, protocol from twisted.mail import pop3, relay, smtp from twisted.python import log +if TYPE_CHECKING: + from twisted.mail.mail import MailService + @implementer(smtp.IMessageDelivery) class DomainDeliveryBase: @@ -250,18 +255,16 @@ class VirtualPOP3(pop3.POP3): """ A virtual hosting POP3 server. - @type service: L{MailService} @ivar service: The email service that created this server. This must be set by the service. - @type domainSpecifier: L{bytes} @ivar domainSpecifier: The character to use to split an email address into local-part and domain. The default is '@'. """ - service = None + service: MailService | None = None - domainSpecifier = b"@" # Gaagh! I hate POP3. No standardized way + domainSpecifier: bytes = b"@" # Gaagh! I hate POP3. No standardized way # to indicate user@host. '@' doesn't work # with NS, e.g. @@ -296,21 +299,18 @@ class VirtualPOP3(pop3.POP3): pop3.APOPCredentials(self.magic, user, digest), None, pop3.IMailbox ) - def authenticateUserPASS(self, user, password): + def authenticateUserPASS( + self, user: bytes, password: bytes + ) -> defer.Deferred[tuple[type[pop3.IMailbox], pop3.IMailbox, Callable[[], None]]]: """ Perform authentication for a username/password login. Override the default lookup scheme to allow virtual domains. - @type user: L{bytes} @param user: The name of the user attempting to log in. - @type password: L{bytes} @param password: The password to authenticate with. - @rtype: L{Deferred} which successfully results in 3-L{tuple} of - (L{IMailbox <pop3.IMailbox>}, L{IMailbox <pop3.IMailbox>} - provider, no-argument callable) @return: A deferred which fires when authentication is complete. If successful, it returns an L{IMailbox <pop3.IMailbox>} interface, a mailbox and a logout function. If authentication fails, the @@ -318,24 +318,28 @@ class VirtualPOP3(pop3.POP3): <twisted.cred.error.UnauthorizedLogin>} error. """ user, domain = self.lookupDomain(user) + assert ( + self.service is not None + ), "must have a service to be able to authenticate" try: portal = self.service.lookupPortal(domain) except KeyError: return defer.fail(UnauthorizedLogin()) else: - return portal.login(UsernamePassword(user, password), None, pop3.IMailbox) + result: defer.Deferred[ + tuple[type[pop3.IMailbox], pop3.IMailbox, Callable[[], None]] + ] = portal.login(UsernamePassword(user, password), None, pop3.IMailbox) + return result - def lookupDomain(self, user): + def lookupDomain(self, user: bytes) -> tuple[bytes, bytes]: """ - Check whether a domain is among the virtual domains supported by the - mail service. + Check whether a domain part of the given email address is among the + virtual domains supported by the mail service. - @type user: L{bytes} @param user: An email address. - @rtype: 2-L{tuple} of (L{bytes}, L{bytes}) - @return: The local part and the domain part of the email address if the - domain is supported. + @return: a 2-tuple of (local part, domain part) of the email address if + the domain is supported. @raise POP3Error: When the domain is not supported by the mail service. """ @@ -343,6 +347,7 @@ class VirtualPOP3(pop3.POP3): user, domain = user.split(self.domainSpecifier, 1) except ValueError: domain = b"" + assert self.service is not None, "cannot look up domain if service not set" if domain not in self.service.domains: raise pop3.POP3Error("no such domain {}".format(domain.decode("utf-8"))) return user, domain diff --git a/contrib/python/Twisted/py3/twisted/persisted/_tokenize.py b/contrib/python/Twisted/py3/twisted/persisted/_tokenize.py index 2ae94292a04..aefd22587f8 100644 --- a/contrib/python/Twisted/py3/twisted/persisted/_tokenize.py +++ b/contrib/python/Twisted/py3/twisted/persisted/_tokenize.py @@ -15,11 +15,11 @@ It accepts a readline-like method which is called repeatedly to get the next line of input (or b"" for EOF). It generates 5-tuples with these members: - the token type (see token.py) - the token (a string) - the starting (row, column) indices of the token (a 2-tuple of ints) - the ending (row, column) indices of the token (a 2-tuple of ints) - the original line (string) + - the token type (see token.py) + - the token (a string) + - the starting (row, column) indices of the token (a 2-tuple of ints) + - the ending (row, column) indices of the token (a 2-tuple of ints) + - the original line (string) It is designed to match the working of the Python tokenizer exactly, except that it produces COMMENT tokens for comments and gives type OP for all @@ -446,9 +446,9 @@ def untokenize(iterable): only two tokens are passed, the resulting output is poor. Round-trip invariant for full input: - Untokenized source will match input source exactly + Untokenized source will match input source exactly - Round-trip invariant for limited input: + Round-trip invariant for limited input:: # Output bytes will tokenize back to the input t1 = [tok[:2] for tok in tokenize(f.readline)] newcode = untokenize(t1) @@ -591,7 +591,7 @@ def tokenize(readline): must be a callable object which provides the same interface as the readline() method of built-in file objects. Each call to the function should return one line of input as bytes. Alternatively, readline - can be a callable function terminating with StopIteration: + can be a callable function terminating with StopIteration:: readline = open(myfile, 'rb').__next__ # Example of alternate readline The generator produces 5-tuples with these members: the token type; the diff --git a/contrib/python/Twisted/py3/twisted/persisted/dirdbm.py b/contrib/python/Twisted/py3/twisted/persisted/dirdbm.py index da8375b77c2..e9bdc560f55 100644 --- a/contrib/python/Twisted/py3/twisted/persisted/dirdbm.py +++ b/contrib/python/Twisted/py3/twisted/persisted/dirdbm.py @@ -3,34 +3,30 @@ # Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. - """ -DBM-style interface to a directory. +A L{DirDBM} is a L{dbm}-style interface to a directory. Each key is stored as a single file. This is not expected to be very fast or efficient, but it's good for easy debugging. -DirDBMs are *not* thread-safe, they should only be accessed by one thread at +L{DirDBM}s are *not* thread-safe, they should only be accessed by one thread at a time. No files should be placed in the working directory of a DirDBM save those created by the DirDBM itself! - -Maintainer: Itamar Shtull-Trauring """ +from __future__ import annotations import base64 import glob import os import pickle +from typing import AnyStr, Iterable, Mapping, TypeVar, overload from twisted.python.filepath import FilePath -try: - _open # type: ignore[has-type, used-before-def] -except NameError: - _open = open +_T = TypeVar("_T", bound=None) class DirDBM: @@ -41,9 +37,8 @@ class DirDBM: flat files. It can only use strings as keys or values. """ - def __init__(self, name): + def __init__(self, name: bytes) -> None: """ - @type name: str @param name: Base path to use for the directory storage. """ self.dname = os.path.abspath(name) @@ -69,46 +64,46 @@ class DirDBM: else: os.rename(f, old) - def _encode(self, k): + def _encode(self, k: bytes) -> bytes: """ Encode a key so it can be used as a filename. """ # NOTE: '_' is NOT in the base64 alphabet! return base64.encodebytes(k).replace(b"\n", b"_").replace(b"/", b"-") - def _decode(self, k): + def _decode(self, k: bytes) -> bytes: """ Decode a filename to get the key. """ return base64.decodebytes(k.replace(b"_", b"\n").replace(b"-", b"/")) - def _readFile(self, path): + def _readFile(self, path: FilePath[AnyStr]) -> bytes: """ Read in the contents of a file. Override in subclasses to e.g. provide transparently encrypted dirdbm. """ - with _open(path.path, "rb") as f: + with path.open() as f: s = f.read() return s - def _writeFile(self, path, data): + def _writeFile(self, path: FilePath[AnyStr], data: bytes) -> None: """ Write data to a file. Override in subclasses to e.g. provide transparently encrypted dirdbm. """ - with _open(path.path, "wb") as f: + with path.open("w") as f: f.write(data) f.flush() - def __len__(self): + def __len__(self) -> int: """ @return: The number of key/value pairs in this Shelf """ return len(self._dnamePath.listdir()) - def __setitem__(self, k, v): + def __setitem__(self, k: bytes, v: bytes) -> None: """ C{dirdbm[k] = v} Create or modify a textfile in this directory @@ -142,15 +137,14 @@ class DirDBM: old.remove() new.moveTo(old) - def __getitem__(self, k): + def __getitem__(self, k: bytes) -> bytes: """ - C{dirdbm[k]} - Get the contents of a file in this directory as a string. + C{dirdbm[k]} Get the contents of a file in this directory as a string. - @type k: bytes @param k: key to lookup @return: The value associated with C{k} + @raise KeyError: Raised when there is no such key """ if not type(k) == bytes: @@ -161,7 +155,7 @@ class DirDBM: except OSError: raise KeyError(k) - def __delitem__(self, k): + def __delitem__(self, k: bytes) -> None: """ C{del dirdbm[foo]} Delete a file in this directory. @@ -179,13 +173,13 @@ class DirDBM: except OSError: raise KeyError(self._decode(k)) - def keys(self): + def keys(self) -> Iterable[bytes]: """ @return: a L{list} of filenames (keys). """ return list(map(self._decode, self._dnamePath.asBytesMode().listdir())) - def values(self): + def values(self) -> Iterable[bytes]: """ @return: a L{list} of file-contents (values). """ @@ -195,7 +189,7 @@ class DirDBM: vals.append(self[key]) return vals - def items(self): + def items(self) -> Iterable[tuple[bytes, bytes]]: """ @return: a L{list} of 2-tuples containing key/value pairs. """ @@ -205,7 +199,7 @@ class DirDBM: items.append((key, self[key])) return items - def has_key(self, key): + def has_key(self, key: bytes) -> bool: """ @type key: bytes @param key: The key to test @@ -218,7 +212,7 @@ class DirDBM: key = self._encode(key) return self._dnamePath.child(key).isfile() - def setdefault(self, key, value): + def setdefault(self, key: bytes, value: bytes) -> bytes: """ @type key: bytes @param key: The key to lookup @@ -231,71 +225,76 @@ class DirDBM: return value return self[key] - def get(self, key, default=None): + @overload + def get(self, key: bytes) -> bytes: + ... + + @overload + def get(self, key: bytes, default: _T) -> bytes | _T: + ... + + def get(self, key: bytes, default: _T | None = None) -> bytes | _T | None: """ - @type key: bytes @param key: The key to lookup @param default: The value to return if the given key does not exist @return: The value associated with C{key} or C{default} if not - L{DirDBM.has_key(key)} + L{DirDBM.has_key(key)} """ if key in self: return self[key] else: return default - def __contains__(self, key): + def __contains__(self, key: bytes) -> bool: """ @see: L{DirDBM.has_key} """ return self.has_key(key) - def update(self, dict): + def update(self, other: Mapping[bytes, bytes]) -> None: """ Add all the key/value pairs in L{dict} to this dirdbm. Any conflicting keys will be overwritten with the values from L{dict}. - @type dict: mapping @param dict: A mapping of key/value pairs to add to this dirdbm. """ - for key, val in dict.items(): + for key, val in other.items(): self[key] = val - def copyTo(self, path): + def copyTo(self, path: bytes) -> DirDBM: """ Copy the contents of this dirdbm to the dirdbm at C{path}. - @type path: L{str} @param path: The path of the dirdbm to copy to. If a dirdbm exists at the destination path, it is cleared first. @rtype: C{DirDBM} @return: The dirdbm this dirdbm was copied to. """ - path = FilePath(path) - assert path != self._dnamePath + fpath = FilePath(path) + assert fpath != self._dnamePath - d = self.__class__(path.path) + d = self.__class__(fpath.path) d.clear() for k in self.keys(): d[k] = self[k] return d - def clear(self): + def clear(self) -> None: """ Delete all key/value pairs in this dirdbm. """ for k in self.keys(): del self[k] - def close(self): + def close(self) -> None: """ Close this dbm: no-op, for dbm-style interface compliance. """ - def getModificationTime(self, key): + def getModificationTime(self, key: bytes) -> float: """ Returns modification time of an entry. diff --git a/contrib/python/Twisted/py3/twisted/python/_pydoctortemplates/stable-link.js b/contrib/python/Twisted/py3/twisted/python/_pydoctortemplates/stable-link.js new file mode 100644 index 00000000000..54d5bfe7988 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/python/_pydoctortemplates/stable-link.js @@ -0,0 +1,18 @@ +// If the documentation isn't stable or latest, insert a stable link +const HTML_BASE_URL = "https://docs.twisted.org/en/stable/api/"; + +if ((window.location.pathname.indexOf('/stable/') == -1) && (window.location.pathname.indexOf('/latest/') == -1)) { + // Give the user a link to this page, but in the stable version of the docs. + var link = document.getElementById('current-docs-link'); + var url = window.location.pathname; + var filename = url.substring(url.lastIndexOf('/')+1); + // And make it visible + var container = document.getElementById('current-docs-container'); + container.style.display = "block"; + link.href = HTML_BASE_URL + filename; + delete link; + delete container; + delete url; + delete filename; + +}
\ No newline at end of file diff --git a/contrib/python/Twisted/py3/twisted/python/_pydoctortemplates/subheader.html b/contrib/python/Twisted/py3/twisted/python/_pydoctortemplates/subheader.html index 7db8ac44af3..de3f5409628 100644 --- a/contrib/python/Twisted/py3/twisted/python/_pydoctortemplates/subheader.html +++ b/contrib/python/Twisted/py3/twisted/python/_pydoctortemplates/subheader.html @@ -5,25 +5,5 @@ </a> </div> - <!-- Google analytics, obviously. --> - <script src="//www.google-analytics.com/urchin.js" type="text/javascript"></script> - <script type="text/javascript"> - _uacct = "UA-99018-6"; - urchinTracker(); - </script> - - <!-- If the documentation isn't current, insert a current link. --> - <script type="text/javascript"> - if (window.location.pathname.indexOf('/current/') == -1) { - <!-- Give the user a link to this page, but in the current version of the docs. --> - var link = document.getElementById('current-docs-link'); - link.href = window.location.pathname.replace(/\/\d+\.\d+\.\d+\/api\//, '/current/api/'); - <!-- And make it visible --> - var container = document.getElementById('current-docs-container'); - container.style.display = ""; - delete link; - delete container; - } - </script> - + <script src="stable-link.js" type="text/javascript"></script> </div> diff --git a/contrib/python/Twisted/py3/twisted/python/deprecate.py b/contrib/python/Twisted/py3/twisted/python/deprecate.py index aba096d59c2..22b0d162aa9 100644 --- a/contrib/python/Twisted/py3/twisted/python/deprecate.py +++ b/contrib/python/Twisted/py3/twisted/python/deprecate.py @@ -310,7 +310,9 @@ def deprecated( return deprecationDecorator -def deprecatedProperty(version, replacement=None): +def deprecatedProperty( + version: Version, replacement: str | Callable[..., object] | None = None +) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: """ Return a decorator that marks a property as deprecated. To deprecate a regular callable or class, see L{deprecated}. diff --git a/contrib/python/Twisted/py3/twisted/runner/procmon.py b/contrib/python/Twisted/py3/twisted/runner/procmon.py index 8b3749a6a43..865ce00624b 100644 --- a/contrib/python/Twisted/py3/twisted/runner/procmon.py +++ b/contrib/python/Twisted/py3/twisted/runner/procmon.py @@ -5,16 +5,25 @@ """ Support for starting, monitoring, and restarting child process. """ -from typing import Dict, List, Optional +from __future__ import annotations + +from typing import Any, Dict, List, Optional import attr import incremental from twisted.application import service from twisted.internet import error, protocol, reactor as _reactor +from twisted.internet.interfaces import ( + IDelayedCall, + IProcessTransport, + IReactorProcess, + IReactorTime, +) from twisted.logger import Logger from twisted.protocols import basic from twisted.python import deprecate +from twisted.python.failure import Failure @attr.s(frozen=True, auto_attribs=True) @@ -46,7 +55,7 @@ class _Process: cwd: Optional[str] = None @deprecate.deprecated(incremental.Version("Twisted", 18, 7, 0)) - def toTuple(self): + def toTuple(self) -> tuple[list[str], int | None, int | None, dict[str, str]]: """ Convert process to tuple. @@ -61,8 +70,6 @@ class _Process: This allows changing the internal structure of the process list, when warranted by bug fixes or additional features. - - @return: tuple representation of process """ return (self.args, self.uid, self.gid, self.env) @@ -75,27 +82,39 @@ transport = DummyTransport() class LineLogger(basic.LineReceiver): + tag: str + stream: str + service: ProcessMonitor + delimiter: bytes + + # These really ought to be set by a constructor, but the legacy API is a + # no-argument constructor. tag = None stream = None delimiter = b"\n" service = None - def lineReceived(self, line): + def lineReceived(self, line: bytes) -> None: try: - line = line.decode("utf-8") + sline = line.decode("utf-8") except UnicodeDecodeError: - line = repr(line) + sline = repr(line) self.service.log.info( - "[{tag}] {line}", tag=self.tag, line=line, stream=self.stream + "[{tag}] {line}", tag=self.tag, line=sline, stream=self.stream ) class LoggingProtocol(protocol.ProcessProtocol): + service: ProcessMonitor + name: str + + # These really ought to be set by a constructor, but the legacy API is a + # no-argument constructor. service = None name = None - def connectionMade(self): + def connectionMade(self) -> None: self._output = LineLogger() self._output.tag = self.name self._output.stream = "stdout" @@ -111,56 +130,56 @@ class LoggingProtocol(protocol.ProcessProtocol): self._output.makeConnection(transport) self._error.makeConnection(transport) - def outReceived(self, data): + def outReceived(self, data: bytes) -> None: self._output.dataReceived(data) self._outputEmpty = data[-1] == b"\n" - def errReceived(self, data): + def errReceived(self, data: bytes) -> None: self._error.dataReceived(data) self._errorEmpty = data[-1] == b"\n" - def processEnded(self, reason): + def processEnded(self, reason: Failure) -> None: if not self._outputEmpty: self._output.dataReceived(b"\n") if not self._errorEmpty: self._error.dataReceived(b"\n") - self.service.connectionLost(self.name) + self.service._monitoredProcessExited(self.name, reason) @property - def output(self): + def output(self) -> LineLogger: return self._output @property - def empty(self): + def empty(self) -> bool: return self._outputEmpty class ProcessMonitor(service.Service): """ - ProcessMonitor runs processes, monitors their progress, and restarts - them when they die. + ProcessMonitor runs processes, monitors their progress, and restarts them + when they die. The ProcessMonitor will not attempt to restart a process that appears to die instantly -- with each "instant" death (less than 1 second, by - default), it will delay approximately twice as long before restarting - it. A successful run will reset the counter. + default), it will delay approximately twice as long before restarting it. + A successful run will reset the counter. - The primary interface is L{addProcess} and L{removeProcess}. When the + The primary interface is L{addProcess} and L{removeProcess}. When the service is running (that is, when the application it is attached to is running), adding a process automatically starts it. - Each process has a name. This name string must uniquely identify the - process. In particular, attempting to add two processes with the same - name will result in a C{KeyError}. + Each process has a name. This name string must uniquely identify the + process. In particular, attempting to add two processes with the same name + will result in a C{KeyError}. @type threshold: C{float} @ivar threshold: How long a process has to live before the death is considered instant, in seconds. The default value is 1 second. @type killTime: C{float} - @ivar killTime: How long a process being killed has to get its affairs - in order before it gets killed with an unmaskable signal. The - default value is 5 seconds. + @ivar killTime: How long a process being killed has to get its affairs in + order before it gets killed with an unmaskable signal. The default + value is 5 seconds. @type minRestartDelay: C{float} @ivar minRestartDelay: The minimum time (in seconds) to wait before @@ -171,13 +190,12 @@ class ProcessMonitor(service.Service): attempting to restart a process. Default 3600s (1h). @type _reactor: L{IReactorProcess} provider - @ivar _reactor: A provider of L{IReactorProcess} and L{IReactorTime} - which will be used to spawn processes and register delayed calls. + @ivar _reactor: A provider of L{IReactorProcess} and L{IReactorTime} which + will be used to spawn processes and register delayed calls. @type log: L{Logger} @ivar log: The logger used to propagate log messages from spawned processes. - """ threshold = 1 @@ -186,18 +204,24 @@ class ProcessMonitor(service.Service): maxRestartDelay = 3600 log = Logger() - def __init__(self, reactor=_reactor): + def __init__( + self, + reactor: IReactorProcess = _reactor, # type:ignore + ) -> None: self._reactor = reactor + self._clock = IReactorTime(reactor) - self._processes = {} - self.protocols = {} - self.delay = {} - self.timeStarted = {} - self.murder = {} - self.restart = {} + self._processes: dict[str, _Process] = {} + self.protocols: dict[str, LoggingProtocol] = {} + self.delay: dict[str, float] = {} + self.timeStarted: dict[str, float] = {} + self.murder: dict[str, IDelayedCall] = {} + self.restart: dict[str, IDelayedCall] = {} @deprecate.deprecatedProperty(incremental.Version("Twisted", 18, 7, 0)) - def processes(self): + def processes( + self, + ) -> dict[str, tuple[list[str], int | None, int | None, dict[str, str]]]: """ Processes as dict of tuples @@ -206,7 +230,7 @@ class ProcessMonitor(service.Service): return {name: process.toTuple() for name, process in self._processes.items()} @deprecate.deprecated(incremental.Version("Twisted", 18, 7, 0)) - def __getstate__(self): + def __getstate__(self) -> Any: dct = service.Service.__getstate__(self) del dct["_reactor"] dct["protocols"] = {} @@ -218,32 +242,41 @@ class ProcessMonitor(service.Service): dct["processes"] = self.processes return dct - def addProcess(self, name, args, uid=None, gid=None, env={}, cwd=None): + def addProcess( + self, + name: str, + args: list[str], + uid: int | None = None, + gid: int | None = None, + env: dict[str, str] = {}, + cwd: str | None = None, + ) -> None: """ Add a new monitored process and start it immediately if the L{ProcessMonitor} service is running. - Note that args are passed to the system call, not to the shell. If + Note that args are passed to the system call, not to the shell. If running the shell is desired, the common idiom is to use C{ProcessMonitor.addProcess("name", ['/bin/sh', '-c', shell_script])} - @param name: A name for this process. This value must be - unique across all processes added to this monitor. - @type name: C{str} + @param name: A name for this process. This value must be unique across + all processes added to this monitor. + @param args: The argv sequence for the process to launch. - @param uid: The user ID to use to run the process. If L{None}, - the current UID is used. - @type uid: C{int} - @param gid: The group ID to use to run the process. If L{None}, - the current GID is used. - @type uid: C{int} - @param env: The environment to give to the launched process. See + + @param uid: The user ID to use to run the process. If L{None}, the + current UID is used. + + @param gid: The group ID to use to run the process. If L{None}, the + current GID is used. + + @param env: The environment to give to the launched process. See L{IReactorProcess.spawnProcess}'s C{env} parameter. - @type env: C{dict} - @param cwd: The initial working directory of the launched process. - The default of C{None} means inheriting the laucnhing process's - working directory. - @type env: C{dict} + + @param cwd: The initial working directory of the launched process. The + default of C{None} means inheriting the laucnhing process's working + directory. + @raise KeyError: If a process with the given name already exists. """ if name in self._processes: @@ -253,18 +286,17 @@ class ProcessMonitor(service.Service): if self.running: self.startProcess(name) - def removeProcess(self, name): + def removeProcess(self, name: str) -> None: """ Stop the named process and remove it from the list of monitored processes. - @type name: C{str} @param name: A string that uniquely identifies the process. """ self.stopProcess(name) del self._processes[name] - def startService(self): + def startService(self) -> None: """ Start all monitored processes. """ @@ -272,7 +304,7 @@ class ProcessMonitor(service.Service): for name in list(self._processes): self.startProcess(name) - def stopService(self): + def stopService(self) -> None: """ Stop all monitored processes and cancel all scheduled process restarts. """ @@ -286,22 +318,49 @@ class ProcessMonitor(service.Service): for name in list(self._processes): self.stopProcess(name) - def connectionLost(self, name): + @deprecate.deprecatedProperty(incremental.Version("Twisted", 25, 5, 0)) + def connectionLost(self, name: str) -> None: + """ + Called when a monitored processes exits. If + L{service.IService.running} is L{True} (ie the service is started), the + process will be restarted. If the process had been running for more + than L{ProcessMonitor.threshold} seconds it will be restarted + immediately. If the process had been running for less than + L{ProcessMonitor.threshold} seconds, the restart will be delayed and + each time the process dies before the configured threshold, the restart + delay will be doubled - up to a maximum delay of maxRestartDelay sec. + + @param name: A string that uniquely identifies the process which + exited. """ - Called when a monitored processes exits. If + return self._monitoredProcessExited(name) # pragma: no cover + + def _monitoredProcessExited(self, name: str, reason: Failure | None = None) -> None: + """ + Called when a monitored processes exits. If L{service.IService.running} is L{True} (ie the service is started), the - process will be restarted. - If the process had been running for more than - L{ProcessMonitor.threshold} seconds it will be restarted immediately. - If the process had been running for less than + process will be restarted. If the process had been running for more + than L{ProcessMonitor.threshold} seconds it will be restarted + immediately. If the process had been running for less than L{ProcessMonitor.threshold} seconds, the restart will be delayed and each time the process dies before the configured threshold, the restart delay will be doubled - up to a maximum delay of maxRestartDelay sec. - @type name: C{str} - @param name: A string that uniquely identifies the process - which exited. + @param name: A string that uniquely identifies the process which + exited. + + @param reason: The reason why the connection was lost. """ + # Log a warning if reason is something other than ProcessDone + if reason is not None and not reason.check( + error.ProcessDone, + error.ProcessTerminated, + ): + self.log.failure( + "Process '{name}' has exited: {failure!r}", + failure=reason, + name=name, + ) # Cancel the scheduled _forceStopProcess function if the process # dies naturally if name in self.murder: @@ -311,7 +370,7 @@ class ProcessMonitor(service.Service): del self.protocols[name] - if self._reactor.seconds() - self.timeStarted[name] < self.threshold: + if self._clock.seconds() - self.timeStarted[name] < self.threshold: # The process died too fast - backoff nextDelay = self.delay[name] self.delay[name] = min(self.delay[name] * 2, self.maxRestartDelay) @@ -324,11 +383,11 @@ class ProcessMonitor(service.Service): # Schedule a process restart if the service is running if self.running and name in self._processes: - self.restart[name] = self._reactor.callLater( + self.restart[name] = self._clock.callLater( nextDelay, self.startProcess, name ) - def startProcess(self, name): + def startProcess(self, name: str) -> None: """ @param name: The name of the process to be started """ @@ -343,27 +402,27 @@ class ProcessMonitor(service.Service): proto.service = self proto.name = name self.protocols[name] = proto - self.timeStarted[name] = self._reactor.seconds() - self._reactor.spawnProcess( - proto, - process.args[0], - process.args, - uid=process.uid, - gid=process.gid, - env=process.env, - path=process.cwd, - ) + self.timeStarted[name] = self._clock.seconds() + try: + self._reactor.spawnProcess( + proto, + process.args[0], + process.args, + uid=process.uid, + gid=process.gid, + env=process.env, + path=process.cwd, + ) + except OSError: + self._monitoredProcessExited(name, Failure()) - def _forceStopProcess(self, proc): - """ - @param proc: An L{IProcessTransport} provider - """ + def _forceStopProcess(self, proc: IProcessTransport) -> None: try: proc.signalProcess("KILL") except error.ProcessExitedAlready: pass - def stopProcess(self, name): + def stopProcess(self, name: str) -> None: """ @param name: The name of the process to be stopped """ @@ -373,16 +432,17 @@ class ProcessMonitor(service.Service): proto = self.protocols.get(name, None) if proto is not None: proc = proto.transport + assert proc is not None try: proc.signalProcess("TERM") except error.ProcessExitedAlready: pass else: - self.murder[name] = self._reactor.callLater( + self.murder[name] = self._clock.callLater( self.killTime, self._forceStopProcess, proc ) - def restartAll(self): + def restartAll(self) -> None: """ Restart all processes. This is useful for third party management services to allow a user to restart servers because of an outside change diff --git a/contrib/python/Twisted/py3/twisted/trial/_asynctest.py b/contrib/python/Twisted/py3/twisted/trial/_asynctest.py index 048e8dc0378..6765c703f66 100644 --- a/contrib/python/Twisted/py3/twisted/trial/_asynctest.py +++ b/contrib/python/Twisted/py3/twisted/trial/_asynctest.py @@ -4,10 +4,9 @@ """ Things likely to be used by writers of unit tests. - -Maintainer: Jonathan Lange """ +from __future__ import annotations import inspect import warnings @@ -21,6 +20,7 @@ from typing_extensions import ParamSpec # installs a user-specified reactor, installing the default reactor and # breaking reactor installation. See also #6047. from twisted.internet import defer, utils +from twisted.internet.defer import _T from twisted.python import failure from twisted.trial import itrial, util from twisted.trial._synctest import FailTest, SkipTest, SynchronousTestCase @@ -58,7 +58,21 @@ class TestCase(SynchronousTestCase): """ super().__init__(methodName) - def assertFailure(self, deferred, *expectedFailures): + # Acquire and store real reactor so that it does not interfere with any + # patching to the reactor done by the user. + from twisted.internet import reactor + + # A unique name must be used in order not to clash with user code that + # sets their own attributes. + self._twistedPrivateScheduler = reactor + + # Define whether tearDown needs to run. + # It is run only if setUp succeeded + self._twistedPrivateNeedsTearDown = False + + def assertFailure( + self, deferred: defer.Deferred[_T], *expectedFailures: type[BaseException] + ) -> defer.Deferred[_T]: """ Fail if C{deferred} does not errback with one of C{expectedFailures}. Returns the original Deferred with callbacks added. You will need @@ -83,14 +97,12 @@ class TestCase(SynchronousTestCase): failUnlessFailure = assertFailure - def _run(self, methodName, result): - from twisted.internet import reactor - + def _run(self, func, funcDescription, result): timeout = self.getTimeout() def onTimeout(d): e = defer.TimeoutError( - f"{self!r} ({methodName}) still running at {timeout} secs" + f"{self!r} ({funcDescription}) still running at {timeout} secs" ) f = failure.Failure(e) # try to errback the deferred that the test returns (for no gorram @@ -102,7 +114,7 @@ class TestCase(SynchronousTestCase): # if the deferred has been called already but the *back chain # is still unfinished, crash the reactor and report timeout # error ourself. - reactor.crash() + self._twistedPrivateScheduler.crash() self._timedOut = True # see self._wait todo = self.getTodo() if todo is not None and todo.expected(f): @@ -113,61 +125,61 @@ class TestCase(SynchronousTestCase): onTimeout = utils.suppressWarnings( onTimeout, util.suppress(category=DeprecationWarning) ) - method = getattr(self, methodName) - if inspect.isgeneratorfunction(method): + if inspect.isgeneratorfunction(func): exc = TypeError( - "{!r} is a generator function and therefore will never run".format( - method - ) + "{!r} is a generator function and therefore will never run".format(func) ) return defer.fail(exc) d = defer.maybeDeferred( - utils.runWithWarningsSuppressed, self._getSuppress(), method + utils.runWithWarningsSuppressed, self._getSuppress(), func ) - call = reactor.callLater(timeout, onTimeout, d) + call = self._twistedPrivateScheduler.callLater(timeout, onTimeout, d) d.addBoth(lambda x: call.active() and call.cancel() or x) return d def __call__(self, *args, **kwargs): return self.run(*args, **kwargs) - def deferSetUp(self, ignored, result): - d = self._run("setUp", result) - d.addCallbacks( - self.deferTestMethod, - self._ebDeferSetUp, - callbackArgs=(result,), - errbackArgs=(result,), - ) - return d + async def _deferSetUp(self, result): + try: + try: + await self._deferSetUpAndRun(result) + finally: + await self._deferRunCleanups(result) + finally: + if self._twistedPrivateNeedsTearDown: + await self._deferTearDown(result) - def _ebDeferSetUp(self, failure, result): - if failure.check(SkipTest): - result.addSkip(self, self._getSkipReason(self.setUp, failure.value)) - else: - result.addError(self, failure) - if failure.check(KeyboardInterrupt): - result.stop() - return self.deferRunCleanups(None, result) - - def deferTestMethod(self, ignored, result): - d = self._run(self._testMethodName, result) - d.addCallbacks( - self._cbDeferTestMethod, - self._ebDeferTestMethod, - callbackArgs=(result,), - errbackArgs=(result,), - ) - d.addBoth(self.deferRunCleanups, result) - d.addBoth(self.deferTearDown, result) - return d + async def _deferSetUpAndRun(self, result): + """ + Execute the setUp and run part of a test. Teardown and cleanups are not executed. + """ + try: + await self._run(self.setUp, "setUp", result) + except SkipTest as e: + result.addSkip(self, self._getSkipReason(self.setUp, e)) + return + except KeyboardInterrupt as e: + result.addError(self, failure.Failure(e)) + result.stop() + return + except BaseException as e: + result.addError(self, failure.Failure(e)) + return - def _cbDeferTestMethod(self, ignored, result): - if self.getTodo() is not None: - result.addUnexpectedSuccess(self, self.getTodo()) - else: - self._passed = True - return ignored + self._twistedPrivateNeedsTearDown = True + + try: + await self._run( + getattr(self, self._testMethodName), self._testMethodName, result + ) + if self.getTodo() is not None: + result.addUnexpectedSuccess(self, self.getTodo()) + else: + self._passed = True + except BaseException as e: + self._ebDeferTestMethod(failure.Failure(e), result) + raise def _ebDeferTestMethod(self, f, result): todo = self.getTodo() @@ -185,19 +197,19 @@ class TestCase(SynchronousTestCase): else: result.addError(self, f) - def deferTearDown(self, ignored, result): - d = self._run("tearDown", result) - d.addErrback(self._ebDeferTearDown, result) - return d - - def _ebDeferTearDown(self, failure, result): - result.addError(self, failure) - if failure.check(KeyboardInterrupt): + async def _deferTearDown(self, result): + try: + await self._run(self.tearDown, "tearDown", result) + except KeyboardInterrupt as e: + result.addError(self, failure.Failure(e)) result.stop() - self._passed = False + self._passed = False + except BaseException as e: + result.addError(self, failure.Failure(e)) + self._passed = False @defer.inlineCallbacks - def deferRunCleanups(self, ignored, result): + def _deferRunCleanups(self, result): """ Run any scheduled cleanups and report errors (if any) to the result. object. @@ -206,7 +218,11 @@ class TestCase(SynchronousTestCase): while len(self._cleanups) > 0: func, args, kwargs = self._cleanups.pop() try: - yield func(*args, **kwargs) + yield self._run( + lambda: func(*args, **kwargs), + f"cleanup function {func.__name__}", + result, + ) except Exception: failures.append(failure.Failure()) @@ -290,7 +306,7 @@ class TestCase(SynchronousTestCase): self._deprecateReactor(reactor) self._timedOut = False try: - d = self.deferSetUp(None, result) + d = defer.Deferred.fromCoroutine(self._deferSetUp(result)) try: self._wait(d) finally: @@ -310,6 +326,9 @@ class TestCase(SynchronousTestCase): If the function C{f} returns a Deferred, C{TestCase} will wait until the Deferred has fired before proceeding to the next function. + + If the function takes more than C{timeout} settings, then the test will + raise an error. """ return super().addCleanup(f, *args, **kwargs) diff --git a/contrib/python/Twisted/py3/twisted/trial/_dist/functional.py b/contrib/python/Twisted/py3/twisted/trial/_dist/functional.py index 8bb5ecf7a4f..f0258bf44c2 100644 --- a/contrib/python/Twisted/py3/twisted/trial/_dist/functional.py +++ b/contrib/python/Twisted/py3/twisted/trial/_dist/functional.py @@ -30,7 +30,7 @@ def fromOptional(default: _A, optional: Optional[_A]) -> _A: def takeWhile(condition: Callable[[_A], bool], xs: Iterable[_A]) -> Iterable[_A]: """ - :return: An iterable over C{xs} that stops when C{condition} returns + @return: An iterable over C{xs} that stops when C{condition} returns ``False`` based on the value of iterated C{xs}. """ for x in xs: diff --git a/contrib/python/Twisted/py3/twisted/trial/_dist/workerreporter.py b/contrib/python/Twisted/py3/twisted/trial/_dist/workerreporter.py index 5f2d7a0cab1..f266d727e36 100644 --- a/contrib/python/Twisted/py3/twisted/trial/_dist/workerreporter.py +++ b/contrib/python/Twisted/py3/twisted/trial/_dist/workerreporter.py @@ -38,11 +38,11 @@ async def addError( Then, L{managercommands.AddError} is called with the rest of the information and the stream IDs. - :param amp: The connection to use. - :param testName: The name (or ID) of the test the error relates to. - :param errorClass: The fully qualified name of the error type. - :param error: The string representation of the error. - :param frames: The lines of the traceback associated with the error. + @param amp: The connection to use. + @param testName: The name (or ID) of the test the error relates to. + @param errorClass: The fully qualified name of the error type. + @param error: The string representation of the error. + @param frames: The lines of the traceback associated with the error. """ errorStreamId = await stream(amp, chunk(error.encode("utf-8"), MAX_VALUE_LENGTH)) @@ -63,12 +63,12 @@ async def addFailure( """ Like L{addError} but for failures. - :param amp: See L{addError} - :param testName: See L{addError} - :param failClass: The fully qualified name of the exception associated + @param amp: See L{addError} + @param testName: See L{addError} + @param failClass: The fully qualified name of the exception associated with the failure. - :param fail: The string representation of the failure. - :param frames: The lines of the traceback associated with the error. + @param fail: The string representation of the failure. + @param frames: The lines of the traceback associated with the error. """ failStreamId = await stream(amp, chunk(fail.encode("utf-8"), MAX_VALUE_LENGTH)) framesStreamId = await stream(amp, (frame.encode("utf-8") for frame in frames)) @@ -86,10 +86,10 @@ async def addExpectedFailure(amp: AMP, testName: str, error: str, todo: str) -> """ Like L{addError} but for expected failures. - :param amp: See L{addError} - :param testName: See L{addError} - :param error: The string representation of the expected failure. - :param todo: The string description of the expectation. + @param amp: See L{addError} + @param testName: See L{addError} + @param error: The string representation of the expected failure. + @param todo: The string description of the expectation. """ errorStreamId = await stream(amp, chunk(error.encode("utf-8"), MAX_VALUE_LENGTH)) @@ -113,10 +113,10 @@ class ReportingResults: runner believes the test is otherwise complete, it can collect the results and do something with any errors. - :ivar _reporter: The L{WorkerReporter} this object is associated with. + @ivar _reporter: The L{WorkerReporter} this object is associated with. This is the object doing the result reporting. - :ivar _results: A list of L{Deferred} instances representing the results + @ivar _results: A list of L{Deferred} instances representing the results of reporting operations. This is expected to grow over the course of the test run and then be inspected by the runner once the test is over. The public interface to this list is via the context manager @@ -130,7 +130,7 @@ class ReportingResults: """ Begin a new reportable context in which results can be collected. - :return: A sequence which will contain the L{Deferred} instances + @return: A sequence which will contain the L{Deferred} instances representing the results of all test result reporting that happens while the context manager is active. The sequence is extended as the test runs so its value should not be consumed until the test diff --git a/contrib/python/Twisted/py3/twisted/trial/reporter.py b/contrib/python/Twisted/py3/twisted/trial/reporter.py index 4ee67ebcf67..2034a83e261 100644 --- a/contrib/python/Twisted/py3/twisted/trial/reporter.py +++ b/contrib/python/Twisted/py3/twisted/trial/reporter.py @@ -105,6 +105,9 @@ class TestResult(pyunit.TestResult): # The duration of the test. It is None until the test completes. _lastTime: Optional[int] + # Make pytest not think this is test class + __test__ = False + def __init__(self): super().__init__() self.skips = [] @@ -532,29 +535,37 @@ class Reporter(TestResult): When a C{SynchronousTestCase} method fails synchronously, the stack looks like this: - - [0]: C{SynchronousTestCase._run} + - [0]: C{TestCase._run} - [1]: C{util.runWithWarningsSuppressed} - [2:-2]: code in the test method which failed - [-1]: C{_synctest.fail} When a C{TestCase} method fails synchronously, the stack looks like this: - - [0]: C{defer.maybeDeferred} - - [1]: C{utils.runWithWarningsSuppressed} - - [2]: C{utils.runWithWarningsSuppressed} - - [3:-2]: code in the test method which failed + - [0]: C{TestCase._deferSetUpAndRun} + - [1]: C{defer.__iter__} + - [2]: C{defer.raiseException} + - [3]: C{defer.maybeDeferred} + - [4]: C{utils.runWithWarningsSuppressed} + - [5]: C{utils.runWithWarningsSuppressed} + - [6:-2]: code in the test method which failed - [-1]: C{_synctest.fail} When a method fails inside a C{Deferred} (i.e., when the test method returns a C{Deferred}, and that C{Deferred}'s errback fires), the stack captured inside the resulting C{Failure} looks like this: - - [0]: C{defer.Deferred._runCallbacks} - - [1:-2]: code in the testmethod which failed + + - [0]: C{defer._deferSetUpAndRun} + - [1]: C{defer.__iter__} + - [2]: C{defer.Deferred._runCallbacks} + - [3:-2]: code in the testmethod which failed - [-1]: C{_synctest.fail} - As a result, we want to trim either [maybeDeferred, runWWS, runWWS] or - [Deferred._runCallbacks] or [SynchronousTestCase._run, runWWS] from the - front, and trim the [unittest.fail] from the end. + As a result, we want to trim either + [deferTestMethod, __iter__, raiseException, maybeDeferred, runWWS, runWWS] or + [defer.deferTestMethod, __iter__, Deferred._runCallbacks] or + [SynchronousTestCase._run, runWWS] from the front, and trim the [unittest.fail] + from the end. There is also another case, when the test method is badly defined and contains extra arguments. @@ -568,19 +579,25 @@ class Reporter(TestResult): """ newFrames = list(frames) - if len(frames) < 2: + if len(frames) < 3: return newFrames - firstMethod = newFrames[0][0] - firstFile = os.path.splitext(os.path.basename(newFrames[0][1]))[0] - - secondMethod = newFrames[1][0] - secondFile = os.path.splitext(os.path.basename(newFrames[1][1]))[0] - - syncCase = (("_run", "_synctest"), ("runWithWarningsSuppressed", "util")) - asyncCase = (("maybeDeferred", "defer"), ("runWithWarningsSuppressed", "utils")) + frames = [ + (frame[0], os.path.splitext(os.path.basename(frame[1]))[0]) + for frame in newFrames[:3] + ] - twoFrames = ((firstMethod, firstFile), (secondMethod, secondFile)) + syncCase = [("_run", "_synctest"), ("runWithWarningsSuppressed", "util")] + asyncCase = [ + ("_deferSetUpAndRun", "_asynctest"), + ("__iter__", "defer"), + ("raiseException", "failure"), + ] + deferCase = [ + ("_deferSetUpAndRun", "_asynctest"), + ("__iter__", "defer"), + ("_runCallbacks", "defer"), + ] # On PY3, we have an extra frame which is reraising the exception for frame in newFrames: @@ -589,12 +606,12 @@ class Reporter(TestResult): # If it's in the compat module and is reraise, BLAM IT newFrames.pop(newFrames.index(frame)) - if twoFrames == syncCase: + if frames[:2] == syncCase: newFrames = newFrames[2:] - elif twoFrames == asyncCase: + elif frames[:3] == asyncCase: + newFrames = newFrames[6:] + elif frames[:3] == deferCase: newFrames = newFrames[3:] - elif (firstMethod, firstFile) == ("_runCallbacks", "defer"): - newFrames = newFrames[1:] if not newFrames: # The method fails before getting called, probably an argument diff --git a/contrib/python/Twisted/py3/twisted/web/_newclient.py b/contrib/python/Twisted/py3/twisted/web/_newclient.py index 32e12521288..9ae0a0c2ecd 100644 --- a/contrib/python/Twisted/py3/twisted/web/_newclient.py +++ b/contrib/python/Twisted/py3/twisted/web/_newclient.py @@ -610,12 +610,12 @@ def _contentLength(connHeaders: Headers) -> Optional[int]: """ Parse the I{Content-Length} connection header. - Two forms of duplicates are permitted. Header repetition: + Two forms of duplicates are permitted. Header repetition:: Content-Length: 42 Content-Length: 42 - And field value repetition: + And field value repetition:: Content-Length: 42, 42 @@ -1488,10 +1488,10 @@ class HTTP11ClientProtocol(Protocol): _state = "QUIESCENT" _parser: HTTPClientParser | None = None - _finishedRequest: Deferred[Response] | None = None + _finishedRequest: Deferred[IResponse] | None = None _currentRequest: Request | None = None _transportProxy = None - _responseDeferred: Deferred[Response] | None = None + _responseDeferred: Deferred[IResponse] | None = None _log = Logger() def __init__(self, quiescentCallback=lambda c: None): @@ -1506,7 +1506,7 @@ class HTTP11ClientProtocol(Protocol): def state(self): return self._state - def request(self, request): + def request(self, request: Request) -> Deferred[IResponse]: """ Issue C{request} over C{self.transport} and return a L{Deferred} which will fire with a L{Response} instance or an error. @@ -1543,7 +1543,7 @@ class HTTP11ClientProtocol(Protocol): self.transport.abortConnection() self._disconnectParser(Failure(CancelledError())) - self._finishedRequest = Deferred(cancelRequest) + self._finishedRequest: Deferred[IResponse] = Deferred(cancelRequest) # Keep track of the Request object in case we need to call stopWriting # on it. diff --git a/contrib/python/Twisted/py3/twisted/web/_websocket_impl.py b/contrib/python/Twisted/py3/twisted/web/_websocket_impl.py new file mode 100644 index 00000000000..17ca57f7bf7 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/web/_websocket_impl.py @@ -0,0 +1,497 @@ +# -*- test-case-name: twisted.web.test.test_websocket -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Private implementation module for public L{twisted.web.websocket}. +""" + +from __future__ import annotations + +import typing +from dataclasses import dataclass, field +from functools import singledispatch +from typing import Callable, Generic, TypeVar, Union + +from zope.interface import implementer + +from hyperlink import URL +from wsproto import Connection, ConnectionType, WSConnection +from wsproto.connection import ConnectionState +from wsproto.events import ( + AcceptConnection, + BytesMessage, + CloseConnection, + Event, + Ping, + Pong, + RejectConnection, + RejectData, + Request as WSRequest, + TextMessage, +) +from wsproto.handshake import H11Handshake +from wsproto.utilities import RemoteProtocolError + +from twisted.internet.defer import Deferred +from twisted.internet.interfaces import ( + IConsumer, + IProtocol, + IPushProducer, + IReactorTCP, + ITransport, +) +from twisted.internet.protocol import Factory as ProtocolFactory +from twisted.logger import Logger +from twisted.python.failure import Failure +from twisted.web._responses import BAD_REQUEST +from twisted.web.client import ( + URI, + BrowserLikePolicyForHTTPS, + Response, + _StandardEndpointFactory, +) +from twisted.web.http_headers import Headers +from twisted.web.iweb import IAgentEndpointFactory, IPolicyForHTTPS +from twisted.web.resource import Resource +from twisted.web.server import NOT_DONE_YET, Request + + +class WebSocketTransport(typing.Protocol): + """ + The transport that can send websocket messages. + """ + + def sendTextMessage(self, text: str) -> None: + """ + Send a text message. + """ + + def sendBytesMessage(self, data: bytes) -> None: + """ + Send a bytes message. + """ + + def loseConnection(self, code: int = 1000) -> None: + """ + Drop the websocket connection. + """ + + def ping(self, payload: bytes = b"") -> None: + """ + Send a websocket Ping request to measure latency. + + @note: Per U{Mozilla's documentation + <https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#pings_and_pongs_the_heartbeat_of_websockets>}, + multiple 'ping' requests may be coalesced into a single 'pong', and + unsolicited 'pong' requests must be ignored, so we do not return a + L{deferred <twisted.internet.defer.Deferred>} here; pongs are + delivered separately. + """ + + def attachProducer(self, producer: IPushProducer) -> None: + """ + Attach the given L{IPushProducer} to this transport. + """ + + def detachProducer(self) -> None: + """ + Detach a previously attached L{IPushProducer} from this transport. + """ + + +@dataclass +class ConnectionRejected(Exception): + """ + A websocket connection was rejected by an HTTP response. + + @ivar response: The HTTP response that describes the rejection. + """ + + response: Response + + +class WebSocketProtocol(typing.Protocol): + """ + An object that conforms to L{WebSocketProtocol} can receive all the events + from a websocket connection. + + @note: While this is I{sort of} like a L{byte-stream protocol + <twisted.internet.interfaces.IProtocol>}, the interface is distinct in + a few ways; in particular, we have discrete C{negotiationStarted} and + C{negotiationFinished} methods, representing those events in the + websocket handshaking process, and no C{connectionMade}; similarly, + since websockets can natively support both text and bytes messages, + rather than fragmentable segments of a byte stream, we have + C{textMessageReceived} and C{bytesMessageReceived} but no + C{dataReceived}. Finally, this is a L{typing.Protocol} and not a + L{zope.interface.Interface}, since it does not predate the L{typing} + module. + """ + + def negotiationStarted(self, transport: WebSocketTransport) -> None: + """ + An underlying transport (e.g.: a TCP connection) has been established; + negotiation of the websocket transport has begun. + """ + + def negotiationFinished(self) -> None: + """ + Negotiation is complete: a bidirectional websocket channel is now fully + established. + """ + + def pongReceived(self, payload: bytes) -> None: + """ + A Pong message was received. + + @note: Per U{the standard + <https://www.rfc-editor.org/rfc/rfc6455#section-5.5.2>}:: + + A Pong frame sent in response to a Ping frame must have identical + "Application data" as found in the message body of the Ping frame + being replied to. + + If an endpoint receives a Ping frame and has not yet sent Pong + frame(s) in response to previous Ping frame(s), the endpoint MAY + elect to send a Pong frame for only the most recently processed + Ping frame. + + Given that some Pong frames may be dropped, this event should only + be used in concert with the transport's L{.ping + <WebSocketTransport.ping>} method for its intended purpose, to + measure latency and connection durability, not to transport + application data. + """ + + def textMessageReceived(self, message: str) -> None: + """ + A text message was received from the peer. + """ + + def bytesMessageReceived(self, data: bytes) -> None: + """ + A bytes message was received from the peer. + """ + + def connectionLost(self, reason: Failure) -> None: + """ + The websocket connection was lost. + """ + + +_WSP = TypeVar("_WSP", covariant=True, bound=WebSocketProtocol) +_Bootstrap = Callable[[Union[WSConnection, Connection], ITransport], None] +_log = Logger() + + +class WebSocketServerFactory(typing.Protocol[_WSP]): + """ + A L{WebSocketServerFactory} is a factory for a particular kind of + L{WebSocketProtocol} that implements server-side websocket listeners via + L{WebSocketResource}. + """ + + def buildProtocol(self, request: Request) -> _WSP: + """ + To conform to L{WebSocketServerFactory}, you must implement a + C{buildProtocol} method which takes a L{Request + <twisted.web.server.Request>} and returns a L{WebSocketProtocol}. + + @return: a L{WebSocketProtocol} that will handle the inbound + connection. + """ + + +class WebSocketClientFactory(typing.Protocol[_WSP]): + """ + A L{WebSocketClientFactory} is a factory for a particular kind of + L{WebSocketProtocol} that implements client-side websocket listeners via + L{WebSocketClientEndpoint}. + """ + + def buildProtocol(self, url: str) -> _WSP: + """ + To conform to L{WebSocketServerFactory}, you must implement a + C{buildProtocol} method which takes a string representing an URL and + returns a L{WebSocketProtocol}. + + @return: a L{WebSocketProtocol} that will handle the outgoing + connection. + """ + + +@dataclass(frozen=True) +class WebSocketClientEndpoint: + """ + A L{WebSocketClientEndpoint} describes an URL to connect to and a way of + connecting to that URL, that can connect a L{WebSocketClientFactory} to + that URL. + """ + + endpointFactory: IAgentEndpointFactory + """ + an L{IAgentEndpointFactory} that constructs agent endpoints when L{connect + <WebSocketClientEndpoint.connect>} + """ + url: str + """ + the URL to connect to. + """ + + @classmethod + def new( + cls, + reactor: IReactorTCP, + url: str, + tlsPolicy: IPolicyForHTTPS = BrowserLikePolicyForHTTPS(), + connectTimeout: int | None = None, + bindAddress: bytes | None = None, + ) -> WebSocketClientEndpoint: + """ + Construct a L{WebSocketClientEndpoint} from a reactor and a URL. + + @param reactor: The reactor to use for the TCP connection. + + @param url: a string describing an URL where a websocket server lives. + + @param tlsPolicy: The TLS policy to use for HTTPS connections. + + @param connectTimeout: The number of seconds for the TCP-level + connection timeout. + + @param bindAddress: The bind address to use for the TCP client + connections. + + @return: the newly constructed endpoint. + """ + endpointFactory = _StandardEndpointFactory( + reactor, tlsPolicy, connectTimeout, bindAddress + ) + return WebSocketClientEndpoint(endpointFactory, url) + + async def connect(self, protocolFactory: WebSocketClientFactory[_WSP]) -> _WSP: + """ + Make an outgoing connection to this L{WebSocketClientEndpoint}'s HTTPS + connection. + + @param protocolFactory: The constructor for the protocol. + + @return: A coroutine (that yields L{Deferred}s) that completes with the + connected L{WebSocketProtocol} once the websocket connection is + established. + """ + endpoint = self.endpointFactory.endpointForURI( + URI.fromBytes(self.url.encode("utf-8")) + ) + + def clientBootstrap(wsc: WSConnection | Connection, t: ITransport) -> None: + h = URL.fromText(self.url) + target = str(h.replace(scheme="", host="", port=None)) + t.write(wsc.send(WSRequest(h.host, target))) + + connected: _WebSocketWireProtocol[_WSP] = await endpoint.connect( + ProtocolFactory.forProtocol( + lambda: _WebSocketWireProtocol( + WSConnection(ConnectionType.CLIENT), + clientBootstrap, + protocolFactory.buildProtocol(self.url), + ) + ) + ) + return await connected._done + + +@singledispatch +def _handleEvent(event: Event, proto: AnyWSWP) -> None: + """ + Handle a websocket protocol event. + """ + + +@implementer(IProtocol) +@dataclass +class _WebSocketWireProtocol(Generic[_WSP]): + # Required constructor arguments. + _wsconn: WSConnection | Connection + _bootstrap: _Bootstrap + _wsp: _WSP + + # Public attribute. + transport: ITransport = field(init=False) + + # Internal state. + _done: Deferred[_WSP] = field(init=False) + _rejectResponse: Response | None = None + + def makeConnection(self, transport: ITransport) -> None: + self.transport = transport + self._done = Deferred() + self.connectionMade() + + def connectionMade(self) -> None: + # TODO: write during negotiationStarted breaks the connection? + self._wsp.negotiationStarted(self) + self._bootstrap(self._wsconn, self.transport) + + def dataReceived(self, data: bytes) -> None: + self._wsconn.receive_data(data) + for event in self._wsconn.events(): + _handleEvent(event, self) + + def connectionLost(self, reason: Failure) -> None: + self._wsp.connectionLost(reason) + if self._rejectResponse is not None: + self._rejectResponse._bodyDataFinished(reason) + self._rejectResponse = None + + # Implementation of WebSocketTransport + def sendTextMessage(self, text: str) -> None: + t = self.transport + assert t is not None + t.write(self._wsconn.send(TextMessage(text))) + + def sendBytesMessage(self, data: bytes) -> None: + t = self.transport + assert t is not None + t.write(self._wsconn.send(BytesMessage(data))) + + def ping(self, payload: bytes = b"") -> None: + t = self.transport + assert t is not None + t.write(self._wsconn.send(Ping(payload))) + + def loseConnection(self, code: int = 1000, reason: str = "") -> None: + t = self.transport + assert t is not None + t.write(self._wsconn.send(CloseConnection(code, reason))) + t.loseConnection() + + def attachProducer(self, producer: IPushProducer) -> None: + IConsumer(self.transport).registerProducer(producer, True) + + def detachProducer(self) -> None: + IConsumer(self.transport).unregisterProducer() + + def _completeConnection(self) -> None: + done = self._done + del self._done + done.callback(self._wsp) + self._wsp.negotiationFinished() + + def _rejectConnection(self) -> None: + assert self._rejectResponse is not None + done = self._done + del self._done + done.errback(ConnectionRejected(self._rejectResponse)) + + +AnyWSWP = _WebSocketWireProtocol[WebSocketProtocol] + + +@_handleEvent.register +def _handle_acceptConnection(event: AcceptConnection, proto: AnyWSWP) -> None: + proto._completeConnection() + + +@_handleEvent.register +def _handle_rejectConnection(event: RejectConnection, proto: AnyWSWP) -> None: + hdr = Headers() + for k, v in event.headers: + hdr.addRawHeader(k, v) + proto._rejectResponse = Response("1.1", event.status_code, "", hdr, proto.transport) + proto._rejectConnection() + + +@_handleEvent.register +def _handle_rejectData(event: RejectData, proto: AnyWSWP) -> None: + assert ( + proto._rejectResponse is not None + ), "response should never be None when receiving RejectData" + proto._rejectResponse._bodyDataReceived(event.data) + if event.body_finished: + proto.transport.loseConnection() + + +@_handleEvent.register +def _handle_textMessage(event: TextMessage, proto: AnyWSWP) -> None: + proto._wsp.textMessageReceived(event.data) + + +@_handleEvent.register +def _handle_bytesMessage(event: BytesMessage, proto: AnyWSWP) -> None: + proto._wsp.bytesMessageReceived(event.data) + + +@_handleEvent.register +def _handle_ping(event: Ping, proto: AnyWSWP) -> None: + proto.transport.write(proto._wsconn.send(event.response())) + + +@_handleEvent.register +def _handle_pong(event: Pong, proto: AnyWSWP) -> None: + proto._wsp.pongReceived(event.payload) + + +@_handleEvent.register +def _handle_closeConnection(event: CloseConnection, proto: AnyWSWP) -> None: + assert proto.transport is not None + if proto._wsconn.state != ConnectionState.CLOSED: + proto.transport.write(proto._wsconn.send(event.response())) + proto.transport.loseConnection() + + +def _negotiationError(request: Request) -> bytes: + request.setResponseCode(BAD_REQUEST) + request.setHeader("content-type", "text/plain") + return b"websocket protocol negotiation error" + + +class WebSocketResource(Resource): + """ + A L{WebSocketResource} is a L{Resource} that presents a websocket listener. + You can install it into any twisted web server resource hierarchy. + """ + + def __init__(self, factory: WebSocketServerFactory[WebSocketProtocol]) -> None: + """ + Create a L{WebSocketResource} that will respond to incoming connections + with the given L{WebSocketServerFactory}. + + @param factory: The factory that will be used to respond to inbound + websocket connections on appropriately formatted GET requests. + """ + super().__init__() + self.factory = factory + + def render_GET(self, request: Request) -> bytes | int: + """ + This implementation of the C{GET} HTTP method will respond to inbound + websocket connections. + """ + handshake = H11Handshake(ConnectionType.SERVER) + raw = request.requestHeaders.getAllRawHeaders() + simpleHeaders = [(hkey, val) for hkey, hvals in raw for val in hvals] + try: + handshake.initiate_upgrade_connection(simpleHeaders, request.path) + except RemoteProtocolError as rpe: + _log.error("{request} failed with {rpe}", request=request, rpe=rpe) + return _negotiationError(request) + wsprot = self.factory.buildProtocol(request) + assert wsprot is not None, "connection not accepted by twisted" + toSend = handshake.send(AcceptConnection()) + wscon = handshake.connection + t = request.channel.transport + assert t is not None, "channel transport not connected" + assert wscon is not None, "connection not accepted by wsproto" + wireProto = _WebSocketWireProtocol(wscon, serverBootstrap, wsprot) + request.channel._protocolUpgradeForWebsockets(wireProto) + t.write(toSend) + wsprot.negotiationFinished() + return NOT_DONE_YET + + +def serverBootstrap(wsc: WSConnection | Connection, t: ITransport) -> None: + """ + The server requires no bootstrapping, so this is a no-op. + """ diff --git a/contrib/python/Twisted/py3/twisted/web/client.py b/contrib/python/Twisted/py3/twisted/web/client.py index cfd945d5d40..d3cd11fb845 100644 --- a/contrib/python/Twisted/py3/twisted/web/client.py +++ b/contrib/python/Twisted/py3/twisted/web/client.py @@ -15,7 +15,7 @@ import zlib from dataclasses import dataclass from functools import wraps from http.cookiejar import CookieJar -from typing import TYPE_CHECKING, Iterable, Optional +from typing import TYPE_CHECKING, Hashable, Iterable, Optional from urllib.parse import urldefrag, urljoin, urlunparse as _urlunparse from zope.interface import implementer @@ -26,7 +26,13 @@ from twisted.internet import defer, protocol, task from twisted.internet.abstract import isIPv6Address from twisted.internet.defer import Deferred from twisted.internet.endpoints import HostnameEndpoint, wrapClientTLS -from twisted.internet.interfaces import IOpenSSLContextFactory, IProtocol +from twisted.internet.interfaces import ( + IAddress, + IOpenSSLContextFactory, + IProtocol, + IReactorTime, + IStreamClientEndpoint, +) from twisted.logger import Logger from twisted.python.compat import nativeString, networkString from twisted.python.components import proxyForInterface @@ -137,7 +143,7 @@ class URI: scheme, netloc, path, params, query, fragment = http.urlparse(uri) if defaultPort is None: - if scheme == b"https": + if scheme in {b"https", b"wss"}: defaultPort = 443 else: defaultPort = 80 @@ -658,7 +664,7 @@ class _HTTP11ClientFactory(protocol.Factory): self._quiescentCallback, self._metadata ) - def buildProtocol(self, addr): + def buildProtocol(self, addr: IAddress) -> HTTP11ClientProtocol: return HTTP11ClientProtocol(self._quiescentCallback) @@ -777,7 +783,9 @@ class HTTPConnectionPool: self._connections = {} self._timeouts = {} - def getConnection(self, key, endpoint): + def getConnection( + self, key: Hashable, endpoint: IStreamClientEndpoint + ) -> Deferred[HTTP11ClientProtocol]: """ Supply a connection, newly created or retrieved from the pool, to be used for one HTTP request. @@ -815,7 +823,9 @@ class HTTPConnectionPool: return self._newConnection(key, endpoint) - def _newConnection(self, key, endpoint): + def _newConnection( + self, key: Hashable, endpoint: IStreamClientEndpoint + ) -> Deferred[HTTP11ClientProtocol]: """ Create a new connection. @@ -826,7 +836,10 @@ class HTTPConnectionPool: self._putConnection(key, protocol) factory = self._factory(quiescentCallback, repr(endpoint)) - return endpoint.connect(factory) + result: Deferred[HTTP11ClientProtocol] = endpoint.connect( + factory + ) # type:ignore[assignment] + return result def _removeConnection(self, key, connection): """ @@ -892,7 +905,7 @@ class _AgentBase: @ivar _pool: The L{HTTPConnectionPool} used to manage HTTP connections. """ - def __init__(self, reactor, pool): + def __init__(self, reactor: IReactorTime, pool: HTTPConnectionPool | None) -> None: if pool is None: pool = HTTPConnectionPool(reactor, False) self._reactor = reactor @@ -910,8 +923,15 @@ class _AgentBase: return b"%b:%d" % (host, port) def _requestWithEndpoint( - self, key, endpoint, method, parsedURI, headers, bodyProducer, requestPath - ): + self, + key: tuple[bytes, bytes, int], + endpoint: IStreamClientEndpoint, + method: bytes, + parsedURI: URI, + headers: Headers | None, + bodyProducer: IBodyProducer | None, + requestPath: bytes, + ) -> Deferred[IResponse]: """ Issue a new request, given the endpoint and the path sent as part of the request. @@ -935,7 +955,7 @@ class _AgentBase: d = self._pool.getConnection(key, endpoint) - def cbConnected(proto): + def cbConnected(proto: HTTP11ClientProtocol) -> Deferred[IResponse]: return proto.request( Request._construct( method, @@ -947,8 +967,7 @@ class _AgentBase: ) ) - d.addCallback(cbConnected) - return d + return d.addCallback(cbConnected) @implementer(IAgentEndpointFactory) @@ -988,7 +1007,7 @@ class _StandardEndpointFactory: self._connectTimeout = connectTimeout self._bindAddress = bindAddress - def endpointForURI(self, uri): + def endpointForURI(self, uri: URI) -> IStreamClientEndpoint: """ Connect directly over TCP for C{b'http'} scheme, and TLS for C{b'https'}. @@ -996,7 +1015,6 @@ class _StandardEndpointFactory: @param uri: L{URI} to connect to. @return: Endpoint to connect to. - @rtype: L{IStreamClientEndpoint} """ kwargs = {} if self._connectTimeout is not None: @@ -1015,9 +1033,9 @@ class _StandardEndpointFactory: ) endpoint = HostnameEndpoint(self._reactor, host, uri.port, **kwargs) - if uri.scheme == b"http": + if uri.scheme in {b"http", b"ws"}: return endpoint - elif uri.scheme == b"https": + elif uri.scheme in {b"https", b"wss"}: connectionCreator = self._policyForHTTPS.creatorForNetloc( uri.host, uri.port ) @@ -1149,7 +1167,13 @@ class Agent(_AgentBase): """ return self._endpointFactory.endpointForURI(uri) - def request(self, method, uri, headers=None, bodyProducer=None): + def request( + self, + method: bytes, + uri: bytes, + headers: Optional[Headers] = None, + bodyProducer: Optional[IBodyProducer] = None, + ) -> Deferred[IResponse]: """ Issue a request to the server indicated by the given C{uri}. diff --git a/contrib/python/Twisted/py3/twisted/web/http.py b/contrib/python/Twisted/py3/twisted/web/http.py index 8aa31dfe306..c2894fbd253 100644 --- a/contrib/python/Twisted/py3/twisted/web/http.py +++ b/contrib/python/Twisted/py3/twisted/web/http.py @@ -108,6 +108,7 @@ import os import re import tempfile import warnings +from collections import defaultdict from email import message_from_bytes from email.message import EmailMessage, Message from io import BufferedIOBase, BytesIO, TextIOWrapper @@ -252,16 +253,16 @@ monthname_lower = [name and name.lower() for name in monthname] def _parseRequestLine(line: bytes) -> tuple[bytes, bytes, bytes]: """ - Parse an HTTP request line, which looks like: + Parse an HTTP request line, which looks like:: GET /foo/bar HTTP/1.1 This function attempts to validate the well-formedness of - the line. RFC 9112 section 3 provides this ABNF: + the line. RFC 9112 section 3 provides this ABNF:: request-line = method SP request-target SP HTTP-version - We allow any method that is a valid token: + We allow any method that is a valid token:: method = token token = 1*tchar @@ -272,7 +273,7 @@ def _parseRequestLine(line: bytes) -> tuple[bytes, bytes, bytes]: We allow any non-empty request-target that contains only printable ASCII characters (no whitespace). - The RFC defines HTTP-version like this: + The RFC defines HTTP-version like this:: HTTP-version = HTTP-name "/" DIGIT "." DIGIT HTTP-name = %s"HTTP" @@ -283,7 +284,7 @@ def _parseRequestLine(line: bytes) -> tuple[bytes, bytes, bytes]: @returns: C{(method, request, version)} three-tuple - @raises: L{ValueError} when malformed + @raises ValueError: when malformed """ method, request, version = line.split(b" ") @@ -323,7 +324,7 @@ def _getMultiPartArgs(content: bytes, ctype: bytes) -> dict[bytes, list[bytes]]: """ Parse the content of a multipart/form-data request. """ - result = {} + result = defaultdict(list) multiPartHeaders = b"MIME-Version: 1.0\r\n" + b"Content-Type: " + ctype + b"\r\n" msg = message_from_bytes(multiPartHeaders + content) if not msg.is_multipart(): @@ -339,7 +340,7 @@ def _getMultiPartArgs(content: bytes, ctype: bytes) -> dict[bytes, list[bytes]]: if not name: continue payload: bytes = part.get_payload(decode=True) # type:ignore[assignment] - result[name.encode("utf8")] = [payload] + result[name.encode("utf8")].append(payload) return result @@ -921,17 +922,26 @@ class Request: etag = None lastModified = None args = None - path = None + path: bytes = None # type:ignore[assignment] content = None _forceSSL = 0 _disconnected = False _log = Logger() + _parsePOSTFormSubmission: bool - def __init__(self, channel: HTTPChannel, queued: object = _QUEUED_SENTINEL) -> None: + def __init__( + self, + channel: HTTPChannel, + queued: object = _QUEUED_SENTINEL, + parsePOSTFormSubmission: bool = True, + ) -> None: """ @param channel: the channel we're connected to. @param queued: (deprecated) are we in the request queue, or can we start writing to the transport? + @param parsePOSTFormSubmission: If C{True}, the default, parse MIME multipart and + URL-encoded body uploads into C{request.args}. This can use large + amounts of memory for large uploads. """ self.notifications: List[Deferred[None]] = [] self.channel = channel @@ -952,6 +962,7 @@ class Request: queued = False self.queued = queued + self._parsePOSTFormSubmission = parsePOSTFormSubmission def _cleanup(self): """ @@ -1069,7 +1080,12 @@ class Request: if ctype is not None: ctype = ctype[0] - if self.method == b"POST" and ctype and clength: + if ( + self.method == b"POST" + and ctype + and clength + and self._parsePOSTFormSubmission + ): mfd = b"multipart/form-data" key = _parseContentType(ctype) if key == b"application/x-www-form-urlencoded": @@ -1362,10 +1378,9 @@ class Request: other than HTTP (and HTTPS) requests @type httpOnly: L{bool} - @param sameSite: One of L{None} (default), C{'lax'} or C{'strict'}. - Direct browsers not to send this cookie on cross-origin requests. - Please see: - U{https://tools.ietf.org/html/draft-west-first-party-cookies-07} + @param sameSite: One of L{None} (default), C{'lax'}, C{'none'} or C{'strict'}. + Direct browsers not to send this cookie on cross-origin requests. + See: U{https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value} @type sameSite: L{None}, L{bytes} or L{str} @raise ValueError: If the value for C{sameSite} is not supported. @@ -1416,7 +1431,15 @@ class Request: cookie = cookie + b"; HttpOnly" if sameSite: sameSite = _ensureBytes(sameSite).lower() - if sameSite not in [b"lax", b"strict"]: + # See more info about sameSite usage here + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + if not secure and sameSite == b"none": + raise ValueError( + "Invalid value for sameSite: " + + repr(sameSite) + + '. Missing the "secure" attribute' + ) + if sameSite not in [b"lax", b"strict", b"none"]: raise ValueError("Invalid value for sameSite: " + repr(sameSite)) cookie += b"; SameSite=" + sameSite self.cookies.append(cookie) @@ -2530,6 +2553,29 @@ class HTTPChannel(basic.LineReceiver, policies.TimeoutMixin): req = self.requests[-1] req.requestReceived(command, path, version) + def _protocolUpgradeForWebsockets(self, protocol: IProtocol) -> None: + """ + Monkeypatch the C{dataReceived} and C{connectionLost} methods on this + L{HTTPChannel} to deliver data to a websocket protocol implementation. + + This API is used by Twisted's own websocket implementation in + L{twisted.web.websocket} and is tested with the same, but is + intentionally NOT publicly exposed yet, and would need to be tested for + a bunch of additional edge cases (in particular, being invoked in other + parts of the request lifecycle and delivering sensible errors) if it + were going to be. + + @param protocol: The byte-level protocol implementing a websocket + transport, which will fully handle all delivered data for this + channel. + """ + self.dataReceived = protocol.dataReceived # type:ignore[method-assign] + self.connectionLost = protocol.connectionLost # type:ignore[method-assign] + assert ( + self.transport is not None + ), "websocket upgraded attempted on disconnected HTTP channel" + protocol.makeConnection(self.transport) + def rawDataReceived(self, data: bytes) -> None: """ This is called when this HTTP/1.1 parser is in raw mode rather than diff --git a/contrib/python/Twisted/py3/twisted/web/http_headers.py b/contrib/python/Twisted/py3/twisted/web/http_headers.py index 88b653439b5..49e945744b4 100644 --- a/contrib/python/Twisted/py3/twisted/web/http_headers.py +++ b/contrib/python/Twisted/py3/twisted/web/http_headers.py @@ -5,6 +5,7 @@ """ An API for storing HTTP header names and values. """ +from __future__ import annotations from typing import ( AnyStr, @@ -96,13 +97,15 @@ class Headers: ) return NotImplemented - def copy(self): + def copy(self) -> Headers: """ Return a copy of itself with the same headers set. @return: A new L{Headers} """ - return self.__class__(self._rawHeaders) + # pretty sure this type:ignore is a mypy bug: + # https://github.com/python/mypy/issues/18279 + return self.__class__(self._rawHeaders) # type:ignore[arg-type] def hasHeader(self, name: AnyStr) -> bool: """ diff --git a/contrib/python/Twisted/py3/twisted/web/pages.py b/contrib/python/Twisted/py3/twisted/web/pages.py index f94f8655b95..051d8293fef 100644 --- a/contrib/python/Twisted/py3/twisted/web/pages.py +++ b/contrib/python/Twisted/py3/twisted/web/pages.py @@ -110,7 +110,7 @@ def notFound( @param brief: A short string displayed as the page title. - @param brief: A longer string displayed in the page body. + @param message: A longer string displayed in the page body. @returns: An L{IResource} """ @@ -127,7 +127,7 @@ def forbidden( @param brief: A short string displayed as the page title. - @param brief: A longer string displayed in the page body. + @param message: A longer string displayed in the page body. @returns: An L{IResource} """ diff --git a/contrib/python/Twisted/py3/twisted/web/server.py b/contrib/python/Twisted/py3/twisted/web/server.py index 1a4318022b9..75344a07588 100644 --- a/contrib/python/Twisted/py3/twisted/web/server.py +++ b/contrib/python/Twisted/py3/twisted/web/server.py @@ -97,8 +97,23 @@ class Request(Copyable, http.Request, components.Componentized): _encoder = None _log = Logger() - def __init__(self, *args, **kw): - _HTTPRequest.__init__(self, *args, **kw) + def __init__(self, channel, *args, parsePOSTFormSubmission=None, **kw): + """ + @param parsePOSTFormSubmission: By default, get this setting from the L{Site}, but + can also be set explicitly. If C{False}, don't parse HTTP bodies. + @type parsePOSTFormSubmission: C{None} or C{bool} + """ + if parsePOSTFormSubmission is None: + parsePOSTFormSubmissionBool = channel.site._parsePOSTFormSubmission + else: + parsePOSTFormSubmissionBool = parsePOSTFormSubmission + _HTTPRequest.__init__( + self, + channel, + *args, + parsePOSTFormSubmission=parsePOSTFormSubmissionBool, + **kw, + ) components.Componentized.__init__(self) def getStateToCopyFor(self, issuer): @@ -784,8 +799,16 @@ class Site(HTTPFactory): sessionFactory = Session sessionCheckTime = 1800 _entropy = os.urandom + _parsePOSTFormSubmission: bool - def __init__(self, resource, requestFactory=None, *args, **kwargs): + def __init__( + self, + resource, + requestFactory=None, + *args, + parsePOSTFormSubmission=True, + **kwargs, + ): """ @param resource: The root of the resource hierarchy. All request traversal for requests received by this factory will begin at this @@ -794,6 +817,11 @@ class Site(HTTPFactory): @param requestFactory: Overwrite for default requestFactory. @type requestFactory: C{callable} or C{class}. + @param parsePOSTFormSubmission: If C{True}, the default, parse MIME multipart and + URL-encoded body uploads into C{request.args}. This can use large + amounts of memory for large uploads. + @type parsePOSTFormSubmission: C{bool} + @see: L{twisted.web.http.HTTPFactory.__init__} """ super().__init__(*args, **kwargs) @@ -801,6 +829,7 @@ class Site(HTTPFactory): self.resource = resource if requestFactory is not None: self.requestFactory = requestFactory + self._parsePOSTFormSubmission = parsePOSTFormSubmission def _openLogFile(self, path): from twisted.python import logfile diff --git a/contrib/python/Twisted/py3/twisted/web/websocket.py b/contrib/python/Twisted/py3/twisted/web/websocket.py new file mode 100644 index 00000000000..ab926d90538 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/web/websocket.py @@ -0,0 +1,40 @@ +# -*- test-case-name: twisted.web.test.test_websocket -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Websocket (rfc6455) client and server support. + +For websocket servers, place a L{WebSocketResource} into your Twisted Web +resource hierarchy. + +For websocket clients, create a new endpoint via L{WebSocketClientEndpoint.new} +with the WebSocket server URL and then, on the newly created endpoint, call +L{WebSocketClientEndpoint.connect}. + +Both client-side and server-side application code must conform to +L{WebSocketProtocol}. + +@note: To use this module, you must install Twisted's C{websocket} extra, i.e. + C{pip install twisted[websocket]}. +""" + +from ._websocket_impl import ( + ConnectionRejected, + WebSocketClientEndpoint, + WebSocketClientFactory, + WebSocketProtocol, + WebSocketResource, + WebSocketServerFactory, + WebSocketTransport, +) + +__all__ = [ + "ConnectionRejected", + "WebSocketClientEndpoint", + "WebSocketClientFactory", + "WebSocketProtocol", + "WebSocketResource", + "WebSocketServerFactory", + "WebSocketTransport", +] diff --git a/contrib/python/Twisted/py3/ya.make b/contrib/python/Twisted/py3/ya.make index 93a5caf1101..6d1f2879d56 100644 --- a/contrib/python/Twisted/py3/ya.make +++ b/contrib/python/Twisted/py3/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(24.11.0) +VERSION(25.5.0) LICENSE(MIT) @@ -401,6 +401,7 @@ PY_SRCS( twisted/web/_responses.py twisted/web/_stan.py twisted/web/_template_util.py + twisted/web/_websocket_impl.py twisted/web/client.py twisted/web/demo.py twisted/web/distrib.py @@ -426,6 +427,7 @@ PY_SRCS( twisted/web/twcgi.py twisted/web/util.py twisted/web/vhost.py + twisted/web/websocket.py twisted/web/wsgi.py twisted/web/xmlrpc.py twisted/words/__init__.py @@ -477,6 +479,7 @@ RESOURCE_FILES( twisted/newsfragments/.gitignore twisted/persisted/newsfragments/9831.misc twisted/py.typed + twisted/python/_pydoctortemplates/stable-link.js twisted/python/_pydoctortemplates/subheader.html twisted/python/twisted-completion.zsh twisted/runner/newsfragments/11681.misc |