diff --git a/CVE-2019-12387.patch b/CVE-2019-12387.patch deleted file mode 100644 index 5eec116764e2acaaaf6952cfc66e1a37fa52bbd3..0000000000000000000000000000000000000000 --- a/CVE-2019-12387.patch +++ /dev/null @@ -1,907 +0,0 @@ -diff --git a/src/twisted/web/_newclient.py b/src/twisted/web/_newclient.py -index 370f47d..74a8a6c 100644 ---- a/src/twisted/web/_newclient.py -+++ b/src/twisted/web/_newclient.py -@@ -29,6 +29,8 @@ Various other classes in this module support this usage: - from __future__ import division, absolute_import - __metaclass__ = type - -+import re -+ - from zope.interface import implementer - - from twisted.python.compat import networkString -@@ -579,6 +581,74 @@ class HTTPClientParser(HTTPParser): - - - -+_VALID_METHOD = re.compile( -+ br"\A[%s]+\Z" % ( -+ bytes().join( -+ ( -+ b"!", b"#", b"$", b"%", b"&", b"'", b"*", -+ b"+", b"-", b".", b"^", b"_", b"`", b"|", b"~", -+ b"\x30-\x39", -+ b"\x41-\x5a", -+ b"\x61-\x7A", -+ ), -+ ), -+ ), -+) -+ -+ -+ -+def _ensureValidMethod(method): -+ """ -+ An HTTP method is an HTTP token, which consists of any visible -+ ASCII character that is not a delimiter (i.e. one of -+ C{"(),/:;<=>?@[\\]{}}.) -+ -+ @param method: the method to check -+ @type method: L{bytes} -+ -+ @return: the method if it is valid -+ @rtype: L{bytes} -+ -+ @raise ValueError: if the method is not valid -+ -+ @see: U{https://tools.ietf.org/html/rfc7230#section-3.1.1}, -+ U{https://tools.ietf.org/html/rfc7230#section-3.2.6}, -+ U{https://tools.ietf.org/html/rfc5234#appendix-B.1} -+ """ -+ if _VALID_METHOD.match(method): -+ return method -+ raise ValueError("Invalid method {!r}".format(method)) -+ -+ -+ -+_VALID_URI = re.compile(br'\A[\x21-\x7e]+\Z') -+ -+ -+ -+def _ensureValidURI(uri): -+ """ -+ A valid URI cannot contain control characters (i.e., characters -+ between 0-32, inclusive and 127) or non-ASCII characters (i.e., -+ characters with values between 128-255, inclusive). -+ -+ @param uri: the URI to check -+ @type uri: L{bytes} -+ -+ @return: the URI if it is valid -+ @rtype: L{bytes} -+ -+ @raise ValueError: if the URI is not valid -+ -+ @see: U{https://tools.ietf.org/html/rfc3986#section-3.3}, -+ U{https://tools.ietf.org/html/rfc3986#appendix-A}, -+ U{https://tools.ietf.org/html/rfc5234#appendix-B.1} -+ """ -+ if _VALID_URI.match(uri): -+ return uri -+ raise ValueError("Invalid URI {!r}".format(uri)) -+ -+ -+ - @implementer(IClientRequest) - class Request: - """ -@@ -618,8 +688,8 @@ class Request: - connection, defaults to C{False}. - @type persistent: L{bool} - """ -- self.method = method -- self.uri = uri -+ self.method = _ensureValidMethod(method) -+ self.uri = _ensureValidURI(uri) - self.headers = headers - self.bodyProducer = bodyProducer - self.persistent = persistent -@@ -664,8 +734,15 @@ class Request: - # method would probably be good. It would be nice if this method - # weren't limited to issuing HTTP/1.1 requests. - requestLines = [] -- requestLines.append(b' '.join([self.method, self.uri, -- b'HTTP/1.1\r\n'])) -+ requestLines.append( -+ b' '.join( -+ [ -+ _ensureValidMethod(self.method), -+ _ensureValidURI(self.uri), -+ b'HTTP/1.1\r\n', -+ ] -+ ), -+ ) - if not self.persistent: - requestLines.append(b'Connection: close\r\n') - if TEorCL is not None: -diff --git a/src/twisted/web/client.py b/src/twisted/web/client.py -index 02eb6e9..a1554d3 100644 ---- a/src/twisted/web/client.py -+++ b/src/twisted/web/client.py -@@ -46,6 +46,9 @@ from twisted.web.iweb import UNKNOWN_LENGTH, IAgent, IBodyProducer, IResponse - from twisted.web.http_headers import Headers - from twisted.logger import Logger - -+from twisted.web._newclient import _ensureValidURI, _ensureValidMethod -+ -+ - - class PartialDownloadError(error.Error): - """ -@@ -77,11 +80,13 @@ class HTTPPageGetter(http.HTTPClient): - - _completelyDone = True - -- _specialHeaders = set((b'host', b'user-agent', b'cookie', b'content-length')) -+ _specialHeaders = set( -+ (b'host', b'user-agent', b'cookie', b'content-length'), -+ ) - - def connectionMade(self): -- method = getattr(self.factory, 'method', b'GET') -- self.sendCommand(method, self.factory.path) -+ method = _ensureValidMethod(getattr(self.factory, 'method', b'GET')) -+ self.sendCommand(method, _ensureValidURI(self.factory.path)) - if self.factory.scheme == b'http' and self.factory.port != 80: - host = self.factory.host + b':' + intToBytes(self.factory.port) - elif self.factory.scheme == b'https' and self.factory.port != 443: -@@ -361,7 +366,7 @@ class HTTPClientFactory(protocol.ClientFactory): - # just in case a broken http/1.1 decides to keep connection alive - self.headers.setdefault(b"connection", b"close") - self.postdata = postdata -- self.method = method -+ self.method = _ensureValidMethod(method) - - self.setURL(url) - -@@ -388,6 +393,7 @@ class HTTPClientFactory(protocol.ClientFactory): - return "<%s: %s>" % (self.__class__.__name__, self.url) - - def setURL(self, url): -+ _ensureValidURI(url.strip()) - self.url = url - uri = URI.fromBytes(url) - if uri.scheme and uri.host: -@@ -732,7 +738,7 @@ def _makeGetterFactory(url, factoryFactory, contextFactory=None, - - @return: The factory created by C{factoryFactory} - """ -- uri = URI.fromBytes(url) -+ uri = URI.fromBytes(_ensureValidURI(url.strip())) - factory = factoryFactory(url, *args, **kwargs) - if uri.scheme == b'https': - from twisted.internet import ssl -@@ -1422,6 +1428,9 @@ class _AgentBase(object): - Issue a new request, given the endpoint and the path sent as part of - the request. - """ -+ -+ method = _ensureValidMethod(method) -+ - # Create minimal headers, if necessary: - if headers is None: - headers = Headers() -@@ -1646,6 +1655,7 @@ class Agent(_AgentBase): - - @see: L{twisted.web.iweb.IAgent.request} - """ -+ uri = _ensureValidURI(uri.strip()) - parsedURI = URI.fromBytes(uri) - try: - endpoint = self._getEndpoint(parsedURI) -@@ -1679,6 +1689,8 @@ class ProxyAgent(_AgentBase): - """ - Issue a new request via the configured proxy. - """ -+ uri = _ensureValidURI(uri.strip()) -+ - # Cache *all* connections under the same key, since we are only - # connecting to a single destination, the proxy: - key = ("http-proxy", self._proxyEndpoint) -diff --git a/src/twisted/web/newsfragments/9647.bugfix b/src/twisted/web/newsfragments/9647.bugfix -new file mode 100644 -index 0000000..b76916c ---- /dev/null -+++ b/src/twisted/web/newsfragments/9647.bugfix -@@ -0,0 +1 @@ -+All HTTP clients in twisted.web.client now raise a ValueError when called with a method and/or URL that contain invalid characters. This mitigates CVE-2019-12387. Thanks to Alex Brasetvik for reporting this vulnerability. -\ No newline at end of file -diff --git a/src/twisted/web/test/injectionhelpers.py b/src/twisted/web/test/injectionhelpers.py -new file mode 100644 -index 0000000..ffeb862 ---- /dev/null -+++ b/src/twisted/web/test/injectionhelpers.py -@@ -0,0 +1,168 @@ -+""" -+Helpers for URI and method injection tests. -+ -+@see: U{CVE-2019-12387} -+""" -+ -+import string -+ -+ -+UNPRINTABLE_ASCII = ( -+ frozenset(range(0, 128)) - -+ frozenset(bytearray(string.printable, 'ascii')) -+) -+ -+NONASCII = frozenset(range(128, 256)) -+ -+ -+ -+class MethodInjectionTestsMixin(object): -+ """ -+ A mixin that runs HTTP method injection tests. Define -+ L{MethodInjectionTestsMixin.attemptRequestWithMaliciousMethod} in -+ a L{twisted.trial.unittest.SynchronousTestCase} subclass to test -+ how HTTP client code behaves when presented with malicious HTTP -+ methods. -+ -+ @see: U{CVE-2019-12387} -+ """ -+ -+ def attemptRequestWithMaliciousMethod(self, method): -+ """ -+ Attempt to send a request with the given method. This should -+ synchronously raise a L{ValueError} if either is invalid. -+ -+ @param method: the method (e.g. C{GET\x00}) -+ -+ @param uri: the URI -+ -+ @type method: -+ """ -+ raise NotImplementedError() -+ -+ -+ def test_methodWithCLRFRejected(self): -+ """ -+ Issuing a request with a method that contains a carriage -+ return and line feed fails with a L{ValueError}. -+ """ -+ with self.assertRaises(ValueError) as cm: -+ method = b"GET\r\nX-Injected-Header: value" -+ self.attemptRequestWithMaliciousMethod(method) -+ self.assertRegex(str(cm.exception), "^Invalid method") -+ -+ -+ def test_methodWithUnprintableASCIIRejected(self): -+ """ -+ Issuing a request with a method that contains unprintable -+ ASCII characters fails with a L{ValueError}. -+ """ -+ for c in UNPRINTABLE_ASCII: -+ method = b"GET%s" % (bytearray([c]),) -+ with self.assertRaises(ValueError) as cm: -+ self.attemptRequestWithMaliciousMethod(method) -+ self.assertRegex(str(cm.exception), "^Invalid method") -+ -+ -+ def test_methodWithNonASCIIRejected(self): -+ """ -+ Issuing a request with a method that contains non-ASCII -+ characters fails with a L{ValueError}. -+ """ -+ for c in NONASCII: -+ method = b"GET%s" % (bytearray([c]),) -+ with self.assertRaises(ValueError) as cm: -+ self.attemptRequestWithMaliciousMethod(method) -+ self.assertRegex(str(cm.exception), "^Invalid method") -+ -+ -+ -+class URIInjectionTestsMixin(object): -+ """ -+ A mixin that runs HTTP URI injection tests. Define -+ L{MethodInjectionTestsMixin.attemptRequestWithMaliciousURI} in a -+ L{twisted.trial.unittest.SynchronousTestCase} subclass to test how -+ HTTP client code behaves when presented with malicious HTTP -+ URIs. -+ """ -+ -+ def attemptRequestWithMaliciousURI(self, method): -+ """ -+ Attempt to send a request with the given URI. This should -+ synchronously raise a L{ValueError} if either is invalid. -+ -+ @param uri: the URI. -+ -+ @type method: -+ """ -+ raise NotImplementedError() -+ -+ -+ def test_hostWithCRLFRejected(self): -+ """ -+ Issuing a request with a URI whose host contains a carriage -+ return and line feed fails with a L{ValueError}. -+ """ -+ with self.assertRaises(ValueError) as cm: -+ uri = b"http://twisted\r\n.invalid/path" -+ self.attemptRequestWithMaliciousURI(uri) -+ self.assertRegex(str(cm.exception), "^Invalid URI") -+ -+ -+ def test_hostWithWithUnprintableASCIIRejected(self): -+ """ -+ Issuing a request with a URI whose host contains unprintable -+ ASCII characters fails with a L{ValueError}. -+ """ -+ for c in UNPRINTABLE_ASCII: -+ uri = b"http://twisted%s.invalid/OK" % (bytearray([c]),) -+ with self.assertRaises(ValueError) as cm: -+ self.attemptRequestWithMaliciousURI(uri) -+ self.assertRegex(str(cm.exception), "^Invalid URI") -+ -+ -+ def test_hostWithNonASCIIRejected(self): -+ """ -+ Issuing a request with a URI whose host contains non-ASCII -+ characters fails with a L{ValueError}. -+ """ -+ for c in NONASCII: -+ uri = b"http://twisted%s.invalid/OK" % (bytearray([c]),) -+ with self.assertRaises(ValueError) as cm: -+ self.attemptRequestWithMaliciousURI(uri) -+ self.assertRegex(str(cm.exception), "^Invalid URI") -+ -+ -+ def test_pathWithCRLFRejected(self): -+ """ -+ Issuing a request with a URI whose path contains a carriage -+ return and line feed fails with a L{ValueError}. -+ """ -+ with self.assertRaises(ValueError) as cm: -+ uri = b"http://twisted.invalid/\r\npath" -+ self.attemptRequestWithMaliciousURI(uri) -+ self.assertRegex(str(cm.exception), "^Invalid URI") -+ -+ -+ def test_pathWithWithUnprintableASCIIRejected(self): -+ """ -+ Issuing a request with a URI whose path contains unprintable -+ ASCII characters fails with a L{ValueError}. -+ """ -+ for c in UNPRINTABLE_ASCII: -+ uri = b"http://twisted.invalid/OK%s" % (bytearray([c]),) -+ with self.assertRaises(ValueError) as cm: -+ self.attemptRequestWithMaliciousURI(uri) -+ self.assertRegex(str(cm.exception), "^Invalid URI") -+ -+ -+ def test_pathWithNonASCIIRejected(self): -+ """ -+ Issuing a request with a URI whose path contains non-ASCII -+ characters fails with a L{ValueError}. -+ """ -+ for c in NONASCII: -+ uri = b"http://twisted.invalid/OK%s" % (bytearray([c]),) -+ with self.assertRaises(ValueError) as cm: -+ self.attemptRequestWithMaliciousURI(uri) -+ self.assertRegex(str(cm.exception), "^Invalid URI") -diff --git a/src/twisted/web/test/test_agent.py b/src/twisted/web/test/test_agent.py -index 7a7669b..9b57512 100644 ---- a/src/twisted/web/test/test_agent.py -+++ b/src/twisted/web/test/test_agent.py -@@ -11,7 +11,7 @@ from io import BytesIO - - from zope.interface.verify import verifyObject - --from twisted.trial.unittest import TestCase -+from twisted.trial.unittest import TestCase, SynchronousTestCase - from twisted.web import client, error, http_headers - from twisted.web._newclient import RequestNotSent, RequestTransmissionFailed - from twisted.web._newclient import ResponseNeverReceived, ResponseFailed -@@ -50,6 +50,10 @@ from twisted.internet.endpoints import HostnameEndpoint - from twisted.test.proto_helpers import AccumulatingProtocol - from twisted.test.iosim import IOPump, FakeTransport - from twisted.test.test_sslverify import certificatesForAuthorityAndServer -+from twisted.web.test.injectionhelpers import ( -+ MethodInjectionTestsMixin, -+ URIInjectionTestsMixin, -+) - from twisted.web.error import SchemeNotSupported - from twisted.logger import globalLogPublisher - -@@ -886,6 +890,7 @@ class AgentTests(TestCase, FakeReactorAndConnectMixin, AgentTestsMixin, - """ - Tests for the new HTTP client API provided by L{Agent}. - """ -+ - def makeAgent(self): - """ - @return: a new L{twisted.web.client.Agent} instance -@@ -1307,6 +1312,48 @@ class AgentTests(TestCase, FakeReactorAndConnectMixin, AgentTestsMixin, - - - -+class AgentMethodInjectionTests( -+ FakeReactorAndConnectMixin, -+ MethodInjectionTestsMixin, -+ SynchronousTestCase, -+): -+ """ -+ Test L{client.Agent} against HTTP method injections. -+ """ -+ -+ def attemptRequestWithMaliciousMethod(self, method): -+ """ -+ Attempt a request with the provided method. -+ -+ @param method: see L{MethodInjectionTestsMixin} -+ """ -+ agent = client.Agent(self.createReactor()) -+ uri = b"http://twisted.invalid" -+ agent.request(method, uri, client.Headers(), None) -+ -+ -+ -+class AgentURIInjectionTests( -+ FakeReactorAndConnectMixin, -+ URIInjectionTestsMixin, -+ SynchronousTestCase, -+): -+ """ -+ Test L{client.Agent} against URI injections. -+ """ -+ -+ def attemptRequestWithMaliciousURI(self, uri): -+ """ -+ Attempt a request with the provided method. -+ -+ @param uri: see L{URIInjectionTestsMixin} -+ """ -+ agent = client.Agent(self.createReactor()) -+ method = b"GET" -+ agent.request(method, uri, client.Headers(), None) -+ -+ -+ - class AgentHTTPSTests(TestCase, FakeReactorAndConnectMixin, - IntegrationTestingMixin): - """ -@@ -3105,3 +3152,100 @@ class ReadBodyTests(TestCase): - - warnings = self.flushWarnings() - self.assertEqual(len(warnings), 0) -+ -+ -+class RequestMethodInjectionTests( -+ MethodInjectionTestsMixin, -+ SynchronousTestCase, -+): -+ """ -+ Test L{client.Request} against HTTP method injections. -+ """ -+ -+ def attemptRequestWithMaliciousMethod(self, method): -+ """ -+ Attempt a request with the provided method. -+ -+ @param method: see L{MethodInjectionTestsMixin} -+ """ -+ client.Request( -+ method=method, -+ uri=b"http://twisted.invalid", -+ headers=http_headers.Headers(), -+ bodyProducer=None, -+ ) -+ -+ -+ -+class RequestWriteToMethodInjectionTests( -+ MethodInjectionTestsMixin, -+ SynchronousTestCase, -+): -+ """ -+ Test L{client.Request.writeTo} against HTTP method injections. -+ """ -+ -+ def attemptRequestWithMaliciousMethod(self, method): -+ """ -+ Attempt a request with the provided method. -+ -+ @param method: see L{MethodInjectionTestsMixin} -+ """ -+ headers = http_headers.Headers({b"Host": [b"twisted.invalid"]}) -+ req = client.Request( -+ method=b"GET", -+ uri=b"http://twisted.invalid", -+ headers=headers, -+ bodyProducer=None, -+ ) -+ req.method = method -+ req.writeTo(StringTransport()) -+ -+ -+ -+class RequestURIInjectionTests( -+ URIInjectionTestsMixin, -+ SynchronousTestCase, -+): -+ """ -+ Test L{client.Request} against HTTP URI injections. -+ """ -+ -+ def attemptRequestWithMaliciousURI(self, uri): -+ """ -+ Attempt a request with the provided URI. -+ -+ @param method: see L{URIInjectionTestsMixin} -+ """ -+ client.Request( -+ method=b"GET", -+ uri=uri, -+ headers=http_headers.Headers(), -+ bodyProducer=None, -+ ) -+ -+ -+ -+class RequestWriteToURIInjectionTests( -+ URIInjectionTestsMixin, -+ SynchronousTestCase, -+): -+ """ -+ Test L{client.Request.writeTo} against HTTP method injections. -+ """ -+ -+ def attemptRequestWithMaliciousURI(self, uri): -+ """ -+ Attempt a request with the provided method. -+ -+ @param method: see L{URIInjectionTestsMixin} -+ """ -+ headers = http_headers.Headers({b"Host": [b"twisted.invalid"]}) -+ req = client.Request( -+ method=b"GET", -+ uri=b"http://twisted.invalid", -+ headers=headers, -+ bodyProducer=None, -+ ) -+ req.uri = uri -+ req.writeTo(StringTransport()) -diff --git a/src/twisted/web/test/test_webclient.py b/src/twisted/web/test/test_webclient.py -index 41cff54..680e027 100644 ---- a/src/twisted/web/test/test_webclient.py -+++ b/src/twisted/web/test/test_webclient.py -@@ -7,6 +7,7 @@ Tests for the old L{twisted.web.client} APIs, C{getPage} and friends. - - from __future__ import division, absolute_import - -+import io - import os - from errno import ENOSPC - -@@ -20,7 +21,8 @@ from twisted.trial import unittest, util - from twisted.web import server, client, error, resource - from twisted.web.static import Data - from twisted.web.util import Redirect --from twisted.internet import reactor, defer, interfaces -+from twisted.internet import address, reactor, defer, interfaces -+from twisted.internet.protocol import ClientFactory - from twisted.python.filepath import FilePath - from twisted.protocols.policies import WrappingFactory - from twisted.test.proto_helpers import ( -@@ -35,6 +37,12 @@ from twisted import test - from twisted.logger import (globalLogPublisher, FilteringLogObserver, - LogLevelFilterPredicate, LogLevel, Logger) - -+from twisted.web.test.injectionhelpers import ( -+ MethodInjectionTestsMixin, -+ URIInjectionTestsMixin, -+) -+ -+ - - serverPEM = FilePath(test.__file__).sibling('server.pem') - serverPEMPath = serverPEM.asBytesMode().path -@@ -1519,3 +1527,306 @@ class DeprecationTests(unittest.TestCase): - L{client.HTTPDownloader} is deprecated. - """ - self._testDeprecatedClass("HTTPDownloader") -+ -+ -+ -+class GetPageMethodInjectionTests( -+ MethodInjectionTestsMixin, -+ unittest.SynchronousTestCase, -+): -+ """ -+ Test L{client.getPage} against HTTP method injections. -+ """ -+ -+ def attemptRequestWithMaliciousMethod(self, method): -+ """ -+ Attempt a request with the provided method. -+ -+ @param method: see L{MethodInjectionTestsMixin} -+ """ -+ uri = b'http://twisted.invalid' -+ client.getPage(uri, method=method) -+ -+ -+ -+class GetPageURIInjectionTests( -+ URIInjectionTestsMixin, -+ unittest.SynchronousTestCase, -+): -+ """ -+ Test L{client.getPage} against URI injections. -+ """ -+ -+ def attemptRequestWithMaliciousURI(self, uri): -+ """ -+ Attempt a request with the provided URI. -+ -+ @param uri: see L{URIInjectionTestsMixin} -+ """ -+ client.getPage(uri) -+ -+ -+ -+class DownloadPageMethodInjectionTests( -+ MethodInjectionTestsMixin, -+ unittest.SynchronousTestCase, -+): -+ """ -+ Test L{client.getPage} against HTTP method injections. -+ """ -+ -+ def attemptRequestWithMaliciousMethod(self, method): -+ """ -+ Attempt a request with the provided method. -+ -+ @param method: see L{MethodInjectionTestsMixin} -+ """ -+ uri = b'http://twisted.invalid' -+ client.downloadPage(uri, file=io.BytesIO(), method=method) -+ -+ -+ -+class DownloadPageURIInjectionTests( -+ URIInjectionTestsMixin, -+ unittest.SynchronousTestCase, -+): -+ """ -+ Test L{client.downloadPage} against URI injections. -+ """ -+ -+ def attemptRequestWithMaliciousURI(self, uri): -+ """ -+ Attempt a request with the provided URI. -+ -+ @param uri: see L{URIInjectionTestsMixin} -+ """ -+ client.downloadPage(uri, file=io.BytesIO()) -+ -+ -+ -+def makeHTTPPageGetterFactory(protocolClass, method, host, path): -+ """ -+ Make a L{ClientFactory} that can be used with -+ L{client.HTTPPageGetter} and its subclasses. -+ -+ @param protocolClass: The protocol class -+ @type protocolClass: A subclass of L{client.HTTPPageGetter} -+ -+ @param method: the HTTP method -+ -+ @param host: the host -+ -+ @param path: The URI path -+ -+ @return: A L{ClientFactory}. -+ """ -+ factory = ClientFactory.forProtocol(protocolClass) -+ -+ factory.method = method -+ factory.host = host -+ factory.path = path -+ -+ factory.scheme = b"http" -+ factory.port = 0 -+ factory.headers = {} -+ factory.agent = b"User/Agent" -+ factory.cookies = {} -+ -+ return factory -+ -+ -+ -+class HTTPPageGetterMethodInjectionTests( -+ MethodInjectionTestsMixin, -+ unittest.SynchronousTestCase, -+): -+ """ -+ Test L{client.HTTPPageGetter} against HTTP method injections. -+ """ -+ protocolClass = client.HTTPPageGetter -+ -+ def attemptRequestWithMaliciousMethod(self, method): -+ """ -+ Attempt a request with the provided method. -+ -+ @param method: L{MethodInjectionTestsMixin} -+ """ -+ transport = StringTransport() -+ factory = makeHTTPPageGetterFactory( -+ self.protocolClass, -+ method=method, -+ host=b"twisted.invalid", -+ path=b"/", -+ ) -+ getter = factory.buildProtocol( -+ address.IPv4Address("TCP", "127.0.0.1", 0), -+ ) -+ getter.makeConnection(transport) -+ -+ -+ -+class HTTPPageGetterURIInjectionTests( -+ URIInjectionTestsMixin, -+ unittest.SynchronousTestCase, -+): -+ """ -+ Test L{client.HTTPPageGetter} against HTTP URI injections. -+ """ -+ protocolClass = client.HTTPPageGetter -+ -+ def attemptRequestWithMaliciousURI(self, uri): -+ """ -+ Attempt a request with the provided URI. -+ -+ @param uri: L{URIInjectionTestsMixin} -+ """ -+ transport = StringTransport() -+ # Setting the host and path to the same value is imprecise but -+ # doesn't require parsing an invalid URI. -+ factory = makeHTTPPageGetterFactory( -+ self.protocolClass, -+ method=b"GET", -+ host=uri, -+ path=uri, -+ ) -+ getter = factory.buildProtocol( -+ address.IPv4Address("TCP", "127.0.0.1", 0), -+ ) -+ getter.makeConnection(transport) -+ -+ -+ -+class HTTPPageDownloaderMethodInjectionTests( -+ HTTPPageGetterMethodInjectionTests -+): -+ -+ """ -+ Test L{client.HTTPPageDownloader} against HTTP method injections. -+ """ -+ protocolClass = client.HTTPPageDownloader -+ -+ -+ -+class HTTPPageDownloaderURIInjectionTests( -+ HTTPPageGetterURIInjectionTests -+): -+ """ -+ Test L{client.HTTPPageDownloader} against HTTP URI injections. -+ """ -+ protocolClass = client.HTTPPageDownloader -+ -+ -+ -+class HTTPClientFactoryMethodInjectionTests( -+ MethodInjectionTestsMixin, -+ unittest.SynchronousTestCase, -+): -+ """ -+ Tests L{client.HTTPClientFactory} against HTTP method injections. -+ """ -+ -+ def attemptRequestWithMaliciousMethod(self, method): -+ """ -+ Attempt a request with the provided method. -+ -+ @param method: L{MethodInjectionTestsMixin} -+ """ -+ client.HTTPClientFactory(b"https://twisted.invalid", method) -+ -+ -+ -+class HTTPClientFactoryURIInjectionTests( -+ URIInjectionTestsMixin, -+ unittest.SynchronousTestCase, -+): -+ """ -+ Tests L{client.HTTPClientFactory} against HTTP URI injections. -+ """ -+ -+ def attemptRequestWithMaliciousURI(self, uri): -+ """ -+ Attempt a request with the provided URI. -+ -+ @param uri: L{URIInjectionTestsMixin} -+ """ -+ client.HTTPClientFactory(uri) -+ -+ -+ -+class HTTPClientFactorySetURLURIInjectionTests( -+ URIInjectionTestsMixin, -+ unittest.SynchronousTestCase, -+): -+ """ -+ Tests L{client.HTTPClientFactory.setURL} against HTTP URI injections. -+ """ -+ -+ def attemptRequestWithMaliciousURI(self, uri): -+ """ -+ Attempt a request with the provided URI. -+ -+ @param uri: L{URIInjectionTestsMixin} -+ """ -+ client.HTTPClientFactory(b"https://twisted.invalid").setURL(uri) -+ -+ -+ -+class HTTPDownloaderMethodInjectionTests( -+ MethodInjectionTestsMixin, -+ unittest.SynchronousTestCase, -+): -+ """ -+ Tests L{client.HTTPDownloader} against HTTP method injections. -+ """ -+ -+ def attemptRequestWithMaliciousMethod(self, method): -+ """ -+ Attempt a request with the provided method. -+ -+ @param method: L{MethodInjectionTestsMixin} -+ """ -+ client.HTTPDownloader( -+ b"https://twisted.invalid", -+ io.BytesIO(), -+ method=method, -+ ) -+ -+ -+ -+class HTTPDownloaderURIInjectionTests( -+ URIInjectionTestsMixin, -+ unittest.SynchronousTestCase, -+): -+ """ -+ Tests L{client.HTTPDownloader} against HTTP URI injections. -+ """ -+ -+ def attemptRequestWithMaliciousURI(self, uri): -+ """ -+ Attempt a request with the provided URI. -+ -+ @param uri: L{URIInjectionTestsMixin} -+ """ -+ client.HTTPDownloader(uri, io.BytesIO()) -+ -+ -+ -+class HTTPDownloaderSetURLURIInjectionTests( -+ URIInjectionTestsMixin, -+ unittest.SynchronousTestCase, -+): -+ """ -+ Tests L{client.HTTPDownloader.setURL} against HTTP URI injections. -+ """ -+ -+ def attemptRequestWithMaliciousURI(self, uri): -+ """ -+ Attempt a request with the provided URI. -+ -+ @param uri: L{URIInjectionTestsMixin} -+ """ -+ downloader = client.HTTPDownloader( -+ b"https://twisted.invalid", -+ io.BytesIO(), -+ ) -+ downloader.setURL(uri) diff --git a/CVE-2019-12855.patch b/CVE-2019-12855.patch deleted file mode 100644 index 94c3270f6a7757c5415c0609932c5e2223bf0e67..0000000000000000000000000000000000000000 --- a/CVE-2019-12855.patch +++ /dev/null @@ -1,1307 +0,0 @@ -From 488bdd0b80cd1084359e34b8d36ae536520b1f86 Mon Sep 17 00:00:00 2001 -From: Ralph Meijer -Date: Tue, 7 May 2019 12:26:14 -0400 -Subject: [PATCH 01/17] Use optionsForClientTLS to verify server certificate by - default - ---- - .../words/protocols/jabber/xmlstream.py | 2 +- - .../words/test/test_jabberxmlstream.py | 61 +++++++++++++------ - 2 files changed, 44 insertions(+), 19 deletions(-) - -diff --git a/src/twisted/words/protocols/jabber/xmlstream.py b/src/twisted/words/protocols/jabber/xmlstream.py -index c191e9ae219..70d9267b705 100644 ---- a/src/twisted/words/protocols/jabber/xmlstream.py -+++ b/src/twisted/words/protocols/jabber/xmlstream.py -@@ -414,7 +414,7 @@ def onProceed(self, obj): - """ - - self.xmlstream.removeObserver('/failure', self.onFailure) -- ctx = ssl.CertificateOptions() -+ ctx = ssl.optionsForClientTLS(self.xmlstream.otherEntity.host) - self.xmlstream.transport.startTLS(ctx) - self.xmlstream.reset() - self.xmlstream.sendHeader() -diff --git a/src/twisted/words/test/test_jabberxmlstream.py b/src/twisted/words/test/test_jabberxmlstream.py -index 302171d7297..ccccf87372c 100644 ---- a/src/twisted/words/test/test_jabberxmlstream.py -+++ b/src/twisted/words/test/test_jabberxmlstream.py -@@ -14,6 +14,7 @@ - from twisted.internet import defer, task - from twisted.internet.error import ConnectionLost - from twisted.internet.interfaces import IProtocolFactory -+from twisted.internet._sslverify import ClientTLSOptions - from twisted.python import failure - from twisted.python.compat import unicode - from twisted.test import proto_helpers -@@ -665,7 +666,7 @@ def setUp(self): - - self.savedSSL = xmlstream.ssl - -- self.authenticator = xmlstream.Authenticator() -+ self.authenticator = xmlstream.ConnectAuthenticator(u'example.com') - self.xmlstream = xmlstream.XmlStream(self.authenticator) - self.xmlstream.send = self.output.append - self.xmlstream.connectionMade() -@@ -679,9 +680,9 @@ def tearDown(self): - xmlstream.ssl = self.savedSSL - - -- def testWantedSupported(self): -+ def test_wantedSupported(self): - """ -- Test start when TLS is wanted and the SSL library available. -+ When TLS is wanted and SSL available, StartTLS is initiated. - """ - self.xmlstream.transport = proto_helpers.StringTransport() - self.xmlstream.transport.startTLS = lambda ctx: self.done.append('TLS') -@@ -690,7 +691,8 @@ def testWantedSupported(self): - - d = self.init.start() - d.addCallback(self.assertEqual, xmlstream.Reset) -- starttls = self.output[0] -+ self.assertEqual(2, len(self.output)) -+ starttls = self.output[1] - self.assertEqual('starttls', starttls.name) - self.assertEqual(NS_XMPP_TLS, starttls.uri) - self.xmlstream.dataReceived("" % NS_XMPP_TLS) -@@ -698,40 +700,63 @@ def testWantedSupported(self): - - return d - -+ -+ def test_certificateVerify(self): -+ """ -+ The server certificate will be verified. -+ """ -+ -+ def fakeStartTLS(contextFactory): -+ self.assertIsInstance(contextFactory, ClientTLSOptions) -+ self.assertEqual(contextFactory._hostname, u"example.com") -+ self.done.append('TLS') -+ -+ self.xmlstream.transport = proto_helpers.StringTransport() -+ self.xmlstream.transport.startTLS = fakeStartTLS -+ self.xmlstream.reset = lambda: self.done.append('reset') -+ self.xmlstream.sendHeader = lambda: self.done.append('header') -+ -+ d = self.init.start() -+ self.xmlstream.dataReceived("" % NS_XMPP_TLS) -+ self.assertEqual(['TLS', 'reset', 'header'], self.done) -+ return d -+ -+ - if not xmlstream.ssl: - testWantedSupported.skip = "SSL not available" -+ test_certificateVerify = "SSL not available" - - -- def testWantedNotSupportedNotRequired(self): -+ def test_wantedNotSupportedNotRequired(self): - """ -- Test start when TLS is wanted and the SSL library available. -+ No StartTLS is initiated when wanted, not required, SSL not available. - """ - xmlstream.ssl = None - - d = self.init.start() - d.addCallback(self.assertEqual, None) -- self.assertEqual([], self.output) -+ self.assertEqual(1, len(self.output)) - - return d - - -- def testWantedNotSupportedRequired(self): -+ def test_wantedNotSupportedRequired(self): - """ -- Test start when TLS is wanted and the SSL library available. -+ TLSNotSupported is raised when TLS is required but not available. - """ - xmlstream.ssl = None - self.init.required = True - - d = self.init.start() - self.assertFailure(d, xmlstream.TLSNotSupported) -- self.assertEqual([], self.output) -+ self.assertEqual(1, len(self.output)) - - return d - - -- def testNotWantedRequired(self): -+ def test_notWantedRequired(self): - """ -- Test start when TLS is not wanted, but required by the server. -+ TLSRequired is raised when TLS is not wanted, but required by server. - """ - tls = domish.Element(('urn:ietf:params:xml:ns:xmpp-tls', 'starttls')) - tls.addElement('required') -@@ -739,15 +764,15 @@ def testNotWantedRequired(self): - self.init.wanted = False - - d = self.init.start() -- self.assertEqual([], self.output) -+ self.assertEqual(1, len(self.output)) - self.assertFailure(d, xmlstream.TLSRequired) - - return d - - -- def testNotWantedNotRequired(self): -+ def test_notWantedNotRequired(self): - """ -- Test start when TLS is not wanted, but required by the server. -+ No StartTLS is initiated when not wanted and not required. - """ - tls = domish.Element(('urn:ietf:params:xml:ns:xmpp-tls', 'starttls')) - self.xmlstream.features = {(tls.uri, tls.name): tls} -@@ -755,13 +780,13 @@ def testNotWantedNotRequired(self): - - d = self.init.start() - d.addCallback(self.assertEqual, None) -- self.assertEqual([], self.output) -+ self.assertEqual(1, len(self.output)) - return d - - -- def testFailed(self): -+ def test_failed(self): - """ -- Test failed TLS negotiation. -+ TLSFailed is raised when the server responds with a failure. - """ - # Pretend that ssl is supported, it isn't actually used when the - # server starts out with a failure in response to our initial - -From 0ff32b1bf115acc90d223b9ce9820063cf89003d Mon Sep 17 00:00:00 2001 -From: Ralph Meijer -Date: Tue, 7 May 2019 15:54:33 -0400 -Subject: [PATCH 02/17] Fix client example to print disconnection reason - ---- - docs/words/examples/xmpp_client.py | 3 ++- - 1 file changed, 2 insertions(+), 1 deletion(-) - -diff --git a/docs/words/examples/xmpp_client.py b/docs/words/examples/xmpp_client.py -index cb80202c67f..4a3651009b4 100644 ---- a/docs/words/examples/xmpp_client.py -+++ b/docs/words/examples/xmpp_client.py -@@ -53,8 +53,9 @@ def connected(self, xs): - xs.rawDataOutFn = self.rawDataOut - - -- def disconnected(self, xs): -+ def disconnected(self, reason): - print('Disconnected.') -+ print(reason) - - self.finished.callback(None) - - -From 89954dfb18e613be583c74e22a3dd55d66e7d975 Mon Sep 17 00:00:00 2001 -From: Ralph Meijer -Date: Tue, 7 May 2019 18:23:49 -0400 -Subject: [PATCH 03/17] Allow for custom contextFactory to TLS initializer - ---- - .../words/protocols/jabber/xmlstream.py | 6 ++++- - .../words/test/test_jabberxmlstream.py | 24 +++++++++++++++++++ - 2 files changed, 29 insertions(+), 1 deletion(-) - -diff --git a/src/twisted/words/protocols/jabber/xmlstream.py b/src/twisted/words/protocols/jabber/xmlstream.py -index 70d9267b705..51a8466b16a 100644 ---- a/src/twisted/words/protocols/jabber/xmlstream.py -+++ b/src/twisted/words/protocols/jabber/xmlstream.py -@@ -406,6 +406,7 @@ class TLSInitiatingInitializer(BaseFeatureInitiatingInitializer): - - feature = (NS_XMPP_TLS, 'starttls') - wanted = True -+ contextFactory = None - _deferred = None - - def onProceed(self, obj): -@@ -414,7 +415,10 @@ def onProceed(self, obj): - """ - - self.xmlstream.removeObserver('/failure', self.onFailure) -- ctx = ssl.optionsForClientTLS(self.xmlstream.otherEntity.host) -+ if self.contextFactory: -+ ctx = self.contextFactory -+ else: -+ ctx = ssl.optionsForClientTLS(self.xmlstream.otherEntity.host) - self.xmlstream.transport.startTLS(ctx) - self.xmlstream.reset() - self.xmlstream.sendHeader() -diff --git a/src/twisted/words/test/test_jabberxmlstream.py b/src/twisted/words/test/test_jabberxmlstream.py -index ccccf87372c..863cad0f328 100644 ---- a/src/twisted/words/test/test_jabberxmlstream.py -+++ b/src/twisted/words/test/test_jabberxmlstream.py -@@ -14,6 +14,7 @@ - from twisted.internet import defer, task - from twisted.internet.error import ConnectionLost - from twisted.internet.interfaces import IProtocolFactory -+from twisted.internet.ssl import CertificateOptions - from twisted.internet._sslverify import ClientTLSOptions - from twisted.python import failure - from twisted.python.compat import unicode -@@ -722,9 +723,32 @@ def fakeStartTLS(contextFactory): - return d - - -+ def test_certificateVerifyContext(self): -+ """ -+ A custom contextFactory is passed through to startTLS. -+ """ -+ ctx = CertificateOptions() -+ self.init.contextFactory = ctx -+ -+ def fakeStartTLS(contextFactory): -+ self.assertIs(ctx, contextFactory) -+ self.done.append('TLS') -+ -+ self.xmlstream.transport = proto_helpers.StringTransport() -+ self.xmlstream.transport.startTLS = fakeStartTLS -+ self.xmlstream.reset = lambda: self.done.append('reset') -+ self.xmlstream.sendHeader = lambda: self.done.append('header') -+ -+ d = self.init.start() -+ self.xmlstream.dataReceived("" % NS_XMPP_TLS) -+ self.assertEqual(['TLS', 'reset', 'header'], self.done) -+ return d -+ -+ - if not xmlstream.ssl: - testWantedSupported.skip = "SSL not available" - test_certificateVerify = "SSL not available" -+ test_certificateVerifyContext = "SSL not available" - - - def test_wantedNotSupportedNotRequired(self): - -From 4759e27af0ffa2e61538d5e0a66c3e57e20d3f5b Mon Sep 17 00:00:00 2001 -From: Ralph Meijer -Date: Wed, 8 May 2019 13:19:17 -0400 -Subject: [PATCH 04/17] Add docstrings for new contextFactory attribute - ---- - src/twisted/words/protocols/jabber/xmlstream.py | 5 +++++ - 1 file changed, 5 insertions(+) - -diff --git a/src/twisted/words/protocols/jabber/xmlstream.py b/src/twisted/words/protocols/jabber/xmlstream.py -index 51a8466b16a..88ad21f76a6 100644 ---- a/src/twisted/words/protocols/jabber/xmlstream.py -+++ b/src/twisted/words/protocols/jabber/xmlstream.py -@@ -402,6 +402,11 @@ class TLSInitiatingInitializer(BaseFeatureInitiatingInitializer): - - @cvar wanted: indicates if TLS negotiation is wanted. - @type wanted: C{bool} -+ -+ @cvar contextFactory: An object which creates appropriately configured TLS -+ connections. This is passed to C{startTLS} on the transport and is -+ preferably created using L{twisted.internet.ssl.optionsForClientTLS}. -+ @type contextFactory: L{IOpenSSLClientConnectionCreator} - """ - - feature = (NS_XMPP_TLS, 'starttls') - -From fa18e8e65cf486ea9adc8e9a9a6df7e168098ce8 Mon Sep 17 00:00:00 2001 -From: Ralph Meijer -Date: Thu, 9 May 2019 11:11:14 -0400 -Subject: [PATCH 05/17] Clean up connecting authenticators - -This adds an option `required` argument to the inits of initializers -deriving from BaseFeatureInitiatingInitializer, to simplify setup. -Additionally it changes the requiredness of two initializers used by -XMPPAuthenticator: - -* Setup of TLS is now required by default. This ensures that if StartTLS -is not advertized by the server, initialization fails instead of -silently proceeding to authentication without encryption. -* Binding a resource is required by default, because without it servers -will not allow any further meaningful interaction. ---- - src/twisted/words/protocols/jabber/client.py | 28 +++++-------- - .../words/protocols/jabber/xmlstream.py | 9 +++-- - src/twisted/words/test/test_jabberclient.py | 39 ++++++++++++++++++- - .../words/test/test_jabberxmlstream.py | 9 +++++ - 4 files changed, 61 insertions(+), 24 deletions(-) - -diff --git a/src/twisted/words/protocols/jabber/client.py b/src/twisted/words/protocols/jabber/client.py -index ffe6c939d8a..566bc9ff177 100644 ---- a/src/twisted/words/protocols/jabber/client.py -+++ b/src/twisted/words/protocols/jabber/client.py -@@ -206,14 +206,10 @@ def associateWithStream(self, xs): - xs.version = (0, 0) - xmlstream.ConnectAuthenticator.associateWithStream(self, xs) - -- inits = [ (xmlstream.TLSInitiatingInitializer, False), -- (IQAuthInitializer, True), -- ] -- -- for initClass, required in inits: -- init = initClass(xs) -- init.required = required -- xs.initializers.append(init) -+ xs.initializers = [ -+ xmlstream.TLSInitiatingInitializer(xs, required=False), -+ IQAuthInitializer(xs), -+ ] - - # TODO: move registration into an Initializer? - -@@ -377,14 +373,10 @@ def associateWithStream(self, xs): - """ - xmlstream.ConnectAuthenticator.associateWithStream(self, xs) - -- xs.initializers = [CheckVersionInitializer(xs)] -- inits = [ (xmlstream.TLSInitiatingInitializer, False), -- (sasl.SASLInitiatingInitializer, True), -- (BindInitializer, False), -- (SessionInitializer, False), -+ xs.initializers = [ -+ CheckVersionInitializer(xs), -+ xmlstream.TLSInitiatingInitializer(xs, required=True), -+ sasl.SASLInitiatingInitializer(xs, required=True), -+ BindInitializer(xs, required=True), -+ SessionInitializer(xs, required=False), - ] -- -- for initClass, required in inits: -- init = initClass(xs) -- init.required = required -- xs.initializers.append(init) -diff --git a/src/twisted/words/protocols/jabber/xmlstream.py b/src/twisted/words/protocols/jabber/xmlstream.py -index 88ad21f76a6..f7512016c5a 100644 ---- a/src/twisted/words/protocols/jabber/xmlstream.py -+++ b/src/twisted/words/protocols/jabber/xmlstream.py -@@ -316,16 +316,17 @@ class BaseFeatureInitiatingInitializer(object): - - @cvar feature: tuple of (uri, name) of the stream feature root element. - @type feature: tuple of (C{str}, C{str}) -+ - @ivar required: whether the stream feature is required to be advertized - by the receiving entity. - @type required: C{bool} - """ - - feature = None -- required = False - -- def __init__(self, xs): -+ def __init__(self, xs, required=False): - self.xmlstream = xs -+ self.required = required - - - def initialize(self): -@@ -400,10 +401,10 @@ class TLSInitiatingInitializer(BaseFeatureInitiatingInitializer): - set the C{wanted} attribute to False instead of removing it from the list - of initializers, so a proper exception L{TLSRequired} can be raised. - -- @cvar wanted: indicates if TLS negotiation is wanted. -+ @ivar wanted: indicates if TLS negotiation is wanted. - @type wanted: C{bool} - -- @cvar contextFactory: An object which creates appropriately configured TLS -+ @ivar contextFactory: An object which creates appropriately configured TLS - connections. This is passed to C{startTLS} on the transport and is - preferably created using L{twisted.internet.ssl.optionsForClientTLS}. - @type contextFactory: L{IOpenSSLClientConnectionCreator} -diff --git a/src/twisted/words/test/test_jabberclient.py b/src/twisted/words/test/test_jabberclient.py -index d54f88651ad..19be60b34eb 100644 ---- a/src/twisted/words/test/test_jabberclient.py -+++ b/src/twisted/words/test/test_jabberclient.py -@@ -379,6 +379,41 @@ def onSession(iq): - - - -+class BasicAuthenticatorTests(unittest.TestCase): -+ """ -+ Test for both XMPPAuthenticator and XMPPClientFactory. -+ """ -+ def testBasic(self): -+ """ -+ Test basic operations. -+ -+ Setup a basicClientFactory, which sets up a BasicAuthenticator, and let -+ it produce a protocol instance. Then inspect the instance variables of -+ the authenticator and XML stream objects. -+ """ -+ self.client_jid = jid.JID('user@example.com/resource') -+ -+ # Get an XmlStream instance. Note that it gets initialized with the -+ # XMPPAuthenticator (that has its associateWithXmlStream called) that -+ # is in turn initialized with the arguments to the factory. -+ xs = client.basicClientFactory(self.client_jid, -+ 'secret').buildProtocol(None) -+ -+ # test authenticator's instance variables -+ self.assertEqual('example.com', xs.authenticator.otherHost) -+ self.assertEqual(self.client_jid, xs.authenticator.jid) -+ self.assertEqual('secret', xs.authenticator.password) -+ -+ # test list of initializers -+ tls, auth = xs.initializers -+ -+ self.assertIsInstance(tls, xmlstream.TLSInitiatingInitializer) -+ self.assertIsInstance(auth, client.IQAuthInitializer) -+ -+ self.assertFalse(tls.required) -+ -+ -+ - class XMPPAuthenticatorTests(unittest.TestCase): - """ - Test for both XMPPAuthenticator and XMPPClientFactory. -@@ -412,7 +447,7 @@ def testBasic(self): - self.assertIsInstance(bind, client.BindInitializer) - self.assertIsInstance(session, client.SessionInitializer) - -- self.assertFalse(tls.required) -+ self.assertTrue(tls.required) - self.assertTrue(sasl.required) -- self.assertFalse(bind.required) -+ self.assertTrue(bind.required) - self.assertFalse(session.required) -diff --git a/src/twisted/words/test/test_jabberxmlstream.py b/src/twisted/words/test/test_jabberxmlstream.py -index 863cad0f328..6df336deb20 100644 ---- a/src/twisted/words/test/test_jabberxmlstream.py -+++ b/src/twisted/words/test/test_jabberxmlstream.py -@@ -681,6 +681,15 @@ def tearDown(self): - xmlstream.ssl = self.savedSSL - - -+ def test_initRequired(self): -+ """ -+ Passing required sets the instance variable. -+ """ -+ self.init = xmlstream.TLSInitiatingInitializer(self.xmlstream, -+ required=True) -+ self.assertTrue(self.init.required) -+ -+ - def test_wantedSupported(self): - """ - When TLS is wanted and SSL available, StartTLS is initiated. - -From cadf08f3481b689929ad471a17ce29683dc0635d Mon Sep 17 00:00:00 2001 -From: Ralph Meijer -Date: Thu, 9 May 2019 12:05:21 -0400 -Subject: [PATCH 06/17] Provide a way to use custom certificate options for - XMPP clients - -This adds an optional `contextFactory` argument to `XMPPClientFactory` -that is passed on to `XMPPAuthenticator`, which in turn passes it to -`TLSInitiatingInitializer`. ---- - src/twisted/words/protocols/jabber/client.py | 25 ++++++++++--- - .../words/protocols/jabber/xmlstream.py | 9 +++++ - src/twisted/words/test/test_jabberclient.py | 35 ++++++++++++++++--- - 3 files changed, 61 insertions(+), 8 deletions(-) - -diff --git a/src/twisted/words/protocols/jabber/client.py b/src/twisted/words/protocols/jabber/client.py -index 566bc9ff177..4b310e34f38 100644 ---- a/src/twisted/words/protocols/jabber/client.py -+++ b/src/twisted/words/protocols/jabber/client.py -@@ -298,7 +298,7 @@ def start(self): - - - --def XMPPClientFactory(jid, password): -+def XMPPClientFactory(jid, password, contextFactory=None): - """ - Client factory for XMPP 1.0 (only). - -@@ -310,12 +310,20 @@ def XMPPClientFactory(jid, password): - - @param jid: Jabber ID to connect with. - @type jid: L{jid.JID} -+ - @param password: password to authenticate with. - @type password: L{unicode} -+ -+ @param contextFactory: An object which creates appropriately configured TLS -+ connections. This is passed to C{startTLS} on the transport and is -+ preferably created using L{twisted.internet.ssl.optionsForClientTLS}. -+ See L{xmlstream.TLSInitiatingInitializer} for details. -+ @type contextFactory: L{IOpenSSLClientConnectionCreator} -+ - @return: XML stream factory. - @rtype: L{xmlstream.XmlStreamFactory} - """ -- a = XMPPAuthenticator(jid, password) -+ a = XMPPAuthenticator(jid, password, contextFactory=contextFactory) - return xmlstream.XmlStreamFactory(a) - - -@@ -350,16 +358,24 @@ class XMPPAuthenticator(xmlstream.ConnectAuthenticator): - resource binding step, and this is stored in this instance - variable. - @type jid: L{jid.JID} -+ - @ivar password: password to be used during SASL authentication. - @type password: L{unicode} -+ -+ @ivar contextFactory: An object which creates appropriately configured TLS -+ connections. This is passed to C{startTLS} on the transport and is -+ preferably created using L{twisted.internet.ssl.optionsForClientTLS}. -+ See L{xmlstream.TLSInitiatingInitializer} for details. -+ @type contextFactory: L{IOpenSSLClientConnectionCreator} - """ - - namespace = 'jabber:client' - -- def __init__(self, jid, password): -+ def __init__(self, jid, password, contextFactory=None): - xmlstream.ConnectAuthenticator.__init__(self, jid.host) - self.jid = jid - self.password = password -+ self.contextFactory = contextFactory - - - def associateWithStream(self, xs): -@@ -375,7 +391,8 @@ def associateWithStream(self, xs): - - xs.initializers = [ - CheckVersionInitializer(xs), -- xmlstream.TLSInitiatingInitializer(xs, required=True), -+ xmlstream.TLSInitiatingInitializer( -+ xs, required=True, contextFactory=self.contextFactory), - sasl.SASLInitiatingInitializer(xs, required=True), - BindInitializer(xs, required=True), - SessionInitializer(xs, required=False), -diff --git a/src/twisted/words/protocols/jabber/xmlstream.py b/src/twisted/words/protocols/jabber/xmlstream.py -index f7512016c5a..1ed79d47726 100644 ---- a/src/twisted/words/protocols/jabber/xmlstream.py -+++ b/src/twisted/words/protocols/jabber/xmlstream.py -@@ -407,6 +407,9 @@ class TLSInitiatingInitializer(BaseFeatureInitiatingInitializer): - @ivar contextFactory: An object which creates appropriately configured TLS - connections. This is passed to C{startTLS} on the transport and is - preferably created using L{twisted.internet.ssl.optionsForClientTLS}. -+ If C{None}, the default is to verify the server certificate against -+ the trust roots as provided by the platform. See -+ L{twisted.internet._sslverify.platformTrust}. - @type contextFactory: L{IOpenSSLClientConnectionCreator} - """ - -@@ -415,6 +418,12 @@ class TLSInitiatingInitializer(BaseFeatureInitiatingInitializer): - contextFactory = None - _deferred = None - -+ def __init__(self, xs, required=True, contextFactory=None): -+ super(TLSInitiatingInitializer, self).__init__( -+ xs, required=required) -+ self.contextFactory = contextFactory -+ -+ - def onProceed(self, obj): - """ - Proceed with TLS negotiation and reset the XML stream. -diff --git a/src/twisted/words/test/test_jabberclient.py b/src/twisted/words/test/test_jabberclient.py -index 19be60b34eb..2e39de72cee 100644 ---- a/src/twisted/words/test/test_jabberclient.py -+++ b/src/twisted/words/test/test_jabberclient.py -@@ -9,7 +9,7 @@ - - from hashlib import sha1 - --from twisted.internet import defer -+from twisted.internet import defer, ssl - from twisted.python.compat import unicode - from twisted.trial import unittest - from twisted.words.protocols.jabber import client, error, jid, xmlstream -@@ -381,9 +381,10 @@ def onSession(iq): - - class BasicAuthenticatorTests(unittest.TestCase): - """ -- Test for both XMPPAuthenticator and XMPPClientFactory. -+ Test for both BasicAuthenticator and basicClientFactory. - """ -- def testBasic(self): -+ -+ def test_basic(self): - """ - Test basic operations. - -@@ -418,7 +419,8 @@ class XMPPAuthenticatorTests(unittest.TestCase): - """ - Test for both XMPPAuthenticator and XMPPClientFactory. - """ -- def testBasic(self): -+ -+ def test_basic(self): - """ - Test basic operations. - -@@ -451,3 +453,28 @@ def testBasic(self): - self.assertTrue(sasl.required) - self.assertTrue(bind.required) - self.assertFalse(session.required) -+ -+ -+ def test_tlsContextFactory(self): -+ """ -+ Test basic operations. -+ -+ Setup an XMPPClientFactory, which sets up an XMPPAuthenticator, and let -+ it produce a protocol instance. Then inspect the instance variables of -+ the authenticator and XML stream objects. -+ """ -+ self.client_jid = jid.JID('user@example.com/resource') -+ -+ # Get an XmlStream instance. Note that it gets initialized with the -+ # XMPPAuthenticator (that has its associateWithXmlStream called) that -+ # is in turn initialized with the arguments to the factory. -+ contextFactory = ssl.CertificateOptions() -+ factory = client.XMPPClientFactory(self.client_jid, 'secret', -+ contextFactory=contextFactory) -+ xs = factory.buildProtocol(None) -+ -+ # test list of initializers -+ version, tls, sasl, bind, session = xs.initializers -+ -+ self.assertIsInstance(tls, xmlstream.TLSInitiatingInitializer) -+ self.assertIs(contextFactory, tls.contextFactory) - -From 5ed194c0514a04500b3190b0ecbad0cce8b9b82d Mon Sep 17 00:00:00 2001 -From: Ralph Meijer -Date: Thu, 9 May 2019 12:12:32 -0400 -Subject: [PATCH 07/17] Adjust tests to TLSInitiatingInitializer being required - by default - ---- - src/twisted/words/test/test_jabberxmlstream.py | 2 ++ - 1 file changed, 2 insertions(+) - -diff --git a/src/twisted/words/test/test_jabberxmlstream.py b/src/twisted/words/test/test_jabberxmlstream.py -index 6df336deb20..2b8dcd9516e 100644 ---- a/src/twisted/words/test/test_jabberxmlstream.py -+++ b/src/twisted/words/test/test_jabberxmlstream.py -@@ -765,6 +765,7 @@ def test_wantedNotSupportedNotRequired(self): - No StartTLS is initiated when wanted, not required, SSL not available. - """ - xmlstream.ssl = None -+ self.init.required = False - - d = self.init.start() - d.addCallback(self.assertEqual, None) -@@ -810,6 +811,7 @@ def test_notWantedNotRequired(self): - tls = domish.Element(('urn:ietf:params:xml:ns:xmpp-tls', 'starttls')) - self.xmlstream.features = {(tls.uri, tls.name): tls} - self.init.wanted = False -+ self.init.required = False - - d = self.init.start() - d.addCallback(self.assertEqual, None) - -From a1f43907c60cb3f92699067c43fdf166cbac2cea Mon Sep 17 00:00:00 2001 -From: Ralph Meijer -Date: Mon, 27 May 2019 11:00:39 +0200 -Subject: [PATCH 08/17] Add news fragments - ---- - src/twisted/words/newsfragments/9561.bugfix | 1 + - src/twisted/words/newsfragments/9561.feature | 1 + - 2 files changed, 2 insertions(+) - create mode 100644 src/twisted/words/newsfragments/9561.bugfix - create mode 100644 src/twisted/words/newsfragments/9561.feature - -diff --git a/src/twisted/words/newsfragments/9561.bugfix b/src/twisted/words/newsfragments/9561.bugfix -new file mode 100644 -index 00000000000..ac5f905a104 ---- /dev/null -+++ b/src/twisted/words/newsfragments/9561.bugfix -@@ -0,0 +1 @@ -+twisted.words.protocols.jabber.xmlstream.TLSInitiatingInitializer now properly verifies the server's certificate against platform CAs and the stream's domain. -diff --git a/src/twisted/words/newsfragments/9561.feature b/src/twisted/words/newsfragments/9561.feature -new file mode 100644 -index 00000000000..955790a0f24 ---- /dev/null -+++ b/src/twisted/words/newsfragments/9561.feature -@@ -0,0 +1 @@ -+twisted.words.protocols.jabber.xmlstream.TLSInitiatingInitializer and twisted.words.protocols.jabber.client.XMPPClientFactory now take an optional contextFactory for customizing certificate options for StartTLS. - -From 0a93949f91ea22cfc5453c326e36e927c8da1015 Mon Sep 17 00:00:00 2001 -From: Ralph Meijer -Date: Mon, 27 May 2019 13:53:31 +0200 -Subject: [PATCH 09/17] Fix skipping renamed test when SSL is not available - ---- - src/twisted/words/test/test_jabberxmlstream.py | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/src/twisted/words/test/test_jabberxmlstream.py b/src/twisted/words/test/test_jabberxmlstream.py -index 2b8dcd9516e..d9f4962ec0c 100644 ---- a/src/twisted/words/test/test_jabberxmlstream.py -+++ b/src/twisted/words/test/test_jabberxmlstream.py -@@ -755,7 +755,7 @@ def fakeStartTLS(contextFactory): - - - if not xmlstream.ssl: -- testWantedSupported.skip = "SSL not available" -+ test_wantedSupported.skip = "SSL not available" - test_certificateVerify = "SSL not available" - test_certificateVerifyContext = "SSL not available" - - -From 751ac6f754146e5b61ab65d2995be2a9534bd41d Mon Sep 17 00:00:00 2001 -From: Ralph Meijer -Date: Mon, 27 May 2019 14:48:26 +0200 -Subject: [PATCH 10/17] Skip TLS tests if OpenSSL is not available - ---- - src/twisted/words/test/test_jabberclient.py | 12 +++++++++- - .../words/test/test_jabberxmlstream.py | 22 ++++++++++++------- - 2 files changed, 25 insertions(+), 9 deletions(-) - -diff --git a/src/twisted/words/test/test_jabberclient.py b/src/twisted/words/test/test_jabberclient.py -index 2e39de72cee..8afb92951f7 100644 ---- a/src/twisted/words/test/test_jabberclient.py -+++ b/src/twisted/words/test/test_jabberclient.py -@@ -9,13 +9,21 @@ - - from hashlib import sha1 - --from twisted.internet import defer, ssl -+from twisted.internet import defer - from twisted.python.compat import unicode - from twisted.trial import unittest - from twisted.words.protocols.jabber import client, error, jid, xmlstream - from twisted.words.protocols.jabber.sasl import SASLInitiatingInitializer - from twisted.words.xish import utility - -+try: -+ from twisted.internet import ssl -+except ImportError: -+ ssl = None -+ skipWhenNoSSL = "SSL not available" -+else: -+ skipWhenNoSSL = None -+ - IQ_AUTH_GET = '/iq[@type="get"]/query[@xmlns="jabber:iq:auth"]' - IQ_AUTH_SET = '/iq[@type="set"]/query[@xmlns="jabber:iq:auth"]' - NS_BIND = 'urn:ietf:params:xml:ns:xmpp-bind' -@@ -478,3 +486,5 @@ def test_tlsContextFactory(self): - - self.assertIsInstance(tls, xmlstream.TLSInitiatingInitializer) - self.assertIs(contextFactory, tls.contextFactory) -+ -+ test_tlsContextFactory.skip = skipWhenNoSSL -diff --git a/src/twisted/words/test/test_jabberxmlstream.py b/src/twisted/words/test/test_jabberxmlstream.py -index d9f4962ec0c..aad0305ef99 100644 ---- a/src/twisted/words/test/test_jabberxmlstream.py -+++ b/src/twisted/words/test/test_jabberxmlstream.py -@@ -14,8 +14,6 @@ - from twisted.internet import defer, task - from twisted.internet.error import ConnectionLost - from twisted.internet.interfaces import IProtocolFactory --from twisted.internet.ssl import CertificateOptions --from twisted.internet._sslverify import ClientTLSOptions - from twisted.python import failure - from twisted.python.compat import unicode - from twisted.test import proto_helpers -@@ -23,7 +21,15 @@ - from twisted.words.xish import domish - from twisted.words.protocols.jabber import error, ijabber, jid, xmlstream - -- -+try: -+ from twisted.internet import ssl -+except ImportError: -+ ssl = None -+ skipWhenNoSSL = "SSL not available" -+else: -+ skipWhenNoSSL = None -+ from twisted.internet.ssl import CertificateOptions -+ from twisted.internet._sslverify import ClientTLSOptions - - NS_XMPP_TLS = 'urn:ietf:params:xml:ns:xmpp-tls' - -@@ -710,6 +716,8 @@ def test_wantedSupported(self): - - return d - -+ test_wantedSupported.skip = skipWhenNoSSL -+ - - def test_certificateVerify(self): - """ -@@ -731,6 +739,8 @@ def fakeStartTLS(contextFactory): - self.assertEqual(['TLS', 'reset', 'header'], self.done) - return d - -+ test_certificateVerify.skip = skipWhenNoSSL -+ - - def test_certificateVerifyContext(self): - """ -@@ -753,11 +763,7 @@ def fakeStartTLS(contextFactory): - self.assertEqual(['TLS', 'reset', 'header'], self.done) - return d - -- -- if not xmlstream.ssl: -- test_wantedSupported.skip = "SSL not available" -- test_certificateVerify = "SSL not available" -- test_certificateVerifyContext = "SSL not available" -+ test_certificateVerifyContext.skip = skipWhenNoSSL - - - def test_wantedNotSupportedNotRequired(self): - -From 672a6338dea08a17cbe18af3d47bdb14fcd0d84b Mon Sep 17 00:00:00 2001 -From: Ralph Meijer -Date: Mon, 27 May 2019 15:33:20 +0200 -Subject: [PATCH 11/17] Fix indents - ---- - src/twisted/words/test/test_jabberclient.py | 4 ++-- - src/twisted/words/test/test_jabberxmlstream.py | 2 +- - 2 files changed, 3 insertions(+), 3 deletions(-) - -diff --git a/src/twisted/words/test/test_jabberclient.py b/src/twisted/words/test/test_jabberclient.py -index 8afb92951f7..7c31bed8656 100644 ---- a/src/twisted/words/test/test_jabberclient.py -+++ b/src/twisted/words/test/test_jabberclient.py -@@ -17,7 +17,7 @@ - from twisted.words.xish import utility - - try: -- from twisted.internet import ssl -+ from twisted.internet import ssl - except ImportError: - ssl = None - skipWhenNoSSL = "SSL not available" -@@ -406,7 +406,7 @@ def test_basic(self): - # XMPPAuthenticator (that has its associateWithXmlStream called) that - # is in turn initialized with the arguments to the factory. - xs = client.basicClientFactory(self.client_jid, -- 'secret').buildProtocol(None) -+ 'secret').buildProtocol(None) - - # test authenticator's instance variables - self.assertEqual('example.com', xs.authenticator.otherHost) -diff --git a/src/twisted/words/test/test_jabberxmlstream.py b/src/twisted/words/test/test_jabberxmlstream.py -index aad0305ef99..7b384645a2c 100644 ---- a/src/twisted/words/test/test_jabberxmlstream.py -+++ b/src/twisted/words/test/test_jabberxmlstream.py -@@ -22,7 +22,7 @@ - from twisted.words.protocols.jabber import error, ijabber, jid, xmlstream - - try: -- from twisted.internet import ssl -+ from twisted.internet import ssl - except ImportError: - ssl = None - skipWhenNoSSL = "SSL not available" - -From a649757186c12d2b4f4a8e215b4d36ba26bd331f Mon Sep 17 00:00:00 2001 -From: Ralph Meijer -Date: Tue, 28 May 2019 16:53:22 +0200 -Subject: [PATCH 12/17] Better docstring for BasicAuthenticatorTests - ---- - src/twisted/words/test/test_jabberclient.py | 13 ++++++++----- - 1 file changed, 8 insertions(+), 5 deletions(-) - -diff --git a/src/twisted/words/test/test_jabberclient.py b/src/twisted/words/test/test_jabberclient.py -index 7c31bed8656..1403131baf6 100644 ---- a/src/twisted/words/test/test_jabberclient.py -+++ b/src/twisted/words/test/test_jabberclient.py -@@ -394,11 +394,14 @@ class BasicAuthenticatorTests(unittest.TestCase): - - def test_basic(self): - """ -- Test basic operations. -- -- Setup a basicClientFactory, which sets up a BasicAuthenticator, and let -- it produce a protocol instance. Then inspect the instance variables of -- the authenticator and XML stream objects. -+ Authenticator and stream are properly constructed by the factory. -+ -+ The L{xmlstream.XmlStream} protocol created by the factory has the new -+ L{client.BasicAuthenticator} instance in its C{authenticator} -+ attribute. It is set up with C{jid} and C{password} as passed to the -+ factory, C{otherHost} taken from the client JID. The stream futher has -+ two initializers, for TLS and authentication, of which the first has -+ its C{required} attribute set to C{True}. - """ - self.client_jid = jid.JID('user@example.com/resource') - - -From ea2d28f7035cdbc56063a0672acef426086875ff Mon Sep 17 00:00:00 2001 -From: Ralph Meijer -Date: Sun, 16 Jun 2019 18:41:49 +0200 -Subject: [PATCH 13/17] Rename contextFactory to configurationForTLS, make - private vars - ---- - src/twisted/words/newsfragments/9561.feature | 2 +- - src/twisted/words/protocols/jabber/client.py | 37 +++++++++++-------- - .../words/protocols/jabber/xmlstream.py | 28 +++++++------- - src/twisted/words/test/test_jabberclient.py | 26 +++++++------ - .../words/test/test_jabberxmlstream.py | 3 ++ - 5 files changed, 55 insertions(+), 41 deletions(-) - -diff --git a/src/twisted/words/newsfragments/9561.feature b/src/twisted/words/newsfragments/9561.feature -index 955790a0f24..c3b41a6a4c3 100644 ---- a/src/twisted/words/newsfragments/9561.feature -+++ b/src/twisted/words/newsfragments/9561.feature -@@ -1 +1 @@ --twisted.words.protocols.jabber.xmlstream.TLSInitiatingInitializer and twisted.words.protocols.jabber.client.XMPPClientFactory now take an optional contextFactory for customizing certificate options for StartTLS. -+twisted.words.protocols.jabber.xmlstream.TLSInitiatingInitializer and twisted.words.protocols.jabber.client.XMPPClientFactory now take an optional configurationForTLS for customizing certificate options for StartTLS. -diff --git a/src/twisted/words/protocols/jabber/client.py b/src/twisted/words/protocols/jabber/client.py -index 4b310e34f38..db4cbfccf21 100644 ---- a/src/twisted/words/protocols/jabber/client.py -+++ b/src/twisted/words/protocols/jabber/client.py -@@ -298,7 +298,7 @@ def start(self): - - - --def XMPPClientFactory(jid, password, contextFactory=None): -+def XMPPClientFactory(jid, password, configurationForTLS=None): - """ - Client factory for XMPP 1.0 (only). - -@@ -314,16 +314,18 @@ def XMPPClientFactory(jid, password, contextFactory=None): - @param password: password to authenticate with. - @type password: L{unicode} - -- @param contextFactory: An object which creates appropriately configured TLS -- connections. This is passed to C{startTLS} on the transport and is -- preferably created using L{twisted.internet.ssl.optionsForClientTLS}. -- See L{xmlstream.TLSInitiatingInitializer} for details. -- @type contextFactory: L{IOpenSSLClientConnectionCreator} -+ @param configurationForTLS: An object which creates appropriately -+ configured TLS connections. This is passed to C{startTLS} on the -+ transport and is preferably created using -+ L{twisted.internet.ssl.optionsForClientTLS}. See -+ L{xmlstream.TLSInitiatingInitializer} for details. -+ @type configurationForTLS: L{IOpenSSLClientConnectionCreator} - - @return: XML stream factory. - @rtype: L{xmlstream.XmlStreamFactory} - """ -- a = XMPPAuthenticator(jid, password, contextFactory=contextFactory) -+ a = XMPPAuthenticator(jid, password, -+ configurationForTLS=configurationForTLS) - return xmlstream.XmlStreamFactory(a) - - -@@ -361,21 +363,23 @@ class XMPPAuthenticator(xmlstream.ConnectAuthenticator): - - @ivar password: password to be used during SASL authentication. - @type password: L{unicode} -- -- @ivar contextFactory: An object which creates appropriately configured TLS -- connections. This is passed to C{startTLS} on the transport and is -- preferably created using L{twisted.internet.ssl.optionsForClientTLS}. -- See L{xmlstream.TLSInitiatingInitializer} for details. -- @type contextFactory: L{IOpenSSLClientConnectionCreator} - """ - - namespace = 'jabber:client' - -- def __init__(self, jid, password, contextFactory=None): -+ def __init__(self, jid, password, configurationForTLS=None): -+ """ -+ @param configurationForTLS: An object which creates appropriately -+ configured TLS connections. This is passed to C{startTLS} on the -+ transport and is preferably created using -+ L{twisted.internet.ssl.optionsForClientTLS}. See -+ L{xmlstream.TLSInitiatingInitializer} for details. -+ @type configurationForTLS: L{IOpenSSLClientConnectionCreator} -+ """ - xmlstream.ConnectAuthenticator.__init__(self, jid.host) - self.jid = jid - self.password = password -- self.contextFactory = contextFactory -+ self._configurationForTLS = configurationForTLS - - - def associateWithStream(self, xs): -@@ -392,7 +396,8 @@ def associateWithStream(self, xs): - xs.initializers = [ - CheckVersionInitializer(xs), - xmlstream.TLSInitiatingInitializer( -- xs, required=True, contextFactory=self.contextFactory), -+ xs, required=True, -+ configurationForTLS=self._configurationForTLS), - sasl.SASLInitiatingInitializer(xs, required=True), - BindInitializer(xs, required=True), - SessionInitializer(xs, required=False), -diff --git a/src/twisted/words/protocols/jabber/xmlstream.py b/src/twisted/words/protocols/jabber/xmlstream.py -index 1ed79d47726..135d71295df 100644 ---- a/src/twisted/words/protocols/jabber/xmlstream.py -+++ b/src/twisted/words/protocols/jabber/xmlstream.py -@@ -403,25 +403,27 @@ class TLSInitiatingInitializer(BaseFeatureInitiatingInitializer): - - @ivar wanted: indicates if TLS negotiation is wanted. - @type wanted: C{bool} -- -- @ivar contextFactory: An object which creates appropriately configured TLS -- connections. This is passed to C{startTLS} on the transport and is -- preferably created using L{twisted.internet.ssl.optionsForClientTLS}. -- If C{None}, the default is to verify the server certificate against -- the trust roots as provided by the platform. See -- L{twisted.internet._sslverify.platformTrust}. -- @type contextFactory: L{IOpenSSLClientConnectionCreator} - """ - - feature = (NS_XMPP_TLS, 'starttls') - wanted = True -- contextFactory = None - _deferred = None -+ _configurationForTLS = None - -- def __init__(self, xs, required=True, contextFactory=None): -+ def __init__(self, xs, required=True, configurationForTLS=None): -+ """ -+ @param configurationForTLS: An object which creates appropriately -+ configured TLS connections. This is passed to C{startTLS} on the -+ transport and is preferably created using -+ L{twisted.internet.ssl.optionsForClientTLS}. If C{None}, the -+ default is to verify the server certificate against the trust roots -+ as provided by the platform. See -+ L{twisted.internet._sslverify.platformTrust}. -+ @type configurationForTLS: L{IOpenSSLClientConnectionCreator} -+ """ - super(TLSInitiatingInitializer, self).__init__( - xs, required=required) -- self.contextFactory = contextFactory -+ self._configurationForTLS = configurationForTLS - - - def onProceed(self, obj): -@@ -430,8 +432,8 @@ def onProceed(self, obj): - """ - - self.xmlstream.removeObserver('/failure', self.onFailure) -- if self.contextFactory: -- ctx = self.contextFactory -+ if self._configurationForTLS: -+ ctx = self._configurationForTLS - else: - ctx = ssl.optionsForClientTLS(self.xmlstream.otherEntity.host) - self.xmlstream.transport.startTLS(ctx) -diff --git a/src/twisted/words/test/test_jabberclient.py b/src/twisted/words/test/test_jabberclient.py -index 1403131baf6..4f5c8092419 100644 ---- a/src/twisted/words/test/test_jabberclient.py -+++ b/src/twisted/words/test/test_jabberclient.py -@@ -466,28 +466,32 @@ def test_basic(self): - self.assertFalse(session.required) - - -- def test_tlsContextFactory(self): -+ def test_tlsConfiguration(self): - """ -- Test basic operations. -- -- Setup an XMPPClientFactory, which sets up an XMPPAuthenticator, and let -- it produce a protocol instance. Then inspect the instance variables of -- the authenticator and XML stream objects. -+ A TLS configuration is passed to the TLS initializer. - """ -+ configs = [] -+ -+ def init(self, xs, required=True, configurationForTLS=None): -+ configs.append(configurationForTLS) -+ - self.client_jid = jid.JID('user@example.com/resource') - - # Get an XmlStream instance. Note that it gets initialized with the - # XMPPAuthenticator (that has its associateWithXmlStream called) that - # is in turn initialized with the arguments to the factory. -- contextFactory = ssl.CertificateOptions() -- factory = client.XMPPClientFactory(self.client_jid, 'secret', -- contextFactory=contextFactory) -+ configurationForTLS = ssl.CertificateOptions() -+ factory = client.XMPPClientFactory( -+ self.client_jid, 'secret', -+ configurationForTLS=configurationForTLS) -+ self.patch(xmlstream.TLSInitiatingInitializer, "__init__", init) - xs = factory.buildProtocol(None) - - # test list of initializers - version, tls, sasl, bind, session = xs.initializers - - self.assertIsInstance(tls, xmlstream.TLSInitiatingInitializer) -- self.assertIs(contextFactory, tls.contextFactory) -+ self.assertIs(configurationForTLS, configs[0]) -+ - -- test_tlsContextFactory.skip = skipWhenNoSSL -+ test_tlsConfiguration.skip = skipWhenNoSSL -diff --git a/src/twisted/words/test/test_jabberxmlstream.py b/src/twisted/words/test/test_jabberxmlstream.py -index 7b384645a2c..85f6d195d4a 100644 ---- a/src/twisted/words/test/test_jabberxmlstream.py -+++ b/src/twisted/words/test/test_jabberxmlstream.py -@@ -747,6 +747,9 @@ def test_certificateVerifyContext(self): - A custom contextFactory is passed through to startTLS. - """ - ctx = CertificateOptions() -+ self.init = xmlstream.TLSInitiatingInitializer( -+ self.xmlstream, configurationForTLS=ctx) -+ - self.init.contextFactory = ctx - - def fakeStartTLS(contextFactory): - -From 05556b6ca14a49e4c7f3b5e8ede83137b869926e Mon Sep 17 00:00:00 2001 -From: Ralph Meijer -Date: Sun, 16 Jun 2019 19:02:52 +0200 -Subject: [PATCH 14/17] Move check for configurationTLS being None to __init__ - ---- - src/twisted/words/protocols/jabber/xmlstream.py | 12 ++++++------ - 1 file changed, 6 insertions(+), 6 deletions(-) - -diff --git a/src/twisted/words/protocols/jabber/xmlstream.py b/src/twisted/words/protocols/jabber/xmlstream.py -index 135d71295df..dd4bd8f1932 100644 ---- a/src/twisted/words/protocols/jabber/xmlstream.py -+++ b/src/twisted/words/protocols/jabber/xmlstream.py -@@ -423,7 +423,11 @@ def __init__(self, xs, required=True, configurationForTLS=None): - """ - super(TLSInitiatingInitializer, self).__init__( - xs, required=required) -- self._configurationForTLS = configurationForTLS -+ if configurationForTLS: -+ self._configurationForTLS = configurationForTLS -+ else: -+ self._configurationForTLS = ssl.optionsForClientTLS( -+ self.xmlstream.authenticator.otherHost) - - - def onProceed(self, obj): -@@ -432,11 +436,7 @@ def onProceed(self, obj): - """ - - self.xmlstream.removeObserver('/failure', self.onFailure) -- if self._configurationForTLS: -- ctx = self._configurationForTLS -- else: -- ctx = ssl.optionsForClientTLS(self.xmlstream.otherEntity.host) -- self.xmlstream.transport.startTLS(ctx) -+ self.xmlstream.transport.startTLS(self._configurationForTLS) - self.xmlstream.reset() - self.xmlstream.sendHeader() - self._deferred.callback(Reset) - -From 7caf8ac8795492e346e8f52633ff6d343a07edde Mon Sep 17 00:00:00 2001 -From: Ralph Meijer -Date: Sun, 16 Jun 2019 19:11:35 +0200 -Subject: [PATCH 15/17] Document configurationForTLS being None directly - ---- - src/twisted/words/protocols/jabber/client.py | 16 ++++++++++------ - src/twisted/words/protocols/jabber/xmlstream.py | 3 ++- - 2 files changed, 12 insertions(+), 7 deletions(-) - -diff --git a/src/twisted/words/protocols/jabber/client.py b/src/twisted/words/protocols/jabber/client.py -index db4cbfccf21..8f197cdafe1 100644 ---- a/src/twisted/words/protocols/jabber/client.py -+++ b/src/twisted/words/protocols/jabber/client.py -@@ -317,9 +317,10 @@ def XMPPClientFactory(jid, password, configurationForTLS=None): - @param configurationForTLS: An object which creates appropriately - configured TLS connections. This is passed to C{startTLS} on the - transport and is preferably created using -- L{twisted.internet.ssl.optionsForClientTLS}. See -- L{xmlstream.TLSInitiatingInitializer} for details. -- @type configurationForTLS: L{IOpenSSLClientConnectionCreator} -+ L{twisted.internet.ssl.optionsForClientTLS}. If C{None}, the default is -+ to verify the server certificate against the trust roots as provided by -+ the platform. See L{twisted.internet._sslverify.platformTrust}. -+ @type configurationForTLS: L{IOpenSSLClientConnectionCreator} or C{None} - - @return: XML stream factory. - @rtype: L{xmlstream.XmlStreamFactory} -@@ -372,9 +373,12 @@ def __init__(self, jid, password, configurationForTLS=None): - @param configurationForTLS: An object which creates appropriately - configured TLS connections. This is passed to C{startTLS} on the - transport and is preferably created using -- L{twisted.internet.ssl.optionsForClientTLS}. See -- L{xmlstream.TLSInitiatingInitializer} for details. -- @type configurationForTLS: L{IOpenSSLClientConnectionCreator} -+ L{twisted.internet.ssl.optionsForClientTLS}. If C{None}, the -+ default is to verify the server certificate against the trust roots -+ as provided by the platform. See -+ L{twisted.internet._sslverify.platformTrust}. -+ @type configurationForTLS: L{IOpenSSLClientConnectionCreator} or -+ C{None} - """ - xmlstream.ConnectAuthenticator.__init__(self, jid.host) - self.jid = jid -diff --git a/src/twisted/words/protocols/jabber/xmlstream.py b/src/twisted/words/protocols/jabber/xmlstream.py -index dd4bd8f1932..905402c5360 100644 ---- a/src/twisted/words/protocols/jabber/xmlstream.py -+++ b/src/twisted/words/protocols/jabber/xmlstream.py -@@ -419,7 +419,8 @@ def __init__(self, xs, required=True, configurationForTLS=None): - default is to verify the server certificate against the trust roots - as provided by the platform. See - L{twisted.internet._sslverify.platformTrust}. -- @type configurationForTLS: L{IOpenSSLClientConnectionCreator} -+ @type configurationForTLS: L{IOpenSSLClientConnectionCreator} or -+ C{None} - """ - super(TLSInitiatingInitializer, self).__init__( - xs, required=required) - -From a66878c15abe99fdb3c72d7ec533ee0ef54e7f95 Mon Sep 17 00:00:00 2001 -From: Ralph Meijer -Date: Sun, 16 Jun 2019 19:14:04 +0200 -Subject: [PATCH 16/17] Mention CVE-2019-12855 in news fragment - ---- - src/twisted/words/newsfragments/9561.bugfix | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/src/twisted/words/newsfragments/9561.bugfix b/src/twisted/words/newsfragments/9561.bugfix -index ac5f905a104..033a128491c 100644 ---- a/src/twisted/words/newsfragments/9561.bugfix -+++ b/src/twisted/words/newsfragments/9561.bugfix -@@ -1 +1 @@ --twisted.words.protocols.jabber.xmlstream.TLSInitiatingInitializer now properly verifies the server's certificate against platform CAs and the stream's domain. -+twisted.words.protocols.jabber.xmlstream.TLSInitiatingInitializer now properly verifies the server's certificate against platform CAs and the stream's domain, mitigating CVE-2019-12855. - -From abbf0fd52c13b1fb5e1429189a3fcc48565870a5 Mon Sep 17 00:00:00 2001 -From: Ralph Meijer -Date: Sun, 16 Jun 2019 19:50:33 +0200 -Subject: [PATCH 17/17] Revert "Move check for configurationTLS being None to - __init__" - -This reverts commit 05556b6ca14a49e4c7f3b5e8ede83137b869926e. ---- - src/twisted/words/protocols/jabber/xmlstream.py | 12 ++++++------ - 1 file changed, 6 insertions(+), 6 deletions(-) - -diff --git a/src/twisted/words/protocols/jabber/xmlstream.py b/src/twisted/words/protocols/jabber/xmlstream.py -index 905402c5360..20948c6d3be 100644 ---- a/src/twisted/words/protocols/jabber/xmlstream.py -+++ b/src/twisted/words/protocols/jabber/xmlstream.py -@@ -424,11 +424,7 @@ def __init__(self, xs, required=True, configurationForTLS=None): - """ - super(TLSInitiatingInitializer, self).__init__( - xs, required=required) -- if configurationForTLS: -- self._configurationForTLS = configurationForTLS -- else: -- self._configurationForTLS = ssl.optionsForClientTLS( -- self.xmlstream.authenticator.otherHost) -+ self._configurationForTLS = configurationForTLS - - - def onProceed(self, obj): -@@ -437,7 +433,11 @@ def onProceed(self, obj): - """ - - self.xmlstream.removeObserver('/failure', self.onFailure) -- self.xmlstream.transport.startTLS(self._configurationForTLS) -+ if self._configurationForTLS: -+ ctx = self._configurationForTLS -+ else: -+ ctx = ssl.optionsForClientTLS(self.xmlstream.otherEntity.host) -+ self.xmlstream.transport.startTLS(ctx) - self.xmlstream.reset() - self.xmlstream.sendHeader() - self._deferred.callback(Reset) diff --git a/CVE-2020-10109_10108.patch b/CVE-2020-10109_10108.patch deleted file mode 100644 index 9a58f55840524c101cfe48f588d43957b25b4453..0000000000000000000000000000000000000000 --- a/CVE-2020-10109_10108.patch +++ /dev/null @@ -1,260 +0,0 @@ -From 4a7d22e490bb8ff836892cc99a1f54b85ccb0281 Mon Sep 17 00:00:00 2001 -From: Mark Williams -Date: Sun, 16 Feb 2020 19:00:10 -0800 -Subject: [PATCH] Fix several request smuggling attacks. - -1. Requests with multiple Content-Length headers were allowed (thanks -to Jake Miller from Bishop Fox and ZeddYu Lu) and now fail with a 400; - -2. Requests with a Content-Length header and a Transfer-Encoding -header honored the first header (thanks to Jake Miller from Bishop -Fox) and now fail with a 400; - -3. Requests whose Transfer-Encoding header had a value other than -"chunked" and "identity" (thanks to ZeddYu Lu) were allowed and now fail -with a 400. ---- - src/twisted/web/http.py | 64 +++++++--- - src/twisted/web/newsfragments/9770.bugfix | 1 + - src/twisted/web/test/test_http.py | 137 ++++++++++++++++++++++ - 3 files changed, 187 insertions(+), 15 deletions(-) - create mode 100644 src/twisted/web/newsfragments/9770.bugfix - -diff --git a/src/twisted/web/http.py b/src/twisted/web/http.py -index f0fb05b4d69..06d830fe30f 100644 ---- a/src/twisted/web/http.py -+++ b/src/twisted/web/http.py -@@ -2171,6 +2171,51 @@ def _finishRequestBody(self, data): - self.allContentReceived() - self._dataBuffer.append(data) - -+ def _maybeChooseTransferDecoder(self, header, data): -+ """ -+ If the provided header is C{content-length} or -+ C{transfer-encoding}, choose the appropriate decoder if any. -+ -+ Returns L{True} if the request can proceed and L{False} if not. -+ """ -+ -+ def fail(): -+ self._respondToBadRequestAndDisconnect() -+ self.length = None -+ -+ # Can this header determine the length? -+ if header == b'content-length': -+ try: -+ length = int(data) -+ except ValueError: -+ fail() -+ return False -+ newTransferDecoder = _IdentityTransferDecoder( -+ length, self.requests[-1].handleContentChunk, self._finishRequestBody) -+ elif header == b'transfer-encoding': -+ # XXX Rather poorly tested code block, apparently only exercised by -+ # test_chunkedEncoding -+ if data.lower() == b'chunked': -+ length = None -+ newTransferDecoder = _ChunkedTransferDecoder( -+ self.requests[-1].handleContentChunk, self._finishRequestBody) -+ elif data.lower() == b'identity': -+ return True -+ else: -+ fail() -+ return False -+ else: -+ # It's not a length related header, so exit -+ return True -+ -+ if self._transferDecoder is not None: -+ fail() -+ return False -+ else: -+ self.length = length -+ self._transferDecoder = newTransferDecoder -+ return True -+ - - def headerReceived(self, line): - """ -@@ -2196,21 +2241,10 @@ def headerReceived(self, line): - - header = header.lower() - data = data.strip() -- if header == b'content-length': -- try: -- self.length = int(data) -- except ValueError: -- self._respondToBadRequestAndDisconnect() -- self.length = None -- return False -- self._transferDecoder = _IdentityTransferDecoder( -- self.length, self.requests[-1].handleContentChunk, self._finishRequestBody) -- elif header == b'transfer-encoding' and data.lower() == b'chunked': -- # XXX Rather poorly tested code block, apparently only exercised by -- # test_chunkedEncoding -- self.length = None -- self._transferDecoder = _ChunkedTransferDecoder( -- self.requests[-1].handleContentChunk, self._finishRequestBody) -+ -+ if not self._maybeChooseTransferDecoder(header, data): -+ return False -+ - reqHeaders = self.requests[-1].requestHeaders - values = reqHeaders.getRawHeaders(header) - if values is not None: -diff --git a/src/twisted/web/newsfragments/9770.bugfix b/src/twisted/web/newsfragments/9770.bugfix -new file mode 100644 -index 00000000000..4f1be97de8a ---- /dev/null -+++ b/src/twisted/web/newsfragments/9770.bugfix -@@ -0,0 +1 @@ -+Fix several request smuggling attacks: requests with multiple Content-Length headers were allowed (thanks to Jake Miller from Bishop Fox and ZeddYu Lu) and now fail with a 400; requests with a Content-Length header and a Transfer-Encoding header honored the first header (thanks to Jake Miller from Bishop Fox) and now fail with a 400; requests whose Transfer-Encoding header had a value other than "chunked" and "identity" (thanks to ZeddYu Lu) were allowed and now fail a 400. -\ No newline at end of file -diff --git a/src/twisted/web/test/test_http.py b/src/twisted/web/test/test_http.py -index 0a0db09b750..578cb500cda 100644 ---- a/src/twisted/web/test/test_http.py -+++ b/src/twisted/web/test/test_http.py -@@ -2252,6 +2252,143 @@ def process(self): - self.flushLoggedErrors(AttributeError) - - -+ def assertDisconnectingBadRequest(self, request): -+ """ -+ Assert that the given request bytes fail with a 400 bad -+ request without calling L{Request.process}. -+ -+ @param request: A raw HTTP request -+ @type request: L{bytes} -+ """ -+ class FailedRequest(http.Request): -+ processed = False -+ def process(self): -+ FailedRequest.processed = True -+ -+ channel = self.runRequest(request, FailedRequest, success=False) -+ self.assertFalse(FailedRequest.processed, "Request.process called") -+ self.assertEqual( -+ channel.transport.value(), -+ b"HTTP/1.1 400 Bad Request\r\n\r\n") -+ self.assertTrue(channel.transport.disconnecting) -+ -+ -+ def test_duplicateContentLengths(self): -+ """ -+ A request which includes multiple C{content-length} headers -+ fails with a 400 response without calling L{Request.process}. -+ """ -+ self.assertRequestRejected([ -+ b'GET /a HTTP/1.1', -+ b'Content-Length: 56', -+ b'Content-Length: 0', -+ b'Host: host.invalid', -+ b'', -+ b'', -+ ]) -+ -+ -+ def test_duplicateContentLengthsWithPipelinedRequests(self): -+ """ -+ Two pipelined requests, the first of which includes multiple -+ C{content-length} headers, trigger a 400 response without -+ calling L{Request.process}. -+ """ -+ self.assertRequestRejected([ -+ b'GET /a HTTP/1.1', -+ b'Content-Length: 56', -+ b'Content-Length: 0', -+ b'Host: host.invalid', -+ b'', -+ b'', -+ b'GET /a HTTP/1.1', -+ b'Host: host.invalid', -+ b'', -+ b'', -+ ]) -+ -+ -+ def test_contentLengthAndTransferEncoding(self): -+ """ -+ A request that includes both C{content-length} and -+ C{transfer-encoding} headers fails with a 400 response without -+ calling L{Request.process}. -+ """ -+ self.assertRequestRejected([ -+ b'GET /a HTTP/1.1', -+ b'Transfer-Encoding: chunked', -+ b'Content-Length: 0', -+ b'Host: host.invalid', -+ b'', -+ b'', -+ ]) -+ -+ -+ def test_contentLengthAndTransferEncodingWithPipelinedRequests(self): -+ """ -+ Two pipelined requests, the first of which includes both -+ C{content-length} and C{transfer-encoding} headers, triggers a -+ 400 response without calling L{Request.process}. -+ """ -+ self.assertRequestRejected([ -+ b'GET /a HTTP/1.1', -+ b'Transfer-Encoding: chunked', -+ b'Content-Length: 0', -+ b'Host: host.invalid', -+ b'', -+ b'', -+ b'GET /a HTTP/1.1', -+ b'Host: host.invalid', -+ b'', -+ b'', -+ ]) -+ -+ -+ def test_unknownTransferEncoding(self): -+ """ -+ A request whose C{transfer-encoding} header includes a value -+ other than C{chunked} or C{identity} fails with a 400 response -+ without calling L{Request.process}. -+ """ -+ self.assertRequestRejected([ -+ b'GET /a HTTP/1.1', -+ b'Transfer-Encoding: unknown', -+ b'Host: host.invalid', -+ b'', -+ b'', -+ ]) -+ -+ -+ def test_transferEncodingIdentity(self): -+ """ -+ A request with a valid C{content-length} and a -+ C{transfer-encoding} whose value is C{identity} succeeds. -+ """ -+ body = [] -+ -+ class SuccessfulRequest(http.Request): -+ processed = False -+ def process(self): -+ body.append(self.content.read()) -+ self.setHeader(b'content-length', b'0') -+ self.finish() -+ -+ request = b'''\ -+GET / HTTP/1.1 -+Host: host.invalid -+Content-Length: 2 -+Transfer-Encoding: identity -+ -+ok -+''' -+ channel = self.runRequest(request, SuccessfulRequest, False) -+ self.assertEqual(body, [b'ok']) -+ self.assertEqual( -+ channel.transport.value(), -+ b'HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n', -+ ) -+ -+ - - class QueryArgumentsTests(unittest.TestCase): - def testParseqs(self): diff --git a/Twisted-18.9.0.tar.bz2 b/Twisted-18.9.0.tar.bz2 deleted file mode 100644 index e76adf1c9931a81ea7c4684c456c4c5e6f542c35..0000000000000000000000000000000000000000 Binary files a/Twisted-18.9.0.tar.bz2 and /dev/null differ diff --git a/python-twisted.spec b/python-twisted.spec index 60e1ab4d85c1ad950cadcffc25c404baddd75adb..4daf4f44b2b73c3a3d9727c5d34393fa303f2007 100644 --- a/python-twisted.spec +++ b/python-twisted.spec @@ -1,15 +1,12 @@ +%define debug_package %{nil} Name: python-twisted -Version: 18.9.0 -Release: 7 +Version: 22.4.0 +Release: 1 Summary: An event-driven networking engine written in Python License: MIT URL: http://twistedmatrix.com/ -Source0: https://files.pythonhosted.org/packages/source/T/Twisted/Twisted-%{version}.tar.bz2 +Source0: https://github.com/twisted/twisted/archive/twisted-%{version}/twisted-%{version}.tar.gz -# https://github.com/twisted/twisted/commit/6c61fc4503ae39ab8ecee52d10f10ee2c371d7e2 -Patch0000: CVE-2019-12387.patch -Patch0001: CVE-2020-10109_10108.patch -Patch0002: CVE-2019-12855.patch %description Twisted is an event-based framework for internet applications, @@ -75,15 +72,11 @@ BuildArch: noarch The python-twisted-help package contains related documents. %prep -%autosetup -n Twisted-%{version} -p1 +%autosetup -n twisted-twisted-%{version} -p1 %build %py3_build -PYTHONPATH=${PWD}/src/ sphinx-build-3 -a docs html -rm -rf html/.doctrees -rm -rf html/.buildinfo - %install %py3_install mv %{buildroot}%{_bindir}/trial %{buildroot}%{_bindir}/trial-%{python3_version} @@ -92,29 +85,32 @@ ln -s ./trial-%{python3_version} %{buildroot}%{_bindir}/trial-3 ln -s ./twistd-%{python3_version} %{buildroot}%{_bindir}/twistd-3 ln -s ./trial-%{python3_version} %{buildroot}%{_bindir}/trial ln -s ./twistd-%{python3_version} %{buildroot}%{_bindir}/twistd -chmod +x %{buildroot}%{python3_sitearch}/twisted/mail/test/pop3testserver.py -chmod +x %{buildroot}%{python3_sitearch}/twisted/trial/test/scripttest.py +chmod +x %{buildroot}%{python3_sitelib}/twisted/mail/test/pop3testserver.py +chmod +x %{buildroot}%{python3_sitelib}/twisted/trial/test/scripttest.py -pathfix.py -pn -i %{__python3} %{buildroot}%{python3_sitearch} +pathfix.py -pn -i %{__python3} %{buildroot}%{python3_sitelib} install -d %{buildroot}%{_mandir}/man1/ cp -a docs/conch/man/*.1 %{buildroot}%{_mandir}/man1/ cp -a docs/core/man/*.1 %{buildroot}%{_mandir}/man1/ cp -a docs/mail/man/*.1 %{buildroot}%{_mandir}/man1/ %check -PATH=%{buildroot}%{_bindir}:$PATH PYTHONPATH=%{buildroot}%{python3_sitearch} %{buildroot}%{_bindir}/trial-3 twisted ||: +PATH=%{buildroot}%{_bindir}:$PATH PYTHONPATH=%{buildroot}%{python3_sitelib} %{buildroot}%{_bindir}/trial-3 twisted ||: %files -n python3-twisted -%doc CONTRIBUTING NEWS.rst README.rst html LICENSE +%doc NEWS.rst README.rst LICENSE %{_bindir}/{trial-3*,twistd-3*} -%{python3_sitearch}/twisted -%{python3_sitearch}/Twisted-%{version}-py%{python3_version}.egg-info +%{python3_sitelib}/twisted +%{python3_sitelib}/Twisted-%{version}-py%{python3_version}.egg-info %{_bindir}/{cftp,ckeygen,conch,mailmail,pyhtmlizer,tkconch,trial,twist,twistd} %files help %{_mandir}/man1/{cftp.1*,ckeygen.1*,conch.1*,mailmail.1*,pyhtmlizer.1*,tkconch.1*,trial.1*,twistd.1*} %changelog +* Wed Aug 03 2022 duyiwei - 22.4.0-1 +- upgrade version to 22.4.0 + * Mon May 09 2022 xu_ping - 18.9.0-7 - fix build error caused by python3.10 wildcard diff --git a/twisted-22.4.0.tar.gz b/twisted-22.4.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..7cd7a719dfbd173a5e9c97589926e7a8bfb74825 Binary files /dev/null and b/twisted-22.4.0.tar.gz differ