commit
9b094fb73c
907
CVE-2019-12387.patch
Normal file
907
CVE-2019-12387.patch
Normal file
@ -0,0 +1,907 @@
|
||||
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)
|
||||
BIN
Twisted-18.9.0.tar.bz2
Normal file
BIN
Twisted-18.9.0.tar.bz2
Normal file
Binary file not shown.
174
python-twisted.spec
Normal file
174
python-twisted.spec
Normal file
@ -0,0 +1,174 @@
|
||||
Name: python-twisted
|
||||
Version: 18.9.0
|
||||
Release: 3
|
||||
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
|
||||
|
||||
# https://github.com/twisted/twisted/commit/6c61fc4503ae39ab8ecee52d10f10ee2c371d7e2
|
||||
Patch0000: CVE-2019-12387.patch
|
||||
|
||||
%description
|
||||
Twisted is an event-based framework for internet applications,
|
||||
supporting Python 2.7 and Python 3.5+. It includes modules for many
|
||||
different purposes, including the following:
|
||||
|
||||
* twisted.web: HTTP clients and servers, HTML templating, and a WSGI server
|
||||
* twisted.conch: SSHv2 and Telnet clients and servers and terminal emulators
|
||||
* twisted.words: Clients and servers for IRC, XMPP, and other IM protocols
|
||||
* twisted.mail: IMAPv4, POP3, SMTP clients and servers
|
||||
* twisted.positioning: Tools for communicating with NMEA-compatible GPS receivers
|
||||
* twisted.names: DNS client and tools for making your own DNS servers
|
||||
* twisted.trial: A unit testing framework that integrates well with Twisted-based code.
|
||||
|
||||
Twisted supports all major system event loops -- select (all platforms),
|
||||
poll (most POSIX platforms), epoll (Linux), kqueue (FreeBSD, macOS),
|
||||
IOCP (Windows), and various GUI event loops (GTK+2/3, Qt, wxWidgets).
|
||||
Third-party reactors can plug into Twisted, and provide support for
|
||||
additional event loops.
|
||||
|
||||
%package -n python2-twisted
|
||||
Summary: An event-driven networking engine written in Python
|
||||
|
||||
%{?python_provide:%python_provide python2-twisted}
|
||||
|
||||
BuildRequires: python2-devel >= 2.6 python2dist(appdirs) >= 1.4.0
|
||||
BuildRequires: python2dist(automat) >= 0.3.0 python2dist(constantly) >= 15.1
|
||||
BuildRequires: python2dist(cryptography) >= 1.5 python2dist(incremental) >= 16.10.1
|
||||
BuildRequires: (python2dist(h2) >= 3.0 with python2dist(h2) < 4.0)
|
||||
BuildRequires: python2dist(hyperlink) >= 17.1.1 python2dist(idna) >= 0.6
|
||||
BuildRequires: (python2dist(priority) >= 1.1.0 with python2dist(priority) < 2.0)
|
||||
BuildRequires: python2dist(pyasn1) python2dist(pyopenssl) >= 16.0.0
|
||||
BuildRequires: python2dist(pyserial) >= 3.0 python2dist(service-identity)
|
||||
BuildRequires: python2dist(setuptools) python2dist(zope.interface) >= 4.4.2
|
||||
BuildRequires: python2dist(pyhamcrest)
|
||||
|
||||
Recommends: python2dist(service-identity)
|
||||
|
||||
%description -n python2-twisted
|
||||
Twisted is an event-based framework for internet applications,
|
||||
supporting Python 2.7 and Python 3.5+. It includes modules for many
|
||||
different purposes, including the following:
|
||||
|
||||
* twisted.web: HTTP clients and servers, HTML templating, and a WSGI server
|
||||
* twisted.conch: SSHv2 and Telnet clients and servers and terminal emulators
|
||||
* twisted.words: Clients and servers for IRC, XMPP, and other IM protocols
|
||||
* twisted.mail: IMAPv4, POP3, SMTP clients and servers
|
||||
* twisted.positioning: Tools for communicating with NMEA-compatible GPS receivers
|
||||
* twisted.names: DNS client and tools for making your own DNS servers
|
||||
* twisted.trial: A unit testing framework that integrates well with Twisted-based code.
|
||||
|
||||
Twisted supports all major system event loops -- select (all platforms),
|
||||
poll (most POSIX platforms), epoll (Linux), kqueue (FreeBSD, macOS),
|
||||
IOCP (Windows), and various GUI event loops (GTK+2/3, Qt, wxWidgets).
|
||||
Third-party reactors can plug into Twisted, and provide support for
|
||||
additional event loops.
|
||||
|
||||
|
||||
%package -n python3-twisted
|
||||
Summary: An event-driven networking engine written in Python
|
||||
|
||||
%{?python_provide:%python_provide python3-twisted}
|
||||
|
||||
BuildRequires: python3-devel >= 3.3 python3dist(appdirs) >= 1.4.0
|
||||
BuildRequires: python3dist(automat) >= 0.3.0 python3dist(constantly) >= 15.1
|
||||
BuildRequires: python3dist(cryptography) >= 1.5 python3dist(hyperlink) >= 17.1.1
|
||||
BuildRequires: (python3dist(h2) >= 3.0 with python3dist(h2) < 4.0)
|
||||
BuildRequires: python3dist(idna) >= 0.6 python3dist(incremental) >= 16.10.1
|
||||
BuildRequires: (python3dist(priority) >= 1.1.0 with python3dist(priority) < 2.0)
|
||||
BuildRequires: python3dist(pyasn1) python3dist(pyopenssl) >= 16.0.0
|
||||
BuildRequires: python3dist(pyserial) >= 3.0 python3dist(service-identity)
|
||||
BuildRequires: python3dist(setuptools) python3dist(sphinx) >= 1.3.1
|
||||
BuildRequires: python3dist(zope.interface) >= 4.4.2 python3dist(pyhamcrest)
|
||||
|
||||
Recommends: python3dist(service-identity)
|
||||
|
||||
%description -n python3-twisted
|
||||
Twisted is an event-based framework for internet applications,
|
||||
supporting Python 2.7 and Python 3.5+. It includes modules for many
|
||||
different purposes, including the following:
|
||||
|
||||
* twisted.web: HTTP clients and servers, HTML templating, and a WSGI server
|
||||
* twisted.conch: SSHv2 and Telnet clients and servers and terminal emulators
|
||||
* twisted.words: Clients and servers for IRC, XMPP, and other IM protocols
|
||||
* twisted.mail: IMAPv4, POP3, SMTP clients and servers
|
||||
* twisted.positioning: Tools for communicating with NMEA-compatible GPS receivers
|
||||
* twisted.names: DNS client and tools for making your own DNS servers
|
||||
* twisted.trial: A unit testing framework that integrates well with Twisted-based code.
|
||||
|
||||
Twisted supports all major system event loops -- select (all platforms),
|
||||
poll (most POSIX platforms), epoll (Linux), kqueue (FreeBSD, macOS),
|
||||
IOCP (Windows), and various GUI event loops (GTK+2/3, Qt, wxWidgets).
|
||||
Third-party reactors can plug into Twisted, and provide support for
|
||||
additional event loops.
|
||||
|
||||
%package help
|
||||
Summary: Documents for python-twisted
|
||||
BuildArch: noarch
|
||||
|
||||
%description help
|
||||
The python-twisted-help package contains related documents.
|
||||
|
||||
%prep
|
||||
%autosetup -n Twisted-%{version} -p1
|
||||
|
||||
%build
|
||||
%py2_build
|
||||
%py3_build
|
||||
|
||||
PYTHONPATH=${PWD}/src/ sphinx-build-3 -a docs html
|
||||
rm -rf html/.doctrees
|
||||
rm -rf html/.buildinfo
|
||||
|
||||
%install
|
||||
%py2_install
|
||||
mv %{buildroot}%{_bindir}/trial %{buildroot}%{_bindir}/trial-%{python2_version}
|
||||
mv %{buildroot}%{_bindir}/twistd %{buildroot}%{_bindir}/twistd-%{python2_version}
|
||||
ln -s ./trial-%{python2_version} %{buildroot}%{_bindir}/trial-2
|
||||
ln -s ./twistd-%{python2_version} %{buildroot}%{_bindir}/twistd-2
|
||||
chmod 755 %{buildroot}%{python2_sitearch}/twisted/python/_sendmsg.so
|
||||
chmod 755 %{buildroot}%{python2_sitearch}/twisted/test/raiser.so
|
||||
chmod +x %{buildroot}%{python2_sitearch}/twisted/mail/test/pop3testserver.py
|
||||
chmod +x %{buildroot}%{python2_sitearch}/twisted/trial/test/scripttest.py
|
||||
|
||||
%py3_install
|
||||
mv %{buildroot}%{_bindir}/trial %{buildroot}%{_bindir}/trial-%{python3_version}
|
||||
mv %{buildroot}%{_bindir}/twistd %{buildroot}%{_bindir}/twistd-%{python3_version}
|
||||
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
|
||||
|
||||
pathfix.py -pn -i %{__python2} %{buildroot}%{python2_sitearch}
|
||||
pathfix.py -pn -i %{__python3} %{buildroot}%{python3_sitearch}
|
||||
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}%{python2_sitearch} %{buildroot}%{_bindir}/trial-2 twisted ||:
|
||||
PATH=%{buildroot}%{_bindir}:$PATH PYTHONPATH=%{buildroot}%{python3_sitearch} %{buildroot}%{_bindir}/trial-3 twisted ||:
|
||||
|
||||
%files -n python2-twisted
|
||||
%doc CONTRIBUTING NEWS.rst README.rst LICENSE
|
||||
%{_bindir}/{trial-2*,twistd-2*}
|
||||
%{python2_sitearch}/twisted
|
||||
%{python2_sitearch}/Twisted-%{version}-py?.?.egg-info
|
||||
|
||||
%files -n python3-twisted
|
||||
%doc CONTRIBUTING NEWS.rst README.rst html LICENSE
|
||||
%{_bindir}/{trial-3*,twistd-3*}
|
||||
%{python3_sitearch}/twisted
|
||||
%{python3_sitearch}/Twisted-%{version}-py?.?.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
|
||||
* Tue Feb 25 2020 Jiangping Hu <hujp1985@foxmail.com> - 18.9.0-3
|
||||
- Package init
|
||||
Loading…
x
Reference in New Issue
Block a user