ndex: Twisted-19.10.0/twisted/web/newsfragments/11716.feature
===================================================================
Index: Twisted-15.2.1/twisted/web/newsfragments/11716.feature
===================================================================
--- /dev/null
+++ Twisted-15.2.1/twisted/web/newsfragments/11716.feature
@@ -0,0 +1 @@
+The twisted.web.pages.errorPage, notFound, and forbidden each return an IResource that displays an HTML error pages safely rendered using twisted.web.template.
Index: Twisted-15.2.1/twisted/web/newsfragments/11716.removal
===================================================================
--- /dev/null
+++ Twisted-15.2.1/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.
Index: Twisted-15.2.1/twisted/web/resource.py
===================================================================
--- Twisted-15.2.1.orig/twisted/web/resource.py
+++ Twisted-15.2.1/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.
 """
 
 from __future__ import division, absolute_import
@@ -20,6 +22,8 @@ from zope.interface import Attribute, In
 from twisted.python.compat import nativeString, unicode
 from twisted.python.reflect import prefixedMethodNames
 from twisted.python.components import proxyForInterface
+from twisted.python.deprecate import deprecatedModuleAttribute
+from twisted.python.versions import Version
 
 from twisted.web._responses import FORBIDDEN, NOT_FOUND
 from twisted.web.error import UnsupportedMethod
@@ -101,7 +105,7 @@ def getChildForRequest(resource, request
 
 
 @implementer(IResource)
-class Resource:
+class Resource(object):
     """
     Define a web-accessible resource.
 
@@ -179,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):
@@ -277,20 +281,25 @@ def _computeAllowedMethods(resource):
 
 
 
-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.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
         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}
@@ -335,24 +344,57 @@ class ErrorPage(Resource):
 
 
 
-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.web.pages.notFound} instead.
     """
     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 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.web.pages.forbidden} instead.
     """
     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.
+ErrorPage = _UnsafeErrorPage
+NoResource = _UnsafeNoResource
+ForbiddenResource = _UnsafeForbiddenResource
+
+deprecatedModuleAttribute(
+    Version("Twisted", 20, 10, 0),
+    "Use twisted.web.pages.errorPage instead, which properly escapes HTML.",
+    __name__,
+    "ErrorPage",
+)
+
+deprecatedModuleAttribute(
+    Version("Twisted", 20, 10, 0),
+    "Use twisted.web.pages.notFound instead, which properly escapes HTML.",
+    __name__,
+    "NoResource",
+)
+
+deprecatedModuleAttribute(
+    Version("Twisted", 20, 10, 0),
+    "Use twisted.web.pages.forbidden instead, which properly escapes HTML.",
+    __name__,
+    "ForbiddenResource",
+)
 
 
 class _IEncodingResource(Interface):
Index: Twisted-15.2.1/twisted/web/test/test_resource.py
===================================================================
--- Twisted-15.2.1.orig/twisted/web/test/test_resource.py
+++ Twisted-15.2.1/twisted/web/test/test_resource.py
@@ -9,20 +9,66 @@ from twisted.trial.unittest import TestC
 
 from twisted.web.error import UnsupportedMethod
 from twisted.web.resource import (
-    NOT_FOUND, FORBIDDEN, Resource, ErrorPage, NoResource, ForbiddenResource,
+    NOT_FOUND, FORBIDDEN, Resource,
+    _UnsafeErrorPage as ErrorPage,
+    _UnsafeForbiddenResource as ForbiddenResource,
+    _UnsafeNoResource as NoResource,
     getChildForRequest)
 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.web.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.web.pages.notFound", 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.web.pages.forbidden", warning["message"])
+
     def test_getChild(self):
         """
         The C{getChild} method of L{ErrorPage} returns the L{ErrorPage} it is
Index: Twisted-15.2.1/twisted/web/pages.py
===================================================================
--- /dev/null
+++ Twisted-15.2.1/twisted/web/pages.py
@@ -0,0 +1,126 @@
+# -*- 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 IRenderable, IRequest
+from twisted.web.resource import IResource, 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 an HTML body containing some
+    descriptive text. This is useful for rendering simple error pages.
+
+    @see: L{twisted.web.pages.errorPage}
+
+    @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, brief, detail):
+        super(_ErrorPage, self).__init__()
+        self._code = code
+        self._brief = brief
+        self._detail = detail
+
+    def render(self, request):
+        """
+        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")
+        return renderElement(
+            request,
+            tags.html(
+                    tags.head(tags.title(str(self._code) + " - " + self._brief)),
+                    tags.body(tags.h1(self._brief), tags.p(self._detail)),
+            )
+        )
+
+    def getChild(self, path, request):
+        """
+        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 errorPage(code, brief, detail):
+    """
+    Build a resource that responds to all requests with a particular HTTP
+    status code and an HTML body containing some descriptive text. This is
+    useful for rendering simple error pages.
+
+    The resource dynamically handles all paths below it. Use
+    L{IResource.putChild()} override specific path.
+
+    @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.
+
+    @returns: An L{IResource}
+    """
+    return _ErrorPage(code, brief, detail)
+
+
+def notFound(
+    brief = "No Such Resource",
+    message = "Sorry. No luck finding that resource.",
+    ):
+    """
+    Generate an L{IResource} with a 404 Not Found status code.
+
+    @see: L{twisted.web.pages.errorPage}
+
+    @param brief: A short string displayed as the page title.
+
+    @param brief: A longer string displayed in the page body.
+
+    @returns: An L{IResource}
+    """
+    return _ErrorPage(http.NOT_FOUND, brief, message)
+
+
+def forbidden(
+    brief = "Forbidden Resource", message = "Sorry, resource is forbidden."
+    ):
+    """
+    Generate an L{IResource} with a 403 Forbidden status code.
+
+    @see: L{twisted.web.pages.errorPage}
+
+    @param brief: A short string displayed as the page title.
+
+    @param brief: A longer string displayed in the page body.
+
+    @returns: An L{IResource}
+    """
+    return _ErrorPage(http.FORBIDDEN, brief, message)
Index: Twisted-15.2.1/twisted/web/test/test_pages.py
===================================================================
--- /dev/null
+++ Twisted-15.2.1/twisted/web/test/test_pages.py
@@ -0,0 +1,108 @@
+# 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.iweb import IRequest
+from twisted.web.pages import errorPage, forbidden, notFound
+from twisted.web.resource import IResource
+from twisted.web.test.requesthelper import DummyRequest
+
+
+def _render(resource):
+    """
+    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([""])
+    resource.render(request)
+    return request
+
+
+class ErrorPageTests(SynchronousTestCase):
+    """
+    Test L{twisted.web.pages._ErrorPage} and its public aliases L{errorPage},
+    L{notFound} and L{forbidden}.
+    """
+
+    maxDiff = None
+
+    def assertResponse(self, request, code, body):
+        self.assertEqual(request.responseCode, code)
+        self.assertEqual(
+            request.responseHeaders,
+            Headers({}),
+        )
+        self.assertEqual(
+            # Decode to str because unittest somehow still doesn't diff bytes
+            # without truncating them in 2022.
+            "".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", "<script>alert('oops!')")),
+            400,
+            (
+                "<!DOCTYPE html>\n"
+                "<html><head><title>400 - A &amp; B</title></head>"
+                "<body><h1>A &amp; B</h1><p>&lt;script&gt;alert('oops!')"
+                "</p></body></html>"
+            ),
+        )
+
+    def test_getChild(self):
+        """
+        The C{getChild} method of the resource returned by L{errorPage} returns
+        the L{_ErrorPage} it is called on.
+        """
+        page = errorPage(404, "foo", "bar")
+        self.assertIs(
+            page.getChild("name", DummyRequest([""])),
+            page,
+        )
+
+    def test_notFoundDefaults(self):
+        """
+        The default arguments to L{twisted.web.pages.notFound} produce
+        a reasonable error page.
+        """
+        self.assertResponse(
+            _render(notFound()),
+            404,
+            (
+                "<!DOCTYPE html>\n"
+                "<html><head><title>404 - No Such Resource</title></head>"
+                "<body><h1>No Such Resource</h1>"
+                "<p>Sorry. No luck finding that resource.</p>"
+                "</body></html>"
+            ),
+        )
+
+    def test_forbiddenDefaults(self):
+        """
+        The default arguments to L{twisted.web.pages.forbidden} produce
+        a reasonable error page.
+        """
+        self.assertResponse(
+            _render(forbidden()),
+            403,
+            (
+                "<!DOCTYPE html>\n"
+                "<html><head><title>403 - Forbidden Resource</title></head>"
+                "<body><h1>Forbidden Resource</h1>"
+                "<p>Sorry, resource is forbidden.</p>"
+                "</body></html>"
+            ),
+        )
Index: Twisted-15.2.1/twisted/web/_auth/wrapper.py
===================================================================
--- Twisted-15.2.1.orig/twisted/web/_auth/wrapper.py
+++ Twisted-15.2.1/twisted/web/_auth/wrapper.py
@@ -17,7 +17,7 @@ from zope.interface import implements
 
 from twisted.python import log
 from twisted.python.components import proxyForInterface
-from twisted.web.resource import IResource, ErrorPage
+from twisted.web.resource import IResource, _UnsafeErrorPage
 from twisted.web import util
 from twisted.cred import error
 from twisted.cred.credentials import Anonymous
@@ -116,7 +116,7 @@ class HTTPAuthSessionWrapper(object):
             return UnauthorizedResource(self._credentialFactories)
         except:
             log.err(None, "Unexpected failure from credentials factory")
-            return ErrorPage(500, None, None)
+            return _UnsafeErrorPage(500, "Internal Error", "")
         else:
             return util.DeferredResource(self._login(credentials))
 
@@ -205,7 +205,7 @@ class HTTPAuthSessionWrapper(object):
                 result,
                 "HTTPAuthSessionWrapper.getChildWithDefault encountered "
                 "unexpected error")
-            return ErrorPage(500, None, None)
+            return _UnsafeErrorPage(500, "Internal Error", "")
 
 
     def _selectParseHeader(self, header):
Index: Twisted-15.2.1/twisted/web/distrib.py
===================================================================
--- Twisted-15.2.1.orig/twisted/web/distrib.py
+++ Twisted-15.2.1/twisted/web/distrib.py
@@ -121,11 +121,12 @@ class Issue:
         #XXX: Argh. FIXME.
         failure = str(failure)
         self.request.write(
-            resource.ErrorPage(http.INTERNAL_SERVER_ERROR,
-                               "Server Connection Lost",
-                               "Connection to distributed server lost:" +
-                               html.PRE(failure)).
-            render(self.request))
+            resource._UnsafeErrorPage(
+                http.INTERNAL_SERVER_ERROR,
+                # GHSA-vg46-2rrj-3647 note: _PRE does HTML-escape the input.
+                "Server Connection Lost",
+                "Connection to distributed server lost:" + html.PRE(failure)
+            ).render(self.request))
         self.request.finish()
         log.msg(failure)
 
@@ -360,7 +361,7 @@ class UserDirectory(resource.Resource):
             pw_name, pw_passwd, pw_uid, pw_gid, pw_gecos, pw_dir, pw_shell \
                      = self._pwd.getpwnam(username)
         except KeyError:
-            return resource.NoResource()
+            return resource._UnsafeNoResource()
         if sub:
             twistdsock = os.path.join(pw_dir, self.userSocketName)
             rs = ResourceSubscription('unix',twistdsock)
@@ -369,5 +370,5 @@ class UserDirectory(resource.Resource):
         else:
             path = os.path.join(pw_dir, self.userDirName)
             if not os.path.exists(path):
-                return resource.NoResource()
+                return resource._UnsafeNoResource()
             return static.File(path)
Index: Twisted-15.2.1/twisted/web/script.py
===================================================================
--- Twisted-15.2.1.orig/twisted/web/script.py
+++ Twisted-15.2.1/twisted/web/script.py
@@ -46,7 +46,7 @@ class CacheScanner:
     def recache(self):
         self.doCache = 1
 
-noRsrc = resource.ErrorPage(500, "Whoops! Internal Error", rpyNoResource)
+noRsrc = resource._UnsafeErrorPage(500, "Whoops! Internal Error", rpyNoResource)
 
 def ResourceScript(path, registry):
     """
@@ -75,7 +75,7 @@ def ResourceTemplate(path, registry):
     from quixote import ptl_compile
 
     glob = {'__file__': _coerceToFilesystemEncoding("", path),
-            'resource': resource.ErrorPage(500, "Whoops! Internal Error",
+            'resource': resource._UnsafeErrorPage(500, "Whoops! Internal Error",
                                            rpyNoResource),
             'registry': registry}
 
@@ -128,10 +128,10 @@ class ResourceScriptDirectory(resource.R
             return ResourceScriptDirectory(fn, self.registry)
         if os.path.exists(fn):
             return ResourceScript(fn, self.registry)
-        return resource.NoResource()
+        return resource._UnsafeNoResource()
 
     def render(self, request):
-        return resource.NoResource().render(request)
+        return resource._UnsafeNoResource().render(request)
 
 
 
Index: Twisted-15.2.1/twisted/web/server.py
===================================================================
--- Twisted-15.2.1.orig/twisted/web/server.py
+++ Twisted-15.2.1/twisted/web/server.py
@@ -274,11 +274,12 @@ class Request(Copyable, http.Request, co
                     'plural': ((len(allowedMethods) > 1) and 's') or '',
                     'allowed': ', '.join(allowedMethods)
                     })
-                epage = resource.ErrorPage(http.NOT_ALLOWED,
-                                           "Method Not Allowed", s)
+                epage = resource._UnsafeErrorPage(
+                    http.NOT_ALLOWED, "Method Not Allowed", s
+                )
                 body = epage.render(self)
             else:
-                epage = resource.ErrorPage(
+                epage = resource._UnsafeErrorPage(
                     http.NOT_IMPLEMENTED, "Huh?",
                     "I don't know how to treat a %s request." %
                     (escape(self.method.decode("charmap")),))
@@ -288,9 +289,10 @@ class Request(Copyable, http.Request, co
         if body == NOT_DONE_YET:
             return
         if not isinstance(body, bytes):
-            body = resource.ErrorPage(
+            body = resource._UnsafeErrorPage(
                 http.INTERNAL_SERVER_ERROR,
                 "Request did not return bytes",
+                # GHSA-vg46-2rrj-3647 note: _PRE does HTML-escape the input.
                 "Request: " + html.PRE(reflect.safe_repr(self)) + "<br />" +
                 "Resource: " + html.PRE(reflect.safe_repr(resrc)) + "<br />" +
                 "Value: " + html.PRE(reflect.safe_repr(body))).render(self)
Index: Twisted-15.2.1/twisted/web/static.py
===================================================================
--- Twisted-15.2.1.orig/twisted/web/static.py
+++ Twisted-15.2.1/twisted/web/static.py
@@ -36,7 +36,7 @@ else:
     from urllib import quote, unquote
     from cgi import escape
 
-dangerousPathError = resource.NoResource("Invalid request URL.")
+dangerousPathError = resource._UnsafeNoResource("Invalid request URL.")
 
 def isDangerous(path):
     return path == b'..' or b'/' in path or networkString(os.sep) in path
@@ -233,8 +233,8 @@ class File(resource.Resource, filepath.F
         """
         self.ignoredExts.append(ext)
 
-    childNotFound = resource.NoResource("File not found.")
-    forbidden = resource.ForbiddenResource()
+    childNotFound = resource._UnsafeNoResource("File not found.")
+    forbidden = resource._UnsafeForbiddenResource()
 
     def directoryListing(self):
         return DirectoryLister(self.path,
Index: Twisted-15.2.1/twisted/web/test/test_vhost.py
===================================================================
--- Twisted-15.2.1.orig/twisted/web/test/test_vhost.py
+++ Twisted-15.2.1/twisted/web/test/test_vhost.py
@@ -103,3 +103,19 @@ class NameVirtualHostTests(TestCase):
             self.assertEqual(request.responseCode, NOT_FOUND)
         d.addCallback(cbRendered)
         return d
+
+
+    def test_renderWithHTMLHost(self):
+        """
+        L{NameVirtualHost.render} doesn't echo unescaped HTML when present in
+        the I{Host} header.
+        """
+        virtualHostResource = NameVirtualHost()
+        request = DummyRequest([''])
+        request.headers['host'] = '<b>example.com</b>'
+        d = _render(virtualHostResource, request)
+        def cbRendered(ignored):
+            self.assertEqual(request.responseCode, NOT_FOUND)
+            self.assertFalse('<b>' in ''.join(request.written))
+        d.addCallback(cbRendered)
+        return d
Index: Twisted-15.2.1/twisted/web/newsfragments/11716.bugfix
===================================================================
--- /dev/null
+++ Twisted-15.2.1/twisted/web/newsfragments/11716.bugfix
@@ -0,0 +1 @@
+twisted.web.vhost.NameVirtualHost no longer echoes HTML received in the Host header without escaping it (CVE-2022-39348, GHSA-vg46-2rrj-3647).
Index: Twisted-15.2.1/twisted/web/vhost.py
===================================================================
--- Twisted-15.2.1.orig/twisted/web/vhost.py
+++ Twisted-15.2.1/twisted/web/vhost.py
@@ -8,7 +8,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):
@@ -76,12 +76,12 @@ class NameVirtualHost(resource.Resource)
         """(Internal) Get the appropriate resource for the given host.
         """
         hostHeader = request.getHeader('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(':', 1)[0]
         return (self.hosts.get(host, self.default)
-                or resource.NoResource("host %s not in vhost map" % repr(host)))
+                or pages.notFound("Not Found", "host %s not in vhost map" % repr(host)))
 
     def render(self, request):
         """Implementation of resource.Resource's render method.
