File CVE-2022-39348-do-not-echo-host-header.patch of Package python-Twisted.34938
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 & B</title></head>"
+ "<body><h1>A & B</h1><p><script>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.