diff -Nru twisted-22.4.0/debian/changelog twisted-22.4.0/debian/changelog --- twisted-22.4.0/debian/changelog 2022-09-02 12:10:50.000000000 +0000 +++ twisted-22.4.0/debian/changelog 2023-01-30 15:12:17.000000000 +0000 @@ -1,3 +1,11 @@ +twisted (22.4.0-4) unstable; urgency=medium + + * Team upload. + * Add upstream patch for Python 3.11 (Closes: #1029579) + * Add upstream patch for CVE-2022-39348 (Closes: #1023359) + + -- Jochen Sprickerhof Mon, 30 Jan 2023 16:12:17 +0100 + twisted (22.4.0-3) unstable; urgency=medium * Team upload. diff -Nru twisted-22.4.0/debian/patches/Add-CVE-to-newsfragment.patch twisted-22.4.0/debian/patches/Add-CVE-to-newsfragment.patch --- twisted-22.4.0/debian/patches/Add-CVE-to-newsfragment.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-22.4.0/debian/patches/Add-CVE-to-newsfragment.patch 2023-01-30 15:10:52.000000000 +0000 @@ -0,0 +1,15 @@ +From: Tom Most +Date: Mon, 24 Oct 2022 22:30:42 -0700 +Subject: Add CVE to newsfragment + +--- + src/twisted/web/newsfragments/11716.bugfix | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/twisted/web/newsfragments/11716.bugfix b/src/twisted/web/newsfragments/11716.bugfix +index 66189e6..5264c8f 100644 +--- a/src/twisted/web/newsfragments/11716.bugfix ++++ b/src/twisted/web/newsfragments/11716.bugfix +@@ -1 +1 @@ +-twisted.web.vhost.NameVirtualHost no longer echoes HTML received in the Host header without escaping it (GHSA-vg46-2rrj-3647). ++twisted.web.vhost.NameVirtualHost no longer echoes HTML received in the Host header without escaping it (CVE-2022-39348, GHSA-vg46-2rrj-3647). diff -Nru twisted-22.4.0/debian/patches/Address-DummyRequest-MyPy-issue.patch twisted-22.4.0/debian/patches/Address-DummyRequest-MyPy-issue.patch --- twisted-22.4.0/debian/patches/Address-DummyRequest-MyPy-issue.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-22.4.0/debian/patches/Address-DummyRequest-MyPy-issue.patch 2023-01-30 15:10:52.000000000 +0000 @@ -0,0 +1,37 @@ +From: Tom Most +Date: Sat, 22 Oct 2022 18:45:06 -0700 +Subject: Address DummyRequest MyPy issue + +Filed https://github.com/twisted/twisted/issues/11719 for this. +--- + src/twisted/web/test/test_pages.py | 8 +++++++- + 1 file changed, 7 insertions(+), 1 deletion(-) + +diff --git a/src/twisted/web/test/test_pages.py b/src/twisted/web/test/test_pages.py +index d83e0eb..1f66c16 100644 +--- a/src/twisted/web/test/test_pages.py ++++ b/src/twisted/web/test/test_pages.py +@@ -5,8 +5,11 @@ + Test L{twisted.web.pages} + """ + ++from typing import cast ++ + from twisted.trial.unittest import SynchronousTestCase + from twisted.web.http_headers import Headers ++from twisted.web.iweb import IRequest + from twisted.web.pages import ErrorPage, forbidden, notFound + from twisted.web.test.requesthelper import DummyRequest + +@@ -20,7 +23,10 @@ def _render(resource: ErrorPage) -> DummyRequest: + @returns: The request that the resource handled, + """ + request = DummyRequest([b""]) +- resource.render(request) ++ # The cast is necessary because DummyRequest isn't annotated ++ # as an IRequest, and this can't be trivially done. See ++ # https://github.com/twisted/twisted/issues/11719 ++ resource.render(cast(IRequest, request)) + return request + + diff -Nru twisted-22.4.0/debian/patches/Address-IRenderable-MyPy-issue.patch twisted-22.4.0/debian/patches/Address-IRenderable-MyPy-issue.patch --- twisted-22.4.0/debian/patches/Address-IRenderable-MyPy-issue.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-22.4.0/debian/patches/Address-IRenderable-MyPy-issue.patch 2023-01-30 15:10:52.000000000 +0000 @@ -0,0 +1,54 @@ +From: Tom Most +Date: Sat, 22 Oct 2022 19:25:51 -0700 +Subject: Address IRenderable MyPy issue + +--- + src/twisted/web/pages.py | 19 +++++++++++++------ + 1 file changed, 13 insertions(+), 6 deletions(-) + +diff --git a/src/twisted/web/pages.py b/src/twisted/web/pages.py +index 8f37b3e..524148f 100644 +--- a/src/twisted/web/pages.py ++++ b/src/twisted/web/pages.py +@@ -12,9 +12,10 @@ __all__ = ( + "forbidden", + ) + ++from typing import cast + + from twisted.web import http +-from twisted.web.iweb import IRequest ++from twisted.web.iweb import IRenderable, IRequest + from twisted.web.resource import Resource + from twisted.web.template import renderElement, tags + +@@ -50,18 +51,24 @@ class ErrorPage(Resource): + self._brief: str = brief + self._detail: str = detail + +- def render(self, request: IRequest) -> None: ++ def render(self, request: IRequest) -> object: + """ + Respond to all requests with the given HTTP status code and an HTML + document containing the explanatory strings. + """ + request.setResponseCode(self._code) + request.setHeader(b"content-type", b"text/html; charset=utf-8") +- renderElement( ++ return renderElement( + request, +- tags.html( +- tags.head(tags.title(f"{self._code} - {self._brief}")), +- tags.body(tags.h1(self._brief), tags.p(self._detail)), ++ # cast because the type annotations here seem off; Tag isn't an ++ # IRenderable but also probably should be? See ++ # https://github.com/twisted/twisted/issues/4982 ++ cast( ++ IRenderable, ++ tags.html( ++ tags.head(tags.title(f"{self._code} - {self._brief}")), ++ tags.body(tags.h1(self._brief), tags.p(self._detail)), ++ ), + ), + ) + diff -Nru twisted-22.4.0/debian/patches/Call-the-superclass-constructor-via-private-alias.patch twisted-22.4.0/debian/patches/Call-the-superclass-constructor-via-private-alias.patch --- twisted-22.4.0/debian/patches/Call-the-superclass-constructor-via-private-alias.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-22.4.0/debian/patches/Call-the-superclass-constructor-via-private-alias.patch 2023-01-30 15:10:52.000000000 +0000 @@ -0,0 +1,30 @@ +From: Tom Most +Date: Mon, 24 Oct 2022 21:25:21 -0700 +Subject: Call the superclass constructor via private alias + +--- + src/twisted/web/resource.py | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/src/twisted/web/resource.py b/src/twisted/web/resource.py +index f196c3a..de3b557 100644 +--- a/src/twisted/web/resource.py ++++ b/src/twisted/web/resource.py +@@ -363,7 +363,7 @@ class _UnsafeNoResource(_UnsafeErrorPage): + """ + + def __init__(self, message="Sorry. No luck finding that resource."): +- ErrorPage.__init__(self, NOT_FOUND, "No Such Resource", message) ++ _UnsafeErrorPage.__init__(self, NOT_FOUND, "No Such Resource", message) + + + class _UnsafeForbiddenResource(_UnsafeErrorPage): +@@ -377,7 +377,7 @@ class _UnsafeForbiddenResource(_UnsafeErrorPage): + """ + + def __init__(self, message="Sorry, resource is forbidden."): +- ErrorPage.__init__(self, FORBIDDEN, "Forbidden Resource", message) ++ _UnsafeErrorPage.__init__(self, FORBIDDEN, "Forbidden Resource", message) + + + # Deliberately undocumented public aliases. See GHSA-vg46-2rrj-3647. diff -Nru twisted-22.4.0/debian/patches/Deprecate-twisted.web.resource.ErrorPage-and-spawn.patch twisted-22.4.0/debian/patches/Deprecate-twisted.web.resource.ErrorPage-and-spawn.patch --- twisted-22.4.0/debian/patches/Deprecate-twisted.web.resource.ErrorPage-and-spawn.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-22.4.0/debian/patches/Deprecate-twisted.web.resource.ErrorPage-and-spawn.patch 2023-01-30 15:10:52.000000000 +0000 @@ -0,0 +1,231 @@ +From: Tom Most +Date: Thu, 20 Oct 2022 23:19:53 -0700 +Subject: Deprecate twisted.web.resource.ErrorPage and spawn + +--- + src/twisted/web/newsfragments/11716.feature | 1 + + src/twisted/web/newsfragments/11716.removal | 1 + + src/twisted/web/resource.py | 69 ++++++++++++++++++++++++----- + src/twisted/web/test/test_resource.py | 51 +++++++++++++++++++-- + 4 files changed, 106 insertions(+), 16 deletions(-) + create mode 100644 src/twisted/web/newsfragments/11716.feature + create mode 100644 src/twisted/web/newsfragments/11716.removal + +diff --git a/src/twisted/web/newsfragments/11716.feature b/src/twisted/web/newsfragments/11716.feature +new file mode 100644 +index 0000000..5693458 +--- /dev/null ++++ b/src/twisted/web/newsfragments/11716.feature +@@ -0,0 +1 @@ ++The twisted.web.pages.ErrorPage, NotFoundPage, and ForbiddenPage IResource implementations provide HTML error pages rendered safely using twisted.web.template. +diff --git a/src/twisted/web/newsfragments/11716.removal b/src/twisted/web/newsfragments/11716.removal +new file mode 100644 +index 0000000..f4d2b36 +--- /dev/null ++++ b/src/twisted/web/newsfragments/11716.removal +@@ -0,0 +1 @@ ++The twisted.web.resource.ErrorPage, NoResource, and ForbiddenResource classes have been deprecated in favor of new implementations twisted.web.pages module because they permit HTML injection. +diff --git a/src/twisted/web/resource.py b/src/twisted/web/resource.py +index 5e6bd83..93c7807 100644 +--- a/src/twisted/web/resource.py ++++ b/src/twisted/web/resource.py +@@ -1,9 +1,11 @@ +-# -*- test-case-name: twisted.web.test.test_web -*- ++# -*- test-case-name: twisted.web.test.test_web, twisted.web.test.test_resource -*- + # Copyright (c) Twisted Matrix Laboratories. + # See LICENSE for details. + + """ + Implementation of the lowest-level Resource class. ++ ++See L{twisted.web.pages} for some utility implementations. + """ + + +@@ -21,8 +23,11 @@ import warnings + + from zope.interface import Attribute, Interface, implementer + ++from incremental import Version ++ + from twisted.python.compat import nativeString + from twisted.python.components import proxyForInterface ++from twisted.python.deprecate import deprecatedModuleAttribute + from twisted.python.reflect import prefixedMethodNames + from twisted.web._responses import FORBIDDEN, NOT_FOUND + from twisted.web.error import UnsupportedMethod +@@ -286,20 +291,25 @@ def _computeAllowedMethods(resource): + return allowedMethods + + +-class ErrorPage(Resource): ++class _UnsafeErrorPage(Resource): + """ +- L{ErrorPage} is a resource which responds with a particular ++ L{_UnsafeErrorPage}, publicly available via the deprecated alias ++ C{ErrorPage}, is a resource which responds with a particular + (parameterized) status and a body consisting of HTML containing some + descriptive text. This is useful for rendering simple error pages. + ++ Deprecated in Twisted NEXT because it permits HTML injection; use ++ L{twisted.pages.ErrorPage} instead. ++ + @ivar template: A native string which will have a dictionary interpolated + into it to generate the response body. The dictionary has the following + keys: + +- - C{"code"}: The status code passed to L{ErrorPage.__init__}. +- - C{"brief"}: The brief description passed to L{ErrorPage.__init__}. ++ - C{"code"}: The status code passed to L{_UnsafeErrorPage.__init__}. ++ - C{"brief"}: The brief description passed to ++ L{_UnsafeErrorPage.__init__}. + - C{"detail"}: The detailed description passed to +- L{ErrorPage.__init__}. ++ L{_UnsafeErrorPage.__init__}. + + @ivar code: An integer status code which will be used for the response. + @type code: C{int} +@@ -342,26 +352,61 @@ class ErrorPage(Resource): + return self + + +-class NoResource(ErrorPage): ++class _UnsafeNoResource(_UnsafeErrorPage): + """ +- L{NoResource} is a specialization of L{ErrorPage} which returns the HTTP +- response code I{NOT FOUND}. ++ L{_UnsafeNoResource}, publicly available via the deprecated alias ++ C{NoResource}, is a specialization of L{_UnsafeErrorPage} which ++ returns the HTTP response code I{NOT FOUND}. ++ ++ Deprecated in Twisted NEXT because it permits HTML injection; use ++ L{twisted.pages.NotFoundPage} instead. + """ + + def __init__(self, message="Sorry. No luck finding that resource."): + ErrorPage.__init__(self, NOT_FOUND, "No Such Resource", message) + + +-class ForbiddenResource(ErrorPage): ++class _UnsafeForbiddenResource(_UnsafeErrorPage): + """ +- L{ForbiddenResource} is a specialization of L{ErrorPage} which returns the +- I{FORBIDDEN} HTTP response code. ++ L{_UnsafeForbiddenResource}, publicly available via the deprecated alias ++ C{ForbiddenResource} is a specialization of L{_UnsafeErrorPage} which ++ returns the I{FORBIDDEN} HTTP response code. ++ ++ Deprecated in Twisted NEXT because it permits HTML injection; use ++ L{twisted.pages.ForbiddenPage} instead. + """ + + def __init__(self, message="Sorry, resource is forbidden."): + ErrorPage.__init__(self, FORBIDDEN, "Forbidden Resource", message) + + ++# Deliberately undocumented public aliases. See GHSA-vg46-2rrj-3647. ++ErrorPage = _UnsafeErrorPage ++NoResource = _UnsafeNoResource ++ForbiddenResource = _UnsafeForbiddenResource ++ ++deprecatedModuleAttribute( ++ Version("Twisted", "NEXT", 0, 0), ++ "Use twisted.pages.ErrorPage instead, which properly escapes HTML.", ++ __name__, ++ "ErrorPage", ++) ++ ++deprecatedModuleAttribute( ++ Version("Twisted", "NEXT", 0, 0), ++ "Use twisted.pages.NotFoundPage instead, which properly escapes HTML.", ++ __name__, ++ "NoResource", ++) ++ ++deprecatedModuleAttribute( ++ Version("Twisted", "NEXT", 0, 0), ++ "Use twisted.pages.ForbiddenPage instead, which properly escapes HTML.", ++ __name__, ++ "ForbiddenResource", ++) ++ ++ + class _IEncodingResource(Interface): + """ + A resource which knows about L{_IRequestEncoderFactory}. +diff --git a/src/twisted/web/test/test_resource.py b/src/twisted/web/test/test_resource.py +index bd2f908..3e83d0e 100644 +--- a/src/twisted/web/test/test_resource.py ++++ b/src/twisted/web/test/test_resource.py +@@ -11,10 +11,10 @@ from twisted.web.http_headers import Headers + from twisted.web.resource import ( + FORBIDDEN, + NOT_FOUND, +- ErrorPage, +- ForbiddenResource, +- NoResource, + Resource, ++ _UnsafeErrorPage as ErrorPage, ++ _UnsafeForbiddenResource as ForbiddenResource, ++ _UnsafeNoResource as NoResource, + getChildForRequest, + ) + from twisted.web.test.requesthelper import DummyRequest +@@ -22,13 +22,56 @@ from twisted.web.test.requesthelper import DummyRequest + + class ErrorPageTests(TestCase): + """ +- Tests for L{ErrorPage}, L{NoResource}, and L{ForbiddenResource}. ++ Tests for L{_UnafeErrorPage}, L{_UnsafeNoResource}, and ++ L{_UnsafeForbiddenResource}. + """ + + errorPage = ErrorPage + noResource = NoResource + forbiddenResource = ForbiddenResource + ++ def test_deprecatedErrorPage(self): ++ """ ++ The public C{twisted.web.resource.ErrorPage} alias for the ++ corresponding C{_Unsafe} class produces a deprecation warning when ++ imported. ++ """ ++ from twisted.web.resource import ErrorPage ++ ++ self.assertIs(ErrorPage, self.errorPage) ++ ++ [warning] = self.flushWarnings() ++ self.assertEqual(warning["category"], DeprecationWarning) ++ self.assertIn("twisted.pages.ErrorPage", warning["message"]) ++ ++ def test_deprecatedNoResource(self): ++ """ ++ The public C{twisted.web.resource.NoResource} alias for the ++ corresponding C{_Unsafe} class produces a deprecation warning when ++ imported. ++ """ ++ from twisted.web.resource import NoResource ++ ++ self.assertIs(NoResource, self.noResource) ++ ++ [warning] = self.flushWarnings() ++ self.assertEqual(warning["category"], DeprecationWarning) ++ self.assertIn("twisted.pages.NotFoundPage", warning["message"]) ++ ++ def test_deprecatedForbiddenResource(self): ++ """ ++ The public C{twisted.web.resource.ForbiddenResource} alias for the ++ corresponding C{_Unsafe} class produce a deprecation warning when ++ imported. ++ """ ++ from twisted.web.resource import ForbiddenResource ++ ++ self.assertIs(ForbiddenResource, self.forbiddenResource) ++ ++ [warning] = self.flushWarnings() ++ self.assertEqual(warning["category"], DeprecationWarning) ++ self.assertIn("twisted.pages.ForbiddenPage", warning["message"]) ++ + def test_getChild(self): + """ + The C{getChild} method of L{ErrorPage} returns the L{ErrorPage} it is diff -Nru twisted-22.4.0/debian/patches/Failing-test.patch twisted-22.4.0/debian/patches/Failing-test.patch --- twisted-22.4.0/debian/patches/Failing-test.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-22.4.0/debian/patches/Failing-test.patch 2023-01-30 15:10:52.000000000 +0000 @@ -0,0 +1,59 @@ +From: Tom Most +Date: Sat, 22 Oct 2022 19:45:24 -0700 +Subject: Failing test + +--- + src/twisted/web/test/test_vhost.py | 19 ++++++++++++++++--- + 1 file changed, 16 insertions(+), 3 deletions(-) + +diff --git a/src/twisted/web/test/test_vhost.py b/src/twisted/web/test/test_vhost.py +index f26d5e5..bb66b55 100644 +--- a/src/twisted/web/test/test_vhost.py ++++ b/src/twisted/web/test/test_vhost.py +@@ -66,7 +66,7 @@ class NameVirtualHostTests(TestCase): + """ + virtualHostResource = NameVirtualHost() + virtualHostResource.default = Data(b"correct result", "") +- request = DummyRequest([""]) ++ request = DummyRequest([b""]) + self.assertEqual(virtualHostResource.render(request), b"correct result") + + def test_renderWithoutHostNoDefault(self): +@@ -76,7 +76,7 @@ class NameVirtualHostTests(TestCase): + header in the request. + """ + virtualHostResource = NameVirtualHost() +- request = DummyRequest([""]) ++ request = DummyRequest([b""]) + d = _render(virtualHostResource, request) + + def cbRendered(ignored): +@@ -140,7 +140,7 @@ class NameVirtualHostTests(TestCase): + matching the value of the I{Host} header in the request. + """ + virtualHostResource = NameVirtualHost() +- request = DummyRequest([""]) ++ request = DummyRequest([b""]) + request.requestHeaders.addRawHeader(b"host", b"example.com") + d = _render(virtualHostResource, request) + +@@ -150,6 +150,19 @@ class NameVirtualHostTests(TestCase): + d.addCallback(cbRendered) + return d + ++ async def test_renderWithHTMLHost(self): ++ """ ++ L{NameVirtualHost.render} doesn't echo unescaped HTML when present in ++ the I{Host} header. ++ """ ++ virtualHostResource = NameVirtualHost() ++ request = DummyRequest([b""]) ++ request.requestHeaders.addRawHeader(b"host", b"example.com") ++ ++ await _render(virtualHostResource, request) ++ ++ self.assertNotIn(b"", b"".join(request.written)) ++ + def test_getChild(self): + """ + L{NameVirtualHost.getChild} returns correct I{Resource} based off diff -Nru twisted-22.4.0/debian/patches/Fix-NameVirtualHost-HTML-injection-vulnerability.patch twisted-22.4.0/debian/patches/Fix-NameVirtualHost-HTML-injection-vulnerability.patch --- twisted-22.4.0/debian/patches/Fix-NameVirtualHost-HTML-injection-vulnerability.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-22.4.0/debian/patches/Fix-NameVirtualHost-HTML-injection-vulnerability.patch 2023-01-30 15:10:52.000000000 +0000 @@ -0,0 +1,48 @@ +From: Tom Most +Date: Sat, 22 Oct 2022 20:01:50 -0700 +Subject: Fix NameVirtualHost HTML injection vulnerability + +--- + src/twisted/web/newsfragments/11716.bugfix | 1 + + src/twisted/web/vhost.py | 11 ++++++----- + 2 files changed, 7 insertions(+), 5 deletions(-) + create mode 100644 src/twisted/web/newsfragments/11716.bugfix + +diff --git a/src/twisted/web/newsfragments/11716.bugfix b/src/twisted/web/newsfragments/11716.bugfix +new file mode 100644 +index 0000000..66189e6 +--- /dev/null ++++ b/src/twisted/web/newsfragments/11716.bugfix +@@ -0,0 +1 @@ ++twisted.web.vhost.NameVirtualHost no longer echoes HTML received in the Host header without escaping it (GHSA-vg46-2rrj-3647). +diff --git a/src/twisted/web/vhost.py b/src/twisted/web/vhost.py +index 2c305f9..9576252 100644 +--- a/src/twisted/web/vhost.py ++++ b/src/twisted/web/vhost.py +@@ -9,7 +9,7 @@ I am a virtual hosts implementation. + + # Twisted Imports + from twisted.python import roots +-from twisted.web import resource ++from twisted.web import pages, resource + + + class VirtualHostCollection(roots.Homogenous): +@@ -77,12 +77,13 @@ class NameVirtualHost(resource.Resource): + def _getResourceForRequest(self, request): + """(Internal) Get the appropriate resource for the given host.""" + hostHeader = request.getHeader(b"host") +- if hostHeader == None: +- return self.default or resource.NoResource() ++ if hostHeader is None: ++ return self.default or pages.notFound() + else: + host = hostHeader.lower().split(b":", 1)[0] +- return self.hosts.get(host, self.default) or resource.NoResource( +- "host %s not in vhost map" % repr(host) ++ return self.hosts.get(host, self.default) or pages.notFound( ++ "Not Found", ++ f"host {host.decode('ascii', 'replace')!r} not in vhost map", + ) + + def render(self, request): diff -Nru twisted-22.4.0/debian/patches/Fix-references-to-twisted.pages.patch twisted-22.4.0/debian/patches/Fix-references-to-twisted.pages.patch --- twisted-22.4.0/debian/patches/Fix-references-to-twisted.pages.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-22.4.0/debian/patches/Fix-references-to-twisted.pages.patch 2023-01-30 15:10:52.000000000 +0000 @@ -0,0 +1,96 @@ +From: Tom Most +Date: Mon, 24 Oct 2022 21:14:07 -0700 +Subject: Fix references to twisted.pages + +--- + src/twisted/web/resource.py | 12 ++++++------ + src/twisted/web/test/test_resource.py | 6 +++--- + 2 files changed, 9 insertions(+), 9 deletions(-) + +diff --git a/src/twisted/web/resource.py b/src/twisted/web/resource.py +index 09fc74a..f196c3a 100644 +--- a/src/twisted/web/resource.py ++++ b/src/twisted/web/resource.py +@@ -299,7 +299,7 @@ class _UnsafeErrorPage(Resource): + descriptive text. This is useful for rendering simple error pages. + + Deprecated in Twisted NEXT because it permits HTML injection; use +- L{twisted.pages.ErrorPage} instead. ++ L{twisted.web.pages.ErrorPage} instead. + + @ivar template: A native string which will have a dictionary interpolated + into it to generate the response body. The dictionary has the following +@@ -359,7 +359,7 @@ class _UnsafeNoResource(_UnsafeErrorPage): + returns the HTTP response code I{NOT FOUND}. + + Deprecated in Twisted NEXT because it permits HTML injection; use +- L{twisted.pages.notFound} instead. ++ L{twisted.web.pages.notFound} instead. + """ + + def __init__(self, message="Sorry. No luck finding that resource."): +@@ -373,7 +373,7 @@ class _UnsafeForbiddenResource(_UnsafeErrorPage): + returns the I{FORBIDDEN} HTTP response code. + + Deprecated in Twisted NEXT because it permits HTML injection; use +- L{twisted.pages.forbidden} instead. ++ L{twisted.web.pages.forbidden} instead. + """ + + def __init__(self, message="Sorry, resource is forbidden."): +@@ -387,21 +387,21 @@ ForbiddenResource = _UnsafeForbiddenResource + + deprecatedModuleAttribute( + Version("Twisted", "NEXT", 0, 0), +- "Use twisted.pages.ErrorPage instead, which properly escapes HTML.", ++ "Use twisted.web.pages.ErrorPage instead, which properly escapes HTML.", + __name__, + "ErrorPage", + ) + + deprecatedModuleAttribute( + Version("Twisted", "NEXT", 0, 0), +- "Use twisted.pages.notFound instead, which properly escapes HTML.", ++ "Use twisted.web.pages.notFound instead, which properly escapes HTML.", + __name__, + "NoResource", + ) + + deprecatedModuleAttribute( + Version("Twisted", "NEXT", 0, 0), +- "Use twisted.pages.forbidden instead, which properly escapes HTML.", ++ "Use twisted.web.pages.forbidden instead, which properly escapes HTML.", + __name__, + "ForbiddenResource", + ) +diff --git a/src/twisted/web/test/test_resource.py b/src/twisted/web/test/test_resource.py +index c039704..cb37942 100644 +--- a/src/twisted/web/test/test_resource.py ++++ b/src/twisted/web/test/test_resource.py +@@ -42,7 +42,7 @@ class ErrorPageTests(TestCase): + + [warning] = self.flushWarnings() + self.assertEqual(warning["category"], DeprecationWarning) +- self.assertIn("twisted.pages.ErrorPage", warning["message"]) ++ self.assertIn("twisted.web.pages.ErrorPage", warning["message"]) + + def test_deprecatedNoResource(self): + """ +@@ -56,7 +56,7 @@ class ErrorPageTests(TestCase): + + [warning] = self.flushWarnings() + self.assertEqual(warning["category"], DeprecationWarning) +- self.assertIn("twisted.pages.notFound", warning["message"]) ++ self.assertIn("twisted.web.pages.notFound", warning["message"]) + + def test_deprecatedForbiddenResource(self): + """ +@@ -70,7 +70,7 @@ class ErrorPageTests(TestCase): + + [warning] = self.flushWarnings() + self.assertEqual(warning["category"], DeprecationWarning) +- self.assertIn("twisted.pages.forbidden", warning["message"]) ++ self.assertIn("twisted.web.pages.forbidden", warning["message"]) + + def test_getChild(self): + """ diff -Nru twisted-22.4.0/debian/patches/Implement-twisted.python.failure._Code.co_positions.patch twisted-22.4.0/debian/patches/Implement-twisted.python.failure._Code.co_positions.patch --- twisted-22.4.0/debian/patches/Implement-twisted.python.failure._Code.co_positions.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-22.4.0/debian/patches/Implement-twisted.python.failure._Code.co_positions.patch 2023-01-30 15:10:52.000000000 +0000 @@ -0,0 +1,32 @@ +From: Colin Watson +Date: Sat, 23 Apr 2022 22:29:07 +0100 +Subject: Implement twisted.python.failure._Code.co_positions + +This is needed for compatibility with Python 3.11. +--- + src/twisted/python/failure.py | 5 ++++- + 1 file changed, 4 insertions(+), 1 deletion(-) + +diff --git a/src/twisted/python/failure.py b/src/twisted/python/failure.py +index 6471e7b..c5a359e 100644 +--- a/src/twisted/python/failure.py ++++ b/src/twisted/python/failure.py +@@ -130,7 +130,7 @@ def _Traceback(stackFrames, tbFrames): + + + # The set of attributes for _TracebackFrame, _Frame and _Code were taken from +-# https://docs.python.org/3.10/library/inspect.html Other Pythons may have a ++# https://docs.python.org/3.11/library/inspect.html Other Pythons may have a + # few more attributes that should be added if needed. + class _TracebackFrame: + """ +@@ -202,6 +202,9 @@ class _Code: + self.co_nlocals = 0 + self.co_stacksize = 0 + ++ def co_positions(self): ++ return ((None, None, None, None),) ++ + + _inlineCallbacksExtraneous = [] + diff -Nru twisted-22.4.0/debian/patches/Implement-twisted.web.pages.patch twisted-22.4.0/debian/patches/Implement-twisted.web.pages.patch --- twisted-22.4.0/debian/patches/Implement-twisted.web.pages.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-22.4.0/debian/patches/Implement-twisted.web.pages.patch 2023-01-30 15:10:52.000000000 +0000 @@ -0,0 +1,335 @@ +From: Tom Most +Date: Thu, 20 Oct 2022 23:38:19 -0700 +Subject: Implement twisted.web.pages + +--- + src/twisted/web/_template_util.py | 6 +- + src/twisted/web/newsfragments/11716.feature | 2 +- + src/twisted/web/pages.py | 108 ++++++++++++++++++++++++++++ + src/twisted/web/resource.py | 10 +-- + src/twisted/web/test/test_pages.py | 106 +++++++++++++++++++++++++++ + src/twisted/web/test/test_resource.py | 4 +- + 6 files changed, 225 insertions(+), 11 deletions(-) + create mode 100644 src/twisted/web/pages.py + create mode 100644 src/twisted/web/test/test_pages.py + +diff --git a/src/twisted/web/_template_util.py b/src/twisted/web/_template_util.py +index bd081bd..38ebbed 100644 +--- a/src/twisted/web/_template_util.py ++++ b/src/twisted/web/_template_util.py +@@ -1034,9 +1034,9 @@ class _TagFactory: + """ + A factory for L{Tag} objects; the implementation of the L{tags} object. + +- This allows for the syntactic convenience of C{from twisted.web.html import +- tags; tags.a(href="linked-page.html")}, where 'a' can be basically any HTML +- tag. ++ This allows for the syntactic convenience of C{from twisted.web.template ++ import tags; tags.a(href="linked-page.html")}, where 'a' can be basically ++ any HTML tag. + + The class is not exposed publicly because you only ever need one of these, + and we already made it for you. +diff --git a/src/twisted/web/newsfragments/11716.feature b/src/twisted/web/newsfragments/11716.feature +index 5693458..e8ba00b 100644 +--- a/src/twisted/web/newsfragments/11716.feature ++++ b/src/twisted/web/newsfragments/11716.feature +@@ -1 +1 @@ +-The twisted.web.pages.ErrorPage, NotFoundPage, and ForbiddenPage IResource implementations provide HTML error pages rendered safely using twisted.web.template. ++The twisted.web.pages.ErrorPage, notFound, and forbidden IResource implementations provide HTML error pages safely rendered using twisted.web.template. +diff --git a/src/twisted/web/pages.py b/src/twisted/web/pages.py +new file mode 100644 +index 0000000..8f37b3e +--- /dev/null ++++ b/src/twisted/web/pages.py +@@ -0,0 +1,108 @@ ++# -*- test-case-name: twisted.web.test.test_pages -*- ++# Copyright (c) Twisted Matrix Laboratories. ++# See LICENSE for details. ++ ++""" ++Utility implementations of L{IResource}. ++""" ++ ++__all__ = ( ++ "ErrorPage", ++ "notFound", ++ "forbidden", ++) ++ ++ ++from twisted.web import http ++from twisted.web.iweb import IRequest ++from twisted.web.resource import Resource ++from twisted.web.template import renderElement, tags ++ ++ ++class ErrorPage(Resource): ++ """ ++ L{ErrorPage} is a resource that responds to all requests with a particular ++ (parameterized) HTTP status code and a body consisting of HTML containing ++ some descriptive text. This is useful for rendering simple error pages. ++ ++ @ivar _code: An integer HTTP status code which will be used for the ++ response. ++ ++ @ivar _brief: A short string which will be included in the response body as ++ the page title. ++ ++ @ivar _detail: A longer string which will be included in the response body. ++ """ ++ ++ def __init__(self, code: int, brief: str, detail: str) -> None: ++ """ ++ @param code: An integer HTTP status code which will be used for the ++ response. ++ ++ @param brief: A short string which will be included in the response ++ body as the page title. ++ ++ @param detail: A longer string which will be included in the ++ response body. ++ """ ++ super().__init__() ++ self._code: int = code ++ self._brief: str = brief ++ self._detail: str = detail ++ ++ def render(self, request: IRequest) -> None: ++ """ ++ Respond to all requests with the given HTTP status code and an HTML ++ document containing the explanatory strings. ++ """ ++ request.setResponseCode(self._code) ++ request.setHeader(b"content-type", b"text/html; charset=utf-8") ++ renderElement( ++ request, ++ tags.html( ++ tags.head(tags.title(f"{self._code} - {self._brief}")), ++ tags.body(tags.h1(self._brief), tags.p(self._detail)), ++ ), ++ ) ++ ++ def getChild(self, path: bytes, request: IRequest) -> Resource: ++ """ ++ Handle all requests for which L{ErrorPage} lacks a child by returning ++ this error page. ++ ++ @param path: A path segment. ++ ++ @param request: HTTP request ++ """ ++ return self ++ ++ ++def notFound( ++ brief: str = "No Such Resource", ++ message: str = "Sorry. No luck finding that resource.", ++) -> ErrorPage: ++ """ ++ Generate an L{ErrorPage} with a 404 Not Found status code. ++ ++ @param brief: A short string displayed as the page title. ++ ++ @param brief: A longer string displayed in the page body. ++ ++ @returns: An L{ErrorPage} ++ """ ++ return ErrorPage(http.NOT_FOUND, brief, message) ++ ++ ++def forbidden( ++ brief: str = "Forbidden Resource", message: str = "Sorry, resource is forbidden." ++) -> ErrorPage: ++ """ ++ Generate an L{ErrorPage} with a 403 Forbidden status code. ++ ++ @param brief: A short string displayed as the page title. ++ ++ @param brief: A longer string displayed in the page body. ++ ++ @returns: An L{ErrorPage} ++ """ ++ return ErrorPage(http.FORBIDDEN, brief, message) +diff --git a/src/twisted/web/resource.py b/src/twisted/web/resource.py +index 93c7807..09fc74a 100644 +--- a/src/twisted/web/resource.py ++++ b/src/twisted/web/resource.py +@@ -183,7 +183,7 @@ class Resource: + Parameters and return value have the same meaning and requirements as + those defined by L{IResource.getChildWithDefault}. + """ +- return NoResource("No such child resource.") ++ return _UnsafeNoResource() + + def getChildWithDefault(self, path, request): + """ +@@ -359,7 +359,7 @@ class _UnsafeNoResource(_UnsafeErrorPage): + returns the HTTP response code I{NOT FOUND}. + + Deprecated in Twisted NEXT because it permits HTML injection; use +- L{twisted.pages.NotFoundPage} instead. ++ L{twisted.pages.notFound} instead. + """ + + def __init__(self, message="Sorry. No luck finding that resource."): +@@ -373,7 +373,7 @@ class _UnsafeForbiddenResource(_UnsafeErrorPage): + returns the I{FORBIDDEN} HTTP response code. + + Deprecated in Twisted NEXT because it permits HTML injection; use +- L{twisted.pages.ForbiddenPage} instead. ++ L{twisted.pages.forbidden} instead. + """ + + def __init__(self, message="Sorry, resource is forbidden."): +@@ -394,14 +394,14 @@ deprecatedModuleAttribute( + + deprecatedModuleAttribute( + Version("Twisted", "NEXT", 0, 0), +- "Use twisted.pages.NotFoundPage instead, which properly escapes HTML.", ++ "Use twisted.pages.notFound instead, which properly escapes HTML.", + __name__, + "NoResource", + ) + + deprecatedModuleAttribute( + Version("Twisted", "NEXT", 0, 0), +- "Use twisted.pages.ForbiddenPage instead, which properly escapes HTML.", ++ "Use twisted.pages.forbidden instead, which properly escapes HTML.", + __name__, + "ForbiddenResource", + ) +diff --git a/src/twisted/web/test/test_pages.py b/src/twisted/web/test/test_pages.py +new file mode 100644 +index 0000000..d83e0eb +--- /dev/null ++++ b/src/twisted/web/test/test_pages.py +@@ -0,0 +1,106 @@ ++# Copyright (c) Twisted Matrix Laboratories. ++# See LICENSE for details. ++ ++""" ++Test L{twisted.web.pages} ++""" ++ ++from twisted.trial.unittest import SynchronousTestCase ++from twisted.web.http_headers import Headers ++from twisted.web.pages import ErrorPage, forbidden, notFound ++from twisted.web.test.requesthelper import DummyRequest ++ ++ ++def _render(resource: ErrorPage) -> DummyRequest: ++ """ ++ Render a response using the given resource. ++ ++ @param resource: The resource to use to handle the request. ++ ++ @returns: The request that the resource handled, ++ """ ++ request = DummyRequest([b""]) ++ resource.render(request) ++ return request ++ ++ ++class ErrorPageTests(SynchronousTestCase): ++ """ ++ Test L{twisted.web.pages.ErrorPage} and its convencience helpers ++ L{notFound} and L{forbidden}. ++ """ ++ ++ maxDiff = None ++ ++ def assertResponse(self, request: DummyRequest, code: int, body: bytes) -> None: ++ self.assertEqual(request.responseCode, code) ++ self.assertEqual( ++ request.responseHeaders, ++ Headers({b"content-type": [b"text/html; charset=utf-8"]}), ++ ) ++ self.assertEqual( ++ # Decode to str because unittest somehow still doesn't diff bytes ++ # without truncating them in 2022. ++ b"".join(request.written).decode("latin-1"), ++ body.decode("latin-1"), ++ ) ++ ++ def test_escapesHTML(self): ++ """ ++ The I{brief} and I{detail} parameters are HTML-escaped on render. ++ """ ++ self.assertResponse( ++ _render(ErrorPage(400, "A & B", "